diff --git a/clapper_src/app.js b/clapper_src/app.js index cb06585d..3310d362 100644 --- a/clapper_src/app.js +++ b/clapper_src/app.js @@ -1,17 +1,12 @@ -const { Gdk, Gio, GLib, GObject, Gtk, GstPlayer } = imports.gi; -const Debug = imports.clapper_src.debug; +const { Gio, GObject, Gtk } = imports.gi; const { HeaderBar } = imports.clapper_src.headerbar; -const { Interface } = imports.clapper_src.interface; -const { Player } = imports.clapper_src.player; +const { Widget } = imports.clapper_src.widget; const Menu = imports.clapper_src.menu; -const { Window } = imports.clapper_src.window; -const APP_NAME = pkg.name.substring( +const APP_NAME = 'Clapper' || pkg.name.substring( pkg.name.lastIndexOf('.') + 1 ); -let { debug } = Debug; - var App = GObject.registerClass( class ClapperApp extends Gtk.Application { @@ -25,41 +20,24 @@ class ClapperApp extends Gtk.Application playlist: [], }; Object.assign(this, defaults, opts); - - this.cssProvider = new Gtk.CssProvider(); - this.cssProvider.load_from_path( - `${pkg.datadir}/${pkg.name}/css/styles.css` - ); - - this.window = null; - this.interface = null; - this.player = null; - this.dragAllowed = false; - - this.posX = 0; - this.posY = 0; } vfunc_startup() { super.vfunc_startup(); - this.window = new Window(this, APP_NAME); - this.windowRealizeSignal = this.window.connect( - 'realize', this._onWindowRealize.bind(this) - ); - this.window.connect( - 'fullscreen-changed', this._onWindowFullscreenChanged.bind(this) - ); - this.window.connect( - 'close-request', this._onWindowCloseRequest.bind(this) - ); + let window = new Gtk.ApplicationWindow({ + application: this, + title: APP_NAME, + }); for(let action of Menu.actions) { let simpleAction = new Gio.SimpleAction({ name: action.name }); - simpleAction.connect('activate', () => action(this.active_window, APP_NAME)); + simpleAction.connect('activate', () => + action(this.active_window, APP_NAME) + ); this.add_action(simpleAction); } let uiBuilder = Gtk.Builder.new_from_file( @@ -68,17 +46,11 @@ class ClapperApp extends Gtk.Application let models = { settingsMenu: uiBuilder.get_object('settingsMenu') }; - let headerBar = new HeaderBar(this.window, models); + let headerBar = new HeaderBar(window, models); + window.set_titlebar(headerBar); - this.interface = new Interface(); - this.interface.addHeaderBar(headerBar, APP_NAME); - - this.interface.controls.unfullscreenButton.connect( - 'clicked', () => this.active_window.unfullscreen() - ); - - this.window.set_titlebar(this.interface.headerBar); - this.window.set_child(this.interface); + let clapperWidget = new Widget(); + window.set_child(clapperWidget); } vfunc_activate() @@ -89,310 +61,21 @@ class ClapperApp extends Gtk.Application 'show', this._onWindowShow.bind(this) ); this.active_window.present(); - - Gtk.StyleContext.add_provider_for_display( - Gdk.Display.get_default(), - this.cssProvider, - Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - ); } run(arr) { - arr = arr || []; - super.run(arr); - } - - setHideCursorTimeout() - { - this.clearTimeout('hideCursor'); - this.hideCursorTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { - this.hideCursorTimeout = null; - - if(this.player.motionController.is_pointer) - this.player.widget.set_cursor(this.blankCursor); - - return GLib.SOURCE_REMOVE; - }); - } - - setHideControlsTimeout() - { - this.clearTimeout('hideControls'); - this.hideControlsTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 3, () => { - this.hideControlsTimeout = null; - - if(this.window.isFullscreen && this.player.motionController.is_pointer) { - this.clearTimeout('updateTime'); - this.interface.revealControls(false); - } - - return GLib.SOURCE_REMOVE; - }); - } - - setUpdateTimeInterval() - { - this.clearTimeout('updateTime'); - let nextUpdate = this.interface.updateTime(); - this.updateTimeTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, nextUpdate, () => { - this.updateTimeTimeout = null; - - if(this.window.isFullscreen) - 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'); - } - - _onWindowRealize() - { - this.window.disconnect(this.windowRealizeSignal); - - this.player = new Player(); - - if(!this.player.widget) - return this.quit(); - - this.player.widget.width_request = 960; - this.player.widget.height_request = 540; - - this.interface.addPlayer(this.player); - this.player.connect('state-changed', this._onPlayerStateChanged.bind(this)); - - this.player.clickGesture.connect( - 'pressed', this._onPlayerPressed.bind(this) - ); - this.player.keyController.connect( - 'key-pressed', this._onPlayerKeyPressed.bind(this) - ); - this.player.motionController.connect( - 'enter', this._onPlayerEnter.bind(this) - ); - this.player.motionController.connect( - 'leave', this._onPlayerLeave.bind(this) - ); - this.player.motionController.connect( - 'motion', this._onPlayerMotion.bind(this) - ); - this.player.dragGesture.connect( - 'drag-update', this._onPlayerDragUpdate.bind(this) - ); - - /* Widget signals that are disconnected after first run */ - this._playerRealizeSignal = this.player.widget.connect( - 'realize', this._onPlayerRealize.bind(this) - ); - } - - _onWindowFullscreenChanged(window, isFullscreen) - { - if(isFullscreen) { - this.setUpdateTimeInterval(); - this.setHideControlsTimeout(); - } - else { - this.clearTimeout('updateTime'); - } - - this.interface.setFullscreenMode(isFullscreen); - } - - _onPlayerKeyPressed(self, keyval, keycode, state) - { - let bool = false; - - switch(keyval) { - case Gdk.KEY_space: - case Gdk.KEY_Return: - this.player.toggle_play(); - break; - case Gdk.KEY_Right: - bool = true; - case Gdk.KEY_Left: - // disabled due to missing "seek on drop" support - //this.interface.controls.handleScaleIncrement('position', bool); - break; - case Gdk.KEY_Up: - bool = true; - case Gdk.KEY_Down: - this.interface.controls.handleScaleIncrement('volume', bool); - break; - case Gdk.KEY_F11: - this.window.toggleFullscreen(); - break; - case Gdk.KEY_Escape: - if(this.window.isFullscreen) - this.window.unfullscreen(); - break; - case Gdk.KEY_q: - case Gdk.KEY_Q: - this._onWindowCloseRequest(); - break; - default: - break; - } - } - - _onPlayerRealize() - { - this.player.widget.disconnect(this._playerRealizeSignal); - this.player.renderer.expose(); - - this.defaultCursor = Gdk.Cursor.new_from_name('default', null); - this.blankCursor = Gdk.Cursor.new_from_name('none', null); - - this.setHideCursorTimeout(); + super.run(arr || []); } _onWindowShow(window) { - this.window.disconnect(this.windowShowSignal); + window.disconnect(this.windowShowSignal); this.windowShowSignal = null; - if(this.playlist.length) - this.player.set_playlist(this.playlist); - } - - _onPlayerStateChanged(self, state) - { - if(state === GstPlayer.PlayerState.BUFFERING) - return; - - let isInhibited = false; - let flags = Gtk.ApplicationInhibitFlags.SUSPEND - | Gtk.ApplicationInhibitFlags.IDLE; - - if(state === GstPlayer.PlayerState.PLAYING) { - if(this.inhibitCookie) - return; - - this.inhibitCookie = this.inhibit( - this.window, - flags, - 'video is playing' - ); - if(!this.inhibitCookie) - debug(new Error('could not inhibit session!')); - - isInhibited = (this.inhibitCookie > 0); - } - else { - //if(!this.inhibitCookie) - return; - - /* Uninhibit seems to be broken as of GTK 3.99.2 - this.uninhibit(this.inhibitCookie); - this.inhibitCookie = null; - */ - } - - debug(`set prevent suspend to: ${isInhibited}`); - } - - _onPlayerPressed(gesture, nPress, x, y) - { - let button = gesture.get_current_button(); - let isDouble = (nPress % 2 == 0); - this.dragAllowed = !isDouble; - - switch(button) { - case Gdk.BUTTON_PRIMARY: - if(isDouble) - this.window.toggleFullscreen(); - break; - case Gdk.BUTTON_SECONDARY: - this.player.toggle_play(); - break; - default: - break; - } - } - - _onPlayerEnter(controller, x, y) - { - this.setHideCursorTimeout(); - if(this.window.isFullscreen) - this.setHideControlsTimeout(); - } - - _onPlayerLeave(controller) - { - this.clearTimeout('hideCursor'); - this.clearTimeout('hideControls'); - } - - _onPlayerMotion(controller, posX, posY) - { - /* 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 - ) { - this.player.widget.set_cursor(this.defaultCursor); - this.setHideCursorTimeout(); - - if(this.window.isFullscreen) { - if(!this.interface.revealerTop.get_reveal_child()) { - this.setUpdateTimeInterval(); - this.interface.revealControls(true); - } - this.setHideControlsTimeout(); - } - else if(this.hideControlsTimeout) { - this.clearTimeout('hideControls'); - } - } - - this.posX = posX; - this.posY = posY; - } - - _onPlayerDragUpdate(gesture, offsetX, offsetY) - { - if(!this.dragAllowed || this.active_window.isFullscreen) - return; - - let { gtk_double_click_distance } = this.player.widget.get_settings(); - - if ( - Math.abs(offsetX) > gtk_double_click_distance - || Math.abs(offsetY) > gtk_double_click_distance - ) { - let [isActive, startX, startY] = gesture.get_start_point(); - if(!isActive) return; - - this.active_window.get_surface().begin_move( - gesture.get_device(), - gesture.get_current_button(), - startX, - startY, - gesture.get_current_event_time() - ); - - gesture.reset(); - } - } - - _onWindowCloseRequest() - { - this.interface.emit('destroy'); - this.quit(); + if(this.playlist.length) { + let { player } = window.get_child(); + player.set_playlist(this.playlist); + } } }); diff --git a/clapper_src/buttons.js b/clapper_src/buttons.js index 0e16a9b5..f64ae251 100644 --- a/clapper_src/buttons.js +++ b/clapper_src/buttons.js @@ -103,17 +103,13 @@ class ClapperPopoverButton extends IconButton orientation: Gtk.Orientation.VERTICAL, }); - this.popover.set_parent(this); this.popover.set_child(this.popoverBox); this.popover.set_offset(0, -this.margin_top); if(this.isFullscreen) this.popover.add_css_class('osd'); - this.changeStateSignal = this.popover.connect('closed', () => - this.unset_state_flags(Gtk.StateFlags.CHECKED) - ); - this.destroySignal = this.connect('destroy', this._onDestroy.bind(this)); + this.popover.connect('closed', this._onClosed.bind(this)); } setFullscreenMode(isFullscreen) @@ -136,16 +132,19 @@ class ClapperPopoverButton extends IconButton vfunc_clicked() { this.set_state_flags(Gtk.StateFlags.CHECKED, false); + + this.popover.set_parent(this); this.popover.popup(); } - _onDestroy() + _onClosed() { - this.disconnect(this.destroySignal); + let root = this.get_root(); + let clapperWidget = root.get_child(); + + clapperWidget.player.widget.grab_focus(); - this.popover.disconnect(this.changeStateSignal); this.popover.unparent(); - this.popoverBox.emit('destroy'); - this.popover.emit('destroy'); + this.unset_state_flags(Gtk.StateFlags.CHECKED); } }); diff --git a/clapper_src/controls.js b/clapper_src/controls.js index 4a9b8a12..20d70f82 100644 --- a/clapper_src/controls.js +++ b/clapper_src/controls.js @@ -1,25 +1,15 @@ -const { GObject, Gdk, Gtk } = imports.gi; +const { GObject, Gtk } = imports.gi; const Buttons = imports.clapper_src.buttons; const Debug = imports.clapper_src.debug; +const Misc = imports.clapper_src.misc; const CONTROLS_MARGIN = 4; const CONTROLS_SPACING = 4; let { debug } = Debug; -var Controls = GObject.registerClass({ - Signals: { - 'position-seeking-changed': { - param_types: [GObject.TYPE_BOOLEAN] - }, - 'track-change-requested': { - param_types: [GObject.TYPE_STRING, GObject.TYPE_INT] - }, - 'visualization-change-requested': { - param_types: [GObject.TYPE_STRING] - }, - } -}, class ClapperControls extends Gtk.Box +var Controls = GObject.registerClass( +class ClapperControls extends Gtk.Box { _init() { @@ -29,8 +19,14 @@ var Controls = GObject.registerClass({ margin_end: CONTROLS_MARGIN, spacing: CONTROLS_SPACING, valign: Gtk.Align.END, + can_focus: false, }); + this.currentVolume = 0; + this.currentPosition = 0; + this.currentDuration = 0; + this.isPositionSeeking = false; + this.durationFormated = '00:00:00'; this.elapsedInitial = '00:00:00/00:00:00'; this.buttonsArr = []; @@ -57,10 +53,11 @@ var Controls = GObject.registerClass({ this.unfullscreenButton = this.addButton( 'view-restore-symbolic', ); + this.unfullscreenButton.connect('clicked', this._onUnfullscreenClicked.bind(this)); this.unfullscreenButton.set_visible(false); this.add_css_class('playercontrols'); - this.destroySignal = this.connect('destroy', this._onDestroy.bind(this)); + this.realizeSignal = this.connect('realize', this._onRealize.bind(this)); } setFullscreenMode(isFullscreen) @@ -69,6 +66,7 @@ var Controls = GObject.registerClass({ button.setFullscreenMode(isFullscreen); this.unfullscreenButton.set_visible(isFullscreen); + this.set_can_focus(isFullscreen); } setLiveMode(isLive, isSeekable) @@ -79,6 +77,16 @@ var Controls = GObject.registerClass({ this.positionScale.visible = isSeekable; } + updateElapsedLabel(value) + { + value = value || 0; + + let elapsed = Misc.getFormatedTime(value) + + '/' + this.durationFormated; + + this.elapsedButton.set_label(elapsed); + } + addButton(buttonIcon) { let button = (buttonIcon instanceof Gtk.Button) @@ -135,7 +143,7 @@ var Controls = GObject.registerClass({ }); checkButton.connect( 'toggled', - this._onCheckButtonToggled.bind(this, checkButton) + this._onCheckButtonToggled.bind(this) ); box.append(checkButton); } @@ -160,23 +168,69 @@ var Controls = GObject.registerClass({ } } - handleScaleIncrement(type, isUp) + _handleTrackChange(checkButton) { - if(type === 'volume' && !this.volumeButton.visible) + let clapperWidget = this.get_ancestor(Gtk.Grid); + + /* Reenabling audio is slow (as expected), + * so it is better to toggle mute instead */ + if(checkButton.type === 'audio') { + if(checkButton.activeId < 0) + return clapperWidget.player.set_mute(true); + + if(clapperWidget.player.get_mute()) + clapperWidget.player.set_mute(false); + + return clapperWidget.player[ + `set_${checkButton.type}_track` + ](checkButton.activeId); + } + + if(checkButton.activeId < 0) { + /* Disabling video leaves last frame frozen, + * so we hide it by making it transparent */ + if(checkButton.type === 'video') + clapperWidget.player.widget.set_opacity(0); + + return clapperWidget.player[ + `set_${checkButton.type}_track_enabled` + ](false); + } + + let setTrack = `set_${checkButton.type}_track`; + + clapperWidget.player[setTrack](checkButton.activeId); + clapperWidget.player[`${setTrack}_enabled`](true); + + if(checkButton.type === 'video' && !clapperWidget.player.widget.opacity) + clapperWidget.player.widget.set_opacity(1); + } + + _handleVisualizationChange(checkButton) + { + let clapperWidget = this.get_ancestor(Gtk.Grid); + let isEnabled = clapperWidget.player.get_visualization_enabled(); + + if(!checkButton.activeId) { + if(isEnabled) { + clapperWidget.player.set_visualization_enabled(false); + debug('disabled visualizations'); + } + return; + } + + let currVis = clapperWidget.player.get_current_visualization(); + + if(currVis === checkButton.activeId) return; - let value = this[`${type}Scale`].get_value(); - let maxValue = this[`${type}Adjustment`].get_upper(); - let increment = this[`${type}Adjustment`].get_page_increment(); + debug(`set visualization: ${checkButton.activeId}`); + clapperWidget.player.set_visualization(checkButton.activeId); - value += (isUp) ? increment : -increment; - value = (value < 0) - ? 0 - : (value > maxValue) - ? maxValue - : value; - - this[`${type}Scale`].set_value(value); + if(!isEnabled) { + clapperWidget.player.set_visualization_enabled(true); + debug('enabled visualizations'); + } } _addTogglePlayButton() @@ -186,6 +240,9 @@ var Controls = GObject.registerClass({ 'media-playback-pause-symbolic' ); this.togglePlayButton.add_css_class('playbackicon'); + this.togglePlayButton.connect( + 'clicked', this._onTogglePlayClicked.bind(this) + ); this.addButton(this.togglePlayButton); } @@ -201,9 +258,10 @@ var Controls = GObject.registerClass({ can_focus: false, visible: false, }); - this.positionScale.add_css_class('positionscale'); - this.positionScale.connect('value-changed', this._onPositionScaleValueChanged.bind(this)); + this.positionScale.connect( + 'value-changed', this._onPositionScaleValueChanged.bind(this) + ); /* GTK4 is missing pressed/released signals for GtkRange/GtkScale. * We cannot add controllers, cause it already has them, so we @@ -229,14 +287,6 @@ var Controls = GObject.registerClass({ this.volumeButton = this.addPopoverButton( 'audio-volume-muted-symbolic' ); - let scrollController = new Gtk.EventControllerScroll(); - scrollController.set_flags( - Gtk.EventControllerScrollFlags.VERTICAL - | Gtk.EventControllerScrollFlags.DISCRETE - ); - scrollController.connect('scroll', this._onScroll.bind(this)); - this.volumeButton.add_controller(scrollController); - this.volumeScale = new Gtk.Scale({ orientation: Gtk.Orientation.VERTICAL, inverted: true, @@ -245,6 +295,9 @@ var Controls = GObject.registerClass({ round_digits: 2, vexpand: true, }); + this.volumeScale.connect( + 'value-changed', this._onVolumeScaleValueChanged.bind(this) + ); this.volumeScale.add_css_class('volumescale'); this.volumeAdjustment = this.volumeScale.get_adjustment(); @@ -264,18 +317,28 @@ var Controls = GObject.registerClass({ this.volumeButton.popoverBox.append(this.volumeScale); } - _getFormatedTime(time) + _onRealize() { - let hours = ('0' + Math.floor(time / 3600)).slice(-2); - time -= hours * 3600; - let minutes = ('0' + Math.floor(time / 60)).slice(-2); - time -= minutes * 60; - let seconds = ('0' + Math.floor(time)).slice(-2); + this.disconnect(this.realizeSignal); + this.realizeSignal = null; - return `${hours}:${minutes}:${seconds}`; + let { player } = this.get_ancestor(Gtk.Grid); + let scrollController = new Gtk.EventControllerScroll(); + scrollController.set_flags( + Gtk.EventControllerScrollFlags.VERTICAL + | Gtk.EventControllerScrollFlags.DISCRETE + ); + scrollController.connect('scroll', player._onScroll.bind(player)); + this.volumeButton.add_controller(scrollController); } - _onCheckButtonToggled(self, checkButton) + _onUnfullscreenClicked(button) + { + let root = button.get_root(); + root.unfullscreen(); + } + + _onCheckButtonToggled(checkButton) { if(!checkButton.get_active()) return; @@ -284,61 +347,73 @@ var Controls = GObject.registerClass({ case 'video': case 'audio': case 'subtitle': - this.emit( - 'track-change-requested', - checkButton.type, - checkButton.activeId - ); + this._handleTrackChange(checkButton); break; case 'visualization': - this.emit( - `${checkButton.type}-change-requested`, - checkButton.activeId - ); + this._handleVisualizationChange(checkButton); break; default: break; } } - _onPositionScaleValueChanged() + _onTogglePlayClicked() { - let elapsed = this._getFormatedTime(this.positionScale.get_value()) - + '/' + this.durationFormated; + /* Parent of controls changes, so get ancestor instead */ + let { player } = this.get_ancestor(Gtk.Grid); + player.toggle_play(); + } - this.elapsedButton.set_label(elapsed); + _onPositionScaleValueChanged(scale) + { + let value = Math.round(scale.get_value()); + this.updateElapsedLabel(value); + + if(this.currentPosition === value || this.isPositionSeeking) + return; + + let { player } = this.get_ancestor(Gtk.Grid); + player.seek_seconds(value); + } + + _onVolumeScaleValueChanged(scale) + { + let volume = Number(scale.get_value().toFixed(2)); + let icon = (volume <= 0) + ? 'muted' + : (volume <= 0.33) + ? 'low' + : (volume <= 0.66) + ? 'medium' + : (volume <= 1) + ? 'high' + : 'overamplified'; + + let iconName = `audio-volume-${icon}-symbolic`; + if(this.volumeButton.icon_name !== iconName) + { + debug(`set volume icon: ${icon}`); + this.volumeButton.set_icon_name(iconName); + } + + if(this.currentVolume === volume) + return; + + let { player } = this.get_ancestor(Gtk.Grid); + player.set_volume(volume); } _onPositionScaleDragging(scale) { let isPositionSeeking = scale.has_css_class('dragging'); - if(this.isPositionSeeking === isPositionSeeking) + if((this.isPositionSeeking = isPositionSeeking)) return; - this.isPositionSeeking = isPositionSeeking; - this.emit('position-seeking-changed', this.isPositionSeeking); - } + let clapperWidget = this.get_ancestor(Gtk.Grid); + if(!clapperWidget) return; - _onScroll(controller, dx, dy) - { - let isVertical = Math.abs(dy) >= Math.abs(dx); - let isIncrease = (isVertical) ? dy < 0 : dx < 0; - let type = (isVertical) ? 'volume' : 'position'; - - this.handleScaleIncrement(type, isIncrease); - - return true; - } - - _onDestroy() - { - this.disconnect(this.destroySignal); - - this.visualizationsButton.emit('destroy'); - this.videoTracksButton.emit('destroy'); - this.audioTracksButton.emit('destroy'); - this.subtitleTracksButton.emit('destroy'); - this.volumeButton.emit('destroy'); + let positionSeconds = Math.round(scale.get_value()); + clapperWidget.player.seek_seconds(positionSeconds); } }); diff --git a/clapper_src/headerbar.js b/clapper_src/headerbar.js index 4202098c..9745c3b4 100644 --- a/clapper_src/headerbar.js +++ b/clapper_src/headerbar.js @@ -1,4 +1,4 @@ -const { GLib, GObject, Gtk, Pango } = imports.gi; +const { GLib, GObject, Gst, Gtk, Pango } = imports.gi; var HeaderBar = GObject.registerClass( class ClapperHeaderBar extends Gtk.HeaderBar @@ -14,7 +14,8 @@ class ClapperHeaderBar extends Gtk.HeaderBar let openMenuButton = new Gtk.MenuButton({ icon_name: 'open-menu-symbolic' }); - openMenuButton.set_menu_model(models.settingsMenu); + let settingsPopover = new HeaderBarPopover(models.settingsMenu); + openMenuButton.set_popover(settingsPopover); this.pack_end(openMenuButton); let fullscreenButton = new Gtk.Button({ @@ -27,9 +28,9 @@ class ClapperHeaderBar extends Gtk.HeaderBar updateHeaderBar(mediaInfo) { let title = mediaInfo.get_title(); - let subtitle = mediaInfo.get_uri() || null; + let subtitle = mediaInfo.get_uri(); - if(subtitle && subtitle.startsWith('file://')) { + if(Gst.Uri.get_protocol(subtitle) === 'file') { subtitle = GLib.path_get_basename( GLib.filename_from_uri(subtitle)[0] ); @@ -84,3 +85,24 @@ class ClapperHeaderBar extends Gtk.HeaderBar return box; } }); + +var HeaderBarPopover = GObject.registerClass( +class ClapperHeaderBarPopover extends Gtk.PopoverMenu +{ + _init(model) + { + super._init({ + menu_model: model, + }); + + this.connect('closed', this._onClosed.bind(this)); + } + + _onClosed() + { + let root = this.get_root(); + let clapperWidget = root.get_child(); + + clapperWidget.player.widget.grab_focus(); + } +}); diff --git a/clapper_src/misc.js b/clapper_src/misc.js new file mode 100644 index 00000000..27faae29 --- /dev/null +++ b/clapper_src/misc.js @@ -0,0 +1,51 @@ +const { GstPlayer, Gtk } = imports.gi; +const Debug = imports.clapper_src.debug; + +let { debug } = Debug; +let inhibitCookie; + +function inhibitForState(state, window) +{ + let isInhibited = false; + let flags = Gtk.ApplicationInhibitFlags.SUSPEND + | Gtk.ApplicationInhibitFlags.IDLE; + + if(state === GstPlayer.PlayerState.PLAYING) { + if(inhibitCookie) + return; + + let app = window.get_application(); + + inhibitCookie = app.inhibit( + window, + flags, + 'video is playing' + ); + if(!inhibitCookie) + debug(new Error('could not inhibit session!')); + + isInhibited = (inhibitCookie > 0); + } + else { + //if(!inhibitCookie) + return; + + /* Uninhibit seems to be broken as of GTK 3.99.2 + this.uninhibit(inhibitCookie); + inhibitCookie = null; + */ + } + + debug(`set prevent suspend to: ${isInhibited}`); +} + +function getFormatedTime(time) +{ + let hours = ('0' + Math.floor(time / 3600)).slice(-2); + time -= hours * 3600; + let minutes = ('0' + Math.floor(time / 60)).slice(-2); + time -= minutes * 60; + let seconds = ('0' + Math.floor(time)).slice(-2); + + return `${hours}:${minutes}:${seconds}`; +} diff --git a/clapper_src/player.js b/clapper_src/player.js index 89b1050c..eb280ee7 100644 --- a/clapper_src/player.js +++ b/clapper_src/player.js @@ -1,4 +1,4 @@ -const { Gio, GObject, Gst, GstPlayer, Gtk } = imports.gi; +const { Gdk, Gio, GLib, GObject, Gst, GstPlayer, Gtk } = imports.gi; const ByteArray = imports.byteArray; const Debug = imports.clapper_src.debug; @@ -44,15 +44,13 @@ class ClapperPlayer extends GstPlayer.Player video_renderer: renderer }); - /* Assign elements to player for later access */ - this.gtkglsink = gtkglsink; - this.glsinkbin = glsinkbin; - this.dispatcher = dispatcher; - this.renderer = renderer; - - this.gstRegistry = Gst.Registry.get(); this.is_local_file = false; - this.seek_done = false; + this.seek_done = true; + this.dragAllowed = false; + + this.posX = 0; + this.posY = 0; + this.keyPressCount = 0; this._playerSignals = []; this._widgetSignals = []; @@ -73,37 +71,54 @@ class ClapperPlayer extends GstPlayer.Player this.set_plugin_rank('vah264dec', 300); this.widget = gtkglsink.widget; + this.widget.vexpand = true; + this.widget.hexpand = true; + this.state = GstPlayer.PlayerState.STOPPED; this.visualization_enabled = false; this.fast_seeking = opts.fast_seeking || false; this._playlist = []; this._trackId = 0; - this.playlist_ext = opts.playlist_ext || 'claps'; - this.keyController = new Gtk.EventControllerKey(); - this.motionController = new Gtk.EventControllerMotion(); - this.scrollController = new Gtk.EventControllerScroll(); - this.clickGesture = new Gtk.GestureClick(); - this.dragGesture = new Gtk.GestureDrag(); + this._hideCursorTimeout = null; + this._hideControlsTimeout = null; + this._updateTimeTimeout = null; - this.scrollController.set_flags( - Gtk.EventControllerScrollFlags.BOTH_AXES - ); - this.clickGesture.set_button(0); + this.cursorInPlayer = false; - this.widget.add_controller(this.keyController); - this.widget.add_controller(this.motionController); - this.widget.add_controller(this.scrollController); - this.widget.add_controller(this.clickGesture); - this.widget.add_controller(this.dragGesture); + let clickGesture = new Gtk.GestureClick(); + clickGesture.set_button(0); + clickGesture.connect('pressed', this._onWidgetPressed.bind(this)); + this.widget.add_controller(clickGesture); - 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)); - this.connect('warning', this._onPlayerWarning.bind(this)); - this.connect('error', this._onPlayerError.bind(this)); - this.connectWidget('destroy', this._onWidgetDestroy.bind(this)); + let dragGesture = new Gtk.GestureDrag(); + dragGesture.connect('drag-update', this._onWidgetDragUpdate.bind(this)); + this.widget.add_controller(dragGesture); + + let 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); + + let scrollController = new Gtk.EventControllerScroll(); + scrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES); + scrollController.connect('scroll', this._onScroll.bind(this)); + this.widget.add_controller(scrollController); + + let 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); + + this.selfConnect('state-changed', this._onStateChanged.bind(this)); + this.selfConnect('uri-loaded', this._onUriLoaded.bind(this)); + this.selfConnect('end-of-stream', this._onStreamEnded.bind(this)); + this.selfConnect('warning', this._onPlayerWarning.bind(this)); + this.selfConnect('error', this._onPlayerError.bind(this)); + + this._realizeSignal = this.widget.connect('realize', this._onWidgetRealize.bind(this)); } set_media(source) @@ -133,7 +148,7 @@ class ClapperPlayer extends GstPlayer.Player return this.set_media(this._playlist[this._trackId]); } - if(file.get_path().endsWith(`.${this.playlist_ext}`)) + if(file.get_path().endsWith('.claps')) return this.load_playlist_file(file); this.is_local_file = true; @@ -200,6 +215,12 @@ class ClapperPlayer extends GstPlayer.Player seek(position) { + if(this.state === GstPlayer.PlayerState.STOPPED) + this.pause(); + + if(position < 0) + position = 0; + this.seek_done = false; debug(`player is seeking to position: ${position}`); @@ -219,6 +240,34 @@ class ClapperPlayer extends GstPlayer.Player this.seek(position * 1000000000); } + set_volume(volume) + { + if(volume < 0) + volume = 0; + else if(volume > 2) + volume = 2; + + super.set_volume(volume); + } + + adjust_position(isIncrease) + { + let { controls } = this.widget.get_ancestor(Gtk.Grid); + + let value = (isIncrease) ? 10 : -10; + let positionSeconds = controls.positionScale.get_value() + value; + controls.positionScale.set_value(positionSeconds); + } + + adjust_volume(isIncrease) + { + let { controls } = this.widget.get_ancestor(Gtk.Grid); + + let value = (isIncrease) ? 0.05 : -0.05; + let volume = controls.volumeScale.get_value() + value; + controls.volumeScale.set_value(volume); + } + toggle_play() { let action = (this.state === GstPlayer.PlayerState.PLAYING) @@ -238,7 +287,8 @@ class ClapperPlayer extends GstPlayer.Player { debug(`changing rank of plugin: ${name}`); - let feature = this.gstRegistry.lookup_feature(name); + let gstRegistry = Gst.Registry.get(); + let feature = gstRegistry.lookup_feature(name); if(!feature) return debug(`plugin unavailable: ${name}`); @@ -248,20 +298,72 @@ class ClapperPlayer extends GstPlayer.Player debug(`changed rank: ${oldRank} -> ${rank} for ${name}`); } - connect(signal, fn) + selfConnect(signal, fn) { - let connection = super.connect(signal, fn); - this._playerSignals.push(connection); - - return connection; + this._playerSignals.push( + super.connect(signal, fn) + ); } - connectWidget(signal, fn) + _setHideCursorTimeout() { - let connection = this.widget.connect(signal, fn); - this._widgetSignals.push(connection); + this._clearTimeout('hideCursor'); + this._hideCursorTimeout = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { + this._hideCursorTimeout = null; - return connection; + if(this.cursorInPlayer) { + let blankCursor = Gdk.Cursor.new_from_name('none', null); + this.widget.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) { + let clapperWidget = this.widget.get_ancestor(Gtk.Grid); + if(clapperWidget.fullscreenMode) { + this._clearTimeout('updateTime'); + clapperWidget.revealControls(false); + } + } + + return GLib.SOURCE_REMOVE; + }); + } + + _setUpdateTimeInterval() + { + this._clearTimeout('updateTime'); + let clapperWidget = this.widget.get_ancestor(Gtk.Grid); + let nextUpdate = clapperWidget.updateTime(); + + 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'); } _onStateChanged(player, state) @@ -272,6 +374,11 @@ class ClapperPlayer extends GstPlayer.Player this.seek_done = true; debug('seeking finished'); } + + let clapperWidget = this.widget.get_ancestor(Gtk.Grid); + if(!clapperWidget) return; + + clapperWidget._onPlayerStateChanged(player, state); } _onStreamEnded(player) @@ -300,12 +407,227 @@ class ClapperPlayer extends GstPlayer.Player debug(error); } - _onWidgetDestroy() + _onWidgetRealize() + { + this.widget.disconnect(this._realizeSignal); + this._realizeSignal = null; + + let root = this.widget.get_root(); + if(!root) return; + + root.connect('close-request', this._onCloseRequest.bind(this)); + } + + _onWidgetKeyPressed(controller, keyval, keycode, state) + { + this.keyPressCount++; + + let bool = false; + let clapperWidget = this.widget.get_ancestor(Gtk.Grid); + + switch(keyval) { + case Gdk.KEY_Up: + bool = true; + case Gdk.KEY_Down: + this.adjust_volume(bool); + break; + case Gdk.KEY_Right: + bool = true; + case Gdk.KEY_Left: + clapperWidget.controls.isPositionSeeking = true; + this._clearTimeout('hideControls'); + if(this.keyPressCount > 1) { + clapperWidget.revealerBottom.set_can_focus(false); + clapperWidget.revealerBottom.revealChild(true); + } + this.adjust_position(bool); + break; + default: + break; + } + } + + _onWidgetKeyReleased(controller, keyval, keycode, state) + { + this.keyPressCount = 0; + + let value; + let clapperWidget = this.widget.get_ancestor(Gtk.Grid); + + switch(keyval) { + case Gdk.KEY_space: + this.toggle_play(); + break; + case Gdk.KEY_Return: + if(clapperWidget.fullscreenMode) { + clapperWidget.revealControls(true); + this._setHideControlsTimeout(); + } + break; + case Gdk.KEY_Right: + case Gdk.KEY_Left: + value = clapperWidget.controls.positionScale.get_value(); + this.seek_seconds(value); + this._setHideControlsTimeout(); + clapperWidget.controls.isPositionSeeking = false; + break; + case Gdk.KEY_F11: + clapperWidget.toggleFullscreen(); + break; + case Gdk.KEY_Escape: + if(clapperWidget.fullscreenMode) { + let root = this.widget.get_root(); + root.unfullscreen(); + } + break; + case Gdk.KEY_q: + case Gdk.KEY_Q: + let root = this.widget.get_root(); + root.emit('close-request'); + root.destroy(); + break; + default: + break; + } + } + + _onWidgetPressed(gesture, nPress, x, y) + { + let button = gesture.get_current_button(); + let isDouble = (nPress % 2 == 0); + this.dragAllowed = !isDouble; + + switch(button) { + case Gdk.BUTTON_PRIMARY: + if(isDouble) { + let 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._setHideCursorTimeout(); + + let clapperWidget = this.widget.get_ancestor(Gtk.Grid); + if(clapperWidget.fullscreenMode) + 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 + ) { + let defaultCursor = Gdk.Cursor.new_from_name('default', null); + this.widget.set_cursor(defaultCursor); + this._setHideCursorTimeout(); + + let clapperWidget = this.widget.get_ancestor(Gtk.Grid); + + 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; + + let clapperWidget = this.widget.get_ancestor(Gtk.Grid); + if(clapperWidget.fullscreenMode) + return; + + let { gtk_double_click_distance } = this.widget.get_settings(); + + if ( + Math.abs(offsetX) > gtk_double_click_distance + || Math.abs(offsetY) > gtk_double_click_distance + ) { + let [isActive, startX, startY] = gesture.get_start_point(); + if(!isActive) return; + + let root = this.widget.get_root(); + if(!root) return; + + root.get_surface().begin_move( + gesture.get_device(), + gesture.get_current_button(), + startX, + startY, + gesture.get_current_event_time() + ); + + gesture.reset(); + } + } + + _onScroll(controller, dx, dy) + { + let isHorizontal = Math.abs(dx) >= Math.abs(dy); + let isIncrease = (isHorizontal) ? dx < 0 : dy < 0; + + if(isHorizontal) + this.adjust_position(isIncrease); + else + this.adjust_volume(isIncrease); + + return true; + } + + _onCloseRequest(window) { while(this._widgetSignals.length) this.widget.disconnect(this._widgetSignals.pop()); while(this._playerSignals.length) this.disconnect(this._playerSignals.pop()); + + if(this.state !== GstPlayer.PlayerState.STOPPED) + this.stop(); } }); diff --git a/clapper_src/revealers.js b/clapper_src/revealers.js index 6af461a6..9cdd4b25 100644 --- a/clapper_src/revealers.js +++ b/clapper_src/revealers.js @@ -1,4 +1,4 @@ -const { Gdk, GLib, GObject, Gtk, Pango } = imports.gi; +const { GLib, GObject, Gtk, Pango } = imports.gi; const Debug = imports.clapper_src.debug; const REVEAL_TIME = 800; @@ -14,6 +14,7 @@ class ClapperCustomRevealer extends Gtk.Revealer let defaults = { visible: false, + can_focus: false, }; Object.assign(opts, defaults); @@ -31,6 +32,9 @@ class ClapperCustomRevealer extends Gtk.Revealer else this._setHideTimeout(); + /* Restore focusability after we are done */ + if(!isReveal) this.set_can_focus(true); + this._timedReveal(isReveal, REVEAL_TIME); } @@ -94,18 +98,6 @@ class ClapperRevealerTop extends CustomRevealer }); this.revealerName = 'top'; -/* - this.set_events( - Gdk.EventMask.BUTTON_PRESS_MASK - | Gdk.EventMask.BUTTON_RELEASE_MASK - | Gdk.EventMask.TOUCH_MASK - | Gdk.EventMask.SCROLL_MASK - | Gdk.EventMask.TOUCHPAD_GESTURE_MASK - | Gdk.EventMask.POINTER_MOTION_MASK - | Gdk.EventMask.ENTER_NOTIFY_MASK - | Gdk.EventMask.LEAVE_NOTIFY_MASK - ); -*/ let initTime = GLib.DateTime.new_now_local().format('%X'); this.timeFormat = (initTime.length > 8) ? '%I:%M %p' @@ -198,7 +190,7 @@ class ClapperRevealerBottom extends CustomRevealer set_visible(isVisible) { let isChange = super.set_visible(isVisible); - if(!isChange) return; + if(!isChange || !this.can_focus) return; let parent = this.get_parent(); let playerWidget = parent.get_first_child(); diff --git a/clapper_src/interface.js b/clapper_src/widget.js similarity index 56% rename from clapper_src/interface.js rename to clapper_src/widget.js index 72b35ee1..af3a74b6 100644 --- a/clapper_src/interface.js +++ b/clapper_src/widget.js @@ -1,12 +1,14 @@ -const { Gdk, GLib, GObject, Gtk, Gst, GstPlayer, Pango } = imports.gi; +const { Gdk, GLib, GObject, Gtk, GstPlayer } = imports.gi; const { Controls } = imports.clapper_src.controls; const Debug = imports.clapper_src.debug; +const Misc = imports.clapper_src.misc; +const { Player } = imports.clapper_src.player; const Revealers = imports.clapper_src.revealers; let { debug } = Debug; -var Interface = GObject.registerClass( -class ClapperInterface extends Gtk.Grid +var Widget = GObject.registerClass( +class ClapperWidget extends Gtk.Grid { _init(opts) { @@ -15,19 +17,23 @@ class ClapperInterface extends Gtk.Grid super._init(); let defaults = { - seekOnDrop: true + cssPath: `${pkg.datadir}/${pkg.name}/css/styles.css`, }; Object.assign(this, defaults, opts); + let cssProvider = new Gtk.CssProvider(); + cssProvider.load_from_path(this.cssPath); + Gtk.StyleContext.add_provider_for_display( + Gdk.Display.get_default(), + cssProvider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION + ); + this.fullscreenMode = false; this.isSeekable = false; - this.lastVolumeValue = null; - this.lastPositionValue = 0; this.lastRevealerEventTime = 0; this.needsTracksUpdate = true; - this.headerBar = null; - this.defaultTitle = null; this.mediaInfoSignal = null; this.videoBox = new Gtk.Box(); @@ -41,54 +47,21 @@ class ClapperInterface extends Gtk.Grid this.attach(this.videoBox, 0, 0, 1, 1); this.attach(this.controls, 0, 1, 1, 1); - this.destroySignal = this.connect('destroy', this._onDestroy.bind(this)); - } + this.mapSignal = this.connect('map', this._onMap.bind(this)); - addPlayer(player) - { - this._player = player; - this._player.widget.vexpand = true; - this._player.widget.hexpand = true; + this.player = new Player(); + this.player.widget.width_request = 960; + this.player.widget.height_request = 540; - this._player.connect('state-changed', this._onPlayerStateChanged.bind(this)); - this._player.connect('volume-changed', this._onPlayerVolumeChanged.bind(this)); - this._player.connect('duration-changed', this._onPlayerDurationChanged.bind(this)); - this._player.connect('position-updated', this._onPlayerPositionUpdated.bind(this)); + this.player.selfConnect('position-updated', this._onPlayerPositionUpdated.bind(this)); + this.player.selfConnect('duration-changed', this._onPlayerDurationChanged.bind(this)); + this.player.selfConnect('volume-changed', this._onPlayerVolumeChanged.bind(this)); - this._player.scrollController.connect( - 'scroll', (ctl, dx, dy) => this.controls._onScroll(ctl, dx, dy) - ); - this.controls.togglePlayButton.connect( - 'clicked', this._onControlsTogglePlayClicked.bind(this) - ); - this.scaleSig = this.controls.positionScale.connect( - 'value-changed', this._onControlsPositionChanged.bind(this) - ); - this.controls.volumeScale.connect( - 'value-changed', this._onControlsVolumeChanged.bind(this) - ); - this.controls.connect( - 'position-seeking-changed', this._onPositionSeekingChanged.bind(this) - ); - this.controls.connect( - 'track-change-requested', this._onTrackChangeRequested.bind(this) - ); - this.controls.connect( - 'visualization-change-requested', this._onVisualizationChangeRequested.bind(this) - ); - //this.revealerTop.connect('event-after', (self, event) => this._player.widget.event(event)); - - 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.revealerBottom); } - addHeaderBar(headerBar, defaultTitle) - { - this.headerBar = headerBar; - this.defaultTitle = defaultTitle || null; - } - revealControls(isReveal) { for(let pos of ['Top', 'Bottom']) @@ -101,30 +74,18 @@ class ClapperInterface extends Gtk.Grid this[`revealer${pos}`].showChild(isShow); } - setFullscreenMode(isFullscreen) + toggleFullscreen() { - if(this.fullscreenMode === isFullscreen) - return; + let root = this.get_root(); + if(!root) return; - 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.fullscreenMode = isFullscreen; - debug(`interface in fullscreen mode: ${isFullscreen}`); + let un = (this.fullscreenMode) ? 'un' : ''; + root[`${un}fullscreen`](); } _onMediaInfoUpdated(player, mediaInfo) { - this._player.disconnect(this.mediaInfoSignal); + player.disconnect(this.mediaInfoSignal); /* Set titlebar media title and path */ this.updateTitles(mediaInfo); @@ -193,7 +154,7 @@ class ClapperInterface extends Gtk.Grid } for(let type of ['video', 'audio', 'subtitle']) { - let currStream = this._player[`get_current_${type}_track`](); + let currStream = player[`get_current_${type}_track`](); let activeId = (currStream) ? currStream.get_index() : -1; if(currStream && type !== 'subtitle') { @@ -227,17 +188,27 @@ class ClapperInterface extends Gtk.Grid updateTitles(mediaInfo) { - if(this.headerBar) - this.headerBar.updateHeaderBar(mediaInfo); + let root = this.get_root(); + if(!root) return; - this.revealerTop.setMediaTitle(this.headerBar.titleLabel.label); + let title; + let headerbar = root.get_titlebar(); + + if(headerbar && headerbar.updateHeaderBar) { + headerbar.updateHeaderBar(mediaInfo); + title = headerbar.titleLabel.label; + } + else + title = mediaInfo.get_title() || mediaInfo.get_uri(); + + this.revealerTop.setMediaTitle(title); } updateTime() { let currTime = GLib.DateTime.new_now_local(); let endTime = currTime.add_seconds( - this.controls.positionAdjustment.get_upper() - this.lastPositionValue + this.controls.positionAdjustment.get_upper() - this.controls.currentPosition ); let nextUpdate = this.revealerTop.setTimes(currTime, endTime); @@ -283,78 +254,20 @@ class ClapperInterface extends Gtk.Grid debug(`show visualizations button: ${isShow}`); } - _onTrackChangeRequested(self, type, activeId) - { - /* Reenabling audio is slow (as expected), - * so it is better to toggle mute instead */ - if(type === 'audio') { - if(activeId < 0) - return this._player.set_mute(true); - - if(this._player.get_mute()) - this._player.set_mute(false); - - return this._player[`set_${type}_track`](activeId); - } - - if(activeId < 0) { - /* Disabling video leaves last frame frozen, - * so we hide it by making it transparent */ - if(type === 'video') - this._player.widget.set_opacity(0); - - return this._player[`set_${type}_track_enabled`](false); - } - - this._player[`set_${type}_track`](activeId); - this._player[`set_${type}_track_enabled`](true); - - if(type === 'video' && !this._player.widget.opacity) { - this._player.widget.set_opacity(1); - this._player.renderer.expose(); - } - } - - _onVisualizationChangeRequested(self, visName) - { - let isEnabled = this._player.get_visualization_enabled(); - - if(!visName) { - if(isEnabled) { - this._player.set_visualization_enabled(false); - debug('disabled visualizations'); - } - return; - } - - let currVis = this._player.get_current_visualization(); - - if(currVis === visName) - return; - - debug(`set visualization: ${visName}`); - this._player.set_visualization(visName); - - if(!isEnabled) { - this._player.set_visualization_enabled(true); - debug('enabled visualizations'); - } - } - _onPlayerStateChanged(player, state) { switch(state) { case GstPlayer.PlayerState.BUFFERING: - if(!this._player.is_local_file) + if(!player.is_local_file) this.needsTracksUpdate = true; break; case GstPlayer.PlayerState.STOPPED: - this.lastPositionValue = 0; - this.controls.positionAdjustment.set_value(0); + this.controls.currentPosition = 0; + this.controls.positionScale.set_value(0); this.controls.togglePlayButton.setPrimaryIcon(); this.needsTracksUpdate = true; if(this.mediaInfoSignal) { - this._player.disconnect(this.mediaInfoSignal); + player.disconnect(this.mediaInfoSignal); this.mediaInfoSignal = null; } case GstPlayer.PlayerState.PAUSED: @@ -364,7 +277,7 @@ class ClapperInterface extends Gtk.Grid this.controls.togglePlayButton.setSecondaryIcon(); if(this.needsTracksUpdate && !this.mediaInfoSignal) { this.needsTracksUpdate = false; - this.mediaInfoSignal = this._player.connect( + this.mediaInfoSignal = player.connect( 'media_info_updated', this._onMediaInfoUpdated.bind(this) ); } @@ -372,127 +285,100 @@ class ClapperInterface extends Gtk.Grid default: break; } + + if(state === GstPlayer.PlayerState.BUFFERING) + return; + + let window = this.get_root(); + Misc.inhibitForState(state, window); } _onPlayerDurationChanged(player) { - let duration = this._player.get_duration() / 1000000000; + let duration = Math.floor(player.get_duration() / 1000000000); + + /* Sometimes GstPlayer might re-emit + * duration changed during playback */ + if(this.controls.currentDuration === duration) + return; + let increment = (duration < 1) ? 0 : (duration < 100) ? 1 : duration / 100; - this.controls.positionAdjustment.set_upper(Math.floor(duration)); + this.controls.positionAdjustment.set_upper(duration); this.controls.positionAdjustment.set_step_increment(increment); this.controls.positionAdjustment.set_page_increment(increment); - this.controls.durationFormated = this.controls._getFormatedTime(duration); - this.controls._onPositionScaleValueChanged(); + this.controls.currentDuration = duration; + this.controls.durationFormated = Misc.getFormatedTime(duration); + this.controls.updateElapsedLabel(); } _onPlayerPositionUpdated(player, position) { if( !this.isSeekable - || !this._player.seek_done + || !player.seek_done || this.controls.isPositionSeeking - || this._player.state === GstPlayer.PlayerState.BUFFERING + || player.state === GstPlayer.PlayerState.BUFFERING ) return; let positionSeconds = Math.round(position / 1000000000); - - if(positionSeconds === this.lastPositionValue) + if(positionSeconds === this.controls.currentPosition) return; - this.lastPositionValue = positionSeconds; + this.controls.currentPosition = positionSeconds; this.controls.positionScale.set_value(positionSeconds); } - _onPlayerVolumeChanged() + _onPlayerVolumeChanged(player) { - let volume = Number(this._player.get_volume().toFixed(2)); - - if(volume === this.lastVolumeValue) + let volume = Number(player.get_volume().toFixed(2)); + if(volume === this.currentVolume) return; - this.lastVolumeValue = volume; + this.controls.currentVolume = volume; this.controls.volumeScale.set_value(volume); } - _onPositionSeekingChanged(self, isPositionSeeking) + _onStateNotify(toplevel) { - if(isPositionSeeking || !this.seekOnDrop) + let isFullscreen = Boolean( + toplevel.state & Gdk.ToplevelState.FULLSCREEN + ); + if(this.fullscreenMode === isFullscreen) return; - this._onControlsPositionChanged(this.controls.positionScale); - } + this.fullscreenMode = isFullscreen; - _onControlsTogglePlayClicked() - { - this._player.toggle_play(); - } - - _onControlsPositionChanged(positionScale) - { - if(this.seekOnDrop && this.controls.isPositionSeeking) - return; - - let positionSeconds = Math.round(positionScale.get_value()); - - if(positionSeconds === this.lastPositionValue) - return; - - this.lastPositionValue = positionSeconds; - this._player.seek_seconds(positionSeconds); - - /* Needed to enable preview after playback is stopped */ - if(this._player.state === GstPlayer.PlayerState.STOPPED) - this._player.pause(); - - if(this.fullscreenMode) - this.updateTime(); - } - - _onControlsVolumeChanged(volumeScale) - { - let volume = Number(volumeScale.get_value().toFixed(2)); - - let icon = (volume <= 0) - ? 'muted' - : (volume <= 0.33) - ? 'low' - : (volume <= 0.66) - ? 'medium' - : (volume <= 1) - ? 'high' - : 'overamplified'; - - let iconName = `audio-volume-${icon}-symbolic`; - if(this.controls.volumeButton.icon_name !== iconName) - { - debug(`set volume icon: ${icon}`); - this.controls.volumeButton.set_icon_name(iconName); + 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); } - if(volume === this.lastVolumeValue) - return; + this.controls.setFullscreenMode(isFullscreen); + this.showControls(isFullscreen); + this.player.widget.grab_focus(); - this.lastVolumeValue = volume; - this._player.set_volume(volume); + debug(`interface in fullscreen mode: ${isFullscreen}`); } - _onDestroy() + _onMap() { - this.disconnect(this.destroySignal); + this.disconnect(this.mapSignal); - if( - this._player - && this._player.state !== GstPlayer.PlayerState.STOPPED - ) - this._player.stop(); + let root = this.get_root(); + if(!root) return; - this.controls.emit('destroy'); + let surface = root.get_surface(); + surface.connect('notify::state', this._onStateNotify.bind(this)); } }); diff --git a/clapper_src/window.js b/clapper_src/window.js index a9228202..5748a589 100644 --- a/clapper_src/window.js +++ b/clapper_src/window.js @@ -1,49 +1,18 @@ const { Gdk, GObject, Gtk } = imports.gi; -var Window = GObject.registerClass({ - Signals: { - 'fullscreen-changed': { - param_types: [GObject.TYPE_BOOLEAN] - }, - } -}, class ClapperWindow extends Gtk.ApplicationWindow +var Window = GObject.registerClass( +class ClapperWindow extends Gtk.ApplicationWindow { _init(application, title) { super._init({ application: application, - title: title || 'Clapper', - resizable: true, - destroy_with_parent: true, + title: title, }); - this.isFullscreen = false; - this.mapSignal = this.connect('map', this._onMap.bind(this)); } - toggleFullscreen() + updateTitlebar(mediaInfo) { - let un = (this.isFullscreen) ? 'un' : ''; - this[`${un}fullscreen`](); - } - - _onStateNotify(toplevel) - { - let isFullscreen = Boolean( - toplevel.state & Gdk.ToplevelState.FULLSCREEN - ); - - if(this.isFullscreen === isFullscreen) - return; - - this.isFullscreen = isFullscreen; - this.emit('fullscreen-changed', this.isFullscreen); - } - - _onMap() - { - this.disconnect(this.mapSignal); - - let surface = this.get_surface(); - surface.connect('notify::state', this._onStateNotify.bind(this)); + } }); diff --git a/gjs-1.0/clapper.js.in b/gjs-1.0/clapper.js.in index e5ba93ca..c426d524 100644 --- a/gjs-1.0/clapper.js.in +++ b/gjs-1.0/clapper.js.in @@ -5,7 +5,6 @@ imports.searchPath.unshift('@importspath@'); const ClapperSrc = imports.clapper_src; var { App } = ClapperSrc.app; -var { Interface } = ClapperSrc.interface; -var { Player } = ClapperSrc.player; +var { Widget } = ClapperSrc.widget; imports.searchPath.shift();