Display Current User Status and Unread Message Amount in Status Tray Icon #103
@ -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<Message> 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<Message> 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<Message>) 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<Chat>) in.readObject());
|
||||
|
||||
// Some chats have changed and should not be overwritten by the saved values
|
||||
|
@ -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<T> implements Serializable {
|
||||
private T value;
|
||||
private String userFriendlyName, description;
|
||||
|
||||
private transient Consumer<T> changeHandler;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
@ -52,8 +49,6 @@ public final class SettingsItem<T> 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<T> 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<T> changeHandler) {
|
||||
this.changeHandler = changeHandler;
|
||||
changeHandler.accept(value);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
* <ul>
|
||||
* <li>Changing the user status</li>
|
||||
* <li>Logging out</li>
|
||||
* <li>Quitting Envoy</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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<Chat> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Boolean>) 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<Boolean>) 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<Boolean>) settingsItems.get("enterToSend"));
|
||||
|
@ -57,7 +57,7 @@
|
||||
<content>
|
||||
<AnchorPane minHeight="0.0" minWidth="0.0">
|
||||
<children>
|
||||
<VBox fx:id="contactOperations" prefHeight="3000.0"
|
||||
<VBox prefHeight="3000.0"
|
||||
prefWidth="316.0">
|
||||
<children>
|
||||
<VBox id="search-panel" maxHeight="-Infinity"
|
||||
@ -146,7 +146,8 @@
|
||||
<Insets right="1.0" />
|
||||
</GridPane.margin>
|
||||
</TabPane>
|
||||
<HBox id="top-bar" alignment="CENTER_LEFT" prefHeight="100.0">
|
||||
<HBox id="top-bar" alignment="CENTER_LEFT" prefHeight="100.0"
|
||||
fx:id="ownContactControl">
|
||||
<children>
|
||||
<ImageView id="profile-pic" fx:id="clientProfilePic"
|
||||
fitHeight="43.0" fitWidth="43.0" pickOnBounds="true"
|
||||
@ -155,22 +156,18 @@
|
||||
<Insets left="15.0" top="5.0" right="10.0" />
|
||||
</HBox.margin>
|
||||
</ImageView>
|
||||
<HBox id="transparent-background" fx:id="ownContactControl">
|
||||
<children>
|
||||
<Region id="transparent-background" prefWidth="120"
|
||||
fx:id="spaceBetweenUserAndSettingsButton" HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="settingsButton" mnemonicParsing="false"
|
||||
onAction="#settingsButtonClicked" prefHeight="30.0"
|
||||
prefWidth="30.0" text="" alignment="CENTER">
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
<HBox.margin>
|
||||
<Insets bottom="35.0" left="5.0" top="35.0" />
|
||||
</HBox.margin>
|
||||
</Button>
|
||||
</children>
|
||||
</HBox>
|
||||
<Region id="transparent-background"
|
||||
HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="settingsButton" mnemonicParsing="false"
|
||||
onAction="#settingsButtonClicked" prefHeight="30.0"
|
||||
prefWidth="30.0" alignment="CENTER_RIGHT">
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
|
||||
</padding>
|
||||
<HBox.margin>
|
||||
<Insets bottom="35.0" left="5.0" top="35.0" right="10.0"/>
|
||||
</HBox.margin>
|
||||
</Button>
|
||||
</children>
|
||||
<GridPane.margin>
|
||||
<Insets bottom="1.0" right="1.0" />
|
||||
|
Reference in New Issue
Block a user