rename all packages and other names from bisq to haveno

This commit is contained in:
woodser 2023-03-06 19:14:00 -05:00
parent ab0b9e3b77
commit 1a1fb130c0
1775 changed files with 14575 additions and 16767 deletions

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2008, 2013 Oracle and/or its affiliates.
* All rights reserved. Use is subject to license terms.
*
* This file is available and licensed under the following license:
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the distribution.
* - Neither the name of Oracle Corporation nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/* ====== CANDLE STICK CHART =========================================================== */
.candlestick-tooltip-label {
-fx-font-size: 0.75em;
-fx-font-weight: bold;
-fx-text-fill: #666666;
-fx-padding: 2 5 2 0;
}
.candlestick-average-line {
-fx-stroke: -bs-candle-stick-average-line;
-fx-stroke-width: 1px;
}
.candlestick-line {
-stick-line-fill: -bs-sell;
-fx-stroke: -stick-line-fill;
-fx-stroke-width: 1px;
}
.candlestick-line.close-above-open {
-stick-line-fill: -bs-candle-stick-won;
}
.candlestick-line.open-above-close {
-stick-line-fill: -bs-candle-stick-loss;
}
.candlestick-bar {
-fx-padding: 5;
-demo-bar-fill: -bs-sell;
-fx-background-color: -demo-bar-fill;
-fx-background-insets: 0;
}
.candlestick-bar.close-above-open {
-demo-bar-fill: -bs-candle-stick-won;
}
.candlestick-bar.open-above-close {
-demo-bar-fill: -bs-candle-stick-loss;
}
.candlestick-bar.empty {
-demo-bar-fill: #cccccc;
}
.volume-bar {
-fx-padding: 5;
-fx-background-color: -bs-volume-transparent;
-fx-background-insets: 0;
}
.chart-alternative-row-fill {
-fx-fill: transparent;
-fx-stroke: transparent;
-fx-stroke-width: 0;
}
.chart-plot-background {
-fx-background-color: -bs-background-color;
}

View file

@ -0,0 +1,48 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop;
import static haveno.common.config.Config.APP_NAME;
import com.google.inject.Singleton;
import com.google.inject.name.Names;
import haveno.common.app.AppModule;
import haveno.common.config.Config;
import haveno.core.locale.Res;
import haveno.desktop.common.fxml.FxmlViewLoader;
import haveno.desktop.common.view.ViewFactory;
import haveno.desktop.common.view.ViewLoader;
import haveno.desktop.common.view.guice.InjectorViewFactory;
import java.util.ResourceBundle;
public class DesktopModule extends AppModule {
public DesktopModule(Config config) {
super(config);
}
@Override
protected void configure() {
bind(ViewFactory.class).to(InjectorViewFactory.class);
bind(ResourceBundle.class).toInstance(Res.getResourceBundle());
bind(ViewLoader.class).to(FxmlViewLoader.class).in(Singleton.class);
bindConstant().annotatedWith(Names.named(APP_NAME)).to(config.appName);
}
}

View file

@ -0,0 +1,171 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop;
import com.google.inject.Inject;
import haveno.common.persistence.PersistenceManager;
import haveno.common.proto.persistable.NavigationPath;
import haveno.common.proto.persistable.PersistedDataHost;
import haveno.desktop.common.view.View;
import haveno.desktop.common.view.ViewPath;
import haveno.desktop.main.MainView;
import haveno.desktop.main.market.MarketView;
import javax.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
@Singleton
public final class Navigation implements PersistedDataHost {
private static final ViewPath DEFAULT_VIEW_PATH = ViewPath.to(MainView.class, MarketView.class);
public interface Listener {
void onNavigationRequested(ViewPath path, @Nullable Object data);
}
// New listeners can be added during iteration so we use CopyOnWriteArrayList to
// prevent invalid array modification
private final CopyOnWriteArraySet<Listener> listeners = new CopyOnWriteArraySet<>();
private final PersistenceManager<NavigationPath> persistenceManager;
private ViewPath currentPath;
// Used for returning to the last important view. After setup is done we want to
// return to the last opened view (e.g. sell/buy)
private ViewPath returnPath;
// this string is updated just before saving to disk so it reflects the latest currentPath situation.
private final NavigationPath navigationPath = new NavigationPath();
// Persisted fields
@Getter
@Setter
private ViewPath previousPath = DEFAULT_VIEW_PATH;
@Inject
public Navigation(PersistenceManager<NavigationPath> persistenceManager) {
this.persistenceManager = persistenceManager;
persistenceManager.initialize(navigationPath, PersistenceManager.Source.PRIVATE_LOW_PRIO);
}
@Override
public void readPersisted(Runnable completeHandler) {
persistenceManager.readPersisted(persisted -> {
List<Class<? extends View>> viewClasses = persisted.getPath().stream()
.map(className -> {
try {
return (Class<? extends View>) Class.forName(className).asSubclass(View.class);
} catch (ClassNotFoundException e) {
log.warn("Could not find the viewPath class {}; exception: {}", className, e);
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (!viewClasses.isEmpty()) {
previousPath = new ViewPath(viewClasses);
}
completeHandler.run();
},
completeHandler);
}
@SafeVarargs
public final void navigateTo(Class<? extends View>... viewClasses) {
navigateTo(ViewPath.to(viewClasses), null);
}
@SafeVarargs
public final void navigateToWithData(Object data, Class<? extends View>... viewClasses) {
navigateTo(ViewPath.to(viewClasses), data);
}
public void navigateTo(ViewPath newPath, @Nullable Object data) {
if (newPath == null)
return;
ArrayList<Class<? extends View>> temp = new ArrayList<>();
for (int i = 0; i < newPath.size(); i++) {
Class<? extends View> viewClass = newPath.get(i);
temp.add(viewClass);
if (currentPath == null ||
(currentPath.size() > i &&
viewClass != currentPath.get(i) &&
i != newPath.size() - 1)) {
ArrayList<Class<? extends View>> temp2 = new ArrayList<>(temp);
for (int n = i + 1; n < newPath.size(); n++) {
//noinspection unchecked
Class<? extends View>[] newTemp = new Class[i + 1];
currentPath = ViewPath.to(temp2.toArray(newTemp));
navigateTo(currentPath, data);
viewClass = newPath.get(n);
temp2.add(viewClass);
}
}
}
currentPath = newPath;
previousPath = currentPath;
listeners.forEach((e) -> e.onNavigationRequested(currentPath, data));
requestPersistence();
}
private void requestPersistence() {
if (currentPath.tip() != null) {
navigationPath.setPath(currentPath.stream().map(Class::getName).collect(Collectors.toUnmodifiableList()));
}
persistenceManager.requestPersistence();
}
public void navigateToPreviousVisitedView() {
if (previousPath == null || previousPath.size() == 0)
previousPath = DEFAULT_VIEW_PATH;
navigateTo(previousPath, null);
}
public void addListener(Listener listener) {
listeners.add(listener);
}
public void removeListener(Listener listener) {
listeners.remove(listener);
}
public ViewPath getReturnPath() {
return returnPath;
}
public ViewPath getCurrentPath() {
return currentPath;
}
public void setReturnPath(ViewPath returnPath) {
this.returnPath = returnPath;
}
}

View file

@ -0,0 +1,388 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.app;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.name.Names;
import haveno.common.app.DevEnv;
import haveno.common.app.Log;
import haveno.common.config.Config;
import haveno.common.setup.GracefulShutDownHandler;
import haveno.common.setup.UncaughtExceptionHandler;
import haveno.common.util.Utilities;
import haveno.core.btc.wallet.BtcWalletService;
import haveno.core.btc.wallet.WalletsManager;
import haveno.core.locale.Res;
import haveno.core.offer.OpenOffer;
import haveno.core.offer.OpenOfferManager;
import haveno.core.user.Cookie;
import haveno.core.user.CookieKey;
import haveno.core.user.Preferences;
import haveno.core.user.User;
import haveno.desktop.common.view.CachingViewLoader;
import haveno.desktop.common.view.View;
import haveno.desktop.common.view.ViewLoader;
import haveno.desktop.main.MainView;
import haveno.desktop.main.debug.DebugView;
import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.main.overlays.windows.BtcEmptyWalletWindow;
import haveno.desktop.main.overlays.windows.FilterWindow;
import haveno.desktop.main.overlays.windows.ManualPayoutTxWindow;
import haveno.desktop.main.overlays.windows.SendAlertMessageWindow;
import haveno.desktop.main.overlays.windows.ShowWalletDataWindow;
import haveno.desktop.util.CssTheme;
import haveno.desktop.util.ImageUtil;
import com.google.common.base.Joiner;
import javafx.application.Application;
import javafx.stage.Modality;
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.StackPane;
import javafx.geometry.BoundingBox;
import javafx.geometry.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import org.slf4j.LoggerFactory;
import static haveno.desktop.util.Layout.INITIAL_WINDOW_HEIGHT;
import static haveno.desktop.util.Layout.INITIAL_WINDOW_WIDTH;
import static haveno.desktop.util.Layout.MIN_WINDOW_HEIGHT;
import static haveno.desktop.util.Layout.MIN_WINDOW_WIDTH;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HavenoApp extends Application implements UncaughtExceptionHandler {
@Setter
private static Consumer<Application> appLaunchedHandler;
@Getter
private static Runnable shutDownHandler;
@Setter
private Injector injector;
@Setter
private GracefulShutDownHandler gracefulShutDownHandler;
private Stage stage;
private boolean popupOpened;
private Scene scene;
private boolean shutDownRequested;
private MainView mainView;
public HavenoApp() {
shutDownHandler = this::stop;
}
///////////////////////////////////////////////////////////////////////////////////////////
// JavaFx Application implementation
///////////////////////////////////////////////////////////////////////////////////////////
// NOTE: This method is not called on the JavaFX Application Thread.
@Override
public void init() {
}
@Override
public void start(Stage stage) {
this.stage = stage;
appLaunchedHandler.accept(this);
}
public void startApplication(Runnable onApplicationStartedHandler) {
log.info("Running startApplication...");
try {
mainView = loadMainView(injector);
mainView.setOnApplicationStartedHandler(onApplicationStartedHandler);
scene = createAndConfigScene(mainView, injector);
setupStage(scene);
} catch (Throwable throwable) {
log.error("Error during app init", throwable);
handleUncaughtException(throwable, false);
}
}
@Override
public void stop() {
if (!shutDownRequested) {
new Popup().headLine(Res.get("popup.shutDownInProgress.headline"))
.backgroundInfo(Res.get("popup.shutDownInProgress.msg"))
.hideCloseButton()
.useAnimation(false)
.show();
new Thread(() -> {
gracefulShutDownHandler.gracefulShutDown(() -> {
log.info("App shutdown complete");
});
}).start();
shutDownRequested = true;
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// UncaughtExceptionHandler implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void handleUncaughtException(Throwable throwable, boolean doShutDown) {
if (!shutDownRequested) {
if (scene == null) {
log.warn("Scene not available yet, we create a new scene. The bug might be caused by an exception in a constructor or by a circular dependency in Guice. throwable=" + throwable.toString());
scene = new Scene(new StackPane(), 1000, 650);
CssTheme.loadSceneStyles(scene, CssTheme.CSS_THEME_LIGHT, false);
stage.setScene(scene);
stage.show();
}
try {
try {
if (!popupOpened) {
popupOpened = true;
new Popup().error(Objects.requireNonNullElse(throwable.getMessage(), throwable.toString()))
.onClose(() -> popupOpened = false)
.show();
}
} catch (Throwable throwable3) {
log.error("Error at displaying Throwable.");
throwable3.printStackTrace();
}
if (doShutDown)
stop();
} catch (Throwable throwable2) {
// If printStackTrace cause a further exception we don't pass the throwable to the Popup.
log.error(throwable2.toString());
if (doShutDown)
stop();
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private Scene createAndConfigScene(MainView mainView, Injector injector) {
//Rectangle maxWindowBounds = new Rectangle();
Rectangle2D maxWindowBounds = new Rectangle2D(0, 0, 0, 0);
try {
maxWindowBounds = Screen.getPrimary().getBounds();
} catch (IllegalArgumentException e) {
// Multi-screen environments may encounter IllegalArgumentException (Window must not be zero)
// Just ignore the exception and continue, which means the window will use the minimum window size below
// since we are unable to determine if we can use a larger size
}
Scene scene = new Scene(mainView.getRoot(),
maxWindowBounds.getWidth() < INITIAL_WINDOW_WIDTH ?
Math.max(maxWindowBounds.getWidth(), MIN_WINDOW_WIDTH) :
INITIAL_WINDOW_WIDTH,
maxWindowBounds.getHeight() < INITIAL_WINDOW_HEIGHT ?
Math.max(maxWindowBounds.getHeight(), MIN_WINDOW_HEIGHT) :
INITIAL_WINDOW_HEIGHT);
addSceneKeyEventHandler(scene, injector);
Preferences preferences = injector.getInstance(Preferences.class);
var config = injector.getInstance(Config.class);
preferences.getCssThemeProperty().addListener((ov) -> {
CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), config.useDevModeHeader);
});
CssTheme.loadSceneStyles(scene, preferences.getCssTheme(), config.useDevModeHeader);
return scene;
}
private void setupStage(Scene scene) {
stage.setOnCloseRequest(event -> {
event.consume();
shutDownByUser();
});
// configure the primary stage
String appName = injector.getInstance(Key.get(String.class, Names.named(Config.APP_NAME)));
List<String> postFixes = new ArrayList<>();
if (!Config.baseCurrencyNetwork().isMainnet()) {
postFixes.add(Config.baseCurrencyNetwork().name());
}
if (injector.getInstance(Config.class).useLocalhostForP2P) {
postFixes.add("LOCALHOST");
}
if (injector.getInstance(Config.class).useDevMode) {
postFixes.add("DEV MODE");
}
if (!postFixes.isEmpty()) {
appName += " [" + Joiner.on(", ").join(postFixes) + " ]";
}
stage.setTitle(appName);
stage.setScene(scene);
stage.setMinWidth(MIN_WINDOW_WIDTH);
stage.setMinHeight(MIN_WINDOW_HEIGHT);
stage.getIcons().add(ImageUtil.getApplicationIconImage());
User user = injector.getInstance(User.class);
layoutStageFromPersistedData(stage, user);
addStageLayoutListeners(stage, user);
// make the UI visible
stage.show();
}
private void layoutStageFromPersistedData(Stage stage, User user) {
Cookie cookie = user.getCookie();
cookie.getAsOptionalDouble(CookieKey.STAGE_X).flatMap(x ->
cookie.getAsOptionalDouble(CookieKey.STAGE_Y).flatMap(y ->
cookie.getAsOptionalDouble(CookieKey.STAGE_W).flatMap(w ->
cookie.getAsOptionalDouble(CookieKey.STAGE_H).map(h -> new BoundingBox(x, y, w, h)))))
.ifPresent(stageBoundingBox -> {
stage.setX(stageBoundingBox.getMinX());
stage.setY(stageBoundingBox.getMinY());
stage.setWidth(stageBoundingBox.getWidth());
stage.setHeight(stageBoundingBox.getHeight());
});
}
private void addStageLayoutListeners(Stage stage, User user) {
stage.widthProperty().addListener((observable, oldValue, newValue) -> {
user.getCookie().putAsDouble(CookieKey.STAGE_W, (double) newValue);
user.requestPersistence();
});
stage.heightProperty().addListener((observable, oldValue, newValue) -> {
user.getCookie().putAsDouble(CookieKey.STAGE_H, (double) newValue);
user.requestPersistence();
});
stage.xProperty().addListener((observable, oldValue, newValue) -> {
user.getCookie().putAsDouble(CookieKey.STAGE_X, (double) newValue);
user.requestPersistence();
});
stage.yProperty().addListener((observable, oldValue, newValue) -> {
user.getCookie().putAsDouble(CookieKey.STAGE_Y, (double) newValue);
user.requestPersistence();
});
}
private MainView loadMainView(Injector injector) {
CachingViewLoader viewLoader = injector.getInstance(CachingViewLoader.class);
return (MainView) viewLoader.load(MainView.class);
}
private void addSceneKeyEventHandler(Scene scene, Injector injector) {
scene.addEventHandler(KeyEvent.KEY_RELEASED, keyEvent -> {
if (Utilities.isCtrlPressed(KeyCode.W, keyEvent) ||
Utilities.isCtrlPressed(KeyCode.Q, keyEvent)) {
shutDownByUser();
} else {
if (Utilities.isAltOrCtrlPressed(KeyCode.E, keyEvent)) {
injector.getInstance(BtcEmptyWalletWindow.class).show();
} else if (Utilities.isAltOrCtrlPressed(KeyCode.M, keyEvent)) {
injector.getInstance(SendAlertMessageWindow.class).show();
} else if (Utilities.isAltOrCtrlPressed(KeyCode.F, keyEvent)) {
injector.getInstance(FilterWindow.class).show();
}
else if (Utilities.isAltOrCtrlPressed(KeyCode.T, keyEvent)) {
// Toggle between show tor logs and only show warnings. Helpful in case of connection problems
String pattern = "org.berndpruenster.netlayer";
Level logLevel = ((Logger) LoggerFactory.getLogger(pattern)).getLevel();
if (logLevel != Level.DEBUG) {
log.info("Set log level for org.berndpruenster.netlayer classes to DEBUG");
Log.setCustomLogLevel(pattern, Level.DEBUG);
} else {
log.info("Set log level for org.berndpruenster.netlayer classes to WARN");
Log.setCustomLogLevel(pattern, Level.WARN);
}
} else if (Utilities.isAltOrCtrlPressed(KeyCode.J, keyEvent)) {
WalletsManager walletsManager = injector.getInstance(WalletsManager.class);
if (walletsManager.areWalletsAvailable())
new ShowWalletDataWindow(walletsManager).show();
else
new Popup().warning(Res.get("popup.warning.walletNotInitialized")).show();
} else if (Utilities.isAltOrCtrlPressed(KeyCode.G, keyEvent)) {
if (injector.getInstance(BtcWalletService.class).isWalletReady())
injector.getInstance(ManualPayoutTxWindow.class).show();
else
new Popup().warning(Res.get("popup.warning.walletNotInitialized")).show();
} else if (DevEnv.isDevMode()) {
if (Utilities.isAltOrCtrlPressed(KeyCode.Z, keyEvent))
showDebugWindow(scene, injector);
}
}
});
}
private void shutDownByUser() {
boolean hasOpenOffers = false;
for (OpenOffer openOffer : injector.getInstance(OpenOfferManager.class).getObservableList()) {
if (openOffer.getState().equals(OpenOffer.State.AVAILABLE)) {
hasOpenOffers = true;
break;
}
}
if (!hasOpenOffers) {
// No open offers, so no need to show the popup.
stop();
return;
}
// We show a popup to inform user that open offers will be removed if Haveno is not running.
String key = "showOpenOfferWarnPopupAtShutDown";
if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) {
new Popup().information(Res.get("popup.info.shutDownWithOpenOffers"))
.dontShowAgainId(key)
.useShutDownButton()
.closeButtonText(Res.get("shared.cancel"))
.show();
} else {
stop();
}
}
// Used for debugging trade process
private void showDebugWindow(Scene scene, Injector injector) {
ViewLoader viewLoader = injector.getInstance(ViewLoader.class);
View debugView = viewLoader.load(DebugView.class);
Parent parent = (Parent) debugView.getRoot();
Stage stage = new Stage();
stage.setScene(new Scene(parent));
stage.setTitle("Debug window"); // Don't translate, just for dev
stage.initModality(Modality.NONE);
stage.initStyle(StageStyle.UTILITY);
stage.initOwner(scene.getWindow());
stage.setX(this.stage.getX() + this.stage.getWidth() + 10);
stage.setY(this.stage.getY());
stage.show();
}
}

View file

@ -0,0 +1,238 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.app;
import haveno.common.UserThread;
import haveno.common.app.AppModule;
import haveno.common.app.Version;
import haveno.common.crypto.IncorrectPasswordException;
import haveno.core.app.AvoidStandbyModeService;
import haveno.core.app.HavenoExecutable;
import haveno.desktop.common.UITimer;
import haveno.desktop.common.view.guice.InjectorViewFactory;
import haveno.desktop.setup.DesktopPersistedDataHost;
import haveno.desktop.util.ImageUtil;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.stage.Stage;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.image.ImageView;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class HavenoAppMain extends HavenoExecutable {
public static final String DEFAULT_APP_NAME = "Haveno";
private HavenoApp application;
public HavenoAppMain() {
super("Haveno Desktop", "haveno-desktop", DEFAULT_APP_NAME, Version.VERSION);
}
public static void main(String[] args) {
// For some reason the JavaFX launch process results in us losing the thread
// context class loader: reset it. In order to work around a bug in JavaFX 8u25
// and below, you must include the following code as the first line of your
// realMain method:
Thread.currentThread().setContextClassLoader(HavenoAppMain.class.getClassLoader());
new HavenoAppMain().execute(args);
}
@Override
public void onSetupComplete() {
log.debug("onSetupComplete");
}
///////////////////////////////////////////////////////////////////////////////////////////
// First synchronous execution tasks
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void configUserThread() {
UserThread.setExecutor(Platform::runLater);
UserThread.setTimerClass(UITimer.class);
}
@Override
protected void launchApplication() {
HavenoApp.setAppLaunchedHandler(application -> {
HavenoAppMain.this.application = (HavenoApp) application;
// Map to user thread!
UserThread.execute(this::onApplicationLaunched);
});
Application.launch(HavenoApp.class);
}
///////////////////////////////////////////////////////////////////////////////////////////
// As application is a JavaFX application we need to wait for onApplicationLaunched
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected void onApplicationLaunched() {
super.onApplicationLaunched();
application.setGracefulShutDownHandler(this);
}
@Override
public void handleUncaughtException(Throwable throwable, boolean doShutDown) {
application.handleUncaughtException(throwable, doShutDown);
}
///////////////////////////////////////////////////////////////////////////////////////////
// We continue with a series of synchronous execution tasks
///////////////////////////////////////////////////////////////////////////////////////////
@Override
protected AppModule getModule() {
return new HavenoAppModule(config);
}
@Override
protected void applyInjector() {
super.applyInjector();
application.setInjector(injector);
injector.getInstance(InjectorViewFactory.class).setInjector(injector);
}
@Override
protected void readAllPersisted(Runnable completeHandler) {
super.readAllPersisted(DesktopPersistedDataHost.getPersistedDataHosts(injector), completeHandler);
}
@Override
protected void setupAvoidStandbyMode() {
injector.getInstance(AvoidStandbyModeService.class).init();
}
@Override
protected void startApplication() {
// We need to be in user thread! We mapped at launchApplication already. Once
// the UI is ready we get onApplicationStarted called and start the setup there.
application.startApplication(this::onApplicationStarted);
}
@Override
protected void onApplicationStarted() {
super.onApplicationStarted();
// Relevant to have this in the logs, for support cases
// This can only be called after JavaFX is initialized, otherwise the version logged will be null
// Therefore, calling this as part of onApplicationStarted()
log.info("Using JavaFX {}", System.getProperty("javafx.version"));
}
@Override
protected CompletableFuture<Boolean> loginAccount() {
// attempt default login
CompletableFuture<Boolean> result = super.loginAccount();
try {
if (result.get()) return result;
} catch (InterruptedException | ExecutionException e) {
throw new IllegalStateException(e);
}
// login using dialog
CompletableFuture<Boolean> dialogResult = new CompletableFuture<>();
Platform.setImplicitExit(false);
Platform.runLater(() -> {
// show password dialog until account open
String errorMessage = null;
while (!accountService.isAccountOpen()) {
// create the password dialog
PasswordDialog passwordDialog = new PasswordDialog(errorMessage);
// wait for user to enter password
Optional<String> passwordResult = passwordDialog.showAndWait();
if (passwordResult.isPresent()) {
try {
accountService.openAccount(passwordResult.get());
dialogResult.complete(accountService.isAccountOpen());
} catch (IncorrectPasswordException e) {
errorMessage = "Incorrect password";
}
} else {
// if the user cancelled the dialog, complete the passwordFuture exceptionally
dialogResult.completeExceptionally(new Exception("Password dialog cancelled"));
break;
}
}
});
return dialogResult;
}
private class PasswordDialog extends Dialog<String> {
public PasswordDialog(String errorMessage) {
setTitle("Enter Password");
setHeaderText("Please enter your Haveno password:");
// Add an icon to the dialog
Stage stage = (Stage) getDialogPane().getScene().getWindow();
stage.getIcons().add(ImageUtil.getImageByPath("lock.png"));
// Create the password field
PasswordField passwordField = new PasswordField();
passwordField.setPromptText("Password");
// Create the error message field
Label errorMessageField = new Label(errorMessage);
errorMessageField.setTextFill(Color.color(1, 0, 0));
// Set the dialog content
VBox vbox = new VBox(10);
vbox.getChildren().addAll(new ImageView(ImageUtil.getImageByPath("logo_splash.png")), passwordField, errorMessageField);
getDialogPane().setContent(vbox);
// Add OK and Cancel buttons
ButtonType okButton = new ButtonType("OK", ButtonBar.ButtonData.OK_DONE);
ButtonType cancelButton = new ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
getDialogPane().getButtonTypes().addAll(okButton, cancelButton);
// Convert the result to a string when the OK button is clicked
setResultConverter(buttonType -> {
if (buttonType == okButton) {
return passwordField.getText();
} else {
new Thread(() -> HavenoApp.getShutDownHandler().run()).start();
return null;
}
});
}
}
}

View file

@ -0,0 +1,36 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.app;
import haveno.common.app.AppModule;
import haveno.common.config.Config;
import haveno.core.app.CoreModule;
import haveno.desktop.DesktopModule;
public class HavenoAppModule extends AppModule {
public HavenoAppModule(Config config) {
super(config);
}
@Override
protected void configure() {
install(new CoreModule(config));
install(new DesktopModule(config));
}
}

View file

@ -0,0 +1,79 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common;
import javafx.application.Platform;
import haveno.common.Timer;
import haveno.common.UserThread;
import haveno.common.reactfx.FxTimer;
import java.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UITimer implements Timer {
private final Logger log = LoggerFactory.getLogger(UITimer.class);
private haveno.common.reactfx.Timer timer;
public UITimer() {
}
@Override
public Timer runLater(Duration delay, Runnable runnable) {
executeDirectlyIfPossible(() -> {
if (timer == null) {
timer = FxTimer.create(delay, runnable);
timer.restart();
} else {
log.warn("runLater called on an already running timer.");
}
});
return this;
}
@Override
public Timer runPeriodically(Duration interval, Runnable runnable) {
executeDirectlyIfPossible(() -> {
if (timer == null) {
timer = FxTimer.createPeriodic(interval, runnable);
timer.restart();
} else {
log.warn("runPeriodically called on an already running timer.");
}
});
return this;
}
@Override
public void stop() {
executeDirectlyIfPossible(() -> {
if (timer != null) {
timer.stop();
timer = null;
}
});
}
private void executeDirectlyIfPossible(Runnable runnable) {
if (Platform.isFxApplicationThread()) {
runnable.run();
} else {
UserThread.execute(runnable);
}
}
}

View file

@ -0,0 +1,30 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common;
import static java.lang.String.format;
public class ViewfxException extends RuntimeException {
public ViewfxException(Throwable cause, String format, Object... args) {
super(format(format, args), cause);
}
public ViewfxException(String format, Object... args) {
super(format(format, args));
}
}

View file

@ -0,0 +1,144 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.fxml;
import javax.inject.Inject;
import javax.inject.Singleton;
import com.google.common.base.Joiner;
import haveno.common.util.Utilities;
import haveno.desktop.common.ViewfxException;
import haveno.desktop.common.view.FxmlView;
import haveno.desktop.common.view.View;
import haveno.desktop.common.view.ViewFactory;
import haveno.desktop.common.view.ViewLoader;
import javafx.fxml.FXMLLoader;
import java.net.URL;
import java.io.IOException;
import java.util.ResourceBundle;
import java.lang.annotation.Annotation;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
@Slf4j
@Singleton
public class FxmlViewLoader implements ViewLoader {
private final ViewFactory viewFactory;
private final ResourceBundle resourceBundle;
@Inject
public FxmlViewLoader(ViewFactory viewFactory, ResourceBundle resourceBundle) {
this.viewFactory = viewFactory;
this.resourceBundle = resourceBundle;
}
@SuppressWarnings("unchecked")
public View load(Class<? extends View> viewClass) {
FxmlView fxmlView = viewClass.getAnnotation(FxmlView.class);
final Class<? extends FxmlView.PathConvention> convention;
final Class<? extends FxmlView.PathConvention> defaultConvention =
(Class<? extends FxmlView.PathConvention>) getDefaultValue(FxmlView.class, "convention");
final String specifiedLocation;
final String defaultLocation = (String) getDefaultValue(FxmlView.class, "location");
if (fxmlView == null) {
convention = defaultConvention;
specifiedLocation = defaultLocation;
} else {
convention = fxmlView.convention();
specifiedLocation = fxmlView.location();
}
if (convention == null || specifiedLocation == null)
throw new IllegalStateException("Convention and location should never be null.");
try {
final String resolvedLocation;
if (specifiedLocation.equals(defaultLocation))
resolvedLocation = convention.newInstance().apply(viewClass);
else
resolvedLocation = specifiedLocation;
URL fxmlUrl = viewClass.getClassLoader().getResource(resolvedLocation);
if (fxmlUrl == null)
throw new ViewfxException(
"Failed to load view class [%s] because FXML file at [%s] could not be loaded " +
"as a classpath resource. Does it exist?", viewClass, specifiedLocation);
return loadFromFxml(fxmlUrl);
} catch (InstantiationException | IllegalAccessException ex) {
throw new ViewfxException(ex, "Failed to load view from class %s", viewClass);
}
}
private View loadFromFxml(URL fxmlUrl) {
checkNotNull(fxmlUrl, "FXML URL must not be null");
try {
FXMLLoader loader = new FXMLLoader(fxmlUrl, resourceBundle);
loader.setControllerFactory(viewFactory);
loader.load();
Object controller = loader.getController();
if (controller == null)
throw new ViewfxException("Failed to load view from FXML file at [%s]. " +
"Does it declare an fx:controller attribute?", fxmlUrl);
if (!(controller instanceof View))
throw new ViewfxException("Controller of type [%s] loaded from FXML file at [%s] " +
"does not implement [%s] as expected.", controller.getClass(), fxmlUrl, View.class);
return (View) controller;
} catch (IOException ex) {
Throwable cause = ex.getCause();
if (cause != null) {
cause.printStackTrace();
log.error(cause.toString());
// We want to show stackTrace in error popup
String stackTrace = Utilities.toTruncatedString(Joiner.on("\n").join(cause.getStackTrace()), 800, false);
throw new ViewfxException(cause, "%s at loading view class\nStack trace:\n%s",
cause.getClass().getSimpleName(), stackTrace);
} else {
throw new ViewfxException(ex, "Failed to load view from FXML file at [%s]", fxmlUrl);
}
}
}
/**
* Copied and adapted from Spring Framework v4.3.6's AnnotationUtils#defaultValue
* method in order to make it possible to drop Haveno's dependency on Spring altogether.
*/
@SuppressWarnings("SameParameterValue")
private static Object getDefaultValue(Class<? extends Annotation> annotationType, String attributeName) {
if (annotationType == null || attributeName == null || attributeName.length() == 0) {
return null;
}
try {
return annotationType.getDeclaredMethod(attributeName).getDefaultValue();
} catch (Exception ex) {
return null;
}
}
}

View file

@ -0,0 +1,25 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.model;
public interface Activatable {
void _activate();
void _deactivate();
}

View file

@ -0,0 +1,41 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class ActivatableDataModel implements Activatable, DataModel {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public final void _activate() {
this.activate();
}
protected void activate() {
}
@Override
public final void _deactivate() {
this.deactivate();
}
protected void deactivate() {
}
}

View file

@ -0,0 +1,41 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class ActivatableViewModel implements Activatable, ViewModel {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
@Override
public final void _activate() {
this.activate();
}
protected void activate() {
}
@Override
public final void _deactivate() {
this.deactivate();
}
protected void deactivate() {
}
}

View file

@ -0,0 +1,43 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.model;
public class ActivatableWithDataModel<D extends Activatable> extends WithDataModel<D> implements Activatable {
public ActivatableWithDataModel(D dataModel) {
super(dataModel);
}
@Override
public final void _activate() {
dataModel._activate();
this.activate();
}
protected void activate() {
}
@Override
public final void _deactivate() {
dataModel._deactivate();
this.deactivate();
}
protected void deactivate() {
}
}

View file

@ -0,0 +1,21 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.model;
public interface DataModel extends Model {
}

View file

@ -0,0 +1,21 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.model;
public interface Model {
}

View file

@ -0,0 +1,21 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.model;
public interface ViewModel extends Model {
}

View file

@ -0,0 +1,33 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkNotNull;
public class WithDataModel<D> {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
public final D dataModel;
protected WithDataModel(D dataModel) {
this.dataModel = checkNotNull(dataModel, "Delegate object must not be null");
}
}

View file

@ -0,0 +1,47 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view;
import javafx.fxml.FXML;
import javafx.scene.Node;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AbstractView<R extends Node, M> implements View {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
protected
@FXML
R root;
protected final M model;
public AbstractView(M model) {
this.model = model;
}
public AbstractView() {
this(null);
}
public R getRoot() {
return root;
}
}

View file

@ -0,0 +1,49 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view;
import javafx.scene.Node;
public abstract class ActivatableView<R extends Node, M> extends InitializableView<R, M> {
public ActivatableView(M model) {
super(model);
}
public ActivatableView() {
this(null);
}
@Override
protected void prepareInitialize() {
if (root != null) {
root.sceneProperty().addListener((ov, oldValue, newValue) -> {
if (oldValue == null && newValue != null)
activate();
else if (oldValue != null && newValue == null)
deactivate();
});
}
}
protected void activate() {
}
protected void deactivate() {
}
}

View file

@ -0,0 +1,46 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view;
import javafx.scene.Node;
import static com.google.common.base.Preconditions.checkNotNull;
import haveno.desktop.common.model.Activatable;
public abstract class ActivatableViewAndModel<R extends Node, M extends Activatable> extends ActivatableView<R, M> {
public ActivatableViewAndModel(M model) {
super(checkNotNull(model, "Model must not be null"));
}
@Override
protected void prepareInitialize() {
if (root != null) {
root.sceneProperty().addListener((ov, oldValue, newValue) -> {
if (oldValue == null && newValue != null) {
model._activate();
activate();
} else if (oldValue != null && newValue == null) {
model._deactivate();
deactivate();
}
});
}
}
}

View file

@ -0,0 +1,50 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.HashMap;
import java.util.Map;
@Singleton
public class CachingViewLoader implements ViewLoader {
private final Map<Class<? extends View>, View> cache = new HashMap<>();
private final ViewLoader viewLoader;
@Inject
public CachingViewLoader(ViewLoader viewLoader) {
this.viewLoader = viewLoader;
}
@Override
public View load(Class<? extends View> viewClass) {
if (cache.containsKey(viewClass))
return cache.get(viewClass);
View view = viewLoader.load(viewClass);
cache.put(viewClass, view);
return view;
}
public void removeFromCache(Class<? extends View> viewClass) {
cache.remove(viewClass);
}
}

View file

@ -0,0 +1,30 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view;
public class DefaultPathConvention implements FxmlView.PathConvention {
/**
* Convert a '.'-based fully-qualified name of {@code viewClass} to a '/'-based
* resource path suffixed with ".fxml".
*/
@Override
public String apply(Class<? extends View> viewClass) {
return viewClass.getName().replace('.', '/').concat(".fxml");
}
}

View file

@ -0,0 +1,46 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view;
import java.util.function.Function;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface FxmlView {
/**
* The location of the FXML file associated with annotated {@link View} class. By default the location will be
* determined by {@link #convention()}.
*/
String location() default "";
/**
* The function used to determine the location of the FXML file associated with the annotated {@link View} class.
* By default it is the fully-qualified view class name, converted to a resource path, replacing the
* {@code .class} suffix replaced with {@code .fxml}.
*/
Class<? extends PathConvention> convention() default DefaultPathConvention.class;
interface PathConvention extends Function<Class<? extends View>, String> {
}
}

View file

@ -0,0 +1,49 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import java.net.URL;
import java.util.ResourceBundle;
public abstract class InitializableView<R extends Node, M> extends AbstractView<R, M> implements Initializable {
public InitializableView(M model) {
super(model);
}
public InitializableView() {
this(null);
}
@Override
public final void initialize(URL location, ResourceBundle resources) {
prepareInitialize();
initialize();
}
protected void prepareInitialize() {
}
protected void initialize() {
}
}

View file

@ -0,0 +1,24 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view;
import javafx.scene.Node;
public interface View {
Node getRoot();
}

View file

@ -0,0 +1,23 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view;
import javafx.util.Callback;
public interface ViewFactory extends Callback<Class<?>, Object> {
}

View file

@ -0,0 +1,22 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view;
public interface ViewLoader {
View load(Class<? extends View> viewClass);
}

View file

@ -0,0 +1,53 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
public final class ViewPath extends ArrayList<Class<? extends View>> {
private ViewPath() {
}
public ViewPath(Collection<? extends Class<? extends View>> c) {
super(c);
}
@SafeVarargs
public static ViewPath to(Class<? extends View>... elements) {
ViewPath path = new ViewPath();
List<Class<? extends View>> list = Arrays.asList(elements);
path.addAll(list);
return path;
}
public static ViewPath from(ViewPath original) {
ViewPath path = new ViewPath();
path.addAll(original);
return path;
}
public Class<? extends View> tip() {
if (size() == 0)
return null;
return get(size() - 1);
}
}

View file

@ -0,0 +1,40 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.common.view.guice;
import com.google.inject.Injector;
import haveno.desktop.common.view.ViewFactory;
import javax.inject.Singleton;
import com.google.common.base.Preconditions;
@Singleton
public class InjectorViewFactory implements ViewFactory {
private Injector injector;
public void setInjector(Injector injector) {
this.injector = injector;
}
@Override
public Object call(Class<?> aClass) {
Preconditions.checkNotNull(injector, "Injector has not yet been provided");
return injector.getInstance(aClass);
}
}

View file

@ -0,0 +1,159 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
import haveno.common.UserThread;
import haveno.core.account.sign.SignedWitnessService;
import haveno.core.locale.Res;
import haveno.core.offer.OfferRestrictions;
import haveno.core.trade.HavenoUtils;
import haveno.desktop.components.controlsfx.control.PopOver;
import haveno.desktop.main.offer.offerbook.OfferBookListItem;
import haveno.desktop.util.FormBuilder;
import haveno.desktop.util.GUIUtil;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import java.util.concurrent.TimeUnit;
public class AccountStatusTooltipLabel extends AutoTooltipLabel {
public static final int DEFAULT_WIDTH = 300;
private final Node textIcon;
private final OfferBookListItem.WitnessAgeData witnessAgeData;
private final String popupTitle;
private PopOver popOver;
private boolean keepPopOverVisible = false;
public AccountStatusTooltipLabel(OfferBookListItem.WitnessAgeData witnessAgeData) {
super(witnessAgeData.getDisplayString());
this.witnessAgeData = witnessAgeData;
this.textIcon = FormBuilder.getIcon(witnessAgeData.getIcon());
this.popupTitle = witnessAgeData.isLimitLifted()
? Res.get("offerbook.timeSinceSigning.tooltip.accountLimitLifted")
: Res.get("offerbook.timeSinceSigning.tooltip.accountLimit", HavenoUtils.formatToXmrWithCode(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT));
positionAndActivateIcon();
}
private void positionAndActivateIcon() {
textIcon.setOpacity(0.4);
textIcon.getStyleClass().add("tooltip-icon");
popOver = createPopOver();
textIcon.setOnMouseEntered(e -> showPopup(textIcon));
textIcon.setOnMouseExited(e -> UserThread.runAfter(() -> {
if (!keepPopOverVisible) {
popOver.hide();
}
}, 200, TimeUnit.MILLISECONDS)
);
setGraphic(textIcon);
setContentDisplay(ContentDisplay.RIGHT);
}
private PopOver createPopOver() {
Label titleLabel = new Label(popupTitle);
titleLabel.setMaxWidth(DEFAULT_WIDTH);
titleLabel.setWrapText(true);
titleLabel.setPadding(new Insets(10, 10, 0, 10));
titleLabel.getStyleClass().add("account-status-title");
Label infoLabel = new Label(witnessAgeData.getInfo());
infoLabel.setMaxWidth(DEFAULT_WIDTH);
infoLabel.setWrapText(true);
infoLabel.setPadding(new Insets(0, 10, 4, 10));
infoLabel.getStyleClass().add("small-text");
Label buyLabel = createDetailsItem(
Res.get("offerbook.timeSinceSigning.tooltip.checkmark.buyBtc"),
witnessAgeData.isAccountSigned()
);
Label waitLabel = createDetailsItem(
Res.get("offerbook.timeSinceSigning.tooltip.checkmark.wait", SignedWitnessService.SIGNER_AGE_DAYS),
witnessAgeData.isLimitLifted()
);
Hyperlink learnMoreLink = new ExternalHyperlink(Res.get("offerbook.timeSinceSigning.tooltip.learnMore"),
null,
"0.769em");
learnMoreLink.setMaxWidth(DEFAULT_WIDTH);
learnMoreLink.setWrapText(true);
learnMoreLink.setPadding(new Insets(10, 10, 2, 10));
learnMoreLink.getStyleClass().addAll("very-small-text");
learnMoreLink.setOnAction((e) -> GUIUtil.openWebPage("https://bisq.wiki/Account_limits"));
VBox vBox = new VBox(2, titleLabel, infoLabel, buyLabel, waitLabel, learnMoreLink);
vBox.setPadding(new Insets(2, 0, 2, 0));
vBox.setAlignment(Pos.CENTER_LEFT);
PopOver popOver = new PopOver(vBox);
popOver.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER);
vBox.setOnMouseEntered(mouseEvent -> keepPopOverVisible = true);
vBox.setOnMouseExited(mouseEvent -> {
keepPopOverVisible = false;
popOver.hide();
});
return popOver;
}
private void showPopup(Node textIcon) {
Bounds bounds = textIcon.localToScreen(textIcon.getBoundsInLocal());
popOver.show(textIcon, bounds.getMaxX() + 10, (bounds.getMinY() + bounds.getHeight() / 2) - 10);
}
private Label createDetailsItem(String text, boolean active) {
Label label = new Label(text);
label.setMaxWidth(DEFAULT_WIDTH);
label.setWrapText(true);
label.setPadding(new Insets(0, 10, 0, 10));
label.getStyleClass().add("small-text");
if (active) {
label.setStyle("-fx-text-fill: -fx-accent");
} else {
label.setStyle("-fx-text-fill: -bs-color-gray-dim");
}
Text icon = FormBuilder.getSmallIconForLabel(active ?
MaterialDesignIcon.CHECKBOX_MARKED_CIRCLE : MaterialDesignIcon.CLOSE_CIRCLE, label);
icon.setLayoutY(4);
if (active) {
icon.getStyleClass().add("account-status-active-info-item");
} else {
icon.getStyleClass().add("account-status-inactive-info-item");
}
return label;
}
}

View file

@ -0,0 +1,164 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import haveno.common.util.Utilities;
import haveno.core.locale.Res;
import haveno.core.trade.HavenoUtils;
import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.util.GUIUtil;
import com.jfoenix.controls.JFXTextField;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.AnchorPane;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import java.math.BigInteger;
import java.net.URI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AddressTextField extends AnchorPane {
private static final Logger log = LoggerFactory.getLogger(AddressTextField.class);
private final StringProperty address = new SimpleStringProperty();
private final StringProperty paymentLabel = new SimpleStringProperty();
private final ObjectProperty<BigInteger> amount = new SimpleObjectProperty<>(BigInteger.ZERO);
private boolean wasPrimaryButtonDown;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public AddressTextField(String label) {
JFXTextField textField = new HavenoTextField();
textField.setId("address-text-field");
textField.setEditable(false);
textField.setLabelFloat(true);
textField.setPromptText(label);
textField.textProperty().bind(address);
String tooltipText = Res.get("addressTextField.openWallet");
textField.setTooltip(new Tooltip(tooltipText));
textField.setOnMousePressed(event -> wasPrimaryButtonDown = event.isPrimaryButtonDown());
textField.setOnMouseReleased(event -> {
if (wasPrimaryButtonDown) openWallet();
wasPrimaryButtonDown = false;
});
textField.focusTraversableProperty().set(focusTraversableProperty().get());
Label extWalletIcon = new Label();
extWalletIcon.setLayoutY(3);
extWalletIcon.getStyleClass().addAll("icon", "highlight");
extWalletIcon.setTooltip(new Tooltip(tooltipText));
AwesomeDude.setIcon(extWalletIcon, AwesomeIcon.SIGNIN);
extWalletIcon.setOnMouseClicked(e -> openWallet());
Label copyIcon = new Label();
copyIcon.setLayoutY(3);
copyIcon.getStyleClass().addAll("icon", "highlight");
Tooltip.install(copyIcon, new Tooltip(Res.get("addressTextField.copyToClipboard")));
AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY);
copyIcon.setOnMouseClicked(e -> {
if (address.get() != null && address.get().length() > 0)
Utilities.copyToClipboard(address.get());
});
AnchorPane.setRightAnchor(copyIcon, 30.0);
AnchorPane.setRightAnchor(extWalletIcon, 5.0);
AnchorPane.setRightAnchor(textField, 55.0);
AnchorPane.setLeftAnchor(textField, 0.0);
getChildren().addAll(textField, copyIcon, extWalletIcon);
}
private void openWallet() {
try {
Utilities.openURI(URI.create(getBitcoinURI()));
} catch (Exception e) {
log.warn(e.getMessage());
new Popup().warning(Res.get("addressTextField.openWallet.failed")).show();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters/Setters
///////////////////////////////////////////////////////////////////////////////////////////
public void setAddress(String address) {
this.address.set(address);
}
public String getAddress() {
return address.get();
}
public StringProperty addressProperty() {
return address;
}
public BigInteger getAmount() {
return amount.get();
}
public ObjectProperty<BigInteger> amountAsProperty() {
return amount;
}
public void setAmount(BigInteger amount) {
this.amount.set(amount);
}
public String getPaymentLabel() {
return paymentLabel.get();
}
public StringProperty paymentLabelProperty() {
return paymentLabel;
}
public void setPaymentLabel(String paymentLabel) {
this.paymentLabel.set(paymentLabel);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private String getBitcoinURI() {
if (amount.get().compareTo(BigInteger.valueOf(0)) < 0) {
log.warn("Amount must not be negative");
setAmount(BigInteger.valueOf(0));
}
return GUIUtil.getBitcoinURI(address.get(), HavenoUtils.atomicUnitsToCoin(amount.get()),
paymentLabel.get());
}
}

View file

@ -0,0 +1,75 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AddressWithIconAndDirection extends HBox {
private static final Logger log = LoggerFactory.getLogger(AddressWithIconAndDirection.class);
private final Hyperlink hyperlink;
public AddressWithIconAndDirection(String text, String address, boolean received) {
Label directionIcon = new Label();
directionIcon.getStyleClass().add("icon");
directionIcon.getStyleClass().add(received ? "received-funds-icon" : "sent-funds-icon");
AwesomeDude.setIcon(directionIcon, received ? AwesomeIcon.SIGNIN : AwesomeIcon.SIGNOUT);
if (received)
directionIcon.setRotate(180);
directionIcon.setMouseTransparent(true);
setAlignment(Pos.CENTER_LEFT);
Label label = new AutoTooltipLabel(text);
label.setMouseTransparent(true);
HBox.setMargin(directionIcon, new Insets(0, 3, 0, 0));
HBox.setHgrow(label, Priority.ALWAYS);
hyperlink = new ExternalHyperlink(address);
HBox.setMargin(hyperlink, new Insets(0));
HBox.setHgrow(hyperlink, Priority.SOMETIMES);
// You need to set max width to Double.MAX_VALUE to make HBox.setHgrow working like expected!
// also pref width needs to be not default (-1)
hyperlink.setMaxWidth(Double.MAX_VALUE);
hyperlink.setPrefWidth(0);
getChildren().addAll(directionIcon, label, hyperlink);
}
public void setOnAction(EventHandler<ActionEvent> handler) {
hyperlink.setOnAction(handler);
}
public void setTooltip(Tooltip tooltip) {
hyperlink.setTooltip(tooltip);
}
}

View file

@ -0,0 +1,62 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.skins.JFXButtonSkin;
import javafx.scene.Node;
import javafx.scene.control.Skin;
public class AutoTooltipButton extends JFXButton {
public AutoTooltipButton() {
super();
}
public AutoTooltipButton(String text) {
super(text.toUpperCase());
}
public AutoTooltipButton(String text, Node graphic) {
super(text.toUpperCase(), graphic);
}
public void updateText(String text) {
setText(text.toUpperCase());
}
@Override
protected Skin<?> createDefaultSkin() {
return new AutoTooltipButtonSkin(this);
}
private class AutoTooltipButtonSkin extends JFXButtonSkin {
public AutoTooltipButtonSkin(JFXButton button) {
super(button);
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
showTooltipIfTruncated(this, getSkinnable());
}
}
}

View file

@ -0,0 +1,53 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.skins.JFXCheckBoxSkin;
import javafx.scene.control.Skin;
public class AutoTooltipCheckBox extends JFXCheckBox {
public AutoTooltipCheckBox() {
super();
}
public AutoTooltipCheckBox(String text) {
super(text);
}
@Override
protected Skin<?> createDefaultSkin() {
return new AutoTooltipCheckBoxSkin(this);
}
private class AutoTooltipCheckBoxSkin extends JFXCheckBoxSkin {
public AutoTooltipCheckBoxSkin(JFXCheckBox checkBox) {
super(checkBox);
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
showTooltipIfTruncated(this, getSkinnable());
}
}
}

View file

@ -0,0 +1,53 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.skin.LabelSkin;
public class AutoTooltipLabel extends Label {
public AutoTooltipLabel() {
super();
}
public AutoTooltipLabel(String text) {
super(text);
}
@Override
protected Skin<?> createDefaultSkin() {
return new AutoTooltipLabelSkin(this);
}
private class AutoTooltipLabelSkin extends LabelSkin {
public AutoTooltipLabelSkin(Label label) {
super(label);
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
showTooltipIfTruncated(this, getSkinnable());
}
}
}

View file

@ -0,0 +1,52 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated;
import com.jfoenix.controls.JFXRadioButton;
import javafx.scene.control.Skin;
public class AutoTooltipRadioButton extends JFXRadioButton {
public AutoTooltipRadioButton() {
super();
}
public AutoTooltipRadioButton(String text) {
super(text);
}
@Override
protected Skin<?> createDefaultSkin() {
return new AutoTooltipRadioButtonSkin(this);
}
private class AutoTooltipRadioButtonSkin extends JFXRadioButtonSkinHavenoStyle {
public AutoTooltipRadioButtonSkin(JFXRadioButton radioButton) {
super(radioButton);
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
showTooltipIfTruncated(this, getSkinnable());
}
}
}

View file

@ -0,0 +1,31 @@
package haveno.desktop.components;
import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated;
import com.jfoenix.controls.JFXToggleButton;
import com.jfoenix.skins.JFXToggleButtonSkin;
import javafx.scene.control.Skin;
public class AutoTooltipSlideToggleButton extends JFXToggleButton {
public AutoTooltipSlideToggleButton() {
super();
}
@Override
protected Skin<?> createDefaultSkin() {
return new AutoTooltipSlideToggleButton.AutoTooltipSlideToggleButtonSkin(this);
}
private class AutoTooltipSlideToggleButtonSkin extends JFXToggleButtonSkin {
public AutoTooltipSlideToggleButtonSkin(JFXToggleButton toggleButton) {
super(toggleButton);
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
showTooltipIfTruncated(this, getSkinnable());
}
}
}

View file

@ -0,0 +1,80 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.fontawesome.AwesomeIcon;
import haveno.desktop.components.controlsfx.control.PopOver;
import haveno.desktop.util.FormBuilder;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.layout.HBox;
public class AutoTooltipTableColumn<S, T> extends TableColumn<S, T> {
private Label helpIcon;
private PopOverWrapper popoverWrapper = new PopOverWrapper();
public AutoTooltipTableColumn(String text) {
super();
setTitle(text);
}
public AutoTooltipTableColumn(String text, String help) {
setTitleWithHelpText(text, help);
}
public void setTitle(String title) {
setGraphic(new AutoTooltipLabel(title));
}
public void setTitleWithHelpText(String title, String help) {
helpIcon = FormBuilder.getSmallIcon(AwesomeIcon.QUESTION_SIGN);
helpIcon.setOpacity(0.4);
helpIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createInfoPopOver(help)));
helpIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver());
final AutoTooltipLabel label = new AutoTooltipLabel(title);
final HBox hBox = new HBox(label, helpIcon);
hBox.setStyle("-fx-alignment: center-left");
hBox.setSpacing(4);
setGraphic(hBox);
}
private PopOver createInfoPopOver(String help) {
Label helpLabel = new Label(help);
helpLabel.setMaxWidth(300);
helpLabel.setWrapText(true);
return createInfoPopOver(helpLabel);
}
private PopOver createInfoPopOver(Node node) {
node.getStyleClass().add("default-text");
PopOver infoPopover = new PopOver(node);
if (helpIcon.getScene() != null) {
infoPopover.setDetachable(false);
infoPopover.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER);
infoPopover.show(helpIcon, -10);
}
return infoPopover;
}
}

View file

@ -0,0 +1,57 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import static haveno.desktop.components.TooltipUtil.showTooltipIfTruncated;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.skin.ToggleButtonSkin;
public class AutoTooltipToggleButton extends ToggleButton {
public AutoTooltipToggleButton() {
super();
}
public AutoTooltipToggleButton(String text) {
super(text);
}
public AutoTooltipToggleButton(String text, Node graphic) {
super(text, graphic);
}
@Override
protected Skin<?> createDefaultSkin() {
return new AutoTooltipToggleButtonSkin(this);
}
private class AutoTooltipToggleButtonSkin extends ToggleButtonSkin {
public AutoTooltipToggleButtonSkin(ToggleButton toggleButton) {
super(toggleButton);
}
@Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
showTooltipIfTruncated(this, getSkinnable());
}
}
}

