package jp.yucchi.newyearcard;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Task;
import static javafx.concurrent.Worker.State.RUNNING;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Point2D;
import javafx.print.Collation;
import javafx.print.PageLayout;
import javafx.print.PageOrientation;
import javafx.print.Paper;
import javafx.print.PaperSource;
import javafx.print.PrintColor;
import javafx.print.PrintQuality;
import javafx.print.Printer;
import javafx.print.PrinterJob;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleButton;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import javafx.scene.transform.Scale;
import javafx.scene.transform.Transform;
import javafx.scene.transform.Translate;
import javafx.stage.FileChooser;
/**
*
* @author Yucchi
*/
public class FXMLDocumentController implements Initializable {
private static final boolean DEBUG = true;
private static final boolean CANCEL_DEBUG = true;
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
private Rectangle printAreaRectangle;
@FXML
private ResourceBundle resources;
@FXML
private URL location;
@FXML
private AnchorPane anchorPane;
@FXML
private Label gashiLabel;
@FXML
private TextField gashiTextField;
@FXML
private Label statusLabel;
@FXML
private Label jobStatusLabel;
@FXML
private Button printButton;
@FXML
private Button cancelButton;
@FXML
private CheckBox scalingCheckBox;
@FXML
private Button openImageButton;
@FXML
private ToggleButton printerInfoToggleButton;
@FXML
private Group greeting;
@FXML
private StackPane stackPane;
@FXML
private ImageView imageView;
@FXML
private Text gashiText;
@FXML
private TextArea printerTextArea;
private Task<Boolean> printTask;
private Printer targetPrinter;
private final BooleanProperty scaling = new SimpleBooleanProperty(true);
@FXML
void initialize() {
assert anchorPane != null : "fx:id=\"anchorPane\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert gashiLabel != null : "fx:id=\"gashiLabel\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert gashiTextField != null : "fx:id=\"gashiTextField\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert statusLabel != null : "fx:id=\"statusLabel\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert jobStatusLabel != null : "fx:id=\"jobStatusLabel\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert printButton != null : "fx:id=\"printButton\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert cancelButton != null : "fx:id=\"cancelButton\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert scalingCheckBox != null : "fx:id=\"scalingCheckBox\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert openImageButton != null : "fx:id=\"openImageButton\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert printerInfoToggleButton != null : "fx:id=\"printerInfoToggleButton\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert greeting != null : "fx:id=\"greeting\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert stackPane != null : "fx:id=\"stackPane\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert imageView != null : "fx:id=\"imageView\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert gashiText != null : "fx:id=\"gashiText\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
assert printerTextArea != null : "fx:id=\"printerTextArea\" was not injected: check your FXML file 'FXMLDocument.fxml'.";
}
@FXML
private void handleOpenImageButtonAction(ActionEvent event) {
var fileChooser = new FileChooser();
fileChooser.setTitle("Change Image");
fileChooser.setInitialDirectory(new File(System.getProperty("user.home")));
fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter(
"All Images", "*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif"));
var image = fileChooser.showOpenDialog(anchorPane.getScene().getWindow());
if (image != null) {
try {
imageView.setImage(new Image(image.toURI().toURL().toString(),
1_000, 675, true, true, false));
} catch (MalformedURLException ex) {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
@FXML
private void handlePrinterInfoToggleButtonAction(ActionEvent event) {
if (printerInfoToggleButton.isSelected()) {
var printerInformation = new PrinterInfomation();
var printerDetails = printerInformation.showPrinterDetails();
printerTextArea.setText(printerDetails);
printerTextArea.setVisible(true);
printerInfoToggleButton.setText("Hide Info");
} else {
printerTextArea.clear();
printerTextArea.setVisible(false);
printerInfoToggleButton.setText("Printer Info");
}
}
@FXML
private void handlePrintButtonAction(ActionEvent event) {
printGreeting(greeting);
}
@FXML
private void handleCancelButtonAction(ActionEvent event) {
if (printTask != null) {
printTask.cancel();
}
}
@Override
public void initialize(URL url, ResourceBundle rb) {
scaling.bind(scalingCheckBox.selectedProperty());
// デフォルトプリンター
var defaultPrinter = Printer.getDefaultPrinter();
// プリンター選択 (デフォルトでないプリンターを選択しています)
targetPrinter = Printer.getAllPrinters()
.stream()
.filter(p -> p.getName().matches("EP-806A"))
.findFirst()
.orElseGet(() -> {
return defaultPrinter;
});
// イメージファイルを設定
imageView.setImage(new Image(this.getClass()
.getResource("resources/ChristmasCard.jpg")
.toExternalForm(), 1_000, 675, true, true, false));
// 賀詞テキストのバインドとテキスト設定
gashiText.textProperty().bind(gashiTextField.textProperty());
gashiText.setFill(Color.LIGHTPINK);
gashiText.setStrokeWidth(2);
gashiText.setStroke(Color.RED);
// 賀詞テキストをドラッグ可能とする
var anchor = new SimpleObjectProperty<>(new Point2D(gashiText.getTranslateX(), gashiText.getTranslateY()));
gashiText.setOnMousePressed(me -> {
gashiText.setCursor(Cursor.MOVE);
double x = me.getSceneX() - anchor.get().getX();
double y = me.getSceneY() - anchor.get().getY();
anchor.set(new Point2D(x, y));
});
gashiText.setOnMouseDragged(me -> {
double x = me.getSceneX() - anchor.get().getX();
double y = me.getSceneY() - anchor.get().getY();
gashiText.setTranslateX(x);
gashiText.setTranslateY(y);
});
gashiText.setOnMouseReleased(me -> {
gashiText.setCursor(Cursor.HAND);
double x = me.getSceneX() - anchor.get().getX();
double y = me.getSceneY() - anchor.get().getY();
anchor.set(new Point2D(x, y));
});
gashiText.setOnMouseEntered(me -> {
gashiText.setCursor(Cursor.HAND);
});
}
private void printGreeting(Group greeting) {
printTask = new Task<>() {
private PageLayout pageLayout;
@Override
public Boolean call() {
Platform.runLater(() -> {
jobStatusLabel.setText("Create a printer job.");
});
// ジョブステータスラベル確認用スリープ
if (DEBUG) {
jobSleep();
}
// プリンタージョブを作成
// // デフォルトプリンターを使ってプリンタージョブを作成
// var job = PrinterJob.createPrinterJob();
// プリンターを指定してプリンタージョブを作成
var job = PrinterJob.createPrinterJob(targetPrinter);
// ジョブステータス確認用
System.out.println("JobStatus: " + job.jobStatusProperty().asString().get());
Platform.runLater(() -> {
jobStatusLabel.setText(job.jobStatusProperty().asString().get());
});
// ジョブステータスラベル確認用スリープ
if (DEBUG) {
jobSleep();
}
// プリンタージョブステータス
job.jobStatusProperty().addListener(e -> {
System.out.println("JobStatus: " + job.jobStatusProperty().asString().get());
Platform.runLater(() -> {
jobStatusLabel.setText(job.jobStatusProperty().asString().get());
});
// ジョブステータスラベル確認用スリープ
if (DEBUG) {
jobSleep();
}
// JobStatus が PRINTING の状態で強制的にキャンセル
if (CANCEL_DEBUG && PrinterJob.JobStatus.PRINTING == job.jobStatusProperty().get()) {
System.out.println("JobStatus が PRINTING の状態で Cancel");
job.cancelJob(); // JobStatus: CANCELED -> JobStatus: ERROR となる。
printTask.cancel();
}
});
// ジョブ設定
var jobSettings = job.getJobSettings();
// ジョブの名前
jobSettings.setJobName("Greeting");
// 自動ではがきに印刷
jobSettings.setPaperSource(PaperSource.AUTOMATIC);
// // この設定でもはがきに印刷
// jobSettings.setPaperSource(PaperSource.TOP);
// ページレイアウト設定
pageLayout = targetPrinter.createPageLayout(Paper.JAPANESE_POSTCARD,
PageOrientation.LANDSCAPE, Printer.MarginType.HARDWARE_MINIMUM);
// ジョブにページレイアウトを設定
jobSettings.setPageLayout(pageLayout);
// 印刷カラーを設定
jobSettings.setPrintColor(PrintColor.COLOR);
// 印刷品質を設定
jobSettings.setPrintQuality(PrintQuality.NORMAL);
// 印刷部数を設定
jobSettings.setCopies(1);
// 部単位で印刷設定
jobSettings.setCollation(Collation.UNCOLLATED);
// Single page printing /////////////////////////////////////////////
if (scaling.get()) {
if (job != null) {
// 印刷対象ノードを印刷範囲内にスケーリング
fit2PrintableSize();
// // 用紙設定ダイアログ
// var nativePageSetup = job.showPageSetupDialog(anchorPane.getScene().getWindow());
// // 用紙設定ダイアログキャンセル
// if (!nativePageSetup) {
// System.out.println("PageSetup has been canceled.");
// job.cancelJob();
// printTask.cancel();
// }
// // 印刷ダイアログ
// var nativeJob = job.showPrintDialog(anchorPane.getScene().getWindow());
// // 印刷ダイアログでキャンセルした場合の処理
// if (!nativeJob) {
// System.out.println("Job has been canceled.");
// job.cancelJob();
// printTask.cancel();
// }
// タスクのキャンセル処理
if (isCancelled()) {
if (PrinterJob.JobStatus.PRINTING == job.jobStatusProperty().get()
|| PrinterJob.JobStatus.NOT_STARTED == job.jobStatusProperty().get()) {
// プリンタージョブをキャンセル
job.cancelJob();
System.out.println("Cancel printing.");
if (scaling.get()) {
// 印刷対象ノードを元の大きさにスケーリング
fit2ImageSize();
}
return false;
} else {
System.out.println("Job has already been canceled.");
}
return false;
}
// ノード(Group greeting)をプリント
boolean success = job.printPage(greeting);
if (success) {
// 印刷対象ノードを元の大きさにスケーリング
fit2ImageSize();
// プリンタのキューに正常にスプールされたら true
return job.endJob();
} else {
System.out.println("Printing failed.");
// 印刷対象ノードを元の大きさにスケーリング
fit2ImageSize();
return false;
}
} else {
Platform.runLater(() -> {
jobStatusLabel.setText("Could not create printer job.");
});
return false;
}
} else {
// Multi-page printing /////////////////////////////////////////
// 印刷用紙に印刷可能な幅と高さ
double printableWidth = pageLayout.getPrintableWidth();
double printableHeight = pageLayout.getPrintableHeight();
// 印刷エリアの設定(印刷対象ノードをそのままの大きさで設定)
printAreaRectangle = new Rectangle(greeting.getBoundsInParent().getWidth(),
greeting.getBoundsInParent().getHeight(), null);
// 印刷エリアの位置と大きさ
double printRectX = printAreaRectangle.getX();
double printRectY = printAreaRectangle.getY();
double printRectWidth = printAreaRectangle.getWidth();
double printRectHeight = printAreaRectangle.getHeight();
// 印刷に必要な用紙枚数(行、列)
int rows = (int) Math.ceil(printRectHeight / printableHeight);
int columns = (int) Math.ceil(printRectWidth / printableWidth);
// ノードのクリップを保存
var oldClip = greeting.getClip();
var oldTransforms = new ArrayList<>(greeting.getTransforms());
// printAreaRectangle のエリアをクリップとして設定する
greeting.setClip(new javafx.scene.shape.Rectangle(printRectX, printRectY,
printRectWidth, printRectHeight));
// 0,0 に移動
greeting.getTransforms().add(new Translate(-printRectX, -printRectY));
// 印刷ページに適合するようにノードを移動する変換
var gridTransform = new Translate();
greeting.getTransforms().add(gridTransform);
if (job != null) {
// // 用紙設定ダイアログ
// var nativePageSetup = job.showPageSetupDialog(anchorPane.getScene().getWindow());
// // 用紙設定ダイアログキャンセル
// if (!nativePageSetup) {
// System.out.println("PageSetup has been canceled.");
// job.cancelJob();
// printTask.cancel();
// }
// // 印刷ダイアログ
// var nativeJob = job.showPrintDialog(anchorPane.getScene().getWindow());
// // 印刷ダイアログでキャンセルした場合の処理
// if (!nativeJob) {
// System.out.println("Job has been canceled.");
// job.cancelJob();
// printTask.cancel();
// }
// タスクのキャンセル処理
if (isCancelled()) {
if (PrinterJob.JobStatus.PRINTING == job.jobStatusProperty().get()
|| PrinterJob.JobStatus.NOT_STARTED == job.jobStatusProperty().get()) {
// プリンタージョブをキャンセル
job.cancelJob();
System.out.println("Cancel printing.");
return false;
} else {
System.out.println("Job has already been canceled.");
}
return false;
}
boolean success = true;
// 印刷ページごとに、ノードを移動
for (int row = 0; row < rows; row++) {
for (int col = 0; col < columns; col++) {
gridTransform.setX(-col * printableWidth);
gridTransform.setY(-row * printableHeight);
if (DEBUG) {
jobSleep();
}
success &= job.printPage(pageLayout, greeting);
}
}
if (success) {
// ノードを元に戻す
restoreNode(greeting, oldTransforms, oldClip);
return job.endJob();
} else {
// ノードを元に戻す
restoreNode(greeting, oldTransforms, oldClip);
System.out.println("Printing failed.");
return false;
}
} else {
Platform.runLater(() -> {
jobStatusLabel.setText("Could not create printer job.");
});
// ノードを元に戻す
restoreNode(greeting, oldTransforms, oldClip);
return false;
}
}
}
private void jobSleep() {
try {
Thread.sleep(1_500);
} catch (InterruptedException ex) {
// ナイスキャッチ!
if (isCancelled()) {
System.out.println("Canceled by InterruptedException: sleep interrupted");
} else {
Logger.getLogger(FXMLDocumentController.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
// 印刷対象ノードを印刷範囲内にスケーリング
private void fit2PrintableSize() {
greeting.getTransforms()
.add(new Scale(pageLayout.getPrintableWidth() / imageView.getBoundsInParent().getWidth(),
pageLayout.getPrintableHeight() / imageView.getBoundsInParent().getHeight()));
}
// 印刷対象ノードを元の大きさにスケーリング
private void fit2ImageSize() {
greeting.getTransforms()
.add(new Scale(imageView.getBoundsInParent().getWidth() / pageLayout.getPrintableWidth(),
imageView.getBoundsInParent().getHeight() / pageLayout.getPrintableHeight()));
}
// ノードを元に戻す
private void restoreNode(Group greeting, List<Transform> oldTransforms, Node oldClip) {
greeting.getTransforms().clear();
greeting.getTransforms().addAll(oldTransforms);
greeting.setClip(oldClip);
}
};
executorService.submit(printTask);
// WorkerStateEvent を使ってWorkerオブジェクトの状態を監視
printTask.setOnScheduled(wse -> {
System.out.println("WorkerState: Scheduled");
});
printTask.setOnRunning(wse -> {
System.out.println("WorkerState: OnRunning");
});
printTask.setOnSucceeded(wse -> {
System.out.println("WorkerState: Succeeded");
});
printTask.setOnCancelled(wse -> {
System.out.println("WorkerState: Cancelled");
});
printTask.setOnFailed(wse -> {
System.out.println("WorkerState: Failed");
});
// disableProperty と Workerオブジェクトの stateProperty をバインド
gashiTextField.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
printButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
cancelButton.disableProperty().bind(printTask.stateProperty().isNotEqualTo(RUNNING));
scalingCheckBox.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
openImageButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
printerInfoToggleButton.disableProperty().bind(printTask.stateProperty().isEqualTo(RUNNING));
}
void stageClose() {
executorService.shutdownNow();
}
}