Add Notifications

This commit is contained in:
Manfred Karrer 2016-02-15 01:48:49 +01:00
parent 6bf2adae7f
commit 2049e384dc
13 changed files with 301 additions and 698 deletions

View File

@ -136,12 +136,6 @@
<version>8.0.0</version>
</dependency>
<dependency>
<groupId>eu.hansolo.enzo</groupId>
<artifactId>Enzo</artifactId>
<version>0.1.5</version>
</dependency>
<!-- <dependency>
<groupId>org.fxmisc.richtext</groupId>
<artifactId>richtextfx</artifactId>

View File

@ -1,544 +0,0 @@
/*
* 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 io.bitsquare.common.UserThread;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
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.Scene;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.util.Duration;
import org.controlsfx.control.PopOver;
import java.util.stream.IntStream;
/**
* A copy of the original {@link eu.hansolo.enzo.notification.Notification} class at revision eb1d321, containing
* several changes that were otherwise not possible through subclassing or other customization via the existing
* Notification API. See git history for this file for exact details as to what has been changed. All other
* {@code eu.hansolo.enzo.*} types are loaded from the enzo jar (see build.gradle for details).
*/
public class Notification {
private static final Image INFO_ICON = new Image(Notifier.class.getResourceAsStream("info.png"));
private static final Image WARNING_ICON = new Image(Notifier.class.getResourceAsStream("warning.png"));
private static final Image SUCCESS_ICON = new Image(Notifier.class.getResourceAsStream("success.png"));
private static final Image ERROR_ICON = new Image(Notifier.class.getResourceAsStream("error.png"));
private final String TITLE;
private final String MESSAGE;
private 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);
}
private 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<PopOver> popups;
// ******************** Constructor ***************************************
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().setAll(
getClass().getResource("/io/bitsquare/gui/bitsquare.css").toExternalForm(),
getClass().getResource("/io/bitsquare/gui/images.css").toExternalForm());
stage = new Stage();
stage.initStyle(StageStyle.TRANSPARENT);
}
// ******************** Methods *******************************************
/**
* @param STAGE_REF The Notification will be positioned relative to the given Stage.<br>
* 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.<br>
* This is only needed if <code>setPopupLocation</code> is called
* <u>without</u> a stage reference.
*
* @param OWNER
*/
public static void setNotificationOwner(final Stage OWNER) {
INSTANCE.stage.initOwner(OWNER);
}
/**
* @param OFFSET_X The horizontal shift required.
* <br> The default is 0 px.
*/
public static void setOffsetX(final double OFFSET_X) {
Notifier.offsetX = OFFSET_X;
}
/**
* @param OFFSET_Y The vertical shift required.
* <br> 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.
* <br> 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 -> UserThread.execute(() -> {
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.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 -> UserThread.execute(() -> {
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<EventHandler<NotificationEvent>> onNotificationPressedProperty() {
return onNotificationPressed;
}
public final void setOnNotificationPressed(EventHandler<NotificationEvent> value) {
onNotificationPressedProperty().set(value);
}
public final EventHandler<NotificationEvent> getOnNotificationPressed() {
return onNotificationPressedProperty().get();
}
private final ObjectProperty<EventHandler<NotificationEvent>> onNotificationPressed = new
ObjectPropertyBase<EventHandler<NotificationEvent>>() {
@Override
public Object getBean() {
return this;
}
@Override
public String getName() {
return "onNotificationPressed";
}
};
public final ObjectProperty<EventHandler<NotificationEvent>> onShowNotificationProperty() {
return onShowNotification;
}
public final void setOnShowNotification(EventHandler<NotificationEvent> value) {
onShowNotificationProperty().set(value);
}
public final EventHandler<NotificationEvent> getOnShowNotification() {
return onShowNotificationProperty().get();
}
private final ObjectProperty<EventHandler<NotificationEvent>> onShowNotification = new
ObjectPropertyBase<EventHandler<NotificationEvent>>() {
@Override
public Object getBean() {
return this;
}
@Override
public String getName() {
return "onShowNotification";
}
};
public final ObjectProperty<EventHandler<NotificationEvent>> onHideNotificationProperty() {
return onHideNotification;
}
public final void setOnHideNotification(EventHandler<NotificationEvent> value) {
onHideNotificationProperty().set(value);
}
public final EventHandler<NotificationEvent> getOnHideNotification() {
return onHideNotificationProperty().get();
}
private final ObjectProperty<EventHandler<NotificationEvent>> onHideNotification = new
ObjectPropertyBase<EventHandler<NotificationEvent>>() {
@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<NotificationEvent> 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);
}
}
}

View File

@ -840,10 +840,27 @@ textfield */
-fx-text-fill: #333;
}
#popup-message {
#popup-bg {
-fx-font-size: 15;
-fx-text-fill: #333;
-fx-background-color: white;
-fx-background-radius: 5 5 5 5;
-fx-background-insets: 10;
-fx-effect: dropshadow(gaussian, #999, 10, 0, 0, 0);
}
#popup-button {
-fx-font-size: 15;
}
#notification-popup-headline {
-fx-font-size: 12;
-fx-font-weight: bold;
-fx-text-fill: #3c3c3c;
}
#notification-popup-bg {
-fx-font-size: 11;
-fx-text-fill: #3c3c3c;
-fx-background-color: linear-gradient(to bottom, #fcfcfc, #e5e5e5);
-fx-background-radius: 5 5 5 5;
-fx-background-insets: 10;
-fx-effect: dropshadow(gaussian, #434343, 10, 0, 0, 0);
}

View File

@ -1,38 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.gui.components;
import eu.hansolo.enzo.notification.Notification;
import eu.hansolo.enzo.notification.NotificationBuilder;
import eu.hansolo.enzo.notification.NotifierBuilder;
import io.bitsquare.common.util.Utilities;
/**
* Not sure if we stick with the eu.hansolo.enzo.notification.Notification implementation, so keep it behind a service
*/
public class SystemNotification {
private static final Notification.Notifier notifier = NotifierBuilder.create().build();
public static void openInfoNotification(String title, String message) {
// On windows it causes problems with the hidden stage used in the hansolo Notification implementation
// Lets deactivate it for the moment and fix that with a more native-like or real native solution later.
// Lets deactivate it for Linux as well, as it is not much tested yet
if (Utilities.isOSX())
notifier.notify(NotificationBuilder.create().title(title).message(message).build());
}
}

View File

@ -176,11 +176,6 @@ public class MainViewModel implements ViewModel {
TxIdTextField.setWalletService(walletService);
BalanceTextField.setWalletService(walletService);
BalanceWithConfirmationTextField.setWalletService(walletService);
if (BitsquareApp.DEV_MODE) {
preferences.setUseAnimations(false);
preferences.setUseEffects(false);
}
}

View File

@ -1,49 +1,17 @@
package io.bitsquare.gui.main.intructions;
import com.google.inject.Inject;
import io.bitsquare.gui.main.notifications.Notification;
import io.bitsquare.trade.TradeManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
public class InstructionCenter {
private final Logger log = LoggerFactory.getLogger(InstructionCenter.class);
private Queue<io.bitsquare.gui.main.notifications.Notification> notifications = new LinkedBlockingQueue<>(3);
private io.bitsquare.gui.main.notifications.Notification displayedNotification;
private TradeManager tradeManager;
private final TradeManager tradeManager;
@Inject
public InstructionCenter(TradeManager tradeManager) {
this.tradeManager = tradeManager;
}
void queueForDisplay(io.bitsquare.gui.main.notifications.Notification notification) {
boolean result = notifications.offer(notification);
if (!result)
log.warn("The capacity is full with popups in the queue.\n\t" +
"Not added new notification=" + notification);
displayNext();
}
void isHidden(Notification notification) {
if (displayedNotification == null || displayedNotification == notification) {
displayedNotification = null;
displayNext();
} else {
log.warn("We got a isHidden called with a wrong notification.\n\t" +
"notification (argument)=" + notification + "\n\tdisplayedPopup=" + displayedNotification);
}
}
private void displayNext() {
if (displayedNotification == null) {
if (!notifications.isEmpty()) {
displayedNotification = notifications.poll();
displayedNotification.display();
}
}
}
}

View File

@ -1,6 +1,18 @@
package io.bitsquare.gui.main.notifications;
import io.bitsquare.gui.main.popups.Popup;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Camera;
import javafx.scene.PerspectiveCamera;
import javafx.scene.transform.Rotate;
import javafx.stage.Modality;
import javafx.stage.Window;
import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -9,6 +21,7 @@ public class Notification extends Popup {
private boolean hasBeenDisplayed;
public Notification() {
width = 320;
NotificationCenter.add(this);
}
@ -28,15 +41,138 @@ public class Notification extends Popup {
return (Notification) super.message(message);
}
@Override
protected void addSeparator() {
// dont show a separator
}
@Override
public void show() {
super.show();
hasBeenDisplayed = true;
}
public void hide() {
super.hide();
@Override
public void display() {
super.display();
}
@Override
protected void animateHide(Runnable onFinishedHandler) {
if (NotificationCenter.useAnimations) {
double duration = 400;
Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1);
gridPane.setRotationAxis(Rotate.X_AXIS);
Camera camera = gridPane.getScene().getCamera();
gridPane.getScene().setCamera(new PerspectiveCamera());
Timeline timeline = new Timeline();
ObservableList<KeyFrame> keyFrames = timeline.getKeyFrames();
keyFrames.add(new KeyFrame(Duration.millis(0),
new KeyValue(gridPane.rotateProperty(), 0, interpolator),
new KeyValue(gridPane.opacityProperty(), 1, interpolator)
));
keyFrames.add(new KeyFrame(Duration.millis(duration),
new KeyValue(gridPane.rotateProperty(), -90, interpolator),
new KeyValue(gridPane.opacityProperty(), 0, interpolator)
));
timeline.setOnFinished(event -> {
gridPane.setRotate(0);
gridPane.setRotationAxis(Rotate.Z_AXIS);
gridPane.getScene().setCamera(camera);
onFinishedHandler.run();
});
timeline.play();
} else {
onFinishedHandler.run();
}
}
@Override
protected void animateDisplay() {
if (NotificationCenter.useAnimations) {
double startX = 320;
double duration = 600;
Interpolator interpolator = Interpolator.SPLINE(0.25, 0.1, 0.25, 1);
Timeline timeline = new Timeline();
ObservableList<KeyFrame> keyFrames = timeline.getKeyFrames();
keyFrames.add(new KeyFrame(Duration.millis(0),
new KeyValue(gridPane.opacityProperty(), 0, interpolator),
new KeyValue(gridPane.translateXProperty(), startX, interpolator)
));
//bouncing
/* keyFrames.add(new KeyFrame(Duration.millis(duration * 0.6),
new KeyValue(gridPane.opacityProperty(), 1, interpolator),
new KeyValue(gridPane.translateXProperty(), -12, interpolator)
));
keyFrames.add(new KeyFrame(Duration.millis(duration * 0.8),
new KeyValue(gridPane.opacityProperty(), 1, interpolator),
new KeyValue(gridPane.translateXProperty(), 4, interpolator)
));*/
keyFrames.add(new KeyFrame(Duration.millis(duration),
new KeyValue(gridPane.opacityProperty(), 1, interpolator),
new KeyValue(gridPane.translateXProperty(), 0, interpolator)
));
timeline.play();
}
}
@Override
protected void createGridPane() {
super.createGridPane();
gridPane.setPadding(new Insets(20, 20, 20, 20));
}
@Override
protected void addCloseButton() {
buttonDistance = 10;
super.addCloseButton();
}
@Override
protected void applyStyles() {
gridPane.setId("notification-popup-bg");
if (headLineLabel != null)
headLineLabel.setId("notification-popup-headline");
}
@Override
protected void setModality() {
stage.initModality(Modality.NONE);
}
@Override
protected void layout() {
Window window = owner.getScene().getWindow();
double titleBarHeight = window.getHeight() - owner.getScene().getHeight();
stage.setX(Math.round(window.getX() + window.getWidth() - stage.getWidth()));
stage.setY(Math.round(window.getY() + titleBarHeight));
}
@Override
protected void addEffectToBackground() {
}
@Override
protected void removeEffectFromBackground() {
}
/* @Override
protected void addCloseButton() {
closeButton = new Button("Close");
closeButton.setOnAction(event -> {
hide();
closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run());
});
GridPane.setHalignment(closeButton, HPos.RIGHT);
GridPane.setRowIndex(closeButton, ++rowIndex);
GridPane.setColumnIndex(closeButton, 1);
gridPane.getChildren().add(closeButton);
}*/
public boolean isHasBeenDisplayed() {
return hasBeenDisplayed;
}

View File

@ -3,18 +3,26 @@ package io.bitsquare.gui.main.notifications;
import com.google.inject.Inject;
import io.bitsquare.app.Log;
import io.bitsquare.arbitration.DisputeManager;
import io.bitsquare.common.UserThread;
import io.bitsquare.gui.Navigation;
import io.bitsquare.gui.main.MainView;
import io.bitsquare.gui.main.portfolio.PortfolioView;
import io.bitsquare.gui.main.portfolio.pendingtrades.PendingTradesView;
import io.bitsquare.trade.Trade;
import io.bitsquare.trade.TradeManager;
import io.bitsquare.user.Preferences;
import javafx.collections.ListChangeListener;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
public class NotificationCenter {
private static final Logger log = LoggerFactory.getLogger(NotificationCenter.class);
@ -25,11 +33,14 @@ public class NotificationCenter {
///////////////////////////////////////////////////////////////////////////////////////////
private final static List<Notification> notifications = new ArrayList<>();
private Consumer<String> selectItemByTradeIdConsumer;
static void add(Notification notification) {
notifications.add(notification);
}
static boolean useAnimations;
///////////////////////////////////////////////////////////////////////////////////////////
// Instance fields
@ -37,9 +48,12 @@ public class NotificationCenter {
private TradeManager tradeManager;
private DisputeManager disputeManager;
private Preferences preferences;
private Navigation navigation;
private final Map<String, Subscription> disputeStateSubscriptionsMap = new HashMap<>();
private final Map<String, Subscription> tradeStateSubscriptionsMap = new HashMap<>();
@Nullable
private String selectedTradeId;
@ -48,16 +62,18 @@ public class NotificationCenter {
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public NotificationCenter(TradeManager tradeManager, DisputeManager disputeManager) {
public NotificationCenter(TradeManager tradeManager, DisputeManager disputeManager, Preferences preferences, Navigation navigation) {
this.tradeManager = tradeManager;
this.disputeManager = disputeManager;
this.preferences = preferences;
this.navigation = navigation;
EasyBind.subscribe(preferences.useAnimationsProperty(), useAnimations -> NotificationCenter.useAnimations = useAnimations);
}
public void onAllServicesInitialized() {
tradeManager.getTrades().addListener((ListChangeListener<Trade>) change -> {
change.next();
log.error("change getRemoved " + change.getRemoved());
log.error("change getAddedSubList " + change.getAddedSubList());
if (change.wasRemoved()) {
change.getRemoved().stream().forEach(trade -> {
String tradeId = trade.getId();
@ -112,10 +128,14 @@ public class NotificationCenter {
return selectedTradeId;
}
public void setSelectedTradeId(String selectedTradeId) {
public void setSelectedTradeId(@Nullable String selectedTradeId) {
this.selectedTradeId = selectedTradeId;
}
public void setSelectItemByTradeIdConsumer(Consumer<String> selectItemByTradeIdConsumer) {
this.selectItemByTradeIdConsumer = selectItemByTradeIdConsumer;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
@ -127,43 +147,54 @@ public class NotificationCenter {
if (tradeManager.isBuyer(trade.getOffer())) {
switch (tradeState) {
case DEPOSIT_PUBLISHED_MSG_RECEIVED:
message = "Your offer has been accepted by a seller.\n" +
"You need to wait for one blockchain confirmation before starting the payment.";
message = "Your offer has been accepted by a seller.";
break;
case DEPOSIT_CONFIRMED:
message = "The deposit transaction of your trade has got the first blockchain confirmation.\n" +
"You have to start the payment to the bitcoin seller now.";
message = "Your trade has at least one blockchain confirmation.\n" +
"You can start the payment now.";
break;
/* case FIAT_PAYMENT_RECEIPT_MSG_RECEIVED:
case PAYOUT_TX_COMMITTED:
case PAYOUT_TX_SENT:*/
case PAYOUT_BROAD_CASTED:
message = "The bitcoin seller has confirmed the receipt of your payment and the payout transaction has been published.\n" +
"The trade is now completed and you can withdraw your funds.";
message = "The trade is now completed and you can withdraw your funds.";
break;
}
} else {
switch (tradeState) {
case DEPOSIT_PUBLISHED_MSG_RECEIVED:
message = "Your offer has been accepted by a buyer.\n" +
"You need to wait for one blockchain confirmation before starting the payment.";
message = "Your offer has been accepted by a buyer.";
break;
case FIAT_PAYMENT_STARTED_MSG_RECEIVED:
message = "The bitcoin buyer has started the payment.\n" +
"Please check your payment account if you have received his payment.";
message = "The bitcoin buyer has started the payment.";
break;
/* case FIAT_PAYMENT_RECEIPT_MSG_SENT:
case PAYOUT_TX_RECEIVED:
case PAYOUT_TX_COMMITTED:*/
case PAYOUT_BROAD_CASTED:
message = "The payout transaction has been published.\n" +
"The trade is now completed and you can withdraw your funds.";
message = "The trade is now completed and you can withdraw your funds.";
}
}
if (message != null && !trade.getId().equals(selectedTradeId))
new Notification().tradeHeadLine(trade.getShortId()).message(message).show();
if (message != null) {
Notification notification = new Notification().tradeHeadLine(trade.getShortId()).message(message);
if (navigation.getCurrentPath() != null && !navigation.getCurrentPath().contains(PendingTradesView.class)) {
notification.actionButtonText("Go to \"Open trades\"")
.onAction(() -> {
navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class);
UserThread.runAfter(() -> {
selectItemByTradeIdConsumer.accept(trade.getId());
}, 1);
})
.show();
} else if (selectedTradeId != null && !trade.getId().equals(selectedTradeId)) {
notification.actionButtonText("Select trade")
.onAction(() -> selectItemByTradeIdConsumer.accept(trade.getId()))
.show();
}
}
}
private void onDisputeStateChanged(Trade trade, Trade.DisputeState disputeState) {

View File

@ -27,7 +27,6 @@ import javafx.beans.value.ChangeListener;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
@ -74,6 +73,7 @@ public class Popup {
private Preferences preferences;
private ChangeListener<Number> positionListener;
private Timer centerTime;
protected double buttonDistance = 20;
///////////////////////////////////////////////////////////////////////////////////////////
@ -86,6 +86,7 @@ public class Popup {
public void show() {
createGridPane();
addHeadLine();
addSeparator();
if (showProgressIndicator)
addProgressIndicator();
@ -96,24 +97,34 @@ public class Popup {
addCloseButton();
addDontShowAgainCheckBox();
applyStyles();
PopupManager.queueForDisplay(this);
}
public void hide() {
owner.getScene().getWindow().xProperty().removeListener(positionListener);
owner.getScene().getWindow().yProperty().removeListener(positionListener);
animateHide(() -> {
Window window = owner.getScene().getWindow();
window.xProperty().removeListener(positionListener);
window.yProperty().removeListener(positionListener);
window.widthProperty().removeListener(positionListener);
if (centerTime != null)
centerTime.cancel();
if (centerTime != null)
centerTime.cancel();
MainView.removeBlur();
if (stage != null)
stage.hide();
else
log.warn("Stage is null");
removeEffectFromBackground();
cleanup();
PopupManager.isHidden(this);
if (stage != null)
stage.hide();
else
log.warn("Stage is null");
cleanup();
PopupManager.isHidden(Popup.this);
});
}
protected void animateHide(Runnable onFinishedHandler) {
onFinishedHandler.run();
}
protected void cleanup() {
@ -217,11 +228,6 @@ public class Popup {
gridPane.setVgap(5);
gridPane.setPadding(new Insets(30, 30, 30, 30));
gridPane.setPrefWidth(width);
gridPane.setStyle("-fx-background-color: white;" +
"-fx-background-radius: 5 5 5 5;" +
"-fx-effect: dropshadow(gaussian, #999, 10, 0, 0, 0);" +
"-fx-background-insets: 10;"
);
ColumnConstraints columnConstraints1 = new ColumnConstraints();
columnConstraints1.setHalignment(HPos.RIGHT);
@ -232,7 +238,7 @@ public class Popup {
}
protected void blurAgain() {
FxTimer.runLater(Duration.ofMillis(Transitions.DEFAULT_DURATION), () -> MainView.blurLight());
FxTimer.runLater(Duration.ofMillis(Transitions.DEFAULT_DURATION), MainView::blurLight);
}
public void display() {
@ -244,14 +250,15 @@ public class Popup {
scene.getStylesheets().setAll(owner.getScene().getStylesheets());
scene.setFill(Color.TRANSPARENT);
stage.setScene(scene);
stage.initModality(Modality.WINDOW_MODAL);
setModality();
stage.initStyle(StageStyle.TRANSPARENT);
stage.initOwner(owner.getScene().getWindow());
Window window = owner.getScene().getWindow();
stage.initOwner(window);
stage.show();
centerPopup();
layout();
MainView.blurLight();
addEffectToBackground();
// On Linux the owner stage does not move the child stage as it does on Mac
// So we need to apply centerPopup. Further with fast movements the handler loses
@ -259,34 +266,61 @@ public class Popup {
// Also on Mac sometimes the popups are positioned outside of the main app, so keep it for all OS
positionListener = (observable, oldValue, newValue) -> {
if (stage != null) {
centerPopup();
layout();
if (centerTime != null)
centerTime.cancel();
centerTime = UserThread.runAfter(this::centerPopup, 3);
centerTime = UserThread.runAfter(this::layout, 3);
}
};
owner.getScene().getWindow().xProperty().addListener(positionListener);
owner.getScene().getWindow().yProperty().addListener(positionListener);
window.xProperty().addListener(positionListener);
window.yProperty().addListener(positionListener);
window.widthProperty().addListener(positionListener);
animateDisplay();
}
protected void centerPopup() {
protected void animateDisplay() {
}
protected void setModality() {
stage.initModality(Modality.WINDOW_MODAL);
}
protected void applyStyles() {
gridPane.setId("popup-bg");
if (headLineLabel != null)
headLineLabel.setId("popup-headline");
}
protected void addEffectToBackground() {
MainView.blurLight();
}
protected void removeEffectFromBackground() {
MainView.removeBlur();
}
protected void layout() {
Window window = owner.getScene().getWindow();
double titleBarHeight = window.getHeight() - owner.getScene().getHeight();
Point2D point = owner.localToScene(0, 0);
stage.setX(Math.round(window.getX() + point.getX() + (owner.getWidth() - stage.getWidth()) / 2));
stage.setY(Math.round(window.getY() + titleBarHeight + point.getY() + (owner.getHeight() - stage.getHeight()) / 2));
stage.setX(Math.round(window.getX() + (owner.getWidth() - stage.getWidth()) / 2));
stage.setY(Math.round(window.getY() + titleBarHeight + (owner.getHeight() - stage.getHeight()) / 2));
}
protected void addHeadLine() {
if (headLine != null) {
headLineLabel = new Label(BSResources.get(headLine));
headLineLabel.setMouseTransparent(true);
headLineLabel.setId("popup-headline");
GridPane.setHalignment(headLineLabel, HPos.LEFT);
GridPane.setRowIndex(headLineLabel, ++rowIndex);
GridPane.setColumnSpan(headLineLabel, 2);
gridPane.getChildren().addAll(headLineLabel);
}
}
protected void addSeparator() {
if (headLine != null) {
Separator separator = new Separator();
separator.setMouseTransparent(true);
separator.setOrientation(Orientation.HORIZONTAL);
@ -295,7 +329,7 @@ public class Popup {
GridPane.setRowIndex(separator, ++rowIndex);
GridPane.setColumnSpan(separator, 2);
gridPane.getChildren().addAll(headLineLabel, separator);
gridPane.getChildren().add(separator);
}
}
@ -322,7 +356,6 @@ public class Popup {
"It will make debugging easier if you can attach the bitsquare.log file which you can find in the application directory.");
Button githubButton = new Button("Report to Github issue tracker");
githubButton.setId("popup-button");
GridPane.setMargin(githubButton, new Insets(20, 0, 0, 0));
GridPane.setHalignment(githubButton, HPos.RIGHT);
GridPane.setRowIndex(githubButton, ++rowIndex);
@ -335,7 +368,6 @@ public class Popup {
});
Button mailButton = new Button("Report by email");
mailButton.setId("popup-button");
GridPane.setHalignment(mailButton, HPos.RIGHT);
GridPane.setRowIndex(mailButton, ++rowIndex);
GridPane.setColumnIndex(mailButton, 1);
@ -373,7 +405,6 @@ public class Popup {
protected void addCloseButton() {
closeButton = new Button(closeButtonText == null ? "Close" : closeButtonText);
closeButton.setId("popup-button");
closeButton.setOnAction(event -> {
hide();
closeHandlerOptional.ifPresent(closeHandler -> closeHandler.run());
@ -381,7 +412,6 @@ public class Popup {
if (actionHandlerOptional.isPresent() || actionButtonText != null) {
actionButton = new Button(actionButtonText == null ? "Ok" : actionButtonText);
actionButton.setId("popup-button");
actionButton.setDefaultButton(true);
//TODO app wide focus
//actionButton.requestFocus();
@ -399,13 +429,13 @@ public class Popup {
GridPane.setHalignment(hBox, HPos.RIGHT);
GridPane.setRowIndex(hBox, ++rowIndex);
GridPane.setColumnSpan(hBox, 2);
GridPane.setMargin(hBox, new Insets(30, 0, 0, 0));
GridPane.setMargin(hBox, new Insets(buttonDistance, 0, 0, 0));
gridPane.getChildren().add(hBox);
} else {
closeButton.setDefaultButton(true);
GridPane.setHalignment(closeButton, HPos.RIGHT);
if (!showReportErrorButtons)
GridPane.setMargin(closeButton, new Insets(20, 0, 0, 0));
GridPane.setMargin(closeButton, new Insets(buttonDistance, 0, 0, 0));
GridPane.setRowIndex(closeButton, ++rowIndex);
GridPane.setColumnIndex(closeButton, 1);
gridPane.getChildren().add(closeButton);

View File

@ -32,6 +32,7 @@ import io.bitsquare.gui.Navigation;
import io.bitsquare.gui.common.model.ActivatableDataModel;
import io.bitsquare.gui.main.MainView;
import io.bitsquare.gui.main.disputes.DisputesView;
import io.bitsquare.gui.main.notifications.NotificationCenter;
import io.bitsquare.gui.main.popups.SelectDepositTxPopup;
import io.bitsquare.gui.main.popups.WalletPasswordPopup;
import io.bitsquare.payment.PaymentAccountContractData;
@ -71,6 +72,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
public final DisputeManager disputeManager;
private final Navigation navigation;
private final WalletPasswordPopup walletPasswordPopup;
private NotificationCenter notificationCenter;
final ObservableList<PendingTradesListItem> list = FXCollections.observableArrayList();
private final ListChangeListener<Trade> tradesListChangeListener;
@ -79,6 +81,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
final ObjectProperty<PendingTradesListItem> selectedItemProperty = new SimpleObjectProperty<>();
public final StringProperty txId = new SimpleStringProperty();
public final Preferences preferences;
private boolean activated;
///////////////////////////////////////////////////////////////////////////////////////////
@ -88,7 +91,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
@Inject
public PendingTradesDataModel(TradeManager tradeManager, WalletService walletService, TradeWalletService tradeWalletService,
User user, KeyRing keyRing, DisputeManager disputeManager, Preferences preferences,
Navigation navigation, WalletPasswordPopup walletPasswordPopup) {
Navigation navigation, WalletPasswordPopup walletPasswordPopup, NotificationCenter notificationCenter) {
this.tradeManager = tradeManager;
this.walletService = walletService;
this.tradeWalletService = tradeWalletService;
@ -98,19 +101,27 @@ public class PendingTradesDataModel extends ActivatableDataModel {
this.preferences = preferences;
this.navigation = navigation;
this.walletPasswordPopup = walletPasswordPopup;
this.notificationCenter = notificationCenter;
tradesListChangeListener = change -> onListChanged();
notificationCenter.setSelectItemByTradeIdConsumer(this::selectItemByTradeId);
}
@Override
protected void activate() {
tradeManager.getTrades().addListener(tradesListChangeListener);
onListChanged();
if (selectedItemProperty.get() != null)
notificationCenter.setSelectedTradeId(selectedItemProperty.get().getTrade().getId());
activated = true;
}
@Override
protected void deactivate() {
tradeManager.getTrades().removeListener(tradesListChangeListener);
notificationCenter.setSelectedTradeId(null);
activated = false;
}
@ -254,12 +265,20 @@ public class PendingTradesDataModel extends ActivatableDataModel {
doSelectItem(null);
}
private void selectItemByTradeId(String tradeId) {
if (activated)
list.stream().filter(e -> e.getTrade().getId().equals(tradeId)).findAny().ifPresent(this::doSelectItem);
}
private void doSelectItem(PendingTradesListItem item) {
if (item != null) {
Trade trade = item.getTrade();
isOfferer = tradeManager.isMyOffer(trade.getOffer());
if (trade.getDepositTx() != null)
txId.set(trade.getDepositTx().getHashAsString());
notificationCenter.setSelectedTradeId(trade.getId());
} else {
notificationCenter.setSelectedTradeId(null);
}
selectedItemProperty.set(item);
}

View File

@ -45,10 +45,6 @@ public class PendingTradesListItem {
return trade.tradeVolumeProperty();
}
public String getId() {
return trade.getShortId();
}
public Fiat getPrice() {
return trade.getOffer().getPrice();
}

View File

@ -217,7 +217,7 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
super.updateItem(item, empty);
if (item != null && !empty) {
field = new HyperlinkWithIcon(item.getId(), true);
field = new HyperlinkWithIcon(item.getTrade().getShortId(), true);
field.setOnAction(event -> tradeDetailsPopup.show(item.getTrade()));
field.setTooltip(new Tooltip("Open popup for details"));
setGraphic(field);

View File

@ -112,9 +112,8 @@ public class BuyerStep5View extends TradeStepView {
() -> {
String id = "TradeCompletedInfoPopup";
if (preferences.showAgain(id)) {
new Popup()
.information("You can review your completed trades under \"Portfolio/History\" or " +
"review your transactions under \"Funds/Transactions\"")
new Popup().information("You can review your completed trades under \"Portfolio/History\" or " +
"review your transactions under \"Funds/Transactions\"")
.dontShowAgainId(id, preferences)
.show();
}