View file

@ -0,0 +1,202 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import org.apache.commons.lang3.StringUtils;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.skins.JFXComboBoxListViewSkin;
import haveno.common.UserThread;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Implements searchable dropdown (an autocomplete like experience).
*
* Clients must use setAutocompleteItems() instead of setItems().
*
* @param <T> type of the ComboBox item; in the simplest case this can be a String
*/
public class AutocompleteComboBox<T> extends JFXComboBox<T> {
private List<? extends T> list;
private List<? extends T> extendedList;
private List<T> matchingList;
private JFXComboBoxListViewSkin<T> comboBoxListViewSkin;
public AutocompleteComboBox() {
this(FXCollections.observableArrayList());
}
private AutocompleteComboBox(ObservableList<T> items) {
super(items);
setEditable(true);
clearOnFocus();
setEmptySkinToGetMoreControlOverListView();
fixSpaceKey();
setAutocompleteItems(items);
reactToQueryChanges();
}
/**
* Set the complete list of ComboBox items. Use this instead of setItems().
*/
public void setAutocompleteItems(List<? extends T> items, List<? extends T> allItems) {
list = items;
extendedList = allItems;
matchingList = new ArrayList<>(list);
setValue(null);
getSelectionModel().clearSelection();
setItems(FXCollections.observableList(matchingList));
getEditor().setText("");
}
public void setAutocompleteItems(List<? extends T> items) {
setAutocompleteItems(items, null);
}
/**
* Triggered when value change is *confirmed*. In practical terms
* this is when user clicks item on the dropdown or hits [ENTER]
* while typing in the text.
*
* This is in contrast to onAction event that is triggered
* on every (unconfirmed) value change. The onAction is not really
* suitable for the search enabled ComboBox.
*/
public final void setOnChangeConfirmed(EventHandler<Event> eh) {
setOnHidden(e -> {
var inputText = getEditor().getText();
// Case 1: fire if input text selects (matches) an item
var selectedItem = getSelectionModel().getSelectedItem();
var inputTextItem = getConverter().fromString(inputText);
if (selectedItem != null && selectedItem.equals(inputTextItem)) {
eh.handle(e);
getParent().requestFocus();
return;
}
// Case 2: fire if the text is empty to support special "show all" case
if (inputText.isEmpty()) {
eh.handle(e);
getParent().requestFocus();
}
});
}
// Clear selection and query when ComboBox gets new focus. This is usually what user
// wants - to have a blank slate for a new search. The primary motivation though
// was to work around UX glitches related to (starting) editing text when combobox
// had specific item selected.
private void clearOnFocus() {
getEditor().focusedProperty().addListener((observableValue, hadFocus, hasFocus) -> {
if (!hadFocus && hasFocus) {
removeFilter();
forceRedraw();
}
});
}
// The ComboBox API does not provide enough control over the underlying
// ListView that is used as a dropdown. The only way to get this control
// is to set custom ListViewSkin. The default skin is null and so useless.
private void setEmptySkinToGetMoreControlOverListView() {
comboBoxListViewSkin = new JFXComboBoxListViewSkin<>(this);
setSkin(comboBoxListViewSkin);
}
// By default pressing [SPACE] caused editor text to reset. The solution
// is to suppress relevant event on the underlying ListViewSkin.
private void fixSpaceKey() {
comboBoxListViewSkin.getPopupContent().addEventFilter(KeyEvent.ANY, (KeyEvent event) -> {
if (event.getCode() == KeyCode.SPACE)
event.consume();
});
}
private void filterBy(String query) {
matchingList = (extendedList != null && query.length() > 0 ? extendedList : list)
.stream()
.filter(item -> StringUtils.containsIgnoreCase(asString(item), query))
.collect(Collectors.toList());
setValue(null);
getSelectionModel().clearSelection();
setItems(FXCollections.observableList(matchingList));
int pos = getEditor().getCaretPosition();
if (pos > query.length()) pos = query.length();
getEditor().setText(query);
getEditor().positionCaret(pos);
}
private void reactToQueryChanges() {
getEditor().addEventHandler(KeyEvent.KEY_RELEASED, (KeyEvent event) -> {
UserThread.execute(() -> {
String query = getEditor().getText();
var exactMatch = list.stream().anyMatch(item -> asString(item).equalsIgnoreCase(query));
if (!exactMatch) {
if (query.isEmpty())
removeFilter();
else
filterBy(query);
forceRedraw();
}
});
});
}
private void removeFilter() {
matchingList = new ArrayList<>(list);
setValue(null);
getSelectionModel().clearSelection();
setItems(FXCollections.observableList(matchingList));
getEditor().setText("");
}
private void forceRedraw() {
adjustVisibleRowCount();
if (matchingListSize() > 0) {
comboBoxListViewSkin.getPopupContent().autosize();
show();
} else {
hide();
}
}
private void adjustVisibleRowCount() {
setVisibleRowCount(Math.min(10, matchingListSize()));
}
private String asString(T item) {
return getConverter().toString(item);
}
private int matchingListSize() {
return matchingList.size();
}
}

View file

@ -0,0 +1,97 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import com.jfoenix.controls.JFXTextField;
import haveno.core.trade.HavenoUtils;
import haveno.core.util.coin.CoinFormatter;
import javafx.scene.effect.BlurType;
import javafx.scene.effect.DropShadow;
import javafx.scene.effect.Effect;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import java.math.BigInteger;
import javax.annotation.Nullable;
public class BalanceTextField extends AnchorPane {
private BigInteger targetAmount;
private final JFXTextField textField;
private final Effect fundedEffect = new DropShadow(BlurType.THREE_PASS_BOX, Color.GREEN, 4, 0.0, 0, 0);
private final Effect notFundedEffect = new DropShadow(BlurType.THREE_PASS_BOX, Color.ORANGERED, 4, 0.0, 0, 0);
private CoinFormatter formatter;
@Nullable
private BigInteger balance;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public BalanceTextField(String label) {
textField = new HavenoTextField();
textField.setLabelFloat(true);
textField.setPromptText(label);
textField.setFocusTraversable(false);
textField.setEditable(false);
textField.setId("info-field");
AnchorPane.setRightAnchor(textField, 0.0);
AnchorPane.setLeftAnchor(textField, 0.0);
getChildren().addAll(textField);
}
public void setFormatter(CoinFormatter formatter) {
this.formatter = formatter;
}
public void setBalance(BigInteger balance) {
this.balance = balance;
updateBalance(balance);
}
public void setTargetAmount(BigInteger targetAmount) {
this.targetAmount = targetAmount;
if (this.balance != null)
updateBalance(balance);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private methods
///////////////////////////////////////////////////////////////////////////////////////////
private void updateBalance(BigInteger balance) {
if (formatter != null)
textField.setText(HavenoUtils.formatToXmrWithCode(balance));
//TODO: replace with new validation logic
// if (targetAmount != null) {
// if (balance.compareTo(targetAmount) >= 0)
// textField.setEffect(fundedEffect);
// else
// textField.setEffect(notFundedEffect);
// } else {
// textField.setEffect(null);
// }
}
}

View file

@ -0,0 +1,65 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import com.jfoenix.controls.JFXSpinner;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
public class BusyAnimation extends JFXSpinner {
private final BooleanProperty isRunningProperty = new SimpleBooleanProperty();
public BusyAnimation() {
this(true);
}
public BusyAnimation(boolean isRunning) {
getStyleClass().add("busyanimation");
isRunningProperty.set(isRunning);
updateVisibility();
}
public void play() {
isRunningProperty.set(true);
setProgress(-1);
updateVisibility();
}
public void stop() {
isRunningProperty.set(false);
setProgress(0);
updateVisibility();
}
public boolean isRunning() {
return isRunningProperty.get();
}
public BooleanProperty isRunningProperty() {
return isRunningProperty;
}
private void updateVisibility() {
setVisible(isRunning());
setManaged(isRunning());
}
}

View file

@ -0,0 +1,59 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import haveno.common.util.Tuple2;
import haveno.core.util.FormattingUtils;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.geometry.Pos;
public class ColoredDecimalPlacesWithZerosText extends HBox {
public ColoredDecimalPlacesWithZerosText(String number, int numberOfZerosToColorize) {
super();
if (numberOfZerosToColorize <= 0) {
getChildren().addAll(new Label(number));
} else if (number.contains(FormattingUtils.RANGE_SEPARATOR)) {
String[] splitNumber = number.split(FormattingUtils.RANGE_SEPARATOR);
Tuple2<Label, Label> numbers = getSplittedNumberNodes(splitNumber[0], numberOfZerosToColorize);
getChildren().addAll(numbers.first, numbers.second);
getChildren().add(new Label(FormattingUtils.RANGE_SEPARATOR));
numbers = getSplittedNumberNodes(splitNumber[1], numberOfZerosToColorize);
getChildren().addAll(numbers.first, numbers.second);
} else {
Tuple2<Label, Label> numbers = getSplittedNumberNodes(number, numberOfZerosToColorize);
getChildren().addAll(numbers.first, numbers.second);
}
setAlignment(Pos.CENTER_LEFT);
}
private Tuple2<Label, Label> getSplittedNumberNodes(String number, int numberOfZeros) {
String placesBeforeZero = number.split("0{1," + Integer.toString(numberOfZeros) + "}$")[0];
String zeroDecimalPlaces = number.substring(placesBeforeZero.length());
Label first = new AutoTooltipLabel(placesBeforeZero);
Label last = new Label(zeroDecimalPlaces);
last.getStyleClass().add("zero-decimals");
return new Tuple2<>(first, last);
}
}

View file

@ -0,0 +1,125 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import haveno.common.util.Utilities;
import haveno.core.locale.Res;
import haveno.core.user.BlockChainExplorer;
import haveno.core.user.Preferences;
import haveno.desktop.util.GUIUtil;
import com.jfoenix.controls.JFXTextField;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.AnchorPane;
import lombok.Getter;
import lombok.Setter;
import javax.annotation.Nullable;
public class ExplorerAddressTextField extends AnchorPane {
@Setter
private static Preferences preferences;
@Getter
private final TextField textField;
private final Label copyIcon, blockExplorerIcon, missingAddressWarningIcon;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public ExplorerAddressTextField() {
copyIcon = new Label();
copyIcon.setLayoutY(3);
copyIcon.getStyleClass().addAll("icon", "highlight");
copyIcon.setTooltip(new Tooltip(Res.get("explorerAddressTextField.copyToClipboard")));
AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY);
AnchorPane.setRightAnchor(copyIcon, 30.0);
Tooltip tooltip = new Tooltip(Res.get("explorerAddressTextField.blockExplorerIcon.tooltip"));
blockExplorerIcon = new Label();
blockExplorerIcon.getStyleClass().addAll("icon", "highlight");
blockExplorerIcon.setTooltip(tooltip);
AwesomeDude.setIcon(blockExplorerIcon, AwesomeIcon.EXTERNAL_LINK);
blockExplorerIcon.setMinWidth(20);
AnchorPane.setRightAnchor(blockExplorerIcon, 52.0);
AnchorPane.setTopAnchor(blockExplorerIcon, 4.0);
missingAddressWarningIcon = new Label();
missingAddressWarningIcon.getStyleClass().addAll("icon", "error-icon");
AwesomeDude.setIcon(missingAddressWarningIcon, AwesomeIcon.WARNING_SIGN);
missingAddressWarningIcon.setTooltip(new Tooltip(Res.get("explorerAddressTextField.missingTx.warning.tooltip")));
missingAddressWarningIcon.setMinWidth(20);
AnchorPane.setRightAnchor(missingAddressWarningIcon, 52.0);
AnchorPane.setTopAnchor(missingAddressWarningIcon, 4.0);
missingAddressWarningIcon.setVisible(false);
missingAddressWarningIcon.setManaged(false);
textField = new JFXTextField();
textField.setId("address-text-field");
textField.setEditable(false);
textField.setTooltip(tooltip);
AnchorPane.setRightAnchor(textField, 80.0);
AnchorPane.setLeftAnchor(textField, 0.0);
textField.focusTraversableProperty().set(focusTraversableProperty().get());
getChildren().addAll(textField, missingAddressWarningIcon, blockExplorerIcon, copyIcon);
}
public void setup(@Nullable String address) {
if (address == null) {
textField.setText(Res.get("shared.na"));
textField.setId("address-text-field-error");
blockExplorerIcon.setVisible(false);
blockExplorerIcon.setManaged(false);
copyIcon.setVisible(false);
copyIcon.setManaged(false);
missingAddressWarningIcon.setVisible(true);
missingAddressWarningIcon.setManaged(true);
return;
}
textField.setText(address);
textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(address));
blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(address));
copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(address));
}
public void cleanup() {
textField.setOnMouseClicked(null);
blockExplorerIcon.setOnMouseClicked(null);
copyIcon.setOnMouseClicked(null);
textField.setText("");
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void openBlockExplorer(String address) {
if (preferences != null) {
BlockChainExplorer blockChainExplorer = preferences.getBlockChainExplorer();
GUIUtil.openWebPage(blockChainExplorer.addressUrl + address, false);
}
}
}

View file

@ -0,0 +1,35 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
public class ExternalHyperlink extends HyperlinkWithIcon {
public ExternalHyperlink(String text) {
super(text, MaterialDesignIcon.LINK);
}
public ExternalHyperlink(String text, String style) {
super(text, MaterialDesignIcon.LINK, style);
}
public ExternalHyperlink(String text, String style, String iconSize) {
super(text, MaterialDesignIcon.LINK, style, iconSize);
}
}

View file

@ -0,0 +1,83 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import static haveno.desktop.util.FormBuilder.getIcon;
import de.jensd.fx.fontawesome.AwesomeIcon;
import haveno.common.util.Utilities;
import haveno.core.locale.Res;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.AnchorPane;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FundsTextField extends InfoTextField {
public static final Logger log = LoggerFactory.getLogger(FundsTextField.class);
private final StringProperty fundsStructure = new SimpleStringProperty();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public FundsTextField() {
super();
textField.textProperty().unbind();
textField.textProperty().bind(Bindings.concat(textProperty())); // TODO: removed `, " ", fundsStructure` for haveno to fix "Funds needed: .123 XMR (null)" bug
Label copyIcon = getIcon(AwesomeIcon.COPY);
copyIcon.setLayoutY(5);
copyIcon.getStyleClass().addAll("icon", "highlight");
Tooltip.install(copyIcon, new Tooltip(Res.get("shared.copyToClipboard")));
copyIcon.setOnMouseClicked(e -> {
String text = getText();
if (text != null && text.length() > 0) {
String copyText;
String[] strings = text.split(" ");
if (strings.length > 1)
copyText = strings[0]; // exclude the BTC postfix
else
copyText = text;
Utilities.copyToClipboard(copyText);
}
});
AnchorPane.setRightAnchor(copyIcon, 30.0);
AnchorPane.setRightAnchor(infoIcon, 62.0);
AnchorPane.setRightAnchor(textField, 55.0);
getChildren().add(copyIcon);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters/Setters
///////////////////////////////////////////////////////////////////////////////////////////
public void setFundsStructure(String fundsStructure) {
this.fundsStructure.set(fundsStructure);
}
}

View file

@ -0,0 +1,12 @@
package haveno.desktop.components;
import com.jfoenix.controls.JFXTextArea;
import javafx.scene.control.Skin;
public class HavenoTextArea extends JFXTextArea {
@Override
protected Skin<?> createDefaultSkin() {
return new JFXTextAreaSkinHavenoStyle(this);
}
}

View file

@ -0,0 +1,21 @@
package haveno.desktop.components;
import com.jfoenix.controls.JFXTextField;
import javafx.scene.control.Skin;
public class HavenoTextField extends JFXTextField {
public HavenoTextField(String value) {
super(value);
}
public HavenoTextField() {
super();
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXTextFieldSkinHavenoStyle<>(this, 0);
}
}

View file

@ -0,0 +1,105 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import de.jensd.fx.glyphs.GlyphIcons;
import haveno.desktop.util.FormBuilder;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.text.Text;
import javafx.geometry.Insets;
import lombok.Getter;
public class HyperlinkWithIcon extends Hyperlink {
@Getter
private Node icon;
public HyperlinkWithIcon(String text) {
this(text, AwesomeIcon.INFO_SIGN);
}
public HyperlinkWithIcon(String text, String fontSize) {
this(text, AwesomeIcon.INFO_SIGN, fontSize);
}
public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon, String fontSize) {
super(text);
Label icon = new Label();
AwesomeDude.setIcon(icon, awesomeIcon, fontSize);
icon.setMinWidth(20);
icon.setOpacity(0.7);
icon.getStyleClass().addAll("hyperlink", "no-underline");
setPadding(new Insets(0));
icon.setPadding(new Insets(0));
setIcon(icon);
}
public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon) {
this(text, awesomeIcon, "1.231em");
}
public HyperlinkWithIcon(String text, GlyphIcons icon) {
this(text, icon, null);
}
public HyperlinkWithIcon(String text, GlyphIcons icon, String style, String iconSize) {
super(text);
Text textIcon = FormBuilder.getIcon(icon, iconSize);
textIcon.setOpacity(0.7);
textIcon.getStyleClass().addAll("hyperlink", "no-underline");
if (style != null) {
textIcon.getStyleClass().add(style);
getStyleClass().add(style);
}
setPadding(new Insets(0));
setIcon(textIcon);
}
public HyperlinkWithIcon(String text, GlyphIcons icon, String style) {
this(text, icon, style, "1.231em");
}
public void hideIcon() {
setGraphic(null);
}
public void setIcon(Node icon) {
this.icon = icon;
setGraphic(icon);
setContentDisplay(ContentDisplay.RIGHT);
setGraphicTextGap(7.0);
}
public void clear() {
setText("");
setGraphic(null);
}
}

View file

@ -0,0 +1,111 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import static haveno.desktop.util.FormBuilder.getIcon;
import de.jensd.fx.fontawesome.AwesomeIcon;
import de.jensd.fx.glyphs.GlyphIcons;
import haveno.desktop.components.controlsfx.control.PopOver;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.geometry.Insets;
public class InfoAutoTooltipLabel extends AutoTooltipLabel {
public static final int DEFAULT_WIDTH = 300;
private Node textIcon;
private PopOverWrapper popoverWrapper = new PopOverWrapper();
private ContentDisplay contentDisplay;
public InfoAutoTooltipLabel(String text, GlyphIcons icon, ContentDisplay contentDisplay, String info) {
this(text, contentDisplay);
setIcon(icon);
positionAndActivateIcon(contentDisplay, info, DEFAULT_WIDTH);
}
public InfoAutoTooltipLabel(String text, AwesomeIcon icon, ContentDisplay contentDisplay, String info, double width) {
super(text);
setIcon(icon);
positionAndActivateIcon(contentDisplay, info, width);
}
public InfoAutoTooltipLabel(String text, ContentDisplay contentDisplay) {
super(text);
this.contentDisplay = contentDisplay;
}
public void setIcon(GlyphIcons icon) {
textIcon = getIcon(icon);
}
public void setIcon(GlyphIcons icon, String info) {
setIcon(icon);
positionAndActivateIcon(contentDisplay, info, DEFAULT_WIDTH);
}
public void setIcon(AwesomeIcon icon) {
textIcon = getIcon(icon);
}
public void hideIcon() {
textIcon = null;
setGraphic(textIcon);
}
// May be required until https://bugs.openjdk.java.net/browse/JDK-8265835 is fixed.
public void disableRolloverPopup() {
textIcon.setOnMouseEntered(null);
textIcon.setOnMouseExited(null);
}
private void positionAndActivateIcon(ContentDisplay contentDisplay, String info, double width) {
textIcon.setOpacity(0.4);
textIcon.getStyleClass().add("tooltip-icon");
textIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createInfoPopOver(info, width)));
textIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver());
setGraphic(textIcon);
setContentDisplay(contentDisplay);
}
private PopOver createInfoPopOver(String info, double width) {
Label helpLabel = new Label(info);
helpLabel.setMaxWidth(width);
helpLabel.setWrapText(true);
helpLabel.setPadding(new Insets(10));
return createInfoPopOver(helpLabel);
}
private PopOver createInfoPopOver(Node node) {
node.getStyleClass().add("default-text");
PopOver infoPopover = new PopOver(node);
if (textIcon.getScene() != null) {
infoPopover.setDetachable(false);
infoPopover.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER);
infoPopover.show(textIcon, -10);
}
return infoPopover;
}
}

View file

