mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 10:21:18 +03:00
* Added base type for UI panels of MIDI input sources. * Renamed BandMenu into BandMidiSource. * Implemented the UI overhaul, skipping channels dialog functionality for now. * Refactored for conventions and reordered type members. Got rid of SetInstrument/ClearInstrument again. * Decreased background opacity for UnavailableOverlay to improve readability. * Localization * Added text to InputMidiSource so players know what it does. * Changed window MinSize to SetSize. Allows players to reduce window size if desired. * Improved button looks by adding style classes. * cleanup * leftover cleanup * Reverted fetching InstrumentSystem via IoCManager (crashes it, woops). * added filter bar to file list * refactored almost all system/manager interaction into instrument BUI * text changed for instruments-component-menu-input-notice-label * fixed InstrumentBoundUserInterface.Dispose not unsubscribing from everything * minor cleanup * finished configuration menu & cleanup * Comments and fixes for InstrumentMidiSourceBase * Fixed RA0051 * Removed unncecessary null check * Added methods for accessing the selected track. * Limited depth of search for instrument master to prevent deadlock. * Cached system access * Storing midis asynchronously * Actual MIDI data now only loaded before playback. * async removed from InstrumentBoundUserInterface.LoadStoredUserMidis * Replaced GetComponent calls with TryGetComponent * Localized MIDI playback track timer * Searching for next track using raw index now * Fixed CS0168 * XML comments, minor fixes and disabled MIDI library management while a track is playing. * removed timers with simple watchdog and switched to MidiFileCollectionManager * Missed one (timer) * Changed access for MidiFileCollectionManager.UserMidiDirectory field to public. * Added instrument config control for MIDI collection utilities. * redesigned instrument config menu * Removed rename function, updated adding implementation * more redesigns * fixed _isMidiFileDialogueWindowOpen not resetting correctly in certain cases. * file scoped namespaces * replaced EntityUid with Entity<InstrumentComponent> * if spaces * returning Tooltip instead of Control * replaced SetEntity with public field * readonly system fields * InstrumentMenu MinSize * xml comment for InstrumentMidiSourceBase * small refactor * more cleanup * TryGetComponent to GetComponent on Open() * more formatting issues * switched to [Dependency] for systems * playback improvements * disable playback button if no file selected and playing * activating shuffle now selects track if none is selected * rearranged playback buttons * channels overhauled * Play/Stop label to Play only * Removed all channel on/off * Program now default channel name display selection * improved RemoveButton.Disable behavior * channels now toggable by clicking anywhere * Shuffle no longer selects already selected track. * Loop & Shuffle no longer both selectable at once * minor improvements to channel item visuals * minor channel toggle fixes * xml docs & code cleanup * `RefreshBandRequest` now sent automatically when band source becomes visible * converted 5 items tuple to MidiChannelInfo record * added markup support to instrument menu configuration header * added note to midi channels config header explaining update behavior.
463 lines
15 KiB
C#
463 lines
15 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Content.Client.Audio.Midi;
|
|
using Robust.Client.AutoGenerated;
|
|
using Robust.Client.UserInterface;
|
|
using Robust.Client.UserInterface.Controls;
|
|
using Robust.Client.UserInterface.CustomControls;
|
|
using Robust.Client.UserInterface.XAML;
|
|
using Robust.Shared.Input;
|
|
using Robust.Shared.Random;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
using static Robust.Client.UserInterface.Controls.BaseButton;
|
|
using Range = Robust.Client.UserInterface.Controls.Range;
|
|
|
|
namespace Content.Client.Instruments.UI;
|
|
|
|
[GenerateTypedNameReferences]
|
|
public sealed partial class FileMidiSource : InstrumentMidiSourceBase
|
|
{
|
|
private const double RecoveryGracePeriod = 2.0;
|
|
|
|
[Dependency] private IRobustRandom _random = default!;
|
|
[Dependency] private MidiFileCollectionManager _midiCollection = default!;
|
|
[Dependency] private IFileDialogManager _dialogs = default!;
|
|
[Dependency] private IUserInterfaceManager _userInterfaceManager = default!;
|
|
|
|
/// <summary>
|
|
/// Raised when MIDI data should be played.
|
|
/// </summary>
|
|
public event Action<byte[]>? StartPlayingRequest;
|
|
|
|
/// <summary>
|
|
/// Raised when any currently playing MIDI track should be stopped.
|
|
/// </summary>
|
|
public event Action? StopPlayingRequest;
|
|
|
|
/// <summary>
|
|
/// Raised when the track position on the currently playing MIDI track should change.
|
|
/// </summary>
|
|
public event Action<int>? TrackPositionChangeRequest;
|
|
|
|
/// <summary>
|
|
/// Raised when looping MIDI tracks is toggled.
|
|
/// </summary>
|
|
public event Action<bool>? LoopingToggled;
|
|
|
|
private readonly List<ResPath> _loadedTracks = [];
|
|
private bool _isMidiFileDialogueWindowOpen;
|
|
private bool _selectedInternally;
|
|
private float _timeSinceLastRecoverAttempt;
|
|
|
|
/// <summary>
|
|
/// The Instrument playing the tracks. This is needed for the UI to read out current track time.
|
|
/// on <see cref="FrameUpdate" />.
|
|
/// </summary>
|
|
public Entity<InstrumentComponent> Instrument;
|
|
|
|
private bool IsShuffle => ShuffleButton.Pressed;
|
|
private string CurrentFilter => FilterBar.Text;
|
|
|
|
/// <inheritdoc />
|
|
public override string ButtonName => Loc.GetString("instruments-component-menu-file-midi-source-button");
|
|
|
|
/// <summary>
|
|
/// Controls if MIDI should currently be played. This is connected to the play button.
|
|
/// </summary>
|
|
public bool IsPlaying
|
|
{
|
|
get => PlayButton.Pressed;
|
|
set => PlayButton.Pressed = value;
|
|
}
|
|
|
|
public FileMidiSource()
|
|
{
|
|
RobustXamlLoader.Load(this);
|
|
IoCManager.InjectDependencies(this);
|
|
|
|
CurrentTrackLabel.Text = string.Empty;
|
|
|
|
_midiCollection.MidiFileAdded += AddTrack;
|
|
_midiCollection.MidiFileRemoved += RemoveTrack;
|
|
_midiCollection.MidiFilesReset += ReloadTrackList;
|
|
|
|
FilterBar.OnTextChanged += OnFilterBarTextChanged;
|
|
TrackList.OnItemSelected += OnTrackListItemSelected;
|
|
TrackList.OnItemDeselected += OnTrackListItemDeselected;
|
|
AddButton.OnPressed += OnAddButtonPressed;
|
|
RemoveButton.OnPressed += OnRemoveButtonPressed;
|
|
PlayButton.OnToggled += OnPlayButtonToggled;
|
|
LoopButton.OnToggled += OnLoopButtonToggled;
|
|
ShuffleButton.OnPressed += OnShuffleButtonPressed;
|
|
PlaybackSlider.OnValueChanged += OnPlaybackSliderValueChanged;
|
|
PlaybackSlider.OnKeyBindUp += OnPlaybackSliderKeyBindUp;
|
|
|
|
RemoveButton.TooltipSupplier = SupplyFileManagementTooltip;
|
|
ReloadTrackList();
|
|
}
|
|
|
|
protected override void FrameUpdate(FrameEventArgs args)
|
|
{
|
|
base.FrameUpdate(args);
|
|
|
|
if (PlaybackSlider.Grabbed)
|
|
return;
|
|
|
|
// Disable the playback button when nothing is selected and playing.
|
|
PlayButton.Disabled = !TrackList.GetSelected().Any() && !Instrument.Comp.IsRendererAlive;
|
|
|
|
if (!Instrument.Comp.IsMidiOpen)
|
|
{
|
|
ResetTrackIndicators();
|
|
PlaybackSlider.Disabled = true;
|
|
|
|
// My patience with the InstrumentSystem ran out, the system will be asked to play tracks until it behaves.
|
|
if (!IsPlaying)
|
|
return;
|
|
|
|
_timeSinceLastRecoverAttempt += args.DeltaSeconds;
|
|
if (!(_timeSinceLastRecoverAttempt > RecoveryGracePeriod))
|
|
return;
|
|
|
|
_timeSinceLastRecoverAttempt = 0;
|
|
ReplayCurrentTrack();
|
|
}
|
|
else if (Instrument.Comp.IsRendererAlive)
|
|
{
|
|
PlaybackSlider.Disabled = false;
|
|
PlaybackSlider.MaxValue = Instrument.Comp.PlayerTotalTick;
|
|
PlaybackSlider.SetValueWithoutEvent(Instrument.Comp.PlayerTick);
|
|
// TODO: SequencerTimeScale does not return the actual ticks/seconds, find solution.
|
|
var totalTime =
|
|
TimeSpan.FromSeconds(Math.Ceiling(Instrument.Comp.PlayerTotalTick /
|
|
Instrument.Comp.Renderer!.SequencerTimeScale));
|
|
var currentTime =
|
|
TimeSpan.FromSeconds(Math.Ceiling(Instrument.Comp.PlayerTick /
|
|
Instrument.Comp.Renderer!.SequencerTimeScale));
|
|
TimeLabel.Text = totalTime.Hours < 1
|
|
? Loc.GetString(
|
|
"instruments-component-menu-files-track-playtime-seconds-minutes",
|
|
("currentMinutes", currentTime.ToString("mm")),
|
|
("currentSeconds", currentTime.ToString("ss")),
|
|
("totalMinutes", totalTime.ToString("mm")),
|
|
("totalSeconds", totalTime.ToString("ss")))
|
|
: Loc.GetString(
|
|
"instruments-component-menu-files-track-playtime-seconds-minutes-hours",
|
|
("currentHours", currentTime.ToString("hh")),
|
|
("currentMinutes", currentTime.ToString("mm")),
|
|
("currentSeconds", currentTime.ToString("ss")),
|
|
("totalHours", totalTime.ToString("hh")),
|
|
("totalMinutes", totalTime.ToString("mm")),
|
|
("totalSeconds", totalTime.ToString("ss")));
|
|
}
|
|
}
|
|
|
|
public override void Disable()
|
|
{
|
|
base.Disable();
|
|
TrackList.ClearSelected();
|
|
IsPlaying = false;
|
|
StopPlayingRequest?.Invoke();
|
|
CurrentTrackLabel.Text = string.Empty;
|
|
}
|
|
|
|
private void OnFilterBarTextChanged(LineEdit.LineEditEventArgs obj)
|
|
{
|
|
FilterTrackList();
|
|
}
|
|
|
|
private void OnTrackListItemSelected(ItemList.ItemListSelectedEventArgs obj)
|
|
{
|
|
UpdateFileRemoveButton();
|
|
if (!_selectedInternally)
|
|
{
|
|
IsPlaying = false;
|
|
StopPlaying();
|
|
}
|
|
else
|
|
{
|
|
PlayTrackListItem(obj.ItemIndex);
|
|
}
|
|
}
|
|
|
|
private void OnTrackListItemDeselected(ItemList.ItemListDeselectedEventArgs obj)
|
|
{
|
|
RemoveButton.Disabled = true;
|
|
}
|
|
|
|
private async void OnAddButtonPressed(ButtonEventArgs obj)
|
|
{
|
|
try
|
|
{
|
|
if (_isMidiFileDialogueWindowOpen)
|
|
return;
|
|
|
|
var filters = new FileDialogFilters(new FileDialogFilters.Group("mid", "midi"));
|
|
|
|
// TODO: Once the file dialogue manager can handle focusing or closing windows, improve this logic to close
|
|
// or focus the previously-opened window.
|
|
_isMidiFileDialogueWindowOpen = true;
|
|
var (stream, name) = await _dialogs.GetFileAndName(filters, FileAccess.Read);
|
|
await using (stream)
|
|
{
|
|
// did the instrument menu get closed while waiting for the user to select a file?
|
|
if (Disposed)
|
|
return;
|
|
|
|
if (stream == null)
|
|
return;
|
|
|
|
if (name != null)
|
|
await _midiCollection.AddMidiFile(new ResPath(name), stream);
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
_userInterfaceManager.Popup(Loc.GetString("instruments-component-menu-files-error"));
|
|
}
|
|
finally
|
|
{
|
|
_isMidiFileDialogueWindowOpen = false;
|
|
}
|
|
}
|
|
|
|
private void OnRemoveButtonPressed(ButtonEventArgs obj)
|
|
{
|
|
if (TryGetSelectedTrackPath(out var trackPath) && trackPath is { } resPath)
|
|
_midiCollection.RemoveMidiFile(resPath);
|
|
UpdateFileRemoveButton();
|
|
}
|
|
|
|
private void OnPlayButtonToggled(ButtonToggledEventArgs obj)
|
|
{
|
|
UpdateFileRemoveButton();
|
|
if (PlayButton.Pressed)
|
|
{
|
|
if (!TryGetSelectedTrack(out var track))
|
|
return;
|
|
|
|
StartPlaying(track);
|
|
}
|
|
else
|
|
{
|
|
StopPlaying();
|
|
}
|
|
}
|
|
|
|
private void OnLoopButtonToggled(ButtonToggledEventArgs obj)
|
|
{
|
|
if (LoopButton.Pressed && ShuffleButton.Pressed)
|
|
ShuffleButton.Pressed = false;
|
|
|
|
LoopingToggled?.Invoke(LoopButton.Pressed);
|
|
}
|
|
|
|
private void OnShuffleButtonPressed(ButtonEventArgs obj)
|
|
{
|
|
if (ShuffleButton.Pressed && LoopButton.Pressed)
|
|
{
|
|
LoopingToggled?.Invoke(false);
|
|
LoopButton.Pressed = false;
|
|
}
|
|
|
|
if (obj.Button.Pressed && !TrackList.GetSelected().Any())
|
|
TrackList[_random.Next(0, TrackList.Count)].Selected = true;
|
|
}
|
|
|
|
private void OnPlaybackSliderValueChanged(Range _)
|
|
{
|
|
// Do not seek while still grabbing.
|
|
if (PlaybackSlider.Grabbed)
|
|
return;
|
|
|
|
TrackPositionChangeRequest?.Invoke((int)Math.Ceiling(PlaybackSlider.Value));
|
|
}
|
|
|
|
private void OnPlaybackSliderKeyBindUp(GUIBoundKeyEventArgs obj)
|
|
{
|
|
if (obj.Function != EngineKeyFunctions.UIClick)
|
|
return;
|
|
|
|
TrackPositionChangeRequest?.Invoke((int)Math.Ceiling(PlaybackSlider.Value));
|
|
}
|
|
|
|
private Tooltip? SupplyFileManagementTooltip(Control? sender)
|
|
{
|
|
if (sender is not Button { Disabled: true })
|
|
return null;
|
|
|
|
var tooltipText = Loc.GetString("instruments-component-menu-files-file-remove-disabled-info");
|
|
var tooltip = new Tooltip();
|
|
tooltip.SetMessage(FormattedMessage.FromUnformatted(tooltipText));
|
|
return tooltip;
|
|
}
|
|
|
|
private void UpdateFileRemoveButton()
|
|
{
|
|
RemoveButton.Disabled = !TrackList.GetSelected().Any() || IsPlaying;
|
|
}
|
|
|
|
private void ResetTrackIndicators()
|
|
{
|
|
PlaybackSlider.MaxValue = 1;
|
|
PlaybackSlider.SetValueWithoutEvent(0);
|
|
CurrentTrackLabel.Text = string.Empty;
|
|
TimeLabel.Text = "--.--/--.--";
|
|
}
|
|
|
|
private void PlayTrackListItem(int index)
|
|
{
|
|
if (index >= TrackList.Count || index < 0)
|
|
return;
|
|
|
|
if (TrackList[index] is not { } item)
|
|
return;
|
|
|
|
StartPlaying(item);
|
|
}
|
|
|
|
private void StopPlaying()
|
|
{
|
|
StopPlayingRequest?.Invoke();
|
|
ResetTrackIndicators();
|
|
UpdateFileRemoveButton();
|
|
}
|
|
|
|
private void StartPlaying(ItemList.Item item)
|
|
{
|
|
if (item.Metadata is not ResPath resPath)
|
|
return;
|
|
|
|
if (item.Text == null)
|
|
return;
|
|
|
|
_timeSinceLastRecoverAttempt = 0;
|
|
var midiData = _midiCollection.GetMidiData(resPath);
|
|
StartPlayingRequest?.Invoke(midiData);
|
|
CurrentTrackLabel.Text = item.Text;
|
|
UpdateFileRemoveButton();
|
|
}
|
|
|
|
private void FilterTrackList()
|
|
{
|
|
var filterString = CurrentFilter.Trim().ToLowerInvariant();
|
|
TrackList.Clear();
|
|
|
|
foreach (var track in _loadedTracks)
|
|
{
|
|
var trackName = track.FilenameWithoutExtension.Trim().ToLowerInvariant();
|
|
if (trackName.Contains(filterString))
|
|
TrackList.AddItem(track.FilenameWithoutExtension, null, true, track);
|
|
}
|
|
}
|
|
|
|
private bool TryGetSelectedTrackPath([NotNullWhen(true)] out ResPath? trackPath)
|
|
{
|
|
trackPath = null;
|
|
if (!TryGetSelectedTrack(out var item) || item.Metadata is not ResPath resPath)
|
|
return false;
|
|
|
|
trackPath = resPath;
|
|
return true;
|
|
}
|
|
|
|
private bool TryGetSelectedTrack([NotNullWhen(true)] out ItemList.Item? item)
|
|
{
|
|
return TrackList.GetSelected().TryFirstOrDefault(out item);
|
|
}
|
|
|
|
private void AddTrack(ResPath trackPath)
|
|
{
|
|
_loadedTracks.Add(trackPath);
|
|
FilterTrackList();
|
|
}
|
|
|
|
private void RemoveTrack(ResPath trackPath)
|
|
{
|
|
_loadedTracks.Remove(trackPath);
|
|
FilterTrackList();
|
|
}
|
|
|
|
private void ReloadTrackList()
|
|
{
|
|
_loadedTracks.Clear();
|
|
_loadedTracks.AddRange(_midiCollection.GetMidiFiles());
|
|
FilterTrackList();
|
|
}
|
|
|
|
private void ReplayCurrentTrack()
|
|
{
|
|
if (!TryGetSelectedTrack(out var track))
|
|
return;
|
|
|
|
StartPlaying(track);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Selects the next MIDI track from the list according to shuffle and filter settings.
|
|
/// </summary>
|
|
/// <remarks>Selecting another track causes its playback if <see cref="IsPlaying" /> is true.</remarks>
|
|
public void SelectNextTrack()
|
|
{
|
|
// Only proceed if file panel is active.
|
|
if (!Enabled)
|
|
return;
|
|
|
|
// Only proceed if the play button is pressed.
|
|
if (!IsPlaying)
|
|
return;
|
|
|
|
// List is empty, return
|
|
if (TrackList.Count == 0)
|
|
return;
|
|
|
|
_selectedInternally = true;
|
|
try
|
|
{
|
|
if (IsShuffle)
|
|
{
|
|
var randomIndex = _random.Next(0, TrackList.Count);
|
|
var randomTrack = TrackList[randomIndex];
|
|
if (randomTrack.Selected)
|
|
{
|
|
// Try going one up or down if we roll the already selected track.
|
|
if (TrackList.Count - 1 > randomIndex)
|
|
TrackList[randomIndex + 1].Selected = true;
|
|
else if (randomIndex > 0)
|
|
TrackList[randomIndex - 1].Selected = true;
|
|
else
|
|
randomTrack.Selected = true;
|
|
}
|
|
else
|
|
{
|
|
randomTrack.Selected = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!TryGetSelectedTrack(out var item))
|
|
{
|
|
TrackList[0].Selected = true;
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < TrackList.Count - 1; i++)
|
|
{
|
|
if (TrackList[i] != item)
|
|
continue;
|
|
TrackList[i + 1].Selected = true;
|
|
return;
|
|
}
|
|
|
|
TrackList[0].Selected = false;
|
|
TrackList[0].Selected = true;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_selectedInternally = false;
|
|
}
|
|
}
|
|
}
|