Merge pull request #1624 from ManfredKarrer/notifications

Add mobile notifications
This commit is contained in:
Manfred Karrer 2018-08-15 15:38:42 +02:00 committed by GitHub
commit 276d526e18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1487 additions and 53 deletions

View File

@ -50,6 +50,9 @@ dependencies {
compile 'de.jensd:fontawesomefx-commons:8.15'
compile 'de.jensd:fontawesomefx-materialdesignfont:1.7.22-4'
compile 'com.googlecode.jcsv:jcsv:1.4.0'
compile 'com.github.sarxos:webcam-capture:0.3.12'
compileOnly 'org.projectlombok:lombok:1.16.16'
annotationProcessor 'org.projectlombok:lombok:1.16.16'
testCompile('org.mockito:mockito-core:2.8.9') {

View File

@ -64,6 +64,7 @@ bg color of non edit textFields: fafafa
-bs-red: red; /* 5 usages */
-bs-error-red: #dd0000; /* 5 usages */
-bs-soft-red: #ee6664; /* 1 usages */
-bs-pink: #ff8986; /* 2 usages */
-bs-orange: #ff8a2b; /* 2 usages */
-bs-orange2: #dd6900; /* 1 usages */
@ -1337,3 +1338,13 @@ textfield */
-fx-background-insets: 0, 0 0 0 0
}
/********************************************************************************************************************
* *
* Notifications *
* *
********************************************************************************************************************/
#notification-erase-button {
-fx-background-color: -bs-soft-red;
-fx-text-fill: #ffffff;
}

View File