@ -0,0 +1,240 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.fontawesome.AwesomeIcon;
import haveno.common.UserThread;
import haveno.core.locale.Res;
import haveno.desktop.util.FormBuilder;
import javafx.scene.Parent;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.OverrunStyle;
import javafx.scene.layout.GridPane;
import javafx.scene.text.TextFlow;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
/**
* Convenience Component for info icon, info text and link display in a GridPane.
* Only the properties needed are supported.
* We need to extend from Parent so we can use it in FXML, but the InfoDisplay is not used as node,
* but add the children nodes to the gridPane.
*/
public class InfoDisplay extends Parent {
private final StringProperty text = new SimpleStringProperty();
private final IntegerProperty rowIndex = new SimpleIntegerProperty(0);
private final IntegerProperty columnIndex = new SimpleIntegerProperty(0);
private final ObjectProperty<EventHandler<ActionEvent>> onAction = new SimpleObjectProperty<>();
private final ObjectProperty<GridPane> gridPane = new SimpleObjectProperty<>();
private boolean useReadMore;
private final Label icon = FormBuilder.getIcon(AwesomeIcon.INFO_SIGN);
private final TextFlow textFlow;
private final Label label;
private final Hyperlink link;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public InfoDisplay() {
icon.setId("non-clickable-icon");
icon.visibleProperty().bind(visibleProperty());
GridPane.setValignment(icon, VPos.TOP);
GridPane.setMargin(icon, new Insets(-2, 0, 0, 0));
GridPane.setRowSpan(icon, 2);
label = new AutoTooltipLabel();
label.textProperty().bind(text);
label.setTextOverrun(OverrunStyle.WORD_ELLIPSIS);
// width is set a frame later so we hide it first
label.setVisible(false);
link = new Hyperlink(Res.get("shared.readMore"));
link.setPadding(new Insets(0, 0, 0, -2));
// We need that to know if we have a wrapping or not.
// Did not find a way to get that from the API.
Label testLabel = new AutoTooltipLabel();
testLabel.textProperty().bind(text);
textFlow = new TextFlow();
textFlow.visibleProperty().bind(visibleProperty());
textFlow.getChildren().addAll(testLabel);
testLabel.widthProperty().addListener((ov, o, n) -> {
useReadMore = (double) n > textFlow.getWidth();
link.setText(Res.get(useReadMore ? "shared.readMore" : "shared.openHelp"));
UserThread.execute(() -> textFlow.getChildren().setAll(label, link));
});
// update the width when the window gets resized
ChangeListener<Number> listener = (ov2, oldValue2, windowWidth) -> {
if (label.prefWidthProperty().isBound())
label.prefWidthProperty().unbind();
label.setPrefWidth((double) windowWidth - localToScene(0, 0).getX() - 35);
};
// when clicking "Read more..." we expand and change the link to the Help
link.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
if (useReadMore) {
label.setWrapText(true);
link.setText(Res.get("shared.openHelp"));
getScene().getWindow().widthProperty().removeListener(listener);
if (label.prefWidthProperty().isBound())
label.prefWidthProperty().unbind();
label.prefWidthProperty().bind(textFlow.widthProperty());
link.setVisited(false);
// focus border is a bit confusing here so we remove it
link.getStyleClass().add("hide-focus");
link.setOnAction(onAction.get());
getParent().layout();
} else {
onAction.get().handle(actionEvent);
}
}
});
sceneProperty().addListener((ov, oldValue, newValue) -> {
if (oldValue == null && newValue != null && newValue.getWindow() != null) {
newValue.getWindow().widthProperty().addListener(listener);
// localToScene does deliver 0 instead of the correct x position when scene property gets set,
// so we delay for 1 render cycle
UserThread.execute(() -> {
label.setVisible(true);
label.prefWidthProperty().unbind();
label.setPrefWidth(newValue.getWindow().getWidth() - localToScene(0, 0).getX() - 35);
});
}
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setters
///////////////////////////////////////////////////////////////////////////////////////////
public void setText(String text) {
this.text.set(text);
UserThread.execute(() -> {
if (getScene() != null) {
label.setVisible(true);
label.prefWidthProperty().unbind();
label.setPrefWidth(getScene().getWindow().getWidth() - localToScene(0, 0).getX() - 35);
}
});
}
public void setGridPane(GridPane gridPane) {
this.gridPane.set(gridPane);
gridPane.getChildren().addAll(icon, textFlow);
GridPane.setColumnIndex(icon, columnIndex.get());
GridPane.setColumnIndex(textFlow, columnIndex.get() + 1);
GridPane.setRowIndex(icon, rowIndex.get());
GridPane.setRowIndex(textFlow, rowIndex.get());
}
public void setRowIndex(int rowIndex) {
this.rowIndex.set(rowIndex);
GridPane.setRowIndex(icon, rowIndex);
GridPane.setRowIndex(textFlow, rowIndex);
}
public void setColumnIndex(int columnIndex) {
this.columnIndex.set(columnIndex);
GridPane.setColumnIndex(icon, columnIndex);
GridPane.setColumnIndex(textFlow, columnIndex + 1);
}
public final void setOnAction(EventHandler<ActionEvent> eventHandler) {
onAction.set(eventHandler);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public int getColumnIndex() {
return columnIndex.get();
}
public IntegerProperty columnIndexProperty() {
return columnIndex;
}
public int getRowIndex() {
return rowIndex.get();
}
public IntegerProperty rowIndexProperty() {
return rowIndex;
}
public EventHandler<ActionEvent> getOnAction() {
return onAction.get();
}
public ObjectProperty<EventHandler<ActionEvent>> onActionProperty() {
return onAction;
}
public GridPane getGridPane() {
return gridPane.get();
}
public ObjectProperty<GridPane> gridPaneProperty() {
return gridPane;
}
}

View file

@ -0,0 +1,156 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import haveno.desktop.components.controlsfx.control.PopOver;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.AnchorPane;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import lombok.Getter;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
public class InfoInputTextField extends AnchorPane {
private final StringProperty text = new SimpleStringProperty();
@Getter
private final InputTextField inputTextField;
private final Label icon;
private final PopOverWrapper popoverWrapper = new PopOverWrapper();
@Nullable
private Node node;
public InfoInputTextField() {
this(0);
}
public InfoInputTextField(double inputLineExtension) {
super();
inputTextField = new InputTextField(inputLineExtension);
AnchorPane.setRightAnchor(inputTextField, 0.0);
AnchorPane.setLeftAnchor(inputTextField, 0.0);
icon = new Label();
icon.setLayoutY(3);
AnchorPane.setLeftAnchor(icon, 7.0);
icon.setOnMouseEntered(e -> {
if (node != null) {
popoverWrapper.showPopOver(() -> checkNotNull(createPopOver()));
}
});
icon.setOnMouseExited(e -> {
if (node != null) {
popoverWrapper.hidePopOver();
}
});
hideIcon();
getChildren().addAll(inputTextField, icon);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public
///////////////////////////////////////////////////////////////////////////////////////////
public void setContentForInfoPopOver(Node node) {
setContentForPopOver(node, AwesomeIcon.INFO_SIGN);
}
public void setContentForWarningPopOver(Node node) {
setContentForPopOver(node, AwesomeIcon.WARNING_SIGN, "warning");
}
public void setContentForPrivacyPopOver(Node node) {
setContentForPopOver(node, AwesomeIcon.EYE_CLOSE);
}
public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon) {
setContentForPopOver(node, awesomeIcon, null);
}
public void setContentForPopOver(Node node, AwesomeIcon awesomeIcon, @Nullable String style) {
this.node = node;
AwesomeDude.setIcon(icon, awesomeIcon);
icon.getStyleClass().removeAll("icon", "info", "warning", style);
icon.getStyleClass().addAll("icon", style == null ? "info" : style);
icon.setManaged(true);
icon.setVisible(true);
}
public void hideIcon() {
icon.setManaged(false);
icon.setVisible(false);
}
public void setIconsRightAligned() {
AnchorPane.clearConstraints(icon);
AnchorPane.clearConstraints(inputTextField);
AnchorPane.setRightAnchor(icon, 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();
}
public StringProperty textProperty() {
return text;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private PopOver createPopOver() {
if (node == null) {
return null;
}
node.getStyleClass().add("default-text");
PopOver popover = new PopOver(node);
if (icon.getScene() != null) {
popover.setDetachable(false);
popover.setArrowLocation(PopOver.ArrowLocation.LEFT_TOP);
popover.setArrowIndent(5);
popover.show(icon, -17);
}
return popover;
}
}

View file

@ -0,0 +1,154 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.fontawesome.AwesomeIcon;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
import haveno.desktop.components.controlsfx.control.PopOver;
import static haveno.desktop.util.FormBuilder.getIcon;
import static haveno.desktop.util.FormBuilder.getRegularIconForLabel;
import com.jfoenix.controls.JFXTextField;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.AnchorPane;
import javafx.scene.text.Text;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.Getter;
public class InfoTextField extends AnchorPane {
public static final Logger log = LoggerFactory.getLogger(InfoTextField.class);
@Getter
protected final JFXTextField textField;
private final StringProperty text = new SimpleStringProperty();
protected final Label infoIcon;
private Label currentIcon;
private PopOverWrapper popoverWrapper = new PopOverWrapper();
private PopOver.ArrowLocation arrowLocation;
public InfoTextField() {
arrowLocation = PopOver.ArrowLocation.RIGHT_TOP;
textField = new HavenoTextField();
textField.setLabelFloat(true);
textField.setEditable(false);
textField.textProperty().bind(text);
textField.setFocusTraversable(false);
textField.setId("info-field");
infoIcon = getIcon(AwesomeIcon.INFO_SIGN);
infoIcon.setLayoutY(5);
infoIcon.getStyleClass().addAll("icon", "info");
AnchorPane.setRightAnchor(infoIcon, 7.0);
AnchorPane.setRightAnchor(textField, 0.0);
AnchorPane.setLeftAnchor(textField, 0.0);
hideIcons();
getChildren().addAll(textField, infoIcon);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public
///////////////////////////////////////////////////////////////////////////////////////////
public void setContentForInfoPopOver(Node node) {
currentIcon = infoIcon;
hideIcons();
setActionHandlers(node);
}
public void setContent(MaterialDesignIcon icon, String info, String style, double opacity) {
hideIcons();
currentIcon = new Label();
Text textIcon = getRegularIconForLabel(icon, currentIcon);
setActionHandlers(new Label(info));
currentIcon.setLayoutY(5);
textIcon.getStyleClass().addAll("icon", style);
currentIcon.setOpacity(opacity);
AnchorPane.setRightAnchor(currentIcon, 7.0);
getChildren().add(currentIcon);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void hideIcons() {
infoIcon.setManaged(false);
infoIcon.setVisible(false);
}
private void setActionHandlers(Node node) {
currentIcon.setManaged(true);
currentIcon.setVisible(true);
// As we don't use binding here we need to recreate it on mouse over to reflect the current state
currentIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createPopOver(node)));
currentIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver());
}
private PopOver createPopOver(Node node) {
node.getStyleClass().add("default-text");
PopOver popover = new PopOver(node);
if (currentIcon.getScene() != null) {
popover.setDetachable(false);
popover.setArrowLocation(arrowLocation);
popover.setArrowIndent(5);
popover.show(currentIcon, -17);
}
return popover;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters/Setters
///////////////////////////////////////////////////////////////////////////////////////////
public void setText(String text) {
this.text.set(text);
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
}

View file

@ -0,0 +1,149 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import com.jfoenix.controls.JFXTextField;
import haveno.core.locale.Res;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.util.validation.JFXInputValidator;
import javafx.scene.control.Skin;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
/**
* TextField with validation support.
* If validator is set it supports on focus out validation with that validator. If a more sophisticated validation is
* needed the validationResultProperty can be used for applying validation result done by external validation.
* In case the isValid property in validationResultProperty get set to false we display a red border and an error
* message within the errorMessageDisplay placed on the right of the text field.
* The errorMessageDisplay gets closed when the ValidatingTextField instance gets removed from the scene graph or when
* hideErrorMessageDisplay() is called.
* There can be only 1 errorMessageDisplays at a time we use static field for it.
* The position is derived from the position of the textField itself or if set from the layoutReference node.
*/
//TODO There are some rare situation where it behaves buggy. Needs further investigation and improvements.
public class InputTextField extends JFXTextField {
private final ObjectProperty<InputValidator.ValidationResult> validationResult = new SimpleObjectProperty<>
(new InputValidator.ValidationResult(true));
private final JFXInputValidator jfxValidationWrapper = new JFXInputValidator();
private double inputLineExtension = 0;
private InputValidator validator;
private String errorMessage = null;
public InputValidator getValidator() {
return validator;
}
public void setValidator(InputValidator validator) {
this.validator = validator;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public InputTextField() {
super();
getValidators().add(jfxValidationWrapper);
validationResult.addListener((ov, oldValue, newValue) -> {
if (newValue != null) {
jfxValidationWrapper.resetValidation();
if (!newValue.isValid) {
if (!newValue.errorMessageEquals(oldValue)) { // avoid blinking
validate(); // ensure that the new error message replaces the old one
}
if (this.errorMessage != null) {
jfxValidationWrapper.applyErrorMessage(this.errorMessage);
} else {
jfxValidationWrapper.applyErrorMessage(newValue);
}
}
validate();
}
});
textProperty().addListener((o, oldValue, newValue) -> {
refreshValidation();
});
focusedProperty().addListener((o, oldValue, newValue) -> {
if (validator != null) {
if (!oldValue && newValue) {
this.validationResult.set(new InputValidator.ValidationResult(true));
} else {
this.validationResult.set(validator.validate(getText()));
}
}
});
}
public InputTextField(double inputLineExtension) {
this();
this.inputLineExtension = inputLineExtension;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
public void resetValidation() {
jfxValidationWrapper.resetValidation();
String input = getText();
if (input.isEmpty()) {
validationResult.set(new InputValidator.ValidationResult(true));
} else {
validationResult.set(validator.validate(input));
}
}
public void refreshValidation() {
if (validator != null) {
this.validationResult.set(validator.validate(getText()));
}
}
public void setInvalid(String message) {
validationResult.set(new InputValidator.ValidationResult(false, message));
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
public ObjectProperty<InputValidator.ValidationResult> validationResultProperty() {
return validationResult;
}
protected Skin<?> createDefaultSkin() {
return new JFXTextFieldSkinHavenoStyle<>(this, inputLineExtension);
}
}

View file

@ -0,0 +1,285 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXRippler;
import com.jfoenix.transitions.JFXAnimationTimer;
import com.jfoenix.transitions.JFXKeyFrame;
import com.jfoenix.transitions.JFXKeyValue;
import javafx.animation.Interpolator;
import javafx.scene.Node;
import javafx.scene.control.RadioButton;
import javafx.scene.control.skin.RadioButtonSkin;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.StrokeType;
import javafx.scene.text.Text;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.util.Duration;
/**
* Code copied and adapted from com.jfoenix.skins.JFXRadioButtonSkin
*/
public class JFXRadioButtonSkinHavenoStyle extends RadioButtonSkin {
private final JFXRippler rippler;
private double padding = 12;
private Circle radio, dot;
private final StackPane container;
private JFXAnimationTimer timer;
public JFXRadioButtonSkinHavenoStyle(JFXRadioButton control) {
super(control);
final double radioRadius = 7;
radio = new Circle(radioRadius);
radio.getStyleClass().setAll("radio");
radio.setStrokeWidth(1);
radio.setStrokeType(StrokeType.INSIDE);
radio.setFill(Color.TRANSPARENT);
radio.setSmooth(true);
dot = new Circle(radioRadius);
dot.getStyleClass().setAll("dot");
dot.fillProperty().bind(control.selectedColorProperty());
dot.setScaleX(0);
dot.setScaleY(0);
dot.setSmooth(true);
container = new StackPane(radio, dot);
container.getStyleClass().add("radio-container");
rippler = new JFXRippler(container, JFXRippler.RipplerMask.CIRCLE) {
@Override
protected double computeRippleRadius() {
double width = ripplerPane.getWidth();
double width2 = width * width;
return Math.min(Math.sqrt(width2 + width2), RIPPLE_MAX_RADIUS) * 1.1 + 5;
}
@Override
protected void setOverLayBounds(Rectangle overlay) {
overlay.setWidth(ripplerPane.getWidth());
overlay.setHeight(ripplerPane.getHeight());
}
protected void initControlListeners() {
// if the control got resized the overlay rect must be rest
control.layoutBoundsProperty().addListener(observable -> resetRippler());
if (getChildren().contains(control)) {
control.boundsInParentProperty().addListener(observable -> resetRippler());
}
control.addEventHandler(MouseEvent.MOUSE_PRESSED,
(event) -> createRipple(event.getX() + padding, event.getY() + padding));
// create fade out transition for the ripple
control.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> releaseRipple());
}
@Override
protected Node getMask() {
double radius = ripplerPane.getWidth() / 2;
return new Circle(radius, radius, radius);
}
@Override
protected void positionControl(Node control) {
}
};
updateChildren();
// show focused state
control.focusedProperty().addListener((o, oldVal, newVal) -> {
if (!control.disableVisualFocusProperty().get()) {
if (newVal) {
if (!getSkinnable().isPressed()) {
rippler.setOverlayVisible(true);
}
} else {
rippler.setOverlayVisible(false);
}
}
});
control.pressedProperty().addListener((o, oldVal, newVal) -> rippler.setOverlayVisible(false));
timer = new JFXAnimationTimer(
new JFXKeyFrame(Duration.millis(200),
JFXKeyValue.builder()
.setTarget(dot.scaleXProperty())
.setEndValueSupplier(() -> getSkinnable().isSelected() ? 0.55 : 0)
.setInterpolator(Interpolator.EASE_BOTH)
.build(),
JFXKeyValue.builder()
.setTarget(dot.scaleYProperty())
.setEndValueSupplier(() -> getSkinnable().isSelected() ? 0.55 : 0)
.setInterpolator(Interpolator.EASE_BOTH)
.build(),
JFXKeyValue.builder()
.setTarget(radio.strokeProperty())
.setEndValueSupplier(() -> getSkinnable().isSelected() ? ((JFXRadioButton) getSkinnable()).getSelectedColor() : ((JFXRadioButton) getSkinnable()).getUnSelectedColor())
.setInterpolator(Interpolator.EASE_BOTH)
.build()
));
registerChangeListener(control.selectedColorProperty(), obs -> updateColors());
registerChangeListener(control.unSelectedColorProperty(), obs -> updateColors());
registerChangeListener(control.selectedProperty(), obs -> {
boolean isSelected = getSkinnable().isSelected();
Color unSelectedColor = ((JFXRadioButton) getSkinnable()).getUnSelectedColor();
Color selectedColor = ((JFXRadioButton) getSkinnable()).getSelectedColor();
rippler.setRipplerFill(isSelected ? selectedColor : unSelectedColor);
if (((JFXRadioButton) getSkinnable()).isDisableAnimation()) {
// apply end values
timer.applyEndValues();
} else {
// play selection animation
timer.reverseAndContinue();
}
});
updateColors();
timer.applyEndValues();
}
@Override
protected void updateChildren() {
super.updateChildren();
if (radio != null) {
removeRadio();
getChildren().addAll(container, rippler);
}
}
@Override
protected void layoutChildren(final double x, final double y, final double w, final double h) {
final RadioButton radioButton = getSkinnable();
final double contWidth = snapSizeX(container.prefWidth(-1));
final double contHeight = snapSizeY(container.prefHeight(-1));
final double computeWidth = Math.max(radioButton.prefWidth(-1), radioButton.minWidth(-1));
final double width = snapSizeX(contWidth);
final double height = snapSizeY(contHeight);
final double labelWidth = Math.min(computeWidth - contWidth, w - width);
final double labelHeight = Math.min(radioButton.prefHeight(labelWidth), h);
final double maxHeight = Math.max(contHeight, labelHeight);
final double xOffset = computeXOffset(w, labelWidth + contWidth, radioButton.getAlignment().getHpos()) + x;
final double yOffset = computeYOffset(h, maxHeight, radioButton.getAlignment().getVpos()) + x + 5;
layoutLabelInArea(xOffset + contWidth + padding / 3, yOffset, labelWidth, maxHeight, radioButton.getAlignment());
((Text) getChildren().get((getChildren().get(0) instanceof Text) ? 0 : 1)).
textProperty().set(getSkinnable().textProperty().get());
container.resize(width, height);
positionInArea(container,
xOffset,
yOffset,
contWidth,
maxHeight,
0,
radioButton.getAlignment().getHpos(),
radioButton.getAlignment().getVpos());
final double ripplerWidth = width + 2 * padding;
final double ripplerHeight = height + 2 * padding;
rippler.resizeRelocate((width / 2 + xOffset) - ripplerWidth / 2,
(height / 2 + yOffset) + 2 - ripplerHeight / 2,
ripplerWidth, ripplerHeight);
}
private void removeRadio() {
// TODO: replace with removeIf
for (int i = 0; i < getChildren().size(); i++) {
if ("radio".equals(getChildren().get(i).getStyleClass().get(0))) {
getChildren().remove(i);
}
}
}
private void updateColors() {
boolean isSelected = getSkinnable().isSelected();
Color unSelectedColor = ((JFXRadioButton) getSkinnable()).getUnSelectedColor();
Color selectedColor = ((JFXRadioButton) getSkinnable()).getSelectedColor();
rippler.setRipplerFill(isSelected ? selectedColor : unSelectedColor);
radio.setStroke(isSelected ? selectedColor : unSelectedColor);
}
@Override
protected double computeMinWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return super.computeMinWidth(height,
topInset,
rightInset,
bottomInset,
leftInset) + snapSize(radio.minWidth(-1)) + padding / 3;
}
@Override
protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
return super.computePrefWidth(height,
topInset,
rightInset,
bottomInset,
leftInset) + snapSizeX(radio.prefWidth(-1)) + padding / 3;
}
private static double computeXOffset(double width, double contentWidth, HPos hpos) {
switch (hpos) {
case LEFT:
return 0;
case CENTER:
return (width - contentWidth) / 2;
case RIGHT:
return width - contentWidth;
}
return 0;
}
private static double computeYOffset(double height, double contentHeight, VPos vpos) {
switch (vpos) {
case TOP:
return 0;
case CENTER:
return (height - contentHeight) / 2;
case BOTTOM:
return height - contentHeight;
default:
return 0;
}
}
@Override
public void dispose() {
super.dispose();
timer.dispose();
timer = null;
}
}

View file

@ -0,0 +1,119 @@
package haveno.desktop.components;
import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.controls.JFXTextArea;
import com.jfoenix.skins.PromptLinesWrapper;
import com.jfoenix.skins.ValidationPane;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.skin.TextAreaSkin;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.geometry.Insets;
import java.util.Arrays;
import java.lang.reflect.Field;
/**
* Code copied and adapted from com.jfoenix.skins.JFXTextAreaSkin
*/
public class JFXTextAreaSkinHavenoStyle extends TextAreaSkin {
private boolean invalid = true;
private ScrollPane scrollPane;
private Text promptText;
private ValidationPane<JFXTextArea> errorContainer;
private PromptLinesWrapper<JFXTextArea> linesWrapper;
public JFXTextAreaSkinHavenoStyle(JFXTextArea textArea) {
super(textArea);
// init text area properties
scrollPane = (ScrollPane) getChildren().get(0);
textArea.setWrapText(true);
linesWrapper = new PromptLinesWrapper<>(
textArea,
promptTextFillProperty(),
textArea.textProperty(),
textArea.promptTextProperty(),
() -> promptText);
linesWrapper.init(() -> createPromptNode(), scrollPane);
errorContainer = new ValidationPane<>(textArea);
getChildren().addAll(linesWrapper.line, linesWrapper.focusedLine, linesWrapper.promptContainer, errorContainer);
registerChangeListener(textArea.disableProperty(), obs -> linesWrapper.updateDisabled());
registerChangeListener(textArea.focusColorProperty(), obs -> linesWrapper.updateFocusColor());
registerChangeListener(textArea.unFocusColorProperty(), obs -> linesWrapper.updateUnfocusColor());
registerChangeListener(textArea.disableAnimationProperty(), obs -> errorContainer.updateClip());
}
@Override
protected void layoutChildren(final double x, final double y, final double w, final double h) {
super.layoutChildren(x, y, w, h);
final double height = getSkinnable().getHeight();
final double width = getSkinnable().getWidth();
linesWrapper.layoutLines(x - 2, y - 2, width, h, height, promptText == null ? 0 : promptText.getLayoutBounds().getHeight() + 3);
errorContainer.layoutPane(x, height + linesWrapper.focusedLine.getHeight(), width, h);
linesWrapper.updateLabelFloatLayout();
if (invalid) {
invalid = false;
// set the default background of text area viewport to white
Region viewPort = (Region) scrollPane.getChildrenUnmodifiable().get(0);
viewPort.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT,
CornerRadii.EMPTY,
Insets.EMPTY)));
// reapply css of scroll pane in case set by the user
viewPort.applyCss();
errorContainer.invalid(w);
// focus
linesWrapper.invalid();
}
}
private void createPromptNode() {
if (promptText != null || !linesWrapper.usePromptText.get()) {
return;
}
promptText = new Text();
promptText.setManaged(false);
promptText.getStyleClass().add("text");
promptText.visibleProperty().bind(linesWrapper.usePromptText);
promptText.fontProperty().bind(getSkinnable().fontProperty());
promptText.textProperty().bind(getSkinnable().promptTextProperty());
promptText.fillProperty().bind(linesWrapper.animatedPromptTextFill);
promptText.getTransforms().add(linesWrapper.promptTextScale);
linesWrapper.promptContainer.getChildren().add(promptText);
if (getSkinnable().isFocused() && ((JFXTextArea) getSkinnable()).isLabelFloat()) {
promptText.setTranslateY(-Math.floor(scrollPane.getHeight()));
linesWrapper.promptTextScale.setX(0.85);
linesWrapper.promptTextScale.setY(0.85);
}
try {
Field field = ReflectionHelper.getField(TextAreaSkin.class, "promptNode");
Object oldValue = field.get(this);
if (oldValue != null) {
removeHighlight(Arrays.asList(((Node) oldValue)));
}
field.set(this, promptText);
} catch (Exception e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,147 @@
package haveno.desktop.components;
import com.jfoenix.adapters.ReflectionHelper;
import com.jfoenix.controls.base.IFXLabelFloatControl;
import com.jfoenix.skins.PromptLinesWrapper;
import com.jfoenix.skins.ValidationPane;
import javafx.scene.Node;
import javafx.scene.control.TextField;
import javafx.scene.control.skin.TextFieldSkin;
import javafx.scene.layout.Pane;
import javafx.scene.text.Text;
import javafx.beans.property.DoubleProperty;
import javafx.beans.value.ObservableDoubleValue;
import java.lang.reflect.Field;
/**
* Code copied and adapted from com.jfoenix.skins.JFXTextFieldSkin
*/
public class JFXTextFieldSkinHavenoStyle<T extends TextField & IFXLabelFloatControl> extends TextFieldSkin {
private double inputLineExtension;
private boolean invalid = true;
private Text promptText;
private Pane textPane;
private Node textNode;
private ObservableDoubleValue textRight;
private DoubleProperty textTranslateX;
private ValidationPane<T> errorContainer;
private PromptLinesWrapper<T> linesWrapper;
public JFXTextFieldSkinHavenoStyle(T textField, double inputLineExtension) {
super(textField);
textPane = (Pane) this.getChildren().get(0);
this.inputLineExtension = inputLineExtension;
// get parent fields
textNode = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textNode");
textTranslateX = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textTranslateX");
textRight = ReflectionHelper.getFieldContent(TextFieldSkin.class, this, "textRight");
linesWrapper = new PromptLinesWrapper<T>(
textField,
promptTextFillProperty(),
textField.textProperty(),
textField.promptTextProperty(),
() -> promptText);
linesWrapper.init(() -> createPromptNode(), textPane);
ReflectionHelper.setFieldContent(TextFieldSkin.class, this, "usePromptText", linesWrapper.usePromptText);
errorContainer = new ValidationPane<>(textField);
getChildren().addAll(linesWrapper.line, linesWrapper.focusedLine, linesWrapper.promptContainer, errorContainer);
registerChangeListener(textField.disableProperty(), obs -> linesWrapper.updateDisabled());
registerChangeListener(textField.focusColorProperty(), obs -> linesWrapper.updateFocusColor());
registerChangeListener(textField.unFocusColorProperty(), obs -> linesWrapper.updateUnfocusColor());
registerChangeListener(textField.disableAnimationProperty(), obs -> errorContainer.updateClip());
}
@Override
protected void layoutChildren(final double x, final double y, final double w, final double h) {
super.layoutChildren(x, y, w, h);
final double height = getSkinnable().getHeight();
final double width = getSkinnable().getWidth() + inputLineExtension;
final double paddingLeft = getSkinnable().getPadding().getLeft();
linesWrapper.layoutLines(x, y, width, h, height, Math.floor(h));
errorContainer.layoutPane(x - paddingLeft, height + linesWrapper.focusedLine.getHeight(), width, h);
if (getSkinnable().getWidth() > 0) {
updateTextPos();
}
linesWrapper.updateLabelFloatLayout();
if (invalid) {
invalid = false;
// update validation container
errorContainer.invalid(w);
// focus
linesWrapper.invalid();
}
}
private void updateTextPos() {
double textWidth = textNode.getLayoutBounds().getWidth();
final double promptWidth = promptText == null ? 0 : promptText.getLayoutBounds().getWidth();
switch (getSkinnable().getAlignment().getHpos()) {
case CENTER:
linesWrapper.promptTextScale.setPivotX(promptWidth / 2);
double midPoint = textRight.get() / 2;
double newX = midPoint - textWidth / 2;
if (newX + textWidth <= textRight.get()) {
textTranslateX.set(newX);
}
break;
case LEFT:
linesWrapper.promptTextScale.setPivotX(0);
break;
case RIGHT:
linesWrapper.promptTextScale.setPivotX(promptWidth);
break;
}
}
private void createPromptNode() {
if (promptText != null || !linesWrapper.usePromptText.get()) {
return;
}
promptText = new Text();
promptText.setManaged(false);
promptText.getStyleClass().add("text");
promptText.visibleProperty().bind(linesWrapper.usePromptText);
promptText.fontProperty().bind(getSkinnable().fontProperty());
promptText.textProperty().bind(getSkinnable().promptTextProperty());
promptText.fillProperty().bind(linesWrapper.animatedPromptTextFill);
promptText.setLayoutX(1);
promptText.getTransforms().add(linesWrapper.promptTextScale);
linesWrapper.promptContainer.getChildren().add(promptText);
if (getSkinnable().isFocused() && ((IFXLabelFloatControl) getSkinnable()).isLabelFloat()) {
promptText.setTranslateY(-Math.floor(textPane.getHeight()));
linesWrapper.promptTextScale.setX(0.85);
linesWrapper.promptTextScale.setY(0.85);
}
try {
Field field = ReflectionHelper.getField(TextFieldSkin.class, "promptNode");
Object oldValue = field.get(this);
if (oldValue != null) {
textPane.getChildren().remove(oldValue);
}
field.set(this, promptText);
} catch (Exception e) {
e.printStackTrace();
}
}
}

View file

@ -0,0 +1,152 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import com.jfoenix.controls.JFXButton;
import haveno.desktop.Navigation;
import haveno.desktop.common.view.View;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;
import javafx.geometry.Pos;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ChangeListener;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
@Slf4j
public class MenuItem extends JFXButton implements Toggle {
private final Navigation navigation;
private final ObjectProperty<ToggleGroup> toggleGroupProperty = new SimpleObjectProperty<>();
private final Class<? extends View> viewClass;
private final List<Class<? extends View>> baseNavPath;
private final BooleanProperty selectedProperty = new SimpleBooleanProperty();
private final ChangeListener<Toggle> listener;
public MenuItem(Navigation navigation,
ToggleGroup toggleGroup,
String title,
Class<? extends View> viewClass,
List<Class<? extends View>> baseNavPath) {
this.navigation = navigation;
this.viewClass = viewClass;
this.baseNavPath = baseNavPath;
setLabelText(title);
setPrefHeight(40);
setPrefWidth(240);
setAlignment(Pos.CENTER_LEFT);
toggleGroupProperty.set(toggleGroup);
toggleGroup.getToggles().add(this);
setUserData(getUid());
listener = (observable, oldValue, newValue) -> {
Object userData = newValue.getUserData();
String uid = getUid();
if (newValue.isSelected() && userData != null && userData.equals(uid)) {
getStyleClass().add("action-button");
} else {
getStyleClass().remove("action-button");
}
};
}
///////////////////////////////////////////////////////////////////////////////////////////
// Toggle implementation
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public ToggleGroup getToggleGroup() {
return toggleGroupProperty.get();
}
@Override
public void setToggleGroup(ToggleGroup toggleGroup) {
toggleGroupProperty.set(toggleGroup);
}
@Override
public ObjectProperty<ToggleGroup> toggleGroupProperty() {
return toggleGroupProperty;
}
@Override
public boolean isSelected() {
return selectedProperty.get();
}
@Override
public BooleanProperty selectedProperty() {
return selectedProperty;
}
@Override
public void setSelected(boolean selected) {
selectedProperty.set(selected);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void activate() {
setOnAction((event) -> navigation.navigateTo(getNavPathClasses()));
toggleGroupProperty.get().selectedToggleProperty().addListener(listener);
}
public void deactivate() {
setOnAction(null);
toggleGroupProperty.get().selectedToggleProperty().removeListener(listener);
}
public void setLabelText(String value) {
setText(value.toUpperCase());
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
@NotNull
private Class<? extends View>[] getNavPathClasses() {
List<Class<? extends View>> list = new ArrayList<>(baseNavPath);
list.add(viewClass);
//noinspection unchecked
Class<? extends View>[] array = new Class[list.size()];
list.toArray(array);
return array;
}
private String getUid() {
return viewClass.getName();
}
}

View file

@ -0,0 +1,32 @@
package haveno.desktop.components;
import com.jfoenix.controls.JFXBadge;
import haveno.core.locale.Res;
import haveno.core.user.Preferences;
import javafx.scene.Node;
import javafx.collections.MapChangeListener;
public class NewBadge extends JFXBadge {
private final String key;
public NewBadge(Node control, String key, Preferences preferences) {
super(control);
this.key = key;
setText(Res.get("shared.new"));
getStyleClass().add("new");
setEnabled(!preferences.getDontShowAgainMap().containsKey(key));
refreshBadge();
preferences.getDontShowAgainMapAsObservable().addListener((MapChangeListener<? super String, ? super Boolean>) change -> {
if (change.getKey().equals(key)) {
setEnabled(!change.wasAdded());
refreshBadge();
}
});
}
}

View file

@ -0,0 +1,35 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import com.jfoenix.controls.JFXPasswordField;
import javafx.scene.control.Skin;
public class PasswordTextField extends JFXPasswordField {
public PasswordTextField() {
super();
setLabelFloat(true);
setMaxWidth(380);
}
@Override
protected Skin<?> createDefaultSkin() {
return new JFXTextFieldSkinHavenoStyle<>(this, 0);
}
}

View file

@ -0,0 +1,228 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import com.google.common.base.Charsets;
import haveno.core.alert.PrivateNotificationManager;
import haveno.core.locale.Res;
import haveno.core.offer.Offer;
import haveno.core.trade.Trade;
import haveno.core.user.Preferences;
import haveno.desktop.main.overlays.editor.PeerInfoWithTagEditor;
import haveno.desktop.util.DisplayUtils;
import haveno.network.p2p.NodeAddress;
import javafx.scene.Group;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.geometry.Point2D;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public class PeerInfoIcon extends Group {
public interface notify {
void avatarTagUpdated();
}
@Setter
private notify callback;
protected Preferences preferences;
protected final String fullAddress;
protected String tooltipText;
protected Label tagLabel;
private Label numTradesLabel;
protected Pane tagPane;
protected Pane numTradesPane;
protected int numTrades = 0;
public PeerInfoIcon(NodeAddress nodeAddress, Preferences preferences) {
this.preferences = preferences;
this.fullAddress = nodeAddress != null ? nodeAddress.getFullAddress() : "";
}
protected void createAvatar(Color ringColor) {
double scaleFactor = getScaleFactor();
double outerSize = 26 * scaleFactor;
Canvas outerBackground = new Canvas(outerSize, outerSize);
GraphicsContext outerBackgroundGc = outerBackground.getGraphicsContext2D();
outerBackgroundGc.setFill(ringColor);
outerBackgroundGc.fillOval(0, 0, outerSize, outerSize);
outerBackground.setLayoutY(1 * scaleFactor);
// inner circle
int maxIndices = 15;
int intValue = 0;
try {
MessageDigest md = MessageDigest.getInstance("SHA1");
byte[] bytes = md.digest(fullAddress.getBytes(Charsets.UTF_8));
intValue = Math.abs(((bytes[0] & 0xFF) << 24) | ((bytes[1] & 0xFF) << 16)
| ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
log.error(e.toString());
}
int index = (intValue % maxIndices) + 1;
double saturation = (intValue % 1000) / 1000d;
int red = (intValue >> 8) % 256;
int green = (intValue >> 16) % 256;
int blue = (intValue >> 24) % 256;
Color innerColor = Color.rgb(red, green, blue);
innerColor = innerColor.deriveColor(1, saturation, 0.8, 1); // reduce saturation and brightness
double innerSize = scaleFactor * 22;
Canvas innerBackground = new Canvas(innerSize, innerSize);
GraphicsContext innerBackgroundGc = innerBackground.getGraphicsContext2D();
innerBackgroundGc.setFill(innerColor);
innerBackgroundGc.fillOval(0, 0, innerSize, innerSize);
innerBackground.setLayoutY(3 * scaleFactor);
innerBackground.setLayoutX(2 * scaleFactor);
ImageView avatarImageView = new ImageView();
avatarImageView.setId("avatar_" + index);
avatarImageView.setLayoutX(0);
avatarImageView.setLayoutY(1 * scaleFactor);
avatarImageView.setFitHeight(scaleFactor * 26);
avatarImageView.setFitWidth(scaleFactor * 26);
numTradesPane = new Pane();
numTradesPane.relocate(scaleFactor * 18, scaleFactor * 14);
numTradesPane.setMouseTransparent(true);
ImageView numTradesCircle = new ImageView();
numTradesCircle.setId("image-green_circle");
numTradesLabel = new AutoTooltipLabel();
numTradesLabel.relocate(scaleFactor * 5, scaleFactor * 1);
numTradesLabel.setId("ident-num-label");
numTradesPane.getChildren().addAll(numTradesCircle, numTradesLabel);
tagPane = new Pane();
tagPane.relocate(Math.round(scaleFactor * 18), scaleFactor * -2);
tagPane.setMouseTransparent(true);
ImageView tagCircle = new ImageView();
tagCircle.setId("image-blue_circle");
tagLabel = new AutoTooltipLabel();
tagLabel.relocate(Math.round(scaleFactor * 5), scaleFactor * 1);
tagLabel.setId("ident-num-label");
tagPane.getChildren().addAll(tagCircle, tagLabel);
updatePeerInfoIcon();
getChildren().addAll(outerBackground, innerBackground, avatarImageView, tagPane, numTradesPane);
}
protected void addMouseListener(int numTrades,
PrivateNotificationManager privateNotificationManager,
@Nullable Trade trade,
Offer offer,
Preferences preferences,
boolean useDevPrivilegeKeys,
boolean isFiatCurrency,
long peersAccountAge,
long peersSignAge,
String peersAccountAgeInfo,
String peersSignAgeInfo,
String accountSigningState) {
final String accountAgeFormatted = isFiatCurrency ?
peersAccountAge > -1 ?
DisplayUtils.formatAccountAge(peersAccountAge) :
Res.get("peerInfo.unknownAge") :
null;
final String signAgeFormatted = isFiatCurrency && peersSignAgeInfo != null ?
peersSignAge > -1 ?
DisplayUtils.formatAccountAge(peersSignAge) :
Res.get("peerInfo.unknownAge") :
null;
setOnMouseClicked(e -> new PeerInfoWithTagEditor(privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys)
.fullAddress(fullAddress)
.numTrades(numTrades)
.accountAge(accountAgeFormatted)
.signAge(signAgeFormatted)
.accountAgeInfo(peersAccountAgeInfo)
.signAgeInfo(peersSignAgeInfo)
.accountSigningState(accountSigningState)
.position(localToScene(new Point2D(0, 0)))
.onSave(newTag -> {
preferences.setTagForPeer(fullAddress, newTag);
updatePeerInfoIcon();
if (callback != null) {
callback.avatarTagUpdated();
}
})
.show());
}
protected double getScaleFactor() {
return 1;
}
protected String getAccountAgeTooltip(Long accountAge) {
return accountAge > -1 ?
Res.get("peerInfoIcon.tooltip.age", DisplayUtils.formatAccountAge(accountAge)) :
Res.get("peerInfoIcon.tooltip.unknownAge");
}
protected void updatePeerInfoIcon() {
String tag;
Map<String, String> peerTagMap = preferences.getPeerTagMap();
if (peerTagMap.containsKey(fullAddress)) {
tag = peerTagMap.get(fullAddress);
final String text = !tag.isEmpty() ? Res.get("peerInfoIcon.tooltip", tooltipText, tag) : tooltipText;
Tooltip.install(this, new Tooltip(text));
} else {
tag = "";
Tooltip.install(this, new Tooltip(tooltipText));
}
if (!tag.isEmpty())
tagLabel.setText(tag.substring(0, 1));
if (numTrades > 0) {
numTradesLabel.setText(numTrades > 99 ? "*" : String.valueOf(numTrades));
double scaleFactor = getScaleFactor();
if (numTrades > 9 && numTrades < 100) {
numTradesLabel.relocate(scaleFactor * 2, scaleFactor * 1);
} else {
numTradesLabel.relocate(scaleFactor * 5, scaleFactor * 1);
}
}
numTradesPane.setVisible(numTrades > 0);
tagPane.setVisible(!tag.isEmpty());
}
}

View file

@ -0,0 +1,47 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import static haveno.desktop.util.Colors.AVATAR_GREY;
import haveno.core.locale.Res;
import haveno.core.user.Preferences;
import haveno.network.p2p.NodeAddress;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class PeerInfoIconDispute extends PeerInfoIcon {
public PeerInfoIconDispute(NodeAddress nodeAddress,
String nrOfDisputes,
long accountAge,
Preferences preferences) {
super(nodeAddress, preferences);
tooltipText = Res.get("peerInfoIcon.tooltip.dispute", fullAddress, nrOfDisputes, getAccountAgeTooltip(accountAge));
// outer circle always display gray
createAvatar(AVATAR_GREY);
addMouseListener(numTrades, null, null, null, preferences, false,
false, accountAge, 0L, null, null, null);
}
public void refreshTag() {
updatePeerInfoIcon();
}
}

View file

@ -0,0 +1,54 @@
package haveno.desktop.components;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.alert.PrivateNotificationManager;
import haveno.core.offer.Offer;
import haveno.core.trade.Trade;
import haveno.core.user.Preferences;
import haveno.network.p2p.NodeAddress;
import javax.annotation.Nullable;
public class PeerInfoIconSmall extends PeerInfoIconTrading {
public PeerInfoIconSmall(NodeAddress nodeAddress,
String role,
Offer offer,
Preferences preferences,
AccountAgeWitnessService accountAgeWitnessService,
boolean useDevPrivilegeKeys) {
// We don't want to show number of trades in that case as it would be unreadable.
// Also we don't need the privateNotificationManager as no interaction will take place with this icon.
super(nodeAddress, role,
0,
null,
offer,
preferences,
accountAgeWitnessService,
useDevPrivilegeKeys);
}
@Override
protected double getScaleFactor() {
return 0.6;
}
@Override
protected void addMouseListener(int numTrades,
PrivateNotificationManager privateNotificationManager,
@Nullable Trade tradeModel,
Offer offer,
Preferences preferences,
boolean useDevPrivilegeKeys,
boolean isFiatCurrency,
long peersAccountAge,
long peersSignAge,
String peersAccountAgeInfo,
String peersSignAgeInfo,
String accountSigningState) {
}
@Override
protected void updatePeerInfoIcon() {
numTradesPane.setVisible(false);
tagPane.setVisible(false);
}
}

View file

@ -0,0 +1,206 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import haveno.common.util.Tuple5;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.alert.PrivateNotificationManager;
import haveno.core.locale.Res;
import haveno.core.offer.Offer;
import haveno.core.payment.payload.PaymentMethod;
import haveno.core.trade.Trade;
import haveno.core.user.Preferences;
import haveno.network.p2p.NodeAddress;
import org.apache.commons.lang3.StringUtils;
import javafx.scene.paint.Color;
import java.util.Date;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
import static com.google.common.base.Preconditions.checkNotNull;
import static haveno.desktop.util.Colors.AVATAR_BLUE;
import static haveno.desktop.util.Colors.AVATAR_GREEN;
import static haveno.desktop.util.Colors.AVATAR_ORANGE;
import static haveno.desktop.util.Colors.AVATAR_RED;
@Slf4j
public class PeerInfoIconTrading extends PeerInfoIcon {
private final AccountAgeWitnessService accountAgeWitnessService;
private boolean isFiatCurrency;
public PeerInfoIconTrading(NodeAddress nodeAddress,
String role,
int numTrades,
PrivateNotificationManager privateNotificationManager,
Offer offer,
Preferences preferences,
AccountAgeWitnessService accountAgeWitnessService,
boolean useDevPrivilegeKeys) {
this(nodeAddress,
role,
numTrades,
privateNotificationManager,
offer,
null,
preferences,
accountAgeWitnessService,
useDevPrivilegeKeys);
}
public PeerInfoIconTrading(NodeAddress nodeAddress,
String role,
int numTrades,
PrivateNotificationManager privateNotificationManager,
Trade Trade,
Preferences preferences,
AccountAgeWitnessService accountAgeWitnessService,
boolean useDevPrivilegeKeys) {
this(nodeAddress,
role,
numTrades,
privateNotificationManager,
Trade.getOffer(),
Trade,
preferences,
accountAgeWitnessService,
useDevPrivilegeKeys);
}
private PeerInfoIconTrading(NodeAddress nodeAddress,
String role,
int numTrades,
PrivateNotificationManager privateNotificationManager,
@Nullable Offer offer,
@Nullable Trade trade,
Preferences preferences,
AccountAgeWitnessService accountAgeWitnessService,
boolean useDevPrivilegeKeys) {
super(nodeAddress, preferences);
this.numTrades = numTrades;
this.accountAgeWitnessService = accountAgeWitnessService;
if (offer == null) {
checkNotNull(trade, "Trade must not be null if offer is null.");
offer = trade.getOffer();
}
checkNotNull(offer, "Offer must not be null");
isFiatCurrency = offer.isFiatOffer();
initialize(role, offer, trade, privateNotificationManager, useDevPrivilegeKeys);
}
protected void initialize(String role,
Offer offer,
Trade trade,
PrivateNotificationManager privateNotificationManager,
boolean useDevPrivilegeKeys) {
boolean hasTraded = numTrades > 0;
Tuple5<Long, Long, String, String, String> peersAccount = getPeersAccountAge(trade, offer);
Long accountAge = peersAccount.first;
Long signAge = peersAccount.second;
tooltipText = hasTraded ?
Res.get("peerInfoIcon.tooltip.trade.traded", role, fullAddress, numTrades, getAccountAgeTooltip(accountAge)) :
Res.get("peerInfoIcon.tooltip.trade.notTraded", role, fullAddress, getAccountAgeTooltip(accountAge));
createAvatar(getRingColor(offer, trade, accountAge, signAge));
addMouseListener(numTrades, privateNotificationManager, trade, offer, preferences, useDevPrivilegeKeys,
isFiatCurrency, accountAge, signAge, peersAccount.third, peersAccount.fourth, peersAccount.fifth);
}
@Override
protected String getAccountAgeTooltip(Long accountAge) {
return isFiatCurrency ? super.getAccountAgeTooltip(accountAge) : "";
}
protected Color getRingColor(Offer offer, Trade Trade, Long accountAge, Long signAge) {
// outer circle
// for altcoins we always display green
Color ringColor = AVATAR_GREEN;
if (isFiatCurrency) {
switch (accountAgeWitnessService.getPeersAccountAgeCategory(hasChargebackRisk(Trade, offer) ? signAge : accountAge)) {
case TWO_MONTHS_OR_MORE:
ringColor = AVATAR_GREEN;
break;
case ONE_TO_TWO_MONTHS:
ringColor = AVATAR_BLUE;
break;
case LESS_ONE_MONTH:
ringColor = AVATAR_ORANGE;
break;
case UNVERIFIED:
default:
ringColor = AVATAR_RED;
break;
}
}
return ringColor;
}
/**
* @param Trade Open trade for trading peer info to be shown
* @param offer Open offer for trading peer info to be shown
* @return account age, sign age, account info, sign info, sign state
*/
private Tuple5<Long, Long, String, String, String> getPeersAccountAge(@Nullable Trade Trade,
@Nullable Offer offer) {
AccountAgeWitnessService.SignState signState = null;
long signAge = -1L;
long accountAge = -1L;
if (Trade != null) {
offer = Trade.getOffer();
if (offer == null) {
// unexpected
return new Tuple5<>(signAge, accountAge, Res.get("peerInfo.age.noRisk"), null, null);
}
if (Trade instanceof Trade) {
Trade trade = Trade;
signState = accountAgeWitnessService.getSignState(trade);
signAge = accountAgeWitnessService.getWitnessSignAge(trade, new Date());
accountAge = accountAgeWitnessService.getAccountAge(trade);
}
} else {
checkNotNull(offer, "Offer must not be null if trade is null.");
signState = accountAgeWitnessService.getSignState(offer);
signAge = accountAgeWitnessService.getWitnessSignAge(offer, new Date());
accountAge = accountAgeWitnessService.getAccountAge(offer);
}
if (signState != null && hasChargebackRisk(Trade, offer)) {
String signAgeInfo = Res.get("peerInfo.age.chargeBackRisk");
String accountSigningState = StringUtils.capitalize(signState.getDisplayString());
if (signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) {
signAgeInfo = null;
}
return new Tuple5<>(accountAge, signAge, Res.get("peerInfo.age.noRisk"), signAgeInfo, accountSigningState);
}
return new Tuple5<>(accountAge, signAge, Res.get("peerInfo.age.noRisk"), null, null);
}
private static boolean hasChargebackRisk(@Nullable Trade Trade, @Nullable Offer offer) {
Offer offerToCheck = Trade != null ? Trade.getOffer() : offer;
return offerToCheck != null &&
PaymentMethod.hasChargebackRisk(offerToCheck.getPaymentMethod(), offerToCheck.getCurrencyCode());
}
}

View file

@ -0,0 +1,71 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import haveno.common.UserThread;
import haveno.desktop.components.controlsfx.control.PopOver;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
public class PopOverWrapper {
private PopOver popover;
private Supplier<PopOver> popoverSupplier;
private boolean hidePopover;
private PopOverState state = PopOverState.HIDDEN;
enum PopOverState {
HIDDEN, SHOWING, SHOWN, HIDING
}
public void showPopOver(Supplier<PopOver> popoverSupplier) {
this.popoverSupplier = popoverSupplier;
hidePopover = false;
if (state == PopOverState.HIDDEN) {
state = PopOverState.SHOWING;
popover = popoverSupplier.get();
UserThread.runAfter(() -> {
state = PopOverState.SHOWN;
if (hidePopover) {
// For some reason, this can result in a brief flicker when invoked
// from a 'runAfter' callback, rather than directly. So make the delay
// very short (25ms) so that we don't reach here often:
hidePopOver();
}
}, 25, TimeUnit.MILLISECONDS);
}
}
public void hidePopOver() {
hidePopover = true;
if (state == PopOverState.SHOWN) {
state = PopOverState.HIDING;
popover.hide();
UserThread.runAfter(() -> {
state = PopOverState.HIDDEN;
if (!hidePopover) {
showPopOver(popoverSupplier);
}
}, 250, TimeUnit.MILLISECONDS);
}
}
}

View file

@ -0,0 +1,59 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import haveno.core.util.SimpleMarkdownParser;
import haveno.desktop.util.GUIUtil;
import javafx.scene.Node;
import javafx.scene.control.Hyperlink;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import java.util.List;
import java.util.stream.Collectors;
public class SimpleMarkdownLabel extends TextFlow {
public SimpleMarkdownLabel(String markdown) {
super();
getStyleClass().add("markdown-label");
if (markdown != null) {
updateContent(markdown);
}
}
public void updateContent(String markdown) {
List<Node> items = SimpleMarkdownParser
.parse(markdown)
.stream()
.map(node -> {
if (node instanceof SimpleMarkdownParser.HyperlinkNode) {
var item = ((SimpleMarkdownParser.HyperlinkNode) node);
Hyperlink hyperlink = new Hyperlink(item.getText());
hyperlink.setOnAction(e -> GUIUtil.openWebPage(item.getHref()));
return hyperlink;
} else {
var item = ((SimpleMarkdownParser.TextNode) node);
return new Text(item.getText());
}
})
.collect(Collectors.toList());
getChildren().setAll(items);
}
}

View file

@ -0,0 +1,89 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.geometry.Insets;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class TableGroupHeadline extends Pane {
private final Label label;
private final StringProperty text = new SimpleStringProperty();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public TableGroupHeadline() {
this("");
}
public TableGroupHeadline(String title) {
text.set(title);
GridPane.setMargin(this, new Insets(-10, -10, -10, -10));
GridPane.setColumnSpan(this, 2);
Pane bg = new StackPane();
bg.setId("table-group-headline");
bg.prefWidthProperty().bind(widthProperty());
bg.prefHeightProperty().bind(heightProperty());
label = new AutoTooltipLabel();
label.textProperty().bind(text);
label.setLayoutX(8);
label.setPadding(new Insets(-8, 7, 0, 5));
setActive();
getChildren().addAll(bg, label);
}
public void setInactive() {
setId("titled-group-bg");
label.setId("titled-group-bg-label");
}
private void setActive() {
setId("titled-group-bg-active");
label.setId("titled-group-bg-label-active");
label.getStyleClass().add("highlight-static");
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public void setText(String text) {
this.text.set(text);
}
}

View file

@ -0,0 +1,120 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import haveno.common.util.Utilities;
import haveno.core.locale.Res;
import com.jfoenix.controls.JFXTextField;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.AnchorPane;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class TextFieldWithCopyIcon extends AnchorPane {
private final StringProperty text = new SimpleStringProperty();
private final TextField textField;
private boolean copyWithoutCurrencyPostFix;
private boolean copyTextAfterDelimiter;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public TextFieldWithCopyIcon() {
this(null);
}
public TextFieldWithCopyIcon(String customStyleClass) {
Label copyIcon = new Label();
copyIcon.setLayoutY(3);
copyIcon.getStyleClass().addAll("icon", "highlight");
copyIcon.setTooltip(new Tooltip(Res.get("shared.copyToClipboard")));
AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY);
copyIcon.setOnMouseClicked(e -> {
String text = getText();
if (text != null && text.length() > 0) {
String copyText;
if (copyWithoutCurrencyPostFix) {
String[] strings = text.split(" ");
if (strings.length > 1)
copyText = strings[0]; // exclude the BTC postfix
else
copyText = text;
} else if (copyTextAfterDelimiter) {
String[] strings = text.split(" ");
if (strings.length > 1)
copyText = strings[2]; // exclude the part before / (slash included)
else
copyText = text;
} else {
copyText = text;
}
Utilities.copyToClipboard(copyText);
}
});
textField = new JFXTextField();
textField.setEditable(false);
if (customStyleClass != null) textField.getStyleClass().add(customStyleClass);
textField.textProperty().bindBidirectional(text);
AnchorPane.setRightAnchor(copyIcon, 5.0);
AnchorPane.setRightAnchor(textField, 30.0);
AnchorPane.setLeftAnchor(textField, 0.0);
textField.focusTraversableProperty().set(focusTraversableProperty().get());
getChildren().addAll(textField, copyIcon);
}
public void setPromptText(String value) {
textField.setPromptText(value);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getter/Setter
///////////////////////////////////////////////////////////////////////////////////////////
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public void setText(String text) {
this.text.set(text);
}
public void setTooltip(Tooltip toolTip) {
textField.setTooltip(toolTip);
}
public void setCopyWithoutCurrencyPostFix(boolean copyWithoutCurrencyPostFix) {
this.copyWithoutCurrencyPostFix = copyWithoutCurrencyPostFix;
}
public void setCopyTextAfterDelimiter(boolean copyTextAfterDelimiter) {
this.copyTextAfterDelimiter = copyTextAfterDelimiter;
}
}

View file

@ -0,0 +1,80 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import haveno.common.UserThread;
import com.jfoenix.controls.JFXTextField;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.text.TextAlignment;
import javafx.geometry.Pos;
import lombok.Getter;
public class TextFieldWithIcon extends AnchorPane {
@Getter
private final Label iconLabel;
@Getter
private final TextField textField;
private final Label dummyTextField;
public TextFieldWithIcon() {
textField = new JFXTextField();
textField.setEditable(false);
textField.setFocusTraversable(false);
setLeftAnchor(textField, 0d);
setRightAnchor(textField, 0d);
dummyTextField = new Label();
dummyTextField.setWrapText(true);
dummyTextField.setAlignment(Pos.CENTER_LEFT);
dummyTextField.setTextAlignment(TextAlignment.LEFT);
dummyTextField.setMouseTransparent(true);
dummyTextField.setFocusTraversable(false);
setLeftAnchor(dummyTextField, 0d);
dummyTextField.setVisible(false);
iconLabel = new Label();
iconLabel.setLayoutX(0);
iconLabel.setLayoutY(3);
dummyTextField.widthProperty().addListener((observable, oldValue, newValue) -> {
iconLabel.setLayoutX(dummyTextField.widthProperty().get() + 20);
});
getChildren().addAll(textField, dummyTextField, iconLabel);
}
public void setIcon(AwesomeIcon iconLabel) {
UserThread.execute(() -> {
AwesomeDude.setIcon(this.iconLabel, iconLabel);
});
}
public void setText(String text) {
UserThread.execute(() -> {
textField.setText(text);
dummyTextField.setText(text);
});
}
}

View file

@ -0,0 +1,98 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
import haveno.desktop.util.FormBuilder;
import haveno.desktop.util.GUIUtil;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.text.Text;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class TitledGroupBg extends Pane {
private final HBox box;
private final Label label;
private final StringProperty text = new SimpleStringProperty();
private Text helpIcon;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public TitledGroupBg() {
GridPane.setMargin(this, new Insets(-10, -10, -10, -10));
GridPane.setColumnSpan(this, 2);
box = new HBox();
box.setSpacing(4);
box.setLayoutX(4);
box.setLayoutY(-8);
box.setPadding(new Insets(0, 7, 0, 5));
box.setAlignment(Pos.CENTER_LEFT);
label = new AutoTooltipLabel();
label.textProperty().bind(text);
setActive();
box.getChildren().add(label);
getChildren().add(box);
}
public void setInactive() {
resetStyles();
getStyleClass().add("titled-group-bg");
label.getStyleClass().add("titled-group-bg-label");
}
private void resetStyles() {
getStyleClass().removeAll("titled-group-bg", "titled-group-bg-active");
label.getStyleClass().removeAll("titled-group-bg-label", "titled-group-bg-label-active");
}
private void setActive() {
resetStyles();
getStyleClass().add("titled-group-bg-active");
label.getStyleClass().add("titled-group-bg-label-active");
}
public StringProperty textProperty() {
return text;
}
public void setText(String text) {
this.text.set(text);
}
public void setHelpUrl(String helpUrl) {
if (helpIcon == null) {
helpIcon = FormBuilder.getIcon(MaterialDesignIcon.HELP_CIRCLE_OUTLINE, "1em");
helpIcon.getStyleClass().addAll("icon", "link-icon");
box.getChildren().add(helpIcon);
}
helpIcon.setOnMouseClicked(e -> GUIUtil.openWebPage(helpUrl));
}
}

View file

@ -0,0 +1,47 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import javafx.scene.control.Labeled;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.text.Text;
public class TooltipUtil {
public static void showTooltipIfTruncated(SkinBase skinBase, Labeled labeled) {
for (Object node : skinBase.getChildren()) {
if (node instanceof Text) {
String displayedText = ((Text) node).getText();
String untruncatedText = labeled.getText();
if (displayedText.equals(untruncatedText)) {
if (labeled.getTooltip() != null) {
labeled.setTooltip(null);
}
} else if (untruncatedText != null && !untruncatedText.trim().isEmpty()) {
final Tooltip tooltip = new Tooltip(untruncatedText);
// Force tooltip to use color, as it takes in some cases the color of the parent label
// and can't be overridden by class or id
tooltip.setStyle("-fx-text-fill: -bs-rd-tooltip-truncated;");
labeled.setTooltip(tooltip);
}
}
}
}
}

View file

@ -0,0 +1,187 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import haveno.common.util.Utilities;
import haveno.core.btc.wallet.XmrWalletService;
import haveno.core.locale.Res;
import haveno.core.user.BlockChainExplorer;
import haveno.core.user.Preferences;
import haveno.desktop.components.indicator.TxConfidenceIndicator;
import haveno.desktop.util.GUIUtil;
import com.jfoenix.controls.JFXTextField;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.AnchorPane;
import lombok.Getter;
import lombok.Setter;
import monero.daemon.model.MoneroTx;
import monero.wallet.model.MoneroWalletListener;
import javax.annotation.Nullable;
public class TxIdTextField extends AnchorPane {
@Setter
private static Preferences preferences;
@Setter
private static XmrWalletService xmrWalletService;
@Getter
private final TextField textField;
private final Tooltip progressIndicatorTooltip;
private final TxConfidenceIndicator txConfidenceIndicator;
private final Label copyIcon, blockExplorerIcon, missingTxWarningIcon;
private MoneroWalletListener txUpdater;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public TxIdTextField() {
txConfidenceIndicator = new TxConfidenceIndicator();
txConfidenceIndicator.setFocusTraversable(false);
txConfidenceIndicator.setMaxSize(20, 20);
txConfidenceIndicator.setId("funds-confidence");
txConfidenceIndicator.setLayoutY(1);
txConfidenceIndicator.setProgress(0);
txConfidenceIndicator.setVisible(false);
AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0);
AnchorPane.setTopAnchor(txConfidenceIndicator, 3.0);
progressIndicatorTooltip = new Tooltip("-");
txConfidenceIndicator.setTooltip(progressIndicatorTooltip);
copyIcon = new Label();
copyIcon.setLayoutY(3);
copyIcon.getStyleClass().addAll("icon", "highlight");
copyIcon.setTooltip(new Tooltip(Res.get("txIdTextField.copyIcon.tooltip")));
AwesomeDude.setIcon(copyIcon, AwesomeIcon.COPY);
AnchorPane.setRightAnchor(copyIcon, 30.0);
Tooltip tooltip = new Tooltip(Res.get("txIdTextField.blockExplorerIcon.tooltip"));
blockExplorerIcon = new Label();
blockExplorerIcon.getStyleClass().addAll("icon", "highlight");
blockExplorerIcon.setTooltip(tooltip);
AwesomeDude.setIcon(blockExplorerIcon, AwesomeIcon.EXTERNAL_LINK);
blockExplorerIcon.setMinWidth(20);
AnchorPane.setRightAnchor(blockExplorerIcon, 52.0);
AnchorPane.setTopAnchor(blockExplorerIcon, 4.0);
missingTxWarningIcon = new Label();
missingTxWarningIcon.getStyleClass().addAll("icon", "error-icon");
AwesomeDude.setIcon(missingTxWarningIcon, AwesomeIcon.WARNING_SIGN);
missingTxWarningIcon.setTooltip(new Tooltip(Res.get("txIdTextField.missingTx.warning.tooltip")));
missingTxWarningIcon.setMinWidth(20);
AnchorPane.setRightAnchor(missingTxWarningIcon, 52.0);
AnchorPane.setTopAnchor(missingTxWarningIcon, 4.0);
missingTxWarningIcon.setVisible(false);
missingTxWarningIcon.setManaged(false);
textField = new JFXTextField();
textField.setId("address-text-field");
textField.setEditable(false);
textField.setTooltip(tooltip);
AnchorPane.setRightAnchor(textField, 80.0);
AnchorPane.setLeftAnchor(textField, 0.0);
textField.focusTraversableProperty().set(focusTraversableProperty().get());
getChildren().addAll(textField, missingTxWarningIcon, blockExplorerIcon, copyIcon, txConfidenceIndicator);
}
public void setup(@Nullable String txId) {
if (txUpdater != null) {
xmrWalletService.removeWalletListener(txUpdater);
txUpdater = null;
}
if (txId == null) {
textField.setText(Res.get("shared.na"));
textField.setId("address-text-field-error");
blockExplorerIcon.setVisible(false);
blockExplorerIcon.setManaged(false);
copyIcon.setVisible(false);
copyIcon.setManaged(false);
txConfidenceIndicator.setVisible(false);
missingTxWarningIcon.setVisible(true);
missingTxWarningIcon.setManaged(true);
return;
}
// listen for tx updates
// TODO: this only listens for new blocks, listen for double spend
txUpdater = new MoneroWalletListener() {
@Override
public void onNewBlock(long lastBlockHeight) {
updateConfidence(txId, false, lastBlockHeight + 1);
}
};
xmrWalletService.addWalletListener(txUpdater);
updateConfidence(txId, true, null);
textField.setText(txId);
textField.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId));
blockExplorerIcon.setOnMouseClicked(mouseEvent -> openBlockExplorer(txId));
copyIcon.setOnMouseClicked(e -> Utilities.copyToClipboard(txId));
}
public void cleanup() {
if (xmrWalletService != null && txUpdater != null) {
xmrWalletService.removeWalletListener(txUpdater);
txUpdater = null;
}
textField.setOnMouseClicked(null);
blockExplorerIcon.setOnMouseClicked(null);
copyIcon.setOnMouseClicked(null);
textField.setText("");
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void openBlockExplorer(String txId) {
if (preferences != null) {
BlockChainExplorer blockChainExplorer = preferences.getBlockChainExplorer();
GUIUtil.openWebPage(blockChainExplorer.txUrl + txId, false);
}
}
private void updateConfidence(String txId, boolean useCache, Long height) {
MoneroTx tx = null;
try {
tx = useCache ? xmrWalletService.getTxWithCache(txId) : xmrWalletService.getTx(txId);
tx.setNumConfirmations(tx.isConfirmed() ? (height == null ? xmrWalletService.getConnectionsService().getLastInfo().getHeight() : height) - tx.getHeight(): 0l); // TODO: don't set if tx.getNumConfirmations() works reliably on non-local testnet
} catch (Exception e) {
// do nothing
}
GUIUtil.updateConfidence(tx, progressIndicatorTooltip, txConfidenceIndicator);
if (txConfidenceIndicator.getProgress() != 0) {
txConfidenceIndicator.setVisible(true);
AnchorPane.setRightAnchor(txConfidenceIndicator, 0.0);
}
if (txConfidenceIndicator.getProgress() >= 1.0 && txUpdater != null) {
xmrWalletService.removeWalletListener(txUpdater); // unregister listener
txUpdater = null;
}
}
}

View file

@ -0,0 +1,95 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.chart;
import haveno.desktop.common.model.ActivatableDataModel;
import java.time.Instant;
import java.time.temporal.TemporalAdjuster;
import java.util.Comparator;
import java.util.Map;
import java.util.function.BinaryOperator;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public abstract class ChartDataModel extends ActivatableDataModel {
protected final TemporalAdjusterModel temporalAdjusterModel = new TemporalAdjusterModel();
protected Predicate<Long> dateFilter = e -> true;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public ChartDataModel() {
super();
}
@Override
public void activate() {
dateFilter = e -> true;
}
///////////////////////////////////////////////////////////////////////////////////////////
// TemporalAdjusterModel delegates
///////////////////////////////////////////////////////////////////////////////////////////
void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) {
temporalAdjusterModel.setTemporalAdjuster(temporalAdjuster);
}
TemporalAdjuster getTemporalAdjuster() {
return temporalAdjusterModel.getTemporalAdjuster();
}
public long toTimeInterval(Instant instant) {
return temporalAdjusterModel.toTimeInterval(instant);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Date filter predicate
///////////////////////////////////////////////////////////////////////////////////////////
public Predicate<Long> getDateFilter() {
return dateFilter;
}
void setDateFilter(long from, long to) {
dateFilter = value -> value >= from && value <= to;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Data
///////////////////////////////////////////////////////////////////////////////////////////
protected abstract void invalidateCache();
protected Map<Long, Long> getMergedMap(Map<Long, Long> map1,
Map<Long, Long> map2,
BinaryOperator<Long> mergeFunction) {
return Stream.concat(map1.entrySet().stream(),
map2.entrySet().stream())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue,
mergeFunction));
}
}

View file

@ -0,0 +1,808 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.chart;
import haveno.common.UserThread;
import haveno.core.locale.Res;
import haveno.desktop.common.view.ActivatableViewAndModel;
import haveno.desktop.components.AutoTooltipSlideToggleButton;
import haveno.desktop.components.AutoTooltipToggleButton;
import javafx.stage.PopupWindow;
import javafx.stage.Stage;
import javafx.scene.Node;
import javafx.scene.chart.Axis;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Label;
import javafx.scene.control.SplitPane;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.event.EventHandler;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.util.Duration;
import java.time.temporal.TemporalAdjuster;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Stream;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
public abstract class ChartView<T extends ChartViewModel<? extends ChartDataModel>> extends ActivatableViewAndModel<VBox, T> {
private Pane center;
private SplitPane timelineNavigation;
protected NumberAxis xAxis, yAxis;
protected LineChart<Number, Number> chart;
private HBox timelineLabels, legendBox2, legendBox3;
private final ToggleGroup timeIntervalToggleGroup = new ToggleGroup();
protected final Set<XYChart.Series<Number, Number>> activeSeries = new HashSet<>();
protected final Map<String, Integer> seriesIndexMap = new HashMap<>();
protected final Map<String, AutoTooltipSlideToggleButton> legendToggleBySeriesName = new HashMap<>();
private final List<Node> dividerNodes = new ArrayList<>();
private final List<Tooltip> dividerNodesTooltips = new ArrayList<>();
private ChangeListener<Number> widthListener;
private ChangeListener<Toggle> timeIntervalChangeListener;
private ListChangeListener<Node> nodeListChangeListener;
private int maxSeriesSize;
private boolean centerPanePressed;
private double x;
@Setter
protected boolean isRadioButtonBehaviour;
@Setter
private int maxDataPointsForShowingSymbols = 100;
private ChangeListener<Number> yAxisWidthListener;
private EventHandler<MouseEvent> dividerMouseDraggedEventHandler;
private final StringProperty fromProperty = new SimpleStringProperty();
private final StringProperty toProperty = new SimpleStringProperty();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public ChartView(T model) {
super(model);
root = new VBox();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void initialize() {
// We need to call prepareInitialize as we are not using FXMLLoader
prepareInitialize();
maxSeriesSize = 0;
centerPanePressed = false;
x = 0;
// Series
createSeries();
// Time interval
HBox timeIntervalBox = getTimeIntervalBox();
// chart
xAxis = getXAxis();
yAxis = getYAxis();
chart = getChart();
// Timeline navigation
addTimelineNavigation();
// Legend
HBox legendBox1 = initLegendsAndGetLegendBox(getSeriesForLegend1());
Collection<XYChart.Series<Number, Number>> seriesForLegend2 = getSeriesForLegend2();
if (seriesForLegend2 != null && !seriesForLegend2.isEmpty()) {
legendBox2 = initLegendsAndGetLegendBox(seriesForLegend2);
}
Collection<XYChart.Series<Number, Number>> seriesForLegend3 = getSeriesForLegend3();
if (seriesForLegend3 != null && !seriesForLegend3.isEmpty()) {
legendBox3 = initLegendsAndGetLegendBox(seriesForLegend3);
}
// Set active series/legends
defineAndAddActiveSeries();
// Put all together
VBox timelineNavigationBox = new VBox();
double paddingLeft = 15;
double paddingRight = 89;
// Y-axis width depends on data so we register a listener to get correct value
yAxisWidthListener = (observable, oldValue, newValue) -> {
double width = newValue.doubleValue();
if (width > 0) {
double rightPadding = width + 14;
VBox.setMargin(timeIntervalBox, new Insets(0, rightPadding, 0, paddingLeft));
VBox.setMargin(timelineNavigation, new Insets(0, rightPadding, 0, paddingLeft));
VBox.setMargin(timelineLabels, new Insets(0, rightPadding, 0, paddingLeft));
VBox.setMargin(legendBox1, new Insets(10, rightPadding, 0, paddingLeft));
if (legendBox2 != null) {
VBox.setMargin(legendBox2, new Insets(-20, rightPadding, 0, paddingLeft));
}
if (legendBox3 != null) {
VBox.setMargin(legendBox3, new Insets(-20, rightPadding, 0, paddingLeft));
}
if (model.getDividerPositions()[0] == 0 && model.getDividerPositions()[1] == 1) {
resetTimeNavigation();
}
}
};
VBox.setMargin(timeIntervalBox, new Insets(0, paddingRight, 0, paddingLeft));
VBox.setMargin(timelineNavigation, new Insets(0, paddingRight, 0, paddingLeft));
VBox.setMargin(timelineLabels, new Insets(0, paddingRight, 0, paddingLeft));
VBox.setMargin(legendBox1, new Insets(0, paddingRight, 0, paddingLeft));
timelineNavigationBox.getChildren().addAll(timelineNavigation, timelineLabels, legendBox1);
if (legendBox2 != null) {
VBox.setMargin(legendBox2, new Insets(-20, paddingRight, 0, paddingLeft));
timelineNavigationBox.getChildren().add(legendBox2);
}
if (legendBox3 != null) {
VBox.setMargin(legendBox3, new Insets(-20, paddingRight, 0, paddingLeft));
timelineNavigationBox.getChildren().add(legendBox3);
}
root.getChildren().addAll(timeIntervalBox, chart, timelineNavigationBox);
// Listeners
widthListener = (observable, oldValue, newValue) -> {
timelineNavigation.setDividerPosition(0, model.getDividerPositions()[0]);
timelineNavigation.setDividerPosition(1, model.getDividerPositions()[1]);
};
timeIntervalChangeListener = (observable, oldValue, newValue) -> {
if (newValue != null) {
onTimeIntervalChanged(newValue);
}
};
nodeListChangeListener = c -> {
while (c.next()) {
if (c.wasAdded()) {
c.getAddedSubList().stream()
.filter(node -> node instanceof Text)
.forEach(node -> node.getStyleClass().add("axis-tick-mark-text-node"));
}
}
};
}
@Override
public void activate() {
timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]);
TemporalAdjuster temporalAdjuster = model.getTemporalAdjuster();
applyTemporalAdjuster(temporalAdjuster);
findTimeIntervalToggleByTemporalAdjuster(temporalAdjuster).ifPresent(timeIntervalToggleGroup::selectToggle);
defineAndAddActiveSeries();
initBoundsForTimelineNavigation();
// Apply listeners and handlers
root.widthProperty().addListener(widthListener);
xAxis.getChildrenUnmodifiable().addListener(nodeListChangeListener);
yAxis.widthProperty().addListener(yAxisWidthListener);
timeIntervalToggleGroup.selectedToggleProperty().addListener(timeIntervalChangeListener);
timelineNavigation.setOnMousePressed(this::onMousePressedSplitPane);
timelineNavigation.setOnMouseDragged(this::onMouseDragged);
center.setOnMousePressed(this::onMousePressedCenter);
center.setOnMouseReleased(this::onMouseReleasedCenter);
addLegendToggleActionHandlers(getSeriesForLegend1());
addLegendToggleActionHandlers(getSeriesForLegend2());
addLegendToggleActionHandlers(getSeriesForLegend3());
addActionHandlersToDividers();
}
@Override
public void deactivate() {
root.widthProperty().removeListener(widthListener);
xAxis.getChildrenUnmodifiable().removeListener(nodeListChangeListener);
yAxis.widthProperty().removeListener(yAxisWidthListener);
timeIntervalToggleGroup.selectedToggleProperty().removeListener(timeIntervalChangeListener);
timelineNavigation.setOnMousePressed(null);
timelineNavigation.setOnMouseDragged(null);
center.setOnMousePressed(null);
center.setOnMouseReleased(null);
removeLegendToggleActionHandlers(getSeriesForLegend1());
removeLegendToggleActionHandlers(getSeriesForLegend2());
removeLegendToggleActionHandlers(getSeriesForLegend3());
removeActionHandlersToDividers();
// clear data, reset states. We keep timeInterval state though
activeSeries.clear();
chart.getData().clear();
legendToggleBySeriesName.values().forEach(e -> e.setSelected(false));
dividerNodes.clear();
dividerNodesTooltips.clear();
model.invalidateCache();
}
///////////////////////////////////////////////////////////////////////////////////////////
// TimeInterval/TemporalAdjuster
///////////////////////////////////////////////////////////////////////////////////////////
protected HBox getTimeIntervalBox() {
ToggleButton year = getTimeIntervalToggleButton(Res.get("time.year"), TemporalAdjusterModel.Interval.YEAR,
timeIntervalToggleGroup, "toggle-left");
ToggleButton halfYear = getTimeIntervalToggleButton(Res.get("time.halfYear"), TemporalAdjusterModel.Interval.HALF_YEAR,
timeIntervalToggleGroup, "toggle-center");
ToggleButton quarter = getTimeIntervalToggleButton(Res.get("time.quarter"), TemporalAdjusterModel.Interval.QUARTER,
timeIntervalToggleGroup, "toggle-center");
ToggleButton month = getTimeIntervalToggleButton(Res.get("time.month"), TemporalAdjusterModel.Interval.MONTH,
timeIntervalToggleGroup, "toggle-center");
ToggleButton week = getTimeIntervalToggleButton(Res.get("time.week"), TemporalAdjusterModel.Interval.WEEK,
timeIntervalToggleGroup, "toggle-center");
ToggleButton day = getTimeIntervalToggleButton(Res.get("time.day"), TemporalAdjusterModel.Interval.DAY,
timeIntervalToggleGroup, "toggle-center");
HBox toggleBox = new HBox();
toggleBox.setSpacing(0);
toggleBox.setAlignment(Pos.CENTER_LEFT);
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
toggleBox.getChildren().addAll(spacer, year, halfYear, quarter, month, week, day);
return toggleBox;
}
private ToggleButton getTimeIntervalToggleButton(String label,
TemporalAdjusterModel.Interval interval,
ToggleGroup toggleGroup,
String style) {
ToggleButton toggleButton = new AutoTooltipToggleButton(label);
toggleButton.setUserData(interval);
toggleButton.setToggleGroup(toggleGroup);
toggleButton.setId(style);
return toggleButton;
}
protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) {
model.applyTemporalAdjuster(temporalAdjuster);
findTimeIntervalToggleByTemporalAdjuster(temporalAdjuster)
.map(e -> (TemporalAdjusterModel.Interval) e.getUserData())
.ifPresent(model::setDateFormatPattern);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Chart
///////////////////////////////////////////////////////////////////////////////////////////
protected NumberAxis getXAxis() {
NumberAxis xAxis = new NumberAxis();
xAxis.setForceZeroInRange(false);
xAxis.setAutoRanging(true);
xAxis.setTickLabelFormatter(model.getTimeAxisStringConverter());
return xAxis;
}
protected NumberAxis getYAxis() {
NumberAxis yAxis = new NumberAxis();
yAxis.setForceZeroInRange(true);
yAxis.setSide(Side.RIGHT);
yAxis.setTickLabelFormatter(model.getYAxisStringConverter());
return yAxis;
}
// Add implementation if update of the y axis is required at series change
protected void onSetYAxisFormatter(XYChart.Series<Number, Number> series) {
}
protected LineChart<Number, Number> getChart() {
LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
chart.setAnimated(false);
chart.setLegendVisible(false);
chart.setMinHeight(200);
chart.setId("charts-dao");
return chart;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Legend
///////////////////////////////////////////////////////////////////////////////////////////
protected HBox initLegendsAndGetLegendBox(Collection<XYChart.Series<Number, Number>> collection) {
HBox hBox = new HBox();
hBox.setSpacing(10);
collection.forEach(series -> {
AutoTooltipSlideToggleButton toggle = new AutoTooltipSlideToggleButton();
toggle.setMinWidth(200);
toggle.setAlignment(Pos.TOP_LEFT);
String seriesId = getSeriesId(series);
legendToggleBySeriesName.put(seriesId, toggle);
toggle.setText(seriesId);
toggle.setId("charts-legend-toggle" + seriesIndexMap.get(seriesId));
toggle.setSelected(false);
hBox.getChildren().add(toggle);
});
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
hBox.getChildren().add(spacer);
return hBox;
}
private void addLegendToggleActionHandlers(@Nullable Collection<XYChart.Series<Number, Number>> collection) {
if (collection != null) {
collection.forEach(series ->
legendToggleBySeriesName.get(getSeriesId(series)).setOnAction(e -> onSelectLegendToggle(series)));
}
}
private void removeLegendToggleActionHandlers(@Nullable Collection<XYChart.Series<Number, Number>> collection) {
if (collection != null) {
collection.forEach(series ->
legendToggleBySeriesName.get(getSeriesId(series)).setOnAction(null));
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Timeline navigation
///////////////////////////////////////////////////////////////////////////////////////////
private void addTimelineNavigation() {
Pane left = new Pane();
center = new Pane();
center.setId("chart-navigation-center-pane");
Pane right = new Pane();
timelineNavigation = new SplitPane(left, center, right);
timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]);
timelineNavigation.setMinHeight(25);
timelineLabels = new HBox();
}
// After initial chart data are created we apply the text from the x-axis ticks to our timeline navigation.
protected void applyTimeLineNavigationLabels() {
timelineLabels.getChildren().clear();
ObservableList<Axis.TickMark<Number>> tickMarks = xAxis.getTickMarks();
int size = tickMarks.size();
for (int i = 0; i < size; i++) {
Axis.TickMark<Number> tickMark = tickMarks.get(i);
Number xValue = tickMark.getValue();
String xValueString;
if (xAxis.getTickLabelFormatter() != null) {
xValueString = xAxis.getTickLabelFormatter().toString(xValue);
} else {
xValueString = String.valueOf(xValue);
}
Label label = new Label(xValueString);
label.setMinHeight(30);
label.setId("chart-navigation-label");
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
if (i < size - 1) {
timelineLabels.getChildren().addAll(label, spacer);
} else {
// After last label we don't add a spacer
timelineLabels.getChildren().add(label);
}
}
}
private void onMousePressedSplitPane(MouseEvent e) {
x = e.getX();
applyFromToDates();
showDividerTooltips();
}
private void onMousePressedCenter(MouseEvent e) {
centerPanePressed = true;
applyFromToDates();
showDividerTooltips();
}
private void onMouseReleasedCenter(MouseEvent e) {
centerPanePressed = false;
onTimelineChanged();
hideDividerTooltips();
}
private void onMouseDragged(MouseEvent e) {
if (centerPanePressed) {
double newX = e.getX();
double width = timelineNavigation.getWidth();
double relativeDelta = (x - newX) / width;
double leftPos = timelineNavigation.getDividerPositions()[0] - relativeDelta;
double rightPos = timelineNavigation.getDividerPositions()[1] - relativeDelta;
// Model might limit application of new values if we hit a boundary
model.onTimelineMouseDrag(leftPos, rightPos);
timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]);
x = newX;
applyFromToDates();
showDividerTooltips();
}
}
private void addActionHandlersToDividers() {
// No API access to dividers ;-( only via css lookup hack (https://stackoverflow.com/questions/40707295/how-to-add-listener-to-divider-position?rq=1)
// Need to be done after added to scene and call requestLayout and applyCss. We keep it in a list atm
// and set action handler in activate.
timelineNavigation.requestLayout();
timelineNavigation.applyCss();
dividerMouseDraggedEventHandler = event -> {
applyFromToDates();
showDividerTooltips();
};
for (Node node : timelineNavigation.lookupAll(".split-pane-divider")) {
dividerNodes.add(node);
node.setOnMouseReleased(e -> {
hideDividerTooltips();
onTimelineChanged();
});
node.addEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler);
Tooltip tooltip = new Tooltip("");
dividerNodesTooltips.add(tooltip);
tooltip.setShowDelay(Duration.millis(300));
tooltip.setShowDuration(Duration.seconds(3));
tooltip.textProperty().bind(dividerNodes.size() == 1 ? fromProperty : toProperty);
Tooltip.install(node, tooltip);
}
}
private void removeActionHandlersToDividers() {
dividerNodes.forEach(node -> {
node.setOnMouseReleased(null);
node.removeEventHandler(MouseEvent.MOUSE_DRAGGED, dividerMouseDraggedEventHandler);
});
for (int i = 0; i < dividerNodesTooltips.size(); i++) {
Tooltip tooltip = dividerNodesTooltips.get(i);
tooltip.textProperty().unbind();
Tooltip.uninstall(dividerNodes.get(i), tooltip);
}
}
private void resetTimeNavigation() {
timelineNavigation.setDividerPositions(0d, 1d);
model.onTimelineNavigationChanged(0, 1);
}
private void showDividerTooltips() {
showDividerTooltip(0);
showDividerTooltip(1);
}
private void hideDividerTooltips() {
dividerNodesTooltips.forEach(PopupWindow::hide);
}
private void showDividerTooltip(int index) {
Node divider = dividerNodes.get(index);
Bounds bounds = divider.localToScene(divider.getBoundsInLocal());
Tooltip tooltip = dividerNodesTooltips.get(index);
double xOffset = index == 0 ? -90 : 10;
Stage stage = (Stage) root.getScene().getWindow();
tooltip.show(stage, stage.getX() + bounds.getMaxX() + xOffset,
stage.getY() + bounds.getMaxY() - 40);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Series
///////////////////////////////////////////////////////////////////////////////////////////
protected abstract void createSeries();
protected abstract Collection<XYChart.Series<Number, Number>> getSeriesForLegend1();
// If a second legend is used this has to be overridden
protected Collection<XYChart.Series<Number, Number>> getSeriesForLegend2() {
return null;
}
protected Collection<XYChart.Series<Number, Number>> getSeriesForLegend3() {
return null;
}
protected abstract void defineAndAddActiveSeries();
protected void activateSeries(XYChart.Series<Number, Number> series) {
if (activeSeries.contains(series)) {
return;
}
chart.getData().add(series);
activeSeries.add(series);
legendToggleBySeriesName.get(getSeriesId(series)).setSelected(true);
applyDataAndUpdate();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Data
///////////////////////////////////////////////////////////////////////////////////////////
protected abstract CompletableFuture<Boolean> applyData();
private void applyDataAndUpdate() {
long ts = System.currentTimeMillis();
applyData().whenComplete((r, t) -> {
log.debug("applyData took {}", System.currentTimeMillis() - ts);
long ts2 = System.currentTimeMillis();
updateChartAfterDataChange();
log.debug("updateChartAfterDataChange took {}", System.currentTimeMillis() - ts2);
onDataApplied();
});
}
/**
* Implementations define which series will be used for setBoundsForTimelineNavigation
*/
protected abstract void initBoundsForTimelineNavigation();
/**
* @param data The series data which determines the min/max x values for the time line navigation.
* If not applicable initBoundsForTimelineNavigation requires custom implementation.
*/
protected void setBoundsForTimelineNavigation(ObservableList<XYChart.Data<Number, Number>> data) {
model.initBounds(data);
xAxis.setLowerBound(model.getLowerBound().doubleValue());
xAxis.setUpperBound(model.getUpperBound().doubleValue());
}
///////////////////////////////////////////////////////////////////////////////////////////
// Handlers triggering a data/chart update
///////////////////////////////////////////////////////////////////////////////////////////
private void onTimeIntervalChanged(Toggle newValue) {
TemporalAdjusterModel.Interval interval = (TemporalAdjusterModel.Interval) newValue.getUserData();
applyTemporalAdjuster(interval.getAdjuster());
model.invalidateCache();
applyDataAndUpdate();
}
private void onTimelineChanged() {
updateTimeLinePositions();
model.invalidateCache();
applyDataAndUpdate();
}
private void updateTimeLinePositions() {
double leftPos = timelineNavigation.getDividerPositions()[0];
double rightPos = timelineNavigation.getDividerPositions()[1];
model.onTimelineNavigationChanged(leftPos, rightPos);
// We need to update as model might have adjusted the values
timelineNavigation.setDividerPositions(model.getDividerPositions()[0], model.getDividerPositions()[1]);
fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " "));
toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " "));
}
private void applyFromToDates() {
double leftPos = timelineNavigation.getDividerPositions()[0];
double rightPos = timelineNavigation.getDividerPositions()[1];
model.applyFromToDates(leftPos, rightPos);
fromProperty.set(model.getTimeAxisStringConverter().toString(model.getFromDate()).replace("\n", " "));
toProperty.set(model.getTimeAxisStringConverter().toString(model.getToDate()).replace("\n", " "));
}
private void onSelectLegendToggle(XYChart.Series<Number, Number> series) {
boolean isSelected = legendToggleBySeriesName.get(getSeriesId(series)).isSelected();
// If we have set that flag we deselect all other toggles
if (isRadioButtonBehaviour) {
new ArrayList<>(chart.getData()).stream() // We need to copy to a new list to avoid ConcurrentModificationException
.filter(activeSeries::contains)
.forEach(seriesToRemove -> {
chart.getData().remove(seriesToRemove);
String seriesId = getSeriesId(seriesToRemove);
activeSeries.remove(seriesToRemove);
legendToggleBySeriesName.get(seriesId).setSelected(false);
});
}
if (isSelected) {
chart.getData().add(series);
activeSeries.add(series);
applyDataAndUpdate();
if (isRadioButtonBehaviour) {
// We support different y-axis formats only if isRadioButtonBehaviour is set, otherwise we would get
// mixed data on y-axis
onSetYAxisFormatter(series);
}
} else if (!isRadioButtonBehaviour) { // if isRadioButtonBehaviour we have removed it already via the code above
chart.getData().remove(series);
activeSeries.remove(series);
updateChartAfterDataChange();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Chart update after data change
///////////////////////////////////////////////////////////////////////////////////////////
// Update of the chart data can be triggered by:
// 1. activate()
// 2. TimeInterval toggle change
// 3. Timeline navigation change
// 4. Legend/series toggle change
// Timeline navigation and legend/series toggles get reset at activate.
// Time interval toggle keeps its state at screen changes.
protected void updateChartAfterDataChange() {
// If a series got no data points after update we need to clear it from the chart
cleanupDanglingSeries();
// Hides symbols if too many data points are created
updateSymbolsVisibility();
// When series gets added/removed the JavaFx charts framework would try to apply styles by the index of
// addition, but we want to use a static color assignment which is synced with the legend color.
applySeriesStyles();
// Set tooltip on symbols
applyTooltip();
}
private void cleanupDanglingSeries() {
List<XYChart.Series<Number, Number>> activeSeriesList = new ArrayList<>(activeSeries);
activeSeriesList.forEach(series -> {
ObservableList<XYChart.Series<Number, Number>> seriesOnChart = chart.getData();
if (series.getData().isEmpty()) {
seriesOnChart.remove(series);
} else if (!seriesOnChart.contains(series)) {
seriesOnChart.add(series);
}
});
}
private void updateSymbolsVisibility() {
maxDataPointsForShowingSymbols = 100;
long numDataPoints = chart.getData().stream()
.map(XYChart.Series::getData)
.mapToLong(List::size)
.max()
.orElse(0);
boolean prevValue = chart.getCreateSymbols();
boolean newValue = numDataPoints < maxDataPointsForShowingSymbols;
if (prevValue != newValue) {
chart.setCreateSymbols(newValue);
}
}
// The chart framework assigns the colored depending on the order it got added, but want to keep colors
// the same so they match with the legend toggle.
private void applySeriesStyles() {
for (int index = 0; index < chart.getData().size(); index++) {
XYChart.Series<Number, Number> series = chart.getData().get(index);
int staticIndex = seriesIndexMap.get(getSeriesId(series));
Set<Node> lines = getNodesForStyle(series.getNode(), ".default-color%d.chart-series-line");
Stream<Node> symbols = series.getData().stream().map(XYChart.Data::getNode)
.flatMap(node -> getNodesForStyle(node, ".default-color%d.chart-line-symbol").stream());
Stream.concat(lines.stream(), symbols).forEach(node -> {
removeStyles(node);
node.getStyleClass().add("default-color" + staticIndex);
});
}
}
private void applyTooltip() {
chart.getData().forEach(series -> {
series.getData().forEach(data -> {
Node node = data.getNode();
if (node == null) {
return;
}
String xValue = model.getTooltipDateConverter(data.getXValue());
String yValue = model.getYAxisStringConverter().toString(data.getYValue());
Tooltip.install(node, new Tooltip(Res.get("dao.factsAndFigures.supply.chart.tradeFee.toolTip", yValue, xValue)));
});
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private void removeStyles(Node node) {
for (int i = 0; i < getMaxSeriesSize(); i++) {
node.getStyleClass().remove("default-color" + i);
}
}
private Set<Node> getNodesForStyle(Node node, String style) {
Set<Node> result = new HashSet<>();
if (node != null) {
for (int i = 0; i < getMaxSeriesSize(); i++) {
result.addAll(node.lookupAll(String.format(style, i)));
}
}
return result;
}
private int getMaxSeriesSize() {
maxSeriesSize = Math.max(maxSeriesSize, chart.getData().size());
return maxSeriesSize;
}
private Optional<Toggle> findTimeIntervalToggleByTemporalAdjuster(TemporalAdjuster adjuster) {
return timeIntervalToggleGroup.getToggles().stream()
.filter(toggle -> ((TemporalAdjusterModel.Interval) toggle.getUserData()).getAdjuster().equals(adjuster))
.findAny();
}
// We use the name as id as there is no other suitable data inside series
protected String getSeriesId(XYChart.Series<Number, Number> series) {
return series.getName();
}
protected void mapToUserThread(Runnable command) {
UserThread.execute(command);
}
protected void onDataApplied() {
// Once we have data applied we need to call initBoundsForTimelineNavigation again
if (model.upperBound.longValue() == 0) {
initBoundsForTimelineNavigation();
}
}
}

View file

@ -0,0 +1,256 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.chart;
import haveno.common.util.Tuple2;
import haveno.desktop.common.model.ActivatableWithDataModel;
import haveno.desktop.util.DisplayUtils;
import javafx.scene.chart.XYChart;
import javafx.util.StringConverter;
import java.time.temporal.TemporalAdjuster;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public abstract class ChartViewModel<T extends ChartDataModel> extends ActivatableWithDataModel<T> {
private final static double LEFT_TIMELINE_SNAP_VALUE = 0.01;
private final static double RIGHT_TIMELINE_SNAP_VALUE = 0.99;
@Getter
private final Double[] dividerPositions = new Double[]{0d, 1d};
@Getter
protected Number lowerBound;
@Getter
protected Number upperBound;
@Getter
protected String dateFormatPatters = "dd MMM\nyyyy";
@Getter
long fromDate;
@Getter
long toDate;
public ChartViewModel(T dataModel) {
super(dataModel);
}
@Override
public void activate() {
dividerPositions[0] = 0d;
dividerPositions[1] = 1d;
}
///////////////////////////////////////////////////////////////////////////////////////////
// TimerInterval/TemporalAdjuster
///////////////////////////////////////////////////////////////////////////////////////////
protected void applyTemporalAdjuster(TemporalAdjuster temporalAdjuster) {
dataModel.setTemporalAdjuster(temporalAdjuster);
}
void setDateFormatPattern(TemporalAdjusterModel.Interval interval) {
switch (interval) {
case YEAR:
dateFormatPatters = "yyyy";
break;
case HALF_YEAR:
case QUARTER:
case MONTH:
dateFormatPatters = "MMM\nyyyy";
break;
default:
dateFormatPatters = "MMM dd\nyyyy";
break;
}
}
protected TemporalAdjuster getTemporalAdjuster() {
return dataModel.getTemporalAdjuster();
}
///////////////////////////////////////////////////////////////////////////////////////////
// TimelineNavigation
///////////////////////////////////////////////////////////////////////////////////////////
void onTimelineNavigationChanged(double leftPos, double rightPos) {
applyFromToDates(leftPos, rightPos);
// TODO find better solution
// The TemporalAdjusters map dates to the lower bound (e.g. 1.1.2016) but our from date is the date of
// the first data entry so if we filter by that we would exclude the first year data in case YEAR was selected
// A trade with data 3.May.2016 gets mapped to 1.1.2016 and our from date will be April 2016, so we would
// filter that. It is a bit tricky to sync the TemporalAdjusters with our date filter. To include at least in
// the case when we have not set the date filter (left =0 / right =1) we set from date to epoch time 0 and
// to date to one year ahead to be sure we include all.
long from, to;
// We only manipulate the from, to variables for the date filter, not the fromDate, toDate properties as those
// are used by the view for tooltip over the time line navigation dividers
if (leftPos < LEFT_TIMELINE_SNAP_VALUE) {
from = 0;
} else {
from = fromDate;
}
if (rightPos > RIGHT_TIMELINE_SNAP_VALUE) {
to = new Date().getTime() / 1000 + TimeUnit.DAYS.toSeconds(365);
} else {
to = toDate;
}
dividerPositions[0] = leftPos;
dividerPositions[1] = rightPos;
dataModel.setDateFilter(from, to);
}
void applyFromToDates(double leftPos, double rightPos) {
// We need to snap into the 0 and 1 values once we are close as otherwise once navigation has been used we
// would not get back to exact 0 or 1. Not clear why but might be rounding issues from values at x positions of
// drag operations.
if (leftPos < LEFT_TIMELINE_SNAP_VALUE) {
leftPos = 0;
}
if (rightPos > RIGHT_TIMELINE_SNAP_VALUE) {
rightPos = 1;
}
long lowerBoundAsLong = lowerBound.longValue();
long totalRange = upperBound.longValue() - lowerBoundAsLong;
fromDate = (long) (lowerBoundAsLong + totalRange * leftPos);
toDate = (long) (lowerBoundAsLong + totalRange * rightPos);
}
void onTimelineMouseDrag(double leftPos, double rightPos) {
// Limit drag operation if we have hit a boundary
if (leftPos > LEFT_TIMELINE_SNAP_VALUE) {
dividerPositions[1] = rightPos;
}
if (rightPos < RIGHT_TIMELINE_SNAP_VALUE) {
dividerPositions[0] = leftPos;
}
}
void initBounds(List<XYChart.Data<Number, Number>> data1,
List<XYChart.Data<Number, Number>> data2) {
Tuple2<Double, Double> xMinMaxTradeFee = getMinMax(data1);
Tuple2<Double, Double> xMinMaxCompensationRequest = getMinMax(data2);
lowerBound = Math.min(xMinMaxTradeFee.first, xMinMaxCompensationRequest.first);
upperBound = Math.max(xMinMaxTradeFee.second, xMinMaxCompensationRequest.second);
}
void initBounds(List<XYChart.Data<Number, Number>> data) {
Tuple2<Double, Double> xMinMaxTradeFee = getMinMax(data);
lowerBound = xMinMaxTradeFee.first;
upperBound = xMinMaxTradeFee.second;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Chart
///////////////////////////////////////////////////////////////////////////////////////////
StringConverter<Number> getTimeAxisStringConverter() {
return new StringConverter<>() {
@Override
public String toString(Number epochSeconds) {
Date date = new Date(epochSeconds.longValue() * 1000);
return DisplayUtils.formatDateAxis(date, getDateFormatPatters());
}
@Override
public Number fromString(String string) {
return 0;
}
};
}
protected StringConverter<Number> getYAxisStringConverter() {
return new StringConverter<>() {
@Override
public String toString(Number value) {
return String.valueOf(value);
}
@Override
public Number fromString(String string) {
return null;
}
};
}
String getTooltipDateConverter(Number date) {
return getTimeAxisStringConverter().toString(date).replace("\n", " ");
}
protected String getTooltipValueConverter(Number value) {
return getYAxisStringConverter().toString(value);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Data
///////////////////////////////////////////////////////////////////////////////////////////
protected void invalidateCache() {
dataModel.invalidateCache();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
protected List<XYChart.Data<Number, Number>> toChartData(Map<Long, Long> map) {
return map.entrySet().stream()
.map(entry -> new XYChart.Data<Number, Number>(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
protected List<XYChart.Data<Number, Number>> toChartDoubleData(Map<Long, Double> map) {
return map.entrySet().stream()
.map(entry -> new XYChart.Data<Number, Number>(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
protected List<XYChart.Data<Number, Number>> toChartLongData(Map<Long, Long> map) {
return map.entrySet().stream()
.map(entry -> new XYChart.Data<Number, Number>(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
}
private Tuple2<Double, Double> getMinMax(List<XYChart.Data<Number, Number>> chartData) {
long min = Long.MAX_VALUE, max = 0;
for (XYChart.Data<Number, ?> data : chartData) {
long value = data.getXValue().longValue();
min = Math.min(value, min);
max = Math.max(value, max);
}
return new Tuple2<>((double) min, (double) max);
}
}

View file

@ -0,0 +1,107 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.chart;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.temporal.TemporalAdjuster;
import java.time.temporal.TemporalAdjusters;
import java.math.RoundingMode;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static java.time.temporal.ChronoField.DAY_OF_YEAR;
import haveno.common.util.MathUtils;
@Slf4j
public class TemporalAdjusterModel {
private static final ZoneId ZONE_ID = ZoneId.systemDefault();
public enum Interval {
YEAR(TemporalAdjusters.firstDayOfYear()),
HALF_YEAR(temporal -> {
long halfYear = temporal.range(DAY_OF_YEAR).getMaximum() / 2;
int dayOfYear = 0;
if (temporal instanceof LocalDate) {
dayOfYear = ((LocalDate) temporal).getDayOfYear(); // getDayOfYear delivers 1-365 (366 in leap years)
}
if (dayOfYear <= halfYear) {
return temporal.with(DAY_OF_YEAR, 1);
} else {
return temporal.with(DAY_OF_YEAR, halfYear + 1);
}
}),
QUARTER(temporal -> {
long quarter1 = temporal.range(DAY_OF_YEAR).getMaximum() / 4;
long halfYear = temporal.range(DAY_OF_YEAR).getMaximum() / 2;
long quarter3 = MathUtils.roundDoubleToLong(temporal.range(DAY_OF_YEAR).getMaximum() * 0.75, RoundingMode.FLOOR);
int dayOfYear = 0;
if (temporal instanceof LocalDate) {
dayOfYear = ((LocalDate) temporal).getDayOfYear();
}
if (dayOfYear <= quarter1) {
return temporal.with(DAY_OF_YEAR, 1);
} else if (dayOfYear <= halfYear) {
return temporal.with(DAY_OF_YEAR, quarter1 + 1);
} else if (dayOfYear <= quarter3) {
return temporal.with(DAY_OF_YEAR, halfYear + 1);
} else {
return temporal.with(DAY_OF_YEAR, quarter3 + 1);
}
}),
MONTH(TemporalAdjusters.firstDayOfMonth()),
WEEK(TemporalAdjusters.next(DayOfWeek.MONDAY)),
DAY(TemporalAdjusters.ofDateAdjuster(d -> d));
@Getter
private final TemporalAdjuster adjuster;
Interval(TemporalAdjuster adjuster) {
this.adjuster = adjuster;
}
}
protected TemporalAdjuster temporalAdjuster = Interval.DAY.getAdjuster();
public void setTemporalAdjuster(TemporalAdjuster temporalAdjuster) {
this.temporalAdjuster = temporalAdjuster;
}
public TemporalAdjuster getTemporalAdjuster() {
return temporalAdjuster;
}
public long toTimeInterval(Instant instant) {
return toTimeInterval(instant, temporalAdjuster);
}
public long toTimeInterval(Instant instant, TemporalAdjuster temporalAdjuster) {
return instant
.atZone(ZONE_ID)
.toLocalDate()
.with(temporalAdjuster)
.atStartOfDay(ZONE_ID)
.toInstant()
.getEpochSecond();
}
}

View file

@ -0,0 +1,13 @@
This package is a very minimal subset of the external library `controlsfx`.
Three files were embedded into the project to avoid having `controlsfx` as dependency.
This is based on version `8.0.6_20` tagged in commit 6a52afec3ef16094cda281abc80b4daa3d3bf1fd:
[https://github.com/controlsfx/controlsfx/commit/6a52afec3ef16094cda281abc80b4daa3d3bf1fd]
These specific files got raw copied (with package name adjustment):
[https://github.com/controlsfx/controlsfx/blob/6a52afec3ef16094cda281abc80b4daa3d3bf1fd/controlsfx/src/main/java/org/controlsfx/control/PopOver.java]
[https://github.com/controlsfx/controlsfx/blob/6a52afec3ef16094cda281abc80b4daa3d3bf1fd/controlsfx/src/main/java/impl/org/controlsfx/skin/PopOverSkin.java]
[https://github.com/controlsfx/controlsfx/blob/6a52afec3ef16094cda281abc80b4daa3d3bf1fd/controlsfx/src/main/resources/org/controlsfx/control/popover.css]

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,36 @@
.popover {
-fx-background-color: transparent;
}
.popover > .border {
-fx-stroke: linear-gradient(to bottom, rgba(0, 0, 0, .3), rgba(0, 0, 0, .7));
-fx-stroke-width: 1;
-fx-fill: rgba(255.0, 255.0, 255.0, .95);
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, .2), 10.0, 0.5, 2.0, 2.0);
}
.popover > .content {
}
.popover > .detached {
}
.popover > .content > .title > .text {
-fx-padding: 6.0 6.0 0.0 6.0;
-fx-text-fill: rgba(120, 120, 120, .8);
-fx-font-weight: bold;
}
.popover > .content > .title > .icon {
-fx-padding: 6.0 0.0 0.0 10.0;
}
.popover > .content > .title > .icon > .graphics > .circle {
-fx-fill: gray;
-fx-effect: innershadow(gaussian, rgba(0, 0, 0, .2), 3, 0.5, 1.0, 1.0);
}
.popover > .content > .title > .icon > .graphics > .line {
-fx-stroke: white;
-fx-stroke-width: 2;
}

View file

@ -0,0 +1,717 @@
/*
* Copyright (c) 2013 - 2015, ControlsFX
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package haveno.desktop.components.controlsfx.skin;
import javafx.stage.Window;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.HLineTo;
import javafx.scene.shape.Line;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.scene.shape.QuadCurveTo;
import javafx.scene.shape.VLineTo;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.event.EventHandler;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation.*;
import static java.lang.Double.MAX_VALUE;
import static javafx.geometry.Pos.CENTER_LEFT;
import static javafx.scene.control.ContentDisplay.GRAPHIC_ONLY;
import static javafx.scene.paint.Color.YELLOW;
import haveno.desktop.components.controlsfx.control.PopOver;
import haveno.desktop.components.controlsfx.control.PopOver.ArrowLocation;
public class PopOverSkin implements Skin<PopOver> {
private static final String DETACHED_STYLE_CLASS = "detached"; //$NON-NLS-1$
private double xOffset;
private double yOffset;
private boolean tornOff;
private final Path path;
private final Path clip;
private final BorderPane content;
private final StackPane titlePane;
private final StackPane stackPane;
private Point2D dragStartLocation;
private final PopOver popOver;
private final Logger log = LoggerFactory.getLogger(this.getClass());
public PopOverSkin(final PopOver popOver) {
this.popOver = popOver;
stackPane = popOver.getRoot();
stackPane.setPickOnBounds(false);
Bindings.bindContent(stackPane.getStyleClass(), popOver.getStyleClass());
/*
* The min width and height equal 2 * corner radius + 2 * arrow indent +
* 2 * arrow size.
*/
stackPane.minWidthProperty().bind(
Bindings.add(Bindings.multiply(2, popOver.arrowSizeProperty()),
Bindings.add(
Bindings.multiply(2,
popOver.cornerRadiusProperty()),
Bindings.multiply(2,
popOver.arrowIndentProperty()))));
stackPane.minHeightProperty().bind(stackPane.minWidthProperty());
Label title = new Label();
title.textProperty().bind(popOver.titleProperty());
title.setMaxSize(MAX_VALUE, MAX_VALUE);
title.setAlignment(Pos.CENTER);
title.getStyleClass().add("text"); //$NON-NLS-1$
Label closeIcon = new Label();
closeIcon.setGraphic(createCloseIcon());
closeIcon.setMaxSize(MAX_VALUE, MAX_VALUE);
closeIcon.setContentDisplay(GRAPHIC_ONLY);
closeIcon.visibleProperty().bind(
popOver.closeButtonEnabledProperty().and(
popOver.detachedProperty().or(popOver.headerAlwaysVisibleProperty())));
closeIcon.getStyleClass().add("icon"); //$NON-NLS-1$
closeIcon.setAlignment(CENTER_LEFT);
closeIcon.getGraphic().setOnMouseClicked(evt -> popOver.hide());
titlePane = new StackPane();
titlePane.getChildren().add(title);
titlePane.getChildren().add(closeIcon);
titlePane.getStyleClass().add("title"); //$NON-NLS-1$
content = new BorderPane();
content.setCenter(popOver.getContentNode());
content.getStyleClass().add("content"); //$NON-NLS-1$
if (popOver.isDetached() || popOver.isHeaderAlwaysVisible()) {
content.setTop(titlePane);
}
if (popOver.isDetached()) {
popOver.getStyleClass().add(DETACHED_STYLE_CLASS);
content.getStyleClass().add(DETACHED_STYLE_CLASS);
}
popOver.headerAlwaysVisibleProperty().addListener((o, oV, isVisible) -> {
if (isVisible) {
content.setTop(titlePane);
} else if (!popOver.isDetached()) {
content.setTop(null);
}
});
InvalidationListener updatePathListener = observable -> updatePath();
getPopupWindow().xProperty().addListener(updatePathListener);
getPopupWindow().yProperty().addListener(updatePathListener);
popOver.arrowLocationProperty().addListener(updatePathListener);
popOver.contentNodeProperty().addListener(
(value, oldContent, newContent) -> content
.setCenter(newContent));
popOver.detachedProperty()
.addListener((value, oldDetached, newDetached) -> {
if (newDetached) {
popOver.getStyleClass().add(DETACHED_STYLE_CLASS);
content.getStyleClass().add(DETACHED_STYLE_CLASS);
content.setTop(titlePane);
switch (getSkinnable().getArrowLocation()) {
case LEFT_TOP:
case LEFT_CENTER:
case LEFT_BOTTOM:
popOver.setAnchorX(
popOver.getAnchorX() + popOver.getArrowSize());
break;
case TOP_LEFT:
case TOP_CENTER:
case TOP_RIGHT:
popOver.setAnchorY(
popOver.getAnchorY() + popOver.getArrowSize());
break;
default:
break;
}
} else {
popOver.getStyleClass().remove(DETACHED_STYLE_CLASS);
content.getStyleClass().remove(DETACHED_STYLE_CLASS);
if (!popOver.isHeaderAlwaysVisible()) {
content.setTop(null);
}
}
popOver.sizeToScene();
updatePath();
});
path = new Path();
path.getStyleClass().add("border"); //$NON-NLS-1$
path.setManaged(false);
clip = new Path();
/*
* The clip is a path and the path has to be filled with a color.
* Otherwise clipping will not work.
*/
clip.setFill(YELLOW);
createPathElements();
updatePath();
final EventHandler<MouseEvent> mousePressedHandler = evt -> {
log.info("mousePressed:" + popOver.isDetachable() + "," + popOver.isDetached());
if (popOver.isDetachable() || popOver.isDetached()) {
tornOff = false;
xOffset = evt.getScreenX();
yOffset = evt.getScreenY();
dragStartLocation = new Point2D(xOffset, yOffset);
}
};
final EventHandler<MouseEvent> mouseReleasedHandler = evt -> {
log.info("mouseReleased:tornOff" + tornOff + ", " + !getSkinnable().isDetached());
if (tornOff && !getSkinnable().isDetached()) {
tornOff = false;
getSkinnable().detach();
}
};
final EventHandler<MouseEvent> mouseDragHandler = evt -> {
log.info("mouseDrag:" + popOver.isDetachable() + "," + popOver.isDetached());
if (popOver.isDetachable() || popOver.isDetached()) {
double deltaX = evt.getScreenX() - xOffset;
double deltaY = evt.getScreenY() - yOffset;
Window window = getSkinnable().getScene().getWindow();
window.setX(window.getX() + deltaX);
window.setY(window.getY() + deltaY);
xOffset = evt.getScreenX();
yOffset = evt.getScreenY();
if (dragStartLocation.distance(xOffset, yOffset) > 20) {
tornOff = true;
updatePath();
} else if (tornOff) {
tornOff = false;
updatePath();
}
}
};
stackPane.setOnMousePressed(mousePressedHandler);
stackPane.setOnMouseDragged(mouseDragHandler);
stackPane.setOnMouseReleased(mouseReleasedHandler);
stackPane.getChildren().add(path);
stackPane.getChildren().add(content);
content.setClip(clip);
}
@Override
public Node getNode() {
return stackPane;
}
@Override
public PopOver getSkinnable() {
return popOver;
}
@Override
public void dispose() {
}
private Node createCloseIcon() {
Group group = new Group();
group.getStyleClass().add("graphics"); //$NON-NLS-1$
Circle circle = new Circle();
circle.getStyleClass().add("circle"); //$NON-NLS-1$
circle.setRadius(6);
circle.setCenterX(6);
circle.setCenterY(6);
group.getChildren().add(circle);
Line line1 = new Line();
line1.getStyleClass().add("line"); //$NON-NLS-1$
line1.setStartX(4);
line1.setStartY(4);
line1.setEndX(8);
line1.setEndY(8);
group.getChildren().add(line1);
Line line2 = new Line();
line2.getStyleClass().add("line"); //$NON-NLS-1$
line2.setStartX(8);
line2.setStartY(4);
line2.setEndX(4);
line2.setEndY(8);
group.getChildren().add(line2);
return group;
}
private MoveTo moveTo;
private QuadCurveTo topCurveTo, rightCurveTo, bottomCurveTo, leftCurveTo;
private HLineTo lineBTop, lineETop, lineHTop, lineKTop;
private LineTo lineCTop, lineDTop, lineFTop, lineGTop, lineITop, lineJTop;
private VLineTo lineBRight, lineERight, lineHRight, lineKRight;
private LineTo lineCRight, lineDRight, lineFRight, lineGRight, lineIRight,
lineJRight;
private HLineTo lineBBottom, lineEBottom, lineHBottom, lineKBottom;
private LineTo lineCBottom, lineDBottom, lineFBottom, lineGBottom,
lineIBottom, lineJBottom;
private VLineTo lineBLeft, lineELeft, lineHLeft, lineKLeft;
private LineTo lineCLeft, lineDLeft, lineFLeft, lineGLeft, lineILeft,
lineJLeft;
private void createPathElements() {
DoubleProperty centerYProperty = new SimpleDoubleProperty();
DoubleProperty centerXProperty = new SimpleDoubleProperty();
DoubleProperty leftEdgeProperty = new SimpleDoubleProperty();
DoubleProperty leftEdgePlusRadiusProperty = new SimpleDoubleProperty();
DoubleProperty topEdgeProperty = new SimpleDoubleProperty();
DoubleProperty topEdgePlusRadiusProperty = new SimpleDoubleProperty();
DoubleProperty rightEdgeProperty = new SimpleDoubleProperty();
DoubleProperty rightEdgeMinusRadiusProperty = new SimpleDoubleProperty();
DoubleProperty bottomEdgeProperty = new SimpleDoubleProperty();
DoubleProperty bottomEdgeMinusRadiusProperty = new SimpleDoubleProperty();
DoubleProperty cornerProperty = getSkinnable().cornerRadiusProperty();
DoubleProperty arrowSizeProperty = getSkinnable().arrowSizeProperty();
DoubleProperty arrowIndentProperty = getSkinnable()
.arrowIndentProperty();
centerYProperty.bind(Bindings.divide(stackPane.heightProperty(), 2));
centerXProperty.bind(Bindings.divide(stackPane.widthProperty(), 2));
leftEdgePlusRadiusProperty.bind(Bindings.add(leftEdgeProperty,
getSkinnable().cornerRadiusProperty()));
topEdgePlusRadiusProperty.bind(Bindings.add(topEdgeProperty,
getSkinnable().cornerRadiusProperty()));
rightEdgeProperty.bind(stackPane.widthProperty());
rightEdgeMinusRadiusProperty.bind(Bindings.subtract(rightEdgeProperty,
getSkinnable().cornerRadiusProperty()));
bottomEdgeProperty.bind(stackPane.heightProperty());
bottomEdgeMinusRadiusProperty.bind(Bindings.subtract(
bottomEdgeProperty, getSkinnable().cornerRadiusProperty()));
// INIT
moveTo = new MoveTo();
moveTo.xProperty().bind(leftEdgePlusRadiusProperty);
moveTo.yProperty().bind(topEdgeProperty);
//
// TOP EDGE
//
lineBTop = new HLineTo();
lineBTop.xProperty().bind(
Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty));
lineCTop = new LineTo();
lineCTop.xProperty().bind(
Bindings.add(lineBTop.xProperty(), arrowSizeProperty));
lineCTop.yProperty().bind(
Bindings.subtract(topEdgeProperty, arrowSizeProperty));
lineDTop = new LineTo();
lineDTop.xProperty().bind(
Bindings.add(lineCTop.xProperty(), arrowSizeProperty));
lineDTop.yProperty().bind(topEdgeProperty);
lineETop = new HLineTo();
lineETop.xProperty().bind(
Bindings.subtract(centerXProperty, arrowSizeProperty));
lineFTop = new LineTo();
lineFTop.xProperty().bind(centerXProperty);
lineFTop.yProperty().bind(
Bindings.subtract(topEdgeProperty, arrowSizeProperty));
lineGTop = new LineTo();
lineGTop.xProperty().bind(
Bindings.add(centerXProperty, arrowSizeProperty));
lineGTop.yProperty().bind(topEdgeProperty);
lineHTop = new HLineTo();
lineHTop.xProperty().bind(
Bindings.subtract(Bindings.subtract(
rightEdgeMinusRadiusProperty, arrowIndentProperty),
Bindings.multiply(arrowSizeProperty, 2)));
lineITop = new LineTo();
lineITop.xProperty().bind(
Bindings.subtract(Bindings.subtract(
rightEdgeMinusRadiusProperty, arrowIndentProperty),
arrowSizeProperty));
lineITop.yProperty().bind(
Bindings.subtract(topEdgeProperty, arrowSizeProperty));
lineJTop = new LineTo();
lineJTop.xProperty().bind(
Bindings.subtract(rightEdgeMinusRadiusProperty,
arrowIndentProperty));
lineJTop.yProperty().bind(topEdgeProperty);
lineKTop = new HLineTo();
lineKTop.xProperty().bind(rightEdgeMinusRadiusProperty);
//
// RIGHT EDGE
//
rightCurveTo = new QuadCurveTo();
rightCurveTo.xProperty().bind(rightEdgeProperty);
rightCurveTo.yProperty().bind(
Bindings.add(topEdgeProperty, cornerProperty));
rightCurveTo.controlXProperty().bind(rightEdgeProperty);
rightCurveTo.controlYProperty().bind(topEdgeProperty);
lineBRight = new VLineTo();
lineBRight.yProperty().bind(
Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty));
lineCRight = new LineTo();
lineCRight.xProperty().bind(
Bindings.add(rightEdgeProperty, arrowSizeProperty));
lineCRight.yProperty().bind(
Bindings.add(lineBRight.yProperty(), arrowSizeProperty));
lineDRight = new LineTo();
lineDRight.xProperty().bind(rightEdgeProperty);
lineDRight.yProperty().bind(
Bindings.add(lineCRight.yProperty(), arrowSizeProperty));
lineERight = new VLineTo();
lineERight.yProperty().bind(
Bindings.subtract(centerYProperty, arrowSizeProperty));
lineFRight = new LineTo();
lineFRight.xProperty().bind(
Bindings.add(rightEdgeProperty, arrowSizeProperty));
lineFRight.yProperty().bind(centerYProperty);
lineGRight = new LineTo();
lineGRight.xProperty().bind(rightEdgeProperty);
lineGRight.yProperty().bind(
Bindings.add(centerYProperty, arrowSizeProperty));
lineHRight = new VLineTo();
lineHRight.yProperty().bind(
Bindings.subtract(Bindings.subtract(
bottomEdgeMinusRadiusProperty, arrowIndentProperty),
Bindings.multiply(arrowSizeProperty, 2)));
lineIRight = new LineTo();
lineIRight.xProperty().bind(
Bindings.add(rightEdgeProperty, arrowSizeProperty));
lineIRight.yProperty().bind(
Bindings.subtract(Bindings.subtract(
bottomEdgeMinusRadiusProperty, arrowIndentProperty),
arrowSizeProperty));
lineJRight = new LineTo();
lineJRight.xProperty().bind(rightEdgeProperty);
lineJRight.yProperty().bind(
Bindings.subtract(bottomEdgeMinusRadiusProperty,
arrowIndentProperty));
lineKRight = new VLineTo();
lineKRight.yProperty().bind(bottomEdgeMinusRadiusProperty);
//
// BOTTOM EDGE
//
bottomCurveTo = new QuadCurveTo();
bottomCurveTo.xProperty().bind(rightEdgeMinusRadiusProperty);
bottomCurveTo.yProperty().bind(bottomEdgeProperty);
bottomCurveTo.controlXProperty().bind(rightEdgeProperty);
bottomCurveTo.controlYProperty().bind(bottomEdgeProperty);
lineBBottom = new HLineTo();
lineBBottom.xProperty().bind(
Bindings.subtract(rightEdgeMinusRadiusProperty,
arrowIndentProperty));
lineCBottom = new LineTo();
lineCBottom.xProperty().bind(
Bindings.subtract(lineBBottom.xProperty(), arrowSizeProperty));
lineCBottom.yProperty().bind(
Bindings.add(bottomEdgeProperty, arrowSizeProperty));
lineDBottom = new LineTo();
lineDBottom.xProperty().bind(
Bindings.subtract(lineCBottom.xProperty(), arrowSizeProperty));
lineDBottom.yProperty().bind(bottomEdgeProperty);
lineEBottom = new HLineTo();
lineEBottom.xProperty().bind(
Bindings.add(centerXProperty, arrowSizeProperty));
lineFBottom = new LineTo();
lineFBottom.xProperty().bind(centerXProperty);
lineFBottom.yProperty().bind(
Bindings.add(bottomEdgeProperty, arrowSizeProperty));
lineGBottom = new LineTo();
lineGBottom.xProperty().bind(
Bindings.subtract(centerXProperty, arrowSizeProperty));
lineGBottom.yProperty().bind(bottomEdgeProperty);
lineHBottom = new HLineTo();
lineHBottom.xProperty().bind(
Bindings.add(Bindings.add(leftEdgePlusRadiusProperty,
arrowIndentProperty), Bindings.multiply(
arrowSizeProperty, 2)));
lineIBottom = new LineTo();
lineIBottom.xProperty().bind(
Bindings.add(Bindings.add(leftEdgePlusRadiusProperty,
arrowIndentProperty), arrowSizeProperty));
lineIBottom.yProperty().bind(
Bindings.add(bottomEdgeProperty, arrowSizeProperty));
lineJBottom = new LineTo();
lineJBottom.xProperty().bind(
Bindings.add(leftEdgePlusRadiusProperty, arrowIndentProperty));
lineJBottom.yProperty().bind(bottomEdgeProperty);
lineKBottom = new HLineTo();
lineKBottom.xProperty().bind(leftEdgePlusRadiusProperty);
//
// LEFT EDGE
//
leftCurveTo = new QuadCurveTo();
leftCurveTo.xProperty().bind(leftEdgeProperty);
leftCurveTo.yProperty().bind(
Bindings.subtract(bottomEdgeProperty, cornerProperty));
leftCurveTo.controlXProperty().bind(leftEdgeProperty);
leftCurveTo.controlYProperty().bind(bottomEdgeProperty);
lineBLeft = new VLineTo();
lineBLeft.yProperty().bind(
Bindings.subtract(bottomEdgeMinusRadiusProperty,
arrowIndentProperty));
lineCLeft = new LineTo();
lineCLeft.xProperty().bind(
Bindings.subtract(leftEdgeProperty, arrowSizeProperty));
lineCLeft.yProperty().bind(
Bindings.subtract(lineBLeft.yProperty(), arrowSizeProperty));
lineDLeft = new LineTo();
lineDLeft.xProperty().bind(leftEdgeProperty);
lineDLeft.yProperty().bind(
Bindings.subtract(lineCLeft.yProperty(), arrowSizeProperty));
lineELeft = new VLineTo();
lineELeft.yProperty().bind(
Bindings.add(centerYProperty, arrowSizeProperty));
lineFLeft = new LineTo();
lineFLeft.xProperty().bind(
Bindings.subtract(leftEdgeProperty, arrowSizeProperty));
lineFLeft.yProperty().bind(centerYProperty);
lineGLeft = new LineTo();
lineGLeft.xProperty().bind(leftEdgeProperty);
lineGLeft.yProperty().bind(
Bindings.subtract(centerYProperty, arrowSizeProperty));
lineHLeft = new VLineTo();
lineHLeft.yProperty().bind(
Bindings.add(Bindings.add(topEdgePlusRadiusProperty,
arrowIndentProperty), Bindings.multiply(
arrowSizeProperty, 2)));
lineILeft = new LineTo();
lineILeft.xProperty().bind(
Bindings.subtract(leftEdgeProperty, arrowSizeProperty));
lineILeft.yProperty().bind(
Bindings.add(Bindings.add(topEdgePlusRadiusProperty,
arrowIndentProperty), arrowSizeProperty));
lineJLeft = new LineTo();
lineJLeft.xProperty().bind(leftEdgeProperty);
lineJLeft.yProperty().bind(
Bindings.add(topEdgePlusRadiusProperty, arrowIndentProperty));
lineKLeft = new VLineTo();
lineKLeft.yProperty().bind(topEdgePlusRadiusProperty);
topCurveTo = new QuadCurveTo();
topCurveTo.xProperty().bind(leftEdgePlusRadiusProperty);
topCurveTo.yProperty().bind(topEdgeProperty);
topCurveTo.controlXProperty().bind(leftEdgeProperty);
topCurveTo.controlYProperty().bind(topEdgeProperty);
}
private Window getPopupWindow() {
return getSkinnable().getScene().getWindow();
}
private boolean showArrow(ArrowLocation location) {
ArrowLocation arrowLocation = getSkinnable().getArrowLocation();
return location.equals(arrowLocation) && !getSkinnable().isDetached()
&& !tornOff;
}
private void updatePath() {
List<PathElement> elements = new ArrayList<>();
elements.add(moveTo);
if (showArrow(TOP_LEFT)) {
elements.add(lineBTop);
elements.add(lineCTop);
elements.add(lineDTop);
}
if (showArrow(TOP_CENTER)) {
elements.add(lineETop);
elements.add(lineFTop);
elements.add(lineGTop);
}
if (showArrow(TOP_RIGHT)) {
elements.add(lineHTop);
elements.add(lineITop);
elements.add(lineJTop);
}
elements.add(lineKTop);
elements.add(rightCurveTo);
if (showArrow(RIGHT_TOP)) {
elements.add(lineBRight);
elements.add(lineCRight);
elements.add(lineDRight);
}
if (showArrow(RIGHT_CENTER)) {
elements.add(lineERight);
elements.add(lineFRight);
elements.add(lineGRight);
}
if (showArrow(RIGHT_BOTTOM)) {
elements.add(lineHRight);
elements.add(lineIRight);
elements.add(lineJRight);
}
elements.add(lineKRight);
elements.add(bottomCurveTo);
if (showArrow(BOTTOM_RIGHT)) {
elements.add(lineBBottom);
elements.add(lineCBottom);
elements.add(lineDBottom);
}
if (showArrow(BOTTOM_CENTER)) {
elements.add(lineEBottom);
elements.add(lineFBottom);
elements.add(lineGBottom);
}
if (showArrow(BOTTOM_LEFT)) {
elements.add(lineHBottom);
elements.add(lineIBottom);
elements.add(lineJBottom);
}
elements.add(lineKBottom);
elements.add(leftCurveTo);
if (showArrow(LEFT_BOTTOM)) {
elements.add(lineBLeft);
elements.add(lineCLeft);
elements.add(lineDLeft);
}
if (showArrow(LEFT_CENTER)) {
elements.add(lineELeft);
elements.add(lineFLeft);
elements.add(lineGLeft);
}
if (showArrow(LEFT_TOP)) {
elements.add(lineHLeft);
elements.add(lineILeft);
elements.add(lineJLeft);
}
elements.add(lineKLeft);
elements.add(topCurveTo);
path.getElements().setAll(elements);
clip.getElements().setAll(elements);
}
}

View file

@ -0,0 +1,273 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package haveno.desktop.components.indicator;
import javafx.scene.control.Control;
import javafx.scene.control.Skin;
import javafx.css.PseudoClass;
import javafx.css.StyleableProperty;
import haveno.desktop.components.indicator.skin.StaticProgressIndicatorSkin;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.DoublePropertyBase;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
// TODO Copied form OpenJFX, check license issues and way how we integrated it
// We changed behaviour which was not exposed via APIs
/**
* A circular control which is used for indicating progress, either
* infinite (aka indeterminate) or finite. Often used with the Task API for
* representing progress of background Tasks.
* <p>
* ProgressIndicator sets focusTraversable to false.
* </p>
* <p/>
* <p/>
* This first example creates a ProgressIndicator with an indeterminate value :
* <pre><code>
* import javafx.scene.control.ProgressIndicator;
* ProgressIndicator p1 = new ProgressIndicator();
* </code></pre>
* <p/>
* <p/>
* This next example creates a ProgressIndicator which is 25% complete :
* <pre><code>
* import javafx.scene.control.ProgressIndicator;
* ProgressIndicator p2 = new ProgressIndicator();
* p2.setProgress(0.25F);
* </code></pre>
* <p/>
* Implementation of ProgressIndicator According to JavaFX UI Control API Specification
*
* @since JavaFX 2.0
*/
@SuppressWarnings({"SameParameterValue", "WeakerAccess"})
public class TxConfidenceIndicator extends Control {
/**
* Value for progress indicating that the progress is indeterminate.
*
* @see #setProgress
*/
public static final double INDETERMINATE_PROGRESS = -1;
/***************************************************************************
* *
* Constructors *
* *
**************************************************************************/
/**
* Initialize the style class to 'progress-indicator'.
* <p/>
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "progress-indicator";
/**
* Pseudoclass indicating this is a determinate (i.e., progress can be
* determined) progress indicator.
*/
private static final PseudoClass PSEUDO_CLASS_DETERMINATE = PseudoClass.getPseudoClass("determinate");
/***************************************************************************
* *
* Properties *
* *
**************************************************************************/
/**
* Pseudoclass indicating this is an indeterminate (i.e., progress cannot
* be determined) progress indicator.
*/
private static final PseudoClass PSEUDO_CLASS_INDETERMINATE = PseudoClass.getPseudoClass("indeterminate");
/**
* A flag indicating whether it is possible to determine the progress
* of the ProgressIndicator. Typically indeterminate progress bars are
* rendered with some form of animation indicating potentially "infinite"
* progress.
*/
private ReadOnlyBooleanWrapper indeterminate;
/**
* The actual progress of the ProgressIndicator. A negative value for
* progress indicates that the progress is indeterminate. A positive value
* between 0 and 1 indicates the percentage of progress where 0 is 0% and 1
* is 100%. Any value greater than 1 is interpreted as 100%.
*/
private DoubleProperty progress;
/**
* Creates a new indeterminate ProgressIndicator.
*/
public TxConfidenceIndicator() {
this(INDETERMINATE_PROGRESS);
}
/**
* Creates a new ProgressIndicator with the given progress value.
*/
@SuppressWarnings("unchecked")
public TxConfidenceIndicator(double progress) {
// focusTraversable is styleable through css. Calling setFocusTraversable
// makes it look to css like the user set the value and css will not
// override. Initializing focusTraversable by calling applyStyle with null
// StyleOrigin ensures that css will be able to override the value.
((StyleableProperty) focusTraversableProperty()).applyStyle(null, Boolean.FALSE);
setProgress(progress);
getStyleClass().setAll(DEFAULT_STYLE_CLASS);
// need to initialize pseudo-class state
final int c = Double.compare(INDETERMINATE_PROGRESS, progress);
pseudoClassStateChanged(PSEUDO_CLASS_INDETERMINATE, c == 0);
pseudoClassStateChanged(PSEUDO_CLASS_DETERMINATE, c != 0);
}
public final boolean isIndeterminate() {
return indeterminate == null || indeterminate.get();
}
private void setIndeterminate(boolean value) {
indeterminatePropertyImpl().set(value);
}
public final ReadOnlyBooleanProperty indeterminateProperty() {
return indeterminatePropertyImpl().getReadOnlyProperty();
}
private ReadOnlyBooleanWrapper indeterminatePropertyImpl() {
if (indeterminate == null) {
indeterminate = new ReadOnlyBooleanWrapper(true) {
@Override
protected void invalidated() {
final boolean active = get();
pseudoClassStateChanged(PSEUDO_CLASS_INDETERMINATE, active);
pseudoClassStateChanged(PSEUDO_CLASS_DETERMINATE, !active);
}
@Override
public Object getBean() {
return TxConfidenceIndicator.this;
}
@Override
public String getName() {
return "indeterminate";
}
};
}
return indeterminate;
}
/**
* ************************************************************************
* *
* Methods *
* *
* ************************************************************************
*/
public final double getProgress() {
return progress == null ? INDETERMINATE_PROGRESS : progress.get();
}
/**
* ************************************************************************
* *
* Stylesheet Handling *
* *
* ************************************************************************
*/
public final void setProgress(double value) {
progressProperty().set(value);
}
public final DoubleProperty progressProperty() {
if (progress == null) {
progress = new DoublePropertyBase(-1.0) {
@Override
protected void invalidated() {
setIndeterminate(getProgress() < 0.0);
}
@Override
public Object getBean() {
return TxConfidenceIndicator.this;
}
@Override
public String getName() {
return "progress";
}
};
}
return progress;
}
/**
* {@inheritDoc}
*/
@Override
protected Skin<?> createDefaultSkin() {
return new StaticProgressIndicatorSkin(this);
}
/**
* Most Controls return true for focusTraversable, so Control overrides
* this method to return true, but ProgressIndicator returns false for
* focusTraversable's initial value; hence the override of the override.
* This method is called from CSS code to get the correct initial value.
*/
@Deprecated
@SuppressWarnings("deprecation")
protected /*do not make final*/ Boolean impl_cssGetFocusTraversableInitialValue() {
return Boolean.FALSE;
}
}

View file

@ -0,0 +1,765 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code 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 General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package haveno.desktop.components.indicator.skin;
import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Circle;
import javafx.scene.transform.Scale;
import javafx.css.CssMetaData;
import javafx.css.StyleOrigin;
import javafx.css.Styleable;
import javafx.css.StyleableBooleanProperty;
import javafx.css.StyleableIntegerProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.css.converter.SizeConverter;
import javafx.geometry.Insets;
import javafx.geometry.NodeOrientation;
import javafx.beans.InvalidationListener;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.collections.ObservableList;
import javafx.collections.FXCollections;
import haveno.desktop.components.indicator.TxConfidenceIndicator;
import java.util.ArrayList;
import java.util.List;
// TODO Copied form OpenJFX, check license issues and way how we integrated it
// We changed behaviour which was not exposed via APIs
public class StaticProgressIndicatorSkin extends SkinBase<TxConfidenceIndicator> {
/**
* ************************************************************************
* *
* UI SubComponents *
* *
* ************************************************************************
*/
private IndeterminateSpinner spinner;
/**
* The number of segments in the spinner.
*/
private final IntegerProperty indeterminateSegmentCount = new StyleableIntegerProperty(8) {
@Override
protected void invalidated() {
if (spinner != null) {
spinner.rebuild();
}
}
@Override
public Object getBean() {
return StaticProgressIndicatorSkin.this;
}
@Override
public String getName() {
return "indeterminateSegmentCount";
}
@Override
public CssMetaData<TxConfidenceIndicator, Number> getCssMetaData() {
return StyleableProperties.INDETERMINATE_SEGMENT_COUNT;
}
};
/**
* True if the progress indicator should rotate as well as animate opacity.
*/
private final BooleanProperty spinEnabled = new StyleableBooleanProperty(false) {
@Override
protected void invalidated() {
if (spinner != null) {
spinner.setSpinEnabled(get());
}
}
@Override
public CssMetaData<TxConfidenceIndicator, Boolean> getCssMetaData() {
return StyleableProperties.SPIN_ENABLED;
}
@Override
public Object getBean() {
return StaticProgressIndicatorSkin.this;
}
@Override
public String getName() {
return "spinEnabled";
}
};
private DeterminateIndicator determinateIndicator;
/**
* The colour of the progress segment.
*/
private final ObjectProperty<Paint> progressColor = new StyleableObjectProperty<>(null) {
@Override
public void set(Paint newProgressColor) {
final Paint color = (newProgressColor instanceof Color) ? newProgressColor : null;
super.set(color);
}
@Override
protected void invalidated() {
if (spinner != null) {
spinner.setFillOverride(get());
}
if (determinateIndicator != null) {
determinateIndicator.setFillOverride(get());
}
}
@Override
public Object getBean() {
return StaticProgressIndicatorSkin.this;
}
@Override
public String getName() {
return "progressColorProperty";
}
@Override
public CssMetaData<TxConfidenceIndicator, Paint> getCssMetaData() {
return StyleableProperties.PROGRESS_COLOR;
}
};
private boolean timeLineNulled = false;
/**
* ************************************************************************
* *
* Constructors *
* *
* ************************************************************************
*/
@SuppressWarnings("deprecation")
public StaticProgressIndicatorSkin(TxConfidenceIndicator control) {
super(control);
InvalidationListener indeterminateListener = valueModel -> initialize();
control.indeterminateProperty().addListener(indeterminateListener);
InvalidationListener visibilityListener = valueModel -> {
if (getSkinnable().isIndeterminate() && timeLineNulled && spinner == null) {
timeLineNulled = false;
spinner = new IndeterminateSpinner(
getSkinnable(), StaticProgressIndicatorSkin.this,
spinEnabled.get(), progressColor.get());
getChildren().add(spinner);
}
if (spinner != null) {
if (getSkinnable().getScene() != null) {
getChildren().remove(spinner);
spinner = null;
timeLineNulled = true;
}
}
};
control.visibleProperty().addListener(visibilityListener);
control.parentProperty().addListener(visibilityListener);
InvalidationListener sceneListener = valueModel -> {
if (spinner != null) {
if (getSkinnable().getScene() == null) {
getChildren().remove(spinner);
spinner = null;
timeLineNulled = true;
}
} else {
if (getSkinnable().getScene() != null && getSkinnable().isIndeterminate()) {
timeLineNulled = false;
spinner = new IndeterminateSpinner(
getSkinnable(), StaticProgressIndicatorSkin.this,
spinEnabled.get(), progressColor.get());
getChildren().add(spinner);
getSkinnable().requestLayout();
}
}
};
control.sceneProperty().addListener(sceneListener);
initialize();
getSkinnable().requestLayout();
}
/**
* @return The CssMetaData associated with this class, which may include the
* CssMetaData of its super classes.
*/
@SuppressWarnings("SameReturnValue")
public static ObservableList<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
@SuppressWarnings("deprecation")
private void initialize() {
TxConfidenceIndicator control = getSkinnable();
boolean isIndeterminate = control.isIndeterminate();
if (isIndeterminate) {
// clean up determinateIndicator
determinateIndicator = null;
// create spinner
spinner = new IndeterminateSpinner(control, this, spinEnabled.get(), progressColor.get());
getChildren().clear();
getChildren().add(spinner);
} else {
// clean up after spinner
if (spinner != null) {
spinner = null;
}
// create determinateIndicator
determinateIndicator =
new StaticProgressIndicatorSkin.DeterminateIndicator(control, this, progressColor.get());
getChildren().clear();
getChildren().add(determinateIndicator);
}
}
@Override
public void dispose() {
super.dispose();
if (spinner != null) {
spinner = null;
}
}
@Override
protected void layoutChildren(final double x, final double y, final double w, final double h) {
if (spinner != null && getSkinnable().isIndeterminate()) {
spinner.layoutChildren();
spinner.resizeRelocate(0, 0, w, h);
} else if (determinateIndicator != null) {
determinateIndicator.layoutChildren();
determinateIndicator.resizeRelocate(0, 0, w, h);
}
}
public Paint getProgressColor() {
return progressColor.get();
}
/**
* {@inheritDoc}
*/
@Override
public ObservableList<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
// *********** Stylesheet Handling *****************************************
/**
* ************************************************************************
* *
* DeterminateIndicator *
* *
* ************************************************************************
*/
@SuppressWarnings({"SameReturnValue", "UnusedParameters"})
static class DeterminateIndicator extends Region {
//private double textGap = 2.0F;
private final TxConfidenceIndicator control;
//private Text text;
private final StackPane indicator;
private final StackPane progress;
private final StackPane tick;
private final Arc arcShape;
private final Circle indicatorCircle;
// only update progress text on whole percentages
private int intProgress;
// only update pie arc to nearest degree
private int degProgress;
DeterminateIndicator(TxConfidenceIndicator control, StaticProgressIndicatorSkin s,
Paint fillOverride) {
this.control = control;
getStyleClass().add("determinate-indicator");
intProgress = (int) Math.round(control.getProgress() * 100.0);
degProgress = (int) (360 * control.getProgress());
InvalidationListener progressListener = valueModel -> updateProgress();
control.progressProperty().addListener(progressListener);
getChildren().clear();
// The circular background for the progress pie piece
indicator = new StackPane();
indicator.setScaleShape(false);
indicator.setCenterShape(false);
indicator.getStyleClass().setAll("indicator");
indicatorCircle = new Circle();
indicator.setShape(indicatorCircle);
// The shape for our progress pie piece
arcShape = new Arc();
arcShape.setType(ArcType.ROUND);
arcShape.setStartAngle(90.0F);
// Our progress pie piece
progress = new StackPane();
progress.getStyleClass().setAll("progress");
progress.setScaleShape(false);
progress.setCenterShape(false);
progress.setShape(arcShape);
progress.getChildren().clear();
setFillOverride(fillOverride);
// The check mark that's drawn at 100%
tick = new StackPane();
tick.getStyleClass().setAll("tick");
getChildren().setAll(indicator, progress, /*text,*/ tick);
updateProgress();
}
private void setFillOverride(Paint fillOverride) {
if (fillOverride instanceof Color) {
Color c = (Color) fillOverride;
progress.setStyle("-fx-background-color: rgba(" + ((int) (255 * c.getRed())) + "," +
"" + ((int) (255 * c.getGreen())) + "," + ((int) (255 * c.getBlue())) + "," +
"" + c.getOpacity() + ");");
} else {
progress.setStyle(null);
}
}
//@Override
public boolean isAutomaticallyMirrored() {
// This is used instead of setting NodeOrientation,
// allowing the Text node to inherit the current
// orientation.
return false;
}
private void updateProgress() {
intProgress = (int) Math.round(control.getProgress() * 100.0);
// text.setText((control.getProgress() >= 1) ? (DONE) : ("" + intProgress + "%"));
degProgress = (int) (360 * control.getProgress());
arcShape.setLength(-degProgress);
indicator.setOpacity(control.getProgress() == 0 ? 0 : 1);
requestLayout();
}
@Override
protected void layoutChildren() {
// Position and size the circular background
//double doneTextHeight = doneText.getLayoutBounds().getHeight();
final Insets controlInsets = control.getInsets();
final double left = snapSizeX(controlInsets.getLeft());
final double right = snapSizeX(controlInsets.getRight());
final double top = snapSizeY(controlInsets.getTop());
final double bottom = snapSizeY(controlInsets.getBottom());
/*
** use the min of width, or height, keep it a circle
*/
final double areaW = control.getWidth() - left - right;
final double areaH = control.getHeight() - top - bottom /*- textGap - doneTextHeight*/;
final double radiusW = areaW / 2;
final double radiusH = areaH / 2;
final double radius = Math.round(Math.min(radiusW, radiusH)); // use round instead of floor
final double centerX = snapPositionX(left + radiusW);
final double centerY = snapPositionY(top + radius);
// find radius that fits inside radius - insetsPadding
final Insets indicatorInsets = indicator.getInsets();
final double iLeft = snapSizeX(indicatorInsets.getLeft());
final double iRight = snapSizeX(indicatorInsets.getRight());
final double iTop = snapSizeY(indicatorInsets.getTop());
final double iBottom = snapSizeY(indicatorInsets.getBottom());
final double progressRadius = snapSizeX(Math.min(Math.min(radius - iLeft, radius - iRight),
Math.min(radius - iTop, radius - iBottom)));
indicatorCircle.setRadius(radius);
indicator.setLayoutX(centerX);
indicator.setLayoutY(centerY);
arcShape.setRadiusX(progressRadius);
arcShape.setRadiusY(progressRadius);
progress.setLayoutX(centerX);
progress.setLayoutY(centerY);
// find radius that fits inside progressRadius - progressInsets
final Insets progressInsets = progress.getInsets();
final double pLeft = snapSizeX(progressInsets.getLeft());
final double pRight = snapSizeX(progressInsets.getRight());
final double pTop = snapSizeY(progressInsets.getTop());
final double pBottom = snapSizeY(progressInsets.getBottom());
final double indicatorRadius = snapSizeX(Math.min(Math.min(progressRadius - pLeft,
progressRadius - pRight), Math.min(progressRadius - pTop, progressRadius - pBottom)));
// find size of spare box that fits inside indicator radius
double squareBoxHalfWidth = Math.ceil(Math.sqrt((indicatorRadius * indicatorRadius) / 2));
// double squareBoxHalfWidth2 = indicatorRadius * (Math.sqrt(2) / 2);
tick.setLayoutX(centerX - squareBoxHalfWidth);
tick.setLayoutY(centerY - squareBoxHalfWidth);
tick.resize(squareBoxHalfWidth + squareBoxHalfWidth, squareBoxHalfWidth + squareBoxHalfWidth);
tick.setVisible(control.getProgress() >= 1);
}
@Override
protected double computePrefWidth(double height) {
final Insets controlInsets = control.getInsets();
final double left = snapSizeX(controlInsets.getLeft());
final double right = snapSizeX(controlInsets.getRight());
final Insets indicatorInsets = indicator.getInsets();
final double iLeft = snapSizeX(indicatorInsets.getLeft());
final double iRight = snapSizeX(indicatorInsets.getRight());
final double iTop = snapSizeY(indicatorInsets.getTop());
final double iBottom = snapSizeY(indicatorInsets.getBottom());
final double indicatorMax = snapSizeX(Math.max(Math.max(iLeft, iRight), Math.max(iTop, iBottom)));
final Insets progressInsets = progress.getInsets();
final double pLeft = snapSizeX(progressInsets.getLeft());
final double pRight = snapSizeX(progressInsets.getRight());
final double pTop = snapSizeY(progressInsets.getTop());
final double pBottom = snapSizeY(progressInsets.getBottom());
final double progressMax = snapSizeX(Math.max(Math.max(pLeft, pRight), Math.max(pTop, pBottom)));
final Insets tickInsets = tick.getInsets();
final double tLeft = snapSizeX(tickInsets.getLeft());
final double tRight = snapSizeX(tickInsets.getRight());
final double indicatorWidth = indicatorMax + progressMax + tLeft + tRight + progressMax + indicatorMax;
return left + indicatorWidth + /*Math.max(indicatorWidth, doneText.getLayoutBounds().getWidth()) + */right;
}
@Override
protected double computePrefHeight(double width) {
final Insets controlInsets = control.getInsets();
final double top = snapSizeY(controlInsets.getTop());
final double bottom = snapSizeY(controlInsets.getBottom());
final Insets indicatorInsets = indicator.getInsets();
final double iLeft = snapSizeX(indicatorInsets.getLeft());
final double iRight = snapSizeX(indicatorInsets.getRight());
final double iTop = snapSizeY(indicatorInsets.getTop());
final double iBottom = snapSizeY(indicatorInsets.getBottom());
final double indicatorMax = snapSizeX(Math.max(Math.max(iLeft, iRight), Math.max(iTop, iBottom)));
final Insets progressInsets = progress.getInsets();
final double pLeft = snapSizeX(progressInsets.getLeft());
final double pRight = snapSizeX(progressInsets.getRight());
final double pTop = snapSizeY(progressInsets.getTop());
final double pBottom = snapSizeY(progressInsets.getBottom());
final double progressMax = snapSizeX(Math.max(Math.max(pLeft, pRight), Math.max(pTop, pBottom)));
final Insets tickInsets = tick.getInsets();
final double tTop = snapSizeY(tickInsets.getTop());
final double tBottom = snapSizeY(tickInsets.getBottom());
final double indicatorHeight = indicatorMax + progressMax + tTop + tBottom + progressMax + indicatorMax;
return top + indicatorHeight /*+ textGap + doneText.getLayoutBounds().getHeight()*/ + bottom;
}
@Override
protected double computeMaxWidth(double height) {
return computePrefWidth(height);
}
@Override
protected double computeMaxHeight(double width) {
return computePrefHeight(width);
}
}
/**
* ************************************************************************
* *
* IndeterminateSpinner *
* *
* ************************************************************************
*/
@SuppressWarnings({"ConstantConditions", "MismatchedQueryAndUpdateOfCollection"})
static class IndeterminateSpinner extends Region {
private final TxConfidenceIndicator control;
private final StaticProgressIndicatorSkin skin;
private final IndicatorPaths pathsG;
private final List<Double> opacities = new ArrayList<>();
private Paint fillOverride = null;
IndeterminateSpinner(TxConfidenceIndicator control, StaticProgressIndicatorSkin s,
boolean spinEnabled, Paint fillOverride) {
this.control = control;
this.skin = s;
this.fillOverride = fillOverride;
setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
getStyleClass().setAll("spinner");
pathsG = new IndicatorPaths(this);
getChildren().add(pathsG);
rebuild();
}
void setFillOverride(Paint fillOverride) {
this.fillOverride = fillOverride;
rebuild();
}
void setSpinEnabled(boolean spinEnabled) {
}
@Override
protected void layoutChildren() {
Insets controlInsets = control.getInsets();
final double w = control.getWidth() - controlInsets.getLeft() - controlInsets.getRight();
final double h = control.getHeight() - controlInsets.getTop() - controlInsets.getBottom();
final double prefW = pathsG.prefWidth(-1);
final double prefH = pathsG.prefHeight(-1);
double scaleX = w / prefW;
double scale = scaleX;
if ((scaleX * prefH) > h) {
scale = h / prefH;
}
double indicatorW = prefW * scale - 3;
double indicatorH = prefH * scale - 3;
pathsG.resizeRelocate((w - indicatorW) / 2, (h - indicatorH) / 2, indicatorW, indicatorH);
}
private void rebuild() {
// update indeterminate indicator
final int segments = skin.indeterminateSegmentCount.get();
opacities.clear();
pathsG.getChildren().clear();
final double step = 0.8 / (segments - 1);
for (int i = 0; i < segments; i++) {
Region region = new Region();
region.setScaleShape(false);
region.setCenterShape(false);
region.getStyleClass().addAll("segment", "segment" + i);
if (fillOverride instanceof Color) {
Color c = (Color) fillOverride;
region.setStyle("-fx-background-color: rgba(" + ((int) (255 * c.getRed())) + "," +
"" + ((int) (255 * c.getGreen())) + "," + ((int) (255 * c.getBlue())) + "," +
"" + c.getOpacity() + ");");
} else {
region.setStyle(null);
}
double opacity = Math.min(1, i * step);
opacities.add(opacity);
region.setOpacity(opacity);
pathsG.getChildren().add(region);
}
}
@SuppressWarnings("deprecation")
private class IndicatorPaths extends Pane {
final IndeterminateSpinner piSkin;
IndicatorPaths(IndeterminateSpinner pi) {
super();
piSkin = pi;
}
@Override
protected double computePrefWidth(double height) {
double w = 0;
for (Node child : getChildren()) {
if (child instanceof Region) {
Region region = (Region) child;
if (region.getShape() != null) {
w = Math.max(w, region.getShape().getLayoutBounds().getMaxX());
} else {
w = Math.max(w, region.prefWidth(height));
}
}
}
return w;
}
@Override
protected double computePrefHeight(double width) {
double h = 0;
for (Node child : getChildren()) {
if (child instanceof Region) {
Region region = (Region) child;
if (region.getShape() != null) {
h = Math.max(h, region.getShape().getLayoutBounds().getMaxY());
} else {
h = Math.max(h, region.prefHeight(width));
}
}
}
return h;
}
@Override
protected void layoutChildren() {
// calculate scale
double scale = getWidth() / computePrefWidth(-1);
getChildren().stream().filter(child -> child instanceof Region).forEach(child -> {
Region region = (Region) child;
if (region.getShape() != null) {
region.resize(region.getShape().getLayoutBounds().getMaxX(),
region.getShape().getLayoutBounds().getMaxY());
region.getTransforms().setAll(new Scale(scale, scale, 0, 0));
} else {
region.autosize();
}
});
}
}
}
/**
* Super-lazy instantiation pattern from Bill Pugh.
*/
@SuppressWarnings({"deprecation", "unchecked", "ConstantConditions"})
private static class StyleableProperties {
static final ObservableList<CssMetaData<? extends Styleable, ?>> STYLEABLES;
private static final CssMetaData<TxConfidenceIndicator, Paint> PROGRESS_COLOR =
new CssMetaData<>(
"-fx-progress-color", PaintConverter.getInstance(), null) {
@Override
public boolean isSettable(TxConfidenceIndicator n) {
final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin();
return skin.progressColor == null || !skin.progressColor.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(TxConfidenceIndicator n) {
final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin();
return (StyleableProperty<Paint>) skin.progressColor;
}
};
private static final CssMetaData<TxConfidenceIndicator, Number> INDETERMINATE_SEGMENT_COUNT =
new CssMetaData<>(
"-fx-indeterminate-segment-count", SizeConverter.getInstance(), 8) {
@Override
public void set(TxConfidenceIndicator node, Number value, StyleOrigin origin) {
super.set(node, value.intValue(), origin);
}
@Override
public boolean isSettable(TxConfidenceIndicator n) {
final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin();
return skin.indeterminateSegmentCount == null || !skin.indeterminateSegmentCount.isBound();
}
@Override
public StyleableProperty<Number> getStyleableProperty(TxConfidenceIndicator n) {
final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) n.getSkin();
return (StyleableProperty<Number>) skin.indeterminateSegmentCount;
}
};
private static final CssMetaData<TxConfidenceIndicator, Boolean> SPIN_ENABLED = new
CssMetaData<>("-fx-spin-enabled",
BooleanConverter.getInstance(),
Boolean.FALSE) {
@Override
public boolean isSettable(TxConfidenceIndicator node) {
final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) node.getSkin();
return skin.spinEnabled == null || !skin.spinEnabled.isBound();
}
@Override
public StyleableProperty<Boolean> getStyleableProperty(TxConfidenceIndicator node) {
final StaticProgressIndicatorSkin skin = (StaticProgressIndicatorSkin) node.getSkin();
return (StyleableProperty<Boolean>) skin.spinEnabled;
}
};
static {
final ObservableList<CssMetaData<? extends Styleable, ?>> styleables =
FXCollections.observableArrayList(SkinBase.getClassCssMetaData());
styleables.add(PROGRESS_COLOR);
styleables.add(INDETERMINATE_SEGMENT_COUNT);
styleables.add(SPIN_ENABLED);
STYLEABLES = FXCollections.unmodifiableObservableList(styleables);
}
}
}

View file

@ -0,0 +1,71 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.list;
import haveno.core.locale.Res;
import haveno.desktop.components.AutoTooltipLabel;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.filtering.FilterableListItem;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.geometry.Insets;
import javafx.beans.value.ChangeListener;
import javafx.collections.transformation.FilteredList;
public class FilterBox extends HBox {
private final InputTextField textField;
private FilteredList<? extends FilterableListItem> filteredList;
private ChangeListener<String> listener;
public FilterBox() {
super();
setSpacing(5.0);
AutoTooltipLabel label = new AutoTooltipLabel(Res.get("shared.filter"));
HBox.setMargin(label, new Insets(5.0, 0, 0, 10.0));
textField = new InputTextField();
textField.setMinWidth(500);
getChildren().addAll(label, textField);
}
public void initialize(FilteredList<? extends FilterableListItem> filteredList,
TableView<? extends FilterableListItem> tableView) {
this.filteredList = filteredList;
listener = (observable, oldValue, newValue) -> {
tableView.getSelectionModel().clearSelection();
applyFilteredListPredicate(textField.getText());
};
}
public void activate() {
textField.textProperty().addListener(listener);
applyFilteredListPredicate(textField.getText());
}
public void deactivate() {
textField.textProperty().removeListener(listener);
}
private void applyFilteredListPredicate(String filterString) {
filteredList.setPredicate(item -> item.match(filterString));
}
}

View file

@ -0,0 +1,102 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.GridPane;
import javafx.collections.FXCollections;
import static haveno.desktop.util.FormBuilder.*;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.BankUtil;
import haveno.core.locale.Country;
import haveno.core.locale.Res;
import haveno.core.payment.AchTransferAccount;
import haveno.core.payment.CountryBasedPaymentAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.AchTransferAccountPayload;
import haveno.core.payment.payload.BankAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
public class AchTransferForm extends GeneralUsBankForm {
public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) {
AchTransferAccountPayload achTransferAccountPayload = (AchTransferAccountPayload) paymentAccountPayload;
return addFormForBuyer(gridPane, gridRow, paymentAccountPayload, achTransferAccountPayload.getAccountType(), achTransferAccountPayload.getHolderAddress());
}
private final AchTransferAccount achTransferAccount;
public AchTransferForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator,
GridPane gridPane, int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.achTransferAccount = (AchTransferAccount) paymentAccount;
}
@Override
public void addFormForEditAccount() {
addFormForEditAccount(achTransferAccount.getPayload(), achTransferAccount.getPayload().getHolderAddress());
}
@Override
public void addFormForAddAccount() {
addFormForAddAccountInternal(achTransferAccount.getPayload(), achTransferAccount.getPayload().getHolderAddress());
}
@Override
protected void setHolderAddress(String holderAddress) {
achTransferAccount.getPayload().setHolderAddress(holderAddress);
}
@Override
protected void maybeAddAccountTypeCombo(BankAccountPayload bankAccountPayload, Country country) {
ComboBox<String> accountTypeComboBox = addComboBox(gridPane, ++gridRow, Res.get("payment.select.account"));
accountTypeComboBox.setItems(FXCollections.observableArrayList(BankUtil.getAccountTypeValues(country.code)));
accountTypeComboBox.setOnAction(e -> {
if (BankUtil.isAccountTypeRequired(country.code)) {
bankAccountPayload.setAccountType(accountTypeComboBox.getSelectionModel().getSelectedItem());
updateFromInputs();
}
});
}
@Override
public void updateAllInputsValid() {
AchTransferAccountPayload achTransferAccountPayload = achTransferAccount.getPayload();
boolean result = isAccountNameValid()
&& paymentAccount.getSingleTradeCurrency() != null
&& ((CountryBasedPaymentAccount) this.paymentAccount).getCountry() != null
&& inputValidator.validate(achTransferAccountPayload.getHolderName()).isValid
&& inputValidator.validate(achTransferAccountPayload.getHolderAddress()).isValid;
result = getValidationResult(result,
achTransferAccountPayload.getCountryCode(),
achTransferAccountPayload.getBankName(),
achTransferAccountPayload.getBankId(),
achTransferAccountPayload.getBranchId(),
achTransferAccountPayload.getAccountNr(),
achTransferAccountPayload.getAccountType(),
achTransferAccountPayload.getHolderTaxId(),
achTransferAccountPayload.getNationalAccountId());
allInputsValid.set(result);
}
}

View file

@ -0,0 +1,118 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane;
import haveno.common.util.Tuple2;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Res;
import haveno.core.payment.AdvancedCashAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.AdvancedCashAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.validation.AdvancedCashValidator;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.FormBuilder;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
@Deprecated
public class AdvancedCashForm extends PaymentMethodForm {
private final AdvancedCashAccount advancedCashAccount;
private final AdvancedCashValidator advancedCashValidator;
public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.wallet"),
((AdvancedCashAccountPayload) paymentAccountPayload).getAccountNr());
return gridRow;
}
public AdvancedCashForm(PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService,
AdvancedCashValidator advancedCashValidator,
InputValidator inputValidator,
GridPane gridPane,
int gridRow,
CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.advancedCashAccount = (AdvancedCashAccount) paymentAccount;
this.advancedCashValidator = advancedCashValidator;
}
@Override
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
InputTextField accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.wallet"));
accountNrInputTextField.setValidator(advancedCashValidator);
accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
advancedCashAccount.setAccountNr(newValue);
updateFromInputs();
});
addCurrenciesGrid(true);
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
private void addCurrenciesGrid(boolean isEditable) {
final Tuple2<Label, FlowPane> labelFlowPaneTuple2 = addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), 0);
FlowPane flowPane = labelFlowPaneTuple2.second;
if (isEditable)
flowPane.setId("flow-pane-checkboxes-bg");
else
flowPane.setId("flow-pane-checkboxes-non-editable-bg");
paymentAccount.getSupportedCurrencies().forEach(e ->
fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, advancedCashAccount));
}
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(advancedCashAccount.getAccountNr());
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(advancedCashAccount.getPaymentMethod().getId()));
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.wallet"),
advancedCashAccount.getAccountNr());
addLimitations(true);
addCurrenciesGrid(false);
}
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& advancedCashValidator.validate(advancedCashAccount.getAccountNr()).isValid
&& advancedCashAccount.getTradeCurrencies().size() > 0);
}
}

