diff --git a/client/src/main/java/envoy/client/data/Chat.java b/client/src/main/java/envoy/client/data/Chat.java index c941344..892b5ca 100644 --- a/client/src/main/java/envoy/client/data/Chat.java +++ b/client/src/main/java/envoy/client/data/Chat.java @@ -3,6 +3,7 @@ package envoy.client.data; import java.io.*; import java.util.*; +import javafx.beans.property.*; import javafx.collections.*; import envoy.data.*; @@ -21,18 +22,20 @@ import envoy.client.net.WriteProxy; */ public class Chat implements Serializable { - protected transient ObservableList messages = FXCollections.observableArrayList(); - - protected int unreadAmount; - protected boolean disabled; - - protected final Contact recipient; + protected boolean disabled; /** * Stores the last time an {@link envoy.event.IsTyping} event has been sent. */ protected transient long lastWritingEvent; + protected transient ObservableList messages = FXCollections.observableArrayList(); + + protected int unreadAmount; + protected static IntegerProperty totalUnreadAmount = new SimpleIntegerProperty(); + + protected final Contact recipient; + private static final long serialVersionUID = 2L; /** @@ -50,6 +53,7 @@ public class Chat implements Serializable { private void readObject(ObjectInputStream stream) throws ClassNotFoundException, IOException { stream.defaultReadObject(); messages = FXCollections.observableList((List) stream.readObject()); + totalUnreadAmount.set(totalUnreadAmount.get() + unreadAmount); } private void writeObject(ObjectOutputStream stream) throws IOException { @@ -111,6 +115,7 @@ public class Chat implements Serializable { writeProxy.writeMessageStatusChange(new MessageStatusChange(m)); } } + totalUnreadAmount.set(totalUnreadAmount.get() - unreadAmount); unreadAmount = 0; } @@ -150,6 +155,12 @@ public class Chat implements Serializable { return messages.removeIf(m -> m.getID() == messageID); } + /** + * @return an integer property storing the total amount of unread messages + * @since Envoy Client v0.3-beta + */ + public static IntegerProperty getTotalUnreadAmount() { return totalUnreadAmount; } + /** * Increments the amount of unread messages. * @@ -157,6 +168,7 @@ public class Chat implements Serializable { */ public void incrementUnreadAmount() { ++unreadAmount; + totalUnreadAmount.set(totalUnreadAmount.get() + 1); } /** diff --git a/client/src/main/java/envoy/client/data/LocalDB.java b/client/src/main/java/envoy/client/data/LocalDB.java index e3c4ac7..2ae3570 100644 --- a/client/src/main/java/envoy/client/data/LocalDB.java +++ b/client/src/main/java/envoy/client/data/LocalDB.java @@ -147,6 +147,7 @@ public final class LocalDB implements EventListener { throw new IllegalStateException("Client user is null, cannot initialize user storage"); userFile = new File(dbDir, user.getID() + ".db"); try (var in = new ObjectInputStream(new FileInputStream(userFile))) { + Chat.getTotalUnreadAmount().set(0); chats = FXCollections.observableList((List) in.readObject()); // Some chats have changed and should not be overwritten by the saved values diff --git a/client/src/main/java/envoy/client/data/SettingsItem.java b/client/src/main/java/envoy/client/data/SettingsItem.java index 590f965..b2c9199 100644 --- a/client/src/main/java/envoy/client/data/SettingsItem.java +++ b/client/src/main/java/envoy/client/data/SettingsItem.java @@ -1,7 +1,6 @@ package envoy.client.data; import java.io.Serializable; -import java.util.function.Consumer; import javax.swing.JComponent; @@ -17,8 +16,6 @@ public final class SettingsItem implements Serializable { private T value; private String userFriendlyName, description; - private transient Consumer changeHandler; - private static final long serialVersionUID = 1L; /** @@ -52,8 +49,6 @@ public final class SettingsItem implements Serializable { * @since Envoy Client v0.3-alpha */ public void set(T value) { - if (changeHandler != null && value != this.value) - changeHandler.accept(value); this.value = value; } @@ -82,16 +77,4 @@ public final class SettingsItem implements Serializable { * @since Envoy Client v0.3-alpha */ public void setDescription(String description) { this.description = description; } - - /** - * Sets a {@code ChangeHandler} for this {@link SettingsItem}. It will be invoked with the - * current value once during the registration and every time when the value changes. - * - * @param changeHandler the changeHandler to set - * @since Envoy Client v0.3-alpha - */ - public void setChangeHandler(Consumer changeHandler) { - this.changeHandler = changeHandler; - changeHandler.accept(value); - } } diff --git a/client/src/main/java/envoy/client/ui/Startup.java b/client/src/main/java/envoy/client/ui/Startup.java index 76fa54b..18468e3 100644 --- a/client/src/main/java/envoy/client/ui/Startup.java +++ b/client/src/main/java/envoy/client/ui/Startup.java @@ -237,17 +237,9 @@ public final class Startup extends Application { e.consume(); }); - if (StatusTrayIcon.isSupported()) { - - // Initialize status tray icon - final var trayIcon = new StatusTrayIcon(stage); - Settings.getInstance().getItems().get("hideOnClose").setChangeHandler(c -> { - if ((Boolean) c) - trayIcon.show(); - else - trayIcon.hide(); - }); - } + // Initialize status tray icon + if (StatusTrayIcon.isSupported()) + new StatusTrayIcon(stage).show(); // Start auto save thread localDB.initAutoSave(); diff --git a/client/src/main/java/envoy/client/ui/StatusTrayIcon.java b/client/src/main/java/envoy/client/ui/StatusTrayIcon.java index c21da7b..8cde432 100644 --- a/client/src/main/java/envoy/client/ui/StatusTrayIcon.java +++ b/client/src/main/java/envoy/client/ui/StatusTrayIcon.java @@ -1,7 +1,10 @@ package envoy.client.ui; +import static java.awt.Image.SCALE_SMOOTH; + import java.awt.*; import java.awt.TrayIcon.MessageType; +import java.awt.image.BufferedImage; import javafx.application.Platform; import javafx.stage.Stage; @@ -12,11 +15,19 @@ import dev.kske.eventbus.Event; import envoy.data.Message; import envoy.data.User.UserStatus; -import envoy.client.event.OwnStatusChange; +import envoy.client.data.*; +import envoy.client.event.*; import envoy.client.helper.ShutdownHelper; import envoy.client.util.*; /** + * A tray icon with the Envoy logo, an "Envoy" tool tip and a pop-up menu with menu items for + *
    + *
  • Changing the user status
  • + *
  • Logging out
  • + *
  • Quitting Envoy
  • + *
+ * * @author Kai S. K. Engelbart * @since Envoy Client v0.2-alpha */ @@ -32,7 +43,20 @@ public final class StatusTrayIcon implements EventListener { * A received {@link Message} is only displayed as a system tray notification if this variable * is set to {@code true}. */ - private boolean displayMessages; + private boolean displayMessageNotification; + + /** + * The size of the tray icon's image. + */ + private final Dimension size; + + /** + * The Envoy logo on which the current user status and unread message count will be drawn to + * compose the tray icon. + */ + private final Image logo; + + private static final Font unreadMessageFont = new Font("sans-serif", Font.PLAIN, 8); /** * @return {@code true} if the status tray icon is supported on this platform @@ -47,9 +71,9 @@ public final class StatusTrayIcon implements EventListener { * @since Envoy Client v0.2-beta */ public StatusTrayIcon(Stage stage) { - trayIcon = new TrayIcon(IconUtil.loadAWTCompatible("/icons/envoy_logo.png"), "Envoy"); - trayIcon.setImageAutoSize(true); - trayIcon.setToolTip("You are notified if you have unread messages."); + size = SystemTray.getSystemTray().getTrayIconSize(); + logo = IconUtil.loadAWTCompatible("/icons/envoy_logo.png").getScaledInstance(size.width, + size.height, SCALE_SMOOTH); final var popup = new PopupMenu(); @@ -60,10 +84,7 @@ public final class StatusTrayIcon implements EventListener { // Adding the logout menu item final var logoutMenuItem = new MenuItem("Logout"); - logoutMenuItem.addActionListener(evt -> { - hide(); - Platform.runLater(UserUtil::logout); - }); + logoutMenuItem.addActionListener(evt -> Platform.runLater(UserUtil::logout)); popup.add(logoutMenuItem); // Adding the status change items @@ -76,12 +97,17 @@ public final class StatusTrayIcon implements EventListener { } popup.add(statusSubMenu); - trayIcon.setPopupMenu(popup); + // Initialize the icon + trayIcon = new TrayIcon(createImage(), "Envoy", popup); // Only display messages if the stage is not focused and the current user status - // is not BUSY (if BUSY, displayMessages will be false) - stage.focusedProperty().addListener((ov, wasFocused, isFocused) -> displayMessages = - !displayMessages && wasFocused ? false : !isFocused); + // is not BUSY (if BUSY, displayMessageNotification will be false) + stage.focusedProperty() + .addListener((ov, wasFocused, isFocused) -> displayMessageNotification = + !displayMessageNotification && wasFocused ? false : !isFocused); + + // Listen to changes in the total unread message amount + Chat.getTotalUnreadAmount().addListener((ov, oldValue, newValue) -> updateImage()); // Show the window if the user clicks on the icon trayIcon.addActionListener(evt -> Platform.runLater(() -> { @@ -102,7 +128,7 @@ public final class StatusTrayIcon implements EventListener { public void show() { try { SystemTray.getSystemTray().add(trayIcon); - } catch (final AWTException e) {} + } catch (AWTException e) {} } /** @@ -110,22 +136,89 @@ public final class StatusTrayIcon implements EventListener { * * @since Envoy Client v0.2-beta */ + @Event(eventType = Logout.class) public void hide() { SystemTray.getSystemTray().remove(trayIcon); } @Event private void onOwnStatusChange(OwnStatusChange statusChange) { - displayMessages = !statusChange.get().equals(UserStatus.BUSY); + displayMessageNotification = !statusChange.get().equals(UserStatus.BUSY); + trayIcon.getImage().flush(); + trayIcon.setImage(createImage()); } @Event private void onMessage(Message message) { - if (displayMessages) + if (displayMessageNotification) trayIcon .displayMessage(message.hasAttachment() ? "New " + message.getAttachment().getType().toString().toLowerCase() + " message received" : "New message received", message.getText(), MessageType.INFO); } + + /** + * Updates the tray icon's image by first releasing the resources held by the current image and + * then setting a new one generated by the {@link StatusTrayIcon#createImage()} method. + * + * @since Envoy Client v0.3-beta + */ + private void updateImage() { + trayIcon.getImage().flush(); + trayIcon.setImage(createImage()); + } + + /** + * Composes an icon that displays the current user status and the amount of unread messages, if + * any are present. + * + * @since Envoy Client v0.3-beta + */ + private BufferedImage createImage() { + + // Create a new image with the dimensions of the logo + var img = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB); + + // Obtain the draw graphics of the image and copy the logo + var g = img.createGraphics(); + g.drawImage(logo, 0, 0, null); + + // Draw the current user status + switch (Context.getInstance().getLocalDB().getUser().getStatus()) { + case ONLINE: + g.setColor(Color.GREEN); + break; + case AWAY: + g.setColor(Color.ORANGE); + break; + case BUSY: + g.setColor(Color.RED); + break; + case OFFLINE: + g.setColor(Color.GRAY); + } + g.fillOval(size.width / 2, size.height / 2, size.width / 2, size.height / 2); + + // Draw total amount of unread messages, if any are present + if (Chat.getTotalUnreadAmount().get() > 0) { + + // Draw black background circle + g.setColor(Color.BLACK); + g.fillOval(size.width / 2, 0, size.width / 2, size.height / 2); + + // Unread amount in white + String unreadAmount = Chat.getTotalUnreadAmount().get() > 9 ? "9+" + : String.valueOf(Chat.getTotalUnreadAmount().get()); + g.setColor(Color.WHITE); + g.setFont(unreadMessageFont); + g.drawString(unreadAmount, + 3 * size.width / 4 - g.getFontMetrics().stringWidth(unreadAmount) / 2, + size.height / 2); + } + + // Finish drawing + g.dispose(); + return img; + } } diff --git a/client/src/main/java/envoy/client/ui/control/ChatControl.java b/client/src/main/java/envoy/client/ui/control/ChatControl.java index de50085..6c45d44 100644 --- a/client/src/main/java/envoy/client/ui/control/ChatControl.java +++ b/client/src/main/java/envoy/client/ui/control/ChatControl.java @@ -32,12 +32,12 @@ public final class ChatControl extends HBox { setPadding(new Insets(0, 0, 3, 0)); // Profile picture - final var contactProfilePic = + var contactProfilePic = new ProfilePicImageView(chat instanceof GroupChat ? groupIcon : userIcon, 32); getChildren().add(contactProfilePic); // Spacing - final var leftSpacing = new Region(); + var leftSpacing = new Region(); leftSpacing.setPrefSize(8, 0); leftSpacing.setMinSize(8, 0); leftSpacing.setMaxSize(8, 0); @@ -48,17 +48,15 @@ public final class ChatControl extends HBox { // Unread messages if (chat.getUnreadAmount() != 0) { - final var spacing = new Region(); + var spacing = new Region(); setHgrow(spacing, Priority.ALWAYS); getChildren().add(spacing); - final var unreadMessagesLabel = new Label(Integer.toString(chat.getUnreadAmount())); + var unreadMessagesLabel = new Label( + chat.getUnreadAmount() > 99 ? "99+" : String.valueOf(chat.getUnreadAmount())); unreadMessagesLabel.setMinSize(15, 15); - final var vbox = new VBox(); - vbox.setAlignment(Pos.CENTER_RIGHT); - unreadMessagesLabel.setAlignment(Pos.CENTER); + unreadMessagesLabel.setAlignment(Pos.CENTER_RIGHT); unreadMessagesLabel.getStyleClass().add("unread-messages-amount"); - vbox.getChildren().add(unreadMessagesLabel); - getChildren().add(vbox); + getChildren().add(unreadMessagesLabel); } getStyleClass().add("list-element"); } diff --git a/client/src/main/java/envoy/client/ui/controller/ChatScene.java b/client/src/main/java/envoy/client/ui/controller/ChatScene.java index c7d6034..b118f8c 100644 --- a/client/src/main/java/envoy/client/ui/controller/ChatScene.java +++ b/client/src/main/java/envoy/client/ui/controller/ChatScene.java @@ -107,9 +107,6 @@ public final class ChatScene implements EventListener, Restorable { @FXML private TextArea contactSearch; - @FXML - private VBox contactOperations; - @FXML private TabPane tabPane; @@ -125,9 +122,6 @@ public final class ChatScene implements EventListener, Restorable { @FXML private HBox ownContactControl; - @FXML - private Region spaceBetweenUserAndSettingsButton; - private Chat currentChat; private FilteredList chats; private Attachment pendingAttachment; @@ -175,7 +169,7 @@ public final class ChatScene implements EventListener, Restorable { // Set the icons of buttons and image views settingsButton.setGraphic( - new ImageView(IconUtil.loadIconThemeSensitive("settings", DEFAULT_ICON_SIZE))); + new ImageView(IconUtil.loadIconThemeSensitive("settings", 22))); voiceButton.setGraphic( new ImageView(IconUtil.loadIconThemeSensitive("microphone", DEFAULT_ICON_SIZE))); attachmentButton.setGraphic( @@ -195,7 +189,6 @@ public final class ChatScene implements EventListener, Restorable { chatList.setItems(chats = new FilteredList<>(localDB.getChats())); // Set the design of the box in the upper-left corner - settingsButton.setAlignment(Pos.BOTTOM_RIGHT); generateOwnStatusControl(); Platform.runLater(() -> { @@ -797,15 +790,15 @@ public final class ChatScene implements EventListener, Restorable { private void generateOwnStatusControl() { // Update the own user status if present - if (ownContactControl.getChildren().get(0) instanceof ContactControl) - ((ContactControl) ownContactControl.getChildren().get(0)).replaceInfoLabel(); + if (ownContactControl.getChildren().get(1) instanceof ContactControl) + ((ContactControl) ownContactControl.getChildren().get(1)).replaceInfoLabel(); else { // Else prepend it to the HBox children final var ownUserControl = new ContactControl(localDB.getUser()); ownUserControl.setAlignment(Pos.CENTER_LEFT); HBox.setHgrow(ownUserControl, Priority.NEVER); - ownContactControl.getChildren().add(0, ownUserControl); + ownContactControl.getChildren().add(1, ownUserControl); } } diff --git a/client/src/main/java/envoy/client/ui/settings/GeneralSettingsPane.java b/client/src/main/java/envoy/client/ui/settings/GeneralSettingsPane.java index 3f773fc..16b6f01 100644 --- a/client/src/main/java/envoy/client/ui/settings/GeneralSettingsPane.java +++ b/client/src/main/java/envoy/client/ui/settings/GeneralSettingsPane.java @@ -27,15 +27,15 @@ public final class GeneralSettingsPane extends SettingsPane { final var settingsItems = settings.getItems(); // Add hide on close if supported - if (StatusTrayIcon.isSupported()) { - final var hideOnCloseCheckbox = - new SettingsCheckbox((SettingsItem) settingsItems.get("hideOnClose")); - final var hideOnCloseTooltip = new Tooltip( - "If selected, Envoy will still be present in the task bar when closed."); - hideOnCloseTooltip.setWrapText(true); - hideOnCloseCheckbox.setTooltip(hideOnCloseTooltip); - getChildren().add(hideOnCloseCheckbox); - } + final var hideOnCloseCheckbox = + new SettingsCheckbox((SettingsItem) settingsItems.get("hideOnClose")); + final var hideOnCloseTooltip = new Tooltip(StatusTrayIcon.isSupported() + ? "If selected, Envoy will still be present in the task bar when closed." + : "status tray icon is not supported on your system."); + hideOnCloseTooltip.setWrapText(true); + hideOnCloseCheckbox.setTooltip(hideOnCloseTooltip); + hideOnCloseCheckbox.setDisable(!StatusTrayIcon.isSupported()); + getChildren().add(hideOnCloseCheckbox); final var enterToSendCheckbox = new SettingsCheckbox((SettingsItem) settingsItems.get("enterToSend")); diff --git a/client/src/main/resources/fxml/ChatScene.fxml b/client/src/main/resources/fxml/ChatScene.fxml index d61325c..f293d3e 100644 --- a/client/src/main/resources/fxml/ChatScene.fxml +++ b/client/src/main/resources/fxml/ChatScene.fxml @@ -57,7 +57,7 @@ - - + - - - - - - + +