@ -32,14 +32,19 @@ import javafx.beans.property.StringProperty;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import static bisq.desktop.util.FormBuilder.getIcon;
public class InfoInputTextField extends AnchorPane {
private final StringProperty text = new SimpleStringProperty();
private final InputTextField textField;
@Getter
private final InputTextField inputTextField;
@Getter
private final Label infoIcon;
@Getter
private final Label warningIcon;
private Label currentIcon;
private PopOver popover;
@ -48,7 +53,7 @@ public class InfoInputTextField extends AnchorPane {
public InfoInputTextField() {
super();
textField = new InputTextField();
inputTextField = new InputTextField();
infoIcon = getIcon(AwesomeIcon.INFO_SIGN);
infoIcon.setLayoutY(3);
@ -60,12 +65,12 @@ public class InfoInputTextField extends AnchorPane {
AnchorPane.setLeftAnchor(infoIcon, 7.0);
AnchorPane.setLeftAnchor(warningIcon, 7.0);
AnchorPane.setRightAnchor(textField, 0.0);
AnchorPane.setLeftAnchor(textField, 0.0);
AnchorPane.setRightAnchor(inputTextField, 0.0);
AnchorPane.setLeftAnchor(inputTextField, 0.0);
hideIcons();
getChildren().addAll(textField, infoIcon, warningIcon);
getChildren().addAll(inputTextField, infoIcon, warningIcon);
}
private void hideIcons() {
@ -93,6 +98,35 @@ public class InfoInputTextField extends AnchorPane {
setActionHandlers(node);
}
public void setIconsRightAligned() {
AnchorPane.clearConstraints(infoIcon);
AnchorPane.clearConstraints(warningIcon);
AnchorPane.clearConstraints(inputTextField);
AnchorPane.setRightAnchor(infoIcon, 7.0);
AnchorPane.setRightAnchor(warningIcon, 7.0);
AnchorPane.setLeftAnchor(inputTextField, 0.0);
AnchorPane.setRightAnchor(inputTextField, 0.0);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters/Setters
///////////////////////////////////////////////////////////////////////////////////////////
public void setText(String text) {
this.text.set(text);
}
public String getText() {
return text.get();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void setActionHandlers(Node node) {
currentIcon.setManaged(true);
@ -116,10 +150,6 @@ public class InfoInputTextField extends AnchorPane {
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void showPopOver(Node node) {
node.getStyleClass().add("default-text");
@ -132,19 +162,4 @@ public class InfoInputTextField extends AnchorPane {
popover.show(currentIcon, -17);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters/Setters
///////////////////////////////////////////////////////////////////////////////////////////
public InputTextField getTextField() { return textField; }
public void setText(String text) {
this.text.set(text);
}
public String getText() {
return text.get();
}
}

View File

@ -49,7 +49,7 @@ import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
@FxmlView
public class AccountView extends ActivatableView<TabPane, AccountViewModel> {
public class AccountView extends ActivatableView<TabPane, Void> {
@FXML
Tab accountSettingsTab;
@ -67,8 +67,8 @@ public class AccountView extends ActivatableView<TabPane, AccountViewModel> {
private EventHandler<KeyEvent> keyEventEventHandler;
@Inject
private AccountView(AccountViewModel model, CachingViewLoader viewLoader, Navigation navigation) {
super(model);
private AccountView(CachingViewLoader viewLoader, Navigation navigation) {
super();
this.viewLoader = viewLoader;
this.navigation = navigation;
}

View File

@ -0,0 +1,211 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.account.content.notifications;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.util.ImageUtil;
import bisq.core.locale.Res;
import bisq.core.notifications.alerts.market.MarketAlertFilter;
import bisq.core.notifications.alerts.market.MarketAlerts;
import bisq.core.util.BSFormatter;
import bisq.common.UserThread;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.geometry.Insets;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.util.Callback;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ManageMarketAlertsWindow extends Overlay<ManageMarketAlertsWindow> {
private final MarketAlerts marketAlerts;
private final BSFormatter formatter;
ManageMarketAlertsWindow(MarketAlerts marketAlerts, BSFormatter formatter) {
this.marketAlerts = marketAlerts;
this.formatter = formatter;
type = Type.Attention;
}
@Override
public void show() {
if (headLine == null)
headLine = Res.get("account.notifications.marketAlert.manageAlerts.title");
width = 900;
createGridPane();
addHeadLine();
addContent();
addCloseButton();
applyStyles();
display();
}
@Override
protected void applyStyles() {
super.applyStyles();
gridPane.setId("popup-grid-pane-bg");
}
private void addContent() {
TableView<MarketAlertFilter> tableView = new TableView<>();
GridPane.setRowIndex(tableView, ++rowIndex);
GridPane.setColumnSpan(tableView, 2);
GridPane.setMargin(tableView, new Insets(10, 0, 0, 0));
gridPane.getChildren().add(tableView);
Label placeholder = new AutoTooltipLabel(Res.get("table.placeholder.noData"));
placeholder.setWrapText(true);
tableView.setPlaceholder(placeholder);
tableView.setPrefHeight(300);
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
setColumns(tableView);
tableView.setItems(FXCollections.observableArrayList(marketAlerts.getMarketAlertFilters()));
}
private void removeMarketAlertFilter(MarketAlertFilter marketAlertFilter, TableView<MarketAlertFilter> tableView) {
marketAlerts.removeMarketAlertFilter(marketAlertFilter);
UserThread.execute(() -> tableView.setItems(FXCollections.observableArrayList(marketAlerts.getMarketAlertFilters())));
}
private void setColumns(TableView<MarketAlertFilter> tableView) {
TableColumn<MarketAlertFilter, MarketAlertFilter> column;
column = new AutoTooltipTableColumn<>(Res.get("account.notifications.marketAlert.manageAlerts.header.paymentAccount"));
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<TableColumn<MarketAlertFilter, MarketAlertFilter>, TableCell<MarketAlertFilter, MarketAlertFilter>>() {
@Override
public TableCell<MarketAlertFilter, MarketAlertFilter> call(TableColumn<MarketAlertFilter, MarketAlertFilter> column) {
return new TableCell<MarketAlertFilter, MarketAlertFilter>() {
@Override
public void updateItem(final MarketAlertFilter item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setText(item.getPaymentAccount().getAccountName());
} else {
setText("");
}
}
};
}
});
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(Res.get("account.notifications.marketAlert.manageAlerts.header.trigger"));
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<TableColumn<MarketAlertFilter, MarketAlertFilter>, TableCell<MarketAlertFilter, MarketAlertFilter>>() {
@Override
public TableCell<MarketAlertFilter, MarketAlertFilter> call(TableColumn<MarketAlertFilter, MarketAlertFilter> column) {
return new TableCell<MarketAlertFilter, MarketAlertFilter>() {
@Override
public void updateItem(final MarketAlertFilter item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setText(formatter.formatPercentagePrice(item.getTriggerValue() / 10000d));
} else {
setText("");
}
}
};
}
});
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(Res.get("account.notifications.marketAlert.manageAlerts.header.offerType"));
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<TableColumn<MarketAlertFilter, MarketAlertFilter>, TableCell<MarketAlertFilter, MarketAlertFilter>>() {
@Override
public TableCell<MarketAlertFilter, MarketAlertFilter> call(TableColumn<MarketAlertFilter, MarketAlertFilter> column) {
return new TableCell<MarketAlertFilter, MarketAlertFilter>() {
@Override
public void updateItem(final MarketAlertFilter item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setText(item.isBuyOffer() ? Res.get("shared.buyBitcoin") : Res.get("shared.sellBitcoin"));
} else {
setText("");
}
}
};
}
});
tableView.getColumns().add(column);
column = new TableColumn<>();
column.setMinWidth(40);
column.setMaxWidth(column.getMinWidth());
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<TableColumn<MarketAlertFilter, MarketAlertFilter>, TableCell<MarketAlertFilter, MarketAlertFilter>>() {
@Override
public TableCell<MarketAlertFilter, MarketAlertFilter> call(TableColumn<MarketAlertFilter, MarketAlertFilter> column) {
return new TableCell<MarketAlertFilter, MarketAlertFilter>() {
final ImageView icon = ImageUtil.getImageViewById(ImageUtil.REMOVE_ICON);
final Button removeButton = new AutoTooltipButton("", icon);
{
removeButton.setId("icon-button");
removeButton.setTooltip(new Tooltip(Res.get("shared.remove")));
}
@Override
public void updateItem(final MarketAlertFilter item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
removeButton.setOnAction(e -> removeMarketAlertFilter(item, tableView));
setGraphic(removeButton);
} else {
setGraphic(null);
removeButton.setOnAction(null);
}
}
};
}
});
tableView.getColumns().add(column);
}
}

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ This file is part of Bisq.
~
~ Bisq 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.
~
~ Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
-->
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<GridPane fx:id="root" fx:controller="bisq.desktop.main.account.content.notifications.MobileNotificationsView"
hgap="5.0" vgap="5.0"
AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="20.0"
AnchorPane.rightAnchor="25.0" AnchorPane.topAnchor="20.0"
xmlns:fx="http://javafx.com/fxml">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" halignment="RIGHT" minWidth="140.0"/>
<ColumnConstraints hgrow="ALWAYS" minWidth="300.0"/>
</columnConstraints>
</GridPane>

View File

@ -0,0 +1,790 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.account.content.notifications;
import bisq.desktop.common.view.ActivatableView;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.InfoInputTextField;
import bisq.desktop.components.InputTextField;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.overlays.windows.WebCamWindow;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.GUIUtil;
import bisq.desktop.util.Layout;
import bisq.desktop.util.validation.AltcoinValidator;
import bisq.desktop.util.validation.FiatPriceValidator;
import bisq.desktop.util.validation.PercentageNumberValidator;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.Res;
import bisq.core.locale.TradeCurrency;
import bisq.core.monetary.Altcoin;
import bisq.core.notifications.MobileMessage;
import bisq.core.notifications.MobileNotificationService;
import bisq.core.notifications.alerts.DisputeMsgEvents;
import bisq.core.notifications.alerts.MyOfferTakenEvents;
import bisq.core.notifications.alerts.TradeEvents;
import bisq.core.notifications.alerts.market.MarketAlertFilter;
import bisq.core.notifications.alerts.market.MarketAlerts;
import bisq.core.notifications.alerts.price.PriceAlert;
import bisq.core.notifications.alerts.price.PriceAlertFilter;
import bisq.core.payment.PaymentAccount;
import bisq.core.provider.price.PriceFeedService;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.BSFormatter;
import bisq.core.util.validation.InputValidator;
import bisq.common.UserThread;
import bisq.common.util.Tuple2;
import bisq.common.util.Tuple3;
import javax.inject.Inject;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TextField;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.GridPane;
import javafx.geometry.Insets;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.util.StringConverter;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@FxmlView
public class MobileNotificationsView extends ActivatableView<GridPane, Void> {
private final Preferences preferences;
private final User user;
private final PriceFeedService priceFeedService;
private final MarketAlerts marketAlerts;
private final MobileNotificationService mobileNotificationService;
private final BSFormatter formatter;
private WebCamWindow webCamWindow;
private QrCodeReader qrCodeReader;
private TextField tokenInputTextField;
private Label tokenInputLabel;
private InputTextField priceAlertHighInputTextField, priceAlertLowInputTextField, marketAlertTriggerInputTextField;
private CheckBox useSoundCheckBox, tradeCheckBox, marketCheckBox, priceCheckBox;
private ComboBox<TradeCurrency> currencyComboBox;
private ComboBox<PaymentAccount> paymentAccountsComboBox;
private Button downloadButton, webCamButton, noWebCamButton, eraseButton, setPriceAlertButton,
removePriceAlertButton, addMarketAlertButton, manageAlertsButton /*,testMsgButton*/;
private ChangeListener<Boolean> useSoundCheckBoxListener, tradeCheckBoxListener, marketCheckBoxListener,
priceCheckBoxListener, priceAlertHighFocusListener, priceAlertLowFocusListener, marketAlertTriggerFocusListener;
private ChangeListener<String> tokenInputTextFieldListener, priceAlertHighListener, priceAlertLowListener, marketAlertTriggerListener;
private ChangeListener<Number> priceFeedServiceListener;
private int gridRow = 0;
private int testMsgCounter = 0;
private RadioButton buyOffersRadioButton, sellOffersRadioButton;
private ToggleGroup offerTypeRadioButtonsToggleGroup;
private ChangeListener<Toggle> offerTypeListener;
private String selectedPriceAlertTradeCurrency;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private MobileNotificationsView(Preferences preferences,
User user,
PriceFeedService priceFeedService,
MarketAlerts marketAlerts,
MobileNotificationService mobileNotificationService,
BSFormatter formatter) {
super();
this.preferences = preferences;
this.user = user;
this.priceFeedService = priceFeedService;
this.marketAlerts = marketAlerts;
this.mobileNotificationService = mobileNotificationService;
this.formatter = formatter;
}
@Override
public void initialize() {
createSetupFields();
createSettingsFields();
createMarketAlertFields();
createPriceAlertFields();
}
@Override
protected void activate() {
// setup
tokenInputTextField.textProperty().addListener(tokenInputTextFieldListener);
downloadButton.setOnAction(e -> onDownload());
webCamButton.setOnAction(e -> onOpenWebCam());
noWebCamButton.setOnAction(e -> onNoWebCam());
// testMsgButton.setOnAction(e -> onSendTestMsg());
eraseButton.setOnAction(e -> onErase());
// settings
useSoundCheckBox.selectedProperty().addListener(useSoundCheckBoxListener);
tradeCheckBox.selectedProperty().addListener(tradeCheckBoxListener);
marketCheckBox.selectedProperty().addListener(marketCheckBoxListener);
priceCheckBox.selectedProperty().addListener(priceCheckBoxListener);
// market alert
marketAlertTriggerInputTextField.textProperty().addListener(marketAlertTriggerListener);
marketAlertTriggerInputTextField.focusedProperty().addListener(marketAlertTriggerFocusListener);
offerTypeRadioButtonsToggleGroup.selectedToggleProperty().addListener(offerTypeListener);
paymentAccountsComboBox.setOnAction(e -> onPaymentAccountSelected());
addMarketAlertButton.setOnAction(e -> onAddMarketAlert());
manageAlertsButton.setOnAction(e -> onManageMarketAlerts());
paymentAccountsComboBox.setItems(FXCollections.observableArrayList(user.getPaymentAccountsAsObservable()));
// price alert
priceAlertHighInputTextField.textProperty().addListener(priceAlertHighListener);
priceAlertLowInputTextField.textProperty().addListener(priceAlertLowListener);
priceAlertHighInputTextField.focusedProperty().addListener(priceAlertHighFocusListener);
priceAlertLowInputTextField.focusedProperty().addListener(priceAlertLowFocusListener);
priceFeedService.updateCounterProperty().addListener(priceFeedServiceListener);
currencyComboBox.setOnAction(e -> onSelectedTradeCurrency());
setPriceAlertButton.setOnAction(e -> onSetPriceAlert());
removePriceAlertButton.setOnAction(e -> onRemovePriceAlert());
currencyComboBox.setItems(preferences.getTradeCurrenciesAsObservable());
if (preferences.getPhoneKeyAndToken() != null) {
tokenInputTextField.setText(preferences.getPhoneKeyAndToken());
setPairingTokenFieldsVisible();
} else {
eraseButton.setDisable(true);
//testMsgButton.setDisable(true);
}
setDisableForSetupFields(!mobileNotificationService.isSetupConfirmationSent());
updateMarketAlertFields();
fillPriceAlertFields();
updatePriceAlertFields();
}
@Override
protected void deactivate() {
// setup
tokenInputTextField.textProperty().removeListener(tokenInputTextFieldListener);
downloadButton.setOnAction(null);
webCamButton.setOnAction(null);
noWebCamButton.setOnAction(null);
//testMsgButton.setOnAction(null);
eraseButton.setOnAction(null);
// settings
useSoundCheckBox.selectedProperty().removeListener(useSoundCheckBoxListener);
tradeCheckBox.selectedProperty().removeListener(tradeCheckBoxListener);
marketCheckBox.selectedProperty().removeListener(marketCheckBoxListener);
priceCheckBox.selectedProperty().removeListener(priceCheckBoxListener);
// market alert
marketAlertTriggerInputTextField.textProperty().removeListener(marketAlertTriggerListener);
marketAlertTriggerInputTextField.focusedProperty().removeListener(marketAlertTriggerFocusListener);
offerTypeRadioButtonsToggleGroup.selectedToggleProperty().removeListener(offerTypeListener);
paymentAccountsComboBox.setOnAction(null);
addMarketAlertButton.setOnAction(null);
manageAlertsButton.setOnAction(null);
// price alert
priceAlertHighInputTextField.textProperty().removeListener(priceAlertHighListener);
priceAlertLowInputTextField.textProperty().removeListener(priceAlertLowListener);
priceAlertHighInputTextField.focusedProperty().removeListener(priceAlertHighFocusListener);
priceAlertLowInputTextField.focusedProperty().removeListener(priceAlertLowFocusListener);
priceFeedService.updateCounterProperty().removeListener(priceFeedServiceListener);
currencyComboBox.setOnAction(null);
setPriceAlertButton.setOnAction(null);
removePriceAlertButton.setOnAction(null);
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI events
///////////////////////////////////////////////////////////////////////////////////////////
// Setup
private void onDownload() {
GUIUtil.openWebPage("https://bisq.network/downloads");
}
private void onOpenWebCam() {
webCamButton.setDisable(true);
log.info("Start WebCamLauncher");
new WebCamLauncher(webCam -> {
log.info("webCam available");
webCamWindow = new WebCamWindow(webCam.getViewSize().width, webCam.getViewSize().height)
.onClose(() -> {
webCamButton.setDisable(false);
qrCodeReader.close();
});
webCamWindow.show();
qrCodeReader = new QrCodeReader(webCam, webCamWindow.getImageView(), qrCode -> {
log.info("Qr code available");
webCamWindow.hide();
webCamButton.setDisable(false);
reset();
tokenInputTextField.setText(qrCode);
updateMarketAlertFields();
updatePriceAlertFields();
});
}, throwable -> {
if (throwable instanceof NoWebCamFoundException) {
new Popup<>().warning(Res.get("account.notifications.noWebCamFound.warning")).show();
webCamButton.setDisable(false);
onNoWebCam();
} else {
log.error(throwable.toString());
new Popup<>().error(throwable.toString()).show();
}
});
}
private void onNoWebCam() {
setPairingTokenFieldsVisible();
noWebCamButton.setManaged(false);
noWebCamButton.setVisible(false);
}
private void onErase() {
try {
boolean success = mobileNotificationService.sendEraseMessage();
if (!success)
log.warn("Erase message sending did not succeed");
reset();
} catch (Exception e) {
new Popup<>().error(e.toString()).show();
}
}
private void onSendTestMsg() {
MobileMessage message = null;
List<MobileMessage> messages = null;
switch (testMsgCounter) {
case 0:
message = MyOfferTakenEvents.getTestMsg();
break;
case 1:
messages = TradeEvents.getTestMessages();
break;
case 2:
message = DisputeMsgEvents.getTestMsg();
break;
case 3:
message = PriceAlert.getTestMsg();
break;
case 4:
default:
message = MarketAlerts.getTestMsg();
break;
}
testMsgCounter++;
if (testMsgCounter > 4)
testMsgCounter = 0;
try {
if (message != null) {
mobileNotificationService.sendMessage(message, useSoundCheckBox.isSelected());
} else if (messages != null) {
messages.forEach(msg -> {
try {
mobileNotificationService.sendMessage(msg, useSoundCheckBox.isSelected());
} catch (Exception e) {
e.printStackTrace();
}
});
}
} catch (Exception e) {
new Popup<>().error(e.toString()).show();
}
}
// Market alerts
private void onPaymentAccountSelected() {
marketAlertTriggerInputTextField.clear();
marketAlertTriggerInputTextField.resetValidation();
offerTypeRadioButtonsToggleGroup.selectToggle(null);
updateMarketAlertFields();
}
private void onAddMarketAlert() {
PaymentAccount paymentAccount = paymentAccountsComboBox.getSelectionModel().getSelectedItem();
double percentAsDouble = formatter.parsePercentStringToDouble(marketAlertTriggerInputTextField.getText());
int triggerValue = (int) Math.round(percentAsDouble * 10000);
boolean isBuyOffer = offerTypeRadioButtonsToggleGroup.getSelectedToggle() == buyOffersRadioButton;
MarketAlertFilter marketAlertFilter = new MarketAlertFilter(paymentAccount, triggerValue, isBuyOffer);
marketAlerts.addMarketAlertFilter(marketAlertFilter);
paymentAccountsComboBox.getSelectionModel().clearSelection();
}
private void onManageMarketAlerts() {
new ManageMarketAlertsWindow(marketAlerts, formatter)
.onClose(this::updateMarketAlertFields)
.show();
}
// Price alerts
private void onSelectedTradeCurrency() {
TradeCurrency selectedItem = currencyComboBox.getSelectionModel().getSelectedItem();
if (selectedItem != null) {
selectedPriceAlertTradeCurrency = selectedItem.getCode();
boolean isCryptoCurrency = CurrencyUtil.isCryptoCurrency(selectedPriceAlertTradeCurrency);
priceAlertHighInputTextField.setValidator(isCryptoCurrency ? new AltcoinValidator() : new FiatPriceValidator());
priceAlertLowInputTextField.setValidator(isCryptoCurrency ? new AltcoinValidator() : new FiatPriceValidator());
} else {
selectedPriceAlertTradeCurrency = null;
}
updatePriceAlertFields();
}
private void onSetPriceAlert() {
if (arePriceAlertInputsValid()) {
String code = selectedPriceAlertTradeCurrency;
long high = getPriceAsLong(priceAlertHighInputTextField);
long low = getPriceAsLong(priceAlertLowInputTextField);
if (high > 0 && low > 0)
user.setPriceAlertFilter(new PriceAlertFilter(code, high, low));
updatePriceAlertFields();
}
}
private void onRemovePriceAlert() {
user.removePriceAlertFilter();
fillPriceAlertFields();
updatePriceAlertFields();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Create views
///////////////////////////////////////////////////////////////////////////////////////////
private void createSetupFields() {
FormBuilder.addTitledGroupBg(root, gridRow, 4, Res.get("account.notifications.setup.title"));
downloadButton = FormBuilder.addLabelButton(root, gridRow,
Res.getWithCol("account.notifications.download.label"), Res.get("account.notifications.download.button"),
Layout.FIRST_ROW_DISTANCE).second;
Tuple3<Label, Button, Button> tuple = FormBuilder.addLabel2Buttons(root, ++gridRow,
Res.getWithCol("account.notifications.webcam.label"),
Res.get("account.notifications.webcam.button"), Res.get("account.notifications.noWebcam.button"), 0);
webCamButton = tuple.second;
noWebCamButton = tuple.third;
Tuple2<Label, InputTextField> tuple2 = FormBuilder.addLabelInputTextField(root, ++gridRow,
Res.get("account.notifications.email.label"));
tokenInputLabel = tuple2.first;
tokenInputTextField = tuple2.second;
tokenInputTextField.setPromptText(Res.get("account.notifications.email.prompt"));
tokenInputTextFieldListener = (observable, oldValue, newValue) -> {
applyKeyAndToken(newValue);
};
tokenInputLabel.setManaged(false);
tokenInputLabel.setVisible(false);
tokenInputTextField.setManaged(false);
tokenInputTextField.setVisible(false);
/*testMsgButton = FormBuilder.addLabelButton(root, ++gridRow, Res.get("account.notifications.testMsg.label"),
Res.get("account.notifications.testMsg.title")).second;
testMsgButton.setDefaultButton(false);*/
eraseButton = FormBuilder.addLabelButton(root, ++gridRow,
Res.get("account.notifications.erase.label"),
Res.get("account.notifications.erase.title")).second;
eraseButton.setId("notification-erase-button");
}
private void createSettingsFields() {
FormBuilder.addTitledGroupBg(root, ++gridRow, 4,
Res.get("account.notifications.settings.title"),
Layout.GROUP_DISTANCE);
useSoundCheckBox = FormBuilder.addLabelCheckBox(root, gridRow,
Res.get("account.notifications.useSound.label"),
"",
Layout.FIRST_ROW_AND_GROUP_DISTANCE).second;
useSoundCheckBox.setSelected(preferences.isUseSoundForMobileNotifications());
useSoundCheckBoxListener = (observable, oldValue, newValue) -> {
mobileNotificationService.getUseSoundProperty().set(newValue);
preferences.setUseSoundForMobileNotifications(newValue);
};
tradeCheckBox = FormBuilder.addLabelCheckBox(root, ++gridRow,
Res.get("account.notifications.trade.label")).second;
tradeCheckBox.setSelected(preferences.isUseTradeNotifications());
tradeCheckBoxListener = (observable, oldValue, newValue) -> {
mobileNotificationService.getUseTradeNotificationsProperty().set(newValue);
preferences.setUseTradeNotifications(newValue);
};
marketCheckBox = FormBuilder.addLabelCheckBox(root, ++gridRow,
Res.get("account.notifications.market.label")).second;
marketCheckBox.setSelected(preferences.isUseMarketNotifications());
marketCheckBoxListener = (observable, oldValue, newValue) -> {
mobileNotificationService.getUseMarketNotificationsProperty().set(newValue);
preferences.setUseMarketNotifications(newValue);
updateMarketAlertFields();
};
priceCheckBox = FormBuilder.addLabelCheckBox(root, ++gridRow,
Res.get("account.notifications.price.label")).second;
priceCheckBox.setSelected(preferences.isUsePriceNotifications());
priceCheckBoxListener = (observable, oldValue, newValue) -> {
mobileNotificationService.getUsePriceNotificationsProperty().set(newValue);
preferences.setUsePriceNotifications(newValue);
updatePriceAlertFields();
};
}
private void createMarketAlertFields() {
FormBuilder.addTitledGroupBg(root, ++gridRow, 3, Res.get("account.notifications.marketAlert.title"),
Layout.GROUP_DISTANCE);
paymentAccountsComboBox = FormBuilder.<PaymentAccount>addLabelComboBox(root, gridRow,
Res.getWithCol("account.notifications.marketAlert.selectPaymentAccount"),
Layout.FIRST_ROW_AND_GROUP_DISTANCE).second;
paymentAccountsComboBox.setPromptText(Res.get("shared.select"));
paymentAccountsComboBox.setConverter(new StringConverter<PaymentAccount>() {
@Override
public String toString(PaymentAccount paymentAccount) {
return paymentAccount.getAccountName();
}
@Override
public PaymentAccount fromString(String string) {
return null;
}
});
offerTypeRadioButtonsToggleGroup = new ToggleGroup();
Tuple3<Label, RadioButton, RadioButton> tuple = FormBuilder.addLabelRadioButtonRadioButton(root, ++gridRow,
offerTypeRadioButtonsToggleGroup, Res.getWithCol("account.notifications.marketAlert.offerType.label"),
Res.get("account.notifications.marketAlert.offerType.buy"),
Res.get("account.notifications.marketAlert.offerType.sell"));
buyOffersRadioButton = tuple.second;
sellOffersRadioButton = tuple.third;
offerTypeListener = (observable, oldValue, newValue) -> {
marketAlertTriggerInputTextField.clear();
marketAlertTriggerInputTextField.resetValidation();
updateMarketAlertFields();
};
InfoInputTextField infoInputTextField = FormBuilder.addLabelInfoInputTextField(root, ++gridRow,
Res.getWithCol("account.notifications.marketAlert.trigger")).second;
marketAlertTriggerInputTextField = infoInputTextField.getInputTextField();
marketAlertTriggerInputTextField.setPromptText(Res.get("account.notifications.marketAlert.trigger.prompt"));
PercentageNumberValidator validator = new PercentageNumberValidator();
validator.setMaxValue(50D);
marketAlertTriggerInputTextField.setValidator(validator);
infoInputTextField.setContentForInfoPopOver(createMarketAlertPriceInfoPopupLabel(Res.get("account.notifications.marketAlert.trigger.info")));
infoInputTextField.setIconsRightAligned();
marketAlertTriggerListener = (observable, oldValue, newValue) -> {
updateMarketAlertFields();
};
marketAlertTriggerFocusListener = (observable, oldValue, newValue) -> {
if (oldValue && !newValue) {
try {
double percentAsDouble = formatter.parsePercentStringToDouble(marketAlertTriggerInputTextField.getText()) * 100;
marketAlertTriggerInputTextField.setText(formatter.formatRoundedDoubleWithPrecision(percentAsDouble, 2) + "%");
} catch (Throwable ignore) {
}
updateMarketAlertFields();
}
};
Tuple2<Button, Button> buttonTuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow,
Res.get("account.notifications.marketAlert.addButton"),
Res.get("account.notifications.marketAlert.manageAlertsButton"));
addMarketAlertButton = buttonTuple.first;
manageAlertsButton = buttonTuple.second;
}
private void createPriceAlertFields() {
FormBuilder.addTitledGroupBg(root, ++gridRow, 3,
Res.get("account.notifications.priceAlert.title"), 20);
currencyComboBox = FormBuilder.<TradeCurrency>addLabelComboBox(root, gridRow,
Res.getWithCol("list.currency.select"), 40).second;
currencyComboBox.setPromptText(Res.get("list.currency.select"));
currencyComboBox.setConverter(new StringConverter<TradeCurrency>() {
@Override
public String toString(TradeCurrency currency) {
return currency.getNameAndCode();
}
@Override
public TradeCurrency fromString(String string) {
return null;
}
});
priceAlertHighInputTextField = FormBuilder.addLabelInputTextField(root, ++gridRow,
Res.getWithCol("account.notifications.priceAlert.high.label")).second;
priceAlertHighListener = (observable, oldValue, newValue) -> {
long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField);
long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField);
if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) {
if (priceAlertHighTextFieldValue > priceAlertLowTextFieldValue)
updatePriceAlertFields();
}
};
priceAlertHighFocusListener = (observable, oldValue, newValue) -> {
if (oldValue && !newValue) {
applyPriceFormatting(priceAlertHighInputTextField);
long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField);
long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField);
if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) {
if (priceAlertHighTextFieldValue <= priceAlertLowTextFieldValue) {
new Popup<>().warning(Res.get("account.notifications.priceAlert.warning.highPriceTooLow")).show();
UserThread.execute(() -> {
priceAlertHighInputTextField.clear();
updatePriceAlertFields();
});
}
}
}
};
priceAlertLowInputTextField = FormBuilder.addLabelInputTextField(root, ++gridRow,
Res.getWithCol("account.notifications.priceAlert.low.label")).second;
priceAlertLowListener = (observable, oldValue, newValue) -> {
long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField);
long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField);
if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) {
if (priceAlertLowTextFieldValue < priceAlertHighTextFieldValue)
updatePriceAlertFields();
}
};
priceAlertLowFocusListener = (observable, oldValue, newValue) -> {
applyPriceFormatting(priceAlertLowInputTextField);
long priceAlertHighTextFieldValue = getPriceAsLong(priceAlertHighInputTextField);
long priceAlertLowTextFieldValue = getPriceAsLong(priceAlertLowInputTextField);
if (priceAlertLowTextFieldValue != 0 && priceAlertHighTextFieldValue != 0) {
if (priceAlertLowTextFieldValue >= priceAlertHighTextFieldValue) {
new Popup<>().warning(Res.get("account.notifications.priceAlert.warning.lowerPriceTooHigh")).show();
UserThread.execute(() -> {
priceAlertLowInputTextField.clear();
updatePriceAlertFields();
});
}
}
};
Tuple2<Button, Button> tuple = FormBuilder.add2ButtonsAfterGroup(root, ++gridRow,
Res.get("account.notifications.priceAlert.setButton"),
Res.get("account.notifications.priceAlert.removeButton"));
setPriceAlertButton = tuple.first;
removePriceAlertButton = tuple.second;
// When we get a price update an existing price alert might get removed.
// We get updated the view at each price update so we get aware of the removed PriceAlertFilter in the
// fillPriceAlertFields method. To be sure that we called after the PriceAlertFilter has been removed we delay
// to the next frame. The priceFeedServiceListener in the mobileNotificationService might get called before
// our listener here.
priceFeedServiceListener = (observable, oldValue, newValue) -> {
UserThread.execute(() -> {
fillPriceAlertFields();
updatePriceAlertFields();
});
};
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
// Setup/Settings
private void applyKeyAndToken(String keyAndToken) {
if (keyAndToken != null && !keyAndToken.isEmpty()) {
boolean isValid = mobileNotificationService.applyKeyAndToken(keyAndToken);
if (isValid) {
setDisableForSetupFields(false);
setPairingTokenFieldsVisible();
updateMarketAlertFields();
updatePriceAlertFields();
}
}
}
private void setDisableForSetupFields(boolean disable) {
// testMsgButton.setDisable(disable);
eraseButton.setDisable(disable);
useSoundCheckBox.setDisable(disable);
tradeCheckBox.setDisable(disable);
marketCheckBox.setDisable(disable);
priceCheckBox.setDisable(disable);
}
private void setPairingTokenFieldsVisible() {
tokenInputLabel.setManaged(true);
tokenInputLabel.setVisible(true);
tokenInputTextField.setManaged(true);
tokenInputTextField.setVisible(true);
}
private void reset() {
mobileNotificationService.reset();
tokenInputTextField.clear();
setDisableForSetupFields(true);
eraseButton.setDisable(true);
//testMsgButton.setDisable(true);
onRemovePriceAlert();
new ArrayList<>(marketAlerts.getMarketAlertFilters()).forEach(marketAlerts::removeMarketAlertFilter);
}
// Market alerts
private Label createMarketAlertPriceInfoPopupLabel(String text) {
final Label label = new Label(text);
label.setPrefWidth(300);
label.setWrapText(true);
label.setPadding(new Insets(10));
return label;
}
private void updateMarketAlertFields() {
boolean setupConfirmationSent = mobileNotificationService.isSetupConfirmationSent();
boolean selected = marketCheckBox.isSelected();
boolean disabled = !selected || !setupConfirmationSent;
boolean isPaymentAccountSelected = paymentAccountsComboBox.getSelectionModel().getSelectedItem() != null;
boolean isOfferTypeSelected = offerTypeRadioButtonsToggleGroup.getSelectedToggle() != null;
boolean isTriggerValueValid = marketAlertTriggerInputTextField.getValidator() != null &&
marketAlertTriggerInputTextField.getValidator().validate(marketAlertTriggerInputTextField.getText()).isValid;
boolean allInputsValid = isPaymentAccountSelected && isOfferTypeSelected && isTriggerValueValid;
paymentAccountsComboBox.setDisable(disabled);
buyOffersRadioButton.setDisable(disabled);
sellOffersRadioButton.setDisable(disabled);
marketAlertTriggerInputTextField.setDisable(disabled);
addMarketAlertButton.setDisable(disabled || !allInputsValid);
manageAlertsButton.setDisable(disabled || marketAlerts.getMarketAlertFilters().isEmpty());
}
// PriceAlert
private void fillPriceAlertFields() {
PriceAlertFilter priceAlertFilter = user.getPriceAlertFilter();
if (priceAlertFilter != null) {
String currencyCode = priceAlertFilter.getCurrencyCode();
Optional<TradeCurrency> optionalTradeCurrency = CurrencyUtil.getTradeCurrency(currencyCode);
if (optionalTradeCurrency.isPresent()) {
currencyComboBox.getSelectionModel().select(optionalTradeCurrency.get());
onSelectedTradeCurrency();
priceAlertHighInputTextField.setText(formatter.formatMarketPrice(priceAlertFilter.getHigh() / 10000d, currencyCode));
priceAlertLowInputTextField.setText(formatter.formatMarketPrice(priceAlertFilter.getLow() / 10000d, currencyCode));
} else {
currencyComboBox.getSelectionModel().clearSelection();
}
} else {
priceAlertHighInputTextField.clear();
priceAlertLowInputTextField.clear();
priceAlertHighInputTextField.resetValidation();
priceAlertLowInputTextField.resetValidation();
currencyComboBox.getSelectionModel().clearSelection();
}
}
private void updatePriceAlertFields() {
boolean setupConfirmationSent = mobileNotificationService.isSetupConfirmationSent();
boolean selected = priceCheckBox.isSelected();
boolean disable = !setupConfirmationSent ||
!selected;
priceAlertHighInputTextField.setDisable(selectedPriceAlertTradeCurrency == null || disable);
priceAlertLowInputTextField.setDisable(selectedPriceAlertTradeCurrency == null || disable);
PriceAlertFilter priceAlertFilter = user.getPriceAlertFilter();
boolean valueSameAsFilter = false;
if (priceAlertFilter != null &&
selectedPriceAlertTradeCurrency != null) {
valueSameAsFilter = priceAlertFilter.getHigh() == getPriceAsLong(priceAlertHighInputTextField) &&
priceAlertFilter.getLow() == getPriceAsLong(priceAlertLowInputTextField) &&
priceAlertFilter.getCurrencyCode().equals(selectedPriceAlertTradeCurrency);
}
setPriceAlertButton.setDisable(disable || !arePriceAlertInputsValid() || valueSameAsFilter);
removePriceAlertButton.setDisable(disable || priceAlertFilter == null);
currencyComboBox.setDisable(disable);
}
private boolean arePriceAlertInputsValid() {
return selectedPriceAlertTradeCurrency != null &&
isPriceInputValid(priceAlertHighInputTextField).isValid &&
isPriceInputValid(priceAlertLowInputTextField).isValid;
}
private InputValidator.ValidationResult isPriceInputValid(InputTextField inputTextField) {
InputValidator validator = inputTextField.getValidator();
if (validator != null)
return validator.validate(inputTextField.getText());
else
return new InputValidator.ValidationResult(false);
}
private long getPriceAsLong(InputTextField inputTextField) {
try {
String inputValue = inputTextField.getText();
if (inputValue != null && !inputValue.isEmpty() && selectedPriceAlertTradeCurrency != null) {
double priceAsDouble = formatter.parseNumberStringToDouble(inputValue);
String currencyCode = selectedPriceAlertTradeCurrency;
int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ?
Altcoin.SMALLEST_UNIT_EXPONENT : 2;
// We want to use the converted value not the inout value as we apply the converted value at focus out.
// E.g. if input is 5555.5555 it will be rounded to 5555.55 and we use that as the value for comparing
// low and high price...
String stringValue = formatter.formatRoundedDoubleWithPrecision(priceAsDouble, precision);
return formatter.parsePriceStringToLong(currencyCode, stringValue, precision);
} else {
return 0;
}
} catch (Throwable ignore) {
return 0;
}
}
private void applyPriceFormatting(InputTextField inputTextField) {
try {
String inputValue = inputTextField.getText();
if (inputValue != null && !inputValue.isEmpty() && selectedPriceAlertTradeCurrency != null) {
double priceAsDouble = formatter.parseNumberStringToDouble(inputValue);
String currencyCode = selectedPriceAlertTradeCurrency;
int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ?
Altcoin.SMALLEST_UNIT_EXPONENT : 2;
String stringValue = formatter.formatRoundedDoubleWithPrecision(priceAsDouble, precision);
inputTextField.setText(stringValue);
}
} catch (Throwable ignore) {
updatePriceAlertFields();
}
}
}