View file

@ -0,0 +1,57 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import javafx.scene.layout.GridPane;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Res;
import haveno.core.payment.AliPayAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.AliPayAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.validation.AliPayValidator;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
public class AliPayForm extends GeneralAccountNumberForm {
private final AliPayAccount aliPayAccount;
public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.no"), ((AliPayAccountPayload) paymentAccountPayload).getAccountNr());
return gridRow;
}
public AliPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, AliPayValidator aliPayValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.aliPayAccount = (AliPayAccount) paymentAccount;
}
@Override
void setAccountNumber(String newValue) {
aliPayAccount.setAccountNr(newValue);
}
@Override
String getAccountNr() {
return aliPayAccount.getAccountNr();
}
}

View file

@ -0,0 +1,171 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.*;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Country;
import haveno.core.locale.CountryUtil;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.AmazonGiftCardAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.AmazonGiftCardAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.Layout;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.collections.FXCollections;
import javafx.util.StringConverter;
import java.util.HashMap;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AmazonGiftCardForm extends PaymentMethodForm {
ComboBox<Country> countryCombo;
private final AmazonGiftCardAccount amazonGiftCardAccount;
public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) {
AmazonGiftCardAccountPayload amazonGiftCardAccountPayload = (AmazonGiftCardAccountPayload) paymentAccountPayload;
addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.amazon.site"),
countryToAmazonSite(amazonGiftCardAccountPayload.getCountryCode()),
Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE);
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email.mobile"),
amazonGiftCardAccountPayload.getEmailOrMobileNr());
String countryText = CountryUtil.getNameAndCode(amazonGiftCardAccountPayload.getCountryCode());
if (countryText.isEmpty()) {
countryText = Res.get("payment.ask");
}
addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1,
Res.get("shared.country"),
countryText);
return gridRow;
}
public AmazonGiftCardForm(PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService,
InputValidator inputValidator,
GridPane gridPane,
int gridRow,
CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.amazonGiftCardAccount = (AmazonGiftCardAccount) paymentAccount;
}
@Override
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
InputTextField accountNrInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email.mobile"));
accountNrInputTextField.setValidator(inputValidator);
accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
amazonGiftCardAccount.setEmailOrMobileNr(newValue);
updateFromInputs();
});
countryCombo = addComboBox(gridPane, ++gridRow, Res.get("shared.country"));
countryCombo.setPromptText(Res.get("payment.select.country"));
countryCombo.setItems(FXCollections.observableArrayList(CountryUtil.getAllAmazonGiftCardCountries()));
TextField ccyField = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), "").second;
countryCombo.setConverter(new StringConverter<>() {
@Override
public String toString(Country country) {
return country.name + " (" + country.code + ")";
}
@Override
public Country fromString(String s) {
return null;
}
});
countryCombo.setOnAction(e -> {
Country countryCode = countryCombo.getValue();
amazonGiftCardAccount.setCountry(countryCode);
TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode.code);
paymentAccount.setSingleTradeCurrency(currency);
ccyField.setText(currency.getNameAndCode());
updateFromInputs();
});
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(amazonGiftCardAccount.getEmailOrMobileNr());
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(paymentAccount.getPaymentMethod().getId()));
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow,
Res.get("payment.email.mobile"), amazonGiftCardAccount.getEmailOrMobileNr()).second;
field.setMouseTransparent(false);
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"),
amazonGiftCardAccount.getCountry() != null ? amazonGiftCardAccount.getCountry().name : "");
String nameAndCode = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getNameAndCode() : "";
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode);
addLimitations(true);
}
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& inputValidator.validate(amazonGiftCardAccount.getEmailOrMobileNr()).isValid
&& paymentAccount.getTradeCurrencies().size() > 0);
}
private static String countryToAmazonSite(String countryCode) {
HashMap<String, String> mapCountryToSite = new HashMap<>() {{
put("AU", "https://www.amazon.au");
put("CA", "https://www.amazon.ca");
put("FR", "https://www.amazon.fr");
put("DE", "https://www.amazon.de");
put("IT", "https://www.amazon.it");
put("NL", "https://www.amazon.nl");
put("ES", "https://www.amazon.es");
put("UK", "https://www.amazon.co.uk");
put("IN", "https://www.amazon.in");
put("JP", "https://www.amazon.co.jp");
put("SA", "https://www.amazon.sa");
put("SE", "https://www.amazon.se");
put("SG", "https://www.amazon.sg");
put("TR", "https://www.amazon.tr");
put("US", "https://www.amazon.com");
put("", Res.get("payment.ask"));
}};
return mapCountryToSite.get(countryCode);
}
}

