Pipeline working, implements FFmpegSetupManager

This commit is contained in:
Guillaume Tâche
2024-08-09 20:30:21 +02:00
parent c2efb71195
commit 155b011c2b
54 changed files with 984 additions and 314 deletions

View File

@@ -1,14 +1,16 @@
package com.github.gtache.autosubtitle.gui.fx;
import com.github.gtache.autosubtitle.File;
import com.github.gtache.autosubtitle.gui.MediaController;
import com.github.gtache.autosubtitle.gui.TimeFormatter;
import com.github.gtache.autosubtitle.impl.FileVideoImpl;
import com.github.gtache.autosubtitle.modules.gui.impl.Pause;
import com.github.gtache.autosubtitle.modules.gui.impl.Play;
import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.gui.fx.SubtitleLabel;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
@@ -31,6 +33,11 @@ import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static java.util.Objects.requireNonNull;
@@ -62,6 +69,8 @@ public class FXMediaController implements MediaController {
private final Image playImage;
private final Image pauseImage;
private final List<Long> startTimes;
private boolean wasPlaying;
@Inject
@@ -72,6 +81,7 @@ public class FXMediaController implements MediaController {
this.timeFormatter = requireNonNull(timeFormatter);
this.playImage = requireNonNull(playImage);
this.pauseImage = requireNonNull(pauseImage);
this.startTimes = new ArrayList<>();
}
@FXML
@@ -101,50 +111,78 @@ public class FXMediaController implements MediaController {
if (videoView.getMediaPlayer() != null) {
videoView.getMediaPlayer().dispose();
}
if (newValue instanceof final FileVideoImpl fileVideo) {
final var media = new Media(fileVideo.path().toUri().toString());
final var player = new MediaPlayer(media);
player.currentTimeProperty().addListener((ignored, oldTime, newTime) -> {
final var millis = newTime.toMillis();
playSlider.setValue(millis);
stackPane.getChildren().removeIf(Label.class::isInstance);
model.subtitles().forEach(s -> {
//TODO optimize using e.g. direction of playback
if (s.start() <= millis && s.end() >= millis) {
logger.info("Adding label {} at {}", s, millis);
final var label = createDraggableLabel(s);
stackPane.getChildren().add(label);
}
});
});
playSlider.setOnMousePressed(e -> {
wasPlaying = model.isPlaying();
model.setIsPlaying(false);
});
playSlider.valueProperty().addListener(observable1 -> {
if (playSlider.isValueChanging()) {
seek((long) playSlider.getValue());
}
});
playSlider.setOnMouseReleased(e -> {
final var value = playSlider.getValue();
Platform.runLater(() -> {
seek((long) value);
model.setIsPlaying(wasPlaying);
});
});
player.volumeProperty().bindBidirectional(model.volumeProperty());
player.setOnPlaying(() -> model.setIsPlaying(true));
player.setOnPaused(() -> model.setIsPlaying(false));
player.setOnEndOfMedia(() -> model.setIsPlaying(false));
playSlider.setMax(model.duration());
playSlider.setValue(0L);
videoView.setMediaPlayer(player);
if (newValue instanceof final File file) {
loadFileVideo(file.path());
} else {
logger.error("Unsupported video type : {}", newValue);
}
});
model.subtitles().addListener((ListChangeListener<EditableSubtitle>) c -> {
startTimes.clear();
model.subtitles().stream().mapToLong(Subtitle::start).forEach(startTimes::add);
});
bindPlayButton();
binder.createBindings();
}
private void loadFileVideo(final Path fileVideoPath) {
final var media = new Media(fileVideoPath.toUri().toString());
final var player = new MediaPlayer(media);
player.statusProperty().addListener((observable12, oldValue1, newValue1) ->
logger.info("New status: {}", newValue1));
player.currentTimeProperty().addListener((ignored, oldTime, newTime) -> currentTimeChanged(oldTime.toMillis(), newTime.toMillis()));
playSlider.setOnMousePressed(e -> {
wasPlaying = model.isPlaying();
model.setIsPlaying(false);
});
playSlider.valueProperty().addListener(observable1 -> {
if (playSlider.isValueChanging()) {
seek((long) playSlider.getValue());
}
});
playSlider.setOnMouseReleased(e -> {
final var value = playSlider.getValue();
Platform.runLater(() -> {
seek((long) value);
model.setIsPlaying(wasPlaying);
});
});
player.volumeProperty().bindBidirectional(model.volumeProperty());
player.setOnPlaying(() -> model.setIsPlaying(true));
player.setOnPaused(() -> model.setIsPlaying(false));
player.setOnEndOfMedia(() -> model.setIsPlaying(false));
playSlider.setMax(model.duration());
playSlider.setValue(0L);
videoView.setMediaPlayer(player);
}
private void currentTimeChanged(final double oldMillis, final double millis) {
playSlider.setValue(millis);
final var subtitleLabels = stackPane.getChildren().stream().filter(SubtitleLabel.class::isInstance).map(SubtitleLabel.class::cast).toList();
subtitleLabels.stream().filter(s -> !s.subtitle().isShowing((long) millis)).forEach(sl -> stackPane.getChildren().remove(sl));
final var containedSubtitles = subtitleLabels.stream().map(SubtitleLabel::subtitle).filter(s -> s.isShowing((long) millis)).collect(Collectors.toSet());
model.subtitles().forEach(s -> {
if (!containedSubtitles.contains(s)) {
logger.info("Adding label {} at {}", s, millis);
final var label = createDraggableLabel(s);
stackPane.getChildren().add(label);
}
});
}
private void currentTimeChangedOptimized(final double oldMillis, final double millis) {
final var forward = oldMillis <= millis;
var index = Collections.binarySearch(startTimes, (long) millis);
if (index < 0) {
index = forward ? -(index + 1) : -(index + 2);
}
//TODO
}
private void bindPlayButton() {
playButton.disableProperty().bind(model.videoProperty().isNull());
playButton.graphicProperty().bind(Bindings.createObjectBinding(() -> {
final ImageView view;
@@ -158,7 +196,6 @@ public class FXMediaController implements MediaController {
view.setFitHeight(24);
return view;
}, model.isPlayingProperty()));
binder.createBindings();
}
@FXML

View File

@@ -6,10 +6,10 @@ import com.github.gtache.autosubtitle.subtitle.EditableSubtitle;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.List;
/**
* FX implementation of {@link com.github.gtache.autosubtitle.gui.MediaModel}
@@ -22,7 +22,7 @@ public class FXMediaModel implements MediaModel {
private final BooleanProperty isPlaying;
private final ReadOnlyLongWrapper duration;
private final LongProperty position;
private final List<EditableSubtitle> subtitles;
private final ObservableList<EditableSubtitle> subtitles;
@Inject
FXMediaModel() {
@@ -103,7 +103,7 @@ public class FXMediaModel implements MediaModel {
}
@Override
public List<EditableSubtitle> subtitles() {
public ObservableList<EditableSubtitle> subtitles() {
return subtitles;
}

View File

@@ -59,6 +59,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
private static final Logger logger = LogManager.getLogger(FXWorkController.class);
private static final String ALL_SUPPORTED = "All supported";
private static final List<String> VIDEO_EXTENSIONS = Stream.of("webm", "mkv", "flv", "vob", "ogv", "ogg",
"drc", "gif", "gifv", "mng", "avi", "mts", "m2ts", "ts", "mov", "qt", "wmv", "yuv", "rm", "rmvb",
"viv", "asf", "amv", "mp4", "m4p", "m4v", "mpg", "mp2", "mpeg", "mpe", "mpv", "m2v", "m4v", "svi",
@@ -131,21 +132,42 @@ public class FXWorkController extends AbstractFXController implements WorkContro
@FXML
private void initialize() {
bindComboboxes();
bindButtons();
bindTable();
bindProgress();
model.selectedSubtitleProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
mediaController.seek(newValue.start());
}
});
binder.createBindings();
subtitleExtractor.addListener(this);
}
private void bindComboboxes() {
languageCombobox.valueProperty().bindBidirectional(model.videoLanguageProperty());
languageCombobox.setItems(model.availableVideoLanguages());
languageCombobox.setConverter(new LanguageStringConverter());
translationsCombobox.setConverter(new LanguageStringConverter());
Bindings.bindContent(translationsCombobox.getItems(), model.availableTranslationsLanguage());
Bindings.bindContent(model.translations(), translationsCombobox.getCheckModel().getCheckedItems());
}
private void bindButtons() {
extractButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
addSubtitleButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
loadSubtitlesButton.disableProperty().bind(model.videoProperty().isNull().or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
resetButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
exportSoftButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
exportHardButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
addSubtitleButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
loadSubtitlesButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
saveSubtitlesButton.disableProperty().bind(Bindings.isEmpty(model.subtitles()).or(model.statusProperty().isNotEqualTo(WorkStatus.IDLE)));
}
private void bindTable() {
subtitlesTable.setItems(model.subtitles());
subtitlesTable.setOnKeyPressed(e -> {
if (e.getCode().isLetterKey() || e.getCode().isDigitKey()) {
@@ -157,6 +179,9 @@ public class FXWorkController extends AbstractFXController implements WorkContro
} else if (e.getCode() == KeyCode.LEFT) {
subtitlesTable.getSelectionModel().selectPrevious();
e.consume();
} else if (e.getCode() == KeyCode.DELETE) {
deleteSelectedSubtitles();
e.consume();
}
});
startColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
@@ -185,11 +210,9 @@ public class FXWorkController extends AbstractFXController implements WorkContro
});
subtitlesTable.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> model.selectedSubtitleProperty().set(newValue));
model.selectedSubtitleProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
mediaController.seek(newValue.start());
}
});
}
private void bindProgress() {
progressLabel.textProperty().bind(Bindings.createStringBinding(() -> resources.getString("work.status." + model.status().name().toLowerCase() + ".label"), model.statusProperty()));
progressLabel.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
progressBar.visibleProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
@@ -198,9 +221,10 @@ public class FXWorkController extends AbstractFXController implements WorkContro
progressBar.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
progressDetailLabel.managedProperty().bind(model.statusProperty().isNotEqualTo(WorkStatus.IDLE));
progressBar.progressProperty().bindBidirectional(model.progressProperty());
binder.createBindings();
}
subtitleExtractor.addListener(this);
private void deleteSelectedSubtitles() {
model.subtitles().removeAll(subtitlesTable.getSelectionModel().getSelectedItems());
}
private void editFocusedCell() {
@@ -213,7 +237,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
@FXML
private void fileButtonPressed() {
final var filePicker = new FileChooser();
final var extensionFilter = new FileChooser.ExtensionFilter("All supported", VIDEO_EXTENSIONS);
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, VIDEO_EXTENSIONS);
filePicker.getExtensionFilters().add(extensionFilter);
filePicker.setSelectedExtensionFilter(extensionFilter);
final var file = filePicker.showOpenDialog(window());
@@ -303,6 +327,9 @@ public class FXWorkController extends AbstractFXController implements WorkContro
@FXML
private void exportSoftPressed() {
final var filePicker = new FileChooser();
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, VIDEO_EXTENSIONS);
filePicker.getExtensionFilters().add(extensionFilter);
filePicker.setSelectedExtensionFilter(extensionFilter);
final var file = filePicker.showSaveDialog(window());
if (file != null) {
final var baseCollection = model.subtitleCollection();
@@ -315,7 +342,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
}, Platform::runLater)
.thenAcceptAsync(collections -> {
try {
videoConverter.addSoftSubtitles(model.video(), collections);
videoConverter.addSoftSubtitles(model.video(), collections, file.toPath());
} catch (final IOException e) {
throw new CompletionException(e);
}
@@ -332,11 +359,14 @@ public class FXWorkController extends AbstractFXController implements WorkContro
@FXML
private void exportHardPressed() {
final var filePicker = new FileChooser();
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, VIDEO_EXTENSIONS);
filePicker.getExtensionFilters().add(extensionFilter);
filePicker.setSelectedExtensionFilter(extensionFilter);
final var file = filePicker.showSaveDialog(window());
if (file != null) {
CompletableFuture.runAsync(() -> {
try {
videoConverter.addHardSubtitles(model.video(), model.subtitleCollection());
videoConverter.addHardSubtitles(model.video(), model.subtitleCollection(), file.toPath());
} catch (final IOException e) {
throw new CompletionException(e);
}
@@ -386,7 +416,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
@FXML
private void loadSubtitlesPressed() {
final var filePicker = new FileChooser();
final var extensionFilter = new FileChooser.ExtensionFilter("All supported", subtitleExtensions);
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
filePicker.getExtensionFilters().add(extensionFilter);
filePicker.setSelectedExtensionFilter(extensionFilter);
final var file = filePicker.showOpenDialog(window());
@@ -396,7 +426,7 @@ public class FXWorkController extends AbstractFXController implements WorkContro
@FXML
private void saveSubtitlesPressed() {
final var filePicker = new FileChooser();
final var extensionFilter = new FileChooser.ExtensionFilter("All supported", subtitleExtensions);
final var extensionFilter = new FileChooser.ExtensionFilter(ALL_SUPPORTED, subtitleExtensions);
filePicker.getExtensionFilters().add(extensionFilter);
filePicker.setSelectedExtensionFilter(extensionFilter);
final var file = filePicker.showSaveDialog(window());

View File

@@ -75,12 +75,11 @@ public class FXWorkModel implements WorkModel {
this.extractionModel = new SimpleObjectProperty<>();
this.progress = new SimpleDoubleProperty(-1);
text.bind(Bindings.createStringBinding(() ->
subtitles.stream().map(EditableSubtitle::content).collect(Collectors.joining(" ")),
subtitles.stream().map(EditableSubtitle::content).collect(Collectors.joining("")),
subtitles));
subtitleCollection.bind(Bindings.createObjectBinding(() -> new SubtitleCollectionImpl(text(), subtitles, videoLanguage())));
videoLanguage.addListener((observable, oldValue, newValue) -> {
FXCollections.observableArrayList(Arrays.stream(Language.values()).filter(l -> l != Language.AUTO && l != newValue).sorted(Comparator.naturalOrder()).toList());
});
subtitleCollection.bind(Bindings.createObjectBinding(() -> new SubtitleCollectionImpl(text(), subtitles, videoLanguage()), text, subtitles, videoLanguage));
videoLanguage.addListener((observable, oldValue, newValue) -> FXCollections.observableArrayList(Arrays.stream(Language.values())
.filter(l -> l != Language.AUTO && l != newValue).sorted(Comparator.naturalOrder()).toList()));
}
@Override

View File

@@ -54,14 +54,12 @@ public abstract class FXModule {
}
@Provides
@Singleton
@Play
static Image providesPlayImage(@Play final byte[] playImage) {
return new Image(new ByteArrayInputStream(playImage));
}
@Provides
@Singleton
@Pause
static Image providesPauseImage(@Pause final byte[] pauseImage) {
return new Image(new ByteArrayInputStream(pauseImage));

View File

@@ -1,7 +1,7 @@
/**
* FX module for auto-subtitle
*/
module com.github.gtache.autosubtitle.fx {
module com.github.gtache.autosubtitle.gui.fx {
requires transitive com.github.gtache.autosubtitle.core;
requires transitive com.github.gtache.autosubtitle.gui.core;
requires transitive javafx.controls;

View File

@@ -1,93 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.RowConstraints?>
<?import org.controlsfx.control.CheckComboBox?>
<?import org.controlsfx.control.PrefixSelectionComboBox?>
<GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.github.gtache.autosubtitle.gui.fx.FXWorkController">
<GridPane hgap="10.0" vgap="10.0" xmlns="http://javafx.com/javafx/22" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.github.gtache.autosubtitle.gui.fx.FXWorkController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES"/>
<ColumnConstraints hgrow="ALWAYS"/>
<ColumnConstraints hgrow="SOMETIMES"/>
<ColumnConstraints hgrow="SOMETIMES" />
<ColumnConstraints hgrow="ALWAYS" />
<ColumnConstraints hgrow="SOMETIMES" />
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="ALWAYS"/>
<RowConstraints vgrow="SOMETIMES"/>
<RowConstraints vgrow="NEVER"/>
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="ALWAYS" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="NEVER" />
</rowConstraints>
<children>
<TextField fx:id="fileField" editable="false" GridPane.columnIndex="1"/>
<Button mnemonicParsing="false" onAction="#fileButtonPressed" text="%work.button.file.label"
GridPane.columnIndex="2"/>
<TextField fx:id="fileField" editable="false" GridPane.columnIndex="1" />
<Button mnemonicParsing="false" onAction="#fileButtonPressed" text="%work.button.file.label" GridPane.columnIndex="2" />
<HBox spacing="10.0" GridPane.columnIndex="1" GridPane.rowIndex="2">
<children>
<Button fx:id="loadSubtitlesButton" mnemonicParsing="false" onAction="#loadSubtitlesPressed"
text="%work.button.load.label"/>
<Button fx:id="extractButton" mnemonicParsing="false" onAction="#extractPressed"
text="%work.button.extract.label"/>
<Button fx:id="extractButton" mnemonicParsing="false" onAction="#extractPressed" text="%work.button.extract.label" />
</children>
</HBox>
<HBox alignment="CENTER_RIGHT" spacing="10.0" GridPane.columnIndex="2" GridPane.rowIndex="2">
<children>
<Button fx:id="exportSoftButton" mnemonicParsing="false" onAction="#exportSoftPressed"
text="%work.button.export.soft.label">
<Button fx:id="exportSoftButton" mnemonicParsing="false" onAction="#exportSoftPressed" text="%work.button.export.soft.label">
<tooltip>
<Tooltip text="%work.button.export.soft.tooltip"/>
<Tooltip text="%work.button.export.soft.tooltip" />
</tooltip>
</Button>
<Button fx:id="exportHardButton" mnemonicParsing="false" onAction="#exportHardPressed"
text="%work.button.export.hard.label">
<Button fx:id="exportHardButton" mnemonicParsing="false" onAction="#exportHardPressed" text="%work.button.export.hard.label">
<tooltip>
<Tooltip text="%work.button.export.hard.tooltip"/>
<Tooltip text="%work.button.export.hard.tooltip" />
</tooltip>
</Button>
</children>
</HBox>
<TableView fx:id="subtitlesTable" editable="true" GridPane.rowIndex="1">
<columns>
<TableColumn fx:id="startColumn" prefWidth="50.0" sortable="false"
text="%work.table.column.from.label"/>
<TableColumn fx:id="endColumn" prefWidth="50.0" sortable="false" text="%work.table.column.to.label"/>
<TableColumn fx:id="textColumn" prefWidth="75.0" sortable="false" text="%work.table.column.text.label"/>
<TableColumn fx:id="startColumn" prefWidth="50.0" sortable="false" text="%work.table.column.from.label" />
<TableColumn fx:id="endColumn" prefWidth="50.0" sortable="false" text="%work.table.column.to.label" />
<TableColumn fx:id="textColumn" prefWidth="75.0" sortable="false" text="%work.table.column.text.label" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY"/>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
<HBox spacing="10.0" GridPane.rowIndex="2">
<children>
<Button fx:id="resetButton" mnemonicParsing="false" onAction="#resetButtonPressed"
text="%work.button.reset.label"/>
<Button fx:id="saveSubtitlesButton" mnemonicParsing="false" onAction="#saveSubtitlesPressed"
text="%work.button.subtitles.save.label"/>
<Button fx:id="addSubtitleButton" mnemonicParsing="false" onAction="#addSubtitlePressed" text="+"/>
<Button fx:id="loadSubtitlesButton" mnemonicParsing="false" onAction="#loadSubtitlesPressed" text="%work.button.load.label" />
<Button fx:id="saveSubtitlesButton" mnemonicParsing="false" onAction="#saveSubtitlesPressed" text="%work.button.subtitles.save.label" />
<Button fx:id="resetButton" mnemonicParsing="false" onAction="#resetButtonPressed" text="%work.button.reset.label" />
<Button fx:id="addSubtitleButton" mnemonicParsing="false" onAction="#addSubtitlePressed" text="+" />
</children>
<GridPane.margin>
<Insets/>
<Insets />
</GridPane.margin>
</HBox>
<fx:include fx:id="media" source="mediaView.fxml" GridPane.columnIndex="1" GridPane.columnSpan="2147483647"
GridPane.rowIndex="1"/>
<fx:include fx:id="media" source="mediaView.fxml" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" GridPane.rowIndex="1" />
<HBox alignment="CENTER_LEFT" spacing="10.0">
<children>
<Label text="%work.language.label"/>
<PrefixSelectionComboBox fx:id="languageCombobox"/>
<Label text="%work.translate.label"/>
<CheckComboBox fx:id="translationsCombobox"/>
<Label text="%work.language.label" />
<PrefixSelectionComboBox fx:id="languageCombobox" />
<Label text="%work.translate.label" />
<CheckComboBox fx:id="translationsCombobox" />
</children>
</HBox>
<Label fx:id="progressLabel" GridPane.columnIndex="1" GridPane.rowIndex="3"/>
<Label fx:id="progressLabel" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<HBox spacing="10.0" GridPane.columnIndex="2" GridPane.rowIndex="3">
<children>
<Label fx:id="progressDetailLabel"/>
<ProgressBar fx:id="progressBar" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS"/>
<Label fx:id="progressDetailLabel" />
<ProgressBar fx:id="progressBar" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS" />
</children>
</HBox>
</children>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0"/>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</padding>
</GridPane>