View File

@ -0,0 +1,24 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.account.content.notifications;
public class NoWebCamFoundException extends Throwable {
public NoWebCamFoundException(String msg) {
super(msg);
}
}

View File

@ -0,0 +1,97 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.account.content.notifications;
import bisq.common.UserThread;
import javafx.scene.image.ImageView;
import javafx.scene.image.WritableImage;
import java.awt.image.BufferedImage;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import com.github.sarxos.webcam.Webcam;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.Result;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.common.HybridBinarizer;
import javafx.embed.swing.SwingFXUtils;
@Slf4j
// Must not be UI thread
class QrCodeReader extends Thread {
private final Webcam webCam;
private final ImageView imageView;
private final Consumer<String> resultHandler;
private boolean isRunning;
QrCodeReader(Webcam webCam, ImageView imageView, Consumer<String> resultHandler) {
this.webCam = webCam;
this.imageView = imageView;
this.resultHandler = resultHandler;
start();
}
@Override
public void run() {
try {
if (!webCam.isOpen())
webCam.open();
isRunning = true;
Result result;
BufferedImage bufferedImage;
while (isRunning) {
bufferedImage = webCam.getImage();
if (bufferedImage != null) {
WritableImage writableImage = SwingFXUtils.toFXImage(bufferedImage, null);
imageView.setImage(writableImage);
LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
try {
result = new MultiFormatReader().decode(bitmap);
isRunning = false;
String qrCode = result.getText();
UserThread.execute(() -> resultHandler.accept(qrCode));
} catch (NotFoundException ignore) {
// No qr code in image...
}
}
}
} catch (Throwable t) {
log.error(t.toString());
} finally {
webCam.close();
}
}
public void close() {
isRunning = false;
}
}