View file

@ -0,0 +1,233 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.DisplayUtils.createAssetsAccountName;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import static haveno.desktop.util.FormBuilder.addLabelCheckBox;
import static haveno.desktop.util.GUIUtil.getComboBoxButtonCell;
import haveno.common.UserThread;
import haveno.common.util.Tuple3;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.filter.FilterManager;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.AssetAccount;
import haveno.core.payment.InstantCryptoCurrencyAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.AssetAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.validation.AltCoinAddressValidator;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.AutocompleteComboBox;
import haveno.desktop.components.InputTextField;
import haveno.desktop.main.overlays.popups.Popup;
import haveno.desktop.util.FormBuilder;
import haveno.desktop.util.Layout;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.geometry.Insets;
import javafx.util.StringConverter;
public class AssetsForm extends PaymentMethodForm {
public static final String INSTANT_TRADE_NEWS = "instantTradeNews0.9.5";
private final AssetAccount assetAccount;
private final AltCoinAddressValidator altCoinAddressValidator;
private final FilterManager filterManager;
private InputTextField addressInputTextField;
private CheckBox tradeInstantCheckBox;
private boolean tradeInstant;
public static int addFormForBuyer(GridPane gridPane,
int gridRow,
PaymentAccountPayload paymentAccountPayload,
String labelTitle) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, labelTitle,
((AssetAccountPayload) paymentAccountPayload).getAddress());
return gridRow;
}
public AssetsForm(PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService,
AltCoinAddressValidator altCoinAddressValidator,
InputValidator inputValidator,
GridPane gridPane,
int gridRow,
CoinFormatter formatter,
FilterManager filterManager) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.assetAccount = (AssetAccount) paymentAccount;
this.altCoinAddressValidator = altCoinAddressValidator;
this.filterManager = filterManager;
tradeInstant = paymentAccount instanceof InstantCryptoCurrencyAccount;
}
@Override
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
addTradeCurrencyComboBox();
currencyComboBox.setPrefWidth(250);
tradeInstantCheckBox = addLabelCheckBox(gridPane, ++gridRow,
Res.get("payment.altcoin.tradeInstantCheckbox"), 10);
tradeInstantCheckBox.setSelected(tradeInstant);
tradeInstantCheckBox.setOnAction(e -> {
tradeInstant = tradeInstantCheckBox.isSelected();
if (tradeInstant)
new Popup().information(Res.get("payment.altcoin.tradeInstant.popup")).show();
paymentLimitationsTextField.setText(getLimitationsText());
});
gridPane.getChildren().remove(tradeInstantCheckBox);
tradeInstantCheckBox.setPadding(new Insets(0, 40, 0, 0));
gridPane.getChildren().add(tradeInstantCheckBox);
addressInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow,
Res.get("payment.altcoin.address"));
addressInputTextField.setValidator(altCoinAddressValidator);
addressInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
if (newValue.startsWith("monero:")) {
UserThread.execute(() -> {
String addressWithoutPrefix = newValue.replace("monero:", "");
addressInputTextField.setText(addressWithoutPrefix);
});
return;
}
assetAccount.setAddress(newValue);
updateFromInputs();
});
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
@Override
public PaymentAccount getPaymentAccount() {
if (tradeInstant) {
InstantCryptoCurrencyAccount instantCryptoCurrencyAccount = new InstantCryptoCurrencyAccount();
instantCryptoCurrencyAccount.init();
instantCryptoCurrencyAccount.setAccountName(paymentAccount.getAccountName());
instantCryptoCurrencyAccount.setSaltAsHex(paymentAccount.getSaltAsHex());
instantCryptoCurrencyAccount.setSalt(paymentAccount.getSalt());
instantCryptoCurrencyAccount.setSingleTradeCurrency(paymentAccount.getSingleTradeCurrency());
instantCryptoCurrencyAccount.setSelectedTradeCurrency(paymentAccount.getSelectedTradeCurrency());
instantCryptoCurrencyAccount.setAddress(assetAccount.getAddress());
return instantCryptoCurrencyAccount;
} else {
return paymentAccount;
}
}
@Override
public void updateFromInputs() {
if (addressInputTextField != null && assetAccount.getSingleTradeCurrency() != null)
addressInputTextField.setPromptText(Res.get("payment.altcoin.address.dyn",
assetAccount.getSingleTradeCurrency().getName()));
super.updateFromInputs();
}
@Override
protected void autoFillNameTextField() {
if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) {
accountNameTextField.setText(createAssetsAccountName(paymentAccount, assetAccount.getAddress()));
}
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(assetAccount.getPaymentMethod().getId()));
Tuple3<Label, TextField, VBox> tuple2 = addCompactTopLabelTextField(gridPane, ++gridRow,
Res.get("payment.altcoin.address"), assetAccount.getAddress());
TextField field = tuple2.second;
field.setMouseTransparent(false);
final TradeCurrency singleTradeCurrency = assetAccount.getSingleTradeCurrency();
final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "";
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.altcoin"),
nameAndCode);
addLimitations(true);
}
@Override
public void updateAllInputsValid() {
TradeCurrency selectedTradeCurrency = assetAccount.getSelectedTradeCurrency();
if (selectedTradeCurrency != null) {
altCoinAddressValidator.setCurrencyCode(selectedTradeCurrency.getCode());
allInputsValid.set(isAccountNameValid()
&& altCoinAddressValidator.validate(assetAccount.getAddress()).isValid
&& assetAccount.getSingleTradeCurrency() != null);
}
}
@Override
protected void addTradeCurrencyComboBox() {
currencyComboBox = FormBuilder.<TradeCurrency>addLabelAutocompleteComboBox(gridPane, ++gridRow, Res.get("payment.altcoin"),
Layout.FIRST_ROW_AND_GROUP_DISTANCE).second;
currencyComboBox.setPromptText(Res.get("payment.select.altcoin"));
currencyComboBox.setButtonCell(getComboBoxButtonCell(Res.get("payment.select.altcoin"), currencyComboBox));
currencyComboBox.getEditor().focusedProperty().addListener(observable ->
currencyComboBox.setPromptText(""));
((AutocompleteComboBox<TradeCurrency>) currencyComboBox).setAutocompleteItems(
CurrencyUtil.getActiveSortedCryptoCurrencies(filterManager));
currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 10));
currencyComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(TradeCurrency tradeCurrency) {
return tradeCurrency != null ? tradeCurrency.getNameAndCode() : "";
}
@Override
public TradeCurrency fromString(String s) {
return currencyComboBox.getItems().stream().
filter(item -> item.getNameAndCode().equals(s)).
findAny().orElse(null);
}
});
((AutocompleteComboBox<?>) currencyComboBox).setOnChangeConfirmed(e -> {
addressInputTextField.resetValidation();
addressInputTextField.validate();
TradeCurrency tradeCurrency = currencyComboBox.getSelectionModel().getSelectedItem();
paymentAccount.setSingleTradeCurrency(tradeCurrency);
updateFromInputs();
if (tradeCurrency != null && tradeCurrency.getCode().equals("BSQ")) {
new Popup().information(Res.get("payment.select.altcoin.bsq.warning")).show();
}
});
}
}

