package com.megatim.manuel.studio; import com.megatim.manuel.core.convert.ConverterFactory; import com.megatim.manuel.core.generate.ManuelGenerator; import com.megatim.manuel.core.generate.SourcesManifest; import com.megatim.manuel.core.theme.ManuelTheme; import com.megatim.manuel.core.viewer.HelpViewer; import java.io.File; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.ResourceBundle; import javafx.application.Platform; import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.ProgressBar; import javafx.scene.control.TextArea; import javafx.scene.control.TextField; import javafx.scene.input.DragEvent; import javafx.scene.input.TransferMode; import javafx.scene.layout.BorderPane; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Contrôleur principal du studio : liste des documents sources, métadonnées, * dossier de sortie, thème, génération en tâche de fond et aperçu intégré. */ public class StudioController implements Initializable { private static final Logger LOGGER = LoggerFactory.getLogger(StudioController.class); private static final String THEME_DEFAUT = "Sirius (défaut)"; @FXML private ListView sourcesList; @FXML private TextField projetField; @FXML private TextField versionField; @FXML private TextField titreAideField; @FXML private TextField outputField; @FXML private ComboBox themeCombo; @FXML private Button btnGenerate; @FXML private BorderPane previewContainer; @FXML private Label statusLabel; @FXML private ProgressBar progressBar; @FXML private TextArea journalArea; private final StudioPrefs prefs = new StudioPrefs(); private ManuelTheme theme = ManuelTheme.defaultTheme(); @Override public void initialize(URL location, ResourceBundle resources) { sourcesList.setCellFactory(lv -> new ListCell<>() { @Override protected void updateItem(File f, boolean empty) { super.updateItem(f, empty); setText(empty || f == null ? null : f.getName()); } }); // Glisser-déposer de documents sur la liste sourcesList.setOnDragOver(this::onDragOver); sourcesList.setOnDragDropped(e -> { if (e.getDragboard().hasFiles()) { e.getDragboard().getFiles().forEach(this::addSourceFile); e.setDropCompleted(true); } e.consume(); }); themeCombo.getItems().add(THEME_DEFAUT); themeCombo.getSelectionModel().select(THEME_DEFAUT); themeCombo.setOnAction(e -> { String sel = themeCombo.getSelectionModel().getSelectedItem(); if (THEME_DEFAUT.equals(sel)) { theme = ManuelTheme.defaultTheme(); prefs.setThemePath(""); reloadPreview(); } }); // Restauration de la session précédente projetField.setText(prefs.getTitreProjet()); versionField.setText(prefs.getVersion()); titreAideField.setText(prefs.getTitreAide()); outputField.setText(prefs.getOutputDir()); if (!prefs.getThemePath().isEmpty()) { applyThemeFile(Path.of(prefs.getThemePath()), false); } if (!outputField.getText().isBlank()) { reloadSourcesManifest(); } reloadPreview(); } private void onDragOver(DragEvent e) { if (e.getDragboard().hasFiles()) { e.acceptTransferModes(TransferMode.COPY); } e.consume(); } private void addSourceFile(File f) { if (ConverterFactory.isSupported(f.getName()) && !sourcesList.getItems().contains(f)) { sourcesList.getItems().add(f); } } /** Recharge liste des sources + métadonnées depuis {@code sources.json} du dossier choisi. */ private void reloadSourcesManifest() { try { Path repSources = outputDir().resolve(ManuelGenerator.SOURCES_DIR); SourcesManifest manifest = SourcesManifest.load(repSources); if (manifest == null) { return; } sourcesList.getItems().clear(); for (String name : manifest.fichiers) { File f = repSources.resolve(name).toFile(); if (f.isFile()) { sourcesList.getItems().add(f); } } if (manifest.titreProjet != null) projetField.setText(manifest.titreProjet); if (manifest.version != null) versionField.setText(manifest.version); if (manifest.titreAide != null) titreAideField.setText(manifest.titreAide); if (manifest.dateGeneration != null) { statusLabel.setText("Dernière génération : " + manifest.dateGeneration); } } catch (Exception ex) { LOGGER.warn("Lecture de sources.json impossible", ex); } } /** Recharge l'aperçu du manuel généré ; placeholder si aucun manuel. */ private void reloadPreview() { try { HelpViewer viewer = new HelpViewer(outputDir(), theme); previewContainer.setCenter(viewer.build()); } catch (Exception ex) { Label placeholder = new Label("Aucun manuel dans ce dossier pour l'instant.\n" + "Choisissez un dossier de sortie, ajoutez des documents sources\n" + "puis cliquez sur « Générer le manuel »."); placeholder.setStyle("-fx-text-fill: #888888;"); previewContainer.setCenter(placeholder); } } private Path outputDir() { return Path.of(outputField.getText().trim()); } @FXML public void addSources(ActionEvent event) { FileChooser chooser = new FileChooser(); chooser.setTitle("Ajouter des documents sources"); File lastDir = new File(prefs.getLastSourceDir()); if (lastDir.isDirectory()) { chooser.setInitialDirectory(lastDir); } chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter( "Documents Word / PowerPoint (*.docx, *.doc, *.pptx, *.ppt)", "*.docx", "*.doc", "*.pptx", "*.ppt")); List files = chooser.showOpenMultipleDialog(currentStage()); if (files == null || files.isEmpty()) { return; } prefs.setLastSourceDir(files.get(0).getParent()); files.forEach(this::addSourceFile); } @FXML public void removeSource(ActionEvent event) { File selected = sourcesList.getSelectionModel().getSelectedItem(); if (selected != null) { sourcesList.getItems().remove(selected); } } @FXML public void moveSourceUp(ActionEvent event) { moveSelected(-1); } @FXML public void moveSourceDown(ActionEvent event) { moveSelected(1); } private void moveSelected(int delta) { int i = sourcesList.getSelectionModel().getSelectedIndex(); int j = i + delta; if (i < 0 || j < 0 || j >= sourcesList.getItems().size()) { return; } File f = sourcesList.getItems().remove(i); sourcesList.getItems().add(j, f); sourcesList.getSelectionModel().select(j); } @FXML public void chooseOutputDir(ActionEvent event) { DirectoryChooser chooser = new DirectoryChooser(); chooser.setTitle("Dossier de sortie du manuel"); File current = outputField.getText().isBlank() ? null : new File(outputField.getText()); if (current != null && current.isDirectory()) { chooser.setInitialDirectory(current); } File dir = chooser.showDialog(currentStage()); if (dir != null) { outputField.setText(dir.getAbsolutePath()); prefs.setOutputDir(dir.getAbsolutePath()); reloadSourcesManifest(); reloadPreview(); } } @FXML public void loadThemeFile(ActionEvent event) { FileChooser chooser = new FileChooser(); chooser.setTitle("Charger un thème (.properties)"); chooser.getExtensionFilters().add( new FileChooser.ExtensionFilter("Thème (*.properties)", "*.properties")); File file = chooser.showOpenDialog(currentStage()); if (file != null) { applyThemeFile(file.toPath(), true); } } private void applyThemeFile(Path file, boolean showError) { try { theme = ThemeLoader.load(file); prefs.setThemePath(file.toString()); String label = file.getFileName().toString(); if (!themeCombo.getItems().contains(label)) { themeCombo.getItems().add(label); } themeCombo.getSelectionModel().select(label); reloadPreview(); } catch (Exception ex) { LOGGER.warn("Chargement du theme impossible : {}", file, ex); if (showError) { error("Impossible de charger le thème : " + ex.getMessage()); } } } @FXML public void generateManuel(ActionEvent event) { if (outputField.getText().isBlank()) { error("Choisissez d'abord le dossier de sortie du manuel."); return; } if (sourcesList.getItems().isEmpty()) { error("Ajoutez au moins un document source (Word ou PowerPoint)."); return; } String titreAide = titreAideField.getText() != null ? titreAideField.getText().trim() : ""; if (titreAide.isEmpty()) { error("Le titre de l'aide est obligatoire."); return; } String projet = projetField.getText() != null ? projetField.getText().trim() : ""; String version = versionField.getText() != null ? versionField.getText().trim() : ""; if (version.isEmpty()) { version = "1.0.0"; versionField.setText(version); } prefs.setTitreProjet(projet); prefs.setVersion(version); prefs.setTitreAide(titreAide); prefs.setOutputDir(outputField.getText().trim()); List sources = new ArrayList<>(); for (File f : sourcesList.getItems()) { sources.add(f.toPath()); } final Path out = outputDir(); final String fProjet = projet; final String fVersion = version; final ManuelTheme fTheme = theme; btnGenerate.setDisable(true); progressBar.setVisible(true); progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS); statusLabel.setText("Génération en cours…"); journalArea.clear(); Task task = new Task<>() { @Override protected Path call() throws Exception { return new ManuelGenerator(fTheme).generate(out, sources, fProjet, fVersion, titreAide, line -> Platform.runLater(() -> journalArea.appendText(line + "\n"))); } }; task.setOnSucceeded(e -> { btnGenerate.setDisable(false); progressBar.setVisible(false); statusLabel.setText("Manuel généré avec succès."); reloadSourcesManifest(); reloadPreview(); }); task.setOnFailed(e -> { btnGenerate.setDisable(false); progressBar.setVisible(false); statusLabel.setText("Échec de la génération."); Throwable ex = task.getException(); LOGGER.error("Echec de la generation du manuel", ex); journalArea.appendText("ERREUR : " + (ex != null ? ex.getMessage() : "inconnue") + "\n"); error("Échec de la génération : " + (ex != null ? ex.getMessage() : "erreur inconnue")); }); Thread worker = new Thread(task, "manuel-studio-generator"); worker.setDaemon(true); worker.start(); } private void error(String message) { Alert alert = new Alert(Alert.AlertType.ERROR, message); alert.setHeaderText("Manuel Studio"); alert.initOwner(currentStage()); alert.showAndWait(); } private Stage currentStage() { return (Stage) sourcesList.getScene().getWindow(); } }