View File

@ -0,0 +1,64 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.account.content.notifications;
import bisq.common.UserThread;
import bisq.common.handlers.ExceptionHandler;
import java.awt.Dimension;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
import com.github.sarxos.webcam.Webcam;
@Slf4j
// Must not be UI thread
class WebCamLauncher extends Thread {
private final Consumer<Webcam> resultHandler;
private final ExceptionHandler exceptionHandler;
WebCamLauncher(Consumer<Webcam> resultHandler, ExceptionHandler exceptionHandler) {
this.resultHandler = resultHandler;
this.exceptionHandler = exceptionHandler;
start();
}
@Override
public void run() {
try {
Webcam webCam = Webcam.getDefault(1000); // one second timeout - the default is too long
if (webCam != null) {
Dimension[] sizes = webCam.getViewSizes();
Dimension size = sizes[sizes.length - 1]; // the largest size
webCam.setViewSize(size);
UserThread.execute(() -> resultHandler.accept(webCam));
} else {
UserThread.execute(() -> exceptionHandler.handleException(new NoWebCamFoundException("No webcam found.")));
}
} catch (TimeoutException e) {
log.error(e.toString());
UserThread.execute(() -> exceptionHandler.handleException(e));
}
}
}

View File

@ -21,7 +21,7 @@
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>
<AnchorPane fx:id="root" prefHeight="660.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/8"
xmlns:fx="http://javafx.com/fxml/1"
xmlns:fx="http://javafx.com/fxml"
fx:controller="bisq.desktop.main.account.settings.AccountSettingsView">
<VBox fx:id="leftVBox" prefWidth="240" spacing="5" AnchorPane.bottomAnchor="20" AnchorPane.leftAnchor="15"