View file

@ -0,0 +1,117 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static haveno.desktop.util.FormBuilder.addTopLabelTextField;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.AustraliaPayidAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.AustraliaPayidAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.validation.AustraliaPayidValidator;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.FormBuilder;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class AustraliaPayidForm extends PaymentMethodForm {
private final AustraliaPayidAccount australiaPayidAccount;
private final AustraliaPayidValidator australiaPayidValidator;
public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) {
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"),
((AustraliaPayidAccountPayload) paymentAccountPayload).getBankAccountName());
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"),
((AustraliaPayidAccountPayload) paymentAccountPayload).getPayid());
return gridRow;
}
public AustraliaPayidForm(PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService,
AustraliaPayidValidator australiaPayidValidator,
InputValidator inputValidator,
GridPane gridPane,
int gridRow,
CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.australiaPayidAccount = (AustraliaPayidAccount) paymentAccount;
this.australiaPayidValidator = australiaPayidValidator;
}
@Override
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow,
Res.get("payment.account.owner"));
holderNameInputTextField.setValidator(inputValidator);
holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
australiaPayidAccount.setBankAccountName(newValue);
updateFromInputs();
});
InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.payid"));
mobileNrInputTextField.setValidator(australiaPayidValidator);
mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
australiaPayidAccount.setPayid(newValue);
updateFromInputs();
});
TradeCurrency singleTradeCurrency = australiaPayidAccount.getSingleTradeCurrency();
String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null";
addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode);
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(australiaPayidAccount.getPayid());
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(australiaPayidAccount.getPaymentMethod().getId()));
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.payid"),
australiaPayidAccount.getPayid());
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"),
australiaPayidAccount.getBankAccountName()).second;
field.setMouseTransparent(false);
TradeCurrency singleTradeCurrency = australiaPayidAccount.getSingleTradeCurrency();
String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null";
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode);
addLimitations(true);
}
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& australiaPayidValidator.validate(australiaPayidAccount.getPayid()).isValid
&& inputValidator.validate(australiaPayidAccount.getBankAccountName()).isValid
&& australiaPayidAccount.getTradeCurrencies().size() > 0);
}
}

