Switch v Replay neměl default větev: INSERT, ELLIPSE, POINT, SOLID, 3DFACE a POLYLINE3D propadly bez akce a bez záznamu. V dodávaných souborech žádná z nich není, ale uživatel si přidává vlastní loga – to nejhorší, co se mu může stát, je logo, které se nevykreslí a nikdo mu to neřekne. - INSERT se rozvine rekurzivně (vztažný bod, měřítko os, natočení), s ochranou proti bloku vkládajícímu sám sebe. Uvnitř bloku se hladina „0" a barva ByBlock berou od vkládající entity – proto jde stejný blok vložit dvakrát a pokaždé v jiné barvě. - TransformRenderer umí celou afinní transformaci, ne jen měřítko a posun. Oblouk zůstane obloukem jen u podobnosti bez zrcadlení; jinak by z kružnice byla elipsa, kterou kreslicí rozhraní nezná, tak se rozloží na lomenou čáru. - ELLIPSE, POINT, SOLID, 3DFACE, POLYLINE3D se kreslí. U SOLID pozor na prohozený 3. a 4. roh, jinak vyjdou přesýpací hodiny. - Zbytek se počítá do DxfTemplate.NezpracovaneEntity a souhrn exportu (PDF i DXF) ho vypíše. Export se kvůli tomu nezastaví. - Spline se dělí podle délky (dřív pevných 64 dílků – málo pro dlouhé křivky, zbytečně moc pro krátké). - Verze výstupního DXF se zafixovala na AC1027 (jako zdrojová šablona). Dřív ji určoval default ACadSharp, který může upgrade balíčku tiše změnit. Šablona i všech 9 log projdou s prázdným počítadlem (test). Render log do PNG je pixel po pixelu shodný s předchozím stavem. 114 testů zelených. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1439 lines
67 KiB
C#
1439 lines
67 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 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 (<see cref="Dxf.RozpiskaRenderer"/>).
|
||
/// </summary>
|
||
public sealed class MainViewModel : ObservableObject
|
||
{
|
||
public Projekt Projekt { get; } = new();
|
||
public Nastaveni Nastaveni { get; } = new();
|
||
public Ciselniky Ciselniky { get; } = new();
|
||
public ObservableCollection<Priloha> Prilohy { get; } = new();
|
||
public ObservableCollection<Revize> Revize { get; } = new();
|
||
|
||
// ---- CAD – připojení (PODKLAD_CAD_Layout_Tisk.md §1) ----
|
||
|
||
/// <summary>Zástupná položka „Manuálně" v comboboxu CADu – po výběru se zpřístupní textové pole
|
||
/// <see cref="RucniProgId"/> na vlastní ProgID (verzově konkrétní, např. „ZWCAD.Application.2023").
|
||
/// Je to sentinel (referenční porovnání), do <c>cad.json</c> se neukládá.</summary>
|
||
private static readonly CadEngine RucniPolozka = new() { ProgId = "", Nazev = "Manuálně (zadat ProgID)" };
|
||
|
||
/// <summary>Nabídka CADů v comboboxu = předpřipravené enginy z <c>cad.json</c> (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.</summary>
|
||
public ObservableCollection<CadEngine> PodporovaneCady { get; }
|
||
|
||
private CadEngine? _vybranyCad;
|
||
/// <summary>Aktuálně vybraný CAD engine v Nastavení. Změna se rovnou uloží do <c>cad.json</c>,
|
||
/// aby se výběr pamatoval napříč projekty i restarty aplikace.</summary>
|
||
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
|
||
}
|
||
}
|
||
|
||
/// <summary>True, když je v comboboxu vybrané „Manuálně" – řídí zpřístupnění textového pole ProgID.</summary>
|
||
public bool JeRucniCad => ReferenceEquals(VybranyCad, RucniPolozka);
|
||
|
||
private string _rucniProgId = "";
|
||
/// <summary>Ručně zadaný ProgID (aktivní jen v režimu „Manuálně"). Ukládá se do <c>cad.json</c>
|
||
/// jako vybraný ProgID, takže se po restartu obnoví i s režimem „Manuálně".</summary>
|
||
public string RucniProgId
|
||
{
|
||
get => _rucniProgId;
|
||
set { if (SetField(ref _rucniProgId, value)) { UlozCadEngines(); CommandManager.InvalidateRequerySuggested(); } }
|
||
}
|
||
|
||
/// <summary>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).</summary>
|
||
private string? EfektivniProgId => JeRucniCad
|
||
? (string.IsNullOrWhiteSpace(RucniProgId) ? null : RucniProgId.Trim())
|
||
: VybranyCad?.ProgId;
|
||
|
||
/// <summary>Popisek CADu do hlášek/stavu – v režimu „Manuálně" samotný ProgID, jinak název enginu.</summary>
|
||
private string EfektivniNazev => JeRucniCad ? EfektivniProgId ?? "Manuálně" : VybranyCad?.Nazev ?? "CAD";
|
||
|
||
/// <summary>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.</summary>
|
||
public bool MaVybranyCad => !string.IsNullOrEmpty(EfektivniProgId);
|
||
|
||
/// <summary>Uloží seznam enginů (bez zástupné položky „Manuálně") + výběr do <c>cad.json</c>:
|
||
/// 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í.</summary>
|
||
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";
|
||
/// <summary>Stav posledního pokusu o připojení na CAD (do UI pod tlačítkem „Připojit").</summary>
|
||
public string CadStav { get => _cadStav; private set => SetField(ref _cadStav, value); }
|
||
|
||
private Priloha? _selectedPriloha;
|
||
public Priloha? SelectedPriloha { get => _selectedPriloha; set => SetField(ref _selectedPriloha, value); }
|
||
|
||
/// <summary>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
|
||
/// <see cref="NastavVyber"/> (SelectionUnit=CellOrRowHeader → tah/Ctrl+klik vybírá buňky).</summary>
|
||
public ObservableCollection<Priloha> VybranePrilohy { get; } = new();
|
||
|
||
/// <summary>Převezme aktuální výběr řádků z View. Kromě naplnění <see cref="VybranePrilohy"/>
|
||
/// přepíná i <see cref="Priloha.JeVybrana"/> 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).</summary>
|
||
public void NastavVyber(IEnumerable<Priloha> 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); }
|
||
}
|
||
|
||
/// <summary>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í).</summary>
|
||
private bool MaVyberPriloh => SelectedPriloha is not null || VybranePrilohy.Count > 0;
|
||
|
||
private string _stavovaZprava = "Připraveno";
|
||
/// <summary>Krátká informace ve stavovém řádku (např. průběh exportu „Exportuji rozpisku 1/41").</summary>
|
||
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;
|
||
/// <summary>Cesta k aktuálně otevřenému souboru projektu (null = neuložený).</summary>
|
||
public string? AktualniSoubor
|
||
{
|
||
get => _aktualniSoubor;
|
||
set { if (SetField(ref _aktualniSoubor, value)) { OnPropertyChanged(nameof(NazevSouboru)); OnPropertyChanged(nameof(Titulek)); } }
|
||
}
|
||
|
||
/// <summary>Název souboru pro stavový řádek/titulek.</summary>
|
||
public string NazevSouboru => _aktualniSoubor is null ? "(neuložený projekt)" : Path.GetFileName(_aktualniSoubor);
|
||
|
||
/// <summary>Složka aktuálního .rzp (pro relativní cestu k orientačnímu schématu); null = neuložený projekt.</summary>
|
||
public string? RzpSlozka => string.IsNullOrEmpty(_aktualniSoubor) ? null : Path.GetDirectoryName(_aktualniSoubor);
|
||
|
||
/// <summary>Sestaví mapu „hladina placeholderu → cesta k DXF" (loga + schéma) pro danou přílohu.</summary>
|
||
public IReadOnlyDictionary<string, string> ResolveLoga(Priloha r)
|
||
=> LogaResolver.Resolve(Projekt, r, Ciselniky, RzpSlozka);
|
||
|
||
/// <summary>Jména DXF log z obou složek <c>loga\</c> – 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.</summary>
|
||
public IReadOnlyList<string> DostupnaLoga
|
||
{
|
||
get
|
||
{
|
||
var list = new List<string> { "" };
|
||
list.AddRange(LogaResolver.DostupnaLoga());
|
||
return list;
|
||
}
|
||
}
|
||
|
||
private bool _jeZmeneno;
|
||
/// <summary>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.</summary>
|
||
public bool JeZmeneno
|
||
{
|
||
get => _jeZmeneno;
|
||
private set { if (SetField(ref _jeZmeneno, value)) OnPropertyChanged(nameof(Titulek)); }
|
||
}
|
||
|
||
private bool _jenProCteni;
|
||
/// <summary>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.</summary>
|
||
public bool JenProCteni
|
||
{
|
||
get => _jenProCteni;
|
||
private set { if (SetField(ref _jenProCteni, value)) OnPropertyChanged(nameof(Titulek)); }
|
||
}
|
||
|
||
/// <summary>Titulek hlavního okna: hvězdička = neuložené změny, pak název souboru a případně příznak režimu.</summary>
|
||
public string Titulek =>
|
||
$"{(JeZmeneno ? "*" : "")}{NazevSouboru}{(JenProCteni ? " [jen pro čtení]" : "")} – Rozpisky – generátor rozpisek";
|
||
|
||
/// <summary>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.</summary>
|
||
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; }
|
||
|
||
/// <summary>Žádost o otevření náhledu vybrané přílohy. Okno otvírá View (zná Window/Owner),
|
||
/// VM jen drží enable/disable a SelectedPriloha.</summary>
|
||
public event Action? NahledPozadovan;
|
||
|
||
/// <summary>Šířky sloupců gridu „Seznam příloh" (px) ve fixním pořadí sloupců. UI stav ukládaný
|
||
/// do .rzp; plní ho <see cref="Views.SeznamPrilohView"/> při ručním roztažení sloupce a čte při
|
||
/// obnově. Prázdný = výchozí šířky z XAML.</summary>
|
||
public List<double> SirkySloupcuSeznamu { get; private set; } = new();
|
||
|
||
/// <summary>Žádost, aby View promítl <see cref="SirkySloupcuSeznamu"/> do sloupců gridu (po načtení
|
||
/// projektu / novém projektu).</summary>
|
||
public event Action? SirkySloupcuObnoveny;
|
||
|
||
/// <summary>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í".</summary>
|
||
public void NastavSirkySloupcuSeznamu(IEnumerable<double> 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; }
|
||
|
||
/// <summary>Otevře v Průzkumníku složku, kam si uživatel dává vlastní loga.</summary>
|
||
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<CadEngine>(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<Priloha>())
|
||
.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) ----
|
||
|
||
/// <summary>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 <see cref="JeZmeneno"/>. Číselníky se nesledují – ukládají
|
||
/// se zvlášť vedle .exe.</summary>
|
||
private void SledujZmeny()
|
||
{
|
||
Projekt.PropertyChanged += (_, _) => JeZmeneno = true;
|
||
Nastaveni.PropertyChanged += (_, _) => JeZmeneno = true;
|
||
Prilohy.CollectionChanged += KolekceZmenena;
|
||
Revize.CollectionChanged += KolekceZmenena;
|
||
}
|
||
|
||
/// <summary>Položky přihlášené k <see cref="PolozkaZmenena"/> 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.</summary>
|
||
private readonly Dictionary<object, List<ObservableObject>> _sledovanePolozky = new();
|
||
|
||
/// <summary>Přidání/odebrání řádku je změna; nové řádky navíc začne sledovat po buňkách.</summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>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.</summary>
|
||
private void PrepocitejDuplicity()
|
||
{
|
||
var pocty = new Dictionary<string, int>(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;
|
||
}
|
||
|
||
/// <summary>Posune vybranou přílohu v seznamu o <paramref name="smer"/> (−1 nahoru, +1 dolů).
|
||
/// Řazení kliknutím na hlavičku je vypnuté, pořadí se mění jen těmito tlačítky.</summary>
|
||
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) ----
|
||
|
||
/// <summary>Načte číselníky: dodávaná vrstva vedle .exe + rozdíl uživatele z jeho složky
|
||
/// (<see cref="CiselnikyStore"/>). Pak zapne autosave při přidání/odebrání položky.</summary>
|
||
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();
|
||
}
|
||
|
||
/// <summary>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.</summary>
|
||
public void UlozCiselniky()
|
||
{
|
||
try { CiselnikyStore.UlozRozdil(Ciselniky); }
|
||
catch { StavovaZprava = $"Číselníky se nepodařilo uložit do složky {UzivatelskaData.Slozka}."; }
|
||
}
|
||
|
||
/// <summary>„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í.</summary>
|
||
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) ----
|
||
|
||
/// <summary>Snímek aktuálního stavu projektu pro uložení (číselníky se neukládají).</summary>
|
||
private ProjektDocument BuildDocument() => new()
|
||
{
|
||
Projekt = Projekt,
|
||
Nastaveni = Nastaveni,
|
||
Prilohy = Prilohy.ToList(),
|
||
Revize = Revize.ToList(),
|
||
SirkySloupcuSeznamu = SirkySloupcuSeznamu.ToList(),
|
||
};
|
||
|
||
/// <summary>Naplní živý model z načteného dokumentu (zachová databinding).</summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>Uloží projekt. Když <paramref name="cesta"/> 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í).</summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>Uvolní zámek aktuálního souboru (zavření projektu/appky).</summary>
|
||
public void UvolniSoubor()
|
||
{
|
||
_souborHandle?.Dispose();
|
||
_souborHandle = null;
|
||
}
|
||
|
||
/// <summary>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š.</summary>
|
||
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) ----
|
||
|
||
/// <summary>Podsložky zakládané v cílové složce exportu. Vygenerovaná PDF jdou do „Rozpisky".</summary>
|
||
private static readonly string[] ExportPodslozky =
|
||
{ "__ Otevřená", "__ Uzavřená", "+Přílohy s rozpiskami", "Přílohy", "Rozpisky" };
|
||
|
||
/// <summary>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ý.</summary>
|
||
private IReadOnlyList<Priloha> VyberKExportu()
|
||
{
|
||
var vyber = VybranePrilohy.Count > 0
|
||
? VybranePrilohy.AsEnumerable()
|
||
: SelectedPriloha is not null ? new[] { SelectedPriloha } : Enumerable.Empty<Priloha>();
|
||
return vyber.OrderBy(Prilohy.IndexOf).ToList(); // exportuj odshora dolů dle pořadí seznamu
|
||
}
|
||
|
||
private bool _exportuje;
|
||
/// <summary>Běží dávkový export (PDF/DXF) – řídí viditelnost tlačítka „Zrušit export“ ve stavovém řádku.</summary>
|
||
public bool ExportBezi { get => _exportuje; private set => SetField(ref _exportuje, value); }
|
||
|
||
/// <summary>Požadavek na zrušení běžícího exportu – kontroluje se mezi položkami dávky
|
||
/// (rozdělaná položka se vždy dokončí).</summary>
|
||
private bool _exportZrusen;
|
||
|
||
/// <summary>Ověří kořenovou cestu exportu (musí existovat) a vytvoří podsložku exportu i vnitřní
|
||
/// strukturu složek (<see cref="ExportPodslozky"/>). Vrací základ exportu (kořen/podsložka), nebo
|
||
/// <c>null</c> při chybě (sám zobrazí hlášení s titulkem <paramref name="titulek"/>).</summary>
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// <summary>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
|
||
/// (<see cref="ExportPodslozky"/>) se v případě potřeby vytvoří. Průběh hlásí stavový řádek
|
||
/// (<see cref="StavovaZprava"/>). Když cílové PDF drží jiný program, zeptá se Pokračovat/Přerušit
|
||
/// (<see cref="ZamcenySouborWindow"/>).</summary>
|
||
/// <summary>Načte renderer nad aktuální šablonou rozpisky. Při chybě (chybějící/poškozená šablona)
|
||
/// zobrazí hlášení s titulkem <paramref name="titulek"/> a vrátí false. Sdílené exportem do PDF i DXF.</summary>
|
||
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<Priloha> 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<string>();
|
||
var pouziteLoga = new HashSet<string>(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));
|
||
}
|
||
|
||
/// <summary>Mezera mezi rozpiskami v dlaždicovém DXF (výkresové jednotky).</summary>
|
||
private const double DxfMezera = 10;
|
||
|
||
/// <summary>Počet rozpisek v jedné řadě dlaždicového DXF (pak se zalomí do dalšího řádku).</summary>
|
||
private const int DxfSloupcu = 10;
|
||
|
||
/// <summary>Vyexportuje zadané přílohy do <b>jednoho</b> DXF (<c>_rozpisky.dxf</c> ve složce
|
||
/// <c>__ Otevřená</c>), rozpisky poskládané do mřížky vedle sebe s mezerou <see cref="DxfMezera"/>.
|
||
/// Rozměr buňky bere z celkového <c>!!NOPRINT</c> rámečku šablony. Atributy jsou ve výsledku
|
||
/// TEXT/MTEXT (<see cref="DxfProfileRenderer"/>). Když je cílový soubor používán, zeptá se
|
||
/// Pokračovat/Přerušit.</summary>
|
||
private async Task ExportujDoDxf(IReadOnlyList<Priloha> 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<string>();
|
||
var pouziteLoga = new HashSet<string>(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) ----
|
||
|
||
/// <summary>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.</summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>Čísla objektů projektu (distinct, seřazená) pro výběr.</summary>
|
||
private List<string> ObjektyProVyber() => Prilohy
|
||
.Select(p => (p.CisloObjektu ?? "").Trim())
|
||
.Where(s => s.Length > 0)
|
||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
|
||
.ToList();
|
||
|
||
/// <summary>Nabídka revizí do kódu seznamu: kódy z tabulky revizí projektu, jinak revize
|
||
/// použité v přílohách (R1–R4).</summary>
|
||
private List<string> 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();
|
||
}
|
||
|
||
/// <summary>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).</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>„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.</summary>
|
||
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_<objekt>.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);
|
||
}
|
||
}
|
||
|
||
/// <summary>Název PDF souboru rozpisky: „Jméno rozvržení" (sloupec seznamu) + u zaškrtnutého
|
||
/// sloupce „Rozpiska" (<see cref="Priloha.JeRozpiska"/>) 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).</summary>
|
||
private static string? SestavNazevSouboru(Priloha p) => SestavNazevSouboru(p, sPriponouRozpiska: true);
|
||
|
||
/// <summary>Sestaví název souboru z <see cref="Priloha.JmenoRozvrzeni"/> (ořez, neplatné znaky → „_“).
|
||
/// Přípona „_rozpiska“ (u <see cref="Priloha.JeRozpiska"/>) se přidává jen při exportu rozpisky
|
||
/// (<paramref name="sPriponouRozpiska"/> = true); tisk samotného rozvržení z CADu ji nepřidává –
|
||
/// je to název layoutu, ne rozpisky.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>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
|
||
/// (<see cref="Priloha.JeDuplicitniJmeno"/>), 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).</summary>
|
||
private static bool PotvrditDuplicitniNazvy(IReadOnlyList<Priloha> 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;
|
||
}
|
||
|
||
/// <summary>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.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>Zjistí, zda existující soubor drží jiný proces (nelze ho otevřít pro výhradní zápis).</summary>
|
||
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; }
|
||
}
|
||
|
||
/// <summary>Souhrn dávkového exportu (počet hotových, přeskočených bez jména, chyby) + cílová složka.</summary>
|
||
private static void ZobrazSouhrnExportu(string cilRozpisky, int hotovo, int bezJmena,
|
||
IReadOnlyList<string> chyby, IReadOnlyList<string> 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);
|
||
}
|
||
|
||
/// <summary>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.</summary>
|
||
private static void PripojNepodporovane(System.Text.StringBuilder sb, IReadOnlyList<string> 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}");
|
||
}
|
||
|
||
/// <summary>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.</summary>
|
||
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
|
||
}
|
||
|
||
/// <summary>Ověří připojení na vybraný CAD (jen attach na běžící instanci, nic nespouští) a
|
||
/// výsledek zapíše do <see cref="CadStav"/>. Spojení se hned po ověření zase uvolní – držení
|
||
/// živého <see cref="CadLink"/> napříč akcemi přijde s dalším krokem (úprava rozvržení).</summary>
|
||
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;
|
||
|
||
/// <summary>„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 <see cref="PripojNaCad"/> 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 <see cref="LayoutLogWindow"/>; zápis <c>Meritko</c>/<c>Formaty</c>
|
||
/// zpět proběhne přímo na instancích z <see cref="Prilohy"/>, takže se v mřížce projeví automaticky.</summary>
|
||
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";
|
||
}
|
||
}
|
||
|
||
/// <summary>„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 (<see cref="PockejNaUvolneniSouboru"/>); název souboru je jméno rozvržení bez
|
||
/// přípony „_rozpiska".</summary>
|
||
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<Priloha, string?> sestavNazev = p => SestavNazevSouboru(p, sPriponouRozpiska: false);
|
||
Func<string, bool> 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);
|
||
|
||
/// <summary>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 <see cref="OtevritManualCmd"/>.</summary>
|
||
private void OtevritNapovedu()
|
||
{
|
||
var okno = new NapovedaWindow { Owner = Application.Current?.MainWindow, DataContext = this };
|
||
okno.ShowDialog();
|
||
}
|
||
|
||
/// <summary>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).</summary>
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>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.</summary>
|
||
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);
|
||
}
|
||
}
|
||
}
|