View File

@ -31,6 +31,7 @@ import bisq.desktop.main.account.content.altcoinaccounts.AltCoinAccountsView;
import bisq.desktop.main.account.content.arbitratorselection.ArbitratorSelectionView;
import bisq.desktop.main.account.content.backup.BackupView;
import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView;
import bisq.desktop.main.account.content.notifications.MobileNotificationsView;
import bisq.desktop.main.account.content.password.PasswordView;
import bisq.desktop.main.account.content.seedwords.SeedWordsView;
import bisq.desktop.util.Colors;
@ -63,7 +64,7 @@ public class AccountSettingsView extends ActivatableViewAndModel {
private final Navigation navigation;
private MenuItem paymentAccount, altCoinsAccountView, arbitratorSelection, password, seedWords, backup;
private MenuItem paymentAccount, altCoinsAccountView, arbitratorSelection, notifications, password, seedWords, backup;
private Navigation.Listener listener;
@FXML
@ -96,11 +97,12 @@ public class AccountSettingsView extends ActivatableViewAndModel {
altCoinsAccountView = new MenuItem(navigation, toggleGroup, Res.get("account.menu.altCoinsAccountView"), AltCoinAccountsView.class, AwesomeIcon.LINK);
arbitratorSelection = new MenuItem(navigation, toggleGroup, Res.get("account.menu.arbitratorSelection"),
ArbitratorSelectionView.class, AwesomeIcon.USER_MD);
notifications = new MenuItem(navigation, toggleGroup, Res.get("account.menu.notifications"), MobileNotificationsView.class, AwesomeIcon.BELL);
password = new MenuItem(navigation, toggleGroup, Res.get("account.menu.password"), PasswordView.class, AwesomeIcon.UNLOCK_ALT);
seedWords = new MenuItem(navigation, toggleGroup, Res.get("account.menu.seedWords"), SeedWordsView.class, AwesomeIcon.KEY);
backup = new MenuItem(navigation, toggleGroup, Res.get("account.menu.backup"), BackupView.class, AwesomeIcon.CLOUD_DOWNLOAD);
leftVBox.getChildren().addAll(paymentAccount, altCoinsAccountView, arbitratorSelection, password, seedWords, backup);
leftVBox.getChildren().addAll(paymentAccount, altCoinsAccountView, arbitratorSelection, notifications, password, seedWords, backup);
}
@Override
@ -108,6 +110,7 @@ public class AccountSettingsView extends ActivatableViewAndModel {
paymentAccount.activate();
altCoinsAccountView.activate();
arbitratorSelection.activate();
notifications.activate();
password.activate();
seedWords.activate();
backup.activate();
@ -133,6 +136,7 @@ public class AccountSettingsView extends ActivatableViewAndModel {
paymentAccount.deactivate();
altCoinsAccountView.deactivate();
arbitratorSelection.deactivate();
notifications.deactivate();
password.deactivate();
seedWords.deactivate();
backup.deactivate();
@ -145,6 +149,7 @@ public class AccountSettingsView extends ActivatableViewAndModel {
if (view instanceof FiatAccountsView) paymentAccount.setSelected(true);
else if (view instanceof AltCoinAccountsView) altCoinsAccountView.setSelected(true);
else if (view instanceof ArbitratorSelectionView) arbitratorSelection.setSelected(true);
else if (view instanceof MobileNotificationsView) notifications.setSelected(true);
else if (view instanceof PasswordView) password.setSelected(true);
else if (view instanceof SeedWordsView) seedWords.setSelected(true);
else if (view instanceof BackupView) backup.setSelected(true);

View File

@ -1152,7 +1152,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel> extends
HBox priceAsPercentageValueCurrencyBox = priceAsPercentageTuple.first;
marketBasedPriceInfoInputTextField = priceAsPercentageTuple.second;
marketBasedPriceTextField = marketBasedPriceInfoInputTextField.getTextField();
marketBasedPriceTextField = marketBasedPriceInfoInputTextField.getInputTextField();
marketBasedPriceTextField.setPrefWidth(200);
editOfferElements.add(marketBasedPriceTextField);
marketBasedPriceLabel = priceAsPercentageTuple.third;

View File

@ -31,15 +31,10 @@ import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static bisq.desktop.util.FormBuilder.addLabelCheckBox;
import static bisq.desktop.util.FormBuilder.addLabelTextArea;
public class ShowWalletDataWindow extends Overlay<ShowWalletDataWindow> {
private static final Logger log = LoggerFactory.getLogger(ShowWalletDataWindow.class);
private final WalletsManager walletsManager;

View File

@ -0,0 +1,95 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.main.overlays.windows;
import bisq.desktop.main.overlays.Overlay;
import bisq.desktop.util.FormBuilder;
import bisq.core.locale.Res;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.GridPane;
import javafx.geometry.HPos;
import javafx.geometry.Pos;
import javafx.beans.value.ChangeListener;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class WebCamWindow extends Overlay<WebCamWindow> {
@Getter
private ImageView imageView = new ImageView();
private ChangeListener<Image> listener;
public WebCamWindow(double width, double height) {
type = Type.Feedback;
imageView.setFitWidth(width);
imageView.setFitHeight(height);
}
public void show() {
headLine = Res.get("account.notifications.webCamWindow.headline");
createGridPane();
addHeadLine();
addSeparator();
addContent();
addCloseButton();
applyStyles();
display();
}
private void addContent() {
GridPane.setHalignment(headLineLabel, HPos.CENTER);
Label label = FormBuilder.addLabel(gridPane, ++rowIndex, Res.get("account.notifications.waitingForWebCam"));
label.setAlignment(Pos.CENTER);
GridPane.setColumnSpan(label, 2);
GridPane.setHalignment(label, HPos.CENTER);
GridPane.setRowIndex(imageView, rowIndex);
GridPane.setColumnSpan(imageView, 2);
gridPane.getChildren().add(imageView);
}
@Override
protected void addCloseButton() {
super.addCloseButton();
closeButton.setVisible(false);
listener = (observable, oldValue, newValue) -> closeButton.setVisible(newValue != null);
imageView.imageProperty().addListener(listener);
}
@Override
public void hide() {
super.hide();
if (listener != null)
imageView.imageProperty().removeListener(listener);
}
}

View File

@ -362,6 +362,27 @@ public class FormBuilder {
}
///////////////////////////////////////////////////////////////////////////////////////////
// Label + InfoInputTextField
///////////////////////////////////////////////////////////////////////////////////////////
public static Tuple2<Label, InfoInputTextField> addLabelInfoInputTextField(GridPane gridPane, int rowIndex, String title) {
return addLabelInfoInputTextField(gridPane, rowIndex, title, 0);
}
public static Tuple2<Label, InfoInputTextField> addLabelInfoInputTextField(GridPane gridPane, int rowIndex, String title, double top) {
Label label = addLabel(gridPane, rowIndex, title, top);
InfoInputTextField inputTextField = new InfoInputTextField();
GridPane.setRowIndex(inputTextField, rowIndex);
GridPane.setColumnIndex(inputTextField, 1);
GridPane.setMargin(inputTextField, new Insets(top, 0, 0, 0));
gridPane.getChildren().add(inputTextField);
return new Tuple2<>(label, inputTextField);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Label + PasswordField
///////////////////////////////////////////////////////////////////////////////////////////
@ -612,6 +633,7 @@ public class FormBuilder {
GridPane.setRowIndex(hBox, rowIndex);
GridPane.setColumnIndex(hBox, 1);
GridPane.setMargin(hBox, new Insets(-5, 0, 0, 0));
gridPane.getChildren().add(hBox);
return new Tuple3<>(label, radioButton1, radioButton2);
@ -955,7 +977,7 @@ public class FormBuilder {
///////////////////////////////////////////////////////////////////////////////////////////
// Label + Button
// Label + Button
///////////////////////////////////////////////////////////////////////////////////////////
public static Tuple2<Label, Button> addLabelButton(GridPane gridPane, int rowIndex, String labelText, String buttonTitle) {
@ -974,6 +996,26 @@ public class FormBuilder {
return new Tuple2<>(label, button);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Label + Button + Button
///////////////////////////////////////////////////////////////////////////////////////////
public static Tuple3<Label, Button, Button> addLabel2Buttons(GridPane gridPane, int rowIndex, String labelText, String title1, String title2, double top) {
Label label = addLabel(gridPane, rowIndex, labelText, top);
HBox hBox = new HBox();
hBox.setSpacing(10);
Button button1 = new AutoTooltipButton(title1);
button1.setDefaultButton(true);
Button button2 = new AutoTooltipButton(title2);
hBox.getChildren().addAll(button1, button2);
GridPane.setRowIndex(hBox, rowIndex);
GridPane.setColumnIndex(hBox, 1);
GridPane.setMargin(hBox, new Insets(top, 10, 0, 0));
gridPane.getChildren().add(hBox);
return new Tuple3<>(label, button1, button2);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Button
///////////////////////////////////////////////////////////////////////////////////////////
@ -1130,7 +1172,7 @@ public class FormBuilder {
public static Tuple3<HBox, InfoInputTextField, Label> getEditableValueCurrencyBoxWithInfo(String promptText) {
InfoInputTextField infoInputTextField = new InfoInputTextField();
InputTextField input = infoInputTextField.getTextField();
InputTextField input = infoInputTextField.getInputTextField();
input.setPrefWidth(170);
input.setAlignment(Pos.CENTER_RIGHT);
input.setId("text-input-with-currency-text-field");

View File

@ -0,0 +1,53 @@
/*
* This file is part of Bisq.
*
* Bisq 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.
*
* Bisq 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 Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.desktop.util.validation;
import bisq.core.locale.Res;
import lombok.Setter;
import javax.annotation.Nullable;
public class PercentageNumberValidator extends NumberValidator {
@Nullable
@Setter
protected Double maxValue; // Keep it Double as we check for null
@Override
public ValidationResult validate(String input) {
ValidationResult result = validateIfNotEmpty(input);
if (result.isValid) {
input = input.replace("%", "");
input = cleanInput(input);
result = validateIfNumber(input);
}
return result.and(validateIfNotExceedsMaxValue(input));
}
private ValidationResult validateIfNotExceedsMaxValue(String input) {
try {
double value = Double.parseDouble(input);
if (maxValue != null && value > maxValue)
return new ValidationResult(false, Res.get("validation.inputTooLarge", maxValue));
else
return new ValidationResult(true);
} catch (Throwable t) {
return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage()));
}
}
}

View File

@ -17,9 +17,6 @@
package bisq.desktop;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipLabel;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
@ -31,35 +28,33 @@ import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.Pane;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AwesomeFontDemo extends Application {
private static final Logger log = LoggerFactory.getLogger(AwesomeFontDemo.class);
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
Pane root = new FlowPane();
FlowPane flowPane = new FlowPane();
flowPane.setStyle("-fx-background-color: #ddd;");
flowPane.setHgap(2);
flowPane.setVgap(2);
List<AwesomeIcon> values = new ArrayList<>(Arrays.asList(AwesomeIcon.values()));
values.sort((o1, o2) -> o1.name().compareTo(o2.name()));
for (AwesomeIcon icon : values) {
Label label = new AutoTooltipLabel();
Button button = new AutoTooltipButton(icon.name(), label);
AwesomeDude.setIcon(label, icon);
root.getChildren().add(button);
Label label = new Label();
Button button = new Button(icon.name(), label);
button.setStyle("-fx-background-color: #fff;");
AwesomeDude.setIcon(label, icon, "12");
flowPane.getChildren().add(button);
}
primaryStage.setScene(new Scene(root, 1200, 950));
primaryStage.setScene(new Scene(flowPane, 1200, 950));
primaryStage.show();
}
}