View file

@ -0,0 +1,434 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.*;
import haveno.common.util.Tuple2;
import haveno.common.util.Tuple4;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.BankUtil;
import haveno.core.locale.Country;
import haveno.core.locale.CountryUtil;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.FiatCurrency;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.CountryBasedPaymentAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.BankAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.GUIUtil;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.collections.FXCollections;
abstract class BankForm extends GeneralBankForm {
static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) {
BankAccountPayload data = (BankAccountPayload) paymentAccountPayload;
String countryCode = ((BankAccountPayload) paymentAccountPayload).getCountryCode();
int colIndex = 0;
if (data.getHolderTaxId() != null) {
final String title = Res.get("payment.account.owner") + " / " + BankUtil.getHolderIdLabelShort(countryCode);
final String value = data.getHolderName() + " / " + data.getHolderTaxId();
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), title, value);
} else {
final String title = Res.get("payment.account.owner");
final String value = data.getHolderName();
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), title, value);
}
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), Res.get("payment.bank.country"),
CountryUtil.getNameAndCode(countryCode));
// We don't want to display more than 6 rows to avoid scrolling, so if we get too many fields we combine them horizontally
int nrRows = 0;
if (BankUtil.isBankNameRequired(countryCode))
nrRows++;
if (BankUtil.isBankIdRequired(countryCode))
nrRows++;
if (BankUtil.isBranchIdRequired(countryCode))
nrRows++;
if (BankUtil.isAccountNrRequired(countryCode))
nrRows++;
if (BankUtil.isAccountTypeRequired(countryCode))
nrRows++;
if (BankUtil.isNationalAccountIdRequired(countryCode))
nrRows++;
String bankNameLabel = BankUtil.getBankNameLabel(countryCode);
String bankIdLabel = BankUtil.getBankIdLabel(countryCode);
String branchIdLabel = BankUtil.getBranchIdLabel(countryCode);
String nationalAccountIdLabel = BankUtil.getNationalAccountIdLabel(countryCode);
String accountNrLabel = BankUtil.getAccountNrLabel(countryCode);
String accountTypeLabel = BankUtil.getAccountTypeLabel(countryCode);
accountNrAccountTypeCombined = false;
nationalAccountIdAccountNrCombined = false;
bankNameBankIdCombined = false;
bankIdBranchIdCombined = false;
bankNameBranchIdCombined = false;
branchIdAccountNrCombined = false;
prepareFormLayoutFlags(countryCode, nrRows);
if (bankNameBankIdCombined) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
bankNameLabel + " / " +
bankIdLabel + ":",
data.getBankName() + " / " + data.getBankId(), true);
}
if (bankNameBranchIdCombined) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
bankNameLabel + " / " +
branchIdLabel + ":",
data.getBankName() + " / " + data.getBranchId(), true);
}
if (!bankNameBankIdCombined && !bankNameBranchIdCombined && BankUtil.isBankNameRequired(countryCode))
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankNameLabel, data.getBankName());
if (!bankNameBankIdCombined && !bankNameBranchIdCombined &&
!branchIdAccountNrCombined && bankIdBranchIdCombined) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
bankIdLabel + " / " +
branchIdLabel + ":",
data.getBankId() + " / " + data.getBranchId());
}
if (!bankNameBankIdCombined && !bankIdBranchIdCombined && BankUtil.isBankIdRequired(countryCode))
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankIdLabel, data.getBankId());
if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && branchIdAccountNrCombined) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
branchIdLabel + " / " +
accountNrLabel + ":",
data.getBranchId() + " / " + data.getAccountNr());
}
if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && !branchIdAccountNrCombined &&
BankUtil.isBranchIdRequired(countryCode))
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), branchIdLabel, data.getBranchId());
if (!branchIdAccountNrCombined && accountNrAccountTypeCombined) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
accountNrLabel + " / " + accountTypeLabel,
data.getAccountNr() + " / " + data.getAccountType());
}
if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && !nationalAccountIdAccountNrCombined &&
BankUtil.isAccountNrRequired(countryCode))
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountNrLabel, data.getAccountNr());
if (!accountNrAccountTypeCombined && BankUtil.isAccountTypeRequired(countryCode))
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountTypeLabel, data.getAccountType());
if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && nationalAccountIdAccountNrCombined)
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
nationalAccountIdLabel + " / " +
accountNrLabel, data.getNationalAccountId() +
" / " + data.getAccountNr());
return gridRow;
}
private final BankAccountPayload bankAccountPayload;
private InputTextField holderNameInputTextField;
private ComboBox<String> accountTypeComboBox;
private Country selectedCountry;
BankForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator,
GridPane gridPane, int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.bankAccountPayload = (BankAccountPayload) paymentAccount.paymentAccountPayload;
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
String countryCode = bankAccountPayload.getCountryCode();
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(paymentAccount.getPaymentMethod().getId()));
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"),
getCountryBasedPaymentAccount().getCountry() != null ? getCountryBasedPaymentAccount().getCountry().name : "");
TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency();
String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null";
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode);
addAcceptedBanksForDisplayAccount();
addHolderNameAndIdForDisplayAccount();
if (BankUtil.isBankNameRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.name"),
bankAccountPayload.getBankName()).second.setMouseTransparent(false);
if (BankUtil.isBankIdRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel(countryCode),
bankAccountPayload.getBankId()).second.setMouseTransparent(false);
if (BankUtil.isBranchIdRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(countryCode),
bankAccountPayload.getBranchId()).second.setMouseTransparent(false);
if (BankUtil.isNationalAccountIdRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel(countryCode),
bankAccountPayload.getNationalAccountId()).second.setMouseTransparent(false);
if (BankUtil.isAccountNrRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(countryCode),
bankAccountPayload.getAccountNr()).second.setMouseTransparent(false);
if (BankUtil.isAccountTypeRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountTypeLabel(countryCode),
bankAccountPayload.getAccountType()).second.setMouseTransparent(false);
addLimitations(true);
}
@Override
public void addFormForAddAccount() {
accountNrInputTextFieldEdited = false;
gridRowFrom = gridRow + 1;
Tuple2<ComboBox<TradeCurrency>, Integer> tuple = GUIUtil.addRegionCountryTradeCurrencyComboBoxes(gridPane, gridRow, this::onCountrySelected, this::onTradeCurrencySelected);
currencyComboBox = tuple.first;
gridRow = tuple.second;
addAcceptedBanksForAddAccount();
addHolderNameAndId();
nationalAccountIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel(""));
nationalAccountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
bankAccountPayload.setNationalAccountId(newValue);
updateFromInputs();
});
bankNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.bank.name"));
bankNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
bankAccountPayload.setBankName(newValue.trim());
updateFromInputs();
});
bankIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel(""));
bankIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
bankAccountPayload.setBankId(newValue.trim());
updateFromInputs();
});
branchIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(""));
branchIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
bankAccountPayload.setBranchId(newValue.trim());
updateFromInputs();
});
accountNrInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(""));
accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
bankAccountPayload.setAccountNr(newValue.trim());
updateFromInputs();
});
accountTypeComboBox = addComboBox(gridPane, ++gridRow, "");
accountTypeComboBox.setPromptText(Res.get("payment.select.account"));
accountTypeComboBox.setOnAction(e -> {
if (BankUtil.isAccountTypeRequired(bankAccountPayload.getCountryCode())) {
bankAccountPayload.setAccountType(accountTypeComboBox.getSelectionModel().getSelectedItem());
updateFromInputs();
}
});
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
updateFromInputs();
}
private void onCountrySelected(Country country) {
selectedCountry = country;
if (country != null) {
getCountryBasedPaymentAccount().setCountry(country);
String countryCode = country.code;
TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode);
paymentAccount.setSingleTradeCurrency(currency);
currencyComboBox.setDisable(false);
currencyComboBox.getSelectionModel().select(currency);
bankIdInputTextField.setPromptText(BankUtil.getBankIdLabel(countryCode));
branchIdInputTextField.setPromptText(BankUtil.getBranchIdLabel(countryCode));
nationalAccountIdInputTextField.setPromptText(BankUtil.getNationalAccountIdLabel(countryCode));
accountNrInputTextField.setPromptText(BankUtil.getAccountNrLabel(countryCode));
accountTypeComboBox.setPromptText(BankUtil.getAccountTypeLabel(countryCode));
bankNameInputTextField.setText("");
bankIdInputTextField.setText("");
branchIdInputTextField.setText("");
nationalAccountIdInputTextField.setText("");
accountNrInputTextField.setText("");
accountNrInputTextField.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) accountNrInputTextFieldEdited = true;
});
accountTypeComboBox.getSelectionModel().clearSelection();
accountTypeComboBox.setItems(FXCollections.observableArrayList(BankUtil.getAccountTypeValues(countryCode)));
validateInput(countryCode);
holderNameInputTextField.resetValidation();
holderNameInputTextField.validate();
bankNameInputTextField.resetValidation();
bankNameInputTextField.validate();
bankIdInputTextField.resetValidation();
bankIdInputTextField.validate();
branchIdInputTextField.resetValidation();
branchIdInputTextField.validate();
accountNrInputTextField.resetValidation();
accountNrInputTextField.validate();
nationalAccountIdInputTextField.resetValidation();
nationalAccountIdInputTextField.validate();
boolean requiresHolderId = BankUtil.isHolderIdRequired(countryCode);
if (requiresHolderId) {
holderNameInputTextField.minWidthProperty().unbind();
holderNameInputTextField.setMinWidth(250);
} else {
holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty());
}
updateHolderIDInput(countryCode, requiresHolderId);
boolean nationalAccountIdRequired = BankUtil.isNationalAccountIdRequired(countryCode);
nationalAccountIdInputTextField.setVisible(nationalAccountIdRequired);
nationalAccountIdInputTextField.setManaged(nationalAccountIdRequired);
boolean bankNameRequired = BankUtil.isBankNameRequired(countryCode);
bankNameInputTextField.setVisible(bankNameRequired);
bankNameInputTextField.setManaged(bankNameRequired);
boolean bankIdRequired = BankUtil.isBankIdRequired(countryCode);
bankIdInputTextField.setVisible(bankIdRequired);
bankIdInputTextField.setManaged(bankIdRequired);
boolean branchIdRequired = BankUtil.isBranchIdRequired(countryCode);
branchIdInputTextField.setVisible(branchIdRequired);
branchIdInputTextField.setManaged(branchIdRequired);
boolean accountNrRequired = BankUtil.isAccountNrRequired(countryCode);
accountNrInputTextField.setVisible(accountNrRequired);
accountNrInputTextField.setManaged(accountNrRequired);
boolean accountTypeRequired = BankUtil.isAccountTypeRequired(countryCode);
accountTypeComboBox.setVisible(accountTypeRequired);
accountTypeComboBox.setManaged(accountTypeRequired);
updateFromInputs();
onCountryChanged();
}
}
private void onTradeCurrencySelected(TradeCurrency tradeCurrency) {
FiatCurrency defaultCurrency = CurrencyUtil.getCurrencyByCountryCode(selectedCountry.code);
applyTradeCurrency(tradeCurrency, defaultCurrency);
}
private CountryBasedPaymentAccount getCountryBasedPaymentAccount() {
return (CountryBasedPaymentAccount) this.paymentAccount;
}
protected void onCountryChanged() {
}
private void addHolderNameAndId() {
Tuple2<InputTextField, InputTextField> tuple = addInputTextFieldInputTextField(gridPane,
++gridRow, Res.get("payment.account.owner"), BankUtil.getHolderIdLabel(""));
holderNameInputTextField = tuple.first;
holderNameInputTextField.setMinWidth(250);
holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
bankAccountPayload.setHolderName(newValue.trim());
updateFromInputs();
});
holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty());
holderNameInputTextField.setValidator(inputValidator);
useHolderID = true;
holderIdInputTextField = tuple.second;
holderIdInputTextField.setVisible(false);
holderIdInputTextField.setManaged(false);
holderIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
bankAccountPayload.setHolderTaxId(newValue);
updateFromInputs();
});
}
@Override
public void updateAllInputsValid() {
boolean result = isAccountNameValid()
&& paymentAccount.getSingleTradeCurrency() != null
&& getCountryBasedPaymentAccount().getCountry() != null
&& inputValidator.validate(bankAccountPayload.getHolderName()).isValid;
String countryCode = bankAccountPayload.getCountryCode();
result = getValidationResult(result, countryCode,
bankAccountPayload.getBankName(),
bankAccountPayload.getBankId(),
bankAccountPayload.getBranchId(),
bankAccountPayload.getAccountNr(),
bankAccountPayload.getAccountType(),
bankAccountPayload.getHolderTaxId(),
bankAccountPayload.getNationalAccountId());
allInputsValid.set(result);
}
private void addHolderNameAndIdForDisplayAccount() {
String countryCode = bankAccountPayload.getCountryCode();
if (BankUtil.isHolderIdRequired(countryCode)) {
Tuple4<Label, TextField, Label, TextField> tuple = addCompactTopLabelTextFieldTopLabelTextField(gridPane, ++gridRow,
Res.get("payment.account.owner"), BankUtil.getHolderIdLabel(countryCode));
TextField holderNameTextField = tuple.second;
holderNameTextField.setText(bankAccountPayload.getHolderName());
holderNameTextField.setMinWidth(250);
tuple.fourth.setText(bankAccountPayload.getHolderTaxId());
} else {
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"), bankAccountPayload.getHolderName());
}
}
protected void addAcceptedBanksForAddAccount() {
}
public void addAcceptedBanksForDisplayAccount() {
}
}

View file

@ -0,0 +1,101 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static haveno.desktop.util.FormBuilder.addTopLabelTextField;
import static haveno.desktop.util.FormBuilder.addTopLabelTextFieldWithCopyIcon;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.CountryUtil;
import haveno.core.locale.Res;
import haveno.core.payment.BizumAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.BizumAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.FormBuilder;
import haveno.desktop.util.Layout;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class BizumForm extends PaymentMethodForm {
private final BizumAccount account;
public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload) {
addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.mobile"),
((BizumAccountPayload) paymentAccountPayload).getMobileNr(), Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE);
return gridRow;
}
public BizumForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService,
InputValidator inputValidator, GridPane gridPane,
int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.account = (BizumAccount) paymentAccount;
}
@Override
public void addFormForAddAccount() {
// this payment method is only for Spain/EUR
account.setSingleTradeCurrency(account.getSupportedCurrencies().get(0));
CountryUtil.findCountryByCode("ES").ifPresent(c -> account.setCountry(c));
gridRowFrom = gridRow + 1;
InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.mobile"));
mobileNrInputTextField.setValidator(inputValidator);
mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
account.setMobileNr(newValue.trim());
updateFromInputs();
});
addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode());
addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name);
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(account.getMobileNr());
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(account.getPaymentMethod().getId()));
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.mobile"),
account.getMobileNr()).second;
field.setMouseTransparent(false);
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), account.getSingleTradeCurrency().getNameAndCode());
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.country"), account.getCountry().name);
addLimitations(true);
}
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& inputValidator.validate(account.getMobileNr()).isValid);
}
}

View file

@ -0,0 +1,117 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import static haveno.desktop.util.FormBuilder.addTopLabelFlowPane;
import haveno.common.util.Tuple2;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Res;
import haveno.core.payment.CapitualAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.CapitualAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.validation.CapitualValidator;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.FormBuilder;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
public class CapitualForm extends PaymentMethodForm {
private final CapitualAccount capitualAccount;
private final CapitualValidator capitualValidator;
public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.capitual.cap"),
((CapitualAccountPayload) paymentAccountPayload).getAccountNr());
return gridRow;
}
public CapitualForm(PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService,
CapitualValidator capitualValidator,
InputValidator inputValidator,
GridPane gridPane,
int gridRow,
CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.capitualAccount = (CapitualAccount) paymentAccount;
this.capitualValidator = capitualValidator;
}
@Override
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
InputTextField accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.capitual.cap"));
accountNrInputTextField.setValidator(capitualValidator);
accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
capitualAccount.setAccountNr(newValue);
updateFromInputs();
});
addCurrenciesGrid(true);
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
private void addCurrenciesGrid(boolean isEditable) {
final Tuple2<Label, FlowPane> labelFlowPaneTuple2 = addTopLabelFlowPane(gridPane, ++gridRow, Res.get("payment.supportedCurrencies"), 0);
FlowPane flowPane = labelFlowPaneTuple2.second;
if (isEditable)
flowPane.setId("flow-pane-checkboxes-bg");
else
flowPane.setId("flow-pane-checkboxes-non-editable-bg");
paymentAccount.getSupportedCurrencies().forEach(e ->
fillUpFlowPaneWithCurrencies(isEditable, flowPane, e, capitualAccount));
}
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(capitualAccount.getAccountNr());
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(capitualAccount.getPaymentMethod().getId()));
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.capitual.cap"),
capitualAccount.getAccountNr());
addLimitations(true);
addCurrenciesGrid(false);
}
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& capitualValidator.validate(capitualAccount.getAccountNr()).isValid
&& !capitualAccount.getTradeCurrencies().isEmpty());
}
}

