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<File> sourcesList;
|
@FXML private TextField projetField;
|
@FXML private TextField versionField;
|
@FXML private TextField titreAideField;
|
@FXML private TextField outputField;
|
@FXML private ComboBox<String> 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<File> 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<Path> 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<Path> 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();
|
}
|
}
|