diff --git a/src/itdelatrisu/opsu/ScoreData.java b/src/itdelatrisu/opsu/ScoreData.java index 881a6fe2..d89eb40a 100644 --- a/src/itdelatrisu/opsu/ScoreData.java +++ b/src/itdelatrisu/opsu/ScoreData.java @@ -82,7 +82,7 @@ public class ScoreData implements Comparable { private String tooltip; /** Drawing values. */ - private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset; + private static float baseX, baseY, buttonWidth, buttonHeight, buttonOffset, buttonAreaHeight; /** Button background colors. */ private static final Color @@ -101,8 +101,16 @@ public class ScoreData implements Comparable { float gradeHeight = GameImage.MENU_BUTTON_BG.getImage().getHeight() * 0.45f; buttonHeight = Math.max(gradeHeight, Utils.FONT_DEFAULT.getLineHeight() * 3.03f); buttonOffset = buttonHeight + gradeHeight / 10f; + buttonAreaHeight = (SongMenu.MAX_SCORE_BUTTONS - 1) * buttonOffset + buttonHeight; } + /** + * Returns the Buttons Offset + */ + public static float getButtonOffset() { + return buttonOffset; + } + /** * Returns true if the coordinates are within the bounds of the * button at the given index. @@ -133,9 +141,17 @@ public class ScoreData implements Comparable { * @param index the start button index * @param total the total number of buttons */ - public static void drawScrollbar(Graphics g, int index, int total) { - UI.drawScrollbar(g, index, total, SongMenu.MAX_SCORE_BUTTONS, 0, baseY, - 0, buttonHeight, buttonOffset, null, Color.white, false); + public static void drawScrollbar(Graphics g, float pos, float total) { + UI.drawScrollbar(g, pos, total, SongMenu.MAX_SCORE_BUTTONS * buttonOffset, 0, baseY, + 0, buttonAreaHeight, null, Color.white, false); + } + + /** + * Sets a clip to the area. + * @param g the graphics context + */ + public static void clipToDownloadArea(Graphics g) { + g.setClip((int) baseX, (int) baseY, (int) buttonWidth, (int) (buttonAreaHeight)); } /** @@ -229,11 +245,11 @@ public class ScoreData implements Comparable { * @param prevScore the previous (lower) score, or -1 if none * @param focus whether the button is focused */ - public void draw(Graphics g, int index, int rank, long prevScore, boolean focus) { + public void draw(Graphics g, float position, int rank, long prevScore, boolean focus) { Image img = getGrade().getMenuImage(); float textX = baseX + buttonWidth * 0.24f; float edgeX = baseX + buttonWidth * 0.98f; - float y = baseY + index * (buttonOffset); + float y = baseY + position; float midY = y + buttonHeight / 2f; float marginY = Utils.FONT_DEFAULT.getLineHeight() * 0.01f; diff --git a/src/itdelatrisu/opsu/downloads/DownloadNode.java b/src/itdelatrisu/opsu/downloads/DownloadNode.java index 36412ae3..9632462a 100644 --- a/src/itdelatrisu/opsu/downloads/DownloadNode.java +++ b/src/itdelatrisu/opsu/downloads/DownloadNode.java @@ -157,6 +157,14 @@ public class DownloadNode { public static void clipToResultArea(Graphics g) { g.setClip((int) buttonBaseX, (int) buttonBaseY, (int) buttonWidth, (int) (buttonOffset * maxResultsShown)); } + + /** + * Sets a clip to the download area. + * @param g the graphics context + */ + public static void clipToDownloadArea(Graphics g) { + g.setClip((int) infoBaseX, (int) infoBaseY, (int) infoWidth, (int) (infoHeight * maxDownloadsShown)); + } /** * Returns true if the coordinates are within the bounds of the @@ -187,6 +195,21 @@ public class DownloadNode { (cy > y + marginY && cy < y + marginY + iconWidth)); } + /** + * Returns the button(Results) offset. + * @return the button offset + */ + public static float getButtonOffset(){ + return buttonOffset; + } + + /** + * Returns the info(Download) height. + * @return the infoHeight + */ + public static float getInfoHeight(){ + return infoHeight; + } /** * Returns true if the coordinates are within the bounds of the * download information button area. @@ -201,12 +224,14 @@ public class DownloadNode { /** * Draws the scroll bar for the download result buttons. * @param g the graphics context - * @param index the start button index + * @param position the start button index * @param total the total number of buttons */ - public static void drawResultScrollbar(Graphics g, int index, int total) { - UI.drawScrollbar(g, index, total, maxResultsShown, buttonBaseX, buttonBaseY, - buttonWidth * 1.01f, buttonHeight, buttonOffset, BG_NORMAL, Color.white, true); + public static void drawResultScrollbar(Graphics g, float position, float total) { + UI.drawScrollbar(g, position, total, maxResultsShown * buttonOffset, + buttonBaseX, buttonBaseY, + buttonWidth * 1.01f, (maxResultsShown-1) * buttonOffset + buttonHeight, + BG_NORMAL, Color.white, true); } /** @@ -215,9 +240,9 @@ public class DownloadNode { * @param index the start index * @param total the total number of downloads */ - public static void drawDownloadScrollbar(Graphics g, int index, int total) { - UI.drawScrollbar(g, index, total, maxDownloadsShown, infoBaseX, infoBaseY, - infoWidth, infoHeight, infoHeight, BG_NORMAL, Color.white, true); + public static void drawDownloadScrollbar(Graphics g, float index, float total) { + UI.drawScrollbar(g, index, total, maxDownloadsShown * infoHeight, infoBaseX, infoBaseY, + infoWidth, maxDownloadsShown * infoHeight, BG_NORMAL, Color.white, true); } /** @@ -310,15 +335,15 @@ public class DownloadNode { /** * Draws the download result as a rectangular button. * @param g the graphics context - * @param index the index (to offset the button from the topmost button) + * @param position the index (to offset the button from the topmost button) * @param hover true if the mouse is hovering over this button * @param focus true if the button is focused * @param previewing true if the beatmap is currently being previewed */ - public void drawResult(Graphics g, int index, boolean hover, boolean focus, boolean previewing) { + public void drawResult(Graphics g, float position, boolean hover, boolean focus, boolean previewing) { float textX = buttonBaseX + buttonWidth * 0.001f; float edgeX = buttonBaseX + buttonWidth * 0.985f; - float y = buttonBaseY + index * buttonOffset; + float y = buttonBaseY + position; float marginY = buttonHeight * 0.04f; Download dl = DownloadList.get().getDownload(beatmapSetID); @@ -350,12 +375,14 @@ public class DownloadNode { // TODO: if the title/artist line is too long, shorten it (e.g. add "...") instead of just clipping if (Options.useUnicodeMetadata()) // load glyphs Utils.loadGlyphs(Utils.FONT_BOLD, getTitle(), getArtist()); - g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Utils.FONT_DEFAULT.getWidth(creator)), Utils.FONT_BOLD.getLineHeight()); + + // TODO can't set clip again or else old clip will be cleared + //g.setClip((int) textX, (int) (y + marginY), (int) (edgeX - textX - Utils.FONT_DEFAULT.getWidth(creator)), Utils.FONT_BOLD.getLineHeight()); Utils.FONT_BOLD.drawString( textX, y + marginY, String.format("%s - %s%s", getArtist(), getTitle(), (dl != null) ? String.format(" [%s]", dl.getStatus().getName()) : ""), Color.white); - g.clearClip(); + //g.clearClip(); Utils.FONT_DEFAULT.drawString( textX, y + marginY + Utils.FONT_BOLD.getLineHeight(), String.format("Last updated: %s", date), Color.white); @@ -367,11 +394,11 @@ public class DownloadNode { /** * Draws the download information. * @param g the graphics context - * @param index the index (to offset from the topmost position) + * @param position the index (to offset from the topmost position) * @param id the list index * @param hover true if the mouse is hovering over this button */ - public void drawDownload(Graphics g, int index, int id, boolean hover) { + public void drawDownload(Graphics g, float position, int id, boolean hover) { if (download == null) { ErrorHandler.error("Trying to draw download information for button without Download object.", null, false); return; @@ -379,7 +406,7 @@ public class DownloadNode { float textX = infoBaseX + infoWidth * 0.02f; float edgeX = infoBaseX + infoWidth * 0.985f; - float y = infoBaseY + index * infoHeight; + float y = infoBaseY + position; float marginY = infoHeight * 0.04f; // rectangle outline diff --git a/src/itdelatrisu/opsu/states/DownloadsMenu.java b/src/itdelatrisu/opsu/states/DownloadsMenu.java index 7ade156f..7b80bf4e 100644 --- a/src/itdelatrisu/opsu/states/DownloadsMenu.java +++ b/src/itdelatrisu/opsu/states/DownloadsMenu.java @@ -36,6 +36,7 @@ import itdelatrisu.opsu.downloads.servers.BloodcatServer; import itdelatrisu.opsu.downloads.servers.DownloadServer; import itdelatrisu.opsu.downloads.servers.HexideServer; import itdelatrisu.opsu.downloads.servers.OsuMirrorServer; +import itdelatrisu.opsu.ui.KinecticScrolling; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; @@ -89,7 +90,7 @@ public class DownloadsMenu extends BasicGameState { private int focusTimer = 0; /** Current start result button (topmost entry). */ - private int startResult = 0; + KinecticScrolling startResultPos = new KinecticScrolling(); /** Total number of results for current query. */ private int totalResults = 0; @@ -110,7 +111,7 @@ public class DownloadsMenu extends BasicGameState { private boolean rankedOnly = true; /** Current start download index. */ - private int startDownloadIndex = 0; + KinecticScrolling startDownloadIndexPos = new KinecticScrolling(); /** Query thread. */ private Thread queryThread; @@ -272,18 +273,24 @@ public class DownloadsMenu extends BasicGameState { if (nodes != null) { DownloadNode.clipToResultArea(g); int maxResultsShown = DownloadNode.maxResultsShown(); - for (int i = 0; i < maxResultsShown; i++) { + int startResult = (int) (startResultPos.getPosition() / DownloadNode.getButtonOffset()); + int offset = (int) (-startResultPos.getPosition() + startResult * DownloadNode.getButtonOffset()); + + for (int i = 0; i < maxResultsShown + 1; i++) { int index = startResult + i; + if(index < 0) + continue; if (index >= nodes.length) break; - nodes[index].drawResult(g, i, DownloadNode.resultContains(mouseX, mouseY, i), + nodes[index].drawResult(g, offset + i * DownloadNode.getButtonOffset(), + DownloadNode.resultContains(mouseX, mouseY - offset, i), (index == focusResult), (previewID == nodes[index].getID())); } g.clearClip(); // scroll bar if (nodes.length > maxResultsShown) - DownloadNode.drawResultScrollbar(g, startResult, nodes.length); + DownloadNode.drawResultScrollbar(g, startResultPos.getPosition(), nodes.length * DownloadNode.getButtonOffset()); // pages if (nodes.length > 0) { @@ -311,19 +318,26 @@ public class DownloadsMenu extends BasicGameState { int downloadsSize = DownloadList.get().size(); if (downloadsSize > 0) { int maxDownloadsShown = DownloadNode.maxDownloadsShown(); - for (int i = 0; i < maxDownloadsShown; i++) { - int index = startDownloadIndex + i; - if (index >= downloadsSize) - break; - DownloadNode node = DownloadList.get().getNode(index); - if (node == null) - break; - node.drawDownload(g, i, index, DownloadNode.downloadContains(mouseX, mouseY, i)); - } + int startDownloadIndex = (int) (startDownloadIndexPos.getPosition() / DownloadNode.getInfoHeight()); + int offset = (int) (-startDownloadIndexPos.getPosition() + startDownloadIndex * DownloadNode.getInfoHeight()); + + DownloadNode.clipToDownloadArea(g); + for (int i = 0; i < maxDownloadsShown + 1; i++) { + int index = startDownloadIndex + i; + if (index >= downloadsSize) + break; + DownloadNode node = DownloadList.get().getNode(index); + if (node == null) + break; + node.drawDownload(g, i * DownloadNode.getInfoHeight() + offset, index, + DownloadNode.downloadContains(mouseX, mouseY - offset, i)); + } + g.clearClip(); + // scroll bar if (downloadsSize > maxDownloadsShown) - DownloadNode.drawDownloadScrollbar(g, startDownloadIndex, downloadsSize); + DownloadNode.drawDownloadScrollbar(g, startDownloadIndexPos.getPosition(), downloadsSize * DownloadNode.getInfoHeight()); } // buttons @@ -367,6 +381,13 @@ public class DownloadsMenu extends BasicGameState { rankedButton.hoverUpdate(delta, mouseX, mouseY); serverButton.hoverUpdate(delta, mouseX, mouseY); + if (DownloadList.get() != null) + startDownloadIndexPos.setMinMax(0, DownloadNode.getInfoHeight() * (DownloadList.get().size() - DownloadNode.maxDownloadsShown())); + startDownloadIndexPos.update(delta); + if (resultList != null) + startResultPos.setMinMax(0, DownloadNode.getButtonOffset() * (resultList.length - DownloadNode.maxResultsShown())); + startResultPos.update(delta); + // focus timer if (focusResult != -1 && focusTimer < FOCUS_DELAY) focusTimer += delta; @@ -423,7 +444,7 @@ public class DownloadsMenu extends BasicGameState { resultList = nodes; totalResults = server.totalResults(); focusResult = -1; - startResult = 0; + startResultPos.setPosition(0); if (nodes == null) searchResultString = "An error has occurred."; else { @@ -482,19 +503,23 @@ public class DownloadsMenu extends BasicGameState { DownloadNode[] nodes = resultList; if (nodes != null) { if (DownloadNode.resultAreaContains(x, y)) { + startResultPos.pressed(); int maxResultsShown = DownloadNode.maxResultsShown(); - for (int i = 0; i < maxResultsShown; i++) { + for (int i = 0; i < maxResultsShown + 1; i++) { + int startResult = (int) (startResultPos.getPosition() / DownloadNode.getButtonOffset()); + int offset = (int) (-startResultPos.getPosition() + startResult * DownloadNode.getButtonOffset()); + int index = startResult + i; if (index >= nodes.length) break; - if (DownloadNode.resultContains(x, y, i)) { + if (DownloadNode.resultContains(x, y - offset, i)) { final DownloadNode node = nodes[index]; // check if map is already loaded boolean isLoaded = BeatmapSetList.get().containsBeatmapSetID(node.getID()); // track preview - if (DownloadNode.resultIconContains(x, y, i)) { + if (DownloadNode.resultIconContains(x, y - offset, i)) { // set focus if (!isLoaded) { SoundController.playSound(SoundEffect.MENUCLICK); @@ -659,7 +684,7 @@ public class DownloadsMenu extends BasicGameState { if (serverButton.contains(x, y)) { SoundController.playSound(SoundEffect.MENUCLICK); resultList = null; - startResult = 0; + startResultPos.setPosition(0); focusResult = -1; totalResults = 0; page = 0; @@ -675,12 +700,15 @@ public class DownloadsMenu extends BasicGameState { // downloads if (!DownloadList.get().isEmpty() && DownloadNode.downloadAreaContains(x, y)) { + startDownloadIndexPos.pressed(); int maxDownloadsShown = DownloadNode.maxDownloadsShown(); - for (int i = 0, n = DownloadList.get().size(); i < maxDownloadsShown; i++) { + int startDownloadIndex = (int) (startDownloadIndexPos.getPosition() / DownloadNode.getInfoHeight()); + int offset = (int) (-startDownloadIndexPos.getPosition() + startDownloadIndex * DownloadNode.getInfoHeight()); + for (int i = 0, n = DownloadList.get().size(); i < maxDownloadsShown + 1; i++) { int index = startDownloadIndex + i; if (index >= n) break; - if (DownloadNode.downloadIconContains(x, y, i)) { + if (DownloadNode.downloadIconContains(x, y - offset, i)) { SoundController.playSound(SoundEffect.MENUCLICK); DownloadNode node = DownloadList.get().getNode(index); if (node == null) @@ -704,6 +732,12 @@ public class DownloadsMenu extends BasicGameState { } } + @Override + public void mouseReleased(int button, int x, int y) { + startDownloadIndexPos.release(); + startResultPos.release(); + } + @Override public void mouseWheelMoved(int newValue) { // change volume @@ -735,8 +769,8 @@ public class DownloadsMenu extends BasicGameState { int diff = newy - oldy; if (diff == 0) return; - int shift = (diff < 0) ? 1 : -1; - scrollLists(oldx, oldy, shift); + startDownloadIndexPos.dragged(-diff); + startResultPos.dragged(-diff); } @Override @@ -805,8 +839,8 @@ public class DownloadsMenu extends BasicGameState { rankedButton.resetHover(); serverButton.resetHover(); focusResult = -1; - startResult = 0; - startDownloadIndex = 0; + startResultPos.setPosition(0); + startDownloadIndexPos.setPosition(0); pageDir = Page.RESET; previewID = -1; if (barNotificationOnLoad != null) { @@ -846,21 +880,12 @@ public class DownloadsMenu extends BasicGameState { private void scrollLists(int cx, int cy, int shift) { // search results if (DownloadNode.resultAreaContains(cx, cy)) { - DownloadNode[] nodes = resultList; - if (nodes != null && nodes.length >= DownloadNode.maxResultsShown()) { - int newStartResult = startResult + shift; - if (newStartResult >= 0 && newStartResult + DownloadNode.maxResultsShown() <= nodes.length) - startResult = newStartResult; - } + startResultPos.scrollOffset(shift * DownloadNode.getButtonOffset()); } // downloads else if (DownloadNode.downloadAreaContains(cx, cy)) { - if (DownloadList.get().size() >= DownloadNode.maxDownloadsShown()) { - int newStartDownloadIndex = startDownloadIndex + shift; - if (newStartDownloadIndex >= 0 && newStartDownloadIndex + DownloadNode.maxDownloadsShown() <= DownloadList.get().size()) - startDownloadIndex = newStartDownloadIndex; - } + startDownloadIndexPos.scrollOffset(shift * DownloadNode.getInfoHeight()); } } diff --git a/src/itdelatrisu/opsu/states/SongMenu.java b/src/itdelatrisu/opsu/states/SongMenu.java index d191d125..2db90587 100644 --- a/src/itdelatrisu/opsu/states/SongMenu.java +++ b/src/itdelatrisu/opsu/states/SongMenu.java @@ -39,6 +39,7 @@ import itdelatrisu.opsu.beatmap.BeatmapSortOrder; import itdelatrisu.opsu.db.BeatmapDB; import itdelatrisu.opsu.db.ScoreDB; import itdelatrisu.opsu.states.ButtonMenu.MenuState; +import itdelatrisu.opsu.ui.KinecticScrolling; import itdelatrisu.opsu.ui.MenuButton; import itdelatrisu.opsu.ui.UI; @@ -123,6 +124,12 @@ public class SongMenu extends BasicGameState { /** Current start node (topmost menu entry). */ private BeatmapSetNode startNode; + /** The first node is about this high above the header. */ + KinecticScrolling songScrolling = new KinecticScrolling(); + + /** The number of Nodes to offset from the top to the startNode. */ + private int startNodeOffset; + /** Current focused (selected) node. */ private BeatmapSetNode focusNode; @@ -142,7 +149,7 @@ public class SongMenu extends BasicGameState { private float hoverOffset = 0f; /** Current index of hovered song button. */ - private int hoverIndex = -1; + private BeatmapSetNode hoverIndex = null; /** The selection buttons. */ private MenuButton selectModsButton, selectRandomButton, selectMapOptionsButton, selectOptionsButton; @@ -190,8 +197,9 @@ public class SongMenu extends BasicGameState { private ScoreData[] focusScores; /** Current start score (topmost score entry). */ - private int startScore = 0; + KinecticScrolling startScorePos = new KinecticScrolling(); + /** Header and footer end and start y coordinates, respectively. */ private float headerY, footerY; @@ -239,7 +247,7 @@ public class SongMenu extends BasicGameState { // song button coordinates buttonX = width * 0.6f; - buttonY = headerY; + //buttonY = headerY; buttonWidth = menuBackground.getWidth(); buttonHeight = menuBackground.getHeight(); buttonOffset = (footerY - headerY - DIVIDER_LINE_WIDTH) / MAX_SONG_BUTTONS; @@ -299,21 +307,61 @@ public class SongMenu extends BasicGameState { // song buttons BeatmapSetNode node = startNode; - int songButtonIndex = 0; - if (node != null && node.prev != null) { - node = node.prev; - songButtonIndex = -1; - } g.setClip(0, (int) (headerY + DIVIDER_LINE_WIDTH / 2), width, (int) (footerY - headerY)); - for (int i = songButtonIndex; i <= MAX_SONG_BUTTONS && node != null; i++, node = node.next) { + for (int i = startNodeOffset; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) { // draw the node - float offset = (i == hoverIndex) ? hoverOffset : 0f; + float offset = (node == hoverIndex) ? hoverOffset : 0f; + float ypos = buttonY + (i*buttonOffset) ; + float mid = height/2 - ypos - buttonOffset/2; + final float circleRadi = 1000 * GameImage.getUIscale(); + //finds points along a very large circle + // x^2 = h^2 - y^2 + float t = circleRadi*circleRadi - (mid*mid); + float xpos = (float)(t>0?Math.sqrt(t):0) - circleRadi + 50 * GameImage.getUIscale(); ScoreData[] scores = getScoreDataForNode(node, false); - node.draw(buttonX - offset, buttonY + (i*buttonOffset) + DIVIDER_LINE_WIDTH / 2, + node.draw(buttonX - offset - xpos, ypos, (scores == null) ? Grade.NULL : scores[0].getGrade(), (node == focusNode)); } g.clearClip(); + + // scroll bar + if (focusNode != null && startNode != null) { + int focusNodes = focusNode.getBeatmapSet().size(); + int totalNodes = BeatmapSetList.get().size() + focusNodes - 1; + if (totalNodes > MAX_SONG_BUTTONS) { + UI.drawScrollbar(g, + songScrolling.getPosition(), + totalNodes * buttonOffset, + MAX_SONG_BUTTONS * buttonOffset, + width, headerY + DIVIDER_LINE_WIDTH / 2, + 0, MAX_SONG_BUTTONS * buttonOffset, + Utils.COLOR_BLACK_ALPHA, Color.white, true); + } + } + + // score buttons + if (focusScores != null) { + ScoreData.clipToDownloadArea(g); + int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset()); + int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset()); + for (int i = 0; i < MAX_SCORE_BUTTONS + 1; i++) { + int rank = startScore + i; + if (rank < 0) + continue; + if (rank >= focusScores.length) + break; + long prevScore = (rank + 1 < focusScores.length) ? focusScores[rank + 1].score : -1; + focusScores[rank].draw(g, offset + i*ScoreData.getButtonOffset(), rank, prevScore, ScoreData.buttonContains(mouseX, mouseY-offset, i)); + } + g.clearClip(); + + // scroll bar + if (focusScores.length > MAX_SCORE_BUTTONS && ScoreData.areaContains(mouseX, mouseY)) + ScoreData.drawScrollbar(g, startScorePos.getPosition() , focusScores.length * ScoreData.getButtonOffset()); + } + + // top/bottom bars g.setColor(Utils.COLOR_BLACK_ALPHA); g.fillRect(0, 0, width, headerY); @@ -362,21 +410,6 @@ public class SongMenu extends BasicGameState { Utils.FONT_SMALL.drawString(marginX, headerTextY, songInfo[4], color4); } - // score buttons - if (focusScores != null) { - for (int i = 0; i < MAX_SCORE_BUTTONS; i++) { - int rank = startScore + i; - if (rank >= focusScores.length) - break; - long prevScore = (rank + 1 < focusScores.length) ? focusScores[rank + 1].score : -1; - focusScores[rank].draw(g, i, rank, prevScore, ScoreData.buttonContains(mouseX, mouseY, i)); - } - - // scroll bar - if (focusScores.length > MAX_SCORE_BUTTONS && ScoreData.areaContains(mouseX, mouseY)) - ScoreData.drawScrollbar(g, startScore, focusScores.length); - } - // selection buttons GameImage.SELECTION_MODS.getImage().drawCentered(selectModsButton.getX(), selectModsButton.getY()); selectModsButton.draw(); @@ -435,22 +468,6 @@ public class SongMenu extends BasicGameState { (searchResultString == null) ? "Searching..." : searchResultString, Color.white); } - // scroll bar - if (focusNode != null) { - int focusNodes = focusNode.getBeatmapSet().size(); - int totalNodes = BeatmapSetList.get().size() + focusNodes - 1; - if (totalNodes > MAX_SONG_BUTTONS) { - int startIndex = startNode.index; - if (startNode.index > focusNode.index) - startIndex += focusNodes; - else if (startNode.index == focusNode.index) - startIndex += startNode.beatmapIndex; - UI.drawScrollbar(g, startIndex, totalNodes, MAX_SONG_BUTTONS, - width, headerY + DIVIDER_LINE_WIDTH / 2, 0, buttonOffset - DIVIDER_LINE_WIDTH * 1.5f, buttonOffset, - Utils.COLOR_BLACK_ALPHA, Color.white, true); - } - } - // reloading beatmaps if (reloadThread != null) { // darken the screen @@ -540,34 +557,30 @@ public class SongMenu extends BasicGameState { searchTransitionTimer = SEARCH_TRANSITION_TIME; } - // slide buttons - int height = container.getHeight(); - if (buttonY > headerY) { - buttonY -= height * delta / 20000f; - if (buttonY < headerY) - buttonY = headerY; - } else if (buttonY < headerY) { - buttonY += height * delta / 20000f; - if (buttonY > headerY) - buttonY = headerY; + // Scores + if (focusScores != null) { + startScorePos.setMinMax(0, (focusScores.length - MAX_SCORE_BUTTONS) * ScoreData.getButtonOffset()); + startScorePos.update(delta); } // mouse hover + songScrolling.update(delta); + updateDrawnSongPos(); boolean isHover = false; if (mouseY > headerY && mouseY < footerY) { BeatmapSetNode node = startNode; - for (int i = 0; i < MAX_SONG_BUTTONS && node != null; i++, node = node.next) { + for (int i = 0; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) { float cx = (node.index == BeatmapSetList.get().getExpandedIndex()) ? buttonX * 0.9f : buttonX; if ((mouseX > cx && mouseX < cx + buttonWidth) && (mouseY > buttonY + (i * buttonOffset) && mouseY < buttonY + (i * buttonOffset) + buttonHeight)) { - if (i == hoverIndex) { + if (node == hoverIndex) { if (hoverOffset < MAX_HOVER_OFFSET) { hoverOffset += delta / 3f; if (hoverOffset > MAX_HOVER_OFFSET) hoverOffset = MAX_HOVER_OFFSET; } } else { - hoverIndex = i; + hoverIndex = node ; hoverOffset = 0f; } isHover = true; @@ -577,17 +590,21 @@ public class SongMenu extends BasicGameState { } if (!isHover) { hoverOffset = 0f; - hoverIndex = -1; + hoverIndex = null; } else return; // tooltips if (focusScores != null) { + int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset()); + int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset()); for (int i = 0; i < MAX_SCORE_BUTTONS; i++) { int rank = startScore + i; + if (rank < 0) + continue; if (rank >= focusScores.length) break; - if (ScoreData.buttonContains(mouseX, mouseY, i)) { + if (ScoreData.buttonContains(mouseX, mouseY - offset, i)) { UI.updateTooltip(delta, focusScores[rank].getTooltipString(), true); break; } @@ -600,6 +617,18 @@ public class SongMenu extends BasicGameState { @Override public void mousePressed(int button, int x, int y) { + songScrolling.pressed(); + startScorePos.pressed(); + } + + @Override + public void mouseReleased(int button, int x, int y) { + songScrolling.release(); + startScorePos.release(); + } + + @Override + public void mouseClicked(int button, int x, int y, int clickCount) { // check mouse button if (button == Input.MOUSE_MIDDLE_BUTTON) return; @@ -655,13 +684,13 @@ public class SongMenu extends BasicGameState { if (y > headerY && y < footerY) { int expandedIndex = BeatmapSetList.get().getExpandedIndex(); BeatmapSetNode node = startNode; - for (int i = 0; i < MAX_SONG_BUTTONS && node != null; i++, node = node.next) { + for (int i = startNodeOffset; i < MAX_SONG_BUTTONS + 1 && node != null; i++, node = node.next) { // is button at this index clicked? float cx = (node.index == expandedIndex) ? buttonX * 0.9f : buttonX; if ((x > cx && x < cx + buttonWidth) && (y > buttonY + (i * buttonOffset) && y < buttonY + (i * buttonOffset) + buttonHeight)) { float oldHoverOffset = hoverOffset; - int oldHoverIndex = hoverIndex; + BeatmapSetNode oldHoverIndex = hoverIndex; // clicked node is already expanded if (node.index == expandedIndex) { @@ -699,11 +728,13 @@ public class SongMenu extends BasicGameState { // score buttons if (focusScores != null && ScoreData.areaContains(x, y)) { - for (int i = 0; i < MAX_SCORE_BUTTONS; i++) { + for (int i = 0; i < MAX_SCORE_BUTTONS + 1; i++) { + int startScore = (int) (startScorePos.getPosition() / ScoreData.getButtonOffset()); + int offset = (int) (-startScorePos.getPosition() + startScore * ScoreData.getButtonOffset()); int rank = startScore + i; if (rank >= focusScores.length) break; - if (ScoreData.buttonContains(x, y, i)) { + if (ScoreData.buttonContains(x, y - offset, i)) { SoundController.playSound(SoundEffect.MENUHIT); if (button != Input.MOUSE_RIGHT_BUTTON) { // view score @@ -821,7 +852,7 @@ public class SongMenu extends BasicGameState { SoundController.playSound(SoundEffect.MENUCLICK); BeatmapSetNode oldStartNode = startNode; float oldHoverOffset = hoverOffset; - int oldHoverIndex = hoverIndex; + BeatmapSetNode oldHoverIndex = hoverIndex; setFocus(next, 0, false, true); if (startNode == oldStartNode) { hoverOffset = oldHoverOffset; @@ -837,7 +868,7 @@ public class SongMenu extends BasicGameState { SoundController.playSound(SoundEffect.MENUCLICK); BeatmapSetNode oldStartNode = startNode; float oldHoverOffset = hoverOffset; - int oldHoverIndex = hoverIndex; + BeatmapSetNode oldHoverIndex = hoverIndex; setFocus(prev, (prev.index == focusNode.index) ? 0 : prev.getBeatmapSet().size() - 1, false, true); if (startNode == oldStartNode) { hoverOffset = oldHoverOffset; @@ -879,12 +910,11 @@ public class SongMenu extends BasicGameState { int diff = newy - oldy; if (diff == 0) return; - int shift = (diff < 0) ? 1 : -1; // check mouse button (right click scrolls faster on songs) int multiplier; if (input.isMouseButtonDown(Input.MOUSE_RIGHT_BUTTON)) - multiplier = 4; + multiplier = 10; else if (input.isMouseButtonDown(Input.MOUSE_LEFT_BUTTON)) multiplier = 1; else @@ -892,14 +922,13 @@ public class SongMenu extends BasicGameState { // score buttons if (focusScores != null && focusScores.length >= MAX_SCORE_BUTTONS && ScoreData.areaContains(oldx, oldy)) { - int newStartScore = startScore + shift; - if (newStartScore >= 0 && newStartScore + MAX_SCORE_BUTTONS <= focusScores.length) - startScore = newStartScore; + startScorePos.dragged(-diff * multiplier); } // song buttons else - changeIndex(shift * multiplier); + songScrolling.dragged(-diff * multiplier); + } @Override @@ -919,9 +948,7 @@ public class SongMenu extends BasicGameState { // score buttons if (focusScores != null && focusScores.length >= MAX_SCORE_BUTTONS && ScoreData.areaContains(mouseX, mouseY)) { - int newStartScore = startScore + shift; - if (newStartScore >= 0 && newStartScore + MAX_SCORE_BUTTONS <= focusScores.length) - startScore = newStartScore; + startScorePos.scrollOffset(ScoreData.getButtonOffset() * shift); } // song buttons @@ -939,8 +966,8 @@ public class SongMenu extends BasicGameState { selectMapOptionsButton.resetHover(); selectOptionsButton.resetHover(); hoverOffset = 0f; - hoverIndex = -1; - startScore = 0; + hoverIndex = null; + startScorePos.setPosition(0); beatmapMenuTimer = -1; searchTransitionTimer = SEARCH_TRANSITION_TIME; songInfo = null; @@ -1008,7 +1035,7 @@ public class SongMenu extends BasicGameState { ScoreDB.deleteScore(stateActionScore); scoreMap = ScoreDB.getMapSetScores(focusNode.getBeatmapSet().get(focusNode.beatmapIndex)); focusScores = getScoreDataForNode(focusNode, true); - startScore = 0; + startScorePos.setPosition(0); break; case BEATMAP_DELETE_CONFIRM: // delete song group if (stateActionNode == null) @@ -1077,7 +1104,7 @@ public class SongMenu extends BasicGameState { randomStack = new Stack(); songInfo = null; hoverOffset = 0f; - hoverIndex = -1; + hoverIndex = null; search.setText(""); searchTimer = SEARCH_DELAY; searchTransitionTimer = SEARCH_TRANSITION_TIME; @@ -1131,38 +1158,46 @@ public class SongMenu extends BasicGameState { if (shift == 0) return; - int n = shift; - boolean shifted = false; - while (n != 0) { - if (startNode == null) - break; - - int height = container.getHeight(); - if (n < 0 && startNode.prev != null) { - startNode = startNode.prev; - buttonY += buttonOffset / 4; - if (buttonY > headerY + height * 0.02f) - buttonY = headerY + height * 0.02f; - n++; - shifted = true; - } else if (n > 0 && startNode.next != null && - BeatmapSetList.get().getNode(startNode, MAX_SONG_BUTTONS) != null) { - startNode = startNode.next; - buttonY -= buttonOffset / 4; - if (buttonY < headerY - height * 0.02f) - buttonY = headerY - height * 0.02f; - n--; - shifted = true; - } else - break; - } - if (shifted) { - hoverOffset = 0f; - hoverIndex = -1; - } - return; + songScrolling.scrollOffset(shift * buttonOffset); } + /** + * Updates the song list data required for drawing. + */ + private void updateDrawnSongPos() { + float songNodePosDrawn = songScrolling.getPosition(); + + int startNodeIndex = (int) (songNodePosDrawn / buttonOffset); + buttonY = -songNodePosDrawn + buttonOffset * startNodeIndex + headerY - DIVIDER_LINE_WIDTH; + + float max = (BeatmapSetList.get().size() + (focusNode != null ? focusNode.getBeatmapSet().size() : 0)); + songScrolling.setMinMax(0 - buttonOffset * 3, (max - MAX_SONG_BUTTONS- 1 + 3) * buttonOffset); + + //negative startNodeIndex means the first Node is below the header so offset it. + if (startNodeIndex <= 0) { + startNodeOffset = -startNodeIndex; + startNodeIndex = 0; + } else { + startNodeOffset = 0; + } + // Finds the start node with the expanded focus node in mind. + if (focusNode != null && startNodeIndex >= focusNode.index) { + //below the focus node. + if (startNodeIndex <= focusNode.index + focusNode.getBeatmapSet().size()) { + //inside the focus nodes expanded nodes. + int nodeIndex = startNodeIndex - focusNode.index; + startNode = BeatmapSetList.get().getBaseNode(focusNode.index); + startNode = startNode.next; + for (int i = 0; i < nodeIndex; i++) + startNode = startNode.next; + } else { + startNodeIndex -= focusNode.getBeatmapSet().size() - 1; + startNode = BeatmapSetList.get().getBaseNode(startNodeIndex); + } + } else + startNode = BeatmapSetList.get().getBaseNode(startNodeIndex); + + } /** * Sets a new focus node. * @param node the base node; it will be expanded if it isn't already @@ -1176,7 +1211,7 @@ public class SongMenu extends BasicGameState { return null; hoverOffset = 0f; - hoverIndex = -1; + hoverIndex = null; songInfo = null; BeatmapSetNode oldFocus = focusNode; @@ -1184,10 +1219,6 @@ public class SongMenu extends BasicGameState { int expandedIndex = BeatmapSetList.get().getExpandedIndex(); if (node.index != expandedIndex) { node = BeatmapSetList.get().expand(node.index); - - // if start node was previously expanded, move it - if (startNode != null && startNode.index == expandedIndex) - startNode = BeatmapSetList.get().getBaseNode(startNode.index); } // check beatmapIndex bounds @@ -1195,9 +1226,6 @@ public class SongMenu extends BasicGameState { if (beatmapIndex < 0 || beatmapIndex > length - 1) // set a random index beatmapIndex = (int) (Math.random() * length); - // change the focus node - if (changeStartNode || (startNode.index == 0 && startNode.beatmapIndex == -1 && startNode.prev == null)) - startNode = node; focusNode = BeatmapSetList.get().getNode(node, beatmapIndex); Beatmap beatmap = focusNode.getBeatmapSet().get(focusNode.beatmapIndex); MusicController.play(beatmap, false, preview); @@ -1205,32 +1233,59 @@ public class SongMenu extends BasicGameState { // load scores scoreMap = ScoreDB.getMapSetScores(beatmap); focusScores = getScoreDataForNode(focusNode, true); - startScore = 0; + startScorePos.setPosition(0); + + if (oldFocus != null && oldFocus.getBeatmapSet() != node.getBeatmapSet()){ + //Close previous node + if(node.index > oldFocus.index){ + float offset = (oldFocus.getBeatmapSet().size() - 1) * buttonOffset; + //updateSongPos(-offset); + songScrolling.addOffset(-offset); + } - // check startNode bounds - while (startNode.index >= BeatmapSetList.get().size() + length - MAX_SONG_BUTTONS && startNode.prev != null) - startNode = startNode.prev; - - // make sure focusNode is on the screen (TODO: cleanup...) - int val = focusNode.index + focusNode.beatmapIndex - (startNode.index + MAX_SONG_BUTTONS) + 1; - if (val > 0) // below screen - changeIndex(val); - else { // above screen - if (focusNode.index == startNode.index) { - val = focusNode.index + focusNode.beatmapIndex - (startNode.index + startNode.beatmapIndex); - if (val < 0) - changeIndex(val); - } else if (startNode.index > focusNode.index) { - val = focusNode.index - focusNode.getBeatmapSet().size() + focusNode.beatmapIndex - startNode.index + 1; - if (val < 0) - changeIndex(val); + if (Math.abs(node.index - oldFocus.index) > MAX_SONG_BUTTONS) { + // open from the middle + float offset = ((node.getBeatmapSet().size() - 1) * buttonOffset) / 2f; + songScrolling.addOffset(offset); + } else if (node.index > oldFocus.index) { + // open from the bottom + float offset = (node.getBeatmapSet().size() - 1) * buttonOffset; + songScrolling.addOffset(offset); + } else { + // open from the top } } + + // change the focus node + if (changeStartNode || (startNode.index == 0 && startNode.beatmapIndex == -1 && startNode.prev == null)){ + songScrolling.setPosition((node.index - 1) * buttonOffset); + updateDrawnSongPos(); + + } - // if start node is expanded and on group node, move it - if (startNode.index == focusNode.index && startNode.beatmapIndex == -1) - changeIndex(1); - + updateDrawnSongPos(); + + // make sure focusNode is on the screen + int val = focusNode.index + focusNode.beatmapIndex; + if (val <= startNode.index) + songScrolling.scrollToPosition(val * buttonOffset); + else if (val > startNode.index + MAX_SONG_BUTTONS - 1) + songScrolling.scrollToPosition((val - MAX_SONG_BUTTONS + 1) * buttonOffset); + + /* + //Centers selected node + int val = focusNode.index + focusNode.beatmapIndex - MAX_SONG_BUTTONS/2; + songScrolling.scrollToPosition(val * buttonOffset); + //*/ + + /* + //Attempts to make all nodes in the set at least visible + if( focusNode.index * buttonOffset < songScrolling.getPosition()) + songScrolling.scrollToPosition(focusNode.index * buttonOffset); + if ( ( focusNode.index + focusNode.getBeatmapSet().size() ) * buttonOffset > songScrolling.getPosition() + footerY - headerY) + songScrolling.scrollToPosition((focusNode.index + focusNode.getBeatmapSet().size() ) * buttonOffset - (footerY - headerY)); + //*/ + return oldFocus; } diff --git a/src/itdelatrisu/opsu/ui/KinecticScrolling.java b/src/itdelatrisu/opsu/ui/KinecticScrolling.java new file mode 100644 index 00000000..f17adb08 --- /dev/null +++ b/src/itdelatrisu/opsu/ui/KinecticScrolling.java @@ -0,0 +1,155 @@ +package itdelatrisu.opsu.ui; + +/** + * Drag to scroll based on: + * http://ariya.ofilabs.com/2013/11/javascript-kinetic-scrolling-part-2.html + * + * @author fluddokt (https://github.com/fluddokt) + */ +public class KinecticScrolling { + + /** The moving averaging constant */ + final static private float AVG_CONST = 0.2f; + final static private float ONE_MINUS_AVG_CONST = 1 - AVG_CONST; + + /** The constant used to determine how fast the target position will be reach. */ + final static private int TIME_CONST = 200; + + /** The constant used to determine how much of the velocity will be used to launch to the target. */ + final static private float AMPLITUDE_CONST = 0.25f; + + /** The current position. */ + float position; + + /** The offset to scroll to the target position. */ + float amplitude; + + /** The current target to scroll to. */ + float target; + + /** The total amount of time since the mouse button was released. */ + float totalDelta; + + /** The maximum and minimum value the position can reach. */ + float max = Float.MAX_VALUE; + float min = -Float.MAX_VALUE; + + /** Whether the mouse is currently pressed or not */ + boolean pressed = false; + + /** The change in position since the last update */ + float deltaPosition; + + /** The moving average of the velocity. */ + float avgVelocity; + + /** + * Returns the current Position. + * @return the position. + */ + public float getPosition() { + return position; + } + + /** + * Updates the scrolling. + * @param delta the elapsed time since the last update + */ + public void update(float delta) { + if (!pressed) { + totalDelta += delta; + position = target + (float) (-amplitude * Math.exp(-totalDelta / TIME_CONST)); + } else { + avgVelocity = (ONE_MINUS_AVG_CONST * avgVelocity + AVG_CONST * (deltaPosition * 1000f / delta)); + + position += deltaPosition; + target = position ; + deltaPosition = 0; + } + if (position > max) { + amplitude = 0; + target = position = max; + } + if (position < min) { + amplitude = 0; + target = position = min; + } + } + + /** + * Scrolls to the position. + * @param newPosition the position to scroll to. + */ + public void scrollToPosition(float newPosition) { + amplitude = newPosition - position; + target = newPosition; + totalDelta = 0; + } + + /** + * Scrolls to an offset from target. + * @param offset the offset from the target to scroll to. + */ + public void scrollOffset(float offset) { + scrollToPosition(target + offset); + } + + /** + * Sets the position. + * @param newPosition the position to be set + */ + public void setPosition(float newPosition) { + pressed(); + release(); + target = newPosition; + position = target; + } + + /** + * Set the position relative to an offset. + * @param offset the offset from the position. + */ + public void addOffset(float offset) { + setPosition(position + offset); + } + + /** + * Call this when the mouse button has been pressed. + */ + public void pressed() { + if (pressed) + return; + pressed = true; + avgVelocity = 0; + } + + /** + * Call this when the mouse button has been released. + */ + public void release() { + if (!pressed) + return; + pressed = false; + amplitude = AMPLITUDE_CONST * avgVelocity; + target = Math.round(target + amplitude); + totalDelta = 0; + } + + /** + * Call this when the mouse has been dragged. + * @param distance the amount that the mouse has been dragged + */ + public void dragged(float distance) { + deltaPosition += distance; + } + + /** + * Set the minimum and maximum bound. + * @param min the minimum bound + * @param max the maximum bound + */ + public void setMinMax(float min, float max) { + this.min = min; + this.max = max; + } +} diff --git a/src/itdelatrisu/opsu/ui/UI.java b/src/itdelatrisu/opsu/ui/UI.java index 91ee4c9a..1f132618 100644 --- a/src/itdelatrisu/opsu/ui/UI.java +++ b/src/itdelatrisu/opsu/ui/UI.java @@ -318,28 +318,25 @@ public class UI { /** * Draws a scroll bar. * @param g the graphics context - * @param unitIndex the unit index - * @param totalUnits the total number of units - * @param maxShown the maximum number of units shown at one time - * @param unitBaseX the base x coordinate of the units - * @param unitBaseY the base y coordinate of the units + * @param position the position in the virtual area + * @param totalLength the total length of the virtual area + * @param lengthShown the length of the virtual area shown + * @param unitBaseX the base x coordinate + * @param unitBaseY the base y coordinate * @param unitWidth the width of a unit - * @param unitHeight the height of a unit - * @param unitOffsetY the y offset between units + * @param scrollAreaHeight the height of the scroll area * @param bgColor the scroll bar area background color (null if none) * @param scrollbarColor the scroll bar color * @param right whether or not to place the scroll bar on the right side of the unit */ public static void drawScrollbar( - Graphics g, int unitIndex, int totalUnits, int maxShown, - float unitBaseX, float unitBaseY, float unitWidth, float unitHeight, float unitOffsetY, + Graphics g, float position, float totalLength, float lengthShown, + float unitBaseX, float unitBaseY, float unitWidth, float scrollAreaHeight, Color bgColor, Color scrollbarColor, boolean right ) { float scrollbarWidth = container.getWidth() * 0.00347f; - float heightRatio = (float) (2.6701f * Math.exp(-0.81 * Math.log(totalUnits))); - float scrollbarHeight = container.getHeight() * heightRatio; - float scrollAreaHeight = unitHeight + unitOffsetY * (maxShown - 1); - float offsetY = (scrollAreaHeight - scrollbarHeight) * ((float) unitIndex / (totalUnits - maxShown)); + float scrollbarHeight = scrollAreaHeight * lengthShown / totalLength; + float offsetY = (scrollAreaHeight - scrollbarHeight) * (position / (totalLength - lengthShown)); float scrollbarX = unitBaseX + unitWidth - ((right) ? scrollbarWidth : 0); if (bgColor != null) { g.setColor(bgColor);