From c70153af5e76ef7d0e2fbf7c6b3365ef8800a719 Mon Sep 17 00:00:00 2001
From: MysticILD <173185810+MysticILD@users.noreply.github.com>
Date: Sat, 16 May 2026 12:22:28 +0300
Subject: [PATCH 1/4] Add customizable Stop button behavior
---
OpenUtau.Core/Util/Preferences.cs | 5 ++
OpenUtauMobile/Assets/Lang/Strings.en.resx | 15 ++++
OpenUtauMobile/Assets/Lang/Strings.ja.resx | 15 ++++
OpenUtauMobile/Assets/Lang/Strings.ru.resx | 15 ++++
OpenUtauMobile/Assets/Lang/Strings.uk.resx | 15 ++++
.../Assets/Lang/Strings.zh-Hans.resx | 15 ++++
OpenUtauMobile/ViewModels/EditorViewModel.cs | 76 ++++++++++++++++++-
.../ViewModels/SettingsViewModel.cs | 52 +++++++++++++
OpenUtauMobile/Views/SettingsView.axaml | 26 +++++++
9 files changed, 233 insertions(+), 1 deletion(-)
diff --git a/OpenUtau.Core/Util/Preferences.cs b/OpenUtau.Core/Util/Preferences.cs
index 704f53c..201dd6a 100644
--- a/OpenUtau.Core/Util/Preferences.cs
+++ b/OpenUtau.Core/Util/Preferences.cs
@@ -307,6 +307,11 @@ public class SerializablePreferences {
/// 自动保存间隔,单位秒(仅在启用时生效)。
///
public int AutoSaveInterval = 120;
+ ///
+ /// Stop button behavior: 1 = StartTick -> SelectedPart -> 0 (default),
+ /// 2 = StartTick -> 0, 3 = Always 0.
+ ///
+ public int StopButtonBehavior = 1;
#endregion
}
}
diff --git a/OpenUtauMobile/Assets/Lang/Strings.en.resx b/OpenUtauMobile/Assets/Lang/Strings.en.resx
index dcf5941..ccb6ee2 100644
--- a/OpenUtauMobile/Assets/Lang/Strings.en.resx
+++ b/OpenUtauMobile/Assets/Lang/Strings.en.resx
@@ -276,6 +276,21 @@
Smooth Scrolling
+
+ Stop Button Behavior
+
+
+ Configure what happens when you press the stop button during and after playback.
+
+
+ Start Tick → Selected Part → 0 (Default)
+
+
+ Start Tick → 0
+
+
+ Always Go to 0
+
Render & Performance
diff --git a/OpenUtauMobile/Assets/Lang/Strings.ja.resx b/OpenUtauMobile/Assets/Lang/Strings.ja.resx
index 73abca1..eb06525 100644
--- a/OpenUtauMobile/Assets/Lang/Strings.ja.resx
+++ b/OpenUtauMobile/Assets/Lang/Strings.ja.resx
@@ -276,6 +276,21 @@
スムーズスクロール有効
+
+ 停止ボタンの動作
+
+
+ 再生中および再生後に停止ボタンを押したときの動作を設定します。
+
+
+ 開始位置 → 選択部分 → 0(デフォルト)
+
+
+ 開始位置 → 0
+
+
+ 常に 0 に移動
+
レンダーとパフォーマンス
diff --git a/OpenUtauMobile/Assets/Lang/Strings.ru.resx b/OpenUtauMobile/Assets/Lang/Strings.ru.resx
index 5395d71..e72197f 100644
--- a/OpenUtauMobile/Assets/Lang/Strings.ru.resx
+++ b/OpenUtauMobile/Assets/Lang/Strings.ru.resx
@@ -276,6 +276,21 @@
Плавная прокрутка
+
+ Поведение кнопки Стоп
+
+
+ Настройка поведения кнопки остановки во время и после воспроизведения.
+
+
+ Начало → Выделенный фрагмент → 0 (По умолчанию)
+
+
+ Начало → 0
+
+
+ Всегда к 0
+
Рендеринг и производительность
diff --git a/OpenUtauMobile/Assets/Lang/Strings.uk.resx b/OpenUtauMobile/Assets/Lang/Strings.uk.resx
index e36f194..493d062 100644
--- a/OpenUtauMobile/Assets/Lang/Strings.uk.resx
+++ b/OpenUtauMobile/Assets/Lang/Strings.uk.resx
@@ -276,6 +276,21 @@
Плавне прокручування
+
+ Поведінка кнопки Стоп
+
+
+ Налаштування поведінки кнопки зупинки під час та після відтворення.
+
+
+ Початок → Виділена партія → 0 (За замовчуванням)
+
+
+ Початок → 0
+
+
+ Завжди до 0
+
Рендеринг і продуктивність
diff --git a/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx b/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx
index 813ab8c..0d1c69f 100644
--- a/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx
+++ b/OpenUtauMobile/Assets/Lang/Strings.zh-Hans.resx
@@ -276,6 +276,21 @@
启用平滑自动翻页
+
+ 停止按钮行为
+
+
+ 设置在播放期间和播放后按下停止按钮时的行为。
+
+
+ 起始位置 → 选中部分 → 0(默认)
+
+
+ 起始位置 → 0
+
+
+ 始终跳转到 0
+
渲染与性能
diff --git a/OpenUtauMobile/ViewModels/EditorViewModel.cs b/OpenUtauMobile/ViewModels/EditorViewModel.cs
index 499756a..0eb6dc6 100644
--- a/OpenUtauMobile/ViewModels/EditorViewModel.cs
+++ b/OpenUtauMobile/ViewModels/EditorViewModel.cs
@@ -226,6 +226,8 @@ public class EditorViewModel : NavigateViewModelBase, ICmdSubscriber, IDisposabl
// 最近一次来自回放流的等待标记(SetPlayPosTickNotification.waitingRendering)
private bool _streamWaitingRender;
+ private int _stopSeekState;
+
// 随机数生成器
private Random Randomer { get; } = new();
@@ -254,13 +256,85 @@ public EditorViewModel(MainViewModel navigator, string path = "") : base(navigat
{
DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(PlaybackManager.Inst.StartTick, true));
}
+
+ if (PlaybackManager.Inst.PlayingMaster || PlaybackManager.Inst.StartingToPlay)
+ {
+ _stopSeekState = 0;
+ }
}).DisposeWith(_disposables);
// 停止命令
StopCommand = ReactiveCommand.Create(() =>
{
+ bool wasPlaying = PlaybackManager.Inst.OutputActive || PlaybackManager.Inst.PlayingMaster;
PlaybackManager.Inst.StopPlayback();
SyncPlaybackStateFromAuthority();
- DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
+
+ int mode = Preferences.Default.StopButtonBehavior;
+ if (mode <= 0 || mode > 3) mode = 1;
+
+ if (wasPlaying)
+ {
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(PlaybackManager.Inst.StartTick));
+ _stopSeekState = 1;
+ }
+ else
+ {
+ switch (mode)
+ {
+ case 2:
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
+ _stopSeekState = 3;
+ break;
+ case 3:
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
+ break;
+ default:
+ if (_stopSeekState <= 0)
+ {
+ int? selectedTick = null;
+ if (SelectedParts is { Count: > 0 } parts)
+ {
+ selectedTick = parts.Min(p => p.position);
+ }
+
+ if (selectedTick.HasValue)
+ {
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(selectedTick.Value));
+ _stopSeekState = 2;
+ }
+ else
+ {
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
+ _stopSeekState = 3;
+ }
+ }
+ else if (_stopSeekState == 1)
+ {
+ int? selectedTick = null;
+ if (SelectedParts is { Count: > 0 } parts)
+ {
+ selectedTick = parts.Min(p => p.position);
+ }
+
+ if (selectedTick.HasValue)
+ {
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(selectedTick.Value));
+ _stopSeekState = 2;
+ }
+ else
+ {
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
+ _stopSeekState = 3;
+ }
+ }
+ else
+ {
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
+ _stopSeekState = 3;
+ }
+ break;
+ }
+ }
}).DisposeWith(_disposables);
// 工程信息编辑命令
EditBpmCommand = ReactiveCommand.CreateFromTask(EditBpmAsync);
diff --git a/OpenUtauMobile/ViewModels/SettingsViewModel.cs b/OpenUtauMobile/ViewModels/SettingsViewModel.cs
index 91d1935..9869ebd 100644
--- a/OpenUtauMobile/ViewModels/SettingsViewModel.cs
+++ b/OpenUtauMobile/ViewModels/SettingsViewModel.cs
@@ -81,6 +81,25 @@ public PianoKeyBehaviorOption(PianoKeyBehavior value, string displayName)
}
}
+public enum StopButtonBehavior
+{
+ StartTickSelectedPartZero = 1,
+ StartTickZero = 2,
+ AlwaysZero = 3
+}
+
+public class StopButtonBehaviorOption
+{
+ public StopButtonBehavior Value { get; }
+ public string DisplayName { get; }
+
+ public StopButtonBehaviorOption(StopButtonBehavior value, string displayName)
+ {
+ Value = value;
+ DisplayName = displayName;
+ }
+}
+
/// 走带自动翻页行为选项(用于绑定到选择器)。
public class AutoScrollBehaviorOption
{
@@ -328,6 +347,20 @@ public class SettingsViewModel : NavigateViewModelBase, IDisposable
[Reactive]
public AutoScrollBehaviorOption? SelectedAutoScrollBehavior { get; set; }
+ // ── Edit & Behaviour: Stop Button ──────────────────────────────
+ /// List of available stop button actions.
+ public IReadOnlyList AvailableStopButtonBehaviors { get; } =
+ new List
+ {
+ new(StopButtonBehavior.StartTickSelectedPartZero, L.S("Settings.StopButton.StartTickSelectedPartZero")),
+ new(StopButtonBehavior.StartTickZero, L.S("Settings.StopButton.StartTickZero")),
+ new(StopButtonBehavior.AlwaysZero, L.S("Settings.StopButton.AlwaysZero"))
+ };
+
+ /// The currently selected behavior option for the Stop button.
+ [Reactive]
+ public StopButtonBehaviorOption? SelectedStopButtonBehavior { get; set; }
+
/// 回放刷新率(1-60)。
[Reactive]
public int PlaybackRefreshRate { get; set; }
@@ -551,6 +584,25 @@ await Task.Run(() =>
})
.DisposeWith(_disposables);
+ // Initializing Stop Button Behavior
+ int stopBehaviorVal = Preferences.Default.StopButtonBehavior;
+ StopButtonBehavior stopBehavior = stopBehaviorVal is >= 1 and <= 3
+ ? (StopButtonBehavior)stopBehaviorVal
+ : StopButtonBehavior.StartTickSelectedPartZero;
+ SelectedStopButtonBehavior = AvailableStopButtonBehaviors.FirstOrDefault(b => b.Value == stopBehavior)
+ ?? AvailableStopButtonBehaviors[0];
+
+ // Monitor changes in the behavior of the Stop button and persist them to Preferences
+ this.WhenAnyValue(x => x.SelectedStopButtonBehavior)
+ .Skip(1)
+ .WhereNotNull()
+ .Subscribe(opt =>
+ {
+ Preferences.Default.StopButtonBehavior = (int)opt.Value;
+ Preferences.Save();
+ })
+ .DisposeWith(_disposables);
+
// 编辑与行为:回放刷新率 / 立绘开关 / 撤销上限
PlaybackRefreshRate = Math.Clamp((int)Math.Round(Preferences.Default.PlaybackRefreshRate), 1, 60);
ShowPortraitEnabled = Preferences.Default.ShowPortrait;
diff --git a/OpenUtauMobile/Views/SettingsView.axaml b/OpenUtauMobile/Views/SettingsView.axaml
index 85f8470..fd99bf6 100644
--- a/OpenUtauMobile/Views/SettingsView.axaml
+++ b/OpenUtauMobile/Views/SettingsView.axaml
@@ -468,6 +468,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From 2e21f83c2fefdc659c74a904d3cdfb23a6d51dd8 Mon Sep 17 00:00:00 2001
From: MysticILD <173185810+MysticILD@users.noreply.github.com>
Date: Sat, 16 May 2026 15:23:49 +0300
Subject: [PATCH 2/4] Update Settings layout
---
OpenUtauMobile/Views/SettingsView.axaml | 554 ++++++++++--------------
1 file changed, 221 insertions(+), 333 deletions(-)
diff --git a/OpenUtauMobile/Views/SettingsView.axaml b/OpenUtauMobile/Views/SettingsView.axaml
index fd99bf6..a5c2003 100644
--- a/OpenUtauMobile/Views/SettingsView.axaml
+++ b/OpenUtauMobile/Views/SettingsView.axaml
@@ -15,20 +15,13 @@
-
-
-
-
-
@@ -58,7 +49,6 @@
-
@@ -89,15 +79,12 @@
-
-
-
-
-
-
-
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
@@ -426,7 +397,6 @@
Margin="12,0,0,0"
VerticalAlignment="Center" />
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
+
+
-
+ Opacity="0.65" />
+
-
+
-
-
+
+
-
-
+
-
+
-
-
-
+
+
-
+ Opacity="0.65" />
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
+
+
-
+ Opacity="0.65" />
+
-
-
-
-
-
+
+
-
+ Opacity="0.65" />
+
-
-
-
-
-
+
+
-
+ Opacity="0.65" />
+
-
+
-
-
+
+
-
@@ -715,61 +646,45 @@
-
-
+
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
-
+ Opacity="0.65" />
+
-
+
-
-
-
+
+
+ FontSize="14"
+ FontWeight="SemiBold" />
-
@@ -809,33 +720,27 @@
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -848,7 +753,6 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
-
-
-
+
+
+
+
+
+ HorizontalAlignment="Left" />
-
+
-
+
+
-
-
+
+
-
@@ -961,31 +858,27 @@
-
+
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
@@ -1058,19 +948,17 @@
-
+
+
-
-
+
+
-
@@ -1079,7 +967,7 @@
-
+
From b5f6fa3ecf85cb9a4e6251aa99a2442ddfe7bc2f Mon Sep 17 00:00:00 2001
From: MysticILD <173185810+MysticILD@users.noreply.github.com>
Date: Sat, 16 May 2026 15:27:05 +0300
Subject: [PATCH 3/4] Update Strings.ru.resx
---
OpenUtauMobile/Assets/Lang/Strings.ru.resx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/OpenUtauMobile/Assets/Lang/Strings.ru.resx b/OpenUtauMobile/Assets/Lang/Strings.ru.resx
index e72197f..a19e3eb 100644
--- a/OpenUtauMobile/Assets/Lang/Strings.ru.resx
+++ b/OpenUtauMobile/Assets/Lang/Strings.ru.resx
@@ -283,7 +283,7 @@
Настройка поведения кнопки остановки во время и после воспроизведения.
- Начало → Выделенный фрагмент → 0 (По умолчанию)
+ Начало → Выделенная партия → 0 (По умолчанию)
Начало → 0
From a3bd67cc75cddcb95178993fedba9622072c0f4b Mon Sep 17 00:00:00 2001
From: MysticILD <173185810+MysticILD@users.noreply.github.com>
Date: Tue, 30 Jun 2026 19:32:50 +0300
Subject: [PATCH 4/4] Fix Stop button behavior
---
OpenUtauMobile/ViewModels/EditorViewModel.cs | 116 ++++++++++++-------
1 file changed, 72 insertions(+), 44 deletions(-)
diff --git a/OpenUtauMobile/ViewModels/EditorViewModel.cs b/OpenUtauMobile/ViewModels/EditorViewModel.cs
index 0eb6dc6..9597253 100644
--- a/OpenUtauMobile/ViewModels/EditorViewModel.cs
+++ b/OpenUtauMobile/ViewModels/EditorViewModel.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -227,6 +227,8 @@ public class EditorViewModel : NavigateViewModelBase, ICmdSubscriber, IDisposabl
private bool _streamWaitingRender;
private int _stopSeekState;
+ private bool _isSeekingInternally = false;
+ private bool _playheadMovedSinceStart = false;
// 随机数生成器
private Random Randomer { get; } = new();
@@ -270,69 +272,84 @@ public EditorViewModel(MainViewModel navigator, string path = "") : base(navigat
SyncPlaybackStateFromAuthority();
int mode = Preferences.Default.StopButtonBehavior;
- if (mode <= 0 || mode > 3) mode = 1;
+ if (mode <= 0 || mode > 3)
+ {
+ mode = 1;
+ }
+
+ if (mode == 3)
+ {
+ _isSeekingInternally = true;
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
+ _stopSeekState = 3;
+ _playheadMovedSinceStart = false;
+ return;
+ }
if (wasPlaying)
{
+ _isSeekingInternally = true;
DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(PlaybackManager.Inst.StartTick));
_stopSeekState = 1;
+ _playheadMovedSinceStart = false;
}
else
{
- switch (mode)
+ if (_stopSeekState == 0 && !_playheadMovedSinceStart)
{
- case 2:
- DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
- _stopSeekState = 3;
- break;
- case 3:
- DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
- break;
- default:
- if (_stopSeekState <= 0)
- {
- int? selectedTick = null;
- if (SelectedParts is { Count: > 0 } parts)
- {
- selectedTick = parts.Min(p => p.position);
- }
-
- if (selectedTick.HasValue)
+ _isSeekingInternally = true;
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(PlaybackManager.Inst.StartTick));
+ _stopSeekState = 1;
+ }
+ else
+ {
+ switch (mode)
+ {
+ case 2:
+ _isSeekingInternally = true;
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
+ _stopSeekState = 3;
+ break;
+ case 3:
+ _isSeekingInternally = true;
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
+ break;
+ default:
+ if (_stopSeekState <= 1)
{
- DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(selectedTick.Value));
- _stopSeekState = 2;
+ int? selectedTick = null;
+ if (SelectedParts is { Count: > 0 } parts)
+ {
+ selectedTick = parts.Min(p => p.position);
+ }
+
+ if (selectedTick.HasValue && selectedTick.Value != PlayPosTick)
+ {
+ _isSeekingInternally = true;
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(selectedTick.Value));
+ _stopSeekState = 2;
+ }
+ else
+ {
+ _isSeekingInternally = true;
+ DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
+ _stopSeekState = 3;
+ }
}
- else
+ else if (_stopSeekState == 2)
{
+ _isSeekingInternally = true;
DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
_stopSeekState = 3;
}
- }
- else if (_stopSeekState == 1)
- {
- int? selectedTick = null;
- if (SelectedParts is { Count: > 0 } parts)
- {
- selectedTick = parts.Min(p => p.position);
- }
-
- if (selectedTick.HasValue)
- {
- DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(selectedTick.Value));
- _stopSeekState = 2;
- }
else
{
+ _isSeekingInternally = true;
DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
_stopSeekState = 3;
}
- }
- else
- {
- DocManager.Inst.ExecuteCmd(new SeekPlayPosTickNotification(0));
- _stopSeekState = 3;
- }
- break;
+ break;
+ }
}
}
}).DisposeWith(_disposables);
@@ -466,6 +483,17 @@ public void OnNext(UCommand cmd, bool isUndo)
// TODO: 一定要发到UI线程吗?:不需要!docman已经做了
switch (cmd)
{
+ case SeekPlayPosTickNotification _:
+ if (_isSeekingInternally)
+ {
+ _isSeekingInternally = false;
+ }
+ else
+ {
+ _playheadMovedSinceStart = true;
+ _stopSeekState = 0;
+ }
+ break;
case SetPlayPosTickNotification setPlayPos:
// 等待渲染期间冻结回放标记,避免位置在等待点附近抖动漂移。
if (!setPlayPos.waitingRendering)