/*
* opsu! - an open-source osu! client
* Copyright (C) 2014-2017 Jeffrey Han
*
* opsu! is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* opsu! 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with opsu!. If not, see <http://www.gnu.org/licenses/>.
*/
package itdelatrisu.opsu.ui;
import itdelatrisu.opsu.ui.animations.AnimationEquation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Input;
import org.newdawn.slick.MouseListener;
import org.newdawn.slick.UnicodeFont;
/**
* Notification manager.
*/
public class NotificationManager {
/** Duration, in milliseconds, to display bubble notifications. */
private static final int NOTIFICATION_TIME = 8000;
/** Duration, in milliseconds, to animate bubble notifications in/out. */
private static final int NOTIFICATION_ANIMATION_TIME = 450;
/** Duration, in milliseconds, to display bar notifications. */
private static final int BAR_NOTIFICATION_TIME = 1500;
/** Listener for notification clicks. */
public interface NotificationListener {
/** Fired when this notification is clicked. */
public void click();
}
/** Notification. */
private class BubbleNotification {
/** The listener. */
private final NotificationListener listener;
/** The notification string. */
private final String message;
/** The lines of text. */
private final List<String> lines;
/** The border color. */
private final Color borderColor, borderFocusColor;
/** The timer. */
private int time = 0;
/** The coordinates. */
private int x, y;
/** The dimensions. */
private final int width, height;
/** The font to use. */
private final UnicodeFont font = Fonts.SMALLBOLD;
/** Whether this notification has been clicked. */
private boolean clicked = false;
/**
* Creates a new bubble notification.
* @param s the notification string
* @param c the border color
* @param listener the listener
* @param width the element width
*/
public BubbleNotification(String s, Color c, NotificationListener listener, int width) {
this.message = s;
this.lines = Fonts.wrap(font, s, (int) (width * 0.96f), true);
this.borderColor = new Color(c);
this.borderFocusColor = (borderColor.equals(Color.white)) ?
new Color(Colors.GREEN) : new Color(Color.white);
this.listener = listener;
this.width = width;
this.height = (int) (font.getLineHeight() * (lines.size() + 0.5f));
}
/** Returns the x position. */
public int getX() { return x; }
/** Returns the y position. */
public int getY() { return y; }
/** Returns the width of this notification. */
public int getWidth() { return width; }
/** Returns the height of this notification. */
public int getHeight() { return height; }
/**
* Sets the position of this bubble.
* @param x the x coordinate
* @param y the y coordinate
*/
public void setPosition(int x, int y) {
this.x = x;
this.y = y;
}
/**
* Returns true if the coordinates are within the bubble bounds.
* @param cx the x coordinate
* @param cy the y coordinate
*/
public boolean contains(int cx, int cy) {
return ((cx > x && cx < x + width) && (cy > y && cy < y + height));
}
/**
* Draws the bubble notification.
* @param g the graphics context
* @param focus true if focused
*/
public void draw(Graphics g, boolean focus) {
// get animation values
float alpha = 1f;
int offset = 0;
if (time < NOTIFICATION_ANIMATION_TIME) {
float t = AnimationEquation.OUT_BACK.calc((float) time / NOTIFICATION_ANIMATION_TIME);
alpha = t;
offset = (int) ((1f - t) * (width / 2f));
} else if (NOTIFICATION_TIME - time < NOTIFICATION_ANIMATION_TIME) {
float t = (float) (NOTIFICATION_TIME - time) / NOTIFICATION_ANIMATION_TIME;
alpha = t;
}
float oldBlackAlpha = Colors.BLACK_ALPHA.a;
float oldWhiteAlpha = Colors.WHITE_FADE.a;
Colors.BLACK_ALPHA.a = alpha;
Colors.WHITE_FADE.a = alpha;
Color border = focus ? borderFocusColor : borderColor;
border.a = alpha;
// draw rectangle
int lineHeight = font.getLineHeight();
int cornerRadius = 6;
g.setColor(Colors.BLACK_ALPHA);
g.fillRoundRect(x + offset, y, width, height, cornerRadius);
g.setLineWidth(1f);
g.setColor(border);
g.drawRoundRect(x + offset, y, width, height, cornerRadius);
// draw text
Fonts.loadGlyphs(font, message);
int cx = x + (int) (width * 0.02f) + offset, cy = y + lineHeight / 4;
for (String s : lines) {
font.drawString(cx, cy, s, Colors.WHITE_FADE);
cy += lineHeight;
}
Colors.BLACK_ALPHA.a = oldBlackAlpha;
Colors.WHITE_FADE.a = oldWhiteAlpha;
}
/**
* Updates the bubble notification by a delta interval.
* @param delta the delta interval since the last call.
* @return true if an update was applied, false if the timer is finished
*/
public boolean update(int delta) {
if (isFinished())
return false;
time = Math.min(time + delta, NOTIFICATION_TIME);
return true;
}
/** Returns whether this notification is finished being displayed. */
public boolean isFinished() { return time >= NOTIFICATION_TIME; }
/** Returns whether this notification has finished animating in. */
public boolean isStartAnimationFinished() { return time >= NOTIFICATION_ANIMATION_TIME; }
/**
* Click handler.
* @param doAction whether to fire the listener
*/
public synchronized void click(boolean doAction) {
if (isFinished() || clicked)
return;
clicked = true;
time = Math.max(time, NOTIFICATION_TIME - NOTIFICATION_ANIMATION_TIME);
if (listener != null && doAction)
listener.click();
}
}
/** All bubble notifications. */
private List<BubbleNotification> notifications;
/** The current bar notification string. */
private String barNotif;
/** The current bar notification timer. */
private int barNotifTimer = -1;
// game-related variables
private final GameContainer container;
private final Input input;
/**
* Constructor.
* @param container the game container
*/
public NotificationManager(GameContainer container) {
this.container = container;
this.input = container.getInput();
this.notifications = Collections.synchronizedList(new ArrayList<BubbleNotification>());
input.addMouseListener(new MouseListener() {
@Override
public void mousePressed(int button, int x, int y) {
if (button == Input.MOUSE_MIDDLE_BUTTON)
return;
synchronized (notifications) {
for (BubbleNotification n : notifications) {
if (n.contains(x, y)) {
n.click(button == Input.MOUSE_LEFT_BUTTON);
break;
}
}
}
}
@Override public void setInput(Input input) {}
@Override public boolean isAcceptingInput() { return true; }
@Override public void inputEnded() {}
@Override public void inputStarted() {}
@Override public void mouseWheelMoved(int change) {}
@Override public void mouseClicked(int button, int x, int y, int clickCount) {}
@Override public void mouseReleased(int button, int x, int y) {}
@Override public void mouseMoved(int oldx, int oldy, int newx, int newy) {}
@Override public void mouseDragged(int oldx, int oldy, int newx, int newy) {}
});
}
/**
* Draws all notifications.
* @param g the graphics context
*/
public void draw(Graphics g) {
drawNotifications(g);
drawBarNotification(g);
}
/**
* Draws the notifications sent from {@link #sendNotification(String, Color)}.
* @param g the graphics context
*/
private void drawNotifications(Graphics g) {
int mouseX = input.getMouseX(), mouseY = input.getMouseY();
synchronized (notifications) {
for (BubbleNotification n : notifications) {
if (!n.isFinished())
n.draw(g, n.contains(mouseX, mouseY));
}
}
}
/**
* Draws the notification sent from {@link #sendBarNotification(String)}.
* @param g the graphics context
*/
private void drawBarNotification(Graphics g) {
if (barNotifTimer <= 0 || barNotifTimer >= BAR_NOTIFICATION_TIME)
return;
float alpha = 1f;
if (barNotifTimer >= BAR_NOTIFICATION_TIME * 0.9f)
alpha -= 1 - ((BAR_NOTIFICATION_TIME - barNotifTimer) / (BAR_NOTIFICATION_TIME * 0.1f));
int midX = container.getWidth() / 2, midY = container.getHeight() / 2;
float barHeight = Fonts.LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
float oldAlphaB = Colors.BLACK_ALPHA.a, oldAlphaW = Colors.WHITE_ALPHA.a;
Colors.BLACK_ALPHA.a *= alpha;
Colors.WHITE_ALPHA.a = alpha;
g.setColor(Colors.BLACK_ALPHA);
g.fillRect(0, midY - barHeight / 2f, container.getWidth(), barHeight);
Fonts.LARGE.drawString(
midX - Fonts.LARGE.getWidth(barNotif) / 2f,
midY - Fonts.LARGE.getLineHeight() / 2f,
barNotif, Colors.WHITE_ALPHA
);
Colors.BLACK_ALPHA.a = oldAlphaB;
Colors.WHITE_ALPHA.a = oldAlphaW;
}
/**
* Updates all notifications by a delta interval.
* @param delta the delta interval since the last call.
*/
public void update(int delta) {
// update notifications
boolean allFinished = true, startFinished = true;
synchronized (notifications) {
for (BubbleNotification n : notifications) {
if (startFinished) {
if (n.update(delta))
allFinished = false;
} else if (!n.isFinished())
allFinished = false;
startFinished = n.isStartAnimationFinished();
}
if (allFinished)
notifications.clear(); // clear when all are finished showing
}
// update bar notification
if (barNotifTimer > -1 && barNotifTimer < BAR_NOTIFICATION_TIME) {
barNotifTimer += delta;
if (barNotifTimer > BAR_NOTIFICATION_TIME)
barNotifTimer = BAR_NOTIFICATION_TIME;
}
}
/**
* Submits a bubble notification for drawing.
* @param s the notification string
*/
public void sendNotification(String s) { sendNotification(s, Color.white); }
/**
* Submits a bubble notification for drawing.
* @param s the notification string
* @param c the border color
*/
public void sendNotification(String s, Color c) { sendNotification(s, c, null); }
/**
* Submits a bubble notification for drawing.
* @param s the notification string
* @param c the border color
* @param listener the listener
*/
public synchronized void sendNotification(String s, Color c, NotificationListener listener) {
BubbleNotification notif = new BubbleNotification(s, c, listener, container.getWidth() / 5);
int x, y;
int bottomY = (int) (container.getHeight() * 0.9645f);
int paddingX = 6;
int paddingY = (int) (container.getHeight() * 0.0144f);
if (notifications.isEmpty()) {
x = container.getWidth() - paddingX - notif.getWidth();
y = bottomY - notif.getHeight();
} else {
BubbleNotification n = notifications.get(notifications.size() - 1);
x = n.getX();
y = n.getY() - paddingY - notif.getHeight();
if (y <= paddingY) {
x -= paddingX + notif.getWidth();
y = bottomY - notif.getHeight();
}
}
notif.setPosition(x, y);
notifications.add(notif);
}
/**
* Submits a bar notification for drawing.
* @param s the notification string
*/
public void sendBarNotification(String s) {
if (s != null) {
barNotif = s;
barNotifTimer = 0;
}
}
/**
* Resets all notifications.
*/
public void reset() {
// resets the bar notification
barNotifTimer = -1;
barNotif = null;
}
}