using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Windows; using System.Windows.Input; using System.Windows.Threading; using Microsoft.Win32; using ACadSharp.IO; using Rozpisky.Cad; using Rozpisky.Data; using Rozpisky.Dxf; using Rozpisky.Models; using Rozpisky.Rendering; using Rozpisky.Views; using Rozpisky.Xlsx; namespace Rozpisky.ViewModels; /// /// Hlavní VM aplikace (ROZHRANI.md §4). Drží datový model v paměti (projekt, seznam /// příloh, revize, číselníky, nastavení) a akce. Náhled a export do PDF/PNG jedou přes /// renderovací engine (). /// public sealed class MainViewModel : ObservableObject { public Projekt Projekt { get; } = new(); public Nastaveni Nastaveni { get; } = new(); public Ciselniky Ciselniky { get; } = new(); public ObservableCollection Prilohy { get; } = new(); public ObservableCollection Revize { get; } = new(); // ---- CAD – připojení (PODKLAD_CAD_Layout_Tisk.md §1) ---- /// Zástupná položka „Manuálně" v comboboxu CADu – po výběru se zpřístupní textové pole /// na vlastní ProgID (verzově konkrétní, např. „ZWCAD.Application.2023"). /// Je to sentinel (referenční porovnání), do cad.json se neukládá. private static readonly CadEngine RucniPolozka = new() { ProgId = "", Nazev = "Manuálně (zadat ProgID)" }; /// Nabídka CADů v comboboxu = předpřipravené enginy z cad.json (databáze od /// programátora, včetně verzově konkrétních jako „ZWCAD 2023") + zástupná položka „Manuálně". /// CAD je vlastnost stroje, ne zakázky, takže výběr žije mimo projekt. public ObservableCollection PodporovaneCady { get; } private CadEngine? _vybranyCad; /// Aktuálně vybraný CAD engine v Nastavení. Změna se rovnou uloží do cad.json, /// aby se výběr pamatoval napříč projekty i restarty aplikace. public CadEngine? VybranyCad { get => _vybranyCad; set { // WPF ComboBox při inicializaci / přegenerování ItemsSource krátce nastaví SelectedItem = null // a přes TwoWay binding to zapíše sem – to by přepsalo uložený výběr nullem (výběr se pak // po restartu „ztratí"). Uživatel nemá jak vybrat „nic", takže null ignorujeme a vrátíme // původní hodnotu zpět do UI (OnPropertyChanged bez zápisu do cad.json). if (value is null) { if (_vybranyCad is not null) OnPropertyChanged(); return; } if (!SetField(ref _vybranyCad, value)) return; OnPropertyChanged(nameof(JeRucniCad)); UlozCadEngines(); CommandManager.InvalidateRequerySuggested(); // CanExecute akcí závisí na výběru CADu } } /// True, když je v comboboxu vybrané „Manuálně" – řídí zpřístupnění textového pole ProgID. public bool JeRucniCad => ReferenceEquals(VybranyCad, RucniPolozka); private string _rucniProgId = ""; /// Ručně zadaný ProgID (aktivní jen v režimu „Manuálně"). Ukládá se do cad.json /// jako vybraný ProgID, takže se po restartu obnoví i s režimem „Manuálně". public string RucniProgId { get => _rucniProgId; set { if (SetField(ref _rucniProgId, value)) { UlozCadEngines(); CommandManager.InvalidateRequerySuggested(); } } } /// ProgID, se kterým se skutečně připojuje: ručně zadaný v režimu „Manuálně", jinak /// z vybraného enginu. Null/prázdné = není s čím se připojit (blokuje akce). private string? EfektivniProgId => JeRucniCad ? (string.IsNullOrWhiteSpace(RucniProgId) ? null : RucniProgId.Trim()) : VybranyCad?.ProgId; /// Popisek CADu do hlášek/stavu – v režimu „Manuálně" samotný ProgID, jinak název enginu. private string EfektivniNazev => JeRucniCad ? EfektivniProgId ?? "Manuálně" : VybranyCad?.Nazev ?? "CAD"; /// Je vybraný použitelný CAD (běžný engine, nebo „Manuálně" s vyplněným ProgID)? /// Podmínka pro tlačítko „Připojit" i CAD akce. public bool MaVybranyCad => !string.IsNullOrEmpty(EfektivniProgId); /// Uloží seznam enginů (bez zástupné položky „Manuálně") + výběr do cad.json: /// režim (combobox vs. Manuálně) i ruční text zvlášť, aby se po startu obnovil přesně stav UI. /// Chyby zápisu (read-only složka) jen ohlásí. private void UlozCadEngines() { OnPropertyChanged(nameof(MaVybranyCad)); try { var enginy = PodporovaneCady.Where(e => !ReferenceEquals(e, RucniPolozka)).ToList(); // Ruční text ukládáme vždy (i mimo režim „Manuálně"), ať zůstane poslední zadaná hodnota. var vyber = new CadEnginesStore.Vyber( JeRucniCad ? null : VybranyCad?.ProgId, JeRucniCad, RucniProgId); CadEnginesStore.Save(enginy, vyber); } catch { StavovaZprava = "Výběr CADu se nepodařilo uložit do cad.json (složka jen pro čtení?)."; } } private string _cadStav = "Nepřipojeno"; /// Stav posledního pokusu o připojení na CAD (do UI pod tlačítkem „Připojit"). public string CadStav { get => _cadStav; private set => SetField(ref _cadStav, value); } private Priloha? _selectedPriloha; public Priloha? SelectedPriloha { get => _selectedPriloha; set => SetField(ref _selectedPriloha, value); } /// Vícenásobný výběr řádků seznamu (pro „Exportuj výběr do PDF", hromadné smazání, /// aktualizaci/tisk rozvržení). Plní ho View z řádků dotčených DataGrid.SelectedCells přes /// (SelectionUnit=CellOrRowHeader → tah/Ctrl+klik vybírá buňky). public ObservableCollection VybranePrilohy { get; } = new(); /// Převezme aktuální výběr řádků z View. Kromě naplnění /// přepíná i na dotčených řádcích – z toho grid jemně podbarví /// celé vybrané řádky (i při vícenásobném výběru tahem). public void NastavVyber(IEnumerable vyber) { var novy = vyber.ToList(); // Zhasni příznak na řádcích, které už ve výběru nejsou (jinak by zvýraznění „zůstalo viset"). foreach (var p in VybranePrilohy) if (!novy.Contains(p)) p.JeVybrana = false; VybranePrilohy.Clear(); foreach (var p in novy) { p.JeVybrana = true; VybranePrilohy.Add(p); } } /// Je co zpracovat na základě výběru? Vícenásobný výběr řádků, nebo aspoň aktivní řádek. /// Sdílená podmínka pro CanExecute akcí nad přílohami (mazání, export, rozvržení). private bool MaVyberPriloh => SelectedPriloha is not null || VybranePrilohy.Count > 0; private string _stavovaZprava = "Připraveno"; /// Krátká informace ve stavovém řádku (např. průběh exportu „Exportuji rozpisku 1/41"). public string StavovaZprava { get => _stavovaZprava; private set => SetField(ref _stavovaZprava, value); } private Revize? _selectedRevize; public Revize? SelectedRevize { get => _selectedRevize; set => SetField(ref _selectedRevize, value); } // ---- Výběry pro auto-fill (náhrada VLOOKUP, §4.2) ---- private Organizace? _selectedInvestor; public Organizace? SelectedInvestor { get => _selectedInvestor; set { if (!SetField(ref _selectedInvestor, value) || value is null) return; Projekt.Investor = value.Nazev; Projekt.InvestorAdresa = value.Adresa; } } private Organizace? _selectedSlozkaSZ; public Organizace? SelectedSlozkaSZ { get => _selectedSlozkaSZ; set { if (!SetField(ref _selectedSlozkaSZ, value) || value is null) return; Projekt.SlozkaSZ = value.Nazev; Projekt.SlozkaSZAdresa = value.Adresa; } } private Firma? _selectedZhotovitelSOD; public Firma? SelectedZhotovitelSOD { get => _selectedZhotovitelSOD; set { if (!SetField(ref _selectedZhotovitelSOD, value) || value is null) return; Projekt.ZhotovitelSOD = value.Nazev; Projekt.AdresaSOD = value.Adresa; Projekt.TelSOD = value.Telefon; Projekt.EmailSOD = value.Email; } } private VerzeDokumentace? _selectedStupen; public VerzeDokumentace? SelectedStupen { get => _selectedStupen; set { if (!SetField(ref _selectedStupen, value) || value is null) return; Projekt.StupenDokumentace = value.Hodnota; } } // ---- Stav projektu / souboru ---- public int PocetPriloh => Prilohy.Count; private string? _aktualniSoubor; /// Cesta k aktuálně otevřenému souboru projektu (null = neuložený). public string? AktualniSoubor { get => _aktualniSoubor; set { if (SetField(ref _aktualniSoubor, value)) { OnPropertyChanged(nameof(NazevSouboru)); OnPropertyChanged(nameof(Titulek)); } } } /// Název souboru pro stavový řádek/titulek. public string NazevSouboru => _aktualniSoubor is null ? "(neuložený projekt)" : Path.GetFileName(_aktualniSoubor); /// Složka aktuálního .rzp (pro relativní cestu k orientačnímu schématu); null = neuložený projekt. public string? RzpSlozka => string.IsNullOrEmpty(_aktualniSoubor) ? null : Path.GetDirectoryName(_aktualniSoubor); /// Sestaví mapu „hladina placeholderu → cesta k DXF" (loga + schéma) pro danou přílohu. public IReadOnlyDictionary ResolveLoga(Priloha r) => LogaResolver.Resolve(Projekt, r, Ciselniky, RzpSlozka); /// Jména DXF log z obou složek loga\ – uživatelské i dodávané (prázdná položka /// = „žádné") – nabídka pro výběr log financujících institucí v Údajích projektu. public IReadOnlyList DostupnaLoga { get { var list = new List { "" }; list.AddRange(LogaResolver.DostupnaLoga()); return list; } } private bool _jeZmeneno; /// Příznak neuložených změn (dirty flag). Nastavují ho automaticky změny modelu; /// shazuje se po uložení, načtení a založení nového projektu. public bool JeZmeneno { get => _jeZmeneno; private set { if (SetField(ref _jeZmeneno, value)) OnPropertyChanged(nameof(Titulek)); } } private bool _jenProCteni; /// Projekt je otevřený jen pro čtení (soubor drží jiný proces). „Uložit" se chová /// jako „Uložit jako" – nelze přepsat cizí zamčený soubor. public bool JenProCteni { get => _jenProCteni; private set { if (SetField(ref _jenProCteni, value)) OnPropertyChanged(nameof(Titulek)); } } /// Titulek hlavního okna: hvězdička = neuložené změny, pak název souboru a případně příznak režimu. public string Titulek => $"{(JeZmeneno ? "*" : "")}{NazevSouboru}{(JenProCteni ? " [jen pro čtení]" : "")} – Rozpisky – generátor rozpisek"; /// Exkluzivní zámek aktuálně otevřeného souboru (drží se po celou dobu otevření, /// aby ho jiný proces nemohl smazat/přepsat). Null = neuložený projekt bez souboru. private ProjektFileHandle? _souborHandle; // ---- Příkazy ---- public ICommand NovyProjektCmd { get; } public ICommand OtevritProjektCmd { get; } public ICommand UlozitProjektCmd { get; } public ICommand UlozitJakoProjektCmd { get; } public ICommand PridejPrilohuCmd { get; } public ICommand DuplikujPrilohuCmd { get; } public ICommand SmazPrilohuCmd { get; } public ICommand PosunPrilohuNahoruCmd { get; } public ICommand PosunPrilohuDoluCmd { get; } public ICommand NahledPrilohyCmd { get; } public ICommand VyberSchemaCmd { get; } public ICommand PridejReviziCmd { get; } public ICommand SmazReviziCmd { get; } /// Žádost o otevření náhledu vybrané přílohy. Okno otvírá View (zná Window/Owner), /// VM jen drží enable/disable a SelectedPriloha. public event Action? NahledPozadovan; /// Šířky sloupců gridu „Seznam příloh" (px) ve fixním pořadí sloupců. UI stav ukládaný /// do .rzp; plní ho při ručním roztažení sloupce a čte při /// obnově. Prázdný = výchozí šířky z XAML. public List SirkySloupcuSeznamu { get; private set; } = new(); /// Žádost, aby View promítl do sloupců gridu (po načtení /// projektu / novém projektu). public event Action? SirkySloupcuObnoveny; /// View sem hlásí aktuální šířky po ručním roztažení sloupce. Změna = neuložená změna /// projektu (uloží se pak spolu s daty). Shoda s uloženým stavem (i po obnově) je no-op, takže /// promítnutí načtených šířek do gridu projekt zpět „nešpiní". public void NastavSirkySloupcuSeznamu(IEnumerable sirky) { var nove = sirky.ToList(); if (nove.SequenceEqual(SirkySloupcuSeznamu)) return; SirkySloupcuSeznamu = nove; JeZmeneno = true; } public ICommand ExportVyberPdfCmd { get; } public ICommand ExportVsePdfCmd { get; } public ICommand ExportVyberDxfCmd { get; } public ICommand ExportVseDxfCmd { get; } public ICommand ZrusitExportCmd { get; } public ICommand PripojitCadCmd { get; } public ICommand AktualizujRozvrzeniCmd { get; } public ICommand VytiskniRozvrzeniCmd { get; } public ICommand TiskSeznamuCmd { get; } public ICommand ExportSeznamuCmd { get; } public ICommand OtevritNapoveduCmd { get; } public ICommand OtevritManualCmd { get; } /// Otevře v Průzkumníku složku, kam si uživatel dává vlastní loga. public ICommand OtevritSlozkuLogCmd { get; } public ICommand OProgramuCmd { get; } public MainViewModel() { var (cadEngines, vyber) = CadEnginesStore.Load(); // Combobox = předpřipravené enginy + zástupná položka „Manuálně" na konci. PodporovaneCady = new ObservableCollection(cadEngines) { RucniPolozka }; // Ruční text se drží vždy (i mimo režim „Manuálně"), aby po přepnutí zpět na „Manuálně" // – i po restartu – zůstala poslední ručně zadaná hodnota. _rucniProgId = vyber.RucniProgId ?? ""; _vybranyCad = vyber.RucniRezim ? RucniPolozka : cadEngines.FirstOrDefault(e => e.ProgId == vyber.VybranyProgId); Prilohy.CollectionChanged += (_, _) => OnPropertyChanged(nameof(PocetPriloh)); NovyProjektCmd = new RelayCommand(NovyProjekt); OtevritProjektCmd = new RelayCommand(OtevritProjekt); UlozitProjektCmd = new RelayCommand(() => Uloz(AktualniSoubor)); UlozitJakoProjektCmd = new RelayCommand(() => Uloz(null)); PridejPrilohuCmd = new RelayCommand(() => { var p = new Priloha(); Prilohy.Add(p); SelectedPriloha = p; }); DuplikujPrilohuCmd = new RelayCommand( () => { if (SelectedPriloha is null) return; var kopie = SelectedPriloha.Klon(); Prilohy.Insert(Prilohy.IndexOf(SelectedPriloha) + 1, kopie); SelectedPriloha = kopie; }, () => SelectedPriloha is not null); SmazPrilohuCmd = new RelayCommand( () => { // Snapshot: Prilohy.Remove spustí SelectedCellsChanged → NastavVyber přepíše VybranePrilohy, // přes kterou bychom jinak iterovali. var kSmazani = (VybranePrilohy.Count > 0 ? VybranePrilohy.AsEnumerable() : SelectedPriloha is not null ? new[] { SelectedPriloha } : Enumerable.Empty()) .ToList(); foreach (var p in kSmazani) Prilohy.Remove(p); SelectedPriloha = null; }, () => MaVyberPriloh); PosunPrilohuNahoruCmd = new RelayCommand( () => PosunPrilohu(-1), () => SelectedPriloha is not null && Prilohy.IndexOf(SelectedPriloha) > 0); PosunPrilohuDoluCmd = new RelayCommand( () => PosunPrilohu(+1), () => SelectedPriloha is not null && Prilohy.IndexOf(SelectedPriloha) is var i && i >= 0 && i < Prilohy.Count - 1); NahledPrilohyCmd = new RelayCommand( () => NahledPozadovan?.Invoke(), () => SelectedPriloha is not null); VyberSchemaCmd = new RelayCommand(VyberSchema, () => SelectedPriloha is not null); PridejReviziCmd = new RelayCommand(() => { var r = new Revize(); Revize.Add(r); SelectedRevize = r; }); SmazReviziCmd = new RelayCommand( () => { if (SelectedRevize is not null) Revize.Remove(SelectedRevize); }, () => SelectedRevize is not null); ExportVyberPdfCmd = new RelayCommand( () => _ = ExportujDoPdf(VyberKExportu()), () => MaVyberPriloh); ExportVsePdfCmd = new RelayCommand( () => _ = ExportujDoPdf(Prilohy.ToList()), () => Prilohy.Count > 0); ExportVyberDxfCmd = new RelayCommand( () => _ = ExportujDoDxf(VyberKExportu()), () => MaVyberPriloh); ExportVseDxfCmd = new RelayCommand( () => _ = ExportujDoDxf(Prilohy.ToList()), () => Prilohy.Count > 0); ZrusitExportCmd = new RelayCommand( () => { _exportZrusen = true; StavovaZprava = "Ruším export…"; }, () => ExportBezi); PripojitCadCmd = new RelayCommand(PripojNaCad, () => MaVybranyCad); AktualizujRozvrzeniCmd = new RelayCommand( () => _ = AktualizovatRozvrzeni(), () => MaVybranyCad && MaVyberPriloh); VytiskniRozvrzeniCmd = new RelayCommand( () => _ = VytisknoutRozvrzeni(), () => MaVybranyCad && MaVyberPriloh); TiskSeznamuCmd = new RelayCommand(TisknoutSeznamPdf, () => Prilohy.Count > 0); ExportSeznamuCmd = new RelayCommand(ExportujSeznamDoXlsx, () => Prilohy.Count > 0); OtevritNapoveduCmd = new RelayCommand(OtevritNapovedu); OtevritManualCmd = new RelayCommand(OtevritManual); OtevritSlozkuLogCmd = new RelayCommand(OtevritSlozkuLog); OProgramuCmd = new RelayCommand(OProgramu); SledujZmeny(); NactiCiselniky(); DemoData.SeedProjekt(this); JeZmeneno = false; // úvodní data nejsou „neuložená změna" } // ---- Sledování neuložených změn (dirty flag) ---- /// Naváže se na změny modelu (Projekt, Nastavení, položky příloh/revizí i jejich kolekce), /// aby se při jakékoli editaci nastavil . Číselníky se nesledují – ukládají /// se zvlášť vedle .exe. private void SledujZmeny() { Projekt.PropertyChanged += (_, _) => JeZmeneno = true; Nastaveni.PropertyChanged += (_, _) => JeZmeneno = true; Prilohy.CollectionChanged += KolekceZmenena; Revize.CollectionChanged += KolekceZmenena; } /// Položky přihlášené k podle kolekce. Clear() vyvolá /// Reset **bez** OldItems, takže odhlašované položky nejde vzít z eventu – bez této evidence by /// řádky starého projektu zůstaly přihlášené a (držené třeba náhledem) dál špinily dirty flag. private readonly Dictionary> _sledovanePolozky = new(); /// Přidání/odebrání řádku je změna; nové řádky navíc začne sledovat po buňkách. private void KolekceZmenena(object? sender, NotifyCollectionChangedEventArgs e) { var sledovane = _sledovanePolozky.TryGetValue(sender!, out var s) ? s : _sledovanePolozky[sender!] = new(); if (e.Action == NotifyCollectionChangedAction.Reset) { foreach (var o in sledovane) o.PropertyChanged -= PolozkaZmenena; sledovane.Clear(); // Reset nemusí znamenat jen Clear – přihlaš, co v kolekci případně zůstalo/přibylo. foreach (ObservableObject o in (System.Collections.IEnumerable)sender!) { o.PropertyChanged += PolozkaZmenena; sledovane.Add(o); } } else { if (e.OldItems is not null) foreach (ObservableObject o in e.OldItems) { o.PropertyChanged -= PolozkaZmenena; sledovane.Remove(o); } if (e.NewItems is not null) foreach (ObservableObject o in e.NewItems) { o.PropertyChanged += PolozkaZmenena; sledovane.Add(o); } } JeZmeneno = true; if (ReferenceEquals(sender, Prilohy)) PrepocitejDuplicity(); } private void PolozkaZmenena(object? sender, PropertyChangedEventArgs e) { // Čistě odvozené / UI příznaky nejsou data projektu – přeskoč je, jinak by pouhé kliknutí // do buňky (výběr řádku → JeVybrana) nebo přepočet kolizí (JeDuplicitniJmeno) označilo // projekt jako neuložený, aniž se cokoli změnilo. if (sender is Priloha && e.PropertyName is nameof(Priloha.JeVybrana) or nameof(Priloha.JeDuplicitniJmeno)) return; JeZmeneno = true; // Změna buňky přílohy může změnit jméno rozvržení → přepočítat kolize. if (sender is Priloha) PrepocitejDuplicity(); } /// Označí řádky, jejichž „Jméno rozvržení" se shoduje s jiným řádkem (kolize názvu /// exportu). Prázdná jména se neporovnávají, porovnání je case-insensitive. private void PrepocitejDuplicity() { var pocty = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var p in Prilohy) { var jmeno = p.JmenoRozvrzeni; if (string.IsNullOrWhiteSpace(jmeno)) continue; pocty[jmeno] = pocty.TryGetValue(jmeno, out var n) ? n + 1 : 1; } foreach (var p in Prilohy) p.JeDuplicitniJmeno = !string.IsNullOrWhiteSpace(p.JmenoRozvrzeni) && pocty.TryGetValue(p.JmenoRozvrzeni, out var n) && n > 1; } /// Posune vybranou přílohu v seznamu o (−1 nahoru, +1 dolů). /// Řazení kliknutím na hlavičku je vypnuté, pořadí se mění jen těmito tlačítky. private void PosunPrilohu(int smer) { if (SelectedPriloha is null) return; int i = Prilohy.IndexOf(SelectedPriloha); int j = i + smer; if (i < 0 || j < 0 || j >= Prilohy.Count) return; Prilohy.Move(i, j); // přesun zachová referenci → řádek zůstane vybraný SelectedPriloha = Prilohy[j]; } // ---- Sdílené číselníky (mimo projekt, vedle .exe) ---- /// Načte číselníky: dodávaná vrstva vedle .exe + rozdíl uživatele z jeho složky /// (). Pak zapne autosave při přidání/odebrání položky. private void NactiCiselniky() { CiselnikyStore.Nacti(Ciselniky); // Autosave na přidání/odebrání položky; úpravy v buňkách se uloží při zavření okna. foreach (var col in new INotifyCollectionChanged[] { Ciselniky.Organizace, Ciselniky.Firmy, Ciselniky.Jmena, Ciselniky.VerzeDokumentace, Ciselniky.Casti, Ciselniky.Kraje }) col.CollectionChanged += (_, _) => UlozCiselniky(); } /// Uloží rozdíl číselníků proti dodávané vrstvě do složky uživatele. Při autosave /// záměrně bez dialogu (nesmí spamovat MessageBoxy) – selhání se ale ukáže ve stavovém řádku, /// ať uživatel ví, že se změny číselníků neukládají. Volá se i při zavření okna. public void UlozCiselniky() { try { CiselnikyStore.UlozRozdil(Ciselniky); } catch { StavovaZprava = $"Číselníky se nepodařilo uložit do složky {UzivatelskaData.Slozka}."; } } /// „Samoučení" našeptávače: jména použitá v projektu/přílohách, která v číselníku /// Jména ještě nejsou, do něj doplní (a tím přes autosave uloží) – nabídka tak roste z používání. private void NaucSeJmena() { void Pridej(string? s) { s = s?.Trim(); if (string.IsNullOrEmpty(s)) return; if (Ciselniky.Jmena.Any(j => string.Equals(j.Hodnota, s, StringComparison.OrdinalIgnoreCase))) return; Ciselniky.Jmena.Add(new Jmeno { Hodnota = s }); // CollectionChanged → autosave } Pridej(Projekt.HIP); foreach (var p in Prilohy) { Pridej(p.Vypracoval); Pridej(p.OdpovednyProjektant); Pridej(p.Specialista); Pridej(p.KontrolovalR1); Pridej(p.KontrolovalR2); Pridej(p.KontrolovalR3); Pridej(p.KontrolovalR4); } } // ---- Ukládání / načítání projektu (ROZHRANI.md §9) ---- /// Snímek aktuálního stavu projektu pro uložení (číselníky se neukládají). private ProjektDocument BuildDocument() => new() { Projekt = Projekt, Nastaveni = Nastaveni, Prilohy = Prilohy.ToList(), Revize = Revize.ToList(), SirkySloupcuSeznamu = SirkySloupcuSeznamu.ToList(), }; /// Naplní živý model z načteného dokumentu (zachová databinding). private void ApplyDocument(ProjektDocument doc) { RozpiskaRenderer.VycistiCache(); // jiný projekt = jiná loga/schémata; ať cache neroste a nedrží staré verze Projekt.CopyFrom(doc.Projekt); Nastaveni.CopyFrom(doc.Nastaveni); Prilohy.Clear(); foreach (var p in doc.Prilohy) Prilohy.Add(p); Revize.Clear(); foreach (var r in doc.Revize) Revize.Add(r); PrepocitejDuplicity(); // Dohledat odpovídající položky číselníků pro comboboxy (podle uloženého názvu/hodnoty). SelectedInvestor = Ciselniky.Organizace.FirstOrDefault(o => o.Nazev == Projekt.Investor); SelectedSlozkaSZ = Ciselniky.Organizace.FirstOrDefault(o => o.Nazev == Projekt.SlozkaSZ); SelectedZhotovitelSOD = Ciselniky.Firmy.FirstOrDefault(f => f.Nazev == Projekt.ZhotovitelSOD); SelectedStupen = Ciselniky.VerzeDokumentace.FirstOrDefault(v => v.Hodnota == Projekt.StupenDokumentace); // Uložené šířky sloupců seznamu → View je promítne do gridu (prázdný seznam = výchozí z XAML). SirkySloupcuSeznamu = doc.SirkySloupcuSeznamu.ToList(); SirkySloupcuObnoveny?.Invoke(); } private void NovyProjekt() { if (!PotvrditZahozeniZmen()) return; ApplyDocument(new ProjektDocument()); SelectedInvestor = SelectedSlozkaSZ = null; SelectedZhotovitelSOD = null; SelectedStupen = null; SelectedPriloha = null; SelectedRevize = null; UvolniSoubor(); // nový projekt nemá soubor → pusť případný zámek AktualniSoubor = null; JenProCteni = false; JeZmeneno = false; } private void OtevritProjekt() { if (!PotvrditZahozeniZmen()) return; var dlg = new OpenFileDialog { Filter = ProjektStore.DialogFilter, DefaultExt = ProjektStore.Pripona }; if (dlg.ShowDialog() != true) return; // Při znovuotevření téhož souboru nejdřív pusť vlastní zámek (jinak by si appka // sama bránila v otevření přes FileShare.Read). if (string.Equals(_souborHandle?.Cesta, dlg.FileName, StringComparison.OrdinalIgnoreCase)) UvolniSoubor(); try { var handle = ProjektFileHandle.Otevri(dlg.FileName, out var doc); UvolniSoubor(); // starý zámek pusť až po úspěšném otevření nového _souborHandle = handle; ApplyDocument(doc); AktualniSoubor = dlg.FileName; JenProCteni = false; JeZmeneno = false; } catch (IOException) { // Soubor drží jiný proces → nabídni otevření jen pro čtení (bez zámku). if (MessageBox.Show( "Soubor je používán jiným programem nebo instancí, takže ho nelze otevřít pro úpravy.\n\n" + "Chcete ho otevřít jen pro čtení?", "Soubor je používán", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes) return; try { var doc = ProjektFileHandle.CtiJenProCteni(dlg.FileName); UvolniSoubor(); ApplyDocument(doc); AktualniSoubor = dlg.FileName; JenProCteni = true; JeZmeneno = false; } catch (Exception ex) { MessageBox.Show($"Projekt se nepodařilo načíst:\n{ex.Message}", "Chyba", MessageBoxButton.OK, MessageBoxImage.Error); } } catch (Exception ex) { MessageBox.Show($"Projekt se nepodařilo načíst:\n{ex.Message}", "Chyba", MessageBoxButton.OK, MessageBoxImage.Error); } } /// Uloží projekt. Když je null, zeptá se na soubor (Uložit jako). /// Vrací true při úspěšném uložení, false při zrušení dialogu nebo chybě (pro guard při zavírání). private bool Uloz(string? cesta) { // Režim jen pro čtení: původní soubor drží jiný proces → „Uložit" musí jít přes „Uložit jako". if (JenProCteni && cesta is not null && string.Equals(cesta, AktualniSoubor, StringComparison.OrdinalIgnoreCase)) cesta = null; if (cesta is null) { var dlg = new SaveFileDialog { Filter = ProjektStore.DialogFilter, DefaultExt = ProjektStore.Pripona, // Už uložený projekt (i „jen pro čtení") → předvyplň jeho původní název i složku, ať // „Uložit jako" nabízí přejmenování/kopii vedle originálu. Neuložený → číslo zakázky, // jinak neutrální „projekt". FileName = !string.IsNullOrEmpty(AktualniSoubor) ? Path.GetFileNameWithoutExtension(AktualniSoubor) : string.IsNullOrWhiteSpace(Projekt.CisloZakazky) ? "projekt" : Projekt.CisloZakazky, InitialDirectory = RzpSlozka ?? "", }; if (dlg.ShowDialog() != true) return false; cesta = dlg.FileName; } try { NaucSeJmena(); // doplň nová jména do číselníku z údajů projektu/příloh var doc = BuildDocument(); if (string.Equals(_souborHandle?.Cesta, cesta, StringComparison.OrdinalIgnoreCase)) { _souborHandle!.Uloz(doc); // zápis přes držený zámek } else { var handle = ProjektFileHandle.Vytvor(cesta, doc); // „Uložit jako" na novou cestu UvolniSoubor(); // starý zámek pusť až po úspěchu _souborHandle = handle; } AktualniSoubor = cesta; JenProCteni = false; // od teď máme vlastní zapisovatelný soubor se zámkem JeZmeneno = false; return true; } catch (IOException ex) { MessageBox.Show($"Soubor nelze uložit – cílový soubor je nejspíš otevřený v jiném programu:\n{ex.Message}", "Soubor je používán", MessageBoxButton.OK, MessageBoxImage.Warning); return false; } catch (Exception ex) { MessageBox.Show($"Projekt se nepodařilo uložit:\n{ex.Message}", "Chyba", MessageBoxButton.OK, MessageBoxImage.Error); return false; } } /// Uvolní zámek aktuálního souboru (zavření projektu/appky). public void UvolniSoubor() { _souborHandle?.Dispose(); _souborHandle = null; } /// Při neuložených změnách se zeptá, zda uložit (Ano/Ne/Storno). Vrací true, když je /// bezpečné pokračovat (uloženo nebo zahodit), false při Storno / zrušeném ukládání → akci přeruš. public bool PotvrditZahozeniZmen() { if (!JeZmeneno) return true; var odpoved = MessageBox.Show( $"Projekt „{NazevSouboru}\" obsahuje neuložené změny.\nChcete je před pokračováním uložit?", "Neuložené změny", MessageBoxButton.YesNoCancel, MessageBoxImage.Warning); return odpoved switch { MessageBoxResult.Yes => Uloz(AktualniSoubor), // false, když uživatel zruší Uložit jako MessageBoxResult.No => true, _ => false, // Storno }; } // ---- Dávkový export rozpisek do PDF (ROZHRANI.md §3.5) ---- /// Podsložky zakládané v cílové složce exportu. Vygenerovaná PDF jdou do „Rozpisky". private static readonly string[] ExportPodslozky = { "__ Otevřená", "__ Uzavřená", "+Přílohy s rozpiskami", "Přílohy", "Rozpisky" }; /// Přílohy pro „Exportuj výběr do PDF": vícenásobný výběr řádků (v pořadí seznamu), /// nebo aktuálně vybraný řádek, když je výběr prázdný. private IReadOnlyList VyberKExportu() { var vyber = VybranePrilohy.Count > 0 ? VybranePrilohy.AsEnumerable() : SelectedPriloha is not null ? new[] { SelectedPriloha } : Enumerable.Empty(); return vyber.OrderBy(Prilohy.IndexOf).ToList(); // exportuj odshora dolů dle pořadí seznamu } private bool _exportuje; /// Běží dávkový export (PDF/DXF) – řídí viditelnost tlačítka „Zrušit export“ ve stavovém řádku. public bool ExportBezi { get => _exportuje; private set => SetField(ref _exportuje, value); } /// Požadavek na zrušení běžícího exportu – kontroluje se mezi položkami dávky /// (rozdělaná položka se vždy dokončí). private bool _exportZrusen; /// Ověří kořenovou cestu exportu (musí existovat) a vytvoří podsložku exportu i vnitřní /// strukturu složek (). Vrací základ exportu (kořen/podsložka), nebo /// null při chybě (sám zobrazí hlášení s titulkem ). private string? PripravZakladExportu(string titulek) { var koren = Nastaveni.CestaExportu?.Trim(); if (string.IsNullOrWhiteSpace(koren)) { MessageBox.Show("Není zadaná kořenová cesta exportu (Nastavení → EXPORT).", titulek, MessageBoxButton.OK, MessageBoxImage.Warning); return null; } if (!Directory.Exists(koren)) { MessageBox.Show($"Kořenová cesta exportu neexistuje:\n{koren}", titulek, MessageBoxButton.OK, MessageBoxImage.Warning); return null; } try { var zaklad = string.IsNullOrWhiteSpace(Nastaveni.SlozkaExportu) ? koren : Path.Combine(koren, Nastaveni.SlozkaExportu.Trim()); foreach (var s in ExportPodslozky) Directory.CreateDirectory(Path.Combine(zaklad, s)); return zaklad; } catch (Exception ex) { MessageBox.Show($"Strukturu složek exportu se nepodařilo vytvořit:\n{ex.Message}", titulek, MessageBoxButton.OK, MessageBoxImage.Error); return null; } } /// Vyexportuje zadané přílohy do PDF do struktury složek pod kořenovou cestou /// (Nastavení → EXPORT). Kořenová cesta musí existovat; podsložka exportu i vnitřní struktura /// () se v případě potřeby vytvoří. Průběh hlásí stavový řádek /// (). Když cílové PDF drží jiný program, zeptá se Pokračovat/Přerušit /// (). /// Načte renderer nad aktuální šablonou rozpisky. Při chybě (chybějící/poškozená šablona) /// zobrazí hlášení s titulkem a vrátí false. Sdílené exportem do PDF i DXF. private bool TryVytvorRenderer(string titulek, out RozpiskaRenderer renderer) { try { renderer = new RozpiskaRenderer(Nastaveni.SablonaDxf); return true; } catch (Exception ex) { MessageBox.Show($"Šablonu rozpisky se nepodařilo načíst:\n{ex.Message}\n\n" + $"Šablona: {RozpiskaRenderer.ResolvePath(Nastaveni.SablonaDxf)}", titulek, MessageBoxButton.OK, MessageBoxImage.Error); renderer = null!; return false; } } private async Task ExportujDoPdf(IReadOnlyList prilohy) { if (ExportBezi) return; // neběž dvakrát naráz if (prilohy.Count == 0) { MessageBox.Show("Není co exportovat – nic není vybráno nebo je seznam příloh prázdný.", "Export do PDF", MessageBoxButton.OK, MessageBoxImage.Information); return; } if (!PotvrditDuplicitniNazvy(prilohy, "Export do PDF")) return; var zaklad = PripravZakladExportu("Export do PDF"); if (zaklad is null) return; var cilRozpisky = Path.Combine(zaklad, "Rozpisky"); if (!TryVytvorRenderer("Export do PDF", out var renderer)) return; NaucSeJmena(); // jména z příloh se zařadí do našeptávače ExportBezi = true; _exportZrusen = false; int hotovo = 0, bezJmena = 0, n = prilohy.Count, index = 0; var chyby = new List(); var pouziteLoga = new HashSet(StringComparer.OrdinalIgnoreCase); try { foreach (var p in prilohy) { if (_exportZrusen) break; // „Zrušit export“ ve stavovém řádku index++; var nazev = SestavNazevSouboru(p); if (nazev is null) { bezJmena++; continue; } StavovaZprava = $"Exportuji rozpisku {index}/{n}: {nazev}"; await Dispatcher.Yield(DispatcherPriority.Background); // nech stavový řádek překreslit var cesta = Path.Combine(cilRozpisky, nazev + ".pdf"); // PDF je otevřené v jiném programu → Pokračovat (zkusit znovu) / Přerušit (zastav export). if (!PockejNaUvolneniSouboru(cesta)) break; try { var loga = ResolveLoga(p); pouziteLoga.UnionWith(loga.Values); renderer.ExportPdf(Projekt, p, Revize, Ciselniky, cesta, loga); hotovo++; } catch (Exception ex) { chyby.Add($"{nazev}.pdf – {ex.Message}"); } } } finally { ExportBezi = false; StavovaZprava = "Připraveno"; } ZobrazSouhrnExportu(cilRozpisky, hotovo, bezJmena, chyby, RozpiskaRenderer.NepodporovanePrvky(pouziteLoga)); } /// Mezera mezi rozpiskami v dlaždicovém DXF (výkresové jednotky). private const double DxfMezera = 10; /// Počet rozpisek v jedné řadě dlaždicového DXF (pak se zalomí do dalšího řádku). private const int DxfSloupcu = 10; /// Vyexportuje zadané přílohy do jednoho DXF (_rozpisky.dxf ve složce /// __ Otevřená), rozpisky poskládané do mřížky vedle sebe s mezerou . /// Rozměr buňky bere z celkového !!NOPRINT rámečku šablony. Atributy jsou ve výsledku /// TEXT/MTEXT (). Když je cílový soubor používán, zeptá se /// Pokračovat/Přerušit. private async Task ExportujDoDxf(IReadOnlyList prilohy) { if (ExportBezi) return; if (prilohy.Count == 0) { MessageBox.Show("Není co exportovat – nic není vybráno nebo je seznam příloh prázdný.", "Export do DXF", MessageBoxButton.OK, MessageBoxImage.Information); return; } var zaklad = PripravZakladExportu("Export do DXF"); if (zaklad is null) return; var cesta = Path.Combine(zaklad, "__ Otevřená", "_rozpisky.dxf"); if (!TryVytvorRenderer("Export do DXF", out var renderer)) return; if (renderer.CellFrame is not { } cell || cell.W <= 0 || cell.H <= 0) { MessageBox.Show("Šablona nemá rámeček (!!NOPRINT / A4) – nelze určit rozměr rozpisky pro mřížku.", "Export do DXF", MessageBoxButton.OK, MessageBoxImage.Error); return; } NaucSeJmena(); ExportBezi = true; _exportZrusen = false; int hotovo = 0, n = prilohy.Count; int cols = Math.Min(DxfSloupcu, Math.Max(1, n)); var dxf = new DxfProfileRenderer(); var chyby = new List(); var pouziteLoga = new HashSet(StringComparer.OrdinalIgnoreCase); try { for (int i = 0; i < n; i++) { if (_exportZrusen) { StavovaZprava = "Připraveno"; return; } // bez zápisu částečného DXF StavovaZprava = $"Generuji rozpisku {i + 1}/{n}"; await Dispatcher.Yield(DispatcherPriority.Background); int col = i % cols, row = i / cols; double ox = col * (cell.W + DxfMezera); double oy = -row * (cell.H + DxfMezera); // další řádky pod sebe (osa Y nahoru) try { var loga = ResolveLoga(prilohy[i]); pouziteLoga.UnionWith(loga.Values); var draw = renderer.BuildDraw(Projekt, prilohy[i], Revize, Ciselniky, vyplnene: true, loga, ramecekA4: true); // Generálka naprázdno: výjimka uprostřed kreslení by ve sdíleném dokumentu // nechala torzo rozpisky (entity přidané před pádem nejde vzít zpět). Do // skutečného DXF se tak kreslí až kresba, která celá prošla. draw(new BoundsRenderer()); draw(new TransformRenderer(dxf, 1.0, ox, oy)); hotovo++; } catch (Exception ex) { var jmeno = string.IsNullOrWhiteSpace(prilohy[i].JmenoRozvrzeni) ? $"#{i + 1}" : prilohy[i].JmenoRozvrzeni; chyby.Add($"{jmeno} – {ex.Message} (rozpiska v DXF není)"); } } StavovaZprava = "Zapisuji DXF…"; await Dispatcher.Yield(DispatcherPriority.Background); // Soubor používán jiným programem → Pokračovat (zkusit znovu) / Přerušit. if (!PockejNaUvolneniSouboru(cesta)) { StavovaZprava = "Připraveno"; return; } // Verzi výstupu drží DxfProfileRenderer (natvrdo, ať ji neurčí default balíčku). DxfWriter.Write(cesta, dxf.Document, binary: true); // binární DXF = výrazně menší soubor } catch (Exception ex) { MessageBox.Show($"DXF se nepodařilo zapsat:\n{ex.Message}", "Export do DXF", MessageBoxButton.OK, MessageBoxImage.Error); return; } finally { ExportBezi = false; StavovaZprava = "Připraveno"; } var sb = new System.Text.StringBuilder(); sb.AppendLine($"Vyexportováno rozpisek do DXF: {hotovo} z {n}"); if (chyby.Count > 0) { sb.AppendLine($"Chyby: {chyby.Count}"); foreach (var ch in chyby) sb.AppendLine($" • {ch}"); } PripojNepodporovane(sb, RozpiskaRenderer.NepodporovanePrvky(pouziteLoga)); sb.AppendLine(); sb.Append($"Soubor: {cesta}"); MessageBox.Show(sb.ToString(), "Export do DXF dokončen", MessageBoxButton.OK, chyby.Count > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information); } // ---- Export seznamu příloh objektu do XLSX (Otevřená verze, ROZHRANI.md §5) ---- /// Vyexportuje „Seznam příloh" zvoleného objektu do .xlsx (vyplní mustr) vedle souboru /// projektu. Projekt musí být uložený (jinak není kam ukládat). Objekt se vybírá v dialogu. private void ExportujSeznamDoXlsx() { // Export jde vedle .rzp → projekt musí být uložený (a bez neuložených změn). if (string.IsNullOrEmpty(AktualniSoubor)) { MessageBox.Show("Projekt zatím není uložený. Seznam se ukládá vedle souboru projektu, " + "proto projekt nejdřív uložte.", "Export seznamu", MessageBoxButton.OK, MessageBoxImage.Information); if (!Uloz(null)) return; // uživatel uložení zrušil } else if (JeZmeneno && !Uloz(AktualniSoubor)) return; // ulož změny, ať seznam odpovídá if (string.IsNullOrEmpty(AktualniSoubor)) return; // pojistka // Výběr objektu + revize do kódu seznamu. var objekty = ObjektyProVyber(); if (objekty.Count == 0) { MessageBox.Show("V seznamu příloh není žádný objekt (vyplněné „Číslo objektu/komplexu“).", "Export seznamu", MessageBoxButton.OK, MessageBoxImage.Information); return; } var revizeMozne = RevizeProVyber(); var vyber = new VyberObjektuWindow(objekty, revizeMozne.Count > 0 ? revizeMozne : null) { Owner = Application.Current?.MainWindow }; if (vyber.ShowDialog() != true || vyber.Vybrane is not { } cisloObjektu) return; var revizeKod = vyber.VybranaRevize; var prilohyObjektu = Prilohy .Where(p => string.Equals(p.CisloObjektu?.Trim(), cisloObjektu, StringComparison.OrdinalIgnoreCase)) .ToList(); if (Path.GetDirectoryName(AktualniSoubor) is not { Length: > 0 } slozkaRzp) { MessageBox.Show($"Ze souboru projektu se nepodařilo určit složku pro seznam:\n{AktualniSoubor}", "Export seznamu", MessageBoxButton.OK, MessageBoxImage.Error); return; } var cil = Path.Combine(slozkaRzp, "SEZNAM_" + ObjektOznaceni.Alfanum(cisloObjektu) + ".xlsx"); // Cílový .xlsx je otevřený v Excelu → Pokračovat (zkusit znovu) / Přerušit. if (!PockejNaUvolneniSouboru(cil)) return; var mustr = RozpiskaRenderer.ResolvePath(Nastaveni.SeznamMustr); if (!File.Exists(mustr)) { MessageBox.Show($"Mustr seznamu (.xlsx) nebyl nalezen:\n{mustr}", "Export seznamu", MessageBoxButton.OK, MessageBoxImage.Error); return; } try { // Pozice 1–34 kódu jsou objektové (libovolná příloha objektu), revize = volba uživatele. var kod = KodPrilohy.SestavSeznam(Projekt, prilohyObjektu[0], revizeKod); SeznamExporter.Export(mustr, cil, Projekt, cisloObjektu, prilohyObjektu, kod); MessageBox.Show($"Seznam příloh objektu {cisloObjektu} byl vyexportován:\n{cil}", "Export seznamu dokončen", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { MessageBox.Show($"Export seznamu se nezdařil:\n{ex.Message}", "Export seznamu", MessageBoxButton.OK, MessageBoxImage.Error); } } /// Čísla objektů projektu (distinct, seřazená) pro výběr. private List ObjektyProVyber() => Prilohy .Select(p => (p.CisloObjektu ?? "").Trim()) .Where(s => s.Length > 0) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) .ToList(); /// Nabídka revizí do kódu seznamu: kódy z tabulky revizí projektu, jinak revize /// použité v přílohách (R1–R4). private List RevizeProVyber() { var z = Revize.Select(r => (r.Cislo ?? "").Trim()).Where(s => s.Length > 0); if (!z.Any()) z = Prilohy.SelectMany(p => new[] { p.R1, p.R2, p.R3, p.R4 }) .Select(s => (s ?? "").Trim()).Where(s => s.Length > 0); return z.Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(s => s, StringComparer.OrdinalIgnoreCase) .ToList(); } /// Nabídne výběr objektu projektu (combobox čísel objektů). Vrací zvolené číslo objektu, /// nebo null (nic k výběru / uživatel zrušil). Používá tisk seznamu (bez výběru revize). private string? VyberObjekt(string titulek) { var objekty = ObjektyProVyber(); if (objekty.Count == 0) { MessageBox.Show("V seznamu příloh není žádný objekt (vyplněné „Číslo objektu/komplexu“).", titulek, MessageBoxButton.OK, MessageBoxImage.Information); return null; } var dlg = new VyberObjektuWindow(objekty) { Owner = Application.Current?.MainWindow }; return dlg.ShowDialog() == true ? dlg.Vybrane : null; } /// „Tisk seznamu": vytiskne dříve vyexportovaný seznam objektu (SEZNAM_*.xlsx vedle .rzp) /// do PDF přes MS Excel; PDF jde do podsložky „Přílohy" ve struktuře exportu. private void TisknoutSeznamPdf() { if (string.IsNullOrEmpty(AktualniSoubor)) { MessageBox.Show("Projekt zatím není uložený. Seznam (.xlsx) se hledá vedle souboru projektu, " + "proto projekt nejdřív uložte (a vyexportujte seznam).", "Tisk seznamu", MessageBoxButton.OK, MessageBoxImage.Information); return; } if (VyberObjekt("Tisk seznamu") is not { } cisloObjektu) return; if (Path.GetDirectoryName(AktualniSoubor) is not { Length: > 0 } slozkaRzp) { MessageBox.Show($"Ze souboru projektu se nepodařilo určit složku se seznamem:\n{AktualniSoubor}", "Tisk seznamu", MessageBoxButton.OK, MessageBoxImage.Error); return; } var xlsx = Path.Combine(slozkaRzp, "SEZNAM_" + ObjektOznaceni.Alfanum(cisloObjektu) + ".xlsx"); if (!File.Exists(xlsx)) { MessageBox.Show($"Seznam pro objekt {cisloObjektu} zatím neexistuje:\n{xlsx}\n\n" + "Nejdřív použijte „Export seznamu“.", "Tisk seznamu", MessageBoxButton.OK, MessageBoxImage.Information); return; } var zaklad = PripravZakladExportu("Tisk seznamu"); if (zaklad is null) return; // Výsledné PDF dle manuálu: objekt_[podobjekt]_XX_X_001 (seznam = „příloha 001", typ XX zůstává). // XLSX mustr zůstává pojmenovaný SEZNAM_.xlsx (vedle .rzp). var pdf = Path.Combine(zaklad, "Přílohy", Priloha.NazevObjektovehoSpecialu(cisloObjektu, "001") + ".pdf"); if (!PockejNaUvolneniSouboru(pdf)) return; try { ExcelToPdf.Convert(xlsx, pdf); MessageBox.Show($"Seznam objektu {cisloObjektu} byl vytištěn do PDF:\n{pdf}", "Tisk seznamu dokončen", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { MessageBox.Show($"Tisk seznamu do PDF se nezdařil:\n{ex.Message}", "Tisk seznamu", MessageBoxButton.OK, MessageBoxImage.Error); } } /// Název PDF souboru rozpisky: „Jméno rozvržení" (sloupec seznamu) + u zaškrtnutého /// sloupce „Rozpiska" () přípona „_rozpiska". Příznak je /// samostatný, nezávislý na „Příloha objektu"/„Desky objektu" – i rozpiska na desky objektu /// může příponu dostat. Neplatné znaky názvu souboru se nahradí podtržítkem. Vrací null, když /// řádek nemá jméno rozvržení (přeskočí se). private static string? SestavNazevSouboru(Priloha p) => SestavNazevSouboru(p, sPriponouRozpiska: true); /// Sestaví název souboru z (ořez, neplatné znaky → „_“). /// Přípona „_rozpiska“ (u ) se přidává jen při exportu rozpisky /// ( = true); tisk samotného rozvržení z CADu ji nepřidává – /// je to název layoutu, ne rozpisky. private static string? SestavNazevSouboru(Priloha p, bool sPriponouRozpiska) { var jmeno = p.JmenoRozvrzeni?.Trim(); if (string.IsNullOrWhiteSpace(jmeno)) return null; if (sPriponouRozpiska && p.JeRozpiska) jmeno += "_rozpiska"; foreach (var c in Path.GetInvalidFileNameChars()) jmeno = jmeno.Replace(c, '_'); return jmeno; } /// Kontrola před exportem do PDF: přílohy se stejným výsledným názvem souboru by si PDF /// navzájem přepsaly (soubor by zůstal jen od poslední z nich). Grid kolize sice podbarvuje /// (), ale při „Exportuj vše" je nemusí mít uživatel před /// očima. Vrací true = pokračovat (žádné kolize, nebo je uživatel vědomě potvrdil). private static bool PotvrditDuplicitniNazvy(IReadOnlyList prilohy, string titulek) { var kolize = prilohy .Select(SestavNazevSouboru) .Where(n => n is not null) .GroupBy(n => n!, StringComparer.OrdinalIgnoreCase) .Where(g => g.Count() > 1) .Select(g => $"{g.Key}.pdf ({g.Count()}×)") .ToList(); if (kolize.Count == 0) return true; return MessageBox.Show( "Některé exportované přílohy mají stejné „Jméno rozvržení“, takže se jejich PDF navzájem " + "přepíšou a zůstane jen poslední z nich:\n\n • " + string.Join("\n • ", kolize) + "\n\nChcete přesto pokračovat?", titulek, MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes; } /// Když cílové PDF drží jiný program, opakovaně se ptá Pokračovat/Přerušit, dokud se /// soubor neuvolní nebo uživatel nezvolí Přerušit. Vrací true = lze zapsat, false = přeruš export. private static bool PockejNaUvolneniSouboru(string cesta) { while (JeSouborZamceny(cesta)) { var dlg = new ZamcenySouborWindow(cesta) { Owner = Application.Current?.MainWindow }; if (dlg.ShowDialog() != true) return false; // Přerušit } return true; } /// Zjistí, zda existující soubor drží jiný proces (nelze ho otevřít pro výhradní zápis). private static bool JeSouborZamceny(string cesta) { if (!File.Exists(cesta)) return false; try { using var fs = new FileStream(cesta, FileMode.Open, FileAccess.ReadWrite, FileShare.None); return false; } catch (IOException) { return true; } catch (UnauthorizedAccessException) { return true; } } /// Souhrn dávkového exportu (počet hotových, přeskočených bez jména, chyby) + cílová složka. private static void ZobrazSouhrnExportu(string cilRozpisky, int hotovo, int bezJmena, IReadOnlyList chyby, IReadOnlyList nepodporovane) { var sb = new System.Text.StringBuilder(); sb.AppendLine($"Vyexportováno do PDF: {hotovo}"); if (bezJmena > 0) sb.AppendLine($"Přeskočeno (bez jména rozvržení): {bezJmena}"); if (chyby.Count > 0) { sb.AppendLine($"Chyby: {chyby.Count}"); foreach (var ch in chyby) sb.AppendLine($" • {ch}"); } PripojNepodporovane(sb, nepodporovane); sb.AppendLine(); sb.Append($"Složka: {cilRozpisky}"); MessageBox.Show(sb.ToString(), "Export do PDF dokončen", MessageBoxButton.OK, chyby.Count > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information); } /// Přidá do souhrnu upozornění na prvky vložených DXF, které kreslítko neumí. /// Export se kvůli nim nezastaví – jen se nevykreslí, a o tom se uživatel musí dozvědět. private static void PripojNepodporovane(System.Text.StringBuilder sb, IReadOnlyList nepodporovane) { if (nepodporovane.Count == 0) return; sb.AppendLine(); sb.AppendLine("Ve vložených DXF jsou prvky, které program neumí vykreslit:"); foreach (var radek in nepodporovane) sb.AppendLine($" • {radek}"); } /// Vybere DXF orientačního schématu pro vybranou přílohu. Leží-li soubor ve složce /// aktuálního .rzp, uloží se relativně (jen jméno), jinak absolutní cestou. private void VyberSchema() { if (SelectedPriloha is null) return; var dlg = new OpenFileDialog { Filter = "Výkres DXF (*.dxf)|*.dxf", DefaultExt = ".dxf", InitialDirectory = RzpSlozka ?? "", }; if (dlg.ShowDialog() != true) return; var dir = RzpSlozka; SelectedPriloha.SchemaSoubor = dir is not null && string.Equals(Path.GetDirectoryName(dlg.FileName), dir, StringComparison.OrdinalIgnoreCase) ? Path.GetFileName(dlg.FileName) // ve složce .rzp → relativně : dlg.FileName; // jinde → absolutní cesta } /// Ověří připojení na vybraný CAD (jen attach na běžící instanci, nic nespouští) a /// výsledek zapíše do . Spojení se hned po ověření zase uvolní – držení /// živého napříč akcemi přijde s dalším krokem (úprava rozvržení). private void PripojNaCad() { if (EfektivniProgId is not { } progId) return; using var filtr = ComMessageFilter.Registruj(); // retry, když je CAD zrovna zaneprázdněný using var cad = new CadLink(); if (!cad.Connect(progId, EfektivniNazev)) { CadStav = cad.LastError ?? $"Připojení na {EfektivniNazev} selhalo."; return; } string nazevVykresu; try { nazevVykresu = cad.Doc!.Name; } catch { nazevVykresu = "?"; } CadStav = $"Připojeno: {cad.Engine} – výkres {nazevVykresu}"; } private bool _cadOperaceBezi; /// „Aktualizuj rozvržení" (PODKLAD_CAD_Layout_Tisk.md §4): pro vybrané řádky seznamu /// přejmenuje odpovídající rozvržení výkresu, vyplní blok hlavičky, spočítá počet papírů z rámečku, /// vyhodnotí měřítko z výřezů a nastaví tiskárnu – vše přes živé spojení na vybraný CAD (na rozdíl /// od zůstává spojení držené po celou dobu akce, ne jen na ověření). /// Průběh a chyby jdou do nemodálního ; zápis Meritko/Formaty /// zpět proběhne přímo na instancích z , takže se v mřížce projeví automaticky. private async Task AktualizovatRozvrzeni() { if (_cadOperaceBezi || !MaVybranyCad) return; var vybrane = VyberKExportu(); if (vybrane.Count == 0) { MessageBox.Show("Není vybrán žádný řádek seznamu (vyberte řádky v Seznamu příloh).", "Aktualizuj rozvržení", MessageBoxButton.OK, MessageBoxImage.Information); return; } var log = new LayoutLogWindow("Aktualizuji rozvržení") { Owner = Application.Current?.MainWindow }; log.Show(); _cadOperaceBezi = true; try { using var filtr = ComMessageFilter.Registruj(); // retry, když je CAD zrovna zaneprázdněný using var cad = new CadLink(); if (!cad.Connect(EfektivniProgId!, EfektivniNazev)) { log.Chyba(cad.LastError ?? $"Připojení na {EfektivniNazev} selhalo."); return; } StavovaZprava = "Aktualizuji rozvržení…"; var cfg = LayoutConfigStore.Load(); // cad.Rodina (z ProgID), ne cad.Engine (popisný název): layout.json páruje tiskárnu/CTB // podle rodiny CADu, ne podle verzovaného/ručního názvu enginu z cad.json. await CadLayoutService.AktualizujRozvrzeni(cad.Doc!, cad.Rodina, vybrane, Projekt, Nastaveni, cfg, log, log.Token); } catch (Exception ex) { log.Chyba($"Neočekávaná chyba: {ex.Message}"); } finally { _cadOperaceBezi = false; StavovaZprava = "Připraveno"; } } /// „Vytiskni rozvržení" (PODKLAD_CAD_Layout_Tisk.md §5): vytiskne rozvržení vybraných /// příloh do PDF do podsložky exportu „Přílohy" (tiskne se celá příloha z CADu – na rozdíl od /// exportu samotné rozpisky, který jde do „Rozpisky"). S exportem do PDF sdílí chování při /// zamčeném cíli (); název souboru je jméno rozvržení bez /// přípony „_rozpiska". private async Task VytisknoutRozvrzeni() { // Breadcrumb hned na začátku (viz CadTiskService.Diag → tisk_diag.log): rozlišuje pád „hned // po kliknutí“ (v tisk_diag.log jen tenhle řádek) od pádu až při tisku (řádky ze CadTiskService). CadTiskService.Diag($"Klik „Vytiskni rozvržení“, CAD = {(MaVybranyCad ? EfektivniNazev : "(žádný)")}"); if (_cadOperaceBezi || !MaVybranyCad) return; var vybrane = VyberKExportu(); if (vybrane.Count == 0) { MessageBox.Show("Není vybrán žádný řádek seznamu (vyberte řádky v Seznamu příloh).", "Vytiskni rozvržení", MessageBoxButton.OK, MessageBoxImage.Information); return; } var zaklad = PripravZakladExportu("Vytiskni rozvržení"); if (zaklad is null) return; var cilPrilohy = Path.Combine(zaklad, "Přílohy"); // tiskne se celá příloha, ne rozpiska var log = new LayoutLogWindow("Tisknu rozvržení") { Owner = Application.Current?.MainWindow }; log.Show(); _cadOperaceBezi = true; try { using var filtr = ComMessageFilter.Registruj(); // retry, když je CAD zrovna zaneprázdněný using var cad = new CadLink(); if (!cad.Connect(EfektivniProgId!, EfektivniNazev)) { log.Chyba(cad.LastError ?? $"Připojení na {EfektivniNazev} selhalo."); return; } StavovaZprava = "Tisknu rozvržení…"; // cad.Doc je dynamic, takže se celé volání váže dynamicky – metody je proto nutné // předat jako explicitně otypované delegáty, ne jako pouhé skupiny metod (CS1976). // Tisk rozvržení z CADu nepřidává příponu „_rozpiska“ – je to název layoutu (na rozdíl // od exportu rozpisky do PDF, který ji přidává). Func sestavNazev = p => SestavNazevSouboru(p, sPriponouRozpiska: false); Func pockejNaUvolneni = PockejNaUvolneniSouboru; await CadTiskService.VytiskniRozvrzeni(cad.Doc!, vybrane, cilPrilohy, Nastaveni, log, sestavNazev, pockejNaUvolneni, log.Token); } catch (Exception ex) { log.Chyba($"Neočekávaná chyba: {ex.Message}"); } finally { _cadOperaceBezi = false; StavovaZprava = "Připraveno"; } } private static void OProgramu() => MessageBox.Show( "Rozpisky – generátor výkresových rohových razítek\n\n" + "Standalone náhrada za původní excelový nástroj \"Rozpisky 1.0.8\".", "O programu", MessageBoxButton.OK, MessageBoxImage.Information); /// Otevře vestavěnou nápovědu k programu (samostatné okno). DataContext = tento VM, /// aby tlačítko „Otevřít PDF manuál" uvnitř okna mohlo bindovat na . private void OtevritNapovedu() { var okno = new NapovedaWindow { Owner = Application.Current?.MainWindow, DataContext = this }; okno.ShowDialog(); } /// Otevře originální manuál SŽ (PDF) uložený vedle .exe ve výchozím prohlížeči PDF. /// Při chybějícím souboru jen upozorní (nespadne). private static void OtevritManual() { var cesta = Path.Combine(AppContext.BaseDirectory, "SZ_SM011_P10_Manual_v6.pdf"); if (!File.Exists(cesta)) { MessageBox.Show("Manuál nebyl nalezen vedle programu:\n" + cesta, "Nápověda", MessageBoxButton.OK, MessageBoxImage.Warning); return; } try { Process.Start(new ProcessStartInfo(cesta) { UseShellExecute = true }); } catch (Exception ex) { MessageBox.Show("Manuál se nepodařilo otevřít:\n" + ex.Message, "Nápověda", MessageBoxButton.OK, MessageBoxImage.Error); } } /// Otevře v Průzkumníku složku pro uživatelská loga (a založí ji, když ještě není). /// Vlastní loga patří sem, ne do složky programu – tu přeinstalace přepíše. private void OtevritSlozkuLog() { var slozka = LogaResolver.UzivatelskaLogaDir; try { Directory.CreateDirectory(slozka); Process.Start(new ProcessStartInfo(slozka) { UseShellExecute = true }); } catch (Exception ex) { MessageBox.Show("Složku s logy se nepodařilo otevřít:\n" + slozka + "\n\n" + ex.Message, "Loga", MessageBoxButton.OK, MessageBoxImage.Error); } } }