Leonel FOFOU
2026-07-28 32450e295200b2bc7896518ff74161ff26772e2d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
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();
    }
}