Add minimize to tray

This commit is contained in:
0xb00bface 2021-04-18 12:17:02 +02:00
parent ce98919499
commit 6e9b92effa
5 changed files with 246 additions and 111 deletions

View File

@ -1,6 +1,5 @@
package ctbrec.ui;
import static ctbrec.event.Event.Type.*;
import java.io.BufferedReader;
@ -15,6 +14,7 @@ import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
@ -110,7 +110,6 @@ public class CamrecApplication extends Application {
private int activeRecordings = 0;
private double bytesPerSecond = 0;
@Override
public void start(Stage primaryStage) throws Exception {
this.primaryStage = primaryStage;
@ -134,6 +133,26 @@ public class CamrecApplication extends Application {
createGui(primaryStage);
checkForUpdates();
registerClipboardListener();
registerTrayIconListener();
}
private void registerTrayIconListener() {
EventBusHolder.BUS.register(new Object() {
@Subscribe
public void trayActionRequest(Map<String, Object> evt) {
if (Objects.equals("shutdown", evt.get("event"))) {
LOG.debug("Shutdown request from tray icon");
try {
Platform.runLater(() -> {
primaryStage.show();
shutdown();
});
} catch (Exception ex) {
LOG.error(ex.getMessage(), ex);
}
}
}
});
}
private void initSites() {
@ -152,7 +171,7 @@ public class CamrecApplication extends Application {
}
private void registerClipboardListener() {
if(config.getSettings().monitorClipboard) {
if (config.getSettings().monitorClipboard) {
ClipboardListener clipboardListener = new ClipboardListener(recorder, sites);
scheduler.scheduleAtFixedRate(clipboardListener, 0, 1, TimeUnit.SECONDS);
}
@ -160,11 +179,11 @@ public class CamrecApplication extends Application {
private void startOnlineMonitor() {
for (Site site : sites) {
if(site.isEnabled()) {
if (site.isEnabled()) {
try {
site.setRecorder(recorder);
site.init();
} catch(Exception e) {
} catch (Exception e) {
LOG.error("Error while initializing site {}", site.getName(), e);
}
}
@ -181,6 +200,8 @@ public class CamrecApplication extends Application {
private void createGui(Stage primaryStage) throws IOException {
LOG.debug("Creating GUI");
DesktopIntegration.setRecorder(recorder);
DesktopIntegration.setPrimaryStage(primaryStage);
CamrecApplication.title = "CTB Recorder " + getVersion();
primaryStage.setTitle(title);
InputStream icon = getClass().getResourceAsStream("/icon.png");
@ -188,7 +209,6 @@ public class CamrecApplication extends Application {
int windowWidth = Config.getInstance().getSettings().windowWidth;
int windowHeight = Config.getInstance().getSettings().windowHeight;
Scene scene = new Scene(rootPane, windowWidth, windowHeight);
primaryStage.setScene(scene);
Dialogs.setScene(scene);
@ -217,7 +237,7 @@ public class CamrecApplication extends Application {
switchToStartTab();
writeColorSchemeStyleSheet();
Color base = Color.web(Config.getInstance().getSettings().colorBase);
if(!base.equals(Color.WHITE)) {
if (!base.equals(Color.WHITE)) {
loadStyleSheet(primaryStage, "color.css");
}
loadStyleSheet(primaryStage, "style.css");
@ -230,8 +250,7 @@ public class CamrecApplication extends Application {
primaryStage.getScene().heightProperty()
.addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowHeight = newVal.intValue());
primaryStage.setMaximized(Config.getInstance().getSettings().windowMaximized);
primaryStage.maximizedProperty()
.addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowMaximized = newVal);
primaryStage.maximizedProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowMaximized = newVal);
Player.scene = primaryStage.getScene();
primaryStage.setX(Config.getInstance().getSettings().windowX);
primaryStage.setY(Config.getInstance().getSettings().windowY);
@ -239,6 +258,8 @@ public class CamrecApplication extends Application {
primaryStage.yProperty().addListener((observable, oldVal, newVal) -> Config.getInstance().getSettings().windowY = newVal.intValue());
primaryStage.show();
primaryStage.setOnCloseRequest(createShutdownHandler());
Runtime.getRuntime().addShutdownHook(new Thread(() -> Platform.runLater(this::shutdown)));
setWindowMinimizeListener(primaryStage);
// register changelistener to activate / deactivate tabs, when the user switches between them
tabPane.getSelectionModel().selectedItemProperty().addListener((ov, from, to) -> {
@ -254,84 +275,95 @@ public class CamrecApplication extends Application {
HBox.setMargin(statusLabel, new Insets(10, 10, 10, 10));
}
private void setWindowMinimizeListener(Stage primaryStage) {
primaryStage.iconifiedProperty().addListener((obs, oldV, newV) -> {
if (newV.booleanValue() && Config.getInstance().getSettings().minimizeToTray && primaryStage.isShowing()) {
DesktopIntegration.minimizeToTray(primaryStage);
}
});
}
private javafx.event.EventHandler<WindowEvent> createShutdownHandler() {
return e -> {
e.consume();
// check for active downloads
if (recordingsTab.isDownloadRunning()) {
boolean exitAnyway = Dialogs.showConfirmDialog("Shutdown", "Do you want to exit anyway?", "There are downloads running",
primaryStage.getScene());
if (!exitAnyway) {
return;
}
}
// check for active recordings
boolean shutdownNow = false;
if (config.getSettings().localRecording) {
try {
if (!recorder.getCurrentlyRecording().isEmpty()) {
ButtonType result = Dialogs.showShutdownDialog(primaryStage.getScene());
if (result == ButtonType.NO) {
return;
} else if (result == ButtonType.YES) {
shutdownNow = true;
}
}
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException ex) {
LOG.warn("Couldn't check, if recordings are running");
}
}
Alert shutdownInfo = new AutosizeAlert(Alert.AlertType.INFORMATION, primaryStage.getScene());
shutdownInfo.setTitle("Shutdown");
shutdownInfo.setContentText("Shutting down. Please wait while recordings are finished...");
shutdownInfo.show();
final boolean immediately = shutdownNow;
new Thread(() -> {
for (Tab tab : tabPane.getTabs()) {
if (tab instanceof ShutdownListener) {
((ShutdownListener) tab).onShutdown();
}
}
onlineMonitor.shutdown();
recorder.shutdown(immediately);
for (Site site : sites) {
if (site.isEnabled()) {
site.shutdown();
}
}
try {
Config.getInstance().save();
LOG.info("Shutdown complete. Goodbye!");
Platform.runLater(() -> {
primaryStage.close();
shutdownInfo.close();
Platform.exit();
// This is needed, because OkHttp?! seems to block the shutdown with its writer threads. They are not daemon threads :(
System.exit(0);
});
} catch (IOException e1) {
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, primaryStage.getScene());
alert.setTitle("Error saving settings");
alert.setContentText("Couldn't save settings: " + e1.getLocalizedMessage());
alert.showAndWait();
System.exit(1);
});
}
try {
ExternalBrowser.getInstance().close();
} catch (IOException e12) {
// noop
}
scheduler.shutdownNow();
}).start();
shutdown();
};
}
private void shutdown() {
// check for active downloads
if (recordingsTab.isDownloadRunning()) {
boolean exitAnyway = Dialogs.showConfirmDialog("Shutdown", "Do you want to exit anyway?", "There are downloads running", primaryStage.getScene());
if (!exitAnyway) {
return;
}
}
// check for active recordings
boolean shutdownNow = false;
if (config.getSettings().localRecording) {
try {
if (!recorder.getCurrentlyRecording().isEmpty()) {
ButtonType result = Dialogs.showShutdownDialog(primaryStage.getScene());
if (result == ButtonType.NO) {
return;
} else if (result == ButtonType.YES) {
shutdownNow = true;
}
}
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException ex) {
LOG.warn("Couldn't check, if recordings are running");
}
}
Alert shutdownInfo = new AutosizeAlert(Alert.AlertType.INFORMATION, primaryStage.getScene());
shutdownInfo.setTitle("Shutdown");
shutdownInfo.setContentText("Shutting down. Please wait while recordings are finished...");
shutdownInfo.show();
final boolean immediately = shutdownNow;
new Thread(() -> {
for (Tab tab : tabPane.getTabs()) {
if (tab instanceof ShutdownListener) {
((ShutdownListener) tab).onShutdown();
}
}
onlineMonitor.shutdown();
recorder.shutdown(immediately);
for (Site site : sites) {
if (site.isEnabled()) {
site.shutdown();
}
}
try {
Config.getInstance().save();
LOG.info("Shutdown complete. Goodbye!");
Platform.runLater(() -> {
primaryStage.close();
shutdownInfo.close();
Platform.exit();
// This is needed, because OkHttp?! seems to block the shutdown with its writer threads. They are not daemon threads :(
System.exit(0);
});
} catch (IOException e1) {
Platform.runLater(() -> {
Alert alert = new AutosizeAlert(Alert.AlertType.ERROR, primaryStage.getScene());
alert.setTitle("Error saving settings");
alert.setContentText("Couldn't save settings: " + e1.getLocalizedMessage());
alert.showAndWait();
System.exit(1);
});
}
try {
ExternalBrowser.getInstance().close();
} catch (IOException e12) {
// noop
}
scheduler.shutdownNow();
}).start();
}
private void registerAlertSystem() {
for (EventHandlerConfiguration eventHandlerConfig : Config.getInstance().getSettings().eventHandlers) {
EventHandler handler = new EventHandler(eventHandlerConfig);
@ -377,7 +409,7 @@ public class CamrecApplication extends Application {
private void registerBandwidthMeterListener() {
BandwidthMeter.addListener((bytes, dur) -> {
long millis = dur.toMillis();
double bytesPerMilli = bytes / (double)millis;
double bytesPerMilli = bytes / (double) millis;
bytesPerSecond = bytesPerMilli * 1000;
updateStatus();
});
@ -394,39 +426,35 @@ public class CamrecApplication extends Application {
private void writeColorSchemeStyleSheet() {
File colorCss = new File(Config.getInstance().getConfigDir(), "color.css");
try(FileOutputStream fos = new FileOutputStream(colorCss)) {
String content = ".root {\n" +
" -fx-base: "+Config.getInstance().getSettings().colorBase+";\n" +
" -fx-accent: "+Config.getInstance().getSettings().colorAccent+";\n" +
" -fx-default-button: -fx-accent;\n" +
" -fx-focus-color: -fx-accent;\n" +
" -fx-control-inner-background-alt: derive(-fx-base, 95%);\n" +
"}";
try (FileOutputStream fos = new FileOutputStream(colorCss)) {
String content = ".root {\n" + " -fx-base: " + Config.getInstance().getSettings().colorBase + ";\n" + " -fx-accent: "
+ Config.getInstance().getSettings().colorAccent + ";\n" + " -fx-default-button: -fx-accent;\n" + " -fx-focus-color: -fx-accent;\n"
+ " -fx-control-inner-background-alt: derive(-fx-base, 95%);\n" + "}";
fos.write(content.getBytes(StandardCharsets.UTF_8));
} catch(Exception e) {
} catch (Exception e) {
LOG.error("Couldn't write stylesheet for user defined color theme");
}
}
private void loadStyleSheet(Stage primaryStage, String filename) {
public static void loadStyleSheet(Stage primaryStage, String filename) {
File css = new File(Config.getInstance().getConfigDir(), filename);
if(css.exists() && css.isFile()) {
if (css.exists() && css.isFile()) {
primaryStage.getScene().getStylesheets().add(css.toURI().toString());
}
}
private void switchToStartTab() {
String startTab = Config.getInstance().getSettings().startTab;
if(StringUtil.isNotBlank(startTab)) {
if (StringUtil.isNotBlank(startTab)) {
for (Tab tab : tabPane.getTabs()) {
if(Objects.equals(startTab, tab.getText())) {
if (Objects.equals(startTab, tab.getText())) {
tabPane.getSelectionModel().select(tab);
break;
}
}
}
if(tabPane.getSelectionModel().getSelectedItem() instanceof TabSelectionListener) {
((TabSelectionListener)tabPane.getSelectionModel().getSelectedItem()).selected();
if (tabPane.getSelectionModel().getSelectedItem() instanceof TabSelectionListener) {
((TabSelectionListener) tabPane.getSelectionModel().getSelectedItem()).selected();
}
}

View File

@ -3,24 +3,38 @@ package ctbrec.ui;
import java.awt.AWTException;
import java.awt.Desktop;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.TrayIcon.MessageType;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import javax.swing.SwingUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ctbrec.OS;
import ctbrec.event.EventBusHolder;
import ctbrec.io.StreamRedirector;
import ctbrec.recorder.Recorder;
import ctbrec.ui.controls.Dialogs;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.control.Alert;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class DesktopIntegration {
@ -30,6 +44,8 @@ public class DesktopIntegration {
private static SystemTray tray;
private static TrayIcon trayIcon;
private static Recorder recorder;
private static Stage primaryStage;
public static void open(String uri) {
try {
@ -152,23 +168,109 @@ public class DesktopIntegration {
private static synchronized void notifySystemTray(String title, String header, String msg) {
if (SystemTray.isSupported()) {
if (tray == null) {
LOG.debug("Creating tray icon");
tray = SystemTray.getSystemTray();
Image image = Toolkit.getDefaultToolkit().createImage(DesktopIntegration.class.getResource("/icon64.png"));
trayIcon = new TrayIcon(image, title);
trayIcon.setImageAutoSize(true);
trayIcon.setToolTip(title);
try {
tray.add(trayIcon);
} catch (AWTException e) {
LOG.error("Coulnd't add tray icon", e);
}
}
LOG.debug("Display tray message");
createTrayIcon(primaryStage);
trayIcon.displayMessage(header, msg, MessageType.INFO);
} else {
LOG.error("SystemTray notifications not supported by this OS");
}
}
public static void minimizeToTray(Stage primaryStage) {
Platform.setImplicitExit(false);
boolean supported = createTrayIcon(primaryStage);
if (supported) {
primaryStage.hide();
}
}
private static boolean createTrayIcon(Stage stage) {
if (SystemTray.isSupported()) {
if (tray == null) {
String title = CamrecApplication.title;
tray = SystemTray.getSystemTray();
Image image = Toolkit.getDefaultToolkit().createImage(DesktopIntegration.class.getResource("/icon64.png"));
PopupMenu menu = createTrayContextMenu(stage);
trayIcon = new TrayIcon(image, title, menu);
trayIcon.setImageAutoSize(true);
trayIcon.setToolTip(title);
try {
tray.add(trayIcon);
} catch (AWTException e) {
LOG.error("Couldn't add tray icon", e);
}
trayIcon.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
toggleVisibility(stage);
}
}
});
}
return true;
} else {
LOG.error("SystemTray notifications not supported by this OS");
return false;
}
}
private static PopupMenu createTrayContextMenu(Stage stage) {
PopupMenu menu = new PopupMenu();
MenuItem show = new MenuItem("Show");
show.addActionListener(evt -> restoreStage(stage));
menu.add(show);
menu.addSeparator();
MenuItem pauseRecording = new MenuItem("Pause recording");
pauseRecording.addActionListener(evt -> {
try {
recorder.pause();
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
Dialogs.showError(stage.getScene(), "Pausing recording", "Pausing of the recorder failed", e);
}
});
menu.add(pauseRecording);
MenuItem resumeRecording = new MenuItem("Resume recording");
resumeRecording.addActionListener(evt -> {
try {
recorder.resume();
} catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
Dialogs.showError(stage.getScene(), "Resuming recording", "Resuming of the recorder failed", e);
}
});
menu.add(resumeRecording);
menu.addSeparator();
MenuItem exit = new MenuItem("Exit");
exit.addActionListener(evt -> exit(stage));
menu.add(exit);
return menu;
}
private static void toggleVisibility(Stage stage) {
if (stage.isShowing()) {
Platform.runLater(stage::hide);
} else {
restoreStage(stage);
}
}
private static void restoreStage(Stage stage) {
Platform.runLater(() -> {
stage.setIconified(false);
stage.show();
stage.toFront();
});
}
private static void exit(Stage stage) {
EventBusHolder.BUS.post(Map.of("event", "shutdown"));
}
public static void setRecorder(Recorder recorder) {
DesktopIntegration.recorder = recorder;
}
public static void setPrimaryStage(Stage primaryStage) {
DesktopIntegration.primaryStage = primaryStage;
}
}

View File

@ -17,6 +17,7 @@ public class Launcher {
LOG.error("Your Java version ({}) is too old. Please update to Java 10 or newer", javaVersion);
System.exit(1);
}
System.setProperty("awt.useSystemAAFontSettings","lcd");
CamrecApplication.main(args);
}
}

View File

@ -138,6 +138,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
private IgnoreList ignoreList;
private Label restartNotification;
private SimpleIntegerProperty playlistRequestTimeout;
private SimpleBooleanProperty minimizeToTray;
public SettingsTab(List<Site> sites, Recorder recorder) {
this.sites = sites;
@ -198,6 +199,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
hlsdlExecutable = new SimpleFileProperty(null, "hlsdlExecutable", settings.hlsdlExecutable);
recentlyWatched = new SimpleBooleanProperty(null, "recentlyWatched", settings.recentlyWatched);
playlistRequestTimeout = new SimpleIntegerProperty(null, "playlistRequestTimeout", settings.playlistRequestTimeout);
minimizeToTray = new SimpleBooleanProperty(null, "minimizeToTray", settings.minimizeToTray);
}
private void createGui() {
@ -223,6 +225,7 @@ public class SettingsTab extends Tab implements TabSelectionListener {
Setting.of("Enable live previews (experimental)", livePreviews),
Setting.of("Enable recently watched tab", recentlyWatched).needsRestart(),
Setting.of("Add models from clipboard", monitorClipboard, "Monitor clipboard for model URLs and automatically add them to the recorder").needsRestart(),
Setting.of("Minimize to tray", minimizeToTray, "Removes the app from the task bar, if minimized"),
Setting.of("Fast scroll speed", fastScrollSpeed, "Makes the thumbnail overviews scroll faster with the mouse wheel").needsRestart(),
Setting.of("Show confirmation dialogs", confirmationDialogs, "Show confirmation dialogs for irreversible actions"),
Setting.of("Total model count in title", totalModelCountInTitle, "Show the total number of models in the title bar"),

View File

@ -103,6 +103,7 @@ public class Settings {
public String mfcModelsTableSortType = "";
public String mfcPassword = "";
public String mfcUsername = "";
public boolean minimizeToTray = true;
public int minimumLengthInSeconds = 0;
public long minimumSpaceLeftInBytes = 0;
public Map<String, String> modelNotes = new HashMap<>();