# Description of Changes This pull request includes several changes primarily focused on improving configuration management, removing deprecated methods, and updating paths for external dependencies. The most important changes are summarized below: ### Configuration Management Improvements: * Added a new `RuntimePathConfig` class to manage dynamic paths for operations and pipeline configurations (`src/main/java/stirling/software/SPDF/config/RuntimePathConfig.java`). * Removed the `bookAndHtmlFormatsInstalled` bean and its associated logic from `AppConfig` and `EndpointConfiguration` (`src/main/java/stirling/software/SPDF/config/AppConfig.java`, `src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java`). [[1]](diffhunk://#diff-4d774ec79aa55750c0a4739bee971b68877078b73654e863fd40ee924347e143L130-L138) [[2]](diffhunk://#diff-750f31f6ecbd64b025567108a33775cad339e835a04360affff82a09410b697dL12-L35) [[3]](diffhunk://#diff-750f31f6ecbd64b025567108a33775cad339e835a04360affff82a09410b697dL275-L280) ### External Dependency Path Updates: * Updated paths for `weasyprint` and `unoconvert` in `ExternalAppDepConfig` to use values from `RuntimePathConfig` (`src/main/java/stirling/software/SPDF/config/ExternalAppDepConfig.java`). [[1]](diffhunk://#diff-c47af298c07c2622aa98b038b78822c56bdb002de71081e102d344794e7832a6R12-L33) [[2]](diffhunk://#diff-c47af298c07c2622aa98b038b78822c56bdb002de71081e102d344794e7832a6L104-R115) ### Minor Adjustments: * Corrected a typo from "Unoconv" to "Unoconvert" in `EndpointConfiguration` (`src/main/java/stirling/software/SPDF/config/EndpointConfiguration.java`). --- ## 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.
419 lines
18 KiB
Java
419 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();
|
|
}
|
|
}
|