const { Gdk, Gio, GLib, GObject, Gst, GstClapper, Gtk } = imports.gi; const { Controls } = imports.src.controls; const Debug = imports.src.debug; const Dialogs = imports.src.dialogs; const Misc = imports.src.misc; const { Player } = imports.src.player; const Revealers = imports.src.revealers; const { debug } = Debug; const { settings } = Misc; let lastTvScaling = null; var Widget = GObject.registerClass({ GTypeName: 'ClapperWidget', }, class ClapperWidget extends Gtk.Grid { _init() { super._init(); /* load CSS here to allow using this class * separately as a pre-made GTK widget */ Misc.loadCustomCss(); this.posX = 0; this.posY = 0; this.layoutWidth = 0; this.isFullscreenMode = false; this.isMobileMonitor = false; this.isSeekable = false; this.isDragAllowed = false; this.isSwipePerformed = false; this.isReleaseKeyEnabled = false; this.isLongPressed = false; this.isCursorInPlayer = false; this.isPopoverOpen = false; this._hideControlsTimeout = null; this._updateTimeTimeout = null; this.surfaceMapSignal = null; this.needsCursorRestore = false; this.overlay = new Gtk.Overlay(); this.revealerTop = new Revealers.RevealerTop(); this.revealerBottom = new Revealers.RevealerBottom(); this.controls = new Controls(); this.controlsBox = new Gtk.Box({ orientation: Gtk.Orientation.HORIZONTAL, }); this.controlsBox.add_css_class('controlsbox'); this.controlsBox.append(this.controls); this.controlsRevealer = new Revealers.ControlsRevealer(); this.controlsRevealer.set_child(this.controlsBox); this.attach(this.overlay, 0, 0, 1, 1); this.attach(this.controlsRevealer, 0, 1, 1, 1); this.player = new Player(); const playerWidget = this.player.widget; this.controls.elapsedButton.scrolledWindow.set_child(this.player.playlistWidget); const speedAdjustment = this.controls.elapsedButton.speedScale.get_adjustment(); speedAdjustment.bind_property( 'value', this.player, 'rate', GObject.BindingFlags.BIDIRECTIONAL ); const volumeAdjustment = this.controls.volumeButton.volumeScale.get_adjustment(); volumeAdjustment.bind_property( 'value', this.player, 'volume', GObject.BindingFlags.BIDIRECTIONAL ); this.player.connect('position-updated', this._onPlayerPositionUpdated.bind(this)); this.player.connect('duration-changed', this._onPlayerDurationChanged.bind(this)); this.player.connect('media-info-updated', this._onMediaInfoUpdated.bind(this)); this.player.connect('video-decoder-changed', this._onPlayerVideoDecoderChanged.bind(this)); this.player.connect('audio-decoder-changed', this._onPlayerAudioDecoderChanged.bind(this)); this.overlay.set_child(playerWidget); this.overlay.add_overlay(this.revealerTop); this.overlay.add_overlay(this.revealerBottom); const clickGesture = this._getClickGesture(); playerWidget.add_controller(clickGesture); const clickGestureTop = this._getClickGesture(); this.revealerTop.add_controller(clickGestureTop); const longPressGesture = this._getLongPressGesture(); playerWidget.add_controller(longPressGesture); const longPressGestureTop = this._getLongPressGesture(); this.revealerTop.add_controller(longPressGestureTop); const dragGesture = this._getDragGesture(); playerWidget.add_controller(dragGesture); const dragGestureTop = this._getDragGesture(); this.revealerTop.add_controller(dragGestureTop); const swipeGesture = this._getSwipeGesture(); playerWidget.add_controller(swipeGesture); const swipeGestureTop = this._getSwipeGesture(); this.revealerTop.add_controller(swipeGestureTop); const scrollController = this._getScrollController(); playerWidget.add_controller(scrollController); const scrollControllerTop = this._getScrollController(); this.revealerTop.add_controller(scrollControllerTop); const motionController = this._getMotionController(); playerWidget.add_controller(motionController); const motionControllerTop = this._getMotionController(); this.revealerTop.add_controller(motionControllerTop); const dropTarget = this._getDropTarget(); playerWidget.add_controller(dropTarget); /* Applied only for widget to detect simple action key releases */ const keyController = new Gtk.EventControllerKey(); keyController.connect('key-released', this._onKeyReleased.bind(this)); this.add_controller(keyController); } revealControls() { this.revealerTop.revealChild(true); this.revealerBottom.revealChild(true); this._checkSetUpdateTimeInterval(); /* Reset timeout if already revealed, otherwise * timeout will be set after reveal finishes */ if(this.revealerTop.child_revealed) this._setHideControlsTimeout(); } toggleFullscreen() { const root = this.get_root(); if(!root) return; const un = (this.isFullscreenMode) ? 'un' : ''; root[`${un}fullscreen`](); } setFullscreenMode(isFullscreen) { if(this.isFullscreenMode === isFullscreen) return; debug('changing fullscreen mode'); this.isFullscreenMode = isFullscreen; if(!isFullscreen) this._clearTimeout('updateTime'); this.revealerTop.setFullscreenMode(isFullscreen, this.isMobileMonitor); this.revealerBottom.revealerBox.visible = isFullscreen; this._changeControlsPlacement(isFullscreen); this.controls.setFullscreenMode(isFullscreen, this.isMobileMonitor); if(this.revealerTop.child_revealed) this._checkSetUpdateTimeInterval(); debug(`interface in fullscreen mode: ${isFullscreen}`); } _changeControlsPlacement(isOnTop) { if(isOnTop) { this.controlsBox.remove(this.controls); this.revealerBottom.append(this.controls); } else { this.revealerBottom.remove(this.controls); this.controlsBox.append(this.controls); } this.controlsBox.set_visible(!isOnTop); } _onMediaInfoUpdated(player, mediaInfo) { /* Set titlebar media title */ this.updateTitle(mediaInfo); /* FIXME: replace number with Gst.CLOCK_TIME_NONE when GJS * can do UINT64: https://gitlab.gnome.org/GNOME/gjs/-/merge_requests/524 */ const isLive = (mediaInfo.is_live() || player.duration === 18446744073709552000); this.isSeekable = (!isLive && mediaInfo.is_seekable()); /* Show/hide position scale on LIVE */ this.controls.setLiveMode(isLive, this.isSeekable); /* Update remaining end time if visible */ this.updateTime(); if(this.player.needsTocUpdate) { if(!isLive) this.updateChapters(mediaInfo.get_toc()); this.player.needsTocUpdate = false; } const streamList = mediaInfo.get_stream_list(); const parsedInfo = { videoTracks: [], audioTracks: [], subtitleTracks: [] }; for(let info of streamList) { let type, text, codec; switch(info.constructor) { case GstClapper.ClapperVideoInfo: type = 'video'; codec = info.get_codec() || _('Undetermined'); text = `${codec}, ${info.get_width()}×${info.get_height()}`; let fps = info.get_framerate(); fps = Number((fps[0] / fps[1]).toFixed(2)); if(fps) text += `@${fps}`; break; case GstClapper.ClapperAudioInfo: type = 'audio'; codec = info.get_codec() || _('Undetermined'); if(codec.includes('(')) { codec = codec.substring( codec.indexOf('(') + 1, codec.indexOf(')') ); } text = info.get_language() || _('Undetermined'); text += `, ${codec}, ${info.get_channels()} ` + _('Channels'); break; case GstClapper.ClapperSubtitleInfo: type = 'subtitle'; const subsLang = info.get_language(); text = (subsLang) ? subsLang.split(',')[0] : _('Undetermined'); const subsTitle = Misc.getSubsTitle(info.get_title()); if(subsTitle) text += `, ${subsTitle}`; break; default: debug(`unrecognized media info type: ${info.constructor}`); break; } const tracksArr = parsedInfo[`${type}Tracks`]; if(!tracksArr.length) { tracksArr[0] = { label: _('Disabled'), type: type, activeId: -1 }; } tracksArr.push({ label: text, type: type, activeId: info.get_index(), }); } let anyButtonShown = false; for(let type of ['video', 'audio', 'subtitle']) { const currStream = this.player[`get_current_${type}_track`](); const activeId = (currStream) ? currStream.get_index() : -1; if(currStream && type !== 'subtitle') { const caps = currStream.get_caps(); if (caps) debug(`${type} caps: ${caps.to_string()}`); } if(type === 'video') { const isShowVis = ( !parsedInfo.videoTracks.length && parsedInfo.audioTracks.length ); this.showVisualizationsButton(isShowVis); } if(!parsedInfo[`${type}Tracks`].length) { debug(`hiding popover button without contents: ${type}`); this.controls[`${type}TracksButton`].set_visible(false); continue; } this.controls.addCheckButtons( this.controls[`${type}TracksButton`].popoverBox, parsedInfo[`${type}Tracks`], activeId ); debug(`showing popover button with contents: ${type}`); this.controls[`${type}TracksButton`].set_visible(true); anyButtonShown = true; } this.controls.revealTracksRevealer.set_visible(anyButtonShown); } updateTitle(mediaInfo) { let title = mediaInfo.get_title(); if(!title) { const item = this.player.playlistWidget.getActiveRow(); title = item.filename; } this.refreshWindowTitle(title); this.revealerTop.title = title; this.revealerTop.showTitle = true; } refreshWindowTitle(title) { const isFloating = !this.controlsRevealer.reveal_child; const pipSuffix = ' - PiP'; const hasPipSuffix = title.endsWith(pipSuffix); this.root.title = (isFloating && !hasPipSuffix) ? title + pipSuffix : (!isFloating && hasPipSuffix) ? title.substring(0, title.length - pipSuffix.length) : title; } updateTime() { if( !this.revealerTop.visible || !this.revealerTop.revealerGrid.visible || !this.isFullscreenMode || this.isMobileMonitor ) return null; const currTime = GLib.DateTime.new_now_local(); const endTime = currTime.add_seconds( this.controls.positionAdjustment.get_upper() - this.controls.currentPosition ); const nextUpdate = this.revealerTop.setTimes(currTime, endTime, this.isSeekable); return nextUpdate; } updateChapters(toc) { if(!toc) return; const entries = toc.get_entries(); if(!entries) return; for(let entry of entries) { const subentries = entry.get_sub_entries(); if(!subentries) continue; for(let subentry of subentries) this._parseTocSubentry(subentry); } } _parseTocSubentry(subentry) { const [success, start, stop] = subentry.get_start_stop_times(); if(!success) { debug('could not obtain toc subentry start/stop times'); return; } const pos = Math.floor(start / Gst.MSECOND) / 1000; const tags = subentry.get_tags(); this.controls.positionScale.add_mark(pos, Gtk.PositionType.TOP, null); this.controls.positionScale.add_mark(pos, Gtk.PositionType.BOTTOM, null); if(!tags) { debug('could not obtain toc subentry tags'); return; } const [isString, title] = tags.get_string('title'); if(!isString) { debug('toc subentry tag does not have a title'); return; } if(!this.controls.chapters) this.controls.chapters = {}; this.controls.chapters[pos] = title; debug(`chapter at ${pos}: ${title}`); } showVisualizationsButton(isShow) { if(isShow && !this.controls.visualizationsButton.isVisList) { debug('creating visualizations list'); const visArr = GstClapper.Clapper.visualizations_get(); if(!visArr.length) return; const parsedVisArr = [{ label: 'Disabled', type: 'visualization', activeId: null }]; visArr.forEach(vis => { parsedVisArr.push({ label: vis.name[0].toUpperCase() + vis.name.substring(1), type: 'visualization', activeId: vis.name, }); }); this.controls.addCheckButtons( this.controls.visualizationsButton.popoverBox, parsedVisArr, null ); this.controls.visualizationsButton.isVisList = true; debug(`total visualizations: ${visArr.length}`); } if(this.controls.visualizationsButton.visible === isShow) return; const action = (isShow) ? 'show' : 'hide'; this.controls.visualizationsButton[action](); debug(`show visualizations button: ${isShow}`); } _onPlayerStateChanged(player, state) { switch(state) { case GstClapper.ClapperState.BUFFERING: debug('player state changed to: BUFFERING'); if(player.needsTocUpdate) { this.controls._setChapterVisible(false); this.controls.positionScale.clear_marks(); this.controls.chapters = null; } break; case GstClapper.ClapperState.STOPPED: debug('player state changed to: STOPPED'); this.controls.setInitialState(); this.revealerTop.showTitle = false; break; case GstClapper.ClapperState.PAUSED: debug('player state changed to: PAUSED'); this.controls.togglePlayButton.setPrimaryIcon(); break; case GstClapper.ClapperState.PLAYING: debug('player state changed to: PLAYING'); this.controls.togglePlayButton.setSecondaryIcon(); break; default: break; } } _onPlayerDurationChanged(player, duration) { const durationSeconds = duration / Gst.SECOND; const durationFloor = Math.floor(durationSeconds); debug(`duration changed: ${durationSeconds}`); this.controls.showHours = (durationFloor >= 3600); this.controls.positionAdjustment.set_upper(durationFloor); this.controls.durationFormatted = Misc.getFormattedTime(durationFloor); this.controls.updateElapsedLabel(); if(settings.get_boolean('resume-enabled')) { const resumeDatabase = JSON.parse(settings.get_string('resume-database')); const title = player.playlistWidget.getActiveFilename(); debug(`searching database for resume info: ${title}`); const resumeInfo = resumeDatabase.find(info => { return (info.title === title && info.duration === durationSeconds); }); if(resumeInfo) { debug('found resume info: ' + JSON.stringify(resumeInfo)); new Dialogs.ResumeDialog(this.root, resumeInfo); const shrunkDatabase = resumeDatabase.filter(info => { return !(info.title === title && info.duration === durationSeconds); }); settings.set_string('resume-database', JSON.stringify(shrunkDatabase)); } else debug('resume info not found'); } } _onPlayerPositionUpdated(player, position) { if( !this.isSeekable || this.controls.isPositionDragging || !player.seekDone ) return; const positionSeconds = Math.round(position / Gst.SECOND); if(positionSeconds === this.controls.currentPosition) return; this.controls.positionScale.set_value(positionSeconds); } _onPlayerVideoDecoderChanged(player, decoder) { this.controls.videoTracksButton.setDecoder(decoder); } _onPlayerAudioDecoderChanged(player, decoder) { this.controls.audioTracksButton.setDecoder(decoder); } _onStateNotify(toplevel) { const isMaximized = Boolean( toplevel.state & Gdk.ToplevelState.MAXIMIZED ); const isFullscreen = Boolean( toplevel.state & Gdk.ToplevelState.FULLSCREEN ); const headerBar = this.revealerTop.headerBar; headerBar.setMaximized(isMaximized); this.setFullscreenMode(isFullscreen); } _onLayoutUpdate(surface, width, height) { if(width === this.layoutWidth) return; /* Launch without showing revealers transitions on mobile width */ if(!this.layoutWidth && width < this.controls.minFullViewWidth) { for(let revealer of this.controls.revealersArr) revealer.revealInstantly(false); } this.layoutWidth = width; if(this.isFullscreenMode) this.revealerBottom.setLayoutMargins(width); this.controls._onPlayerResize(width, height); } _onWindowMap(window) { const surface = window.get_surface(); if(!surface.mapped) this.surfaceMapSignal = surface.connect( 'notify::mapped', this._onSurfaceMapNotify.bind(this) ); else this._onSurfaceMapNotify(surface); surface.connect('notify::state', this._onStateNotify.bind(this)); surface.connect('enter-monitor', this._onEnterMonitor.bind(this)); surface.connect('layout', this._onLayoutUpdate.bind(this)); this.player._onWindowMap(window); } _onSurfaceMapNotify(surface) { if(!surface.mapped) return; if(this.surfaceMapSignal) { surface.disconnect(this.surfaceMapSignal); this.surfaceMapSignal = null; } const monitor = surface.display.get_monitor_at_surface(surface); const size = JSON.parse(settings.get_string('window-size')); const hasMonitor = Boolean(monitor && monitor.geometry); /* Let GTK handle window restore if no monitor, otherwise check if its size is greater then saved window size */ if( !hasMonitor || (monitor.geometry.width >= size[0] && monitor.geometry.height >= size[1]) ) { if(!hasMonitor) debug('restoring window size without monitor geometry'); this.root.set_default_size(size[0], size[1]); debug(`restored window size: ${size[0]}x${size[1]}`); } } _onEnterMonitor(surface, monitor) { debug('entered new monitor'); const { geometry } = monitor; debug(`monitor application-pixels: ${geometry.width}x${geometry.height}`); const monitorWidth = Math.max(geometry.width, geometry.height); this.isMobileMonitor = (monitorWidth < 1280); debug(`mobile monitor detected: ${this.isMobileMonitor}`); const hasTVCss = this.root.has_css_class('tvmode'); if(hasTVCss === this.isMobileMonitor) { const action = (this.isMobileMonitor) ? 'remove' : 'add'; this.root[action + '_css_class']('tvmode'); } /* Mobile does not have TV mode, so we do not care about removing scaling */ if(!this.isMobileMonitor) { const pixWidth = monitorWidth * monitor.scale_factor; const tvScaling = (pixWidth <= 1280) ? 'lowres' : (pixWidth > 1920) ? 'hires' : null; if(lastTvScaling !== tvScaling) { if(lastTvScaling) this.root.remove_css_class(lastTvScaling); if(tvScaling) this.root.add_css_class(tvScaling); lastTvScaling = tvScaling; } debug(`using scaling mode: ${tvScaling || 'normal'}`); } /* Update top revealer display mode */ this.revealerTop.setFullscreenMode(this.isFullscreenMode, this.isMobileMonitor); } _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'); } _setHideControlsTimeout() { this._clearTimeout('hideControls'); let time = 2500; if(this.isFullscreenMode && !this.isMobileMonitor) time += 1500; this._hideControlsTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, time, () => { this._hideControlsTimeout = null; if(this.isCursorInPlayer) { const blankCursor = Gdk.Cursor.new_from_name('none', null); this.player.widget.set_cursor(blankCursor); this.revealerTop.set_cursor(blankCursor); this.needsCursorRestore = true; } if(!this.isPopoverOpen) { this._clearTimeout('updateTime'); this.revealerTop.revealChild(false); this.revealerBottom.revealChild(false); } return GLib.SOURCE_REMOVE; }); } _checkSetUpdateTimeInterval() { if( this.isFullscreenMode && !this.isMobileMonitor && !this._updateTimeTimeout ) { debug('setting update time interval'); this._setUpdateTimeInterval(); } } _setUpdateTimeInterval() { this._clearTimeout('updateTime'); const nextUpdate = this.updateTime(); if(nextUpdate === null) return; this._updateTimeTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, nextUpdate, () => { this._updateTimeTimeout = null; if(this.isFullscreenMode) this._setUpdateTimeInterval(); return GLib.SOURCE_REMOVE; }); } _handleDoublePress(gesture, x, y) { if(!this.isFullscreenMode || !Misc.getIsTouch(gesture)) return this.toggleFullscreen(); const fieldSize = this.layoutWidth / 6; if(x < fieldSize) { debug('left side double press'); this.player.playlistWidget.prevTrack(); } else if(x > this.layoutWidth - fieldSize) { debug('right side double press'); this.player.playlistWidget.nextTrack(); } else { this.toggleFullscreen(); } } _getClickGesture() { const clickGesture = new Gtk.GestureClick({ button: 0, propagation_phase: Gtk.PropagationPhase.CAPTURE, }); clickGesture.connect('pressed', this._onPressed.bind(this)); clickGesture.connect('released', this._onReleased.bind(this)); return clickGesture; } _getLongPressGesture() { const longPressGesture = new Gtk.GestureLongPress({ touch_only: true, delay_factor: 0.9, propagation_phase: Gtk.PropagationPhase.CAPTURE, }); longPressGesture.connect('pressed', this._onLongPressed.bind(this)); return longPressGesture; } _getDragGesture() { const dragGesture = new Gtk.GestureDrag({ propagation_phase: Gtk.PropagationPhase.CAPTURE, }); dragGesture.connect('drag-update', this._onDragUpdate.bind(this)); return dragGesture; } _getSwipeGesture() { const swipeGesture = new Gtk.GestureSwipe({ touch_only: true, propagation_phase: Gtk.PropagationPhase.CAPTURE, }); swipeGesture.connect('swipe', this._onSwipe.bind(this)); swipeGesture.connect('update', this._onSwipeUpdate.bind(this)); return swipeGesture; } _getScrollController() { const scrollController = new Gtk.EventControllerScroll(); scrollController.set_flags(Gtk.EventControllerScrollFlags.BOTH_AXES); scrollController.connect('scroll', this._onScroll.bind(this)); return scrollController; } _getMotionController() { const motionController = new Gtk.EventControllerMotion(); motionController.connect('enter', this._onEnter.bind(this)); motionController.connect('leave', this._onLeave.bind(this)); motionController.connect('motion', this._onMotion.bind(this)); return motionController; } _getDropTarget() { const dropTarget = new Gtk.DropTarget({ actions: Gdk.DragAction.COPY | Gdk.DragAction.MOVE, }); dropTarget.set_gtypes([GObject.TYPE_STRING]); dropTarget.connect('motion', this._onDataMotion.bind(this)); dropTarget.connect('drop', this._onDataDrop.bind(this)); return dropTarget; } _getIsSwipeOk(velocity, otherVelocity) { if(!velocity) return false; const absVel = Math.abs(velocity); if(absVel < 20 || Math.abs(otherVelocity) * 1.5 >= absVel) return false; return this.isFullscreenMode; } _onPressed(gesture, nPress, x, y) { const button = gesture.get_current_button(); const isDouble = (nPress % 2 == 0); this.isDragAllowed = !isDouble; this.isSwipePerformed = false; this.isLongPressed = false; switch(button) { case Gdk.BUTTON_PRIMARY: if(isDouble) this._handleDoublePress(gesture, x, y); break; case Gdk.BUTTON_SECONDARY: this.player.toggle_play(); break; default: break; } } _onReleased(gesture, nPress, x, y) { /* Reveal if touch was not a swipe/long press or was already revealed */ if( ((!this.isSwipePerformed && !this.isLongPressed) || this.revealerBottom.child_revealed) && Misc.getIsTouch(gesture) ) this.revealControls(); } _onLongPressed(gesture, x, y) { if(!this.isDragAllowed || !this.isFullscreenMode) return; this.isLongPressed = true; this.player.toggle_play(); } _onKeyReleased(controller, keyval, keycode, state) { /* Ignore releases that did not trigger keypress * e.g. while holding left "Super" key */ if(!this.isReleaseKeyEnabled) return; switch(keyval) { case Gdk.KEY_Right: case Gdk.KEY_Left: const value = Math.round( this.controls.positionScale.get_value() ); this.player.seek_seconds(value); this._setHideControlsTimeout(); this.isReleaseKeyEnabled = false; break; default: break; } } _onDragUpdate(gesture, offsetX, offsetY) { if(!this.isDragAllowed || this.isFullscreenMode) return; const { gtk_double_click_distance } = this.get_settings(); if ( Math.abs(offsetX) > gtk_double_click_distance || Math.abs(offsetY) > gtk_double_click_distance ) { const [isActive, startX, startY] = gesture.get_start_point(); if(!isActive) return; const playerWidget = this.player.widget; const native = playerWidget.get_native(); if(!native) return; let [isShared, winX, winY] = playerWidget.translate_coordinates( native, startX, startY ); if(!isShared) return; const [nativeX, nativeY] = native.get_surface_transform(); winX += nativeX; winY += nativeY; native.get_surface().begin_move( gesture.get_device(), gesture.get_current_button(), winX, winY, gesture.get_current_event_time() ); gesture.reset(); } } _onSwipe(gesture, velocityX, velocityY) { if(!this._getIsSwipeOk(velocityX, velocityY)) return; this._onScroll(gesture, -velocityX, 0); this.isSwipePerformed = true; } _onSwipeUpdate(gesture, sequence) { const [isCalc, velocityX, velocityY] = gesture.get_velocity(); if(!isCalc) return; if(!this._getIsSwipeOk(velocityY, velocityX)) return; const isIncrease = velocityY < 0; this.player.adjust_volume(isIncrease, 0.01); this.isSwipePerformed = true; } _onScroll(controller, dx, dy) { const isHorizontal = (Math.abs(dx) >= Math.abs(dy)); const isIncrease = (isHorizontal) ? dx < 0 : dy < 0; if(isHorizontal) { this.player.adjust_position(isIncrease); const value = Math.round(this.controls.positionScale.get_value()); this.player.seek_seconds(value); } else this.player.adjust_volume(isIncrease); return true; } _onEnter(controller, x, y) { this.isCursorInPlayer = true; } _onLeave(controller) { if(this.isFullscreenMode) return; this.isCursorInPlayer = false; } _onMotion(controller, posX, posY) { this.isCursorInPlayer = 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 ) { if(this.needsCursorRestore) { const defaultCursor = Gdk.Cursor.new_from_name('default', null); this.player.widget.set_cursor(defaultCursor); this.revealerTop.set_cursor(defaultCursor); this.needsCursorRestore = false; } this.revealControls(); } this.posX = posX; this.posY = posY; } _onDataMotion(dropTarget, x, y) { return Gdk.DragAction.MOVE; } _onDataDrop(dropTarget, value, x, y) { const files = value.split(/\r?\n/).filter(uri => { return Gst.uri_is_valid(uri); }); if(!files.length) return false; for(let index in files) files[index] = Gio.File.new_for_uri(files[index]); /* TODO: remove GTK < 4.3.2 compat someday */ const currentDrop = dropTarget.current_drop || dropTarget.drop; const app = this.root.application; app.isFileAppend = Boolean(currentDrop.actions & Gdk.DragAction.COPY); app.open(files, ""); return true; } });