feat: align legacy operator and playout behavior
This commit is contained in:
@@ -33,6 +33,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
private long _draftSequence;
|
||||
private int _domesticSearchVersion;
|
||||
private int _worldSearchVersion;
|
||||
private int _activationGeneration;
|
||||
private bool _isBusy;
|
||||
|
||||
public LegacyComparisonWorkflowController(
|
||||
@@ -45,12 +46,44 @@ public sealed class LegacyComparisonWorkflowController
|
||||
|
||||
public LegacyComparisonWorkflowSnapshot Current => CreateSnapshot();
|
||||
|
||||
/// <summary>
|
||||
/// Recreates UC3's per-control state before its saved CP949 pairs are loaded for
|
||||
/// a new tab activation. Search and pair-load generations invalidate completions
|
||||
/// that belong to the discarded control instance.
|
||||
/// </summary>
|
||||
public LegacyComparisonWorkflowSnapshot ResetForTabActivation()
|
||||
{
|
||||
Interlocked.Increment(ref _activationGeneration);
|
||||
Interlocked.Increment(ref _domesticSearchVersion);
|
||||
Interlocked.Increment(ref _worldSearchVersion);
|
||||
_domesticResults = [];
|
||||
_worldResults = [];
|
||||
_savedPairs = [];
|
||||
_firstTarget = null;
|
||||
_secondTarget = null;
|
||||
_selectedDomesticResultId = null;
|
||||
_selectedWorldResultId = null;
|
||||
_selectedFixedTargetId = null;
|
||||
_selectedPairId = null;
|
||||
_domesticQuery = string.Empty;
|
||||
_worldQuery = string.Empty;
|
||||
_domesticError = string.Empty;
|
||||
_worldError = string.Empty;
|
||||
_statusMessage = string.Empty;
|
||||
_domesticStatus = LegacyComparisonSearchStatus.Idle;
|
||||
_worldStatus = LegacyComparisonSearchStatus.Idle;
|
||||
_statusKind = LegacyOperatorStatusKind.Neutral;
|
||||
_isBusy = false;
|
||||
Touch();
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
public async Task<LegacyComparisonWorkflowSnapshot> SearchDomesticAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedQuery = NormalizeSearchQuery(query);
|
||||
var version = ++_domesticSearchVersion;
|
||||
var version = Interlocked.Increment(ref _domesticSearchVersion);
|
||||
_domesticQuery = normalizedQuery;
|
||||
_domesticStatus = LegacyComparisonSearchStatus.Loading;
|
||||
_domesticError = string.Empty;
|
||||
@@ -64,7 +97,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
normalizedQuery,
|
||||
LegacyComparisonWorkflowCatalog.DomesticMaximumResults,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (version != _domesticSearchVersion)
|
||||
if (version != Volatile.Read(ref _domesticSearchVersion))
|
||||
{
|
||||
return CreateSnapshot();
|
||||
}
|
||||
@@ -81,7 +114,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (version == _domesticSearchVersion)
|
||||
if (version == Volatile.Read(ref _domesticSearchVersion))
|
||||
{
|
||||
_domesticStatus = LegacyComparisonSearchStatus.Idle;
|
||||
_domesticError = string.Empty;
|
||||
@@ -92,7 +125,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (version == _domesticSearchVersion)
|
||||
if (version == Volatile.Read(ref _domesticSearchVersion))
|
||||
{
|
||||
_domesticResults = [];
|
||||
_domesticStatus = LegacyComparisonSearchStatus.Error;
|
||||
@@ -110,7 +143,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
{
|
||||
// UC3's blank world query loaded the complete US/TW universe, so blank remains valid.
|
||||
var normalizedQuery = NormalizeSearchQuery(query);
|
||||
var version = ++_worldSearchVersion;
|
||||
var version = Interlocked.Increment(ref _worldSearchVersion);
|
||||
_worldQuery = normalizedQuery;
|
||||
_worldStatus = LegacyComparisonSearchStatus.Loading;
|
||||
_worldError = string.Empty;
|
||||
@@ -124,7 +157,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
normalizedQuery,
|
||||
LegacyComparisonWorkflowCatalog.WorldMaximumResults,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
if (version != _worldSearchVersion)
|
||||
if (version != Volatile.Read(ref _worldSearchVersion))
|
||||
{
|
||||
return CreateSnapshot();
|
||||
}
|
||||
@@ -141,7 +174,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
if (version == _worldSearchVersion)
|
||||
if (version == Volatile.Read(ref _worldSearchVersion))
|
||||
{
|
||||
_worldStatus = LegacyComparisonSearchStatus.Idle;
|
||||
_worldError = string.Empty;
|
||||
@@ -152,7 +185,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (version == _worldSearchVersion)
|
||||
if (version == Volatile.Read(ref _worldSearchVersion))
|
||||
{
|
||||
_worldResults = [];
|
||||
_worldStatus = LegacyComparisonSearchStatus.Error;
|
||||
@@ -220,6 +253,7 @@ public sealed class LegacyComparisonWorkflowController
|
||||
public async Task<LegacyComparisonWorkflowSnapshot> LoadSavedPairsAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var activationGeneration = Volatile.Read(ref _activationGeneration);
|
||||
BeginBusy();
|
||||
try
|
||||
{
|
||||
@@ -251,6 +285,11 @@ public sealed class LegacyComparisonWorkflowController
|
||||
}
|
||||
}
|
||||
|
||||
if (activationGeneration != Volatile.Read(ref _activationGeneration))
|
||||
{
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
_savedPairs = candidate;
|
||||
_pairSequence = Math.Max(_pairSequence, candidate.Count);
|
||||
_selectedPairId = candidate.FirstOrDefault()?.RowId;
|
||||
@@ -261,13 +300,21 @@ public sealed class LegacyComparisonWorkflowController
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (activationGeneration != Volatile.Read(ref _activationGeneration))
|
||||
{
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
_statusMessage = "CP949 종목비교.dat를 안전하게 불러오지 못했습니다.";
|
||||
_statusKind = LegacyOperatorStatusKind.Error;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndBusy();
|
||||
if (activationGeneration == Volatile.Read(ref _activationGeneration))
|
||||
{
|
||||
EndBusy();
|
||||
}
|
||||
}
|
||||
|
||||
return CreateSnapshot();
|
||||
|
||||
@@ -104,7 +104,6 @@ public sealed class LegacyFixedAction
|
||||
public sealed record LegacyMaterializedFixedAction(
|
||||
LegacyFixedAction Action,
|
||||
LegacyFixedSelection Selection,
|
||||
LegacyMovingAverageSelection MovingAverages,
|
||||
LegacyFixedRuntimeAsset RuntimeAsset);
|
||||
|
||||
public sealed class LegacyFixedSection
|
||||
@@ -262,8 +261,7 @@ public sealed class LegacyFixedActionCatalog
|
||||
|
||||
public LegacyMaterializedFixedAction Materialize(
|
||||
string actionId,
|
||||
DateTimeOffset localNow,
|
||||
LegacyMovingAverageSelection movingAverages = default)
|
||||
DateTimeOffset localNow)
|
||||
{
|
||||
var action = GetAction(actionId);
|
||||
if (!action.Available)
|
||||
@@ -271,14 +269,9 @@ public sealed class LegacyFixedActionCatalog
|
||||
throw new InvalidOperationException(action.Prerequisite);
|
||||
}
|
||||
|
||||
var trustedMovingAverages = action.SupportsMovingAverages
|
||||
? movingAverages
|
||||
: default;
|
||||
var graphicType = action.DynamicNxtSession
|
||||
? localNow.Hour <= 12 ? "PRE_MARKET" : "AFTER_MARKET"
|
||||
: action.SupportsMovingAverages
|
||||
? MovingAverageText(trustedMovingAverages)
|
||||
: action.Selection.GraphicType;
|
||||
: action.Selection.GraphicType;
|
||||
var dataCode = action.DynamicDate
|
||||
? localNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
|
||||
: action.Selection.DataCode;
|
||||
@@ -291,7 +284,6 @@ public sealed class LegacyFixedActionCatalog
|
||||
return new LegacyMaterializedFixedAction(
|
||||
action,
|
||||
selection,
|
||||
trustedMovingAverages,
|
||||
RuntimeAssets[action.Market]);
|
||||
}
|
||||
|
||||
@@ -602,21 +594,6 @@ public sealed class LegacyFixedActionCatalog
|
||||
}
|
||||
}
|
||||
|
||||
private static string MovingAverageText(LegacyMovingAverageSelection movingAverages)
|
||||
{
|
||||
if (movingAverages.Ma5 && movingAverages.Ma20)
|
||||
{
|
||||
return "MA5,MA20";
|
||||
}
|
||||
|
||||
if (movingAverages.Ma5)
|
||||
{
|
||||
return "MA5";
|
||||
}
|
||||
|
||||
return movingAverages.Ma20 ? "MA20" : string.Empty;
|
||||
}
|
||||
|
||||
private static LegacyFixedMarket ParseMarket(string value) => value switch
|
||||
{
|
||||
"overseas" => LegacyFixedMarket.Overseas,
|
||||
|
||||
@@ -82,6 +82,26 @@ public sealed class LegacyIndustrySelectionWorkflow
|
||||
|
||||
public LegacyIndustrySelectionSnapshot Current => CreateSnapshot();
|
||||
|
||||
/// <summary>
|
||||
/// Recreates the observable UC2 state for one tab activation. The generation
|
||||
/// increment prevents a request started by the discarded control from restoring
|
||||
/// its rows after the operator has re-entered the tab.
|
||||
/// </summary>
|
||||
public LegacyIndustrySelectionSnapshot ResetForTabActivation(IndustryMarket market)
|
||||
{
|
||||
ValidateMarket(market);
|
||||
Interlocked.Increment(ref _loadGeneration);
|
||||
_market = market;
|
||||
_industries = NoIndustries;
|
||||
_actions = LegacyIndustryActionCatalog.GetActions(market);
|
||||
_selectedIndex = null;
|
||||
_selectedIndustry = null;
|
||||
_firstIndustry = null;
|
||||
_secondIndustry = null;
|
||||
Touch();
|
||||
return CreateSnapshot();
|
||||
}
|
||||
|
||||
public async Task<LegacyIndustrySelectionSnapshot> LoadMarketAsync(
|
||||
IndustryMarket market,
|
||||
CancellationToken cancellationToken = default)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,4 +53,7 @@ public sealed record LegacyOperatorPlayoutSnapshot(
|
||||
bool BackgroundEnabled,
|
||||
string BackgroundFileName,
|
||||
bool CanChangeBackground,
|
||||
string Message);
|
||||
string Message)
|
||||
{
|
||||
public bool IsTakeOutCompletionPending { get; init; }
|
||||
}
|
||||
|
||||
@@ -273,6 +273,41 @@ public sealed class LegacyOverseasSelectionWorkflow
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreates UC5's per-control search and selection state while retaining the
|
||||
/// MainForm-owned moving-average checkboxes. Incrementing both generations makes
|
||||
/// results from the discarded control stale before they can commit.
|
||||
/// </summary>
|
||||
public LegacyOverseasWorkflowSnapshot ResetForTabActivation()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
checked
|
||||
{
|
||||
_industryGeneration++;
|
||||
_stockGeneration++;
|
||||
}
|
||||
|
||||
_selectedFixedIndex = null;
|
||||
_industryStatus = LegacyOverseasSearchStatus.Idle;
|
||||
_industryQuery = string.Empty;
|
||||
_industryRetrievedAt = null;
|
||||
_industryResults = NoIndustryResults;
|
||||
_industryIsTruncated = false;
|
||||
_selectedIndustry = null;
|
||||
_industryError = string.Empty;
|
||||
_stockStatus = LegacyOverseasSearchStatus.Idle;
|
||||
_stockQuery = string.Empty;
|
||||
_stockRetrievedAt = null;
|
||||
_stockResults = NoStockResults;
|
||||
_stockIsTruncated = false;
|
||||
_selectedStock = null;
|
||||
_stockError = string.Empty;
|
||||
Touch();
|
||||
return CreateSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
public LegacyOverseasWorkflowSnapshot SelectFixedIndex(string targetId)
|
||||
{
|
||||
var target = LegacyOverseasActionCatalog.GetFixedIndexTarget(targetId);
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
using System.Text;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
|
||||
public enum LegacyRuntimeStockCutMenuFailure
|
||||
{
|
||||
None,
|
||||
InvalidPath,
|
||||
FileNotFound,
|
||||
FileUnavailable,
|
||||
FileTooLarge,
|
||||
InvalidEncoding,
|
||||
InvalidFormat,
|
||||
TooManyRows,
|
||||
UnknownCut,
|
||||
EmptyMenu
|
||||
}
|
||||
|
||||
public enum LegacyRuntimeStockCutMenuSource
|
||||
{
|
||||
ExternalOverride,
|
||||
Packaged,
|
||||
BuiltIn
|
||||
}
|
||||
|
||||
public sealed record LegacyRuntimeStockCutMenuComposition(
|
||||
IReadOnlyList<LegacyCutCatalogRow> Rows,
|
||||
LegacyRuntimeStockCutMenuSource Source,
|
||||
LegacyRuntimeStockCutMenuFailure Failure,
|
||||
string? WarningMessage)
|
||||
{
|
||||
public bool UsesBuiltInCatalog => Source == LegacyRuntimeStockCutMenuSource.BuiltIn;
|
||||
}
|
||||
|
||||
public static class LegacyRuntimeStockCutMenuLoader
|
||||
{
|
||||
public const int MaximumRows = 50;
|
||||
public const int MaximumFileBytes = 64 * 1024;
|
||||
public const string StockSectionHeader = "[종목]";
|
||||
public const string FallbackWarningMessage =
|
||||
"런타임 컷 메뉴를 읽지 못해 내장 컷 목록을 사용합니다.";
|
||||
|
||||
private static readonly Encoding Cp949 = CreateCp949Encoding();
|
||||
private static readonly HashSet<string> KnownCutLabels = LegacyStockCutCatalog.Rows
|
||||
.Where(row => !row.IsSeparator)
|
||||
.Select(row => row.RawLabel)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
public static LegacyRuntimeStockCutMenuComposition ResolveFromBaseDirectory(
|
||||
string? baseDirectory)
|
||||
{
|
||||
if (!TryCreatePackagedCandidatePath(baseDirectory, out var path))
|
||||
{
|
||||
return CreateFallbackComposition(
|
||||
LegacyRuntimeStockCutMenuFailure.InvalidPath);
|
||||
}
|
||||
|
||||
if (TryLoad(path, out var rows, out var failure))
|
||||
{
|
||||
return CreateRuntimeComposition(
|
||||
rows,
|
||||
LegacyRuntimeStockCutMenuSource.Packaged);
|
||||
}
|
||||
|
||||
return CreateFallbackComposition(failure);
|
||||
}
|
||||
|
||||
public static LegacyRuntimeStockCutMenuComposition ResolveFromCandidates(
|
||||
string? externalOverridePath,
|
||||
string? packagedBaseDirectory)
|
||||
{
|
||||
if (externalOverridePath is not null)
|
||||
{
|
||||
if (!TryNormalizeExternalOverridePath(
|
||||
externalOverridePath,
|
||||
out var normalizedExternalPath))
|
||||
{
|
||||
return CreateFallbackComposition(
|
||||
LegacyRuntimeStockCutMenuFailure.InvalidPath);
|
||||
}
|
||||
|
||||
if (TryLoad(
|
||||
normalizedExternalPath,
|
||||
out var externalRows,
|
||||
out var externalFailure))
|
||||
{
|
||||
return CreateRuntimeComposition(
|
||||
externalRows,
|
||||
LegacyRuntimeStockCutMenuSource.ExternalOverride);
|
||||
}
|
||||
|
||||
if (externalFailure != LegacyRuntimeStockCutMenuFailure.FileNotFound)
|
||||
{
|
||||
return CreateFallbackComposition(externalFailure);
|
||||
}
|
||||
}
|
||||
|
||||
return ResolveFromBaseDirectory(packagedBaseDirectory);
|
||||
}
|
||||
|
||||
public static bool TryLoad(
|
||||
string? path,
|
||||
out IReadOnlyList<LegacyCutCatalogRow> rows,
|
||||
out LegacyRuntimeStockCutMenuFailure failure)
|
||||
{
|
||||
rows = Array.Empty<LegacyCutCatalogRow>();
|
||||
if (string.IsNullOrWhiteSpace(path) ||
|
||||
path.Any(char.IsControl) ||
|
||||
!Path.IsPathFullyQualified(path))
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.InvalidPath;
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.Read,
|
||||
bufferSize: 4096,
|
||||
FileOptions.SequentialScan);
|
||||
if (stream.Length > MaximumFileBytes)
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.FileTooLarge;
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[(int)stream.Length];
|
||||
stream.ReadExactly(bytes);
|
||||
return TryParse(bytes, out rows, out failure);
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is FileNotFoundException or
|
||||
DirectoryNotFoundException)
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.FileNotFound;
|
||||
return false;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is IOException or
|
||||
UnauthorizedAccessException or
|
||||
NotSupportedException or
|
||||
ArgumentException or
|
||||
System.Security.SecurityException)
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.FileUnavailable;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool TryParse(
|
||||
ReadOnlySpan<byte> cp949Bytes,
|
||||
out IReadOnlyList<LegacyCutCatalogRow> rows,
|
||||
out LegacyRuntimeStockCutMenuFailure failure)
|
||||
{
|
||||
rows = Array.Empty<LegacyCutCatalogRow>();
|
||||
if (cp949Bytes.Length > MaximumFileBytes)
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.FileTooLarge;
|
||||
return false;
|
||||
}
|
||||
|
||||
string text;
|
||||
try
|
||||
{
|
||||
text = Cp949.GetString(cp949Bytes);
|
||||
}
|
||||
catch (DecoderFallbackException)
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.InvalidEncoding;
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParseDecodedText(text, out rows, out failure);
|
||||
}
|
||||
|
||||
private static bool TryParseDecodedText(
|
||||
string text,
|
||||
out IReadOnlyList<LegacyCutCatalogRow> rows,
|
||||
out LegacyRuntimeStockCutMenuFailure failure)
|
||||
{
|
||||
rows = Array.Empty<LegacyCutCatalogRow>();
|
||||
var parsedRows = new List<LegacyCutCatalogRow>();
|
||||
var sawSection = false;
|
||||
var displayOrdinal = 0;
|
||||
|
||||
using var reader = new StringReader(text);
|
||||
while (reader.ReadLine() is { } sourceLine)
|
||||
{
|
||||
var line = sourceLine.Trim();
|
||||
if (line.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!sawSection)
|
||||
{
|
||||
if (!string.Equals(line, StockSectionHeader, StringComparison.Ordinal))
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
|
||||
sawSection = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("[", StringComparison.Ordinal))
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parsedRows.Count >= MaximumRows)
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.TooManyRows;
|
||||
return false;
|
||||
}
|
||||
|
||||
var isSeparator = string.Equals(line, "-", StringComparison.Ordinal);
|
||||
if (!isSeparator && !KnownCutLabels.Contains(line))
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.UnknownCut;
|
||||
return false;
|
||||
}
|
||||
|
||||
parsedRows.Add(new LegacyCutCatalogRow(
|
||||
parsedRows.Count,
|
||||
isSeparator ? null : ++displayOrdinal,
|
||||
isSeparator ? string.Empty : line,
|
||||
isSeparator));
|
||||
}
|
||||
|
||||
if (!sawSection)
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.InvalidFormat;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (displayOrdinal == 0)
|
||||
{
|
||||
failure = LegacyRuntimeStockCutMenuFailure.EmptyMenu;
|
||||
return false;
|
||||
}
|
||||
|
||||
rows = parsedRows.AsReadOnly();
|
||||
failure = LegacyRuntimeStockCutMenuFailure.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Encoding CreateCp949Encoding()
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
return Encoding.GetEncoding(
|
||||
949,
|
||||
EncoderFallback.ExceptionFallback,
|
||||
DecoderFallback.ExceptionFallback);
|
||||
}
|
||||
|
||||
private static LegacyRuntimeStockCutMenuComposition CreateFallbackComposition(
|
||||
LegacyRuntimeStockCutMenuFailure failure) =>
|
||||
new(
|
||||
LegacyStockCutCatalog.Rows,
|
||||
LegacyRuntimeStockCutMenuSource.BuiltIn,
|
||||
failure,
|
||||
FallbackWarningMessage);
|
||||
|
||||
private static LegacyRuntimeStockCutMenuComposition CreateRuntimeComposition(
|
||||
IReadOnlyList<LegacyCutCatalogRow> rows,
|
||||
LegacyRuntimeStockCutMenuSource source) =>
|
||||
new(
|
||||
rows,
|
||||
source,
|
||||
LegacyRuntimeStockCutMenuFailure.None,
|
||||
WarningMessage: null);
|
||||
|
||||
private static bool TryCreatePackagedCandidatePath(
|
||||
string? baseDirectory,
|
||||
out string path)
|
||||
{
|
||||
path = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(baseDirectory) ||
|
||||
baseDirectory.Any(char.IsControl) ||
|
||||
!Path.IsPathFullyQualified(baseDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
path = Path.Combine(
|
||||
Path.GetFullPath(baseDirectory),
|
||||
"Res",
|
||||
"종목.ini");
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is ArgumentException or
|
||||
IOException or
|
||||
NotSupportedException or
|
||||
System.Security.SecurityException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryNormalizeExternalOverridePath(
|
||||
string path,
|
||||
out string normalizedPath)
|
||||
{
|
||||
normalizedPath = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(path) ||
|
||||
path.Any(char.IsControl) ||
|
||||
!Path.IsPathFullyQualified(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
normalizedPath = Path.GetFullPath(path);
|
||||
var resourceDirectory = Path.GetDirectoryName(normalizedPath);
|
||||
var applicationDirectory = resourceDirectory is null
|
||||
? null
|
||||
: Path.GetDirectoryName(resourceDirectory);
|
||||
return string.Equals(
|
||||
Path.GetFileName(normalizedPath),
|
||||
"종목.ini",
|
||||
StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(
|
||||
resourceDirectory is null
|
||||
? null
|
||||
: Path.GetFileName(resourceDirectory),
|
||||
"Res",
|
||||
StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(
|
||||
applicationDirectory is null
|
||||
? null
|
||||
: Path.GetFileName(applicationDirectory),
|
||||
"MBN_STOCK_WEBVIEW",
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is ArgumentException or
|
||||
IOException or
|
||||
NotSupportedException or
|
||||
System.Security.SecurityException)
|
||||
{
|
||||
normalizedPath = string.Empty;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout;
|
||||
using MBN_STOCK_WEBVIEW.Core.Playout.Scenes;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
@@ -21,7 +22,8 @@ public sealed record LegacyOperatorPlaylistRow(
|
||||
int FadeDuration,
|
||||
bool IsSelected = false,
|
||||
string SceneAlias = "",
|
||||
LegacySceneSelection? PlayoutSelection = null)
|
||||
LegacySceneSelection? PlayoutSelection = null,
|
||||
bool IsActive = false)
|
||||
{
|
||||
public LegacyPlaylistEntry ToPlayoutEntry()
|
||||
{
|
||||
@@ -40,7 +42,47 @@ public sealed record LegacyOperatorPlaylistRow(
|
||||
StockName,
|
||||
GraphicType,
|
||||
Subtype,
|
||||
StockCode));
|
||||
StockCode),
|
||||
CreatePageNavigation());
|
||||
}
|
||||
|
||||
private LegacyPlaylistPageNavigation CreatePageNavigation()
|
||||
{
|
||||
var pageSize = GraphicType switch
|
||||
{
|
||||
"5단 표그래프" => ScenePageSize.Five,
|
||||
"6종목 현재가" => ScenePageSize.Six,
|
||||
"12종목 현재가" => ScenePageSize.Twelve,
|
||||
_ => (ScenePageSize?)null
|
||||
};
|
||||
if (!pageSize.HasValue)
|
||||
{
|
||||
// MainForm's PageN branches compared the visible FarPoint column text
|
||||
// exactly. A scene alias that happens to resolve to a paged builder is still
|
||||
// an ordinary playlist row when that column uses any other spelling.
|
||||
return LegacyPlaylistPageNavigation.Disabled;
|
||||
}
|
||||
|
||||
var separator = PageText.IndexOf('/');
|
||||
if (separator <= 0 || separator != PageText.LastIndexOf('/') ||
|
||||
!int.TryParse(
|
||||
PageText.AsSpan(0, separator),
|
||||
System.Globalization.NumberStyles.None,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var currentPage) ||
|
||||
!int.TryParse(
|
||||
PageText.AsSpan(separator + 1),
|
||||
System.Globalization.NumberStyles.None,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var totalPages) ||
|
||||
totalPages is < 1 or > ScenePaging.MaximumPageCount ||
|
||||
currentPage < 1 || currentPage > totalPages)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"A PageN playlist row must use current/total with a total from 1 through 20.");
|
||||
}
|
||||
|
||||
return new LegacyPlaylistPageNavigation(pageSize, totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,6 +119,33 @@ public sealed class LegacyTradingHaltWorkflowController
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recreates UC7's per-control query, result, selection and column-sort state.
|
||||
/// MainForm-owned moving-average values remain in force across tab activation.
|
||||
/// </summary>
|
||||
public LegacyTradingHaltWorkflowSnapshot ResetForTabActivation()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
checked
|
||||
{
|
||||
_searchGeneration++;
|
||||
}
|
||||
|
||||
_results = [];
|
||||
_selected = null;
|
||||
_query = string.Empty;
|
||||
_errorMessage = string.Empty;
|
||||
_statusMessage = string.Empty;
|
||||
_status = LegacyTradingHaltRequestStatus.Idle;
|
||||
_nameSort = LegacyTradingHaltNameSort.None;
|
||||
_statusKind = LegacyOperatorStatusKind.Neutral;
|
||||
_isTruncated = false;
|
||||
Touch();
|
||||
return CreateSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<LegacyTradingHaltWorkflowSnapshot> SearchAsync(
|
||||
string query,
|
||||
CancellationToken cancellationToken = default)
|
||||
|
||||
Reference in New Issue
Block a user