From dc649eeadf76060acce5d2b9fe0bcf022f0504f7 Mon Sep 17 00:00:00 2001 From: danielku15 Date: Fri, 10 Jul 2026 17:06:52 +0200 Subject: [PATCH] feat: individual settings to control user interaction --- packages/alphatab/src/AlphaTabApiBase.ts | 89 +++++++++++++++---- packages/alphatab/src/PlayerSettings.ts | 40 ++++++++- .../src/generated/PlayerSettingsJson.ts | 24 +++++ .../src/generated/PlayerSettingsSerializer.ts | 13 ++- .../src/platform/javascript/AlphaTabApi.ts | 3 +- 5 files changed, 150 insertions(+), 19 deletions(-) diff --git a/packages/alphatab/src/AlphaTabApiBase.ts b/packages/alphatab/src/AlphaTabApiBase.ts index 9ca2e45d4..b23913e9f 100644 --- a/packages/alphatab/src/AlphaTabApiBase.ts +++ b/packages/alphatab/src/AlphaTabApiBase.ts @@ -2863,7 +2863,7 @@ export class AlphaTabApiBase { return; } - if (this._hasCursor && this.settings.player.enableUserInteraction) { + if (this._hasCursor && this.settings.player.enablePlaybackRangeSelection) { this._selectionStart = { beat }; this._selectionEnd = undefined; } @@ -2887,10 +2887,14 @@ export class AlphaTabApiBase { return; } - if (this.settings.player.enableUserInteraction) { + if (this.settings.player.enablePlaybackRangeSelection) { if (!this._selectionEnd || this._selectionEnd.beat !== beat) { - this._selectionEnd = { beat }; - this._cursorSelectRange(this._selectionStart, this._selectionEnd); + // ensure we do not clear the selection until we have different beats + const hasRange = this._selectionStart?.beat !== beat; + if (hasRange) { + this._selectionEnd = { beat }; + this._cursorSelectRange(this._selectionStart, this._selectionEnd); + } } } (this.beatMouseMove as EventEmitterOfT).trigger(beat); @@ -2911,8 +2915,17 @@ export class AlphaTabApiBase { return; } - if (this._hasCursor && this.settings.player.enableUserInteraction) { - this.applyPlaybackRangeFromHighlight(); + if (this._hasCursor) { + let shouldSeekToBeat = beat && this.settings.player.enableSeekToClick; + if (this.settings.player.enablePlaybackRangeSelection) { + if (this._internalApplyPlaybackRangeFromHighlight()) { + shouldSeekToBeat = false; + } + } + + if (shouldSeekToBeat) { + this._seekToBeat(beat!); + } } (this.beatMouseUp as EventEmitterOfT).trigger(beat); @@ -2920,6 +2933,34 @@ export class AlphaTabApiBase { this._isBeatMouseDown = false; } + private _seekToBeat(beat: Beat) { + const tickCache = this._tickCache; + if (!tickCache) { + return; + } + + this._currentBeat = null; + const realStartMasterBarStart: number = tickCache.getMasterBarStart(beat.voice.bar.masterBar); + const startBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(beat); + const startBeatPlaybackStart = startBeatPlaybackRange?.startTick ?? beat.playbackStart; + + // clamp to playback range + let beatTick = realStartMasterBarStart + startBeatPlaybackStart; + const playbackRange = this.playbackRange; + if (playbackRange) { + if (beatTick < playbackRange.startTick) { + beatTick = playbackRange.startTick; + } else if (beatTick > playbackRange.endTick) { + beatTick = playbackRange.endTick; + } + } + + if (this._player.state === PlayerState.Paused) { + this._cursorUpdateTick(beatTick, false, 1); + } + this.tickPosition = beatTick; + } + private _onNoteMouseUp(originalEvent: IMouseEventArgs, note: Note | null): void { if (this._isDestroyed) { return; @@ -2952,7 +2993,7 @@ export class AlphaTabApiBase { if (!e.isLeftMouseButton) { return; } - if (this.settings.player.enableUserInteraction) { + if (this.settings.player.enablePlaybackRangeSelection || this.settings.player.enableSeekToClick) { e.preventDefault(); } const relX: number = e.getX(this.canvasElement); @@ -2991,7 +3032,7 @@ export class AlphaTabApiBase { if (!this._isBeatMouseDown) { return; } - if (this.settings.player.enableUserInteraction) { + if (this.settings.player.enablePlaybackRangeSelection || this.settings.player.enableSeekToClick) { e.preventDefault(); } const relX: number = e.getX(this.canvasElement); @@ -3009,7 +3050,7 @@ export class AlphaTabApiBase { } }); this._renderer.postRenderFinished.on(() => { - if (!this._selectionStart || !this._hasCursor || !this.settings.player.enableUserInteraction) { + if (!this._selectionStart || !this._hasCursor || !this.settings.player.enablePlaybackRangeSelection) { return; } this._cursorSelectRange(this._selectionStart, this._selectionEnd); @@ -3105,6 +3146,10 @@ export class AlphaTabApiBase { * ``` */ public applyPlaybackRangeFromHighlight() { + this._internalApplyPlaybackRangeFromHighlight(); + } + + private _internalApplyPlaybackRangeFromHighlight() { if (this._selectionEnd) { const startTick: number = this._tickCache?.getBeatStart(this._selectionStart!.beat) ?? @@ -3117,7 +3162,11 @@ export class AlphaTabApiBase { this._selectionStart = this._selectionEnd; this._selectionEnd = t; } + } else if (!this.settings.player.resetPlaybackRangeOnClick) { + // no new range selected -> do not reset playback range if respective option is set + return false; } + if (this._selectionStart && this._tickCache) { // get the start and stop ticks (which consider properly repeats) const tickCache: MidiTickLookup = this._tickCache; @@ -3126,14 +3175,12 @@ export class AlphaTabApiBase { ); const startBeatPlaybackRange = tickCache.getRelativeBeatPlaybackRange(this._selectionStart.beat); const startBeatPlaybackStart = startBeatPlaybackRange?.startTick ?? this._selectionStart.beat.playbackStart; - // move to selection start - this._currentBeat = null; // reset current beat so it is updating the cursor - if (this._player.state === PlayerState.Paused) { - this._cursorUpdateTick(realStartMasterBarStart + startBeatPlaybackStart, false, 1); - } - this.tickPosition = realStartMasterBarStart + startBeatPlaybackStart; + + let seekToStart = this.settings.player.enableSeekToClick; + // set playback range if (this._selectionEnd && this._selectionStart.beat !== this._selectionEnd.beat) { + seekToStart = true; const realEndMasterBarStart: number = tickCache.getMasterBarStart( this._selectionEnd.beat.voice.bar.masterBar ); @@ -3145,12 +3192,22 @@ export class AlphaTabApiBase { range.startTick = realStartMasterBarStart + startBeatPlaybackStart; range.endTick = realEndMasterBarStart + endBeatPlaybackEnd - 50; this.playbackRange = range; - } else { + } else if (this.settings.player.resetPlaybackRangeOnClick) { this._selectionStart = undefined; this.playbackRange = null; this._cursorSelectRange(this._selectionStart, this._selectionEnd); } + + if (seekToStart) { + this._currentBeat = null; // reset current beat so it is updating the cursor + if (this._player.state === PlayerState.Paused) { + this._cursorUpdateTick(realStartMasterBarStart + startBeatPlaybackStart, false, 1); + } + this.tickPosition = realStartMasterBarStart + startBeatPlaybackStart; + } } + + return true; } /** diff --git a/packages/alphatab/src/PlayerSettings.ts b/packages/alphatab/src/PlayerSettings.ts index 0e8fc9413..e53d344ef 100644 --- a/packages/alphatab/src/PlayerSettings.ts +++ b/packages/alphatab/src/PlayerSettings.ts @@ -287,11 +287,49 @@ export class PlayerSettings { * @since 0.9.7 * @defaultValue `true` * @category Player + * @json_read_only * @remarks * This setting configures whether alphaTab provides the default user interaction features like selection of the playback range and "seek on click". * By default users can select the desired playback range with the mouse and also jump to individual beats by click. This behavior can be contolled with this setting. + * @deprecated Use {@link enableSeekToClick} and {@link enablePlaybackRangeSelection} individually + */ + public get enableUserInteraction(): boolean { + return this.enableSeekToClick || this.enablePlaybackRangeSelection; + } + + /** + * @deprecated Use {@link enableSeekToClick} and {@link enablePlaybackRangeSelection} individually + */ + public set enableUserInteraction(value: boolean) { + this.enableSeekToClick = value; + this.enablePlaybackRangeSelection = value; + this.resetPlaybackRangeOnClick = value; + } + + /** + * Whether the a click on the music sheet triggers a player seek to the note/beat at the + * clicked location. + * @since 1.9.0 + * @defaultValue `true` + * @category Player + */ + public enableSeekToClick: boolean = true; + + /** + * Whether user click and drag results in a selection defining the playback range. + * @since 1.9.0 + * @defaultValue `true` + * @category Player + */ + public enablePlaybackRangeSelection: boolean = true; + + /** + * Whether a simple click (no range drag) should reset the current playback range. + * @since 1.9.0 + * @defaultValue `true` + * @category Player */ - public enableUserInteraction: boolean = true; + public resetPlaybackRangeOnClick: boolean = true; /** * The X-offset to add when scrolling. diff --git a/packages/alphatab/src/generated/PlayerSettingsJson.ts b/packages/alphatab/src/generated/PlayerSettingsJson.ts index fed686082..7ed2a76f9 100644 --- a/packages/alphatab/src/generated/PlayerSettingsJson.ts +++ b/packages/alphatab/src/generated/PlayerSettingsJson.ts @@ -129,11 +129,35 @@ export interface PlayerSettingsJson { * @since 0.9.7 * @defaultValue `true` * @category Player + * @json_read_only * @remarks * This setting configures whether alphaTab provides the default user interaction features like selection of the playback range and "seek on click". * By default users can select the desired playback range with the mouse and also jump to individual beats by click. This behavior can be contolled with this setting. + * @deprecated Use {@link enableSeekToClick} and {@link enablePlaybackRangeSelection} individually */ enableUserInteraction?: boolean; + /** + * Whether the a click on the music sheet triggers a player seek to the note/beat at the + * clicked location. + * @since 1.9.0 + * @defaultValue `true` + * @category Player + */ + enableSeekToClick?: boolean; + /** + * Whether user click and drag results in a selection defining the playback range. + * @since 1.9.0 + * @defaultValue `true` + * @category Player + */ + enablePlaybackRangeSelection?: boolean; + /** + * Whether a simple click (no range drag) should reset the current playback range. + * @since 1.9.0 + * @defaultValue `true` + * @category Player + */ + resetPlaybackRangeOnClick?: boolean; /** * The X-offset to add when scrolling. * @since 0.9.6 diff --git a/packages/alphatab/src/generated/PlayerSettingsSerializer.ts b/packages/alphatab/src/generated/PlayerSettingsSerializer.ts index 82f02505c..905a332f3 100644 --- a/packages/alphatab/src/generated/PlayerSettingsSerializer.ts +++ b/packages/alphatab/src/generated/PlayerSettingsSerializer.ts @@ -34,7 +34,9 @@ export class PlayerSettingsSerializer { o.set("enablecursor", obj.enableCursor); o.set("enableanimatedbeatcursor", obj.enableAnimatedBeatCursor); o.set("enableelementhighlighting", obj.enableElementHighlighting); - o.set("enableuserinteraction", obj.enableUserInteraction); + o.set("enableseektoclick", obj.enableSeekToClick); + o.set("enableplaybackrangeselection", obj.enablePlaybackRangeSelection); + o.set("resetplaybackrangeonclick", obj.resetPlaybackRangeOnClick); o.set("scrolloffsetx", obj.scrollOffsetX); o.set("scrolloffsety", obj.scrollOffsetY); o.set("scrollmode", obj.scrollMode as number); @@ -81,6 +83,15 @@ export class PlayerSettingsSerializer { case "enableuserinteraction": obj.enableUserInteraction = v! as boolean; return true; + case "enableseektoclick": + obj.enableSeekToClick = v! as boolean; + return true; + case "enableplaybackrangeselection": + obj.enablePlaybackRangeSelection = v! as boolean; + return true; + case "resetplaybackrangeonclick": + obj.resetPlaybackRangeOnClick = v! as boolean; + return true; case "scrolloffsetx": obj.scrollOffsetX = v! as number; return true; diff --git a/packages/alphatab/src/platform/javascript/AlphaTabApi.ts b/packages/alphatab/src/platform/javascript/AlphaTabApi.ts index cbacf1432..bee79a977 100644 --- a/packages/alphatab/src/platform/javascript/AlphaTabApi.ts +++ b/packages/alphatab/src/platform/javascript/AlphaTabApi.ts @@ -142,7 +142,8 @@ export class AlphaTabApi extends AlphaTabApiBase { settings.player.enableCursor = false; settings.player.playerMode = PlayerMode.Disabled; settings.player.enableElementHighlighting = false; - settings.player.enableUserInteraction = false; + settings.player.enableSeekToClick = false; + settings.player.enablePlaybackRangeSelection = false; settings.player.soundFont = null; settings.display.scale = 0.8; settings.display.stretchForce = 0.8;