From 1a6fb9f5b5ccf5eb370c10635eee495b2e98eae2 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Thu, 25 Sep 2014 02:40:57 +0200 Subject: [PATCH] Add System notification (simulated not OS native) --- .../eu/hansolo/enzo/notification/Demo.java | 88 +++ .../enzo/notification/Notification.java | 566 ++++++++++++++++++ .../notification/NotificationBuilder.java | 84 +++ .../enzo/notification/NotificationEvent.java | 43 ++ .../enzo/notification/NotifierBuilder.java | 112 ++++ src/main/java/io/bitsquare/BitSquare.java | 1 + .../java/io/bitsquare/NotificationTest.java | 143 +++++ .../gui/components/SystemNotification.java | 38 ++ .../processbar/ProcessStepBarSkin.java | 4 +- src/main/java/io/bitsquare/gui/images.css | 4 + .../io/bitsquare/gui/main/MainViewCB.java | 8 +- .../java/io/bitsquare/gui/util/Colors.java | 2 +- .../java/io/bitsquare/trade/TradeManager.java | 1 - 13 files changed, 1086 insertions(+), 8 deletions(-) create mode 100644 src/main/java/eu/hansolo/enzo/notification/Demo.java create mode 100644 src/main/java/eu/hansolo/enzo/notification/Notification.java create mode 100644 src/main/java/eu/hansolo/enzo/notification/NotificationBuilder.java create mode 100644 src/main/java/eu/hansolo/enzo/notification/NotificationEvent.java create mode 100644 src/main/java/eu/hansolo/enzo/notification/NotifierBuilder.java create mode 100644 src/main/java/io/bitsquare/NotificationTest.java create mode 100644 src/main/java/io/bitsquare/gui/components/SystemNotification.java diff --git a/src/main/java/eu/hansolo/enzo/notification/Demo.java b/src/main/java/eu/hansolo/enzo/notification/Demo.java new file mode 100644 index 0000000000..5de95ace61 --- /dev/null +++ b/src/main/java/eu/hansolo/enzo/notification/Demo.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2013 by Gerrit Grunwald + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.hansolo.enzo.notification; + +/** + * Created by + * User: hansolo + * Date: 01.07.13 + * Time: 07:10 + */ + +import java.util.Random; + +import javafx.application.Application; +import javafx.geometry.Insets; +import javafx.scene.*; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.stage.Stage; + + +public class Demo extends Application { + private static final Random RND = new Random(); + private static final Notification[] NOTIFICATIONS = { + NotificationBuilder.create().title("Info").message("New Information").image(Notification.INFO_ICON).build(), + NotificationBuilder.create().title("Warning").message("Attention, somethings wrong").image(Notification + .WARNING_ICON).build(), + NotificationBuilder.create().title("Success").message("Great it works").image(Notification.SUCCESS_ICON) + .build(), + NotificationBuilder.create().title("Error").message("ZOMG").image(Notification.ERROR_ICON).build() + }; + private Notification.Notifier notifier; + private Button button; + + + // ******************** Initialization ************************************ + @Override + public void init() { + button = new Button("Notify"); + button.setOnAction(event -> { + notifier.notify(NOTIFICATIONS[RND.nextInt(4)]); + }); + } + + + // ******************** Application start ********************************* + @Override + public void start(Stage stage) { + notifier = NotifierBuilder.create() + //.popupLocation(Pos.BOTTOM_RIGHT) + .build(); + notifier.setOnNotificationPressed(event -> System.out.println("Notification pressed: " + event.NOTIFICATION + .TITLE)); + notifier.setOnShowNotification(event -> System.out.println("Notification shown: " + event.NOTIFICATION.TITLE)); + notifier.setOnHideNotification(event -> System.out.println("Notification hidden: " + event.NOTIFICATION.TITLE)); + + StackPane pane = new StackPane(); + pane.setPadding(new Insets(10, 10, 10, 10)); + pane.getChildren().addAll(button); + + Scene scene = new Scene(pane); + stage.setOnCloseRequest(observable -> notifier.stop()); + stage.setScene(scene); + stage.show(); + } + + @Override + public void stop() { + } + + public static void main(String[] args) { + launch(args); + } +} diff --git a/src/main/java/eu/hansolo/enzo/notification/Notification.java b/src/main/java/eu/hansolo/enzo/notification/Notification.java new file mode 100644 index 0000000000..4f00edf772 --- /dev/null +++ b/src/main/java/eu/hansolo/enzo/notification/Notification.java @@ -0,0 +1,566 @@ +/* + * Copyright (c) 2013 by Gerrit Grunwald + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.hansolo.enzo.notification; + +import java.util.stream.IntStream; + +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ObjectPropertyBase; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.event.EventType; +import javafx.event.WeakEventHandler; +import javafx.geometry.Pos; +import javafx.scene.*; +import javafx.scene.control.*; +import javafx.scene.image.*; +import javafx.scene.input.*; +import javafx.scene.layout.*; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.Duration; + +import org.controlsfx.control.PopOver; + + +/** + * Created by + * User: hansolo + * Date: 01.07.13 + * Time: 07:10 + */ +public class Notification { + public static final Image INFO_ICON = new Image(Notifier.class.getResourceAsStream("info.png")); + public static final Image WARNING_ICON = new Image(Notifier.class.getResourceAsStream("warning.png")); + public static final Image SUCCESS_ICON = new Image(Notifier.class.getResourceAsStream("success.png")); + public static final Image ERROR_ICON = new Image(Notifier.class.getResourceAsStream("error.png")); + public final String TITLE; + public final String MESSAGE; + public final Image IMAGE; + + + // ******************** Constructors ************************************** + public Notification(final String TITLE, final String MESSAGE) { + this(TITLE, MESSAGE, null); + } + + public Notification(final String MESSAGE, final Image IMAGE) { + this("", MESSAGE, IMAGE); + } + + public Notification(final String TITLE, final String MESSAGE, final Image IMAGE) { + this.TITLE = TITLE; + this.MESSAGE = MESSAGE; + this.IMAGE = IMAGE; + } + + + // ******************** Inner Classes ************************************* + public enum Notifier { + INSTANCE; + + private static final double ICON_WIDTH = 24; + private static final double ICON_HEIGHT = 24; + private static double width = 321; + private static double height = 49; + private static double offsetX = 0; + private static double offsetY = 2; + private static double spacingY = 5; + private static Pos popupLocation = Pos.TOP_RIGHT; + private static Stage stageRef = null; + private Duration popupLifetime; + private Duration popupAnimationTime; + private Stage stage; + private Scene scene; + private ObservableList popups; + + + // ******************** Constructor *************************************** + private Notifier() { + init(); + initGraphics(); + } + + + // ******************** Initialization ************************************ + private void init() { + popupLifetime = Duration.millis(5000); + popupAnimationTime = Duration.millis(500); + popups = FXCollections.observableArrayList(); + } + + private void initGraphics() { + scene = new Scene(new Region()); + scene.setFill(null); + // scene.getStylesheets().add(getClass().getResource("notifier.css").toExternalForm()); + scene.getStylesheets().setAll(getClass().getResource("/io/bitsquare/gui/bitsquare.css").toExternalForm(), + getClass().getResource("/io/bitsquare/gui/images.css").toExternalForm()); + + stage = new Stage(); + stage.initStyle(StageStyle.TRANSPARENT); + // stage.setAlwaysOnTop(true); + } + + + // ******************** Methods ******************************************* + + /** + * @param STAGE_REF The Notification will be positioned relative to the given Stage.
+ * If null then the Notification will be positioned relative to the primary Screen. + * @param POPUP_LOCATION The default is TOP_RIGHT of primary Screen. + */ + public static void setPopupLocation(final Stage STAGE_REF, final Pos POPUP_LOCATION) { + if (null != STAGE_REF) { + INSTANCE.stage.initOwner(STAGE_REF); + Notifier.stageRef = STAGE_REF; + } + Notifier.popupLocation = POPUP_LOCATION; + } + + /** + * Sets the Notification's owner stage so that when the owner + * stage is closed Notifications will be shut down as well.
+ * This is only needed if setPopupLocation is called + * without a stage reference. + * + * @param OWNER + */ + public static void setNotificationOwner(final Stage OWNER) { + INSTANCE.stage.initOwner(OWNER); + } + + /** + * @param OFFSET_X The horizontal shift required. + *
The default is 0 px. + */ + public static void setOffsetX(final double OFFSET_X) { + Notifier.offsetX = OFFSET_X; + } + + /** + * @param OFFSET_Y The vertical shift required. + *
The default is 25 px. + */ + public static void setOffsetY(final double OFFSET_Y) { + Notifier.offsetY = OFFSET_Y; + } + + /** + * @param WIDTH The default is 300 px. + */ + public static void setWidth(final double WIDTH) { + Notifier.width = WIDTH; + } + + /** + * @param HEIGHT The default is 80 px. + */ + public static void setHeight(final double HEIGHT) { + Notifier.height = HEIGHT; + } + + /** + * @param SPACING_Y The spacing between multiple Notifications. + *
The default is 5 px. + */ + public static void setSpacingY(final double SPACING_Y) { + Notifier.spacingY = SPACING_Y; + } + + public void stop() { + popups.clear(); + stage.close(); + } + + /** + * Returns the Duration that the notification will stay on screen before it + * will fade out. The default is 5000 ms + * + * @return the Duration the popup notification will stay on screen + */ + public Duration getPopupLifetime() { + return popupLifetime; + } + + /** + * Defines the Duration that the popup notification will stay on screen before it + * will fade out. The parameter is limited to values between 2 and 20 seconds. + * + * @param POPUP_LIFETIME + */ + public void setPopupLifetime(final Duration POPUP_LIFETIME) { + popupLifetime = Duration.millis(clamp(2000, 20000, POPUP_LIFETIME.toMillis())); + } + + /** + * Returns the Duration that it takes to fade out the notification + * The parameter is limited to values between 0 and 1000 ms + * + * @return the Duration that it takes to fade out the notification + */ + public Duration getPopupAnimationTime() { + return popupAnimationTime; + } + + /** + * Defines the Duration that it takes to fade out the notification + * The parameter is limited to values between 0 and 1000 ms + * Default value is 500 ms + * + * @param POPUP_ANIMATION_TIME + */ + public void setPopupAnimationTime(final Duration POPUP_ANIMATION_TIME) { + popupAnimationTime = Duration.millis(clamp(0, 1000, POPUP_ANIMATION_TIME.toMillis())); + } + + /** + * Show the given Notification on the screen + * + * @param NOTIFICATION + */ + public void notify(final Notification NOTIFICATION) { + preOrder(); + showPopup(NOTIFICATION); + } + + /** + * Show a Notification with the given parameters on the screen + * + * @param TITLE + * @param MESSAGE + * @param IMAGE + */ + public void notify(final String TITLE, final String MESSAGE, final Image IMAGE) { + notify(new Notification(TITLE, MESSAGE, IMAGE)); + } + + /** + * Show a Notification with the given title and message and an Info icon + * + * @param TITLE + * @param MESSAGE + */ + public void notifyInfo(final String TITLE, final String MESSAGE) { + notify(new Notification(TITLE, MESSAGE, Notification.INFO_ICON)); + } + + /** + * Show a Notification with the given title and message and a Warning icon + * + * @param TITLE + * @param MESSAGE + */ + public void notifyWarning(final String TITLE, final String MESSAGE) { + notify(new Notification(TITLE, MESSAGE, Notification.WARNING_ICON)); + } + + /** + * Show a Notification with the given title and message and a Checkmark icon + * + * @param TITLE + * @param MESSAGE + */ + public void notifySuccess(final String TITLE, final String MESSAGE) { + notify(new Notification(TITLE, MESSAGE, Notification.SUCCESS_ICON)); + } + + /** + * Show a Notification with the given title and message and an Error icon + * + * @param TITLE + * @param MESSAGE + */ + public void notifyError(final String TITLE, final String MESSAGE) { + notify(new Notification(TITLE, MESSAGE, Notification.ERROR_ICON)); + } + + /** + * Makes sure that the given VALUE is within the range of MIN to MAX + * + * @param MIN + * @param MAX + * @param VALUE + * @return + */ + private double clamp(final double MIN, final double MAX, final double VALUE) { + if (VALUE < MIN) return MIN; + if (VALUE > MAX) return MAX; + return VALUE; + } + + /** + * Reorder the popup Notifications on screen so that the latest Notification will stay on top + */ + private void preOrder() { + if (popups.isEmpty()) return; + IntStream.range(0, popups.size()).parallel().forEachOrdered( + i -> { + Platform.runLater(() -> preOrderTask(i)); + /* switch (popupLocation) { + case TOP_LEFT: case TOP_CENTER: case TOP_RIGHT: + popups.get(i).setY(popups.get(i).getY() + height + spacingY); + break; + + case BOTTOM_LEFT: case BOTTOM_CENTER: case BOTTOM_RIGHT: + popups.get(i).setY(popups.get(i).getY() - height - spacingY); + break; + + default: + popups.get(i).setY(popups.get(i).getY() - height - spacingY); + break; + } */ + } + ); + } + + private void preOrderTask(int i) { + switch (popupLocation) { + case TOP_LEFT: + case TOP_CENTER: + case TOP_RIGHT: + popups.get(i).setY(popups.get(i).getY() + height + spacingY); + break; + + case BOTTOM_LEFT: + case BOTTOM_CENTER: + case BOTTOM_RIGHT: + popups.get(i).setY(popups.get(i).getY() - height - spacingY); + break; + + default: + popups.get(i).setY(popups.get(i).getY() - height - spacingY); + break; + } + } + + /** + * Creates and shows a popup with the data from the given Notification object + * + * @param NOTIFICATION + */ + private void showPopup(final Notification NOTIFICATION) { + ImageView icon = new ImageView(new Image(Notifier.class.getResourceAsStream("/images/notification_logo" + + ".png"))); + //icon.setId("notification-logo"); + icon.relocate(10, 7); + + Label title = new Label(NOTIFICATION.TITLE); + title.setStyle(" -fx-text-fill:#333333; -fx-font-size:12; -fx-font-weight:bold;"); + title.relocate(60, 6); + + Label message = new Label(NOTIFICATION.MESSAGE); + message.relocate(60, 25); + message.setStyle(" -fx-text-fill:#333333; -fx-font-size:11; "); + + Pane popupLayout = new Pane(); + popupLayout.setPrefSize(width, height); + popupLayout.getChildren().addAll(icon, title, message); + + PopOver popOver = new PopOver(popupLayout); + popOver.setDetachable(false); + popOver.setArrowSize(0); + popOver.setX(getX()); + popOver.setY(getY()); + popOver.addEventHandler(MouseEvent.MOUSE_PRESSED, new WeakEventHandler<>(event -> + fireNotificationEvent(new NotificationEvent(NOTIFICATION, Notifier.this, popOver, + NotificationEvent.NOTIFICATION_PRESSED)) + )); + popups.add(popOver); + + // Add a timeline for popup fade out + KeyValue fadeOutBegin = new KeyValue(popOver.opacityProperty(), 1.0); + KeyValue fadeOutEnd = new KeyValue(popOver.opacityProperty(), 0.0); + + KeyFrame kfBegin = new KeyFrame(Duration.ZERO, fadeOutBegin); + KeyFrame kfEnd = new KeyFrame(popupAnimationTime, fadeOutEnd); + + Timeline timeline = new Timeline(kfBegin, kfEnd); + timeline.setDelay(popupLifetime); + timeline.setOnFinished(actionEvent -> Platform.runLater(() -> { + popOver.hide(); + popups.remove(popOver); + fireNotificationEvent(new NotificationEvent(NOTIFICATION, Notifier.this, popOver, + NotificationEvent.HIDE_NOTIFICATION)); + })); + + if (stage.isShowing()) { + stage.toFront(); + } + else { + stage.show(); + } + + popOver.show(stage); + fireNotificationEvent(new NotificationEvent(NOTIFICATION, Notifier.this, popOver, + NotificationEvent.SHOW_NOTIFICATION)); + timeline.play(); + } + + private double getX() { + if (null == stageRef) return calcX(0.0, Screen.getPrimary().getBounds().getWidth()); + + return calcX(stageRef.getX(), stageRef.getWidth()); + } + + private double getY() { + if (null == stageRef) return calcY(0.0, Screen.getPrimary().getBounds().getHeight()); + return calcY(stageRef.getY(), stageRef.getHeight()); + } + + private double calcX(final double LEFT, final double TOTAL_WIDTH) { + switch (popupLocation) { + case TOP_LEFT: + case CENTER_LEFT: + case BOTTOM_LEFT: + return LEFT + offsetX; + case TOP_CENTER: + case CENTER: + case BOTTOM_CENTER: + return LEFT + (TOTAL_WIDTH - width) * 0.5 - offsetX; + case TOP_RIGHT: + case CENTER_RIGHT: + case BOTTOM_RIGHT: + return LEFT + TOTAL_WIDTH - width - offsetX; + default: + return 0.0; + } + } + + private double calcY(final double TOP, final double TOTAL_HEIGHT) { + switch (popupLocation) { + case TOP_LEFT: + case TOP_CENTER: + case TOP_RIGHT: + return TOP + offsetY; + case CENTER_LEFT: + case CENTER: + case CENTER_RIGHT: + return TOP + (TOTAL_HEIGHT - height) / 2 - offsetY; + case BOTTOM_LEFT: + case BOTTOM_CENTER: + case BOTTOM_RIGHT: + return TOP + TOTAL_HEIGHT - height - offsetY; + default: + return 0.0; + } + } + + + // ******************** Event handling ******************************** + public final ObjectProperty> onNotificationPressedProperty() { + return onNotificationPressed; + } + + public final void setOnNotificationPressed(EventHandler value) { + onNotificationPressedProperty().set(value); + } + + public final EventHandler getOnNotificationPressed() { + return onNotificationPressedProperty().get(); + } + + private ObjectProperty> onNotificationPressed = new + ObjectPropertyBase>() { + @Override + public Object getBean() { + return this; + } + + @Override + public String getName() { + return "onNotificationPressed"; + } + }; + + public final ObjectProperty> onShowNotificationProperty() { + return onShowNotification; + } + + public final void setOnShowNotification(EventHandler value) { + onShowNotificationProperty().set(value); + } + + public final EventHandler getOnShowNotification() { + return onShowNotificationProperty().get(); + } + + private ObjectProperty> onShowNotification = new + ObjectPropertyBase>() { + @Override + public Object getBean() { + return this; + } + + @Override + public String getName() { + return "onShowNotification"; + } + }; + + public final ObjectProperty> onHideNotificationProperty() { + return onHideNotification; + } + + public final void setOnHideNotification(EventHandler value) { + onHideNotificationProperty().set(value); + } + + public final EventHandler getOnHideNotification() { + return onHideNotificationProperty().get(); + } + + private ObjectProperty> onHideNotification = new + ObjectPropertyBase>() { + @Override + public Object getBean() { + return this; + } + + @Override + public String getName() { + return "onHideNotification"; + } + }; + + + public void fireNotificationEvent(final NotificationEvent EVENT) { + final EventType TYPE = EVENT.getEventType(); + final EventHandler HANDLER; + if (NotificationEvent.NOTIFICATION_PRESSED == TYPE) { + HANDLER = getOnNotificationPressed(); + } + else if (NotificationEvent.SHOW_NOTIFICATION == TYPE) { + HANDLER = getOnShowNotification(); + } + else if (NotificationEvent.HIDE_NOTIFICATION == TYPE) { + HANDLER = getOnHideNotification(); + } + else { + HANDLER = null; + } + if (null == HANDLER) return; + HANDLER.handle(EVENT); + } + + } +} diff --git a/src/main/java/eu/hansolo/enzo/notification/NotificationBuilder.java b/src/main/java/eu/hansolo/enzo/notification/NotificationBuilder.java new file mode 100644 index 0000000000..2fb81c52a1 --- /dev/null +++ b/src/main/java/eu/hansolo/enzo/notification/NotificationBuilder.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2014 by Gerrit Grunwald + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.hansolo.enzo.notification; + +import java.util.HashMap; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; +import javafx.scene.image.*; + + +/** + * User: hansolo + * Date: 29.04.14 + * Time: 08:53 + */ +public class NotificationBuilder> { + private HashMap properties = new HashMap<>(); + + + // ******************** Constructors ************************************** + protected NotificationBuilder() { + } + + + // ******************** Methods ******************************************* + public final static NotificationBuilder create() { + return new NotificationBuilder(); + } + + public final B title(final String TITLE) { + properties.put("title", new SimpleStringProperty(TITLE)); + return (B) this; + } + + public final B message(final String MESSAGE) { + properties.put("message", new SimpleStringProperty(MESSAGE)); + return (B) this; + } + + public final B image(final Image IMAGE) { + properties.put("image", new SimpleObjectProperty<>(IMAGE)); + return (B) this; + } + + public final Notification build() { + final Notification NOTIFICATION; + if (properties.keySet().contains("title") && properties.keySet().contains("message") && properties.keySet() + .contains("image")) { + NOTIFICATION = new Notification(((StringProperty) properties.get("title")).get(), + ((StringProperty) properties.get("message")).get(), + ((ObjectProperty) properties.get("image")).get()); + } + else if (properties.keySet().contains("title") && properties.keySet().contains("message")) { + NOTIFICATION = new Notification(((StringProperty) properties.get("title")).get(), + ((StringProperty) properties.get("message")).get()); + } + else if (properties.keySet().contains("message") && properties.keySet().contains("image")) { + NOTIFICATION = new Notification(((StringProperty) properties.get("message")).get(), + ((ObjectProperty) properties.get("image")).get()); + } + else { + throw new IllegalArgumentException("Wrong or missing parameters."); + } + return NOTIFICATION; + } +} diff --git a/src/main/java/eu/hansolo/enzo/notification/NotificationEvent.java b/src/main/java/eu/hansolo/enzo/notification/NotificationEvent.java new file mode 100644 index 0000000000..7e4a52938c --- /dev/null +++ b/src/main/java/eu/hansolo/enzo/notification/NotificationEvent.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2014 by Gerrit Grunwald + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.hansolo.enzo.notification; + +import javafx.event.Event; +import javafx.event.EventTarget; +import javafx.event.EventType; + + +/** + * User: hansolo + * Date: 29.04.14 + * Time: 10:03 + */ +public class NotificationEvent extends Event { + public static final EventType NOTIFICATION_PRESSED = new EventType(ANY, "NOTIFICATION_PRESSED"); + public static final EventType SHOW_NOTIFICATION = new EventType(ANY, "SHOW_NOTIFICATION"); + public static final EventType HIDE_NOTIFICATION = new EventType(ANY, "HIDE_NOTIFICATION"); + + public final Notification NOTIFICATION; + + + // ******************** Constructors ************************************** + public NotificationEvent(final Notification NOTIFICATION, final Object SOURCE, final EventTarget TARGET, + EventType TYPE) { + super(SOURCE, TARGET, TYPE); + this.NOTIFICATION = NOTIFICATION; + } +} diff --git a/src/main/java/eu/hansolo/enzo/notification/NotifierBuilder.java b/src/main/java/eu/hansolo/enzo/notification/NotifierBuilder.java new file mode 100644 index 0000000000..b4fb514ee2 --- /dev/null +++ b/src/main/java/eu/hansolo/enzo/notification/NotifierBuilder.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2014 by Gerrit Grunwald + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.hansolo.enzo.notification; + +import java.util.HashMap; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.geometry.Pos; +import javafx.stage.Stage; +import javafx.util.Duration; + + +/** + * User: hansolo + * Date: 29.04.14 + * Time: 08:32 + */ +public class NotifierBuilder> { + private HashMap properties = new HashMap<>(); + + + // ******************** Constructors ************************************** + protected NotifierBuilder() { + } + + + // ******************** Methods ******************************************* + public final static NotifierBuilder create() { + return new NotifierBuilder(); + } + + public final B owner(final Stage OWNER) { + properties.put("stage", new SimpleObjectProperty<>(OWNER)); + return (B) this; + } + + public final B popupLocation(Pos LOCATION) { + properties.put("popupLocation", new SimpleObjectProperty<>(LOCATION)); + return (B) this; + } + + public final B width(final double WIDTH) { + properties.put("width", new SimpleDoubleProperty(WIDTH)); + return (B) this; + } + + public final B height(final double HEIGHT) { + properties.put("height", new SimpleDoubleProperty(HEIGHT)); + return (B) this; + } + + public final B spacingY(final double SPACING_Y) { + properties.put("spacingY", new SimpleDoubleProperty(SPACING_Y)); + return (B) this; + } + + public final B popupLifeTime(final Duration POPUP_LIFETIME) { + properties.put("popupLifeTime", new SimpleObjectProperty<>(POPUP_LIFETIME)); + return (B) this; + } + + public final B popupAnimationTime(final Duration POPUP_ANIMATION_TIME) { + properties.put("popupAnimationTime", new SimpleObjectProperty<>(POPUP_ANIMATION_TIME)); + return (B) this; + } + + public final Notification.Notifier build() { + final Notification.Notifier NOTIFIER = Notification.Notifier.INSTANCE; + for (String key : properties.keySet()) { + if ("owner".equals(key)) { + NOTIFIER.setNotificationOwner(((ObjectProperty) properties.get(key)).get()); + } + else if ("popupLocation".equals(key)) { + NOTIFIER.setPopupLocation(null, ((ObjectProperty) properties.get(key)).get()); + } + else if ("width".equals(key)) { + NOTIFIER.setWidth(((DoubleProperty) properties.get(key)).get()); + } + else if ("height".equals(key)) { + NOTIFIER.setHeight(((DoubleProperty) properties.get(key)).get()); + } + else if ("spacingY".equals(key)) { + NOTIFIER.setSpacingY(((DoubleProperty) properties.get(key)).get()); + } + else if ("popupLifeTime".equals(key)) { + NOTIFIER.setPopupLifetime(((ObjectProperty) properties.get(key)).get()); + } + else if ("popupAnimationTime".equals(key)) { + NOTIFIER.setPopupAnimationTime(((ObjectProperty) properties.get(key)).get()); + } + } + return NOTIFIER; + } +} diff --git a/src/main/java/io/bitsquare/BitSquare.java b/src/main/java/io/bitsquare/BitSquare.java index 140a3ec9bd..0422edfb5c 100644 --- a/src/main/java/io/bitsquare/BitSquare.java +++ b/src/main/java/io/bitsquare/BitSquare.java @@ -130,6 +130,7 @@ public class BitSquare extends Application { Profiler.initScene(primaryStage.getScene()); + // primaryStage.setOnCloseRequest(observable -> stop()); primaryStage.show(); } catch (IOException e) { diff --git a/src/main/java/io/bitsquare/NotificationTest.java b/src/main/java/io/bitsquare/NotificationTest.java new file mode 100644 index 0000000000..a18374c993 --- /dev/null +++ b/src/main/java/io/bitsquare/NotificationTest.java @@ -0,0 +1,143 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package io.bitsquare; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.*; +import javafx.scene.control.*; +import javafx.scene.image.*; +import javafx.scene.layout.*; +import javafx.scene.paint.*; +import javafx.stage.Screen; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javafx.util.Duration; + +import org.controlsfx.control.PopOver; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class NotificationTest extends Application { + private static final Logger log = LoggerFactory.getLogger(NotificationTest.class); + private Scene notificationScene; + private Stage notificationStage; + + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage primaryStage) { + Pane view = new StackPane(); + Button b = new Button("open"); + b.setOnAction(e -> addItem()); + view.getChildren().addAll(b); + Scene scene = new Scene(view, 1000, 750); + scene.getStylesheets().setAll(getClass().getResource("/io/bitsquare/gui/bitsquare.css").toExternalForm(), + getClass().getResource("/io/bitsquare/gui/images.css").toExternalForm()); + + primaryStage.setScene(scene); + primaryStage.setMinWidth(750); + primaryStage.setMinHeight(500); + primaryStage.show(); + initNotification(); + } + + private List popOvers = new ArrayList<>(); + + private HBox getNotificationItem(String headline, String info) { + Label headlineLabel = new Label(headline); + Label infoLabel = new Label(info); + ImageView icon = new ImageView(); + icon.setId("image-info"); + + VBox vBox = new VBox(); + vBox.setPadding(new Insets(10, 10, 10, 10)); + vBox.setSpacing(10); + vBox.getChildren().addAll(headlineLabel, infoLabel); + HBox hBox = new HBox(); + hBox.setPadding(new Insets(10, 10, 10, 10)); + hBox.setSpacing(10); + hBox.getChildren().addAll(icon, vBox); + return hBox; + } + + private void addItem() { + HBox hBox = getNotificationItem("Headline " + new Random().nextInt(), "test " + new Random().nextInt()); + PopOver popOver = new PopOver(hBox); + popOver.setDetachable(false); + popOver.setArrowSize(0); + popOver.setPrefSize(200, 100); + popOver.show(notificationScene.getWindow(), Screen.getPrimary().getBounds().getWidth() - 200, 0); + popOvers.add(popOver); + + // Add a timeline for popup fade out + KeyValue fadeOutBegin = new KeyValue(popOver.opacityProperty(), 1.0); + KeyValue fadeOutEnd = new KeyValue(popOver.opacityProperty(), 0.0); + + KeyFrame kfBegin = new KeyFrame(Duration.ZERO, fadeOutBegin); + KeyFrame kfEnd = new KeyFrame(Duration.millis(5000), fadeOutEnd); + + Timeline timeline = new Timeline(kfBegin, kfEnd); + timeline.setDelay(Duration.millis(500)); + timeline.setOnFinished(actionEvent -> Platform.runLater(() -> { + popOvers.remove(popOver); + })); + + if (notificationStage.isShowing()) { + notificationStage.toFront(); + } + else { + notificationStage.show(); + } + + popOver.show(notificationStage); + timeline.play(); + } + + private void initNotification() { + Region region = new Region(); + region.setMouseTransparent(true); + region.setStyle("-fx-background-color:transparent;"); + region.setPrefSize(1, 1); + + notificationScene = new Scene(region); + notificationScene.setFill(Color.TRANSPARENT); + notificationScene.getStylesheets().setAll(getClass().getResource("/io/bitsquare/gui/bitsquare.css") + .toExternalForm(), + getClass().getResource("/io/bitsquare/gui/images.css").toExternalForm()); + + notificationStage = new Stage(); + notificationStage.initStyle(StageStyle.TRANSPARENT); + notificationStage.setScene(notificationScene); + } + + @Override + public void stop() throws Exception { + } +} diff --git a/src/main/java/io/bitsquare/gui/components/SystemNotification.java b/src/main/java/io/bitsquare/gui/components/SystemNotification.java new file mode 100644 index 0000000000..16f72abed0 --- /dev/null +++ b/src/main/java/io/bitsquare/gui/components/SystemNotification.java @@ -0,0 +1,38 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bitsquare is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bitsquare. If not, see . + */ + +package io.bitsquare.gui.components; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import eu.hansolo.enzo.notification.Notification; +import eu.hansolo.enzo.notification.NotificationBuilder; +import eu.hansolo.enzo.notification.NotifierBuilder; + +/** + * Not sure if we stick with the eu.hansolo.enzo.notification.Notification implementation, so keep it behind a facade + */ +public class SystemNotification { + private static final Logger log = LoggerFactory.getLogger(SystemNotification.class); + private static final Notification.Notifier notifier = NotifierBuilder.create().build(); + + public static void openInfoNotification(String headline, String message) { + notifier.notify(NotificationBuilder.create().title(headline).message(message).build()); + } + +} diff --git a/src/main/java/io/bitsquare/gui/components/processbar/ProcessStepBarSkin.java b/src/main/java/io/bitsquare/gui/components/processbar/ProcessStepBarSkin.java index 7e076b0159..5fb6b878e7 100644 --- a/src/main/java/io/bitsquare/gui/components/processbar/ProcessStepBarSkin.java +++ b/src/main/java/io/bitsquare/gui/components/processbar/ProcessStepBarSkin.java @@ -176,10 +176,10 @@ class ProcessStepBarSkin extends BehaviorSkinBase, Behavior public void past() { log.debug("deSelect " + processStepItem.getLabel()); - BorderStroke borderStroke = new BorderStroke(Colors.DARK_GREY, BorderStrokeStyle.SOLID, null, + BorderStroke borderStroke = new BorderStroke(Colors.GREEN, BorderStrokeStyle.SOLID, null, new BorderWidths(borderWidth, borderWidth, borderWidth, borderWidth), Insets.EMPTY); this.setBorder(new Border(borderStroke)); - setTextFill(Colors.DARK_GREY); + setTextFill(Colors.GREEN); } public double getArrowWidth() { diff --git a/src/main/java/io/bitsquare/gui/images.css b/src/main/java/io/bitsquare/gui/images.css index 276d283897..e7480e1dbf 100644 --- a/src/main/java/io/bitsquare/gui/images.css +++ b/src/main/java/io/bitsquare/gui/images.css @@ -3,6 +3,10 @@ -fx-image: url("../../../images/logo_splash.png"); } +/* notification */ +#notification-logo { + -fx-image: url("../../../images/notification_logo.png"); +} /* shared*/ #image-info { diff --git a/src/main/java/io/bitsquare/gui/main/MainViewCB.java b/src/main/java/io/bitsquare/gui/main/MainViewCB.java index 35f8d8b491..bf27e546f2 100644 --- a/src/main/java/io/bitsquare/gui/main/MainViewCB.java +++ b/src/main/java/io/bitsquare/gui/main/MainViewCB.java @@ -17,13 +17,14 @@ package io.bitsquare.gui.main; +import io.bitsquare.BitSquare; import io.bitsquare.bank.BankAccount; -import io.bitsquare.gui.AWTSystemTray; import io.bitsquare.gui.Navigation; import io.bitsquare.gui.OverlayManager; import io.bitsquare.gui.ViewCB; import io.bitsquare.gui.components.NetworkSyncPane; import io.bitsquare.gui.components.Popups; +import io.bitsquare.gui.components.SystemNotification; import io.bitsquare.gui.util.Profiler; import io.bitsquare.gui.util.Transitions; import io.bitsquare.util.ViewLoader; @@ -200,17 +201,16 @@ public class MainViewCB extends ViewCB { alert.setEffect(new DropShadow(4, 1, 2, Color.GREY)); alert.getChildren().addAll(icon, numPendingTradesLabel); ordersButtonButtonPane.getChildren().add(alert); - - AWTSystemTray.setAlertIcon(); } else { numPendingTradesLabel.setText(String.valueOf(numPendingTrades)); } + + SystemNotification.openInfoNotification(BitSquare.getAppName(), "You got a new trade message."); } else { if (ordersButtonButtonPane.getChildren().size() > 1) ordersButtonButtonPane.getChildren().remove(1); - AWTSystemTray.setIcon(); } } diff --git a/src/main/java/io/bitsquare/gui/util/Colors.java b/src/main/java/io/bitsquare/gui/util/Colors.java index 9d36956364..1b1c9fbd74 100644 --- a/src/main/java/io/bitsquare/gui/util/Colors.java +++ b/src/main/java/io/bitsquare/gui/util/Colors.java @@ -24,6 +24,6 @@ public class Colors { public static final Paint LIGHT_GREY = Color.valueOf("#CCCCCC"); public static final Paint MID_GREY = Color.valueOf("#666666"); public static final Paint DARK_GREY = Color.valueOf("#333333"); - public static final Paint GREEN = Color.valueOf("#009900"); + public static final Paint GREEN = Color.valueOf("#008800"); } diff --git a/src/main/java/io/bitsquare/trade/TradeManager.java b/src/main/java/io/bitsquare/trade/TradeManager.java index 3f50f2f0d9..7583a80b47 100644 --- a/src/main/java/io/bitsquare/trade/TradeManager.java +++ b/src/main/java/io/bitsquare/trade/TradeManager.java @@ -272,7 +272,6 @@ public class TradeManager { persistTrades(); } - @Override public void onPayoutTxPublished(Transaction payoutTx) { trade.setPayoutTx(payoutTx);