Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 27 additions & 10 deletions src/qz/common/TrayManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,21 +117,22 @@ public TrayManager(boolean isHeadless) {
// Set up the shortcut name so that the UI components can use it
shortcutCreator = ShortcutCreator.getInstance();

iconCache = IconCache.getInstance();

SystemUtilities.setSystemLookAndFeel(headless);
iconCache = new IconCache();

if (SystemUtilities.isSystemTraySupported(headless)) { // UI mode with tray
switch(SystemUtilities.getOs()) {
case WINDOWS:
tray = TrayType.JX.init(iconCache);
tray = TrayType.JX.init();
// Undocumented HiDPI behavior
tray.setImageAutoSize(true);
break;
case MAC:
tray = TrayType.CLASSIC.init(iconCache);
tray = TrayType.CLASSIC.init();
break;
default:
tray = TrayType.MODERN.init(iconCache);
tray = TrayType.MODERN.init();
}

// OS-specific tray icon handling
Expand All @@ -151,7 +152,7 @@ public TrayManager(boolean isHeadless) {
headless = true;
}
} else if (!headless) { // UI mode without tray
tray = TrayType.TASKBAR.init(exitListener, iconCache);
tray = TrayType.TASKBAR.init(exitListener);
tray.setIcon(DANGER_ICON);
tray.setToolTip(name);
tray.showTaskbar();
Expand All @@ -167,11 +168,11 @@ public TrayManager(boolean isHeadless) {
componentList = new ArrayList<>();

// The allow/block dialog
gatewayDialog = new GatewayDialog(null, "Action Required", iconCache);
gatewayDialog = new GatewayDialog(null, "Action Required");
componentList.add(gatewayDialog);

// The ok/cancel dialog
confirmDialog = new ConfirmDialog(null, "Please Confirm", iconCache);
confirmDialog = new ConfirmDialog(null, "Please Confirm");
componentList.add(confirmDialog);

// Detect theme changes
Expand Down Expand Up @@ -258,7 +259,7 @@ private void addMenuItems() {
JMenuItem sitesItem = new JMenuItem("Site Manager...", iconCache.getIcon(SAVED_ICON));
sitesItem.setMnemonic(KeyEvent.VK_M);
sitesItem.addActionListener(savedListener);
sitesDialog = new SiteManagerDialog(sitesItem, iconCache, prefs);
sitesDialog = new SiteManagerDialog(sitesItem, prefs);
componentList.add(sitesDialog);

JMenuItem diagnosticMenu = new JMenu("Diagnostic");
Expand Down Expand Up @@ -290,6 +291,13 @@ private void addMenuItems() {
notificationsItem.addActionListener(notificationsListener);
diagnosticMenu.add(notificationsItem);

JCheckBoxMenuItem previewItem = new JCheckBoxMenuItem("Preview HTML Prints");
previewItem.setToolTipText("Preview all HTML prints and optionally resize the content.");
previewItem.setMnemonic(KeyEvent.VK_P);
previewItem.setState(getPref(TRAY_PREVIEW));
diagnosticMenu.add(previewItem);
previewItem.addActionListener(previewListener);

JCheckBoxMenuItem monocleItem = new JCheckBoxMenuItem("Use Monocle for HTML");
monocleItem.setToolTipText("Use monocle platform for HTML printing (restart required)");
monocleItem.setMnemonic(KeyEvent.VK_U);
Expand All @@ -311,7 +319,7 @@ private void addMenuItems() {
logItem.setMnemonic(KeyEvent.VK_L);
logItem.addActionListener(logListener);
diagnosticMenu.add(logItem);
logDialog = new LogDialog(logItem, iconCache);
logDialog = new LogDialog(logItem);
componentList.add(logDialog);

JMenuItem zipLogs = new JMenuItem("Zip logs (to Desktop)");
Expand Down Expand Up @@ -346,7 +354,7 @@ private void addMenuItems() {
JMenuItem aboutItem = new JMenuItem("About...", iconCache.getIcon(ABOUT_ICON));
aboutItem.setMnemonic(KeyEvent.VK_B);
aboutItem.addActionListener(aboutListener);
aboutDialog = new AboutDialog(aboutItem, iconCache);
aboutDialog = new AboutDialog(aboutItem);
componentList.add(aboutDialog);

if (SystemUtilities.isMac()) {
Expand Down Expand Up @@ -389,6 +397,15 @@ public void actionPerformed(ActionEvent e) {
}
};

private final ActionListener previewListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JCheckBoxMenuItem j = (JCheckBoxMenuItem)e.getSource();
prefs.setProperty(TRAY_PREVIEW, j.getState());
//todo warn about monocle/headless
}
};

private final ActionListener monocleListener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
Expand Down
5 changes: 3 additions & 2 deletions src/qz/printer/action/PrintHTML.java
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,14 @@ public void print(PrintOutput output, PrintOptions options) throws PrinterExcept
try {
if (cSupport != null && cSupport.contains(pxlOpts.getCopies())) {
for(WebAppModel model : models) {
WebApp.print(job, model);
//todo: how do we end up with n-models? we need to test this
WebApp.print(job, model, options);
}
} else {
settings.setCopies(1); //manually handle copies if they are not supported
for(int i = 0; i < pxlOpts.getCopies(); i++) {
for(WebAppModel model : models) {
WebApp.print(job, model);
WebApp.print(job, model, options);
}
}
}
Expand Down
245 changes: 245 additions & 0 deletions src/qz/printer/action/html/AbstractHtmlInstance.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package qz.printer.action.html;

import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.concurrent.Worker;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.IntPredicate;

abstract class AbstractHtmlInstance {
private static final Logger log = LogManager.getLogger(AbstractHtmlInstance.class);

protected Stage renderStage;
protected WebView webView;

protected double pageWidth;
protected double pageHeight;
protected double pageZoom;

protected boolean raster;
protected IntPredicate printAction;
protected final AtomicReference<Throwable> thrown = new AtomicReference<>();

protected CountDownLatch captureLatch;

//listens for a Succeeded state to activate image capture
protected ChangeListener<Worker.State> stateListener = (ov, oldState, newState) -> {
log.trace("New state: {} > {}", oldState, newState);

// Cancelled should probably throw exception listener, but does not
if (newState == Worker.State.CANCELLED) {
// This can happen for file downloads, e.g. "response-content-disposition=attachment"
// See https://github.com/qzind/tray/issues/1183
unlatch(new IOException("Page load was cancelled for an unknown reason"));
}
if (newState == Worker.State.SUCCEEDED) {
if (!hasBody()) return;
disableHtmlScrollbars();

// width was resized earlier (for responsive html), then calculate the best fit height
// FIXME: Should only be needed when height is unknown but fixes blank vector prints
double fittedHeight = findHeight();
boolean heightNeeded = pageHeight <= 0;

if (heightNeeded) {
pageHeight = fittedHeight;
}
pageHeight = (pageHeight <= 0) ? findHeight() : pageHeight;

// find and set page zoom for increased quality
double usableZoom = calculateSupportedZoom(pageWidth, pageHeight);
if (usableZoom < pageZoom) {
log.warn("Zoom level {} decreased to {} due to physical memory limitations", pageZoom, usableZoom);
pageZoom = usableZoom;
}
webView.setZoom(pageZoom);
log.trace("Zooming in by x{} for increased quality", pageZoom);

adjustSize(pageWidth * pageZoom, pageHeight * pageZoom);

//need to check for height again as resizing can cause partial results
if (heightNeeded) {
fittedHeight = findHeight();
if (fittedHeight != pageHeight) {
adjustSize(pageWidth * pageZoom, fittedHeight * pageZoom);
}
}

log.trace("Set HTML page height to {}", pageHeight);

autosize(webView);

firePrintAction();
}
};

//listens for load progress
protected ChangeListener<Number> workDoneListener = (ov, oldWork, newWork) -> log.trace("Done: {} > {}", oldWork, newWork);

protected ChangeListener<String> msgListener = (ov, oldMsg, newMsg) -> log.trace("New status: {}", newMsg);

//listens for failures
protected ChangeListener<Throwable> exceptListener = (obs, oldExc, newExc) -> {
if (newExc != null) { unlatch(newExc); }
};

protected void initStateListeners(Worker<Void> worker) {
worker.stateProperty().addListener(stateListener);
worker.workDoneProperty().addListener(workDoneListener);
worker.exceptionProperty().addListener(exceptListener);
worker.messageProperty().addListener(msgListener);
}

/**
* Prints the loaded source specified in the passed {@code model}.
*
* @param model The model specifying the web page parameters.
* @param action EventHandler that will be ran when the WebView completes loading.
*/
protected synchronized void load(WebAppModel model, IntPredicate action) {
captureLatch = new CountDownLatch(1);
thrown.set(null);

Platform.runLater(() -> {
//zoom should only be factored on raster prints
pageZoom = model.getZoom();
pageWidth = model.getWebWidth();
pageHeight = model.getWebHeight();

log.trace("Setting starting size {}:{}", pageWidth, pageHeight);
adjustSize(pageWidth * pageZoom, pageHeight * pageZoom);

if (pageHeight == 0) {
webView.setMinHeight(1);
webView.setPrefHeight(1);
webView.setMaxHeight(1);
}

autosize(webView);

printAction = action;

loadSource(model);
});
}

protected void loadSource(WebAppModel model) {
if (model.isPlainText()) {
webView.getEngine().loadContent(model.getSource(), "text/html");
} else {
webView.getEngine().load(model.getSource());
}
}

protected double findHeight() {
String heightText = webView.getEngine().executeScript("Math.max(document.body.offsetHeight, document.body.scrollHeight)").toString();
return Double.parseDouble(heightText);
}

protected void adjustSize(double toWidth, double toHeight) {
webView.setMinSize(toWidth, toHeight);
webView.setPrefSize(toWidth, toHeight);
webView.setMaxSize(toWidth, toHeight);
}

protected void firePrintAction() {
Platform.runLater(() -> new AnimationTimer() {
int frames = 0;

@Override
public void handle(long l) {
if (printAction.test(++frames)) {
stop();
}
}
}.start());
}

/**
* Fix blank page after autosize is called
*/
protected void autosize(WebView webView) {
webView.autosize();

if (!raster) {
// Call updatePeer; fixes a bug with webView resizing
// Can be avoided by calling stage.show() but breaks headless environments
// See: https://github.com/qzind/tray/issues/513
String[] methods = {"impl_updatePeer" /*jfx8*/, "doUpdatePeer" /*jfx11*/};
try {
for(Method m : webView.getClass().getDeclaredMethods()) {
for(String method : methods) {
if (m.getName().equals(method)) {
m.setAccessible(true);
m.invoke(webView);
return;
}
}
}
}
catch(SecurityException | ReflectiveOperationException e) {
log.warn("Unable to update peer; Blank pages may occur.", e);
}
}
}

protected double calculateSupportedZoom(double width, double height) {
long memory = Runtime.getRuntime().maxMemory();
int allowance = (memory / 1048576L) > 1024? 3:2;
if (WebApp.isHeadless()) { allowance--; }
long availSpace = memory << allowance;

// Memory needed for print is roughly estimated as
// (width * height) [pixels needed] * (pageZoom * 72d) [print density used] * 3 [rgb channels]
return Math.sqrt(availSpace / ((width * height) * (pageZoom * 72d) * 3));
}

protected void disableHtmlScrollbars() {
//ensure html tag doesn't use scrollbars, clipping page instead
Document doc = webView.getEngine().getDocument();
NodeList tags = doc.getElementsByTagName("html");
if (tags != null && tags.getLength() > 0) {
Node base = tags.item(0);
Attr applied = (Attr)base.getAttributes().getNamedItem("style");
if (applied == null) {
applied = doc.createAttribute("style");
}
applied.setValue(applied.getValue() + "; overflow: hidden;");
base.getAttributes().setNamedItem(applied);
}
}

protected boolean hasBody() {
boolean hasBody = (boolean)webView.getEngine().executeScript("document.body != null");
if (!hasBody) {
log.warn("Loaded page has no body - likely a redirect, skipping state");
}
return hasBody;
}

/**
* Final cleanup when no longer capturing
*/
protected void unlatch(Throwable t) {
//todo kill this instead of hiding
if (t != null) {
thrown.set(t);
}

captureLatch.countDown();
renderStage.hide();
}
}
Loading