I pulsanti contestuali stavano sulla riga del marchio, fuori dal riquadro della scheda a cui appartenevano: per trovare il comando che riguardava una tabella bisognava risalire fino al bordo della finestra. Ora stanno dentro la sezione, sopra il contenuto su cui agiscono, e ciascuno spiega a comparsa cosa fa. Il marchio scende nella barra delle sezioni, che diventa alta quanto la finestra e si riduce ai soli simboli; a destra non resta altro che la scheda, dall'alto al basso. Lo spazio liberato in cima ospita l'avanzamento delle operazioni di massa — una barra sola, perche' la scheda Esportazione ne aveva una seconda che diceva la stessa cosa piu' in basso — che passa anche sull'icona nella barra delle applicazioni tramite ITaskbarList3 dichiarata a mano. Durante un'operazione di massa i controlli che la disturberebbero sono spenti: il progetto viene letto da un altro thread a ogni fotogramma, e cambiare risoluzione o ritaglio a meta' significa cambiare le regole a partita iniziata. L'anteprima guadagna due letture. La maschera del ritaglio mostra il fotogramma intero scurendo cio' che il formato d'uscita taglia via, perche' il rapporto d'immagine si sceglie guardando cosa si perde; e lo schermo intero apre il fotogramma in una finestra a parte alla risoluzione nativa, con i dati di scatto in un pannello che si chiude, perche' decidere se una stella e' puntiforme si fa al cento per cento. La scheda Esportazione si apre su una preimpostazione. I parametri di codifica non sono indipendenti — il bitrate abbondante a 1080p lascia gradini sul cielo a 4K — e sceglierli bene significa muoverli insieme; restano tutti modificabili, e toccarne uno riporta la voce a «Personalizzata». Sull'annullamento: non sono riuscito a riprodurre un crollo sull'albero di partenza in dodici scenari automatici, ma ho chiuso quattro strade per cui poteva passare. Cancel() veniva chiamato nudo dentro il gestore del clic, dove un'eccezione di una registrazione avrebbe ucciso la finestra. FrameWindow non tollerava ObjectDisposedException quando il file di parcheggio veniva chiuso sotto le decodifiche in volo, non impediva l'avvio di nuove decodifiche dopo la chiusura, e lasciava non osservate le eccezioni dei compiti falliti. Soprattutto, la coda di un'esportazione annullata tocca i controlli dopo che la finestra puo' essere gia' stata chiusa — ed e' proprio la cosa piu' naturale da fare dopo aver annullato: quel percorso ora si ferma prima. Resta una rete: le eccezioni non gestite finiscono in Documenti\Titano\errori.txt invece di portarsi via il lavoro. I dati dell'applicazione si spostano da AppData a Documenti, perche' sono file fatti per essere aperti; le preferenze nella vecchia collocazione vengono migrate. Tre nuove verifiche coprono l'annullamento con il parcheggio su disco attivo: 63 su 63 passano, provate anche sulle sequenze reali in K:\Time Lapse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
6.5 KiB
C#
156 lines
6.5 KiB
C#
using System.Runtime.InteropServices;
|
||
using Titano.Diagnostics;
|
||
|
||
namespace Titano;
|
||
|
||
internal static class Program
|
||
{
|
||
private const int AttachParentProcess = -1;
|
||
|
||
[DllImport("kernel32.dll")]
|
||
private static extern bool AttachConsole(int processId);
|
||
|
||
[STAThread]
|
||
private static int Main(string[] args)
|
||
{
|
||
if (args.Length > 0 && args[0] is "--selftest" or "-t")
|
||
{
|
||
EnsureConsole();
|
||
string directory = args.Length > 1
|
||
? args[1]
|
||
: Path.Combine(Path.GetTempPath(), "Titano.SelfTest");
|
||
Directory.CreateDirectory(directory);
|
||
|
||
Console.WriteLine();
|
||
Console.Write(SelfTest.DescribeEnvironment());
|
||
return SelfTest.Run(directory, Console.Out);
|
||
}
|
||
|
||
if (args.Length > 1 && args[0] == "--make-icon")
|
||
{
|
||
EnsureConsole();
|
||
IconGenerator.Write(args[1]);
|
||
Console.WriteLine($"Icona generata in {args[1]}.");
|
||
return 0;
|
||
}
|
||
|
||
if (args.Length > 1 && args[0] == "--diagnose")
|
||
{
|
||
EnsureConsole();
|
||
Console.WriteLine();
|
||
|
||
// Le parole chiave possono comparire in qualunque posizione: gli argomenti
|
||
// posizionali erano ormai troppi perché ricordarne l'ordine avesse senso.
|
||
var modules = SequenceDiagnostics.AdvancedModules.None;
|
||
int analysisFrames = 0;
|
||
var positional = new List<string>();
|
||
|
||
foreach (string argument in args[1..])
|
||
{
|
||
if (argument.Equals("avanzato", StringComparison.OrdinalIgnoreCase))
|
||
modules |= SequenceDiagnostics.AdvancedModules.Analysis;
|
||
else if (argument.Equals("camera", StringComparison.OrdinalIgnoreCase))
|
||
modules |= SequenceDiagnostics.AdvancedModules.Camera;
|
||
else if (argument.Equals("mediana", StringComparison.OrdinalIgnoreCase))
|
||
modules |= SequenceDiagnostics.AdvancedModules.MedianStack;
|
||
else if (argument.Equals("scie", StringComparison.OrdinalIgnoreCase))
|
||
modules |= SequenceDiagnostics.AdvancedModules.StarTrails;
|
||
else if (argument.Equals("luminanza", StringComparison.OrdinalIgnoreCase))
|
||
modules |= SequenceDiagnostics.AdvancedModules.RegionsByLuminance;
|
||
else if (argument.StartsWith("analisi=", StringComparison.OrdinalIgnoreCase))
|
||
int.TryParse(argument[8..], out analysisFrames);
|
||
else
|
||
positional.Add(argument);
|
||
}
|
||
|
||
return SequenceDiagnostics.Run(positional[0], Console.Out,
|
||
renderPath: positional.Count > 1 ? positional[1] : null,
|
||
renderFrames: positional.Count > 2 && int.TryParse(positional[2], out int n) ? n : 24,
|
||
renderWidth: positional.Count > 3 && int.TryParse(positional[3], out int w) ? w : 0,
|
||
renderCodec: positional.Count > 4 && positional[4].Equals("hevc", StringComparison.OrdinalIgnoreCase)
|
||
? Video.VideoCodec.Hevc : Video.VideoCodec.H264,
|
||
advanced: modules,
|
||
analysisFrames: analysisFrames);
|
||
}
|
||
|
||
if (args.Length > 1 && args[0] == "--capture")
|
||
{
|
||
EnsureConsole();
|
||
ApplicationConfiguration.Initialize();
|
||
return Capture(args[1], args.Length > 2 ? args[2] : null,
|
||
args.Length > 3 && int.TryParse(args[3], out int tab) ? tab : 0,
|
||
args.Length > 4 && args[4].Equals("avanzato", StringComparison.OrdinalIgnoreCase));
|
||
}
|
||
|
||
ApplicationConfiguration.Initialize();
|
||
CrashGuard.Install();
|
||
Application.Run(new UI.MainForm());
|
||
return 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Rende visibili i messaggi delle modalità a riga di comando.
|
||
///
|
||
/// L'eseguibile è compilato come applicazione a finestre e quindi nasce senza console:
|
||
/// agganciare quella del processo padre è l'unico modo di farsi leggere da un terminale.
|
||
/// Ma se lo standard output è già dirottato — una pipe, un file di registro, il task di
|
||
/// un editor — l'aggancio sostituirebbe quel dirottamento e i messaggi finirebbero nella
|
||
/// console invece che dove sono attesi. In quel caso non si aggancia niente: i gestori
|
||
/// ereditati vanno già bene.
|
||
/// </summary>
|
||
private static void EnsureConsole()
|
||
{
|
||
if (Console.IsOutputRedirected) return;
|
||
AttachConsole(AttachParentProcess);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Apre l'interfaccia, opzionalmente vi carica una sequenza, e ne salva un'immagine.
|
||
/// Serve a verificare la resa dei controlli disegnati a mano senza intervento manuale.
|
||
/// </summary>
|
||
private static int Capture(string outputPath, string? sequenceDirectory, int settingsTab,
|
||
bool advanced = false)
|
||
{
|
||
using var form = new UI.MainForm();
|
||
form.Show();
|
||
Pump(200);
|
||
|
||
// I moduli avanzati vanno accesi prima di caricare: la maschera delle regioni e il
|
||
// percorso della stabilizzazione nascono durante l'analisi, non dopo.
|
||
if (advanced) form.EnableAdvancedModulesForCapture();
|
||
|
||
if (sequenceDirectory is not null && Directory.Exists(sequenceDirectory))
|
||
{
|
||
string[] files = [.. Directory.EnumerateFiles(sequenceDirectory)
|
||
.Where(Metadata.MetadataReader.IsSupported)];
|
||
var work = form.PrepareForCaptureAsync(files);
|
||
while (!work.IsCompleted) Pump(30);
|
||
Pump(2500); // attesa del rendering asincrono dell'anteprima
|
||
}
|
||
|
||
if (settingsTab >= 0)
|
||
{
|
||
form.SelectSettingsTab(settingsTab);
|
||
Pump(200);
|
||
}
|
||
|
||
using var bitmap = new Bitmap(form.Width, form.Height);
|
||
form.DrawToBitmap(bitmap, new Rectangle(0, 0, form.Width, form.Height));
|
||
bitmap.Save(outputPath, System.Drawing.Imaging.ImageFormat.Png);
|
||
|
||
Console.WriteLine($"Interfaccia catturata in {outputPath} ({bitmap.Width}×{bitmap.Height}).");
|
||
return 0;
|
||
}
|
||
|
||
private static void Pump(int milliseconds)
|
||
{
|
||
var deadline = Environment.TickCount64 + milliseconds;
|
||
do
|
||
{
|
||
Application.DoEvents();
|
||
Thread.Sleep(10);
|
||
}
|
||
while (Environment.TickCount64 < deadline);
|
||
}
|
||
}
|