using System.Diagnostics; using System.Drawing.Drawing2D; using Microsoft.Data.Sqlite; namespace PrepStep; public partial class Form1 : Form { private readonly Button _btnApriDb = new(); private readonly Button _btnAggiorna = new(); private readonly Button _btnCreaRaw = new(); private readonly Button _btnTrovaPrt = new(); private readonly Button _btnVerifica = new(); private readonly FlowLayoutPanel _rigaVerifica = new(); private readonly ListView _lstAnteprimaAsm = new(); private readonly Label _lblStatus = new(); private readonly Label _lblDb = new(); private readonly Label _lblUltimoAggiornamento = new(); private readonly Panel _indicatoreDb = new(); private bool _dbCaricato; private readonly DataGridView _gridImport = new(); private readonly DataGridView _gridRaw = new(); private readonly Label _lblImportStatus = new(); private readonly ToolTip _tipImportStatus = new(); private readonly MenuStrip _menu = new(); private readonly ToolStripMenuItem _mnuNuovaEstrazione = new("Nuovo"); private readonly ToolStripMenuItem _mnuPulisci = new("Pulisci raw e step..."); private readonly ToolStripMenuItem _mnuCambiaCartella = new("Scegli cartella di lavoro..."); // Wizard: una fase alla volta occupa tutta la finestra, si avanza solo quando quella // corrente è "pronta" (vedi AvantiConsentito). NomiFasi/_panelFasi sono paralleli per indice. private static readonly string[] NomiFasi = { "1. Database", "2. Importa Distinta Base", "3. Crea cartella raw", "4. Trova componenti referenziati", "5. Verifica completezza", }; private readonly Panel[] _panelFasi = new Panel[NomiFasi.Length]; private readonly Panel _faseContainer = new() { Dock = DockStyle.Fill }; private readonly Label _lblFase = new(); private readonly Button _btnIndietro = new(); private readonly Button _btnAvanti = new(); private int _faseAttuale; private static readonly string[] TipiPredefiniti = { "prt", "asm" }; private const string RadiceDiscoDefault = @"X:\"; private string? _dbPath; private string? _cartellaLavoro; private string? _percorsoInputCorrente; private List<(string Componente, string Descrizione)>? _righeCorrenti; private Dictionary>? _ultimaVerifica; private string? _ultimaCartellaRaw; public Form1() { InitializeComponent(); BuildUi(); Load += (_, _) => { InizializzaDatabase(); InizializzaCartellaLavoro(); }; } // Palette: blu primario per l'azione principale del flusso ("Crea cartella raw"), // il resto resta ai colori di sistema per restare coerente col tema Windows dell'utente. private static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177); private void BuildUi() { AllowDrop = true; // Versione letta dall'assembly ( nel csproj) invece che duplicata qui a mano. var versione = typeof(Form1).Assembly.GetName().Version; Text = $"PrepStep - Cevolani {versione?.ToString(3)}"; MinimumSize = new Size(760, 480); Font = new Font("Segoe UI", 9.5f); // Stessa icona di ApplicationIcon nel csproj: la si rilegge dall'exe compilato invece // di duplicare il file .ico come Content, funziona identica in Debug/Release/publish. try { Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); } catch (Exception ex) when (ex is IOException or ArgumentException) { // ponytail: nessuna icona nell'exe (es. lanciato da dotnet run) -> resta quella di default } var headerPanel = new Panel { Dock = DockStyle.Top, Height = 40, BackColor = SystemColors.ControlLight }; _lblFase.Dock = DockStyle.Fill; _lblFase.TextAlign = ContentAlignment.MiddleLeft; _lblFase.Padding = new Padding(12, 0, 0, 0); _lblFase.Font = new Font(Font, FontStyle.Bold); headerPanel.Controls.Add(_lblFase); // Floatbar di navigazione: card staccata dai bordi della finestra invece di un footer // a tutta larghezza appoggiato direttamente sul contenuto/sulla barra di stato. var (navContainer, navPanel) = UiStile.FloatBar(); _btnAvanti.Text = "Avanti ›"; UiStile.Primario(_btnAvanti); _btnIndietro.Text = "‹ Indietro"; _btnIndietro.Margin = new Padding(8, 0, 0, 0); UiStile.SecondarioPieno(_btnIndietro); // Dimensioni uguali decise PRIMA di arrotondare: se si arrotonda un bottone e solo dopo // lo si ridimensiona, la Region calcolata sulla dimensione vecchia lascia un bordo // "fantasma" finché non arriva un repaint completo della finestra. UiStile.LarghezzaUguale(_btnAvanti, _btnIndietro); UiStile.Arrotonda(_btnAvanti, 10); UiStile.Arrotonda(_btnIndietro, 10); _btnAvanti.Click += (_, _) => VaiAllaFase(_faseAttuale + 1); navPanel.Controls.Add(_btnAvanti); _btnIndietro.Click += (_, _) => VaiAllaFase(_faseAttuale - 1); navPanel.Controls.Add(_btnIndietro); var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28, BackColor = SystemColors.ControlLight }; _lblStatus.Dock = DockStyle.Fill; _lblStatus.TextAlign = ContentAlignment.MiddleLeft; _lblStatus.Padding = new Padding(12, 0, 0, 0); _lblStatus.Text = "Pronto."; statusPanel.Controls.Add(_lblStatus); _panelFasi[0] = BuildFaseDatabase(); _panelFasi[1] = BuildFaseImporta(); _panelFasi[2] = BuildFaseCreaRaw(); _panelFasi[3] = BuildFaseTrovaComponenti(); _panelFasi[4] = BuildFaseVerifica(); foreach (var panel in _panelFasi) { panel.Dock = DockStyle.Fill; panel.Visible = false; _faseContainer.Controls.Add(panel); } // Ordine dei Dock.Bottom: l'ultimo aggiunto è il più esterno (il vero bordo inferiore // della finestra). La barra di stato va tra contenuto e azioni; i bottoni Avanti/Indietro // restano il footer più esterno, come in ogni wizard/dialog standard di Windows. var mnuFile = new ToolStripMenuItem("File"); _mnuNuovaEstrazione.ShortcutKeys = Keys.Control | Keys.N; _mnuNuovaEstrazione.Click += (_, _) => NuovaEstrazione(); mnuFile.DropDownItems.Add(_mnuNuovaEstrazione); _mnuCambiaCartella.Click += (_, _) => ScegliCartellaLavoro(); mnuFile.DropDownItems.Add(_mnuCambiaCartella); _mnuPulisci.Click += (_, _) => PulisciRawEStep(); mnuFile.DropDownItems.Add(_mnuPulisci); var mnuAiuto = new ToolStripMenuItem("Aiuto"); var mnuGuida = new ToolStripMenuItem("Guida") { ShortcutKeys = Keys.F1 }; mnuGuida.Click += (_, _) => new GuidaForm().ShowDialog(this); mnuAiuto.DropDownItems.Add(mnuGuida); _menu.Items.Add(mnuFile); _menu.Items.Add(mnuAiuto); MainMenuStrip = _menu; Controls.Add(_faseContainer); Controls.Add(statusPanel); Controls.Add(navContainer); Controls.Add(headerPanel); Controls.Add(_menu); DragEnter += (_, e) => e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true ? DragDropEffects.Copy : DragDropEffects.None; DragDrop += (_, e) => { if (e.Data?.GetData(DataFormats.FileDrop) is string[] { Length: > 0 } file) ImportaDistinta(file[0]); }; VaiAllaFase(0); } /// Nasconde la fase corrente e mostra quella richiesta (se nei limiti e se la fase /// corrente permette di avanzare); aggiorna titolo e stato dei pulsanti Indietro/Avanti. private void VaiAllaFase(int fase) { if (fase < 0 || fase >= NomiFasi.Length) return; if (fase > _faseAttuale && !AvantiConsentito(_faseAttuale)) return; _panelFasi[_faseAttuale].Visible = false; _faseAttuale = fase; _panelFasi[_faseAttuale].Visible = true; _lblFase.Text = NomiFasi[_faseAttuale]; _btnIndietro.Visible = _faseAttuale > 0; _btnAvanti.Visible = _faseAttuale < NomiFasi.Length - 1; AggiornaStatoAvanti(); if (_faseAttuale == 4) AggiornaAnteprimaAsm(); } /// Popola le icone trascinabili (drag&drop) dei file .asm della distinta importata /// (non i sotto-assiemi di fase 4, che in "raw" servono solo a completarla) già copiati in /// "raw" (non riletti dal DB/share: sono già in locale), mostrate in fase 5 prima ancora di /// avviare la verifica. private void AggiornaAnteprimaAsm() { _lstAnteprimaAsm.Items.Clear(); _lstAnteprimaAsm.SmallImageList!.Images.Clear(); if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw) || _righeCorrenti is null) return; var codiciDistinta = _righeCorrenti.Select(r => r.Componente).ToHashSet(StringComparer.OrdinalIgnoreCase); var percorsiAsm = Directory.GetFiles(_ultimaCartellaRaw) .Where(p => { var (codice, tipo, _) = Indexer.ParseFilename(Path.GetFileName(p)); return tipo.Equals("asm", StringComparison.OrdinalIgnoreCase) && codiciDistinta.Contains(codice); }); foreach (var percorso in percorsiAsm) { Icon? icona = null; try { icona = Icon.ExtractAssociatedIcon(percorso); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException) { // ponytail: file irraggiungibile (share offline) -> icona generica, non blocca gli altri } int indice = _lstAnteprimaAsm.SmallImageList.Images.Count; _lstAnteprimaAsm.SmallImageList.Images.Add(icona?.ToBitmap() ?? SystemIcons.WinLogo.ToBitmap()); icona?.Dispose(); _lstAnteprimaAsm.Items.Add(new ListViewItem(Path.GetFileName(percorso), indice) { Tag = percorso }); } } /// Condizione per cui la fase indicata è "conclusa" e si può passare alla successiva. private bool AvantiConsentito(int fase) => fase switch { 0 => _dbPath is not null && _cartellaLavoro is not null, 1 => _righeCorrenti is { Count: > 0 } && _ultimaVerifica is not null, 2 => _ultimaCartellaRaw is not null, 3 => true, // risoluzione ricorsiva opzionale: non tutti gli assiemi hanno sotto-componenti _ => true, }; private void AggiornaStatoAvanti() => _btnAvanti.Enabled = AvantiConsentito(_faseAttuale); /// Azzera distinta/raw/verifica (fasi 2-5) per ripartire con una nuova distinta; /// DB indicizzato e cartella di lavoro (fase 1) restano quelli già caricati, non serve /// rifare lo scan del disco per ogni estrazione. private void NuovaEstrazione() { if (_dbPath is null || _cartellaLavoro is null) { MessageBox.Show(this, "Carica prima il database in fase 1.", "Nuovo", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } _percorsoInputCorrente = null; _righeCorrenti = null; _ultimaCartellaRaw = null; _ultimaVerifica = null; _gridImport.Rows.Clear(); _gridRaw.Rows.Clear(); _lstAnteprimaAsm.Items.Clear(); ImpostaStatoImport("", Color.Black); _rigaVerifica.Visible = false; VaiAllaFase(1); } private Panel BuildFaseDatabase() { var contenuto = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, ColumnCount = 1, Padding = new Padding(16), }; // Colonna al 100%: le label sotto (Dock=Top) vanno a capo sulla larghezza reale della // finestra invece che su un limite fisso in pixel, e si riadattano se viene ridimensionata. contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); var lblIstruzioni = new Label { Dock = DockStyle.Top, AutoSize = true, ForeColor = SystemColors.GrayText, Margin = new Padding(0, 0, 0, 16), Text = "L'indice CAD viene caricato automaticamente all'avvio, se già presente.", }; contenuto.Controls.Add(lblIstruzioni); var cardDb = UiStile.Card(); var contenutoCardDb = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, ColumnCount = 1, }; contenutoCardDb.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); var rigaTitoloDb = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight, Margin = new Padding(0, 0, 0, 6), }; _indicatoreDb.Size = new Size(10, 10); _indicatoreDb.Margin = new Padding(0, 5, 8, 0); _indicatoreDb.Paint += (_, e) => { e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; var colore = _dbCaricato ? Color.FromArgb(30, 160, 90) : Color.FromArgb(200, 60, 60); using var pennello = new SolidBrush(colore); e.Graphics.FillEllipse(pennello, 0, 0, _indicatoreDb.Width - 1, _indicatoreDb.Height - 1); }; rigaTitoloDb.Controls.Add(_indicatoreDb); var lblTitoloDb = new Label { AutoSize = true, Font = new Font(Font, FontStyle.Bold), Text = "Database", }; rigaTitoloDb.Controls.Add(lblTitoloDb); contenutoCardDb.Controls.Add(rigaTitoloDb); _lblDb.Dock = DockStyle.Top; _lblDb.AutoSize = true; _lblDb.ForeColor = SystemColors.ControlText; _lblDb.Margin = new Padding(0, 0, 0, 2); contenutoCardDb.Controls.Add(_lblDb); _lblUltimoAggiornamento.AutoSize = true; _lblUltimoAggiornamento.ForeColor = SystemColors.GrayText; _lblUltimoAggiornamento.Margin = new Padding(0, 0, 0, 14); contenutoCardDb.Controls.Add(_lblUltimoAggiornamento); var bottoniDb = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight }; _btnApriDb.Text = "Apri DB..."; _btnApriDb.Margin = new Padding(0, 0, 8, 0); UiStile.Secondario(_btnApriDb); UiStile.Arrotonda(_btnApriDb); _btnApriDb.Click += (_, _) => ScegliDatabase(); bottoniDb.Controls.Add(_btnApriDb); _btnAggiorna.Text = "Aggiorna indice"; UiStile.Secondario(_btnAggiorna); UiStile.Arrotonda(_btnAggiorna); _btnAggiorna.Click += (_, _) => AggiornaIndice(); bottoniDb.Controls.Add(_btnAggiorna); contenutoCardDb.Controls.Add(bottoniDb); cardDb.Controls.Add(contenutoCardDb); contenuto.Controls.Add(cardDb); var panel = new Panel(); panel.Controls.Add(contenuto); return panel; } private Panel BuildFaseImporta() { var contenuto = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, ColumnCount = 1, Padding = new Padding(16), }; contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); var lblIstruzioni = new Label { Dock = DockStyle.Top, AutoSize = true, ForeColor = SystemColors.GrayText, Margin = new Padding(0, 0, 0, 8), Text = "Trascina qui il file .xls/.xml esportato dal gestionale (foglio 'Distinta " + "Base'), oppure scegli il file col pulsante sotto: genera un CSV con Componente " + "e Descrizione, in anteprima nella griglia.", }; contenuto.Controls.Add(lblIstruzioni); var rigaImporta = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight, Margin = new Padding(0, 0, 0, 8), }; var btnSfoglia = new Button { Text = "Scegli file distinta...", Margin = new Padding(0, 0, 8, 0), }; UiStile.Secondario(btnSfoglia); btnSfoglia.Click += (_, _) => ScegliFileDistinta(); rigaImporta.Controls.Add(btnSfoglia); rigaImporta.Controls.Add(new Label { AutoSize = true, ForeColor = SystemColors.GrayText, Margin = new Padding(0, 9, 0, 0), Text = "1. Carica la distinta", }); contenuto.Controls.Add(rigaImporta); _rigaVerifica.AutoSize = true; _rigaVerifica.FlowDirection = FlowDirection.LeftToRight; _rigaVerifica.Margin = new Padding(0, 0, 0, 8); _rigaVerifica.Visible = false; // compare solo dopo un import riuscito (file o drag&drop) _btnVerifica.Text = "Verifica presenza"; _btnVerifica.Margin = new Padding(0, 0, 8, 0); UiStile.Secondario(_btnVerifica); _btnVerifica.Click += (_, _) => VerificaCodiciNelDb(); _rigaVerifica.Controls.Add(_btnVerifica); _rigaVerifica.Controls.Add(new Label { AutoSize = true, ForeColor = SystemColors.GrayText, Margin = new Padding(0, 9, 0, 0), Text = "2. Verifica la presenza dei file sul server", }); contenuto.Controls.Add(_rigaVerifica); _lblImportStatus.Dock = DockStyle.Top; _lblImportStatus.AutoSize = false; _lblImportStatus.AutoEllipsis = true; _lblImportStatus.Height = 24; _lblImportStatus.ForeColor = SystemColors.GrayText; contenuto.Controls.Add(_lblImportStatus); _gridImport.Dock = DockStyle.Fill; _gridImport.ReadOnly = true; _gridImport.BorderStyle = BorderStyle.None; _gridImport.BackgroundColor = SystemColors.Window; _gridImport.RowHeadersVisible = false; _gridImport.AllowUserToAddRows = false; _gridImport.AllowUserToDeleteRows = false; _gridImport.AllowUserToResizeRows = false; _gridImport.SelectionMode = DataGridViewSelectionMode.FullRowSelect; _gridImport.MultiSelect = false; _gridImport.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; _gridImport.ColumnHeadersHeight = 32; _gridImport.ColumnHeadersDefaultCellStyle.Font = new Font(Font, FontStyle.Bold); _gridImport.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft; _gridImport.RowTemplate.Height = 26; _gridImport.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(245, 247, 250); _gridImport.DefaultCellStyle.SelectionBackColor = ColorePrimario; _gridImport.DefaultCellStyle.Padding = new Padding(4, 0, 4, 0); _gridImport.Columns.Add("componente", "Componente"); _gridImport.Columns.Add("descrizione", "Descrizione"); _gridImport.Columns["componente"]!.FillWeight = 30; _gridImport.Columns["descrizione"]!.FillWeight = 70; var panel = new Panel(); panel.Controls.Add(_gridImport); panel.Controls.Add(contenuto); return panel; } private Panel BuildFaseCreaRaw() { var contenuto = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, ColumnCount = 1, Padding = new Padding(16), }; contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); var rigaCreaRaw = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight, Margin = new Padding(0, 0, 0, 12), }; _btnCreaRaw.Text = "Crea cartella raw"; _btnCreaRaw.Margin = new Padding(0, 0, 8, 0); UiStile.Primario(_btnCreaRaw); _btnCreaRaw.Click += (_, _) => CreaCartellaRaw(); rigaCreaRaw.Controls.Add(_btnCreaRaw); rigaCreaRaw.Controls.Add(new Label { AutoSize = true, ForeColor = SystemColors.GrayText, Margin = new Padding(0, 9, 0, 0), Text = "Copia i file dal server nella cartella 'raw'.", }); contenuto.Controls.Add(rigaCreaRaw); _gridRaw.Dock = DockStyle.Fill; _gridRaw.ReadOnly = true; _gridRaw.BorderStyle = BorderStyle.None; _gridRaw.BackgroundColor = SystemColors.Window; _gridRaw.RowHeadersVisible = false; _gridRaw.AllowUserToAddRows = false; _gridRaw.AllowUserToDeleteRows = false; _gridRaw.AllowUserToResizeRows = false; _gridRaw.SelectionMode = DataGridViewSelectionMode.FullRowSelect; _gridRaw.MultiSelect = false; _gridRaw.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; _gridRaw.ColumnHeadersHeight = 32; _gridRaw.ColumnHeadersDefaultCellStyle.Font = new Font(Font, FontStyle.Bold); _gridRaw.RowTemplate.Height = 26; _gridRaw.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(245, 247, 250); _gridRaw.DefaultCellStyle.SelectionBackColor = ColorePrimario; _gridRaw.DefaultCellStyle.Padding = new Padding(4, 0, 4, 0); _gridRaw.Columns.Add("componente", "Componente"); _gridRaw.Columns.Add("percorso", "Percorso CAD"); _gridRaw.Columns["componente"]!.FillWeight = 25; _gridRaw.Columns["percorso"]!.FillWeight = 75; _gridRaw.CellDoubleClick += (_, _) => ApriCartellaFileSelezionato(); var panel = new Panel(); panel.Controls.Add(_gridRaw); panel.Controls.Add(contenuto); return panel; } private Panel BuildFaseTrovaComponenti() { var contenuto = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, ColumnCount = 1, Padding = new Padding(16), }; contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); var lblIstruzioni = new Label { Dock = DockStyle.Top, AutoSize = true, ForeColor = SystemColors.GrayText, Margin = new Padding(0, 0, 0, 12), Text = "Gli assiemi referenziano altri PRT e sotto-assiemi al loro interno: premi " + "qui per risolvere l'albero componenti (ricorsivamente) e copiare tutto ciò che " + "viene trovato nell'indice dentro 'raw'.", }; contenuto.Controls.Add(lblIstruzioni); _btnTrovaPrt.Text = "Trova componenti referenziati"; UiStile.Secondario(_btnTrovaPrt); _btnTrovaPrt.Click += (_, _) => TrovaPrtReferenziati(); contenuto.Controls.Add(_btnTrovaPrt); var panel = new Panel(); panel.Controls.Add(contenuto); return panel; } private Panel BuildFaseVerifica() { // Padding qui invece che su ogni sotto-pannello: si applica uniformemente a top/fill/bottom // (il layout docked usa il DisplayRectangle del contenitore, già ridotto dal Padding). var panel = new Panel { Padding = new Padding(16) }; var top = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, AutoSizeMode = AutoSizeMode.GrowAndShrink, ColumnCount = 1, }; top.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); var lblAnteprimaAsm = new Label { AutoSize = true, ForeColor = SystemColors.GrayText, Margin = new Padding(0, 0, 0, 4), Text = "File .asm della distinta trovati nell'indice (trascinali su Creo/Esplora risorse):", }; top.Controls.Add(lblAnteprimaAsm); // View.Details invece di LargeIcon: con codici Pro-E lunghi e simili tra loro // ("9244218...", "9244228..."), le icone in griglia troncavano tutti i nomi allo stesso // modo (illeggibili, indistinguibili) e lasciavano un grande vuoto sotto le poche righe. // Una lista a riga singola mostra il nome per intero, in linea con lo stile delle altre // griglie dell'app (_gridImport/_gridRaw), restando comunque trascinabile. _lstAnteprimaAsm.View = View.Details; _lstAnteprimaAsm.MultiSelect = true; _lstAnteprimaAsm.FullRowSelect = true; _lstAnteprimaAsm.GridLines = false; _lstAnteprimaAsm.HeaderStyle = ColumnHeaderStyle.Nonclickable; _lstAnteprimaAsm.SmallImageList = new ImageList { ImageSize = new Size(20, 20) }; _lstAnteprimaAsm.Columns.Add("File .asm", -2); _lstAnteprimaAsm.Dock = DockStyle.Fill; _lstAnteprimaAsm.Margin = new Padding(0, 4, 0, 12); _lstAnteprimaAsm.BorderStyle = BorderStyle.None; // La colonna unica segue sempre la larghezza disponibile (niente scroll orizzontale // né spazio sprecato a destra quando la finestra viene ingrandita). _lstAnteprimaAsm.Resize += (_, _) => { if (_lstAnteprimaAsm.Columns.Count > 0) _lstAnteprimaAsm.Columns[0].Width = _lstAnteprimaAsm.ClientSize.Width; }; _lstAnteprimaAsm.ItemDrag += (_, e) => { var percorsi = _lstAnteprimaAsm.SelectedItems.Cast() .Select(item => (string)item.Tag!) .ToArray(); if (percorsi.Length > 0) _lstAnteprimaAsm.DoDragDrop(new DataObject(DataFormats.FileDrop, percorsi), DragDropEffects.Copy); }; panel.Controls.Add(_lstAnteprimaAsm); panel.Controls.Add(top); return panel; } private void ScegliFileDistinta() { using var dialog = new OpenFileDialog { Filter = "File distinta base (*.xls;*.xml)|*.xls;*.xml|Tutti i file|*.*", Title = "Seleziona il file Distinta Base", }; if (dialog.ShowDialog(this) == DialogResult.OK) ImportaDistinta(dialog.FileName); } private void ImportaDistinta(string percorso) { try { var (_, righe) = DistintaBaseImporter.ConvertiInCsv(percorso, _cartellaLavoro!); _percorsoInputCorrente = percorso; _righeCorrenti = righe; _gridImport.Rows.Clear(); foreach (var (componente, descrizione) in righe) { _gridImport.Rows.Add(componente, descrizione); } _gridRaw.Rows.Clear(); _ultimaCartellaRaw = null; _ultimaVerifica = null; ImpostaStatoImport("", Color.Black); _rigaVerifica.Visible = true; } catch (Exception ex) { _percorsoInputCorrente = null; _righeCorrenti = null; _gridImport.Rows.Clear(); ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed); _rigaVerifica.Visible = false; } AggiornaStatoAvanti(); } /// Imposta testo/colore dello stato import e il tooltip con il testo completo /// (la label tronca con "..." quando il percorso non ci sta in una riga). private void ImpostaStatoImport(string testo, Color colore) { _lblImportStatus.ForeColor = colore; _lblImportStatus.Text = testo; _tipImportStatus.SetToolTip(_lblImportStatus, testo); } private void VerificaCodiciNelDb() { if (_righeCorrenti is null) return; var trovati = TrovaPercorsiCad(_righeCorrenti.Select(r => r.Componente)); _ultimaVerifica = trovati; foreach (DataGridViewRow riga in _gridImport.Rows) { string componente = (string)riga.Cells["componente"].Value!; riga.DefaultCellStyle.BackColor = trovati.ContainsKey(componente) ? Color.FromArgb(198, 239, 206) : Color.FromArgb(255, 199, 206); } AggiornaStatoAvanti(); } private void CreaCartellaRaw() { if (_percorsoInputCorrente is null || _righeCorrenti is null) return; // L'assieme generale è già una riga di _righeCorrenti (aggiunta da // DistintaBaseImporter.ConvertiInCsv leggendo il nome del file distinta), quindi viene // trovato e copiato in raw come qualsiasi altro componente, senza bisogno di un caso speciale. var percorsiTrovati = _ultimaVerifica ?? TrovaPercorsiCad(_righeCorrenti.Select(r => r.Componente)); _gridRaw.Rows.Clear(); foreach (var (componente, _) in _righeCorrenti) { string percorsoCad = percorsiTrovati.TryGetValue(componente, out var lista) ? string.Join("; ", lista) : ""; _gridRaw.Rows.Add(componente, percorsoCad); } var cartellaRaw = Path.Combine(_cartellaLavoro!, "raw"); int copiati = CopiaFileTrovati(percorsiTrovati.Values.SelectMany(p => p).Distinct(), cartellaRaw); _ultimaCartellaRaw = cartellaRaw; ImpostaStatoImport( $"{percorsiTrovati.Count} componenti con percorso CAD trovato su {_righeCorrenti.Count}. " + $"{copiati} file copiati in {cartellaRaw}", Color.DarkGreen); AggiornaStatoAvanti(); } /// Svuota in un colpo solo le cartelle "raw" e "step" nella cartella di lavoro, /// previa un'unica conferma. Non tocca l'indice CAD: solo le copie locali generate dalla /// pipeline. private void PulisciRawEStep() { string radice = _cartellaLavoro!; var cartelle = new[] { Path.Combine(radice, "raw"), Path.Combine(radice, "step") } .Where(Directory.Exists) .ToList(); if (cartelle.Count == 0) { MessageBox.Show(this, $"Nessuna cartella 'raw' o 'step' trovata in {radice}.", "Pulisci raw e step", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var conferma = MessageBox.Show(this, $"Eliminare tutti i file in:\n{string.Join("\n", cartelle)}\n\nL'operazione non è reversibile.", "Pulisci raw e step", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (conferma != DialogResult.Yes) return; int eliminati = 0; foreach (var cartella in cartelle) { foreach (var file in Directory.GetFiles(cartella)) { try { File.Delete(file); eliminati++; } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // ponytail: file in uso (es. aperto in Creo) -> si salta, non blocca gli altri } } } MessageBox.Show(this, $"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", "Pulisci raw e step", MessageBoxButtons.OK, MessageBoxIcon.Information); } /// Tutti i codici prt/asm noti all'indice, con i relativi percorsi (ultima /// versione, un solo file per tipo - stesso criterio di dedup per data_modifica DESC /// usato ovunque in questa classe). Caricato in un'unica query invece che una per /// codice: serve sia a risolvere i percorsi dei componenti trovati, sia a /// AssemblyComponentScanner per validare i token generici (parti commerciali). private static Dictionary> CaricaCodiciNoti(string dbPath) { var codiciNoti = new Dictionary>(StringComparer.OrdinalIgnoreCase); using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione " + "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC"; using var reader = command.ExecuteReader(); while (reader.Read()) { string codice = reader.GetString(0); string tipo = reader.GetString(1).ToLowerInvariant(); var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new(); if (!lista.Any(x => x.Tipo == tipo)) lista.Add((tipo, reader.GetString(2))); } return codiciNoti; } /// Tutti i file prt/asm dell'indice raggruppati per codice, tenendo per ogni /// cartella la sola versione più alta. A differenza di non /// sceglie un vincitore fra le cartelle: lo stesso codice può esistere in più punti della /// share (copie in cartelle di studio, di lavoro, temporanee) con contenuto diverso, e /// quale sia quello giusto dipende da chi lo referenzia - vedi /// . private static Dictionary> CaricaCandidati(string dbPath) { var candidati = new Dictionary>(StringComparer.OrdinalIgnoreCase); using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly"); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT codice, tipo_file, percorso_completo, versione, data_modifica FROM files " + "WHERE LOWER(tipo_file) IN ('prt', 'asm')"; using var reader = command.ExecuteReader(); while (reader.Read()) { string codice = reader.GetString(0); string tipo = reader.GetString(1).ToLowerInvariant(); string percorso = reader.GetString(2); int versione = reader.GetInt32(3); var lista = candidati.TryGetValue(codice, out var l) ? l : candidati[codice] = new(); string cartella = Path.GetDirectoryName(percorso) ?? ""; int esistente = lista.FindIndex(x => x.Tipo == tipo && string.Equals(Path.GetDirectoryName(x.Percorso), cartella, StringComparison.OrdinalIgnoreCase)); if (esistente < 0) lista.Add((tipo, percorso, versione, reader.GetString(4))); else if (versione > lista[esistente].Versione) lista[esistente] = (tipo, percorso, versione, reader.GetString(4)); } return candidati; } /// Fra più copie dello stesso codice sulla share sceglie quella che userebbe Creo: /// prima cerca nella cartella dell'assieme che la referenzia (Creo risolve i componenti /// partendo da lì), poi ripiega sulla versione più alta e, a parità, sulla più recente. /// Senza questo criterio si finisce per copiare in "raw" una copia omonima ma di un altro /// modello - Creo la carica e i vincoli di accoppiamento non si risolvono, quindi l'assieme /// non si apre affatto. private static List<(string Tipo, string Percorso)> ScegliPerCartella( List<(string Tipo, string Percorso, int Versione, string Data)> candidati, string? cartellaPadre) { return candidati .GroupBy(c => c.Tipo) .Select(perTipo => { var scelto = perTipo .OrderByDescending(c => cartellaPadre is not null && string.Equals(Path.GetDirectoryName(c.Percorso), cartellaPadre, StringComparison.OrdinalIgnoreCase)) .ThenByDescending(c => c.Versione) .ThenByDescending(c => c.Data) .First(); return (scelto.Tipo, scelto.Percorso); }) .ToList(); } /// Codici referenziati direttamente in un assieme: prova prima il parser esatto /// dei record binari (, vedi docs/formato-asm-record.md), che /// non ha i falsi positivi/negativi noti dell'euristica testuale (docs/formato-asm.md); se /// il file non rispetta il formato validato ( - capita su /// .asm di versioni Creo mai riscontrate nei campioni usati per il reverse engineering) /// ripiega su , che ha copertura più ampia anche se /// meno precisa. Un'eventuale IOException (file irraggiungibile) non viene intercettata qui: /// resta responsabilità del chiamante, come prima di questo cambiamento. private static IEnumerable EstraiCodiciReferenziati(string percorsoAsm, ICollection codiciNoti) { try { return AsmRecordReader.EstraiCodiciReferenziati(percorsoAsm); } catch (InvalidDataException) { return AssemblyComponentScanner.EstraiCodiciReferenziati(percorsoAsm, codiciNoti); } } /// Componenti soppressi di un assieme (): /// non sono nella distinta, ma senza il loro file Creo non apre l'assieme. Vuoto per i file /// che il parser esatto non sa leggere: lì si usa l'euristica testuale, che i piazzamenti li /// riconosce già per conto suo (). private static IEnumerable CodiciSoppressi(string percorsoAsm) { try { return AsmRecordReader.EstraiCodiciSoppressi(percorsoAsm); } catch (InvalidDataException) { return Array.Empty(); } } /// Cartella della share da cui proviene un file già copiato in "raw": fra le copie /// omonime indicizzate è quella di pari dimensione. Serve per gli assiemi di partenza, che /// arrivano dalla fase precedente senza portarsi dietro l'origine. Null se non si riesce a /// stabilirla (il file non è nell'indice, o la share non risponde): in quel caso la scelta /// del componente ricade sul criterio generale. private static string? CartellaDiOrigine(string fileInRaw, Dictionary> candidati) { string nome = Path.GetFileName(fileInRaw); if (!candidati.TryGetValue(nome.Split('.')[0], out var perCodice)) return null; try { long dimensione = new FileInfo(fileInRaw).Length; foreach (var c in perCodice.Where(c => string.Equals(Path.GetFileName(c.Percorso), nome, StringComparison.OrdinalIgnoreCase))) { var info = new FileInfo(c.Percorso); if (info.Exists && info.Length == dimensione) return Path.GetDirectoryName(c.Percorso); } } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // share non raggiungibile: si prosegue col criterio generale } return null; } /// Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati /// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li /// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li /// accoda per essere scansionati finché non restano solo PRT (foglie) o codici non presenti /// nell'indice. Usata dal bottone "Trova componenti referenziati". private (List<(string Codice, string Tipo, string Percorso)> Trovati, List NonTrovati) RisolviAlberoComponenti( IEnumerable asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null) { // Ogni assieme viaggia con la cartella della share da cui proviene: e' li' che Creo // cerca per primi i suoi componenti, quindi e' li' che li cerchiamo anche noi. // "Esterno" marca i rami raggiunti da un riferimento esterno invece che dalla distinta: // servono su disco ma non sono componenti, quindi i loro codici mancanti non vanno fra i // "non trovati" (sarebbero falsi allarmi, come per i soppressi). var daScansionare = new Queue<(string File, string? CartellaOrigine, bool Esterno)>( asmIniziali.Select(f => (f, (string?)null, false))); var codiciIniziali = asmIniziali.Select(f => Path.GetFileName(f).Split('.')[0]).ToList(); var codiciVisti = new HashSet( codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)), StringComparer.OrdinalIgnoreCase); var codiciSoppressiVisti = new HashSet(StringComparer.OrdinalIgnoreCase); var codiciEsterniVisti = new HashSet(StringComparer.OrdinalIgnoreCase); var trovati = new List<(string Codice, string Tipo, string Percorso)>(); var codiciNonTrovati = new List(); if (daScansionare.Count == 0 || _dbPath is null) return (trovati, codiciNonTrovati); var codiciNoti = CaricaCodiciNoti(_dbPath); var candidati = CaricaCandidati(_dbPath); int processati = 0; while (daScansionare.Count > 0) { var (asm, cartellaOrigine, esterno) = daScansionare.Dequeue(); cartellaOrigine ??= CartellaDiOrigine(asm, candidati); processati++; foreach (var codice in EstraiCodiciReferenziati(asm, codiciNoti.Keys)) { if (!codiciVisti.Add(codice)) continue; if (!candidati.TryGetValue(codice, out var perCodice)) { if (!esterno) codiciNonTrovati.Add(codice); continue; } foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine)) { CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!); trovati.Add((codice, tipo, percorso)); if (tipo == "asm") { daScansionare.Enqueue((Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso)), Path.GetDirectoryName(percorso), esterno)); } } } // I componenti soppressi non compaiono nella BOM, ma Creo ne vuole comunque il file // per aprire l'assieme: si copiano senza entrare nel conteggio dei "non trovati" // (non essendo componenti della distinta, segnalarli come mancanti sarebbe un falso // allarme) e senza ricorrervi dentro. foreach (var codice in CodiciSoppressi(asm)) { if (!codiciSoppressiVisti.Add(codice) || codiciVisti.Contains(codice)) continue; if (!candidati.TryGetValue(codice, out var perCodice)) continue; foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine)) { CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!); trovati.Add((codice, tipo, percorso)); } } // I riferimenti esterni (assembly feature, datum copiati verso un altro modello) non // sono componenti e non compaiono fra i piazzamenti, ma Creo carica il modello citato // per aprire l'assieme - e con lui i *suoi* componenti, quindi qui, a differenza dei // soppressi, si ricorre dentro. Vedi docs/formato-asm-record.md §7.5. foreach (var codice in AsmRecordReader.EstraiCodiciRiferimentiEsterni(asm)) { if (!codiciEsterniVisti.Add(codice) || codiciVisti.Contains(codice)) continue; if (!candidati.TryGetValue(codice, out var perCodice)) continue; foreach (var (tipo, percorso) in ScegliPerCartella(perCodice, cartellaOrigine)) { CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!); trovati.Add((codice, tipo, percorso)); if (tipo == "asm") { daScansionare.Enqueue((Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso)), Path.GetDirectoryName(percorso), true)); } } } progress?.Report((processati, processati + daScansionare.Count)); } return (trovati, codiciNonTrovati); } private async void TrovaPrtReferenziati() { if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw)) return; if (_dbPath is null) { ImpostaStatoImport("Nessun database caricato: impossibile risolvere i componenti referenziati.", Color.DarkRed); return; } // L'assieme generale è già in raw come qualsiasi altro assieme importato (vedi // CreaCartellaRaw), quindi rientra qui senza bisogno di un caso speciale. var asmIniziali = Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm.")).ToList(); using var progressForm = new ProgressoComponentiForm(); var progress = new Progress<(int Fatti, int Totale)>(p => progressForm.Aggiorna(p.Fatti, p.Totale)); var task = Task.Run(() => RisolviAlberoComponenti(asmIniziali, progress)); progressForm.Shown += async (_, _) => { await task; progressForm.Close(); }; progressForm.ShowDialog(this); var (trovati, codiciNonTrovati) = await task; int numPrt = trovati.Count(r => r.Tipo == "prt"); int numAsm = trovati.Count(r => r.Tipo == "asm"); ImpostaStatoImport( $"Componenti referenziati: {numPrt} PRT e {numAsm} sotto-assiemi trovati e copiati in raw, " + $"{codiciNonTrovati.Count} codici referenziati non trovati nell'indice.", Color.DarkGreen); using var dialog = new PrtMancantiForm(trovati, codiciNonTrovati, _ultimaCartellaRaw); dialog.ShowDialog(this); } /// Copia i file trovati nella cartella "raw" indicata, creandola se necessario. /// Ritorna il numero di file copiati con successo. Parallelizzata (grado limitato): il /// collo di bottiglia è la latenza di rete verso la share, non la CPU, quindi poche copie /// concorrenti bastano a sovrapporre le attese senza saturare la share. internal static int CopiaFileTrovati(IEnumerable percorsi, string cartellaRaw) { Directory.CreateDirectory(cartellaRaw); int copiati = 0; Parallel.ForEach(percorsi, new ParallelOptions { MaxDegreeOfParallelism = 6 }, percorso => { try { File.Copy(percorso, Path.Combine(cartellaRaw, Path.GetFileName(percorso)), overwrite: true); Interlocked.Increment(ref copiati); } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { // ponytail: file non raggiungibile (share offline, permessi) -> lo si salta, non blocca gli altri } }); return copiati; } /// Cerca, per ogni codice componente, i percorsi dei file CAD (ultima versione, /// solo prt/asm) corrispondenti nell'indice. private Dictionary> TrovaPercorsiCad(IEnumerable codici) { var risultato = new Dictionary>(); if (_dbPath is null) return risultato; using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly"); connection.Open(); var nomiParametri = TipiPredefiniti.Select((_, i) => $"@tipo{i}"); using var command = connection.CreateCommand(); command.CommandText = $""" SELECT tipo_file, percorso_completo FROM files_ultima_versione WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ({string.Join(",", nomiParametri)}) ORDER BY tipo_file, data_modifica DESC """; var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text); for (int i = 0; i < TipiPredefiniti.Length; i++) { command.Parameters.AddWithValue($"@tipo{i}", TipiPredefiniti[i]); } foreach (var codice in codici.Distinct()) { pCodice.Value = codice; using var reader = command.ExecuteReader(); var percorsi = new List(); // Lo stesso codice+tipo può esistere in più cartelle radice (duplicati sulla share): // essendo ordinato per data_modifica DESC, il primo per tipo è il più recente, gli altri si scartano. var tipiVisti = new HashSet(); while (reader.Read()) { if (tipiVisti.Add(reader.GetString(0).ToLowerInvariant())) { percorsi.Add(reader.GetString(1)); } } if (percorsi.Count > 0) risultato[codice] = percorsi; } return risultato; } private static readonly string CartellaImpostazioni = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PrepStep"); private static readonly string PercorsoImpostazioniCartella = Path.Combine(CartellaImpostazioni, "ultima_cartella_lavoro.txt"); /// Propone l'ultima cartella di lavoro usata (se ancora esiste su disco) o, se non /// c'è ancora una preferenza salvata (o punta a una cartella cancellata), la cartella di /// installazione (%LOCALAPPDATA%\Programs\PrepStep col setup). Sempre modificabile con /// "File → Scegli cartella di lavoro...". private void InizializzaCartellaLavoro() { var ultima = LeggiUltimaCartellaLavoro(); _cartellaLavoro = ultima is not null && Directory.Exists(ultima) ? ultima : AppContext.BaseDirectory; AggiornaStatoAvanti(); } private void ScegliCartellaLavoro() { using var dialog = new FolderBrowserDialog { Description = "Seleziona la cartella di lavoro (CSV, raw, step)", SelectedPath = _cartellaLavoro ?? string.Empty, }; if (dialog.ShowDialog(this) != DialogResult.OK) return; string percorso = dialog.SelectedPath; _cartellaLavoro = percorso; SalvaUltimaCartellaLavoro(percorso); AggiornaStatoAvanti(); } private static string? LeggiUltimaCartellaLavoro() { try { return File.Exists(PercorsoImpostazioniCartella) ? File.ReadAllText(PercorsoImpostazioniCartella).Trim() : null; } catch (IOException) { return null; } } private static void SalvaUltimaCartellaLavoro(string path) { try { Directory.CreateDirectory(CartellaImpostazioni); File.WriteAllText(PercorsoImpostazioniCartella, path); } catch (IOException) { // ponytail: non critico se non si riesce a salvare, si ripropone il default al prossimo avvio } } private void InizializzaDatabase() { // Il DB sta sempre nella cartella di installazione; "Apri DB..." ne carica un altro // solo per la sessione corrente, senza ricordarlo al prossimo avvio. string predefinito = Path.Combine(AppContext.BaseDirectory, "cad_index.db"); if (!File.Exists(predefinito)) { _lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo oppure 'Aggiorna indice' per crearlo."; _lblDb.Text = "Nessun database caricato. Premi 'Aggiorna indice' per crearlo da X:\\, oppure 'Apri DB...' se ne hai gi\u00e0 uno."; _lblUltimoAggiornamento.Text = ""; _dbCaricato = false; _indicatoreDb.Invalidate(); AggiornaStatoAvanti(); return; } CaricaDatabase(predefinito); } private void ScegliDatabase() { using var dialog = new OpenFileDialog { Filter = "Database SQLite (*.db)|*.db|Tutti i file (*.*)|*.*", Title = "Seleziona cad_index.db", }; if (dialog.ShowDialog(this) == DialogResult.OK) { CaricaDatabase(dialog.FileName); } } private void CaricaDatabase(string path) { try { using var connection = new SqliteConnection($"Data Source={path};Mode=ReadOnly"); connection.Open(); long totale = (long)(new SqliteCommand("SELECT COUNT(*) FROM files", connection).ExecuteScalar() ?? 0L); object? ultimaScansioneRaw = new SqliteCommand("SELECT MAX(scansionato_il) FROM files", connection).ExecuteScalar(); _dbPath = path; _lblDb.Text = $"Database: {path} ({totale:N0} file indicizzati)"; _lblUltimoAggiornamento.Text = ultimaScansioneRaw is string testoData && DateTime.TryParse(testoData, out var ultimaScansione) ? $"Ultimo aggiornamento indice: {ultimaScansione.ToLocalTime():dd/MM/yyyy HH:mm}" : "Ultimo aggiornamento indice: sconosciuto"; _lblStatus.Text = "Pronto."; } catch (Exception ex) { MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore", MessageBoxButtons.OK, MessageBoxIcon.Error); } _dbCaricato = _dbPath is not null; _indicatoreDb.Invalidate(); AggiornaStatoAvanti(); } private async void AggiornaIndice() { string dbPath = _dbPath ?? Path.Combine(AppContext.BaseDirectory, "cad_index.db"); if (!Directory.Exists(RadiceDiscoDefault)) { MessageBox.Show(this, $"Disco non raggiungibile: {RadiceDiscoDefault}", "Errore", MessageBoxButtons.OK, MessageBoxIcon.Error); return; } _btnAggiorna.Enabled = false; _lblStatus.Text = "Aggiornamento indice in corso..."; using var cts = new CancellationTokenSource(); using var dialog = new ProgressForm(); var progress = new Progress(dialog.Aggiorna); dialog.Annullato += (_, _) => cts.Cancel(); var scanTask = Indexer.ScanAsync(RadiceDiscoDefault, dbPath, progress, cts.Token); // Chiude automaticamente il dialog quando lo scan termina (successo, errore o annullamento) _ = scanTask.ContinueWith(_ => { if (dialog.Visible) { dialog.BeginInvoke(dialog.Close); } }, TaskScheduler.Default); dialog.ShowDialog(this); try { await scanTask; _lblStatus.Text = "Indice aggiornato."; CaricaDatabase(dbPath); } catch (OperationCanceledException) { _lblStatus.Text = "Aggiornamento annullato."; } catch (Exception ex) { MessageBox.Show(this, $"Errore durante l'aggiornamento dell'indice:\n{ex.Message}", "Errore", MessageBoxButtons.OK, MessageBoxIcon.Error); _lblStatus.Text = "Aggiornamento fallito."; } finally { _btnAggiorna.Enabled = true; } } private void ApriCartellaFileSelezionato() { if (_gridRaw.CurrentRow is null) { return; } string? percorsi = _gridRaw.CurrentRow.Cells[1].Value as string; if (string.IsNullOrEmpty(percorsi)) { return; } string primoPercorso = percorsi.Split("; ", 2)[0]; try { Process.Start("explorer.exe", $"/select,\"{primoPercorso}\""); } catch (Exception ex) { MessageBox.Show(this, $"Impossibile aprire Esplora risorse:\n{ex.Message}", "Errore", MessageBoxButtons.OK, MessageBoxIcon.Error); } } }