# Description of Changes This pull request includes several updates to the Docker configuration and Java application UI scaling. The changes enhance environment variable management, dependency installation, and UI responsiveness to different screen sizes. ### Docker Configuration Updates: * Added new environment variables `STIRLING_PDF_DESKTOP_UI`, `PYTHONPATH`, `UNO_PATH`, and `URE_BOOTSTRAP` to `Dockerfile` and `Dockerfile.fat` to improve the configuration and integration of the LibreOffice environment. [[1]](diffhunk://#diff-dd2c0eb6ea5cfc6c4bd4eac30934e2d5746747af48fef6da689e85b752f39557L38-R46) [[2]](diffhunk://#diff-571631582b988e88c52c86960cc083b0b8fa63cf88f056f26e9e684195221c27L40-R49) * Updated the `CMD` instruction in `Dockerfile` and `Dockerfile.fat` to run both the Java application and `unoserver` simultaneously. [[1]](diffhunk://#diff-dd2c0eb6ea5cfc6c4bd4eac30934e2d5746747af48fef6da689e85b752f39557L87-R96) [[2]](diffhunk://#diff-571631582b988e88c52c86960cc083b0b8fa63cf88f056f26e9e684195221c27L87-R100) * Modified the `RUN` instruction to include additional Python dependencies and setup a virtual environment. [[1]](diffhunk://#diff-dd2c0eb6ea5cfc6c4bd4eac30934e2d5746747af48fef6da689e85b752f39557L68-R81) [[2]](diffhunk://#diff-571631582b988e88c52c86960cc083b0b8fa63cf88f056f26e9e684195221c27R72-R86) ### Workflow Enhancements: * Added `STIRLING_PDF_DESKTOP_UI` environment variable to the GitHub Actions workflows (`PR-Demo-Comment.yml` and `push-docker.yml`) to ensure consistent environment settings. [[1]](diffhunk://#diff-145fe5c0ed8c24e4673c9ad39800dd171a2d0a2e8050497cff980fc7e3a3df0dR106) [[2]](diffhunk://#diff-76056236de05155107f6a660f1e3956059e37338011b8f0e72188afcb9b17b6fR41) ### Java Application UI Scaling: * Introduced `UIScaling` utility to dynamically adjust the size of UI components based on screen resolution in `DesktopBrowser` and `LoadingWindow` classes. [[1]](diffhunk://#diff-dff83b0fe53cba8ee80dc8cee96b9c2bfec612ec1f2c636ebdf22dedb36671e8L218-R219) [[2]](diffhunk://#diff-dff83b0fe53cba8ee80dc8cee96b9c2bfec612ec1f2c636ebdf22dedb36671e8L267-R270) [[3]](diffhunk://#diff-3e287daf297213b698b3c94d6e6ed4aae139d570ba6b115da459d72b5c36c42fL44-R64) [[4]](diffhunk://#diff-3e287daf297213b698b3c94d6e6ed4aae139d570ba6b115da459d72b5c36c42fL86-R102) * Improved the loading of icons by using the `UIScaling` utility for better visual quality. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: pixeebot[bot] <104101892+pixeebot[bot]@users.noreply.github.com> Co-authored-by: a <a>
416 lines
18 KiB
Java
416 lines
18 KiB
Java
package stirling.software.SPDF.UI.impl;
|
|
|
|
import java.awt.AWTException;
|
|
import java.awt.BorderLayout;
|
|
import java.awt.Frame;
|
|
import java.awt.Image;
|
|
import java.awt.MenuItem;
|
|
import java.awt.PopupMenu;
|
|
import java.awt.SystemTray;
|
|
import java.awt.TrayIcon;
|
|
import java.awt.event.WindowEvent;
|
|
import java.awt.event.WindowStateListener;
|
|
import java.io.File;
|
|
import java.io.InputStream;
|
|
import java.util.Objects;
|
|
import java.util.concurrent.CompletableFuture;
|
|
|
|
import javax.imageio.ImageIO;
|
|
import javax.swing.JFrame;
|
|
import javax.swing.JPanel;
|
|
import javax.swing.SwingUtilities;
|
|
import javax.swing.Timer;
|
|
|
|
import org.cef.CefApp;
|
|
import org.cef.CefClient;
|
|
import org.cef.CefSettings;
|
|
import org.cef.browser.CefBrowser;
|
|
import org.cef.callback.CefBeforeDownloadCallback;
|
|
import org.cef.callback.CefDownloadItem;
|
|
import org.cef.callback.CefDownloadItemCallback;
|
|
import org.cef.handler.CefDownloadHandlerAdapter;
|
|
import org.cef.handler.CefLoadHandlerAdapter;
|
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
import jakarta.annotation.PreDestroy;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import me.friwi.jcefmaven.CefAppBuilder;
|
|
import me.friwi.jcefmaven.EnumProgress;
|
|
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
|
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
|
import stirling.software.SPDF.UI.WebBrowser;
|
|
import stirling.software.SPDF.config.InstallationPathConfig;
|
|
import stirling.software.SPDF.utils.UIScaling;
|
|
|
|
@Component
|
|
@Slf4j
|
|
@ConditionalOnProperty(
|
|
name = "STIRLING_PDF_DESKTOP_UI",
|
|
havingValue = "true",
|
|
matchIfMissing = false)
|
|
public class DesktopBrowser implements WebBrowser {
|
|
private static CefApp cefApp;
|
|
private static CefClient client;
|
|
private static CefBrowser browser;
|
|
private static JFrame frame;
|
|
private static LoadingWindow loadingWindow;
|
|
private static volatile boolean browserInitialized = false;
|
|
private static TrayIcon trayIcon;
|
|
private static SystemTray systemTray;
|
|
|
|
public DesktopBrowser() {
|
|
SwingUtilities.invokeLater(
|
|
() -> {
|
|
loadingWindow = new LoadingWindow(null, "Initializing...");
|
|
loadingWindow.setVisible(true);
|
|
});
|
|
}
|
|
|
|
public void initWebUI(String url) {
|
|
CompletableFuture.runAsync(
|
|
() -> {
|
|
try {
|
|
CefAppBuilder builder = new CefAppBuilder();
|
|
configureCefSettings(builder);
|
|
builder.setProgressHandler(createProgressHandler());
|
|
builder.setInstallDir(
|
|
new File(InstallationPathConfig.getClientWebUIPath()));
|
|
// Build and initialize CEF
|
|
cefApp = builder.build();
|
|
client = cefApp.createClient();
|
|
|
|
// Set up download handler
|
|
setupDownloadHandler();
|
|
|
|
// Create browser and frame on EDT
|
|
SwingUtilities.invokeAndWait(
|
|
() -> {
|
|
browser = client.createBrowser(url, false, false);
|
|
setupMainFrame();
|
|
setupLoadHandler();
|
|
|
|
// Show the frame immediately but transparent
|
|
frame.setVisible(true);
|
|
});
|
|
} catch (Exception e) {
|
|
log.error("Error initializing JCEF browser: ", e);
|
|
cleanup();
|
|
}
|
|
});
|
|
}
|
|
|
|
private void configureCefSettings(CefAppBuilder builder) {
|
|
CefSettings settings = builder.getCefSettings();
|
|
String basePath = InstallationPathConfig.getClientWebUIPath();
|
|
log.info("basePath " + basePath);
|
|
settings.cache_path = new File(basePath + "cache").getAbsolutePath();
|
|
settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath();
|
|
// settings.browser_subprocess_path = new File(basePath +
|
|
// "subprocess").getAbsolutePath();
|
|
// settings.resources_dir_path = new File(basePath + "resources").getAbsolutePath();
|
|
// settings.locales_dir_path = new File(basePath + "locales").getAbsolutePath();
|
|
settings.log_file = new File(basePath, "debug.log").getAbsolutePath();
|
|
|
|
settings.persist_session_cookies = true;
|
|
settings.windowless_rendering_enabled = false;
|
|
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
|
|
|
|
builder.setAppHandler(
|
|
new MavenCefAppHandlerAdapter() {
|
|
@Override
|
|
public void stateHasChanged(org.cef.CefApp.CefAppState state) {
|
|
log.info("CEF state changed: " + state);
|
|
if (state == CefApp.CefAppState.TERMINATED) {
|
|
System.exit(0);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private void setupDownloadHandler() {
|
|
client.addDownloadHandler(
|
|
new CefDownloadHandlerAdapter() {
|
|
@Override
|
|
public boolean onBeforeDownload(
|
|
CefBrowser browser,
|
|
CefDownloadItem downloadItem,
|
|
String suggestedName,
|
|
CefBeforeDownloadCallback callback) {
|
|
callback.Continue("", true);
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void onDownloadUpdated(
|
|
CefBrowser browser,
|
|
CefDownloadItem downloadItem,
|
|
CefDownloadItemCallback callback) {
|
|
if (downloadItem.isComplete()) {
|
|
log.info("Download completed: " + downloadItem.getFullPath());
|
|
} else if (downloadItem.isCanceled()) {
|
|
log.info("Download canceled: " + downloadItem.getFullPath());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private ConsoleProgressHandler createProgressHandler() {
|
|
return new ConsoleProgressHandler() {
|
|
@Override
|
|
public void handleProgress(EnumProgress state, float percent) {
|
|
Objects.requireNonNull(state, "state cannot be null");
|
|
SwingUtilities.invokeLater(
|
|
() -> {
|
|
if (loadingWindow != null) {
|
|
switch (state) {
|
|
case LOCATING:
|
|
loadingWindow.setStatus("Locating Files...");
|
|
loadingWindow.setProgress(0);
|
|
break;
|
|
case DOWNLOADING:
|
|
if (percent >= 0) {
|
|
loadingWindow.setStatus(
|
|
String.format(
|
|
"Downloading additional files: %.0f%%",
|
|
percent));
|
|
loadingWindow.setProgress((int) percent);
|
|
}
|
|
break;
|
|
case EXTRACTING:
|
|
loadingWindow.setStatus("Extracting files...");
|
|
loadingWindow.setProgress(60);
|
|
break;
|
|
case INITIALIZING:
|
|
loadingWindow.setStatus("Initializing UI...");
|
|
loadingWindow.setProgress(80);
|
|
break;
|
|
case INITIALIZED:
|
|
loadingWindow.setStatus("Finalising startup...");
|
|
loadingWindow.setProgress(90);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
};
|
|
}
|
|
|
|
private void setupMainFrame() {
|
|
frame = new JFrame("Stirling-PDF");
|
|
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
|
frame.setUndecorated(true);
|
|
frame.setOpacity(0.0f);
|
|
|
|
JPanel contentPane = new JPanel(new BorderLayout());
|
|
contentPane.setDoubleBuffered(true);
|
|
contentPane.add(browser.getUIComponent(), BorderLayout.CENTER);
|
|
frame.setContentPane(contentPane);
|
|
|
|
frame.addWindowListener(
|
|
new java.awt.event.WindowAdapter() {
|
|
@Override
|
|
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
|
|
cleanup();
|
|
System.exit(0);
|
|
}
|
|
});
|
|
|
|
frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800));
|
|
frame.setLocationRelativeTo(null);
|
|
|
|
loadIcon();
|
|
}
|
|
|
|
private void setupLoadHandler() {
|
|
final long initStartTime = System.currentTimeMillis();
|
|
log.info("Setting up load handler at: {}", initStartTime);
|
|
|
|
client.addLoadHandler(
|
|
new CefLoadHandlerAdapter() {
|
|
@Override
|
|
public void onLoadingStateChange(
|
|
CefBrowser browser,
|
|
boolean isLoading,
|
|
boolean canGoBack,
|
|
boolean canGoForward) {
|
|
log.debug(
|
|
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward: {}, "
|
|
+ "browserInitialized: {}, Time elapsed: {}ms",
|
|
isLoading,
|
|
canGoBack,
|
|
canGoForward,
|
|
browserInitialized,
|
|
System.currentTimeMillis() - initStartTime);
|
|
|
|
if (!isLoading && !browserInitialized) {
|
|
log.info(
|
|
"Browser finished loading, preparing to initialize UI components");
|
|
browserInitialized = true;
|
|
SwingUtilities.invokeLater(
|
|
() -> {
|
|
try {
|
|
if (loadingWindow != null) {
|
|
log.info("Starting UI initialization sequence");
|
|
|
|
// Close loading window first
|
|
loadingWindow.setVisible(false);
|
|
loadingWindow.dispose();
|
|
loadingWindow = null;
|
|
log.info("Loading window disposed");
|
|
|
|
// Then setup the main frame
|
|
frame.setVisible(false);
|
|
frame.dispose();
|
|
frame.setOpacity(1.0f);
|
|
frame.setUndecorated(false);
|
|
frame.pack();
|
|
frame.setSize(
|
|
UIScaling.scaleWidth(1280),
|
|
UIScaling.scaleHeight(800));
|
|
frame.setLocationRelativeTo(null);
|
|
log.debug("Frame reconfigured");
|
|
|
|
// Show the main frame
|
|
frame.setVisible(true);
|
|
frame.requestFocus();
|
|
frame.toFront();
|
|
log.info("Main frame displayed and focused");
|
|
|
|
// Focus the browser component
|
|
Timer focusTimer =
|
|
new Timer(
|
|
100,
|
|
e -> {
|
|
try {
|
|
browser.getUIComponent()
|
|
.requestFocus();
|
|
log.info(
|
|
"Browser component focused");
|
|
} catch (Exception ex) {
|
|
log.error(
|
|
"Error focusing browser",
|
|
ex);
|
|
}
|
|
});
|
|
focusTimer.setRepeats(false);
|
|
focusTimer.start();
|
|
}
|
|
} catch (Exception e) {
|
|
log.error("Error during UI initialization", e);
|
|
// Attempt cleanup on error
|
|
if (loadingWindow != null) {
|
|
loadingWindow.dispose();
|
|
loadingWindow = null;
|
|
}
|
|
if (frame != null) {
|
|
frame.setVisible(true);
|
|
frame.requestFocus();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private void setupTrayIcon(Image icon) {
|
|
if (!SystemTray.isSupported()) {
|
|
log.warn("System tray is not supported");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
systemTray = SystemTray.getSystemTray();
|
|
|
|
// Create popup menu
|
|
PopupMenu popup = new PopupMenu();
|
|
|
|
// Create menu items
|
|
MenuItem showItem = new MenuItem("Show");
|
|
showItem.addActionListener(
|
|
e -> {
|
|
frame.setVisible(true);
|
|
frame.setState(Frame.NORMAL);
|
|
});
|
|
|
|
MenuItem exitItem = new MenuItem("Exit");
|
|
exitItem.addActionListener(
|
|
e -> {
|
|
cleanup();
|
|
System.exit(0);
|
|
});
|
|
|
|
// Add menu items to popup menu
|
|
popup.add(showItem);
|
|
popup.addSeparator();
|
|
popup.add(exitItem);
|
|
|
|
// Create tray icon
|
|
trayIcon = new TrayIcon(icon, "Stirling-PDF", popup);
|
|
trayIcon.setImageAutoSize(true);
|
|
|
|
// Add double-click behavior
|
|
trayIcon.addActionListener(
|
|
e -> {
|
|
frame.setVisible(true);
|
|
frame.setState(Frame.NORMAL);
|
|
});
|
|
|
|
// Add tray icon to system tray
|
|
systemTray.add(trayIcon);
|
|
|
|
// Modify frame behavior to minimize to tray
|
|
frame.addWindowStateListener(
|
|
new WindowStateListener() {
|
|
public void windowStateChanged(WindowEvent e) {
|
|
if (e.getNewState() == Frame.ICONIFIED) {
|
|
frame.setVisible(false);
|
|
}
|
|
}
|
|
});
|
|
|
|
} catch (AWTException e) {
|
|
log.error("Error setting up system tray icon", e);
|
|
}
|
|
}
|
|
|
|
private void loadIcon() {
|
|
try {
|
|
Image icon = null;
|
|
String[] iconPaths = {"/static/favicon.ico"};
|
|
|
|
for (String path : iconPaths) {
|
|
if (icon != null) break;
|
|
try {
|
|
try (InputStream is = getClass().getResourceAsStream(path)) {
|
|
if (is != null) {
|
|
icon = ImageIO.read(is);
|
|
break;
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
log.debug("Could not load icon from " + path, e);
|
|
}
|
|
}
|
|
|
|
if (icon != null) {
|
|
frame.setIconImage(icon);
|
|
setupTrayIcon(icon);
|
|
} else {
|
|
log.warn("Could not load icon from any source");
|
|
}
|
|
} catch (Exception e) {
|
|
log.error("Error loading icon", e);
|
|
}
|
|
}
|
|
|
|
@PreDestroy
|
|
public void cleanup() {
|
|
if (browser != null) browser.close(true);
|
|
if (client != null) client.dispose();
|
|
if (cefApp != null) cefApp.dispose();
|
|
if (loadingWindow != null) loadingWindow.dispose();
|
|
}
|
|
}
|