View file

@ -0,0 +1,146 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.*;
import com.jfoenix.controls.JFXTextArea;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.CashByMailAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.CashByMailAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.Layout;
import javafx.scene.control.TextArea;
import javafx.scene.layout.GridPane;
import javafx.collections.FXCollections;
public class CashByMailForm extends PaymentMethodForm {
private final CashByMailAccount cashByMailAccount;
private TextArea postalAddressTextArea;
public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload) {
CashByMailAccountPayload cbm = (CashByMailAccountPayload) paymentAccountPayload;
addTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1,
Res.get("payment.account.owner"),
cbm.getHolderName(),
Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE);
TextArea textAddress = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second;
textAddress.setMinHeight(70);
textAddress.setEditable(false);
textAddress.setText(cbm.getPostalAddress());
TextArea textExtraInfo = addCompactTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second;
textExtraInfo.setMinHeight(70);
textExtraInfo.setEditable(false);
textExtraInfo.setText(cbm.getExtraInfo());
return gridRow;
}
public CashByMailForm(PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService,
InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.cashByMailAccount = (CashByMailAccount) paymentAccount;
}
@Override
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
addTradeCurrencyComboBox();
currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getAllSortedFiatCurrencies()));
InputTextField contactField = addInputTextField(gridPane, ++gridRow,
Res.get("payment.cashByMail.contact"));
contactField.setPromptText(Res.get("payment.cashByMail.contact.prompt"));
contactField.setValidator(inputValidator);
contactField.textProperty().addListener((ov, oldValue, newValue) -> {
cashByMailAccount.setContact(newValue);
updateFromInputs();
});
postalAddressTextArea = addTopLabelTextArea(gridPane, ++gridRow,
Res.get("payment.postal.address"), "").second;
postalAddressTextArea.setMinHeight(70);
postalAddressTextArea.textProperty().addListener((ov, oldValue, newValue) -> {
cashByMailAccount.setPostalAddress(newValue);
updateFromInputs();
});
TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow,
Res.get("payment.shared.optionalExtra"), Res.get("payment.cashByMail.extraInfo.prompt")).second;
extraTextArea.setMinHeight(70);
((JFXTextArea) extraTextArea).setLabelFloat(false);
extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> {
cashByMailAccount.setExtraInfo(newValue);
updateFromInputs();
});
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(cashByMailAccount.getContact());
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(cashByMailAccount.getPaymentMethod().getId()));
TradeCurrency tradeCurrency = paymentAccount.getSingleTradeCurrency();
String nameAndCode = tradeCurrency != null ? tradeCurrency.getNameAndCode() : "";
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode);
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.f2f.contact"),
cashByMailAccount.getContact());
TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.postal.address"), "").second;
textArea.setText(cashByMailAccount.getPostalAddress());
textArea.setMinHeight(70);
textArea.setEditable(false);
TextArea textAreaExtra = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second;
textAreaExtra.setText(cashByMailAccount.getExtraInfo());
textAreaExtra.setMinHeight(70);
textAreaExtra.setEditable(false);
addLimitations(true);
}
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& !cashByMailAccount.getPostalAddress().isEmpty()
&& inputValidator.validate(cashByMailAccount.getContact()).isValid
&& paymentAccount.getSingleTradeCurrency() != null);
}
}

View file

@ -0,0 +1,478 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.*;
import haveno.common.util.Tuple2;
import haveno.common.util.Tuple4;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.BankUtil;
import haveno.core.locale.Country;
import haveno.core.locale.CountryUtil;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.FiatCurrency;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.CountryBasedPaymentAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.CashDepositAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.validation.EmailValidator;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.GUIUtil;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.collections.FXCollections;
public class CashDepositForm extends GeneralBankForm {
public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) {
CashDepositAccountPayload data = (CashDepositAccountPayload) paymentAccountPayload;
String countryCode = data.getCountryCode();
String requirements = data.getRequirements();
boolean showRequirements = requirements != null && !requirements.isEmpty();
int colIndex = 0;
if (data.getHolderTaxId() != null)
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
Res.get("payment.account.name.emailAndHolderId", BankUtil.getHolderIdLabel(countryCode)),
data.getHolderName() + " / " + data.getHolderEmail() + " / " + data.getHolderTaxId());
else
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
Res.get("payment.account.name.email"),
data.getHolderName() + " / " + data.getHolderEmail());
if (!showRequirements)
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), Res.getWithCol("payment.bank.country"),
CountryUtil.getNameAndCode(countryCode));
else
requirements += "\n" + Res.get("payment.bank.country") + " " + CountryUtil.getNameAndCode(countryCode);
// We don't want to display more than 6 rows to avoid scrolling, so if we get too many fields we combine them horizontally
int nrRows = 0;
if (BankUtil.isBankNameRequired(countryCode))
nrRows++;
if (BankUtil.isBankIdRequired(countryCode))
nrRows++;
if (BankUtil.isBranchIdRequired(countryCode))
nrRows++;
if (BankUtil.isAccountNrRequired(countryCode))
nrRows++;
if (BankUtil.isAccountTypeRequired(countryCode))
nrRows++;
if (BankUtil.isNationalAccountIdRequired(countryCode))
nrRows++;
String bankNameLabel = BankUtil.getBankNameLabel(countryCode);
String bankIdLabel = BankUtil.getBankIdLabel(countryCode);
String branchIdLabel = BankUtil.getBranchIdLabel(countryCode);
String nationalAccountIdLabel = BankUtil.getNationalAccountIdLabel(countryCode);
String accountNrLabel = BankUtil.getAccountNrLabel(countryCode);
String accountTypeLabel = BankUtil.getAccountTypeLabel(countryCode);
accountNrAccountTypeCombined = false;
nationalAccountIdAccountNrCombined = false;
bankNameBankIdCombined = false;
bankIdBranchIdCombined = false;
bankNameBranchIdCombined = false;
branchIdAccountNrCombined = false;
prepareFormLayoutFlags(countryCode, nrRows);
if (bankNameBankIdCombined) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
bankNameLabel + " / " +
bankIdLabel,
data.getBankName() + " / " + data.getBankId());
}
if (bankNameBranchIdCombined) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
bankNameLabel + " / " +
branchIdLabel,
data.getBankName() + " / " + data.getBranchId());
}
if (!bankNameBankIdCombined && !bankNameBranchIdCombined && BankUtil.isBankNameRequired(countryCode))
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankNameLabel, data.getBankName());
if (!bankNameBankIdCombined && !bankNameBranchIdCombined && !branchIdAccountNrCombined && bankIdBranchIdCombined) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
bankIdLabel + " / " +
branchIdLabel,
data.getBankId() + " / " + data.getBranchId());
}
if (!bankNameBankIdCombined && !bankIdBranchIdCombined && BankUtil.isBankIdRequired(countryCode))
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), bankIdLabel, data.getBankId());
if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && branchIdAccountNrCombined) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
branchIdLabel + " / " +
accountNrLabel,
data.getBranchId() + " / " + data.getAccountNr());
}
if (!bankNameBranchIdCombined && !bankIdBranchIdCombined && !branchIdAccountNrCombined &&
BankUtil.isBranchIdRequired(countryCode))
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), branchIdLabel, data.getBranchId());
if (!branchIdAccountNrCombined && accountNrAccountTypeCombined) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
accountNrLabel + " / " + accountTypeLabel,
data.getAccountNr() + " / " + data.getAccountType());
}
if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && !nationalAccountIdAccountNrCombined && BankUtil.isAccountNrRequired(countryCode))
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountNrLabel, data.getAccountNr());
if (!accountNrAccountTypeCombined && BankUtil.isAccountTypeRequired(countryCode))
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++), accountTypeLabel, data.getAccountType());
if (!branchIdAccountNrCombined && !accountNrAccountTypeCombined && nationalAccountIdAccountNrCombined)
addCompactTopLabelTextFieldWithCopyIcon(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
nationalAccountIdLabel + " / " +
accountNrLabel, data.getNationalAccountId() +
" / " + data.getAccountNr());
if (showRequirements) {
TextArea textArea = addCompactTopLabelTextArea(gridPane, getIndexOfColumn(colIndex) == 0 ? ++gridRow : gridRow, getIndexOfColumn(colIndex++),
Res.get("payment.extras"), "").second;
textArea.setMinHeight(45);
textArea.setMaxHeight(45);
textArea.setEditable(false);
textArea.setId("text-area-disabled");
textArea.setText(requirements);
}
return gridRow;
}
private final CashDepositAccountPayload cashDepositAccountPayload;
private InputTextField holderNameInputTextField, emailInputTextField;
private ComboBox<String> accountTypeComboBox;
private final EmailValidator emailValidator;
private Country selectedCountry;
public CashDepositForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator,
GridPane gridPane, int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.cashDepositAccountPayload = (CashDepositAccountPayload) paymentAccount.paymentAccountPayload;
emailValidator = new EmailValidator();
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
String countryCode = cashDepositAccountPayload.getCountryCode();
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(paymentAccount.getPaymentMethod().getId()));
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"),
getCountryBasedPaymentAccount().getCountry() != null ? getCountryBasedPaymentAccount().getCountry().name : "");
TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency();
String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null";
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"),
nameAndCode);
addHolderNameAndIdForDisplayAccount();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"),
cashDepositAccountPayload.getHolderEmail());
if (BankUtil.isBankNameRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.bank.name"),
cashDepositAccountPayload.getBankName()).second.setMouseTransparent(false);
if (BankUtil.isBankIdRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel(countryCode),
cashDepositAccountPayload.getBankId()).second.setMouseTransparent(false);
if (BankUtil.isBranchIdRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(countryCode),
cashDepositAccountPayload.getBranchId()).second.setMouseTransparent(false);
if (BankUtil.isNationalAccountIdRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel(countryCode),
cashDepositAccountPayload.getNationalAccountId()).second.setMouseTransparent(false);
if (BankUtil.isAccountNrRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(countryCode),
cashDepositAccountPayload.getAccountNr()).second.setMouseTransparent(false);
if (BankUtil.isAccountTypeRequired(countryCode))
addCompactTopLabelTextField(gridPane, ++gridRow, BankUtil.getAccountTypeLabel(countryCode),
cashDepositAccountPayload.getAccountType()).second.setMouseTransparent(false);
String requirements = cashDepositAccountPayload.getRequirements();
boolean showRequirements = requirements != null && !requirements.isEmpty();
if (showRequirements) {
TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.extras"), "").second;
textArea.setMinHeight(30);
textArea.setMaxHeight(30);
textArea.setEditable(false);
textArea.setId("text-area-disabled");
textArea.setText(requirements);
}
addLimitations(true);
}
@Override
public void addFormForAddAccount() {
accountNrInputTextFieldEdited = false;
gridRowFrom = gridRow + 1;
Tuple2<ComboBox<TradeCurrency>, Integer> tuple = GUIUtil.addRegionCountryTradeCurrencyComboBoxes(gridPane, gridRow, this::onCountrySelected, this::onTradeCurrencySelected);
currencyComboBox = tuple.first;
gridRow = tuple.second;
addHolderNameAndId();
nationalAccountIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getNationalAccountIdLabel(""));
nationalAccountIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
cashDepositAccountPayload.setNationalAccountId(newValue);
updateFromInputs();
});
bankNameInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.bank.name"));
bankNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
cashDepositAccountPayload.setBankName(newValue);
updateFromInputs();
});
bankIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBankIdLabel(""));
bankIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
cashDepositAccountPayload.setBankId(newValue);
updateFromInputs();
});
branchIdInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getBranchIdLabel(""));
branchIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
cashDepositAccountPayload.setBranchId(newValue);
updateFromInputs();
});
accountNrInputTextField = addInputTextField(gridPane, ++gridRow, BankUtil.getAccountNrLabel(""));
accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
cashDepositAccountPayload.setAccountNr(newValue);
updateFromInputs();
});
accountTypeComboBox = addComboBox(gridPane, ++gridRow, Res.get("payment.select.account"));
accountTypeComboBox.setOnAction(e -> {
if (BankUtil.isAccountTypeRequired(cashDepositAccountPayload.getCountryCode())) {
cashDepositAccountPayload.setAccountType(accountTypeComboBox.getSelectionModel().getSelectedItem());
updateFromInputs();
}
});
TextArea requirementsTextArea = addTextArea(gridPane, ++gridRow, Res.get("payment.extras"));
requirementsTextArea.setMinHeight(30);
requirementsTextArea.setMaxHeight(90);
requirementsTextArea.textProperty().addListener((ov, oldValue, newValue) -> {
cashDepositAccountPayload.setRequirements(newValue);
updateFromInputs();
});
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
updateFromInputs();
}
private void onTradeCurrencySelected(TradeCurrency tradeCurrency) {
FiatCurrency defaultCurrency = CurrencyUtil.getCurrencyByCountryCode(selectedCountry.code);
applyTradeCurrency(tradeCurrency, defaultCurrency);
}
private void onCountrySelected(Country country) {
selectedCountry = country;
if (selectedCountry != null) {
getCountryBasedPaymentAccount().setCountry(selectedCountry);
String countryCode = selectedCountry.code;
TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode);
paymentAccount.setSingleTradeCurrency(currency);
currencyComboBox.setDisable(false);
currencyComboBox.getSelectionModel().select(currency);
bankIdInputTextField.setPromptText(BankUtil.getBankIdLabel(countryCode));
branchIdInputTextField.setPromptText(BankUtil.getBranchIdLabel(countryCode));
nationalAccountIdInputTextField.setPromptText(BankUtil.getNationalAccountIdLabel(countryCode));
accountNrInputTextField.setPromptText(BankUtil.getAccountNrLabel(countryCode));
accountTypeComboBox.setPromptText(BankUtil.getAccountTypeLabel(countryCode));
bankNameInputTextField.setText("");
bankIdInputTextField.setText("");
branchIdInputTextField.setText("");
nationalAccountIdInputTextField.setText("");
accountNrInputTextField.setText("");
accountNrInputTextField.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) accountNrInputTextFieldEdited = true;
});
accountTypeComboBox.getSelectionModel().clearSelection();
accountTypeComboBox.setItems(FXCollections.observableArrayList(BankUtil.getAccountTypeValues(countryCode)));
validateInput(countryCode);
holderNameInputTextField.resetValidation();
emailInputTextField.resetValidation();
bankNameInputTextField.resetValidation();
bankIdInputTextField.resetValidation();
branchIdInputTextField.resetValidation();
accountNrInputTextField.resetValidation();
nationalAccountIdInputTextField.resetValidation();
holderNameInputTextField.validate();
emailInputTextField.validate();
bankNameInputTextField.validate();
bankIdInputTextField.validate();
branchIdInputTextField.validate();
accountNrInputTextField.validate();
nationalAccountIdInputTextField.validate();
boolean requiresHolderId = BankUtil.isHolderIdRequired(countryCode);
if (requiresHolderId) {
holderNameInputTextField.minWidthProperty().unbind();
holderNameInputTextField.setMinWidth(300);
} else {
holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty());
}
updateHolderIDInput(countryCode, requiresHolderId);
boolean nationalAccountIdRequired = BankUtil.isNationalAccountIdRequired(countryCode);
nationalAccountIdInputTextField.setVisible(nationalAccountIdRequired);
nationalAccountIdInputTextField.setManaged(nationalAccountIdRequired);
boolean bankNameRequired = BankUtil.isBankNameRequired(countryCode);
bankNameInputTextField.setVisible(bankNameRequired);
bankNameInputTextField.setManaged(bankNameRequired);
boolean bankIdRequired = BankUtil.isBankIdRequired(countryCode);
bankIdInputTextField.setVisible(bankIdRequired);
bankIdInputTextField.setManaged(bankIdRequired);
boolean branchIdRequired = BankUtil.isBranchIdRequired(countryCode);
branchIdInputTextField.setVisible(branchIdRequired);
branchIdInputTextField.setManaged(branchIdRequired);
boolean accountNrRequired = BankUtil.isAccountNrRequired(countryCode);
accountNrInputTextField.setVisible(accountNrRequired);
accountNrInputTextField.setManaged(accountNrRequired);
boolean accountTypeRequired = BankUtil.isAccountTypeRequired(countryCode);
accountTypeComboBox.setVisible(accountTypeRequired);
accountTypeComboBox.setManaged(accountTypeRequired);
updateFromInputs();
onCountryChanged();
}
}
private CountryBasedPaymentAccount getCountryBasedPaymentAccount() {
return (CountryBasedPaymentAccount) this.paymentAccount;
}
private void onCountryChanged() {
}
private void addHolderNameAndId() {
Tuple2<InputTextField, InputTextField> tuple = addInputTextFieldInputTextField(gridPane,
++gridRow, Res.get("payment.account.owner"), BankUtil.getHolderIdLabel(""));
holderNameInputTextField = tuple.first;
holderNameInputTextField.setMinWidth(300);
holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
cashDepositAccountPayload.setHolderName(newValue);
updateFromInputs();
});
holderNameInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty());
holderNameInputTextField.setValidator(inputValidator);
emailInputTextField = addInputTextField(gridPane, ++gridRow, Res.get("payment.email"));
emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
cashDepositAccountPayload.setHolderEmail(newValue);
updateFromInputs();
});
emailInputTextField.minWidthProperty().bind(currencyComboBox.widthProperty());
emailInputTextField.setValidator(emailValidator);
useHolderID = true;
holderIdInputTextField = tuple.second;
holderIdInputTextField.setMinWidth(250);
holderIdInputTextField.setVisible(false);
holderIdInputTextField.setManaged(false);
holderIdInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
cashDepositAccountPayload.setHolderTaxId(newValue);
updateFromInputs();
});
}
@Override
public void updateAllInputsValid() {
boolean result = isAccountNameValid()
&& paymentAccount.getSingleTradeCurrency() != null
&& getCountryBasedPaymentAccount().getCountry() != null
&& inputValidator.validate(cashDepositAccountPayload.getHolderName()).isValid
&& emailValidator.validate(cashDepositAccountPayload.getHolderEmail()).isValid;
String countryCode = cashDepositAccountPayload.getCountryCode();
result = getValidationResult(result, countryCode,
cashDepositAccountPayload.getBankName(),
cashDepositAccountPayload.getBankId(),
cashDepositAccountPayload.getBranchId(),
cashDepositAccountPayload.getAccountNr(),
cashDepositAccountPayload.getAccountType(),
cashDepositAccountPayload.getHolderTaxId(),
cashDepositAccountPayload.getNationalAccountId());
allInputsValid.set(result);
}
private void addHolderNameAndIdForDisplayAccount() {
String countryCode = cashDepositAccountPayload.getCountryCode();
if (BankUtil.isHolderIdRequired(countryCode)) {
Tuple4<Label, TextField, Label, TextField> tuple = addCompactTopLabelTextFieldTopLabelTextField(gridPane, ++gridRow,
Res.get("payment.account.owner"), BankUtil.getHolderIdLabel(countryCode));
TextField holderNameTextField = tuple.second;
holderNameTextField.setText(cashDepositAccountPayload.getHolderName());
holderNameTextField.setMinWidth(300);
tuple.fourth.setText(cashDepositAccountPayload.getHolderTaxId());
} else {
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"),
cashDepositAccountPayload.getHolderName());
}
}
}

View file

@ -0,0 +1,111 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Res;
import haveno.core.payment.CelPayAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.CelPayAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.validation.EmailValidator;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.FormBuilder;
import javafx.scene.control.TextField;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
public class CelPayForm extends PaymentMethodForm {
private final CelPayAccount account;
private final EmailValidator validator = new EmailValidator();
public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.email"),
((CelPayAccountPayload) paymentAccountPayload).getEmail());
return gridRow;
}
public CelPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService,
InputValidator inputValidator, GridPane gridPane,
int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.account = (CelPayAccount) paymentAccount;
}
@Override
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
InputTextField emailInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email"));
emailInputTextField.setValidator(validator);
emailInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
account.setEmail(newValue.trim());
updateFromInputs();
});
addCurrenciesGrid(true);
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
private void addCurrenciesGrid(boolean isEditable) {
FlowPane flowPane = FormBuilder.addTopLabelFlowPane(gridPane, ++gridRow,
Res.get("payment.celpay.supportedCurrenciesForReceiver"), 20, 20).second;
if (isEditable) {
flowPane.setId("flow-pane-checkboxes-bg");
} else {
flowPane.setId("flow-pane-checkboxes-non-editable-bg");
}
account.getSupportedCurrencies().forEach(currency ->
fillUpFlowPaneWithCurrencies(isEditable, flowPane, currency, account));
}
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(account.getEmail());
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(account.getPaymentMethod().getId()));
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"),
account.getEmail()).second;
field.setMouseTransparent(false);
addLimitations(true);
addCurrenciesGrid(false);
}
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& account.getEmail() != null
&& validator.validate(account.getEmail()).isValid
&& account.getTradeCurrencies().size() > 0);
}
}

View file

@ -0,0 +1,113 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static haveno.desktop.util.FormBuilder.addTopLabelTextField;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.ChaseQuickPayAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.ChaseQuickPayAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.validation.ChaseQuickPayValidator;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.FormBuilder;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class ChaseQuickPayForm extends PaymentMethodForm {
private final ChaseQuickPayAccount chaseQuickPayAccount;
private final ChaseQuickPayValidator chaseQuickPayValidator;
public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) {
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"),
((ChaseQuickPayAccountPayload) paymentAccountPayload).getHolderName());
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"),
((ChaseQuickPayAccountPayload) paymentAccountPayload).getEmail());
return gridRow;
}
public ChaseQuickPayForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, ChaseQuickPayValidator chaseQuickPayValidator,
InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.chaseQuickPayAccount = (ChaseQuickPayAccount) paymentAccount;
this.chaseQuickPayValidator = chaseQuickPayValidator;
}
@Override
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow,
Res.get("payment.account.owner"));
holderNameInputTextField.setValidator(inputValidator);
holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
chaseQuickPayAccount.setHolderName(newValue);
updateFromInputs();
});
InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.email"));
mobileNrInputTextField.setValidator(chaseQuickPayValidator);
mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
chaseQuickPayAccount.setEmail(newValue);
updateFromInputs();
});
TradeCurrency singleTradeCurrency = chaseQuickPayAccount.getSingleTradeCurrency();
String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null";
addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode);
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(chaseQuickPayAccount.getEmail());
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(chaseQuickPayAccount.getPaymentMethod().getId()));
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"),
chaseQuickPayAccount.getHolderName());
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email"),
chaseQuickPayAccount.getEmail()).second;
field.setMouseTransparent(false);
TradeCurrency singleTradeCurrency = chaseQuickPayAccount.getSingleTradeCurrency();
String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null";
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode);
addLimitations(true);
}
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& chaseQuickPayValidator.validate(chaseQuickPayAccount.getEmail()).isValid
&& inputValidator.validate(chaseQuickPayAccount.getHolderName()).isValid
&& chaseQuickPayAccount.getTradeCurrencies().size() > 0);
}
}

View file

@ -0,0 +1,114 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import static haveno.desktop.util.FormBuilder.addTopLabelTextField;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.ClearXchangeAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.ClearXchangeAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.validation.EmailOrMobileNrValidator;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.FormBuilder;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class ClearXchangeForm extends PaymentMethodForm {
private final ClearXchangeAccount clearXchangeAccount;
private final EmailOrMobileNrValidator clearXchangeValidator;
public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) {
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.account.owner"),
((ClearXchangeAccountPayload) paymentAccountPayload).getHolderName());
addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.email.mobile"),
((ClearXchangeAccountPayload) paymentAccountPayload).getEmailOrMobileNr());
return gridRow;
}
public ClearXchangeForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, EmailOrMobileNrValidator clearXchangeValidator, InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.clearXchangeAccount = (ClearXchangeAccount) paymentAccount;
this.clearXchangeValidator = clearXchangeValidator;
}
@Override
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
InputTextField holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow,
Res.get("payment.account.owner"));
holderNameInputTextField.setValidator(inputValidator);
holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
clearXchangeAccount.setHolderName(newValue.trim());
updateFromInputs();
});
InputTextField mobileNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow,
Res.get("payment.email.mobile"));
mobileNrInputTextField.setValidator(clearXchangeValidator);
mobileNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
clearXchangeAccount.setEmailOrMobileNr(newValue.trim());
updateFromInputs();
});
final TradeCurrency singleTradeCurrency = clearXchangeAccount.getSingleTradeCurrency();
final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "";
addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"),
nameAndCode);
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(clearXchangeAccount.getEmailOrMobileNr());
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(clearXchangeAccount.getPaymentMethod().getId()));
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"),
clearXchangeAccount.getHolderName());
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.email.mobile"),
clearXchangeAccount.getEmailOrMobileNr()).second;
field.setMouseTransparent(false);
final TradeCurrency singleTradeCurrency = clearXchangeAccount.getSingleTradeCurrency();
final String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "";
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"),
nameAndCode);
addLimitations(true);
}
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& clearXchangeValidator.validate(clearXchangeAccount.getEmailOrMobileNr()).isValid
&& inputValidator.validate(clearXchangeAccount.getHolderName()).isValid
&& clearXchangeAccount.getTradeCurrencies().size() > 0);
}
}

View file

@ -0,0 +1,88 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Country;
import haveno.core.payment.CountryBasedPaymentAccount;
import haveno.core.payment.DomesticWireTransferAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.BankAccountPayload;
import haveno.core.payment.payload.DomesticWireTransferAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import javafx.scene.layout.GridPane;
public class DomesticWireTransferForm extends GeneralUsBankForm {
public static int addFormForBuyer(GridPane gridPane, int gridRow, PaymentAccountPayload paymentAccountPayload) {
DomesticWireTransferAccountPayload domesticWireTransferAccountPayload = (DomesticWireTransferAccountPayload) paymentAccountPayload;
return addFormForBuyer(gridPane, gridRow, paymentAccountPayload, null,
domesticWireTransferAccountPayload.getHolderAddress());
}
private final DomesticWireTransferAccount domesticWireTransferAccount;
public DomesticWireTransferForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, InputValidator inputValidator,
GridPane gridPane, int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.domesticWireTransferAccount = (DomesticWireTransferAccount) paymentAccount;
}
@Override
public void addFormForEditAccount() {
addFormForEditAccount(domesticWireTransferAccount.getPayload(), domesticWireTransferAccount.getPayload().getHolderAddress());
}
@Override
public void addFormForAddAccount() {
addFormForAddAccountInternal(domesticWireTransferAccount.getPayload(), domesticWireTransferAccount.getPayload().getHolderAddress());
}
@Override
protected void setHolderAddress(String holderAddress) {
domesticWireTransferAccount.getPayload().setHolderAddress(holderAddress);
}
@Override
protected void maybeAddAccountTypeCombo(BankAccountPayload bankAccountPayload, Country country) {
// DomesticWireTransfer does not use the account type combo
}
@Override
public void updateAllInputsValid() {
DomesticWireTransferAccountPayload domesticWireTransferAccountPayload = domesticWireTransferAccount.getPayload();
boolean result = isAccountNameValid()
&& paymentAccount.getSingleTradeCurrency() != null
&& ((CountryBasedPaymentAccount) this.paymentAccount).getCountry() != null
&& inputValidator.validate(domesticWireTransferAccountPayload.getHolderName()).isValid
&& inputValidator.validate(domesticWireTransferAccountPayload.getHolderAddress()).isValid;
result = getValidationResult(result,
domesticWireTransferAccountPayload.getCountryCode(),
domesticWireTransferAccountPayload.getBankName(),
domesticWireTransferAccountPayload.getBankId(),
domesticWireTransferAccountPayload.getBranchId(),
domesticWireTransferAccountPayload.getAccountNr(),
domesticWireTransferAccountPayload.getAccountNr(),
domesticWireTransferAccountPayload.getHolderTaxId(),
domesticWireTransferAccountPayload.getNationalAccountId());
allInputsValid.set(result);
}
}

View file

@ -0,0 +1,177 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.*;
import com.jfoenix.controls.JFXTextArea;
import haveno.common.util.Tuple2;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Country;
import haveno.core.locale.CountryUtil;
import haveno.core.locale.CurrencyUtil;
import haveno.core.locale.FiatCurrency;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.offer.Offer;
import haveno.core.payment.CountryBasedPaymentAccount;
import haveno.core.payment.F2FAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.F2FAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.validation.F2FValidator;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.GUIUtil;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextArea;
import javafx.scene.layout.GridPane;
public class F2FForm extends PaymentMethodForm {
private final F2FAccount f2fAccount;
private final F2FValidator f2fValidator;
private Country selectedCountry;
public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload, Offer offer, double top) {
F2FAccountPayload f2fAccountPayload = (F2FAccountPayload) paymentAccountPayload;
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, 0, Res.get("shared.country"),
CountryUtil.getNameAndCode(f2fAccountPayload.getCountryCode()), top);
addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.f2f.city"),
offer.getF2FCity(), top);
addCompactTopLabelTextFieldWithCopyIcon(gridPane, ++gridRow, Res.get("payment.f2f.contact"),
f2fAccountPayload.getContact());
TextArea textArea = addTopLabelTextArea(gridPane, gridRow, 1, Res.get("payment.shared.extraInfo"), "").second;
textArea.setMinHeight(70);
textArea.setEditable(false);
textArea.setId("text-area-disabled");
textArea.setText(offer.getExtraInfo());
return gridRow;
}
public F2FForm(PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService, F2FValidator f2fValidator,
InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.f2fAccount = (F2FAccount) paymentAccount;
this.f2fValidator = f2fValidator;
}
@Override
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
Tuple2<ComboBox<TradeCurrency>, Integer> tuple = GUIUtil.addRegionCountryTradeCurrencyComboBoxes(gridPane, gridRow, this::onCountrySelected, this::onTradeCurrencySelected);
currencyComboBox = tuple.first;
gridRow = tuple.second;
InputTextField contactInputTextField = addInputTextField(gridPane, ++gridRow,
Res.get("payment.f2f.contact"));
contactInputTextField.setPromptText(Res.get("payment.f2f.contact.prompt"));
contactInputTextField.setValidator(f2fValidator);
contactInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
f2fAccount.setContact(newValue);
updateFromInputs();
});
InputTextField cityInputTextField = addInputTextField(gridPane, ++gridRow,
Res.get("payment.f2f.city"));
cityInputTextField.setPromptText(Res.get("payment.f2f.city.prompt"));
cityInputTextField.setValidator(f2fValidator);
cityInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
f2fAccount.setCity(newValue);
updateFromInputs();
});
TextArea extraTextArea = addTopLabelTextArea(gridPane, ++gridRow,
Res.get("payment.shared.optionalExtra"), Res.get("payment.shared.extraInfo.prompt")).second;
extraTextArea.setMinHeight(70);
((JFXTextArea) extraTextArea).setLabelFloat(false);
//extraTextArea.setValidator(f2fValidator);
extraTextArea.textProperty().addListener((ov, oldValue, newValue) -> {
f2fAccount.setExtraInfo(newValue);
updateFromInputs();
});
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
private void onCountrySelected(Country country) {
selectedCountry = country;
if (selectedCountry != null) {
getCountryBasedPaymentAccount().setCountry(selectedCountry);
String countryCode = selectedCountry.code;
TradeCurrency currency = CurrencyUtil.getCurrencyByCountryCode(countryCode);
paymentAccount.setSingleTradeCurrency(currency);
currencyComboBox.setDisable(false);
currencyComboBox.getSelectionModel().select(currency);
updateFromInputs();
}
}
private void onTradeCurrencySelected(TradeCurrency tradeCurrency) {
FiatCurrency defaultCurrency = CurrencyUtil.getCurrencyByCountryCode(selectedCountry.code);
applyTradeCurrency(tradeCurrency, defaultCurrency);
}
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(f2fAccount.getCity());
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(paymentAccount.getPaymentMethod().getId()));
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.country"),
getCountryBasedPaymentAccount().getCountry() != null ? getCountryBasedPaymentAccount().getCountry().name : "");
TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency();
String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null";
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode);
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.f2f.contact", f2fAccount.getContact()),
f2fAccount.getContact());
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.f2f.city", f2fAccount.getCity()),
f2fAccount.getCity());
TextArea textArea = addCompactTopLabelTextArea(gridPane, ++gridRow, Res.get("payment.shared.extraInfo"), "").second;
textArea.setText(f2fAccount.getExtraInfo());
textArea.setMinHeight(70);
textArea.setEditable(false);
addLimitations(true);
}
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& f2fValidator.validate(f2fAccount.getContact()).isValid
&& f2fValidator.validate(f2fAccount.getCity()).isValid
&& f2fAccount.getTradeCurrencies().size() > 0);
}
private CountryBasedPaymentAccount getCountryBasedPaymentAccount() {
return (CountryBasedPaymentAccount) this.paymentAccount;
}
}

View file

@ -0,0 +1,144 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package haveno.desktop.components.paymentmethods;
import static haveno.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static haveno.desktop.util.FormBuilder.addTopLabelTextField;
import haveno.core.account.witness.AccountAgeWitnessService;
import haveno.core.locale.Res;
import haveno.core.locale.TradeCurrency;
import haveno.core.payment.FasterPaymentsAccount;
import haveno.core.payment.PaymentAccount;
import haveno.core.payment.payload.FasterPaymentsAccountPayload;
import haveno.core.payment.payload.PaymentAccountPayload;
import haveno.core.payment.validation.AccountNrValidator;
import haveno.core.payment.validation.BranchIdValidator;
import haveno.core.util.coin.CoinFormatter;
import haveno.core.util.validation.InputValidator;
import haveno.desktop.components.InputTextField;
import haveno.desktop.util.FormBuilder;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
public class FasterPaymentsForm extends PaymentMethodForm {
private static final String UK_SORT_CODE = "UK sort code";
public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload) {
if (!((FasterPaymentsAccountPayload) paymentAccountPayload).getHolderName().isEmpty()) {
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"),
((FasterPaymentsAccountPayload) paymentAccountPayload).getHolderName());
}
// do not translate as it is used in English only
addCompactTopLabelTextField(gridPane, ++gridRow, UK_SORT_CODE,
((FasterPaymentsAccountPayload) paymentAccountPayload).getSortCode());
addCompactTopLabelTextField(gridPane, gridRow, 1, Res.get("payment.accountNr"),
((FasterPaymentsAccountPayload) paymentAccountPayload).getAccountNr());
return gridRow;
}
private final FasterPaymentsAccount fasterPaymentsAccount;
private InputTextField holderNameInputTextField;
private InputTextField accountNrInputTextField;
private InputTextField sortCodeInputTextField;
private final BranchIdValidator branchIdValidator;
private final AccountNrValidator accountNrValidator;
public FasterPaymentsForm(PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService,
InputValidator inputValidator,
GridPane gridPane,
int gridRow,
CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.fasterPaymentsAccount = (FasterPaymentsAccount) paymentAccount;
this.branchIdValidator = new BranchIdValidator("GB");
this.accountNrValidator = new AccountNrValidator("GB");
}
@Override
public void addFormForAddAccount() {
gridRowFrom = gridRow + 1;
holderNameInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.account.owner"));
holderNameInputTextField.setValidator(inputValidator);
holderNameInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
fasterPaymentsAccount.setHolderName(newValue);
updateFromInputs();
});
// do not translate as it is used in English only
sortCodeInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, UK_SORT_CODE);
sortCodeInputTextField.setValidator(inputValidator);
sortCodeInputTextField.setValidator(branchIdValidator);
sortCodeInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
fasterPaymentsAccount.setSortCode(newValue);
updateFromInputs();
});
accountNrInputTextField = FormBuilder.addInputTextField(gridPane, ++gridRow, Res.get("payment.accountNr"));
accountNrInputTextField.setValidator(accountNrValidator);
accountNrInputTextField.textProperty().addListener((ov, oldValue, newValue) -> {
fasterPaymentsAccount.setAccountNr(newValue);
updateFromInputs();
});
TradeCurrency singleTradeCurrency = fasterPaymentsAccount.getSingleTradeCurrency();
String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "";
addTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"),
nameAndCode);
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
}
@Override
protected void autoFillNameTextField() {
setAccountNameWithString(fasterPaymentsAccount.getAccountNr());
}
@Override
public void addFormForEditAccount() {
gridRowFrom = gridRow;
addAccountNameTextFieldWithAutoFillToggleButton();
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.paymentMethod"),
Res.get(fasterPaymentsAccount.getPaymentMethod().getId()));
if (!fasterPaymentsAccount.getHolderName().isEmpty()) {
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.owner"),
fasterPaymentsAccount.getHolderName());
}
// do not translate as it is used in English only
addCompactTopLabelTextField(gridPane, ++gridRow, UK_SORT_CODE, fasterPaymentsAccount.getSortCode());
TextField field = addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("payment.accountNr"),
fasterPaymentsAccount.getAccountNr()).second;
field.setMouseTransparent(false);
TradeCurrency singleTradeCurrency = fasterPaymentsAccount.getSingleTradeCurrency();
String nameAndCode = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "";
addCompactTopLabelTextField(gridPane, ++gridRow, Res.get("shared.currency"), nameAndCode);
addLimitations(true);
}
@Override
public void updateAllInputsValid() {
allInputsValid.set(isAccountNameValid()
&& inputValidator.validate(fasterPaymentsAccount.getHolderName()).isValid
&& branchIdValidator.validate(fasterPaymentsAccount.getSortCode()).isValid
&& accountNrValidator.validate(fasterPaymentsAccount.getAccountNr()).isValid
&& fasterPaymentsAccount.getTradeCurrencies().size() > 0);
}
}

Some files were not shown because too many files have changed in this diff Show more