diff --git a/changelog.adoc b/changelog.adoc index 0022a3e0e..c829bdb93 100644 --- a/changelog.adoc +++ b/changelog.adoc @@ -30,6 +30,7 @@ * Column sizing == Release 3.0.2 +* 03/28/2019 Reimplemented the page format dialog for reports to address OSX issues and to allow custom paper sizes. * 03/28/2019 Prevent an NPE from a race condition between a background security price update and an application shutdown. * 03/27/2019 Prevent an NPE from occurring when closing after a report has been shown and GC is slow. * 03/26/2019 Improved the update behavior when performing a Save As and when packing databases. diff --git a/jgnash-fx/src/main/java/jgnash/uifx/report/pdf/PageFormatDialogController.java b/jgnash-fx/src/main/java/jgnash/uifx/report/pdf/PageFormatDialogController.java new file mode 100644 index 000000000..4b9afe858 --- /dev/null +++ b/jgnash-fx/src/main/java/jgnash/uifx/report/pdf/PageFormatDialogController.java @@ -0,0 +1,306 @@ +/* + * jGnash, a personal finance application + * Copyright (C) 2001-2019 Craig Cavanaugh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package jgnash.uifx.report.pdf; + + +import java.awt.print.PageFormat; +import java.awt.print.Paper; +import java.math.BigDecimal; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.RadioButton; +import javafx.stage.Stage; + +import jgnash.report.pdf.Constants; +import jgnash.report.pdf.PageSize; +import jgnash.report.ui.ReportPrintFactory; +import jgnash.uifx.control.DecimalTextField; +import jgnash.uifx.util.InjectFXML; +import jgnash.uifx.util.JavaFXUtils; +import jgnash.util.Nullable; + +/** + * Page Format Dialog Controller + * + * @author Craig Cavanaugh + */ +public class PageFormatDialogController { + + @InjectFXML + private final ObjectProperty parent = new SimpleObjectProperty<>(); + + @FXML + private DecimalTextField leftMarginField; + + @FXML + private DecimalTextField rightMarginField; + + @FXML + private DecimalTextField topMarginField; + + @FXML + private DecimalTextField bottomMarginField; + + @FXML + private DecimalTextField widthField; + + @FXML + private DecimalTextField heightField; + + @FXML + private RadioButton portraitRadioButton; + + @FXML + private RadioButton landscapeRadioButton; + + @FXML + private ComboBox pageSizeComboBox; + + @FXML + private ComboBox unitsComboBox; + + @FXML + private Button okayButton; + + private final BooleanProperty invalidPageFormatProperty = new SimpleBooleanProperty(); + + private final DoubleProperty unitScaleProperty = new SimpleDoubleProperty(Unit.INCHES.scale); + + private Unit lastUnit = Unit.INCHES; + + private PageFormat pageFormat = ReportPrintFactory.getDefaultPage(); + + private final DoubleProperty minWidth = new SimpleDoubleProperty(); + + // anything less than 2 inches is considered bad + private final DoubleProperty minWidthPoints = new SimpleDoubleProperty(Constants.POINTS_PER_INCH * 2); + + @FXML + void initialize() { + + // setup the defaults + unitsComboBox.getItems().setAll(Unit.POINTS, Unit.MM, Unit.INCHES); + unitsComboBox.setValue(Unit.INCHES); + + pageSizeComboBox.getItems().setAll(PageSize.values()); + pageSizeComboBox.setValue(PageSize.LETTER); + + widthField.emptyWhenZeroProperty().setValue(false); + heightField.emptyWhenZeroProperty().setValue(false); + leftMarginField.emptyWhenZeroProperty().setValue(false); + rightMarginField.emptyWhenZeroProperty().setValue(false); + topMarginField.emptyWhenZeroProperty().setValue(false); + bottomMarginField.emptyWhenZeroProperty().setValue(false); + + // inches + widthField.setDecimal(new BigDecimal(8.5)); + heightField.setDecimal(new BigDecimal(11)); + + portraitRadioButton.setSelected(true); + + // inches + leftMarginField.setDecimal(new BigDecimal(0.5)); + rightMarginField.setDecimal(new BigDecimal(0.5)); + topMarginField.setDecimal(new BigDecimal(0.5)); + bottomMarginField.setDecimal(new BigDecimal(0.5)); + + // install the listeners + pageSizeComboBox.valueProperty().addListener((observable, oldValue, newValue) -> handlePageSizeChange()); + unitsComboBox.valueProperty().addListener((observable, oldValue, newValue) -> handleUnitChange()); + + // bindings to prevent math mischief from occurring + minWidth.bind(minWidthPoints.divide(unitScaleProperty)); + + invalidPageFormatProperty.bind(widthField.doubleProperty().lessThan(minWidth) + .or(heightField.doubleProperty().lessThan(minWidth)) + .or(rightMarginField.doubleProperty().greaterThan(widthField.doubleProperty())) + .or(leftMarginField.doubleProperty().greaterThan(widthField.doubleProperty())) + .or(topMarginField.doubleProperty().greaterThan(heightField.doubleProperty())) + .or(bottomMarginField.doubleProperty().greaterThan(heightField.doubleProperty())) + .or(rightMarginField.doubleProperty().add(leftMarginField.doubleProperty()) + .greaterThan(widthField.doubleProperty().subtract(minWidth))) + .or(topMarginField.doubleProperty().add(bottomMarginField.doubleProperty()) + .greaterThan(heightField.doubleProperty().subtract(minWidth))) + ); + + okayButton.disableProperty().bind(invalidPageFormatProperty); + } + + void setPageFormat(final PageFormat pageFormat) { + + this.pageFormat = pageFormat; + + if (pageFormat.getOrientation() == PageFormat.LANDSCAPE) { + landscapeRadioButton.setSelected(true); + } else { + portraitRadioButton.setSelected(true); + } + + // force for correct form orientation + pageFormat.setOrientation(PageFormat.PORTRAIT); + + final float width = (float) pageFormat.getWidth(); + final float height = (float) pageFormat.getHeight(); + final float imageableX = (float) pageFormat.getImageableX(); + final float imageableY = (float) pageFormat.getImageableY(); + + final float rightMargin = width - (float) pageFormat.getImageableWidth() - imageableX; + final float bottomMargin = height - (float) pageFormat.getImageableHeight() - imageableY; + + // load the fields with the new values + final Unit currentUnit = unitsComboBox.getValue(); + + handleUnitChange(widthField, width, currentUnit); + handleUnitChange(heightField, height, currentUnit); + handleUnitChange(leftMarginField, imageableX, currentUnit); + handleUnitChange(topMarginField, imageableY, currentUnit); + handleUnitChange(rightMarginField, rightMargin, currentUnit); + handleUnitChange(bottomMarginField, bottomMargin, currentUnit); + } + + @Nullable + PageFormat getPageFormat() { + return pageFormat; + } + + private PageFormat generatePageFormat() { + final PageFormat pageFormat = new PageFormat(); + final Unit unit = unitsComboBox.getValue(); + + if (portraitRadioButton.isSelected()) { + pageFormat.setOrientation(PageFormat.PORTRAIT); + } else { + pageFormat.setOrientation(PageFormat.LANDSCAPE); + } + + double width = widthField.getDecimal().doubleValue() * unit.scale; + double height = heightField.getDecimal().doubleValue() * unit.scale; + double rightMargin = rightMarginField.getDecimal().doubleValue() * unit.scale; + double bottomMargin = bottomMarginField.getDecimal().doubleValue() * unit.scale; + double imageableX = leftMarginField.getDecimal().doubleValue() * unit.scale; + double imageableY = topMarginField.getDecimal().doubleValue() * unit.scale; + + double imageableWidth = width - imageableX - rightMargin; + double imageableHeight = height - imageableY - bottomMargin; + + final Paper paper = pageFormat.getPaper(); + paper.setSize(width, height); + paper.setImageableArea(imageableX, imageableY, imageableWidth, imageableHeight); + pageFormat.setPaper(paper); + + return pageFormat; + } + + private void handlePageSizeChange() { + final PageSize pageSize = pageSizeComboBox.getValue(); + + final Unit unit = unitsComboBox.getValue(); + + // convert points to selected unit of measure + widthField.setDecimal(new BigDecimal(pageSize.width / unit.scale)); + heightField.setDecimal(new BigDecimal(pageSize.height / unit.scale)); + } + + /** + * Rescales to a new unit of measure + */ + private void handleUnitChange() { + final Unit newUnit = unitsComboBox.getValue(); + + unitScaleProperty.set(newUnit.scale); // update binding + + handleUnitChange(widthField, lastUnit, newUnit); + handleUnitChange(heightField, lastUnit, newUnit); + handleUnitChange(leftMarginField, lastUnit, newUnit); + handleUnitChange(rightMarginField, lastUnit, newUnit); + handleUnitChange(topMarginField, lastUnit, newUnit); + handleUnitChange(bottomMarginField, lastUnit, newUnit); + + lastUnit = newUnit; + } + + /** + * Correctly scales and sets the decimal field + * + * @param decimalTextField field to set + * @param newValue new value in Points + * @param newUnit unit of measure + */ + private void handleUnitChange(final DecimalTextField decimalTextField, final float newValue, final Unit newUnit) { + if (!decimalTextField.getDecimal().equals(BigDecimal.ZERO)) { + JavaFXUtils.runLater(() -> decimalTextField.setDecimal(new BigDecimal(newValue / newUnit.scale))); + } + } + + private void handleUnitChange(final DecimalTextField decimalTextField, final Unit oldUnit, final Unit newUnit) { + if (!decimalTextField.getDecimal().equals(BigDecimal.ZERO)) { + float oldValue = decimalTextField.getDecimal().floatValue() * oldUnit.scale; + JavaFXUtils.runLater(() -> decimalTextField.setDecimal(new BigDecimal(oldValue / newUnit.scale))); + } + } + + @FXML + private void handleCloseAction() { + ((Stage) parent.get().getWindow()).close(); + } + + @FXML + private void handleCancelAction() { + pageFormat = null; + handleCloseAction(); + } + + @FXML + private void handleOkAction() { + pageFormat = generatePageFormat(); + + handleCloseAction(); + } + + private enum Unit { + POINTS("Points", 1), + MM("Millimeters", Constants.POINTS_PER_MM), + INCHES("Inches", Constants.POINTS_PER_INCH); + + Unit(final String description, final float scale) { + this.description = description; + this.scale = scale; + } + + private final transient String description; + + /** + * Number of points per unit of measure + */ + final transient float scale; + + @Override + public String toString() { + return description; + } + } +} diff --git a/jgnash-fx/src/main/java/jgnash/uifx/report/pdf/ReportViewerDialogController.java b/jgnash-fx/src/main/java/jgnash/uifx/report/pdf/ReportViewerDialogController.java index 518e2ef0b..25681b01c 100644 --- a/jgnash-fx/src/main/java/jgnash/uifx/report/pdf/ReportViewerDialogController.java +++ b/jgnash-fx/src/main/java/jgnash/uifx/report/pdf/ReportViewerDialogController.java @@ -22,8 +22,10 @@ import jgnash.uifx.StaticUIMethods; import jgnash.uifx.control.BusyPane; import jgnash.uifx.report.ReportActions; +import jgnash.uifx.util.FXMLUtils; import jgnash.uifx.util.InjectFXML; import jgnash.uifx.util.JavaFXUtils; +import jgnash.uifx.util.StageUtils; import jgnash.uifx.views.main.MainView; import jgnash.util.DefaultDaemonThreadFactory; import jgnash.util.FileUtils; @@ -87,6 +89,7 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; +import javafx.stage.Stage; import static jgnash.report.ui.ReportPrintFactory.pageFormatToMediaPrintableArea; @@ -560,13 +563,25 @@ protected Void call() { @FXML private void handleFormatAction() { + final PageFormat oldFormat = report.get().getPageFormat(); - final PrinterJob job = PrinterJob.getPrinterJob(); - final PageFormat format = job.pageDialog(oldFormat); + final FXMLUtils.Pair pair = FXMLUtils.load(PageFormatDialogController.class.getResource("PageFormatDialog.fxml"), + "Page Format"); + + final PageFormatDialogController controller = pair.getController(); + final Stage stage = pair.getStage(); + + StageUtils.addBoundsListener(stage, PageFormatDialogController.class); + + stage.setResizable(false); + controller.setPageFormat(oldFormat); + stage.showAndWait(); + + final PageFormat newFormat = controller.getPageFormat(); - if (format != oldFormat) { - report.get().setPageFormat(format); + if (newFormat != null) { + report.get().setPageFormat(newFormat); reportController.refreshReport(); } } diff --git a/jgnash-fx/src/main/resources/jgnash/uifx/report/pdf/PageFormatDialog.fxml b/jgnash-fx/src/main/resources/jgnash/uifx/report/pdf/PageFormatDialog.fxml new file mode 100644 index 000000000..51b6f8608 --- /dev/null +++ b/jgnash-fx/src/main/resources/jgnash/uifx/report/pdf/PageFormatDialog.fxml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + diff --git a/jgnash-report-core/src/main/java/jgnash/report/pdf/Constants.java b/jgnash-report-core/src/main/java/jgnash/report/pdf/Constants.java new file mode 100644 index 000000000..d7958165d --- /dev/null +++ b/jgnash-report-core/src/main/java/jgnash/report/pdf/Constants.java @@ -0,0 +1,32 @@ +/* + * jGnash, a personal finance application + * Copyright (C) 2001-2019 Craig Cavanaugh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package jgnash.report.pdf; + +/** + * Print / Report specific constants + * + * @author Craig Cavanaugh + */ +public class Constants { + public static final float POINTS_PER_INCH = 72; + public static final float POINTS_PER_MM = (1 / 25.4f) * POINTS_PER_INCH; + + private Constants() { + // utility class + } +} diff --git a/jgnash-report-core/src/main/java/jgnash/report/pdf/PageSize.java b/jgnash-report-core/src/main/java/jgnash/report/pdf/PageSize.java new file mode 100644 index 000000000..44a1d0ea9 --- /dev/null +++ b/jgnash-report-core/src/main/java/jgnash/report/pdf/PageSize.java @@ -0,0 +1,53 @@ +/* + * jGnash, a personal finance application + * Copyright (C) 2001-2019 Craig Cavanaugh + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package jgnash.report.pdf; + +import static jgnash.report.pdf.Constants.POINTS_PER_INCH; +import static jgnash.report.pdf.Constants.POINTS_PER_MM; + +/** + * Enumeration of standard page sizes. + *

+ * Page sizes are in Points + */ +public enum PageSize { + + A2("ISO A2", 420 * POINTS_PER_MM, 594 * POINTS_PER_MM), + A3("ISO A3", 297 * POINTS_PER_MM, 420 * POINTS_PER_MM), + A4("ISO A4", 210 * POINTS_PER_MM, 297 * POINTS_PER_MM), + LETTER("US Letter", 8.5f * POINTS_PER_INCH, 11f * POINTS_PER_INCH), + LEGAL("US Legal", 8.5f * POINTS_PER_INCH, 14f * POINTS_PER_INCH), + TABLOID("US Tabloid", 11f * POINTS_PER_INCH, 17f * POINTS_PER_INCH); + + public transient float width; + public transient float height; + + private final transient String description; + + PageSize(final String description, final float width, final float height) { + this.description = description; + this.width = width; + this.height = height; + } + + @Override + public String toString() { + return description; + } + +}