From 3731232c4916c83a694cf752dde76c2383f563ba Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 28 Jul 2025 11:39:42 -0400 Subject: [PATCH 01/53] added marking preset-added goals as complete/incomplete added color selector for goal completion message. added check upon goal set and login for completed goal. --- .../goaltracker/GoalTrackerConfig.java | 18 ++++++++++++++-- .../goaltracker/GoalTrackerPlugin.java | 16 +++++++++++++- .../toofifty/goaltracker/ui/GoalPanel.java | 19 ++++------------- .../ui/components/ListTaskPanel.java | 21 ++++++++++++++++++- 4 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/GoalTrackerConfig.java b/src/main/java/com/toofifty/goaltracker/GoalTrackerConfig.java index ad07313..d008d91 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalTrackerConfig.java +++ b/src/main/java/com/toofifty/goaltracker/GoalTrackerConfig.java @@ -1,8 +1,11 @@ package com.toofifty.goaltracker; -import net.runelite.client.config.Config; -import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigGroup; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Config; +import net.runelite.client.config.Alpha; +import java.awt.Color; @ConfigGroup("goaltracker") public interface GoalTrackerConfig extends Config @@ -33,4 +36,15 @@ default String goalTrackerItemNoteMapCache() @ConfigItem(keyName = "goalTrackerItemNoteMapCache", name = "", description = "", hidden = true) void goalTrackerItemNoteMapCache(String str); + + @ConfigItem( + keyName = "completionMessageColor", + name = "Completion Message Color", + description = "Color of the chat message when a goal is completed" + ) + @Alpha + default Color completionMessageColor() + { + return new Color(0xFF16ABE5, true); + } } diff --git a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java index 21c110e..4ec6743 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java +++ b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java @@ -184,7 +184,7 @@ public void notifyTask(Task task) log.debug("Notify: [Goal Tracker] You have completed a task: " + task + "!"); String message = "[Goal Tracker] You have completed a task: " + task + "!"; - String formattedMessage = new ChatMessageBuilder().append(ColorScheme.PROGRESS_COMPLETE_COLOR, message).build(); + String formattedMessage = new ChatMessageBuilder().append(config.completionMessageColor(), message).build(); chatMessageManager.queue(QueuedMessage.builder() .type(ChatMessageType.CONSOLE) .name("Goal Tracker") @@ -220,6 +220,20 @@ public void onGameTick(GameTick event) // onGameStateChanged reports incorrect quest statuses, // so this need to be done in this subscriber goalTrackerPanel.refresh(); + + goalManager.getGoals().stream() + .flatMap(goal -> goal.getTasks().stream()) + .filter(task -> !task.getStatus().isCompleted()) + .forEach(task -> { + if (taskUpdateService.update(task)) { + if (task.getStatus().isCompleted()) { + notifyTask(task); + } + uiStatusManager.refresh(task); + } + }); + + goalManager.save(); } @Subscribe diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java index e076aad..7049356 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java @@ -3,6 +3,7 @@ import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.enums.TaskType; +import com.toofifty.goaltracker.models.enums.Status; import com.toofifty.goaltracker.models.task.ManualTask; import com.toofifty.goaltracker.models.task.Task; import com.toofifty.goaltracker.ui.components.EditableInput; @@ -51,23 +52,11 @@ public class GoalPanel extends JPanel implements Refreshable taskListPanel = new ListPanel<>(goal.getTasks(), (task) -> { ListTaskPanel taskPanel = new ListTaskPanel(goal.getTasks(), task); - taskPanel.add(new TaskItemContent(plugin, task)); + TaskItemContent taskContent = new TaskItemContent(plugin, task); + taskPanel.add(taskContent); + taskPanel.setTaskContent(taskContent); taskPanel.setBorder(new EmptyBorder(2, 4, 2, 4)); - if (TaskType.MANUAL.equals(task.getType())) { - ManualTask manualTask = (ManualTask) task; - - taskPanel.onClick(e -> { - manualTask.toggle(); - - if (task.getStatus().isCompleted()) { - plugin.notifyTask(task); - } - - plugin.getUiStatusManager().refresh(task); - this.taskUpdatedListener.accept(manualTask); - }); - } taskPanel.onIndented(e -> { this.goalUpdatedListener.accept(goal); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java index 46d4e81..df43bd0 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java @@ -1,12 +1,17 @@ package com.toofifty.goaltracker.ui.components; +import com.toofifty.goaltracker.ui.TaskItemContent; + import com.toofifty.goaltracker.utils.ReorderableList; import com.toofifty.goaltracker.models.task.Task; +import com.toofifty.goaltracker.models.enums.Status; import java.util.function.Consumer; import javax.swing.JMenuItem; public class ListTaskPanel extends ListItemPanel { + private TaskItemContent taskContent; + private final JMenuItem indentItem = new JMenuItem("Indent"); private final JMenuItem unindentItem = new JMenuItem("Unindent"); @@ -70,7 +75,7 @@ public void refreshMenu() } var previousItem = list.getPreviousItem(item); - + if (item.isNotFullyIndented() && previousItem != null && previousItem.getIndentLevel() >= item.getIndentLevel()) { popupMenu.add(indentItem); } @@ -79,6 +84,16 @@ public void refreshMenu() popupMenu.add(unindentItem); } + String toggleLabel = "Mark as " + (item.getStatus() == Status.COMPLETED ? "Incomplete" : "Completed"); + JMenuItem toggleStatusItem = new JMenuItem(toggleLabel); + toggleStatusItem.addActionListener(e -> { + item.setStatus(item.getStatus() == Status.COMPLETED ? Status.NOT_STARTED : Status.COMPLETED); + if (taskContent != null) { + taskContent.refresh(); + } + }); + popupMenu.add(toggleStatusItem); + popupMenu.add(removeItem); } @@ -89,4 +104,8 @@ public void onIndented(Consumer indentedListener) { public void onUnindented(Consumer unindentedListener) { this.unindentedListener = unindentedListener; } + + public void setTaskContent(TaskItemContent taskContent) { + this.taskContent = taskContent; + } } From 58fa17dc178e47c62c248c745953217b51e799d9 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 28 Jul 2025 12:12:10 -0400 Subject: [PATCH 02/53] Implement manual toggle for preset tasks, configurable completion color, and login goal evaluation - Introduced right-click context menu handling in GoalPanel to allow toggling Task.Status for tasks created via presets - Updated Task model and TaskItemContent to support manual status toggling independent of task source - Added @Alpha color picker to GoalTrackerConfig for customizing chat message color on task completion (default: FF16ABE5) - Applied selected color in plugin logic where task completion messages are sent via chat - Hooked into GameState.LOGGED_IN to invoke goalManager.evaluateAllGoals(), checking for and marking completed tasks based on current game state - Amended README changelog to clarify new features vs previous "fix" label --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index ae30a12..c9179f8 100644 --- a/README.md +++ b/README.md @@ -60,3 +60,10 @@ Track quest progress and completion, just select a quest or miniquest from the d #### Item tasks Select an item using the search button and searching via the in-game chatbox, then set the desired quantity. The plugin will keep track of your items and tally up quantities stored in different inventories (bank, player, GIMP storage), and will be automatically completed once you get that amount of the item. + +## Changelog + +### Recent Additions +- Added manual completion toggling for tasks created from presets, allowing users to right-click and mark them complete/incomplete just like quick-added tasks. +- Added customizable color setting for task completion messages shown in the chatbox. +- Implemented automatic goal status checking upon login to mark goals as completed if requirements are already met. From af239cfc4dedc1b4779bdb67e3911347228bcdf7 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 28 Jul 2025 12:30:47 -0400 Subject: [PATCH 03/53] - Implement right-click context menu on parent goals with options to mark all child tasks as completed or incomplete - Add recursive task status update method to Goal model for batch completion toggling - Ensure goalTrackerPanel refreshes after task updates triggered by quest chat messages - Add quest task validation to onGameTick to auto-update completion status on login - Fix issue where quest task completions were not visually reflected until user re-entered the goal view --- README.md | 2 ++ .../goaltracker/GoalTrackerPlugin.java | 14 ++++++++++++ .../com/toofifty/goaltracker/models/Goal.java | 6 +++++ .../ui/components/ListItemPanel.java | 22 +++++++++++++++++++ 4 files changed, 44 insertions(+) diff --git a/README.md b/README.md index c9179f8..df9b3c2 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ Select an item using the search button and searching via the in-game chatbox, th ## Changelog ### Recent Additions +- Added right-click menu to parent goals to mark all child tasks as completed or incomplete. +- Fixed visual refresh issue where quest task statuses didn’t show correctly on login unless re-entering the goal. - Added manual completion toggling for tasks created from presets, allowing users to right-click and mark them complete/incomplete just like quick-added tasks. - Added customizable color setting for task completion messages shown in the chatbox. - Implemented automatic goal status checking upon login to mark goals as completed if requirements are already met. diff --git a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java index 4ec6743..9002220 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java +++ b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java @@ -147,6 +147,7 @@ protected void shutDown() public void onSessionOpen(SessionOpen event) { goalManager.load(); + goalTrackerPanel.refresh(); } @Subscribe @@ -233,7 +234,19 @@ public void onGameTick(GameTick event) } }); + List questTasks = goalManager.getIncompleteTasksByType(TaskType.QUEST); + for (QuestTask task : questTasks) { + if (!taskUpdateService.update(task)) continue; + + if (task.getStatus().isCompleted()) { + notifyTask(task); + } + + uiStatusManager.refresh(task); + } + goalManager.save(); + goalTrackerPanel.refresh(); } @Subscribe @@ -252,6 +265,7 @@ public void onChatMessage(ChatMessage event) uiStatusManager.refresh(task); this.goalManager.save(); } + goalTrackerPanel.refresh(); } @Subscribe diff --git a/src/main/java/com/toofifty/goaltracker/models/Goal.java b/src/main/java/com/toofifty/goaltracker/models/Goal.java index 72f6952..649301e 100644 --- a/src/main/java/com/toofifty/goaltracker/models/Goal.java +++ b/src/main/java/com/toofifty/goaltracker/models/Goal.java @@ -57,4 +57,10 @@ public Status getStatus() { return Status.NOT_STARTED; } + + public void setAllTasksCompleted(boolean completed) { + for (Task task : tasks) { + task.setStatus(completed ? Status.COMPLETED : Status.NOT_STARTED); + } + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java index fcc9882..71f6429 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java @@ -2,6 +2,7 @@ import com.toofifty.goaltracker.utils.ReorderableList; import com.toofifty.goaltracker.ui.Refreshable; +import com.toofifty.goaltracker.models.Goal; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; @@ -105,6 +106,27 @@ public void refreshMenu() popupMenu.add(moveToBottom); } popupMenu.add(removeItem); + + if (item instanceof Goal) { + JMenuItem markAllComplete = new JMenuItem("Mark all as completed"); + JMenuItem markAllIncomplete = new JMenuItem("Mark all as incomplete"); + + markAllComplete.addActionListener(e -> { + Goal goal = (Goal) item; + goal.setAllTasksCompleted(true); + refresh(); + }); + + markAllIncomplete.addActionListener(e -> { + Goal goal = (Goal) item; + goal.setAllTasksCompleted(false); + refresh(); + }); + + popupMenu.addSeparator(); + popupMenu.add(markAllComplete); + popupMenu.add(markAllIncomplete); + } } public ListItemPanel add(Component comp) From 3ed26e7cd67ac09e88f781fb621981a11791b8eb Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 18 Aug 2025 14:13:05 -0400 Subject: [PATCH 04/53] working presets --- build.gradle | 4 +- .../toofifty/goaltracker/ui/GoalPanel.java | 4 + .../goaltracker/ui/components/ComboBox.java | 16 +- .../ui/components/ListTaskPanel.java | 11 ++ .../goaltracker/ui/inputs/ItemTaskInput.java | 6 +- .../ui/inputs/ManualTaskInput.java | 4 +- .../goaltracker/ui/inputs/QuestTaskInput.java | 171 ++++++++++++++++-- .../ui/inputs/SkillLevelTaskInput.java | 7 +- .../ui/inputs/SkillXpTaskInput.java | 6 +- .../goaltracker/ui/inputs/TaskInput.java | 30 ++- .../goaltracker/utils/QuestRequirements.java | 106 +++++++++++ .../utils/QuestRequirementsTest.java | 56 ++++++ 12 files changed, 384 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java create mode 100644 src/test/java/com/toofifty/goaltracker/utils/QuestRequirementsTest.java diff --git a/build.gradle b/build.gradle index b48403e..cc76a13 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,8 @@ dependencies { compileOnly 'org.projectlombok:lombok:1.18.20' annotationProcessor 'org.projectlombok:lombok:1.18.20' + implementation 'org.apache.commons:commons-lang3:3.12.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.5.2' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.5.2' @@ -40,7 +42,7 @@ tasks.withType(JavaCompile) { tasks.test { useJUnitPlatform() - + finalizedBy jacocoTestReport // report is always generated after tests run } diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java index 7049356..f1695a3 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java @@ -55,6 +55,7 @@ public class GoalPanel extends JPanel implements Refreshable TaskItemContent taskContent = new TaskItemContent(plugin, task); taskPanel.add(taskContent); taskPanel.setTaskContent(taskContent); + taskContent.refresh(); taskPanel.setBorder(new EmptyBorder(2, 4, 2, 4)); @@ -86,6 +87,9 @@ public void updateFromNewTask(Task task) taskListPanel.tryBuildList(); taskListPanel.refresh(); plugin.setValidateAll(true); + plugin.getUiStatusManager().refresh(goal); + revalidate(); + repaint(); if (Objects.nonNull(this.taskAddedListener)) this.taskAddedListener.accept(task); if (Objects.nonNull(this.taskUpdatedListener)) this.taskUpdatedListener.accept(task); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java b/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java index 70fd11d..0493db3 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java @@ -13,9 +13,9 @@ import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; -import javax.swing.ListCellRenderer; import javax.swing.border.EmptyBorder; import javax.swing.plaf.basic.BasicComboBoxUI; +import javax.swing.ListCellRenderer; import net.runelite.client.ui.ColorScheme; import net.runelite.client.util.ImageUtil; import net.runelite.client.util.Text; @@ -47,6 +47,18 @@ public void setFormatter(Function formatter) this.formatter = formatter; setRenderer(new ComboBoxListRenderer<>(formatter)); } + + @SuppressWarnings("unchecked") + public void setItems(List items) + { + removeAllItems(); + for (T item : items) { + addItem(item); + } + if (getItemCount() > 0) { + setSelectedIndex(0); + } + } } class ComboBoxUI extends BasicComboBoxUI @@ -110,4 +122,4 @@ public Component getListCellRendererComponent( return container; } -} +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java index df43bd0..0a9c2ee 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java @@ -7,6 +7,8 @@ import com.toofifty.goaltracker.models.enums.Status; import java.util.function.Consumer; import javax.swing.JMenuItem; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; public class ListTaskPanel extends ListItemPanel { @@ -55,6 +57,15 @@ public ListTaskPanel(ReorderableList list, Task item) item.unindent(); this.unindentedListener.accept(item); }); + // Allow shift-click to remove this item + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.isShiftDown() && e.getButton() == MouseEvent.BUTTON1) { + removeItem.doClick(); + } + } + }); } @Override diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java index 431c780..ad34fb6 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java @@ -17,7 +17,7 @@ import java.awt.*; import java.util.regex.Pattern; -public class ItemTaskInput extends TaskInput +public class ItemTaskInput extends TaskInput { private final ItemManager itemManager; private final ClientThread clientThread; @@ -122,7 +122,7 @@ protected void submit() .itemId(selectedItem.getId()) .itemName(selectedItem.getName()) .quantity(Integer.parseInt(quantityField.getText())) - .build()); + .build()); } @Override @@ -143,4 +143,4 @@ private void clearSelectedItem() revalidate(); repaint(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/ManualTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/ManualTaskInput.java index 4a7f1ce..39fe519 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/ManualTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/ManualTaskInput.java @@ -10,7 +10,7 @@ import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; -public class ManualTaskInput extends TaskInput +public class ManualTaskInput extends TaskInput { private final FlatTextField titleField; @@ -50,4 +50,4 @@ protected void reset() titleField.setText(""); titleField.requestFocusInWindow(); } -} +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java index 9aeff72..99879c0 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java @@ -3,41 +3,184 @@ import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.task.QuestTask; -import com.toofifty.goaltracker.ui.components.ComboBox; +import com.toofifty.goaltracker.models.task.SkillLevelTask; +import com.toofifty.goaltracker.models.task.Task; +import com.toofifty.goaltracker.utils.QuestRequirements; import net.runelite.api.Quest; +import org.apache.commons.lang3.StringUtils; +import net.runelite.client.ui.ColorScheme; +import javax.swing.*; +import javax.swing.event.DocumentListener; +import javax.swing.event.DocumentEvent; +import javax.swing.border.EmptyBorder; import java.awt.*; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; import java.util.Arrays; import java.util.Comparator; import java.util.List; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.ArrayList; -public class QuestTaskInput extends TaskInput +public class QuestTaskInput extends TaskInput { - private final ComboBox questField; + private final JTextField searchField; + private final JLabel matchLabel; + private final List allQuests; + private Quest bestMatch; public QuestTaskInput(GoalTrackerPlugin plugin, Goal goal) { super(plugin, goal, "Quest"); - List quests = Arrays.asList(Quest.values()); - quests.sort(Comparator.comparing( - (quest) -> quest.getName().replaceFirst("^(A|The) ", ""))); - questField = new ComboBox<>(quests); - questField.setFormatter(Quest::getName); - getInputRow().add(questField, BorderLayout.CENTER); + + // Initialize all quests and sort them + allQuests = Arrays.asList(Quest.values()); + allQuests.sort(Comparator.comparing(Quest::getName)); + + + // Initialize search field + searchField = new JTextField(); + searchField.setPreferredSize(new Dimension(150, 24)); + searchField.setBackground(ColorScheme.DARKER_GRAY_COLOR); + searchField.setForeground(Color.WHITE); + searchField.setBorder(new EmptyBorder(4, 4, 4, 4)); + + // Add Enter key listener + searchField.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + submit(); + } + } + }); + + // Initialize match label + matchLabel = new JLabel("Enter a quest name"); + matchLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + matchLabel.setFont(new Font("SansSerif", Font.ITALIC, 12)); + + // Add listener for real-time search + searchField.getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { updateMatch(); } + @Override + public void removeUpdate(DocumentEvent e) { updateMatch(); } + @Override + public void changedUpdate(DocumentEvent e) { updateMatch(); } + }); + + // Add components to layout + JPanel container = new JPanel(new BorderLayout(5, 5)); + container.add(searchField, BorderLayout.NORTH); + container.add(matchLabel, BorderLayout.CENTER); + getInputRow().add(container, BorderLayout.CENTER); + } + + private void updateMatch() + { + String searchText = searchField.getText().trim(); + bestMatch = null; + + if (searchText.isEmpty()) { + matchLabel.setText("Enter a quest name"); + matchLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + return; + } + + final String lower = searchText.toLowerCase(Locale.ROOT); + + // 1) Exact (case-insensitive) match first + for (Quest q : allQuests) { + if (q.getName().equalsIgnoreCase(searchText)) { + bestMatch = q; + matchLabel.setText("Match: " + q.getName()); + matchLabel.setForeground(ColorScheme.BRAND_ORANGE); + return; + } + } + + // 2) Prefix match (case-insensitive); pick the shortest matching name + Quest prefixBest = null; + for (Quest q : allQuests) { + String nameLower = q.getName().toLowerCase(Locale.ROOT); + if (nameLower.startsWith(lower)) { + if (prefixBest == null || q.getName().length() < prefixBest.getName().length()) { + prefixBest = q; + } + } + } + if (prefixBest != null) { + bestMatch = prefixBest; + matchLabel.setText("Match: " + bestMatch.getName()); + matchLabel.setForeground(ColorScheme.BRAND_ORANGE); + return; + } + + // 3) Fuzzy: compute Levenshtein distances once and pick the minimum + Quest candidate = null; + int bestDistance = Integer.MAX_VALUE; + for (Quest q : allQuests) { + int d = StringUtils.getLevenshteinDistance( + q.getName().toLowerCase(Locale.ROOT), lower + ); + if (d < bestDistance) { + bestDistance = d; + candidate = q; + } + } + + if (candidate != null) { + int nameLen = candidate.getName().length(); + int threshold = Math.max(1, (int)Math.floor(nameLen * 0.3)); + if (bestDistance <= threshold) { + bestMatch = candidate; + matchLabel.setText("Match: " + bestMatch.getName()); + matchLabel.setForeground(ColorScheme.BRAND_ORANGE); + return; + } + } + + matchLabel.setText("No close match found"); + matchLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); } @Override protected void submit() { - addTask(QuestTask.builder() - .quest((Quest) questField.getSelectedItem()) - .build()); + if (bestMatch != null) { + // Build list first so reset() in addTasks doesn't clear bestMatch prematurely + List toAdd = new ArrayList<>(); + toAdd.add( + QuestTask.builder() + .quest(bestMatch) + .indentLevel(0) + .build() + ); + + List requirements = QuestRequirements.getRequirements(bestMatch, 0); + toAdd.addAll(requirements); + + // Add all at once (single refresh/reset) + addTasks(toAdd); + + matchLabel.setText("Quest and requirements added: " + bestMatch.getName()); + matchLabel.setForeground(ColorScheme.BRAND_ORANGE); + } else { + matchLabel.setText("Please select a valid quest"); + matchLabel.setForeground(Color.RED); + } } @Override protected void reset() { - questField.setSelectedIndex(0); + searchField.setText(""); + matchLabel.setText("Enter a quest name"); + matchLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + bestMatch = null; } -} +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillLevelTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillLevelTaskInput.java index 71d07f1..fc29ba8 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillLevelTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillLevelTaskInput.java @@ -14,9 +14,8 @@ import java.awt.*; import java.util.regex.Pattern; -public class SkillLevelTaskInput extends TaskInput +public class SkillLevelTaskInput extends TaskInput { - private FlatTextField levelField; private String levelFieldValue = "99"; @@ -61,7 +60,7 @@ protected void submit() addTask(SkillLevelTask.builder() .skill((Skill) skillField.getSelectedItem()) .level(Integer.parseInt(levelField.getText())) - .build()); + .build()); } @Override @@ -72,4 +71,4 @@ protected void reset() skillField.setSelectedIndex(0); } -} +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillXpTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillXpTaskInput.java index 408ba23..f02a9a4 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillXpTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillXpTaskInput.java @@ -14,7 +14,7 @@ import java.awt.*; import java.util.regex.Pattern; -public class SkillXpTaskInput extends TaskInput +public class SkillXpTaskInput extends TaskInput { private final FlatTextField xpField; private final ComboBox skillField; @@ -79,7 +79,7 @@ protected void submit() addTask(SkillXpTask.builder() .skill((Skill) skillField.getSelectedItem()) .xp(Integer.parseInt(xpField.getText())) - .build()); + .build()); } @Override @@ -90,4 +90,4 @@ protected void reset() skillField.setSelectedIndex(0); } -} +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java index f651f6f..3e310f9 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java @@ -7,23 +7,24 @@ import java.awt.BorderLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; -import java.util.function.Consumer; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.border.EmptyBorder; import lombok.Getter; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.FontManager; +import java.util.function.Consumer; +import java.util.Collection; -public abstract class TaskInput extends JPanel +public abstract class TaskInput extends JPanel { protected final int PREFERRED_INPUT_HEIGHT = 16; - protected GoalTrackerPlugin plugin; + protected final GoalTrackerPlugin plugin; private final Goal goal; @Getter private final JPanel inputRow; @Getter - private Consumer listener; + private Consumer listener; TaskInput(GoalTrackerPlugin plugin, Goal goal, String title) { @@ -63,18 +64,31 @@ public abstract class TaskInput extends JPanel abstract protected void submit(); - public void addTask(T task) + public void addTask(Task task) { goal.getTasks().add(task); - this.listener.accept(task); + if (listener != null) { + listener.accept(task); + } + this.reset(); + } + + public void addTasks(Collection tasks) + { + goal.getTasks().addAll(tasks); + if (listener != null) { + for (Task t : tasks) { + listener.accept(t); + } + } this.reset(); } abstract protected void reset(); - public TaskInput onSubmit(Consumer listener) + public TaskInput onSubmit(Consumer listener) { this.listener = listener; return this; } -} +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java new file mode 100644 index 0000000..9d3eb0b --- /dev/null +++ b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java @@ -0,0 +1,106 @@ +package com.toofifty.goaltracker.utils; + +import com.toofifty.goaltracker.models.task.QuestTask; +import com.toofifty.goaltracker.models.task.SkillLevelTask; +import com.toofifty.goaltracker.models.task.Task; +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class QuestRequirements +{ + // Map of quests to their requirements (quests and skills) + private static final Map> REQUIREMENT_MAP; + + // Static initializer with minimal dependencies + static { + REQUIREMENT_MAP = new HashMap<>(); + REQUIREMENT_MAP.put( + Quest.FAIRYTALE_I__GROWING_PAINS, + Arrays.asList( + QuestTask.builder().quest(Quest.LOST_CITY).build(), + QuestTask.builder().quest(Quest.NATURE_SPIRIT).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(18).build() + ) + ); + REQUIREMENT_MAP.put( + Quest.NATURE_SPIRIT, + Arrays.asList( + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), + QuestTask.builder().quest(Quest.THE_RESTLESS_GHOST).build(), + SkillLevelTask.builder().skill(Skill.PRAYER).level(18).build() + ) + ); + REQUIREMENT_MAP.put( + Quest.LOST_CITY, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(31).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(36).build() + ) + ); + REQUIREMENT_MAP.put( + Quest.PRIEST_IN_PERIL, + Collections.emptyList() // No requirements + ); + REQUIREMENT_MAP.put( + Quest.THE_RESTLESS_GHOST, + Collections.emptyList() // No requirements + ); + } + + /** + * Get all requirements for a quest, including recursive sub-quest requirements. + * @param quest The quest to get requirements for. + * @param indentLevel The indentation level for the tasks (0 for main quest). + * @return List of tasks (quest and skill requirements) with appropriate indentation. + */ + public static List getRequirements(Quest quest, int indentLevel) + { + List tasks = new ArrayList<>(); + if (indentLevel >= 3) { + // Respect Task.java's max indent level of 3 + return tasks; + } + + List directRequirements = REQUIREMENT_MAP.getOrDefault(quest, Collections.emptyList()); + for (Task task : directRequirements) { + Task withIndent = copyWithIndent(task, indentLevel + 1); + tasks.add(withIndent); + + if (withIndent instanceof QuestTask) { + Quest subQuest = ((QuestTask) withIndent).getQuest(); + List subRequirements = getRequirements(subQuest, indentLevel + 1); + tasks.addAll(subRequirements); + } + } + + return tasks; + } + + private static Task copyWithIndent(Task task, int indentLevel) + { + if (task instanceof QuestTask) { + QuestTask qt = (QuestTask) task; + return QuestTask.builder() + .quest(qt.getQuest()) + .indentLevel(indentLevel) + .build(); + } + if (task instanceof SkillLevelTask) { + SkillLevelTask st = (SkillLevelTask) task; + return SkillLevelTask.builder() + .skill(st.getSkill()) + .level(st.getLevel()) + .indentLevel(indentLevel) + .build(); + } + // Fallback: set indent on the same instance (least preferable, but safe for unknown subclasses) + task.setIndentLevel(indentLevel); + return task; + } +} \ No newline at end of file diff --git a/src/test/java/com/toofifty/goaltracker/utils/QuestRequirementsTest.java b/src/test/java/com/toofifty/goaltracker/utils/QuestRequirementsTest.java new file mode 100644 index 0000000..8d5fec6 --- /dev/null +++ b/src/test/java/com/toofifty/goaltracker/utils/QuestRequirementsTest.java @@ -0,0 +1,56 @@ +package com.toofifty.goaltracker.utils; + +import com.toofifty.goaltracker.models.task.QuestTask; +import com.toofifty.goaltracker.models.task.SkillLevelTask; +import com.toofifty.goaltracker.models.task.Task; +import net.runelite.api.Quest; +import net.runelite.api.Skill; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +public class QuestRequirementsTest +{ + @Test + void testGetRequirements() + { + List requirements = QuestRequirements.getRequirements(Quest.FAIRYTALE_I__GROWING_PAINS, 0); + assertNotNull(requirements, "Requirements list should not be null"); + assertEquals(3, requirements.size(), "Fairytale I should have 3 direct requirements"); + + // All items should be indented one level when called with indentLevel=0 + requirements.forEach(t -> assertEquals(1, t.getIndentLevel(), "Each requirement should have indent 1")); + + // Expect two quest tasks and one skill requirement + long questCount = requirements.stream().filter(t -> t instanceof QuestTask).count(); + long skillCount = requirements.stream().filter(t -> t instanceof SkillLevelTask).count(); + assertEquals(2, questCount, "Should contain two QuestTask requirements"); + assertEquals(1, skillCount, "Should contain one SkillLevelTask requirement"); + + // Verify specific quests are present + boolean hasLostCity = requirements.stream().anyMatch(t -> t instanceof QuestTask && ((QuestTask) t).getQuest() == Quest.LOST_CITY); + boolean hasNatureSpirit = requirements.stream().anyMatch(t -> t instanceof QuestTask && ((QuestTask) t).getQuest() == Quest.NATURE_SPIRIT); + assertTrue(hasLostCity, "Lost City should be a requirement"); + assertTrue(hasNatureSpirit, "Nature Spirit should be a requirement"); + + // Verify specific skill requirement (Farming 18) + boolean hasFarming18 = requirements.stream().anyMatch(t -> t instanceof SkillLevelTask && ((SkillLevelTask) t).getSkill() == Skill.FARMING && ((SkillLevelTask) t).getLevel() == 18); + assertTrue(hasFarming18, "Farming 18 should be a requirement"); + } + + @Test + void testRequirementsAreFreshInstances() + { + List first = QuestRequirements.getRequirements(Quest.FAIRYTALE_I__GROWING_PAINS, 0); + List second = QuestRequirements.getRequirements(Quest.FAIRYTALE_I__GROWING_PAINS, 0); + + assertEquals(first.size(), second.size(), "Both calls should return the same number of items"); + for (int i = 0; i < first.size(); i++) { + assertNotSame(first.get(i), second.get(i), "Each requirement should be a fresh instance on each call"); + assertEquals(first.get(i).getClass(), second.get(i).getClass(), "Classes should match between calls"); + assertEquals(first.get(i).getIndentLevel(), second.get(i).getIndentLevel(), "Indent levels should match between calls"); + } + } +} \ No newline at end of file From d1fad2950dc98497a09c6acf14648d3c31accf82 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 18 Aug 2025 14:17:07 -0400 Subject: [PATCH 05/53] working presets --- .../ui/components/ListTaskPanel.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java index 0a9c2ee..239d0a6 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java @@ -9,6 +9,7 @@ import javax.swing.JMenuItem; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.Component; public class ListTaskPanel extends ListItemPanel { @@ -57,15 +58,38 @@ public ListTaskPanel(ReorderableList list, Task item) item.unindent(); this.unindentedListener.accept(item); }); - // Allow shift-click to remove this item + // Allow shift-click to remove this item and all its indented children addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.isShiftDown() && e.getButton() == MouseEvent.BUTTON1) { + int index = list.indexOf(item); + int baseIndent = item.getIndentLevel(); + // Remove all children that are more indented than this item + while (index + 1 < list.size() && list.get(index + 1).getIndentLevel() > baseIndent) { + list.remove(list.get(index + 1)); + } + // Remove the item itself removeItem.doClick(); } } }); + // Also apply the same shift-click removal listener to all child components + for (Component child : getComponents()) { + child.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.isShiftDown() && e.getButton() == MouseEvent.BUTTON1) { + int index = list.indexOf(item); + int baseIndent = item.getIndentLevel(); + while (index + 1 < list.size() && list.get(index + 1).getIndentLevel() > baseIndent) { + list.remove(list.get(index + 1)); + } + removeItem.doClick(); + } + } + }); + } } @Override From 988718ca96e805d53a33d8451b91628543bdb2a7 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 18 Aug 2025 14:42:50 -0400 Subject: [PATCH 06/53] added making complete/incomplete for all indented as well. --- .../toofifty/goaltracker/ui/GoalPanel.java | 11 +- .../goaltracker/ui/TaskItemContent.java | 29 +++- .../ui/components/ListTaskPanel.java | 36 +++- .../goaltracker/ui/inputs/QuestTaskInput.java | 160 ++++-------------- 4 files changed, 105 insertions(+), 131 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java index f1695a3..8b73680 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java @@ -52,7 +52,7 @@ public class GoalPanel extends JPanel implements Refreshable taskListPanel = new ListPanel<>(goal.getTasks(), (task) -> { ListTaskPanel taskPanel = new ListTaskPanel(goal.getTasks(), task); - TaskItemContent taskContent = new TaskItemContent(plugin, task); + TaskItemContent taskContent = new TaskItemContent(plugin, goal, task); taskPanel.add(taskContent); taskPanel.setTaskContent(taskContent); taskContent.refresh(); @@ -95,6 +95,15 @@ public void updateFromNewTask(Task task) if (Objects.nonNull(this.taskUpdatedListener)) this.taskUpdatedListener.accept(task); } + public void refreshTaskList() + { + taskListPanel.tryBuildList(); + taskListPanel.refresh(); + plugin.getUiStatusManager().refresh(goal); + revalidate(); + repaint(); + } + @Override public void refresh() { diff --git a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java index 9f0adbf..192085a 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java @@ -1,8 +1,14 @@ package com.toofifty.goaltracker.ui; import com.toofifty.goaltracker.GoalTrackerPlugin; +import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.task.Task; import com.toofifty.goaltracker.services.TaskIconService; +import com.toofifty.goaltracker.models.task.QuestTask; +import com.toofifty.goaltracker.utils.QuestRequirements; +import java.util.List; + +import com.toofifty.goaltracker.ui.components.ListPanel; import javax.swing.*; import javax.swing.border.EmptyBorder; @@ -13,19 +19,40 @@ public class TaskItemContent extends JPanel implements Refreshable { private final Task task; + private final Goal goal; private final TaskIconService iconService; private final JLabel titleLabel = new JLabel(); private final JLabel iconLabel = new JLabel(); + private final JButton prereqButton = new JButton("Add prereqs"); - TaskItemContent(GoalTrackerPlugin plugin, Task task) + TaskItemContent(GoalTrackerPlugin plugin, Goal goal, Task task) { super(new BorderLayout()); this.task = task; + this.goal = goal; iconService = plugin.getTaskIconService(); titleLabel.setPreferredSize(new Dimension(0, 24)); add(titleLabel, BorderLayout.CENTER); + if (task instanceof QuestTask) { + prereqButton.setMargin(new Insets(2, 4, 2, 4)); + prereqButton.setFocusable(false); + prereqButton.addActionListener(e -> { + QuestTask qt = (QuestTask) task; + List reqs = QuestRequirements.getRequirements(qt.getQuest(), qt.getIndentLevel()); + for (Task req : reqs) { + goal.getTasks().add(req); + } + Container parent = SwingUtilities.getAncestorOfClass(ListPanel.class, TaskItemContent.this); + if (parent instanceof ListPanel) { + ((ListPanel) parent).tryBuildList(); + ((ListPanel) parent).refresh(); + } + }); + add(prereqButton, BorderLayout.EAST); + } + JPanel iconWrapper = new JPanel(new BorderLayout()); iconWrapper.setBorder(new EmptyBorder(4, 0, 0, 4)); iconWrapper.add(iconLabel, BorderLayout.NORTH); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java index 239d0a6..8d04080 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java @@ -10,6 +10,8 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.Component; +import javax.swing.SwingUtilities; +import java.awt.Container; public class ListTaskPanel extends ListItemPanel { @@ -40,6 +42,7 @@ public ListTaskPanel(ReorderableList list, Task item) item.indent(); this.indentedListener.accept(item); + refreshParentList(); }); unindentItem.addActionListener(e -> { @@ -57,6 +60,7 @@ public ListTaskPanel(ReorderableList list, Task item) item.unindent(); this.unindentedListener.accept(item); + refreshParentList(); }); // Allow shift-click to remove this item and all its indented children addMouseListener(new MouseAdapter() { @@ -71,6 +75,7 @@ public void mouseClicked(MouseEvent e) { } // Remove the item itself removeItem.doClick(); + refreshParentList(); } } }); @@ -86,6 +91,7 @@ public void mouseClicked(MouseEvent e) { list.remove(list.get(index + 1)); } removeItem.doClick(); + refreshParentList(); } } }); @@ -122,10 +128,25 @@ public void refreshMenu() String toggleLabel = "Mark as " + (item.getStatus() == Status.COMPLETED ? "Incomplete" : "Completed"); JMenuItem toggleStatusItem = new JMenuItem(toggleLabel); toggleStatusItem.addActionListener(e -> { - item.setStatus(item.getStatus() == Status.COMPLETED ? Status.NOT_STARTED : Status.COMPLETED); + Status newStatus = (item.getStatus() == Status.COMPLETED ? Status.NOT_STARTED : Status.COMPLETED); + item.setStatus(newStatus); + + // Cascade status change to all indented children + int index = list.indexOf(item); + int baseIndent = item.getIndentLevel(); + for (int i = index + 1; i < list.size(); i++) { + Task child = list.get(i); + if (child.getIndentLevel() <= baseIndent) { + break; // stop at siblings or parents + } + child.setStatus(newStatus); + } + if (taskContent != null) { taskContent.refresh(); } + // Refresh the entire list panel so UI updates immediately + refreshParentList(); }); popupMenu.add(toggleStatusItem); @@ -143,4 +164,17 @@ public void onUnindented(Consumer unindentedListener) { public void setTaskContent(TaskItemContent taskContent) { this.taskContent = taskContent; } + + private void refreshParentList() + { + Container parent = SwingUtilities.getAncestorOfClass(ListPanel.class, this); + if (parent instanceof ListPanel) { + ((ListPanel) parent).tryBuildList(); + ((ListPanel) parent).refresh(); + } else { + // Fallback + revalidate(); + repaint(); + } + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java index 99879c0..46f1a61 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java @@ -3,184 +3,88 @@ import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.task.QuestTask; -import com.toofifty.goaltracker.models.task.SkillLevelTask; import com.toofifty.goaltracker.models.task.Task; import com.toofifty.goaltracker.utils.QuestRequirements; import net.runelite.api.Quest; -import org.apache.commons.lang3.StringUtils; import net.runelite.client.ui.ColorScheme; import javax.swing.*; -import javax.swing.event.DocumentListener; -import javax.swing.event.DocumentEvent; import javax.swing.border.EmptyBorder; import java.awt.*; -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; import java.util.Arrays; import java.util.Comparator; import java.util.List; -import java.util.Locale; -import java.util.stream.Collectors; import java.util.ArrayList; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; public class QuestTaskInput extends TaskInput { - private final JTextField searchField; - private final JLabel matchLabel; private final List allQuests; private Quest bestMatch; + private final JComboBox questDropdown; public QuestTaskInput(GoalTrackerPlugin plugin, Goal goal) { super(plugin, goal, "Quest"); - // Initialize all quests and sort them allQuests = Arrays.asList(Quest.values()); allQuests.sort(Comparator.comparing(Quest::getName)); - - // Initialize search field - searchField = new JTextField(); - searchField.setPreferredSize(new Dimension(150, 24)); - searchField.setBackground(ColorScheme.DARKER_GRAY_COLOR); - searchField.setForeground(Color.WHITE); - searchField.setBorder(new EmptyBorder(4, 4, 4, 4)); - - // Add Enter key listener - searchField.addKeyListener(new KeyAdapter() { + // Initialize dropdown with all quests and custom renderer + questDropdown = new JComboBox<>(allQuests.toArray(new Quest[0])); + questDropdown.setRenderer(new DefaultListCellRenderer() { @Override - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ENTER) { - submit(); + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value instanceof Quest) { + String name = ((Quest) value).getName(); + setText(name); } + return this; + } + }); + questDropdown.setSelectedIndex(-1); // no selection initially + questDropdown.addActionListener(e -> { + Quest selected = (Quest) questDropdown.getSelectedItem(); + if (selected != null) { + bestMatch = selected; } }); - // Initialize match label - matchLabel = new JLabel("Enter a quest name"); - matchLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - matchLabel.setFont(new Font("SansSerif", Font.ITALIC, 12)); - - // Add listener for real-time search - searchField.getDocument().addDocumentListener(new DocumentListener() { - @Override - public void insertUpdate(DocumentEvent e) { updateMatch(); } + questDropdown.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "submitQuest"); + questDropdown.getActionMap().put("submitQuest", new AbstractAction() { @Override - public void removeUpdate(DocumentEvent e) { updateMatch(); } - @Override - public void changedUpdate(DocumentEvent e) { updateMatch(); } + public void actionPerformed(ActionEvent e) { + submit(); + } }); // Add components to layout JPanel container = new JPanel(new BorderLayout(5, 5)); - container.add(searchField, BorderLayout.NORTH); - container.add(matchLabel, BorderLayout.CENTER); + container.add(questDropdown, BorderLayout.CENTER); getInputRow().add(container, BorderLayout.CENTER); } - private void updateMatch() - { - String searchText = searchField.getText().trim(); - bestMatch = null; - - if (searchText.isEmpty()) { - matchLabel.setText("Enter a quest name"); - matchLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - return; - } - - final String lower = searchText.toLowerCase(Locale.ROOT); - - // 1) Exact (case-insensitive) match first - for (Quest q : allQuests) { - if (q.getName().equalsIgnoreCase(searchText)) { - bestMatch = q; - matchLabel.setText("Match: " + q.getName()); - matchLabel.setForeground(ColorScheme.BRAND_ORANGE); - return; - } - } - - // 2) Prefix match (case-insensitive); pick the shortest matching name - Quest prefixBest = null; - for (Quest q : allQuests) { - String nameLower = q.getName().toLowerCase(Locale.ROOT); - if (nameLower.startsWith(lower)) { - if (prefixBest == null || q.getName().length() < prefixBest.getName().length()) { - prefixBest = q; - } - } - } - if (prefixBest != null) { - bestMatch = prefixBest; - matchLabel.setText("Match: " + bestMatch.getName()); - matchLabel.setForeground(ColorScheme.BRAND_ORANGE); - return; - } - - // 3) Fuzzy: compute Levenshtein distances once and pick the minimum - Quest candidate = null; - int bestDistance = Integer.MAX_VALUE; - for (Quest q : allQuests) { - int d = StringUtils.getLevenshteinDistance( - q.getName().toLowerCase(Locale.ROOT), lower - ); - if (d < bestDistance) { - bestDistance = d; - candidate = q; - } - } - - if (candidate != null) { - int nameLen = candidate.getName().length(); - int threshold = Math.max(1, (int)Math.floor(nameLen * 0.3)); - if (bestDistance <= threshold) { - bestMatch = candidate; - matchLabel.setText("Match: " + bestMatch.getName()); - matchLabel.setForeground(ColorScheme.BRAND_ORANGE); - return; - } - } - - matchLabel.setText("No close match found"); - matchLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); - } - @Override protected void submit() { if (bestMatch != null) { - // Build list first so reset() in addTasks doesn't clear bestMatch prematurely - List toAdd = new ArrayList<>(); - toAdd.add( - QuestTask.builder() - .quest(bestMatch) - .indentLevel(0) - .build() - ); - - List requirements = QuestRequirements.getRequirements(bestMatch, 0); - toAdd.addAll(requirements); - - // Add all at once (single refresh/reset) - addTasks(toAdd); + QuestTask mainQuestTask = QuestTask.builder() + .quest(bestMatch) + .indentLevel(0) + .build(); - matchLabel.setText("Quest and requirements added: " + bestMatch.getName()); - matchLabel.setForeground(ColorScheme.BRAND_ORANGE); - } else { - matchLabel.setText("Please select a valid quest"); - matchLabel.setForeground(Color.RED); + addTask(mainQuestTask); } } @Override protected void reset() { - searchField.setText(""); - matchLabel.setText("Enter a quest name"); - matchLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + questDropdown.setSelectedIndex(-1); bestMatch = null; } } \ No newline at end of file From e1d1108904562bb21dd22963c7307a6f32cef569 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 18 Aug 2025 14:50:10 -0400 Subject: [PATCH 07/53] pre-reqs are added directly under the related item --- .../com/toofifty/goaltracker/ui/TaskItemContent.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java index 192085a..2465bbf 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java @@ -36,14 +36,15 @@ public class TaskItemContent extends JPanel implements Refreshable add(titleLabel, BorderLayout.CENTER); if (task instanceof QuestTask) { - prereqButton.setMargin(new Insets(2, 4, 2, 4)); + prereqButton.setMargin(new Insets(1, 3, 1, 3)); + prereqButton.setFont(prereqButton.getFont().deriveFont(prereqButton.getFont().getSize2D() * 0.75f)); prereqButton.setFocusable(false); prereqButton.addActionListener(e -> { QuestTask qt = (QuestTask) task; List reqs = QuestRequirements.getRequirements(qt.getQuest(), qt.getIndentLevel()); - for (Task req : reqs) { - goal.getTasks().add(req); - } + // Insert directly after this item + int insertAt = Math.max(0, goal.getTasks().indexOf(task) + 1); + goal.getTasks().addAll(insertAt, reqs); Container parent = SwingUtilities.getAncestorOfClass(ListPanel.class, TaskItemContent.this); if (parent instanceof ListPanel) { ((ListPanel) parent).tryBuildList(); From e896997c9023cfe4204af2c1ddc9b875803371ae Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 18 Aug 2025 15:03:52 -0400 Subject: [PATCH 08/53] pre-reqs are added directly under the related item --- changelog.md | 16 ++++++ .../goaltracker/utils/QuestRequirements.java | 52 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 changelog.md diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..b03245a --- /dev/null +++ b/changelog.md @@ -0,0 +1,16 @@ + + +# Changelog + +## [Unreleased] - 2025-08-18 + +### Added +- Quest prerequisites button: Each quest task now has an **Add prereqs** button to insert its prerequisites directly beneath it. +- Shift+Click removal: Shift+Click a task to remove it and all its indented children at once. +- Completion cascading: Marking a parent quest as complete/incomplete now automatically updates all its child tasks. +- Dropdown quest selector: Replaced fuzzy search with a clean dropdown of all quests, displaying natural names (e.g., "Tree Gnome Village"). + +### Changed +- Pre-req button made more compact (~25% smaller). +- Prerequisite insertion now places child tasks directly below their parent instead of at the end of the list. +- UI now automatically refreshes after task mutations (add/remove/indent/status change). diff --git a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java index 9d3eb0b..2eac875 100644 --- a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java +++ b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java @@ -51,6 +51,58 @@ public class QuestRequirements Quest.THE_RESTLESS_GHOST, Collections.emptyList() // No requirements ); + REQUIREMENT_MAP.put( + Quest.DRAGON_SLAYER_I, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.ATTACK).level(32).build(), + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build() + ) + ); + REQUIREMENT_MAP.put( + Quest.COOKS_ASSISTANT, + Collections.emptyList() // No requirements + ); + REQUIREMENT_MAP.put( + Quest.DEMON_SLAYER, + Collections.emptyList() // No requirements + ); + REQUIREMENT_MAP.put( + Quest.HEROES_QUEST, + Arrays.asList( + QuestTask.builder().quest(Quest.DRAGON_SLAYER_I).build(), + QuestTask.builder().quest(Quest.LOST_CITY).build(), + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(48).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(53).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(55).build() + ) + ); + REQUIREMENT_MAP.put( + Quest.MONKS_FRIEND, + Collections.emptyList() // No requirements + ); + REQUIREMENT_MAP.put( + Quest.THE_GRAND_TREE, + Arrays.asList( + QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(25).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(25).build() + ) + ); + REQUIREMENT_MAP.put( + Quest.TREE_GNOME_VILLAGE, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(8).build() + ) + ); + REQUIREMENT_MAP.put( + Quest.SHIELD_OF_ARRAV, + Collections.emptyList() // No requirements + ); + REQUIREMENT_MAP.put( + Quest.FIGHT_ARENA, + Collections.emptyList() // No requirements + ); } /** From 260044624bfe7626342008d36a973dbbd0744f6b Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 18 Aug 2025 15:41:07 -0400 Subject: [PATCH 09/53] pre-reqs are added directly under the related item --- .../goaltracker/utils/QuestRequirements.java | 288 ++++++++++++++---- 1 file changed, 226 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java index 2eac875..ec903e6 100644 --- a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java +++ b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java @@ -21,87 +21,251 @@ public class QuestRequirements static { REQUIREMENT_MAP = new HashMap<>(); REQUIREMENT_MAP.put( - Quest.FAIRYTALE_I__GROWING_PAINS, - Arrays.asList( - QuestTask.builder().quest(Quest.LOST_CITY).build(), - QuestTask.builder().quest(Quest.NATURE_SPIRIT).build(), - SkillLevelTask.builder().skill(Skill.FARMING).level(18).build() - ) + Quest.FAIRYTALE_I__GROWING_PAINS, + Arrays.asList( + QuestTask.builder().quest(Quest.LOST_CITY).build(), + QuestTask.builder().quest(Quest.NATURE_SPIRIT).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(18).build() + ) ); REQUIREMENT_MAP.put( - Quest.NATURE_SPIRIT, - Arrays.asList( - QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), - QuestTask.builder().quest(Quest.THE_RESTLESS_GHOST).build(), - SkillLevelTask.builder().skill(Skill.PRAYER).level(18).build() - ) + Quest.NATURE_SPIRIT, + Arrays.asList( + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), + QuestTask.builder().quest(Quest.THE_RESTLESS_GHOST).build(), + SkillLevelTask.builder().skill(Skill.PRAYER).level(18).build() + ) ); REQUIREMENT_MAP.put( - Quest.LOST_CITY, - Arrays.asList( - SkillLevelTask.builder().skill(Skill.CRAFTING).level(31).build(), - SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(36).build() - ) + Quest.LOST_CITY, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(31).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(36).build() + ) ); REQUIREMENT_MAP.put( - Quest.PRIEST_IN_PERIL, - Collections.emptyList() // No requirements + Quest.DRAGON_SLAYER_I, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.ATTACK).level(32).build(), + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build() + ) ); REQUIREMENT_MAP.put( - Quest.THE_RESTLESS_GHOST, - Collections.emptyList() // No requirements + Quest.HEROES_QUEST, + Arrays.asList( + QuestTask.builder().quest(Quest.DRAGON_SLAYER_I).build(), + QuestTask.builder().quest(Quest.LOST_CITY).build(), + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(48).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(53).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(55).build() + ) ); REQUIREMENT_MAP.put( - Quest.DRAGON_SLAYER_I, - Arrays.asList( - SkillLevelTask.builder().skill(Skill.ATTACK).level(32).build(), - QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build() - ) + Quest.THE_GRAND_TREE, + Arrays.asList( + QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(25).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(25).build() + ) ); REQUIREMENT_MAP.put( - Quest.COOKS_ASSISTANT, - Collections.emptyList() // No requirements + Quest.TREE_GNOME_VILLAGE, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(8).build() + ) ); + + // Additional quest requirements based on OSRS Wiki + // Animal Magnetism requires completion of The Restless Ghost, Ernest the Chicken and Priest in Peril. + // It also requires Slayer 18, Crafting 19, Ranged 30 and Woodcutting 35【816824159430957†L50-L60】. REQUIREMENT_MAP.put( - Quest.DEMON_SLAYER, - Collections.emptyList() // No requirements + Quest.ANIMAL_MAGNETISM, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_RESTLESS_GHOST).build(), + QuestTask.builder().quest(Quest.ERNEST_THE_CHICKEN).build(), + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(18).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(19).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(30).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(35).build() + ) ); + + // Another Slice of H.A.M. requires Death to the Dorgeshuun, The Giant Dwarf and The Dig Site quests + // plus Attack 15 and Prayer 25【497556247310731†L53-L64】. + REQUIREMENT_MAP.put( + Quest.ANOTHER_SLICE_OF_HAM, + Arrays.asList( + QuestTask.builder().quest(Quest.DEATH_TO_THE_DORGESHUUN).build(), + QuestTask.builder().quest(Quest.THE_GIANT_DWARF).build(), + QuestTask.builder().quest(Quest.THE_DIG_SITE).build(), + SkillLevelTask.builder().skill(Skill.ATTACK).level(15).build(), + SkillLevelTask.builder().skill(Skill.PRAYER).level(25).build() + ) + ); + + // The Giant Dwarf requires Crafting 12, Firemaking 16, Magic 33 and Thieving 14【870029561849796†L64-L70】. REQUIREMENT_MAP.put( - Quest.HEROES_QUEST, - Arrays.asList( - QuestTask.builder().quest(Quest.DRAGON_SLAYER_I).build(), - QuestTask.builder().quest(Quest.LOST_CITY).build(), - QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), - SkillLevelTask.builder().skill(Skill.CRAFTING).level(48).build(), - SkillLevelTask.builder().skill(Skill.HERBLORE).level(53).build(), - SkillLevelTask.builder().skill(Skill.SLAYER).level(55).build() - ) + Quest.THE_GIANT_DWARF, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(12).build(), + SkillLevelTask.builder().skill(Skill.FIREMAKING).level(16).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(33).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(14).build() + ) ); + + // The Lost Tribe requires Goblin Diplomacy and Rune Mysteries plus Agility 13, Thieving 13 and Mining 17【123371285222949†L58-L64】. REQUIREMENT_MAP.put( - Quest.MONKS_FRIEND, - Collections.emptyList() // No requirements + Quest.THE_LOST_TRIBE, + Arrays.asList( + QuestTask.builder().quest(Quest.GOBLIN_DIPLOMACY).build(), + QuestTask.builder().quest(Quest.RUNE_MYSTERIES).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(13).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(13).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(17).build() + ) ); + + // Death to the Dorgeshuun requires The Lost Tribe along with Agility 23 and Thieving 23【61594254303929†L61-L67】. REQUIREMENT_MAP.put( - Quest.THE_GRAND_TREE, - Arrays.asList( - QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build(), - SkillLevelTask.builder().skill(Skill.AGILITY).level(25).build(), - SkillLevelTask.builder().skill(Skill.RANGED).level(25).build() - ) + Quest.DEATH_TO_THE_DORGESHUUN, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_LOST_TRIBE).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(23).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(23).build() + ) ); + + + // The Dig Site requires Agility 10, Herblore 10 and Thieving 25【916139208382379†L63-L68】. REQUIREMENT_MAP.put( - Quest.TREE_GNOME_VILLAGE, - Arrays.asList( - SkillLevelTask.builder().skill(Skill.AGILITY).level(8).build() - ) + Quest.THE_DIG_SITE, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(10).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(10).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(25).build() + ) ); + + // Bone Voyage requires completion of The Dig Site and 100 Kudos; no skill levels【366447167732673†L46-L51】. REQUIREMENT_MAP.put( - Quest.SHIELD_OF_ARRAV, - Collections.emptyList() // No requirements + Quest.BONE_VOYAGE, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_DIG_SITE).build() + ) ); + + // Client of Kourend requires X Marks the Spot; no skill requirements【215435182682270†L42-L47】. + REQUIREMENT_MAP.put( + Quest.CLIENT_OF_KOUREND, + Arrays.asList( + QuestTask.builder().quest(Quest.X_MARKS_THE_SPOT).build() + ) + ); + + + // Ghosts Ahoy requires Priest in Peril, The Restless Ghost, Agility 25 and Cooking 20【735691433885456†L54-L58】. + REQUIREMENT_MAP.put( + Quest.GHOSTS_AHOY, + Arrays.asList( + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), + QuestTask.builder().quest(Quest.THE_RESTLESS_GHOST).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(25).build(), + SkillLevelTask.builder().skill(Skill.COOKING).level(20).build() + ) + ); + + // Dream Mentor requires completion of Lunar Diplomacy and Eadgar's Ruse【148132524076550†L48-L60】【282840601330941†L49-L56】. + REQUIREMENT_MAP.put( + Quest.DREAM_MENTOR, + Arrays.asList( + QuestTask.builder().quest(Quest.LUNAR_DIPLOMACY).build(), + QuestTask.builder().quest(Quest.EADGARS_RUSE).build() + ) + ); + + // Lunar Diplomacy requires The Fremennik Trials, Lost City, Rune Mysteries and Shilo Village. + // It also requires multiple skills: Herblore 5, Crafting 61, Defence 40, Firemaking 49, Magic 65, Mining 60 and Woodcutting 55【209799958490204†L50-L67】. + REQUIREMENT_MAP.put( + Quest.LUNAR_DIPLOMACY, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_FREMENNIK_TRIALS).build(), + QuestTask.builder().quest(Quest.LOST_CITY).build(), + QuestTask.builder().quest(Quest.RUNE_MYSTERIES).build(), + QuestTask.builder().quest(Quest.SHILO_VILLAGE).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(5).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(61).build(), + SkillLevelTask.builder().skill(Skill.DEFENCE).level(40).build(), + SkillLevelTask.builder().skill(Skill.FIREMAKING).level(49).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(65).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(60).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(55).build() + ) + ); + + // Eadgar's Ruse requires Druidic Ritual and Troll Stronghold, with Herblore 31【282840601330941†L49-L56】. + REQUIREMENT_MAP.put( + Quest.EADGARS_RUSE, + Arrays.asList( + QuestTask.builder().quest(Quest.DRUIDIC_RITUAL).build(), + QuestTask.builder().quest(Quest.TROLL_STRONGHOLD).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(31).build() + ) + ); + + // Troll Stronghold requires Death Plateau and Agility 15【881893560221627†L45-L49】. + REQUIREMENT_MAP.put( + Quest.TROLL_STRONGHOLD, + Arrays.asList( + QuestTask.builder().quest(Quest.DEATH_PLATEAU).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(15).build() + ) + ); + + // Shilo Village requires Jungle Potion (and thus Druidic Ritual) plus Crafting 20 and Agility 32【397040585224810†L50-L57】. + REQUIREMENT_MAP.put( + Quest.SHILO_VILLAGE, + Arrays.asList( + QuestTask.builder().quest(Quest.JUNGLE_POTION).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(20).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(32).build() + ) + ); + + // Jungle Potion requires Druidic Ritual and Herblore 3【532018769049742†L45-L49】. + REQUIREMENT_MAP.put( + Quest.JUNGLE_POTION, + Arrays.asList( + QuestTask.builder().quest(Quest.DRUIDIC_RITUAL).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(3).build() + ) + ); + + + // Dragon Slayer II requires numerous quests and high skill levels【916074840360349†L82-L92】【916074840360349†L93-L129】. REQUIREMENT_MAP.put( - Quest.FIGHT_ARENA, - Collections.emptyList() // No requirements + Quest.DRAGON_SLAYER_II, + Arrays.asList( + // Quest prerequisites + QuestTask.builder().quest(Quest.LEGENDS_QUEST).build(), + QuestTask.builder().quest(Quest.DREAM_MENTOR).build(), + QuestTask.builder().quest(Quest.A_TAIL_OF_TWO_CATS).build(), + QuestTask.builder().quest(Quest.ANIMAL_MAGNETISM).build(), + QuestTask.builder().quest(Quest.GHOSTS_AHOY).build(), + QuestTask.builder().quest(Quest.BONE_VOYAGE).build(), + QuestTask.builder().quest(Quest.CLIENT_OF_KOUREND).build(), + // Skill requirements + SkillLevelTask.builder().skill(Skill.MAGIC).level(75).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(70).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(68).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(62).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(60).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(60).build(), + SkillLevelTask.builder().skill(Skill.CONSTRUCTION).level(50).build(), + SkillLevelTask.builder().skill(Skill.HITPOINTS).level(50).build() + ) ); } @@ -139,17 +303,17 @@ private static Task copyWithIndent(Task task, int indentLevel) if (task instanceof QuestTask) { QuestTask qt = (QuestTask) task; return QuestTask.builder() - .quest(qt.getQuest()) - .indentLevel(indentLevel) - .build(); + .quest(qt.getQuest()) + .indentLevel(indentLevel) + .build(); } if (task instanceof SkillLevelTask) { SkillLevelTask st = (SkillLevelTask) task; return SkillLevelTask.builder() - .skill(st.getSkill()) - .level(st.getLevel()) - .indentLevel(indentLevel) - .build(); + .skill(st.getSkill()) + .level(st.getLevel()) + .indentLevel(indentLevel) + .build(); } // Fallback: set indent on the same instance (least preferable, but safe for unknown subclasses) task.setIndentLevel(indentLevel); From c3deb59d54243a0bf28ebcfc6a6cc5d91ff241de Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 18 Aug 2025 16:37:23 -0400 Subject: [PATCH 10/53] pre-reqs are added directly under the related item --- .../goaltracker/models/task/ItemTask.java | 3 +- .../goaltracker/utils/QuestRequirements.java | 1448 +++++++++++++++++ 2 files changed, 1450 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/toofifty/goaltracker/models/task/ItemTask.java b/src/main/java/com/toofifty/goaltracker/models/task/ItemTask.java index e002232..448481f 100644 --- a/src/main/java/com/toofifty/goaltracker/models/task/ItemTask.java +++ b/src/main/java/com/toofifty/goaltracker/models/task/ItemTask.java @@ -11,7 +11,8 @@ @Setter @Getter @SuperBuilder -public class ItemTask extends Task +public class +ItemTask extends Task { private transient BufferedImage cachedIcon; private int quantity; diff --git a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java index ec903e6..c2fa0e5 100644 --- a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java +++ b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java @@ -266,6 +266,1454 @@ public class QuestRequirements SkillLevelTask.builder().skill(Skill.CONSTRUCTION).level(50).build(), SkillLevelTask.builder().skill(Skill.HITPOINTS).level(50).build() ) + + ); + REQUIREMENT_MAP.put( + Quest.BIOHAZARD, + Arrays.asList( + QuestTask.builder().quest(Quest.PLAGUE_CITY).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.BLACK_KNIGHTS_FORTRESS, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.CLOCK_TOWER, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.COOKS_ASSISTANT, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.DEATH_PLATEAU, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.DEMON_SLAYER, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.DORICS_QUEST, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.DRUIDIC_RITUAL, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.DWARF_CANNON, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.EAGLES_PEAK, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.HUNTER).level(27).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.ELEMENTAL_WORKSHOP_I, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.MINING).level(20).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(20).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(20).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.ERNEST_THE_CHICKEN, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.FISHING_CONTEST, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.FISHING).level(10).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.GERTRUDES_CAT, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.GOBLIN_DIPLOMACY, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.HAZEEL_CULT, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.IMP_CATCHER, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.JUNGLE_POTION, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.MONKS_FRIEND, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.MURDER_MYSTERY, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.NATURE_SPIRIT, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.OBSERVATORY_QUEST, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(10).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.PIRATES_TREASURE, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.PLAGUE_CITY, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.PRIEST_IN_PERIL, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.PRINCE_ALI_RESCUE, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.RAG_AND_BONE_MAN_I, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.RECRUITMENT_DRIVE, + Arrays.asList( + QuestTask.builder().quest(Quest.BLACK_KNIGHTS_FORTRESS).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_RESTLESS_GHOST, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.ROMEO__JULIET, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.RUNE_MYSTERIES, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.SHEEP_HERDER, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.SHEEP_SHEARER, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.SHIELD_OF_ARRAV, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.A_SOULS_BANE, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.TOWER_OF_LIFE, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CONSTRUCTION).level(10).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.VAMPYRE_SLAYER, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.WITCHS_POTION, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.ANIMAL_MAGNETISM, + Arrays.asList( + QuestTask.builder().quest(Quest.ERNEST_THE_CHICKEN).build(), + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), + QuestTask.builder().quest(Quest.THE_RESTLESS_GHOST).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(18).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(30).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(19).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(35).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.ANOTHER_SLICE_OF_HAM, + Arrays.asList( + QuestTask.builder().quest(Quest.DEATH_TO_THE_DORGESHUUN).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.BIG_CHOMPY_BIRD_HUNTING, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.COOKING).level(30).build(), + SkillLevelTask.builder().skill(Skill.FLETCHING).level(5).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(30).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.COLD_WAR, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(30).build(), + SkillLevelTask.builder().skill(Skill.CONSTRUCTION).level(34).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(30).build(), + SkillLevelTask.builder().skill(Skill.HUNTER).level(10).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(15).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.CREATURE_OF_FENKENSTRAIN, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(20).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(25).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.DARKNESS_OF_HALLOWVALE, + Arrays.asList( + QuestTask.builder().quest(Quest.IN_AID_OF_THE_MYREQUE).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(26).build(), + SkillLevelTask.builder().skill(Skill.CONSTRUCTION).level(5).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(32).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(33).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(20).build(), + SkillLevelTask.builder().skill(Skill.STRENGTH).level(40).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(22).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.DEATH_TO_THE_DORGESHUUN, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_LOST_TRIBE).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(23).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(17).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(23).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_DIG_SITE, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(10).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(10).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(25).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.ELEMENTAL_WORKSHOP_II, + Arrays.asList( + QuestTask.builder().quest(Quest.ELEMENTAL_WORKSHOP_I).build(), + SkillLevelTask.builder().skill(Skill.DEFENCE).level(20).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(20).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(20).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(30).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.ENLIGHTENED_JOURNEY, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(36).build(), + SkillLevelTask.builder().skill(Skill.FIREMAKING).level(20).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(30).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_EYES_OF_GLOUPHRIE, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_GRAND_TREE).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(46).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(5).build(), + SkillLevelTask.builder().skill(Skill.CONSTRUCTION).level(5).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_FEUD, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.THIEVING).level(30).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.FORGETTABLE_TALE, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_GIANT_DWARF).build(), + SkillLevelTask.builder().skill(Skill.COOKING).level(22).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(12).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(17).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(16).build(), + SkillLevelTask.builder().skill(Skill.FISHING).level(10).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(33).build(), + SkillLevelTask.builder().skill(Skill.STRENGTH).level(14).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_FREMENNIK_TRIALS, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.GARDEN_OF_TRANQUILLITY, + Arrays.asList( + QuestTask.builder().quest(Quest.CREATURE_OF_FENKENSTRAIN).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(25).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(20).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(25).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.GHOSTS_AHOY, + Arrays.asList( + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(25).build(), + SkillLevelTask.builder().skill(Skill.COOKING).level(20).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_GIANT_DWARF, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(12).build(), + SkillLevelTask.builder().skill(Skill.FIREMAKING).level(16).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(33).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(14).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_GOLEM, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.THIEVING).level(20).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(25).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_HAND_IN_THE_SAND, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(49).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(17).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.HOLY_GRAIL, + Arrays.asList( + QuestTask.builder().quest(Quest.MERLINS_CRYSTAL).build(), + SkillLevelTask.builder().skill(Skill.ATTACK).level(20).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.ICTHLARINS_LITTLE_HELPER, + Arrays.asList( + QuestTask.builder().quest(Quest.PRINCE_ALI_RESCUE).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.IN_AID_OF_THE_MYREQUE, + Arrays.asList( + QuestTask.builder().quest(Quest.IN_SEARCH_OF_THE_MYREQUE).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(25).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(25).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(7).build(), + SkillLevelTask.builder().skill(Skill.FISHING).level(15).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.IN_SEARCH_OF_THE_MYREQUE, + Arrays.asList( + QuestTask.builder().quest(Quest.NATURE_SPIRIT).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(25).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_KNIGHTS_SWORD, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.MINING).level(10).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_LOST_TRIBE, + Arrays.asList( + QuestTask.builder().quest(Quest.GOBLIN_DIPLOMACY).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(13).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(17).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(13).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.MAKING_HISTORY, + Arrays.asList( + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.MERLINS_CRYSTAL, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.MOUNTAIN_DAUGHTER, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(20).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.MY_ARMS_BIG_ADVENTURE, + Arrays.asList( + QuestTask.builder().quest(Quest.JUNGLE_POTION).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(15).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(29).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(31).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(30).build(), + SkillLevelTask.builder().skill(Skill.COOKING).level(10).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.OLAFS_QUEST, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(40).build(), + SkillLevelTask.builder().skill(Skill.FIREMAKING).level(50).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.RATCATCHERS, + Arrays.asList( + QuestTask.builder().quest(Quest.ICTHLARINS_LITTLE_HELPER).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(12).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(16).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(33).build(), + SkillLevelTask.builder().skill(Skill.HUNTER).level(14).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SCORPION_CATCHER, + Arrays.asList( + QuestTask.builder().quest(Quest.ALFRED_GRIMHANDS_BARCRAWL).build(), + SkillLevelTask.builder().skill(Skill.PRAYER).level(31).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SEA_SLUG, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.FIREMAKING).level(30).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SHADES_OF_MORTTON, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(20).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(5).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(15).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SHADOW_OF_THE_STORM, + Arrays.asList( + QuestTask.builder().quest(Quest.DEMON_SLAYER).build(), + QuestTask.builder().quest(Quest.THE_GOLEM).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(30).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(25).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_SLUG_MENACE, + Arrays.asList( + QuestTask.builder().quest(Quest.WANTED).build(), + SkillLevelTask.builder().skill(Skill.RUNECRAFT).level(13).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(30).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(30).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(17).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(30).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SPIRITS_OF_THE_ELID, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.MAGIC).level(33).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(37).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(37).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(37).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.TAI_BWO_WANNAI_TRIO, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(15).build(), + SkillLevelTask.builder().skill(Skill.COOKING).level(30).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(5).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.A_TAIL_OF_TWO_CATS, + Arrays.asList( + QuestTask.builder().quest(Quest.ICTHLARINS_LITTLE_HELPER).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.TEARS_OF_GUTHIX, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(20).build(), + SkillLevelTask.builder().skill(Skill.FIREMAKING).level(49).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(20).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_TOURIST_TRAP, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.FLETCHING).level(10).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(20).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.TREE_GNOME_VILLAGE, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.TRIBAL_TOTEM, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.THIEVING).level(21).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.WANTED, + Arrays.asList( + QuestTask.builder().quest(Quest.RECRUITMENT_DRIVE).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(13).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(17).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(13).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.WATCHTOWER, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(25).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(14).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(15).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(40).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(15).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.WATERFALL_QUEST, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.WHAT_LIES_BELOW, + Arrays.asList( + QuestTask.builder().quest(Quest.RUNE_MYSTERIES).build(), + SkillLevelTask.builder().skill(Skill.RUNECRAFT).level(35).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.WITCHS_HOUSE, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.ZOGRE_FLESH_EATERS, + Arrays.asList( + QuestTask.builder().quest(Quest.JUNGLE_POTION).build(), + QuestTask.builder().quest(Quest.BIG_CHOMPY_BIRD_HUNTING).build(), + SkillLevelTask.builder().skill(Skill.FLETCHING).level(30).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(8).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(4).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(30).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.BETWEEN_A_ROCK, + Arrays.asList( + QuestTask.builder().quest(Quest.DWARF_CANNON).build(), + SkillLevelTask.builder().skill(Skill.DEFENCE).level(30).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(10).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(40).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(50).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.CABIN_FEVER, + Arrays.asList( + QuestTask.builder().quest(Quest.RUM_DEAL).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(42).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(30).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(45).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(40).build(), + SkillLevelTask.builder().skill(Skill.COOKING).level(50).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.DEVIOUS_MINDS, + Arrays.asList( + QuestTask.builder().quest(Quest.WANTED).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(50).build(), + SkillLevelTask.builder().skill(Skill.RUNECRAFT).level(50).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(65).build(), + SkillLevelTask.builder().skill(Skill.FLETCHING).level(50).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.DRAGON_SLAYER_I, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.EADGARS_RUSE, + Arrays.asList( + QuestTask.builder().quest(Quest.DRUIDIC_RITUAL).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(31).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.ENAKHRAS_LAMENT, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(50).build(), + SkillLevelTask.builder().skill(Skill.FIREMAKING).level(45).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(39).build(), + SkillLevelTask.builder().skill(Skill.PRAYER).level(43).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.FAIRYTALE_I__GROWING_PAINS, + Arrays.asList( + QuestTask.builder().quest(Quest.LOST_CITY).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(30).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(36).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.FAIRYTALE_II__CURE_A_QUEEN, + Arrays.asList( + QuestTask.builder().quest(Quest.FAIRYTALE_I__GROWING_PAINS).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(40).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(49).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(57).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(37).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.FAMILY_CREST, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.MINING).level(40).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(59).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(40).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(40).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.FIGHT_ARENA, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_FREMENNIK_ISLES, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_FREMENNIK_TRIALS).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(40).build(), + SkillLevelTask.builder().skill(Skill.CONSTRUCTION).level(20).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_GRAND_TREE, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(25).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_GREAT_BRAIN_ROBBERY, + Arrays.asList( + QuestTask.builder().quest(Quest.CABIN_FEVER).build(), + SkillLevelTask.builder().skill(Skill.PRAYER).level(50).build(), + SkillLevelTask.builder().skill(Skill.CONSTRUCTION).level(30).build(), + SkillLevelTask.builder().skill(Skill.COOKING).level(31).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(45).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.HAUNTED_MINE, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(15).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(35).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.HEROES_QUEST, + Arrays.asList( + QuestTask.builder().quest(Quest.SHIELD_OF_ARRAV).build(), + QuestTask.builder().quest(Quest.DRAGON_SLAYER_I).build(), + QuestTask.builder().quest(Quest.MERLINS_CRYSTAL).build(), + QuestTask.builder().quest(Quest.LOST_CITY).build(), + SkillLevelTask.builder().skill(Skill.COOKING).level(53).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(25).build(), + SkillLevelTask.builder().skill(Skill.FISHING).level(53).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(25).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(50).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.HORROR_FROM_THE_DEEP, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(35).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.KINGS_RANSOM, + Arrays.asList( + QuestTask.builder().quest(Quest.BLACK_KNIGHTS_FORTRESS).build(), + QuestTask.builder().quest(Quest.HOLY_GRAIL).build(), + QuestTask.builder().quest(Quest.MURDER_MYSTERY).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(65).build(), + SkillLevelTask.builder().skill(Skill.DEFENCE).level(45).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(30).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(18).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(45).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.LOST_CITY, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.CRAFTING).level(31).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(36).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.LUNAR_DIPLOMACY, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_FREMENNIK_TRIALS).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(55).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(60).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(5).build(), + SkillLevelTask.builder().skill(Skill.FIREMAKING).level(49).build(), + SkillLevelTask.builder().skill(Skill.DEFENCE).level(40).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(61).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(32).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.ONE_SMALL_FAVOUR, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(36).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(25).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(18).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(30).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.ROYAL_TROUBLE, + Arrays.asList( + QuestTask.builder().quest(Quest.THRONE_OF_MISCELLANIA).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(40).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(40).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(53).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.RUM_DEAL, + Arrays.asList( + QuestTask.builder().quest(Quest.ZOGRE_FLESH_EATERS).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(30).build(), + SkillLevelTask.builder().skill(Skill.PRAYER).level(47).build(), + SkillLevelTask.builder().skill(Skill.FISHING).level(50).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(40).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SHILO_VILLAGE, + Arrays.asList( + QuestTask.builder().quest(Quest.JUNGLE_POTION).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(32).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(20).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.TEMPLE_OF_IKOV, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.RANGED).level(40).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(42).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THRONE_OF_MISCELLANIA, + Arrays.asList( + QuestTask.builder().quest(Quest.HEROES_QUEST).build(), + SkillLevelTask.builder().skill(Skill.COOKING).level(45).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(40).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.TROLL_ROMANCE, + Arrays.asList( + QuestTask.builder().quest(Quest.TROLL_STRONGHOLD).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(28).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.TROLL_STRONGHOLD, + Arrays.asList( + QuestTask.builder().quest(Quest.DEATH_PLATEAU).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(15).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.UNDERGROUND_PASS, + Arrays.asList( + QuestTask.builder().quest(Quest.BIOHAZARD).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(25).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.CONTACT, + Arrays.asList( + QuestTask.builder().quest(Quest.PRINCE_ALI_RESCUE).build(), + QuestTask.builder().quest(Quest.ICTHLARINS_LITTLE_HELPER).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.DESERT_TREASURE_I, + Arrays.asList( + QuestTask.builder().quest(Quest.TEMPLE_OF_IKOV).build(), + QuestTask.builder().quest(Quest.THE_DIG_SITE).build(), + QuestTask.builder().quest(Quest.TROLL_STRONGHOLD).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(53).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(50).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(10).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(10).build(), + SkillLevelTask.builder().skill(Skill.FIREMAKING).level(50).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.DREAM_MENTOR, + Arrays.asList( + QuestTask.builder().quest(Quest.LUNAR_DIPLOMACY).build(), + SkillLevelTask.builder().skill(Skill.DEFENCE).level(85).build(), + SkillLevelTask.builder().skill(Skill.HITPOINTS).level(65).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(55).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(60).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(45).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(40).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(49).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.GRIM_TALES, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.AGILITY).level(59).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(52).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(45).build(), + SkillLevelTask.builder().skill(Skill.FISHING).level(58).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(71).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.LEGENDS_QUEST, + Arrays.asList( + QuestTask.builder().quest(Quest.FAMILY_CREST).build(), + QuestTask.builder().quest(Quest.SHILO_VILLAGE).build(), + QuestTask.builder().quest(Quest.HEROES_QUEST).build(), + QuestTask.builder().quest(Quest.UNDERGROUND_PASS).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(50).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(50).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(50).build(), + SkillLevelTask.builder().skill(Skill.FISHING).level(50).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(52).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(50).build(), + SkillLevelTask.builder().skill(Skill.STRENGTH).level(50).build(), + SkillLevelTask.builder().skill(Skill.PRAYER).level(42).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(50).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.MONKEY_MADNESS_I, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_GRAND_TREE).build(), + QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(0).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.MOURNINGS_END_PART_I, + Arrays.asList( + QuestTask.builder().quest(Quest.ROVING_ELVES).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(60).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(50).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(0).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.MOURNINGS_END_PART_II, + Arrays.asList( + QuestTask.builder().quest(Quest.MOURNINGS_END_PART_I).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(60).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.RECIPE_FOR_DISASTER, + Arrays.asList( + QuestTask.builder().quest(Quest.COOKS_ASSISTANT).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.REGICIDE, + Arrays.asList( + QuestTask.builder().quest(Quest.UNDERGROUND_PASS).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(56).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(10).build(), + SkillLevelTask.builder().skill(Skill.FLETCHING).level(25).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.ROVING_ELVES, + Arrays.asList( + QuestTask.builder().quest(Quest.REGICIDE).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(56).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(10).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(25).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SWAN_SONG, + Arrays.asList( + QuestTask.builder().quest(Quest.ONE_SMALL_FAVOUR).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(62).build(), + SkillLevelTask.builder().skill(Skill.COOKING).level(66).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(42).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(45).build(), + SkillLevelTask.builder().skill(Skill.FISHING).level(62).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(18).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.DRAGON_SLAYER_II, + Arrays.asList( + QuestTask.builder().quest(Quest.LEGENDS_QUEST).build(), + QuestTask.builder().quest(Quest.A_TAIL_OF_TWO_CATS).build(), + QuestTask.builder().quest(Quest.DREAM_MENTOR).build(), + QuestTask.builder().quest(Quest.BONE_VOYAGE).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(60).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(50).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(60).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(68).build(), + SkillLevelTask.builder().skill(Skill.HUNTER).level(60).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(60).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(75).build(), + SkillLevelTask.builder().skill(Skill.HITPOINTS).level(0).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.MONKEY_MADNESS_II, + Arrays.asList( + QuestTask.builder().quest(Quest.ENLIGHTENED_JOURNEY).build(), + QuestTask.builder().quest(Quest.THE_EYES_OF_GLOUPHRIE).build(), + QuestTask.builder().quest(Quest.THE_GRAND_TREE).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(69).build(), + SkillLevelTask.builder().skill(Skill.HUNTER).level(60).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(55).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(70).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(70).build(), + SkillLevelTask.builder().skill(Skill.FLETCHING).level(0).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SONG_OF_THE_ELVES, + Arrays.asList( + QuestTask.builder().quest(Quest.MOURNINGS_END_PART_II).build(), + QuestTask.builder().quest(Quest.MAKING_HISTORY).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(70).build(), + SkillLevelTask.builder().skill(Skill.CONSTRUCTION).level(70).build(), + SkillLevelTask.builder().skill(Skill.FARMING).level(70).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(70).build(), + SkillLevelTask.builder().skill(Skill.HUNTER).level(70).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(70).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(70).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(70).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SINS_OF_THE_FATHER, + Arrays.asList( + QuestTask.builder().quest(Quest.A_TASTE_OF_HOPE).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(62).build(), + SkillLevelTask.builder().skill(Skill.FLETCHING).level(60).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(56).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(52).build(), + SkillLevelTask.builder().skill(Skill.ATTACK).level(50).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(50).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(49).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.A_TASTE_OF_HOPE, + Arrays.asList( + QuestTask.builder().quest(Quest.DARKNESS_OF_HALLOWVALE).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.A_KINGDOM_DIVIDED, + Arrays.asList( + QuestTask.builder().quest(Quest.CLIENT_OF_KOUREND).build(), + QuestTask.builder().quest(Quest.X_MARKS_THE_SPOT).build(), + QuestTask.builder().quest(Quest.THE_DEPTHS_OF_DESPAIR).build(), + QuestTask.builder().quest(Quest.THE_QUEEN_OF_THIEVES).build(), + QuestTask.builder().quest(Quest.THE_ASCENT_OF_ARCEUUS).build(), + QuestTask.builder().quest(Quest.THE_FORSAKEN_TOWER).build(), + QuestTask.builder().quest(Quest.TALE_OF_THE_RIGHTEOUS).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(54).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(52).build(), + SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(52).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(50).build(), + SkillLevelTask.builder().skill(Skill.MINING).level(42).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(38).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(35).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.A_NIGHT_AT_THE_THEATRE, + Arrays.asList( + QuestTask.builder().quest(Quest.A_TASTE_OF_HOPE).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.BELOW_ICE_MOUNTAIN, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.A_PORCINE_OF_INTEREST, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.GETTING_AHEAD, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.MISTHALIN_MYSTERY, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_FREMENNIK_EXILES, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_FREMENNIK_ISLES).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(65).build(), + SkillLevelTask.builder().skill(Skill.SMITHING).level(60).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(60).build(), + SkillLevelTask.builder().skill(Skill.FISHING).level(60).build(), + SkillLevelTask.builder().skill(Skill.RUNECRAFT).level(55).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.BONE_VOYAGE, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_DIG_SITE).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SLEEPING_GIANTS, + Arrays.asList( + SkillLevelTask.builder().skill(Skill.SMITHING).level(15).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.TEMPLE_OF_THE_EYE, + Arrays.asList( + QuestTask.builder().quest(Quest.RUNE_MYSTERIES).build(), + SkillLevelTask.builder().skill(Skill.RUNECRAFT).level(10).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_GARDEN_OF_DEATH, + Arrays.asList( + + ) + ); + + REQUIREMENT_MAP.put( + Quest.LAND_OF_THE_GOBLINS, + Arrays.asList( + QuestTask.builder().quest(Quest.ANOTHER_SLICE_OF_HAM).build(), + SkillLevelTask.builder().skill(Skill.PRAYER).level(30).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(36).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(36).build(), + SkillLevelTask.builder().skill(Skill.HERBLORE).level(37).build(), + SkillLevelTask.builder().skill(Skill.FISHING).level(36).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.BENEATH_CURSED_SANDS, + Arrays.asList( + QuestTask.builder().quest(Quest.CONTACT).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(62).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(55).build(), + SkillLevelTask.builder().skill(Skill.FIREMAKING).level(55).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SECRETS_OF_THE_NORTH, + Arrays.asList( + QuestTask.builder().quest(Quest.MAKING_HISTORY).build(), + QuestTask.builder().quest(Quest.HAZEEL_CULT).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(69).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(64).build(), + SkillLevelTask.builder().skill(Skill.HUNTER).level(56).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.DESERT_TREASURE_II__THE_FALLEN_EMPIRE, + Arrays.asList( + QuestTask.builder().quest(Quest.DESERT_TREASURE_I).build(), + QuestTask.builder().quest(Quest.SECRETS_OF_THE_NORTH).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.TWILIGHTS_PROMISE, + Arrays.asList( + QuestTask.builder().quest(Quest.CHILDREN_OF_THE_SUN).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.AT_FIRST_LIGHT, + Arrays.asList( + QuestTask.builder().quest(Quest.TWILIGHTS_PROMISE).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.PERILOUS_MOONS, + Arrays.asList( + QuestTask.builder().quest(Quest.TWILIGHTS_PROMISE).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_RIBBITING_TALE_OF_A_LILY_PAD_LABOUR_DISPUTE, + Arrays.asList( + QuestTask.builder().quest(Quest.TWILIGHTS_PROMISE).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_HEART_OF_DARKNESS, + Arrays.asList( + QuestTask.builder().quest(Quest.TWILIGHTS_PROMISE).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.DEATH_ON_THE_ISLE, + Arrays.asList( + QuestTask.builder().quest(Quest.TWILIGHTS_PROMISE).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.MEAT_AND_GREET, + Arrays.asList( + QuestTask.builder().quest(Quest.TWILIGHTS_PROMISE).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.ETHICALLY_ACQUIRED_ANTIQUITIES, + Arrays.asList( + QuestTask.builder().quest(Quest.TWILIGHTS_PROMISE).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.DEFENDER_OF_VARROCK, + Arrays.asList( + QuestTask.builder().quest(Quest.SHIELD_OF_ARRAV).build(), + QuestTask.builder().quest(Quest.ROMEO__JULIET).build(), + QuestTask.builder().quest(Quest.THE_KNIGHTS_SWORD).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_CURSE_OF_ARRAV, + Arrays.asList( + QuestTask.builder().quest(Quest.DEFENDER_OF_VARROCK).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.WHILE_GUTHIX_SLEEPS, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_CURSE_OF_ARRAV).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.THE_FINAL_DAWN, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_HEART_OF_DARKNESS).build(), + QuestTask.builder().quest(Quest.PERILOUS_MOONS).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(66).build(), + SkillLevelTask.builder().skill(Skill.FLETCHING).level(52).build(), + SkillLevelTask.builder().skill(Skill.RUNECRAFT).level(52).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SHADOWS_OF_CUSTODIA, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_FINAL_DAWN).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.SCRAMBLED, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_FINAL_DAWN).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.AN_EXISTENTIAL_CRISIS, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_FINAL_DAWN).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.IMPENDING_CHAOS, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_FINAL_DAWN).build() + ) + ); + + REQUIREMENT_MAP.put( + Quest.VALE_TOTEMS, + Arrays.asList( + QuestTask.builder().quest(Quest.THE_FINAL_DAWN).build() + ) ); } From 42b2c99d5a943be545573c403673e753f68b3578 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 18 Aug 2025 16:40:58 -0400 Subject: [PATCH 11/53] pre-reqs are added directly under the related item --- .../goaltracker/utils/QuestRequirements.java | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java index c2fa0e5..cb5b5a1 100644 --- a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java +++ b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java @@ -1321,8 +1321,7 @@ public class QuestRequirements Quest.MONKEY_MADNESS_I, Arrays.asList( QuestTask.builder().quest(Quest.THE_GRAND_TREE).build(), - QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build(), - SkillLevelTask.builder().skill(Skill.AGILITY).level(0).build() + QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build() ) ); @@ -1331,8 +1330,7 @@ public class QuestRequirements Arrays.asList( QuestTask.builder().quest(Quest.ROVING_ELVES).build(), SkillLevelTask.builder().skill(Skill.AGILITY).level(60).build(), - SkillLevelTask.builder().skill(Skill.THIEVING).level(50).build(), - SkillLevelTask.builder().skill(Skill.SLAYER).level(0).build() + SkillLevelTask.builder().skill(Skill.THIEVING).level(50).build() ) ); @@ -1397,8 +1395,7 @@ public class QuestRequirements SkillLevelTask.builder().skill(Skill.SMITHING).level(68).build(), SkillLevelTask.builder().skill(Skill.HUNTER).level(60).build(), SkillLevelTask.builder().skill(Skill.THIEVING).level(60).build(), - SkillLevelTask.builder().skill(Skill.MAGIC).level(75).build(), - SkillLevelTask.builder().skill(Skill.HITPOINTS).level(0).build() + SkillLevelTask.builder().skill(Skill.MAGIC).level(75).build() ) ); @@ -1412,8 +1409,7 @@ public class QuestRequirements SkillLevelTask.builder().skill(Skill.HUNTER).level(60).build(), SkillLevelTask.builder().skill(Skill.THIEVING).level(55).build(), SkillLevelTask.builder().skill(Skill.AGILITY).level(70).build(), - SkillLevelTask.builder().skill(Skill.CRAFTING).level(70).build(), - SkillLevelTask.builder().skill(Skill.FLETCHING).level(0).build() + SkillLevelTask.builder().skill(Skill.CRAFTING).level(70).build() ) ); From eb6a9ec907e6a244b76ba4273b4124da69aef43f Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 18 Aug 2025 16:59:39 -0400 Subject: [PATCH 12/53] pre-reqs are added directly under the related item --- .../goaltracker/utils/QuestRequirements.java | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java index cb5b5a1..ae1a92e 100644 --- a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java +++ b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java @@ -186,25 +186,6 @@ public class QuestRequirements ) ); - // Lunar Diplomacy requires The Fremennik Trials, Lost City, Rune Mysteries and Shilo Village. - // It also requires multiple skills: Herblore 5, Crafting 61, Defence 40, Firemaking 49, Magic 65, Mining 60 and Woodcutting 55【209799958490204†L50-L67】. - REQUIREMENT_MAP.put( - Quest.LUNAR_DIPLOMACY, - Arrays.asList( - QuestTask.builder().quest(Quest.THE_FREMENNIK_TRIALS).build(), - QuestTask.builder().quest(Quest.LOST_CITY).build(), - QuestTask.builder().quest(Quest.RUNE_MYSTERIES).build(), - QuestTask.builder().quest(Quest.SHILO_VILLAGE).build(), - SkillLevelTask.builder().skill(Skill.HERBLORE).level(5).build(), - SkillLevelTask.builder().skill(Skill.CRAFTING).level(61).build(), - SkillLevelTask.builder().skill(Skill.DEFENCE).level(40).build(), - SkillLevelTask.builder().skill(Skill.FIREMAKING).level(49).build(), - SkillLevelTask.builder().skill(Skill.MAGIC).level(65).build(), - SkillLevelTask.builder().skill(Skill.MINING).level(60).build(), - SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(55).build() - ) - ); - // Eadgar's Ruse requires Druidic Ritual and Troll Stronghold, with Herblore 31【282840601330941†L49-L56】. REQUIREMENT_MAP.put( Quest.EADGARS_RUSE, @@ -389,13 +370,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.JUNGLE_POTION, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.MONKS_FRIEND, Arrays.asList( From 1503c3c7711891c38b079a6cec63f7589f563ebe Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 18 Aug 2025 17:33:33 -0400 Subject: [PATCH 13/53] pre-reqs are added directly under the related item --- .../goaltracker/utils/QuestRequirements.java | 502 +----------------- 1 file changed, 1 insertion(+), 501 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java index ae1a92e..b468856 100644 --- a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java +++ b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java @@ -36,13 +36,6 @@ public class QuestRequirements SkillLevelTask.builder().skill(Skill.PRAYER).level(18).build() ) ); - REQUIREMENT_MAP.put( - Quest.LOST_CITY, - Arrays.asList( - SkillLevelTask.builder().skill(Skill.CRAFTING).level(31).build(), - SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(36).build() - ) - ); REQUIREMENT_MAP.put( Quest.DRAGON_SLAYER_I, Arrays.asList( @@ -50,171 +43,6 @@ public class QuestRequirements QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build() ) ); - REQUIREMENT_MAP.put( - Quest.HEROES_QUEST, - Arrays.asList( - QuestTask.builder().quest(Quest.DRAGON_SLAYER_I).build(), - QuestTask.builder().quest(Quest.LOST_CITY).build(), - QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), - SkillLevelTask.builder().skill(Skill.CRAFTING).level(48).build(), - SkillLevelTask.builder().skill(Skill.HERBLORE).level(53).build(), - SkillLevelTask.builder().skill(Skill.SLAYER).level(55).build() - ) - ); - REQUIREMENT_MAP.put( - Quest.THE_GRAND_TREE, - Arrays.asList( - QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build(), - SkillLevelTask.builder().skill(Skill.AGILITY).level(25).build(), - SkillLevelTask.builder().skill(Skill.RANGED).level(25).build() - ) - ); - REQUIREMENT_MAP.put( - Quest.TREE_GNOME_VILLAGE, - Arrays.asList( - SkillLevelTask.builder().skill(Skill.AGILITY).level(8).build() - ) - ); - - // Additional quest requirements based on OSRS Wiki - // Animal Magnetism requires completion of The Restless Ghost, Ernest the Chicken and Priest in Peril. - // It also requires Slayer 18, Crafting 19, Ranged 30 and Woodcutting 35【816824159430957†L50-L60】. - REQUIREMENT_MAP.put( - Quest.ANIMAL_MAGNETISM, - Arrays.asList( - QuestTask.builder().quest(Quest.THE_RESTLESS_GHOST).build(), - QuestTask.builder().quest(Quest.ERNEST_THE_CHICKEN).build(), - QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), - SkillLevelTask.builder().skill(Skill.SLAYER).level(18).build(), - SkillLevelTask.builder().skill(Skill.CRAFTING).level(19).build(), - SkillLevelTask.builder().skill(Skill.RANGED).level(30).build(), - SkillLevelTask.builder().skill(Skill.WOODCUTTING).level(35).build() - ) - ); - - // Another Slice of H.A.M. requires Death to the Dorgeshuun, The Giant Dwarf and The Dig Site quests - // plus Attack 15 and Prayer 25【497556247310731†L53-L64】. - REQUIREMENT_MAP.put( - Quest.ANOTHER_SLICE_OF_HAM, - Arrays.asList( - QuestTask.builder().quest(Quest.DEATH_TO_THE_DORGESHUUN).build(), - QuestTask.builder().quest(Quest.THE_GIANT_DWARF).build(), - QuestTask.builder().quest(Quest.THE_DIG_SITE).build(), - SkillLevelTask.builder().skill(Skill.ATTACK).level(15).build(), - SkillLevelTask.builder().skill(Skill.PRAYER).level(25).build() - ) - ); - - // The Giant Dwarf requires Crafting 12, Firemaking 16, Magic 33 and Thieving 14【870029561849796†L64-L70】. - REQUIREMENT_MAP.put( - Quest.THE_GIANT_DWARF, - Arrays.asList( - SkillLevelTask.builder().skill(Skill.CRAFTING).level(12).build(), - SkillLevelTask.builder().skill(Skill.FIREMAKING).level(16).build(), - SkillLevelTask.builder().skill(Skill.MAGIC).level(33).build(), - SkillLevelTask.builder().skill(Skill.THIEVING).level(14).build() - ) - ); - - // The Lost Tribe requires Goblin Diplomacy and Rune Mysteries plus Agility 13, Thieving 13 and Mining 17【123371285222949†L58-L64】. - REQUIREMENT_MAP.put( - Quest.THE_LOST_TRIBE, - Arrays.asList( - QuestTask.builder().quest(Quest.GOBLIN_DIPLOMACY).build(), - QuestTask.builder().quest(Quest.RUNE_MYSTERIES).build(), - SkillLevelTask.builder().skill(Skill.AGILITY).level(13).build(), - SkillLevelTask.builder().skill(Skill.THIEVING).level(13).build(), - SkillLevelTask.builder().skill(Skill.MINING).level(17).build() - ) - ); - - // Death to the Dorgeshuun requires The Lost Tribe along with Agility 23 and Thieving 23【61594254303929†L61-L67】. - REQUIREMENT_MAP.put( - Quest.DEATH_TO_THE_DORGESHUUN, - Arrays.asList( - QuestTask.builder().quest(Quest.THE_LOST_TRIBE).build(), - SkillLevelTask.builder().skill(Skill.AGILITY).level(23).build(), - SkillLevelTask.builder().skill(Skill.THIEVING).level(23).build() - ) - ); - - - // The Dig Site requires Agility 10, Herblore 10 and Thieving 25【916139208382379†L63-L68】. - REQUIREMENT_MAP.put( - Quest.THE_DIG_SITE, - Arrays.asList( - SkillLevelTask.builder().skill(Skill.AGILITY).level(10).build(), - SkillLevelTask.builder().skill(Skill.HERBLORE).level(10).build(), - SkillLevelTask.builder().skill(Skill.THIEVING).level(25).build() - ) - ); - - // Bone Voyage requires completion of The Dig Site and 100 Kudos; no skill levels【366447167732673†L46-L51】. - REQUIREMENT_MAP.put( - Quest.BONE_VOYAGE, - Arrays.asList( - QuestTask.builder().quest(Quest.THE_DIG_SITE).build() - ) - ); - - // Client of Kourend requires X Marks the Spot; no skill requirements【215435182682270†L42-L47】. - REQUIREMENT_MAP.put( - Quest.CLIENT_OF_KOUREND, - Arrays.asList( - QuestTask.builder().quest(Quest.X_MARKS_THE_SPOT).build() - ) - ); - - - // Ghosts Ahoy requires Priest in Peril, The Restless Ghost, Agility 25 and Cooking 20【735691433885456†L54-L58】. - REQUIREMENT_MAP.put( - Quest.GHOSTS_AHOY, - Arrays.asList( - QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), - QuestTask.builder().quest(Quest.THE_RESTLESS_GHOST).build(), - SkillLevelTask.builder().skill(Skill.AGILITY).level(25).build(), - SkillLevelTask.builder().skill(Skill.COOKING).level(20).build() - ) - ); - - // Dream Mentor requires completion of Lunar Diplomacy and Eadgar's Ruse【148132524076550†L48-L60】【282840601330941†L49-L56】. - REQUIREMENT_MAP.put( - Quest.DREAM_MENTOR, - Arrays.asList( - QuestTask.builder().quest(Quest.LUNAR_DIPLOMACY).build(), - QuestTask.builder().quest(Quest.EADGARS_RUSE).build() - ) - ); - - // Eadgar's Ruse requires Druidic Ritual and Troll Stronghold, with Herblore 31【282840601330941†L49-L56】. - REQUIREMENT_MAP.put( - Quest.EADGARS_RUSE, - Arrays.asList( - QuestTask.builder().quest(Quest.DRUIDIC_RITUAL).build(), - QuestTask.builder().quest(Quest.TROLL_STRONGHOLD).build(), - SkillLevelTask.builder().skill(Skill.HERBLORE).level(31).build() - ) - ); - - // Troll Stronghold requires Death Plateau and Agility 15【881893560221627†L45-L49】. - REQUIREMENT_MAP.put( - Quest.TROLL_STRONGHOLD, - Arrays.asList( - QuestTask.builder().quest(Quest.DEATH_PLATEAU).build(), - SkillLevelTask.builder().skill(Skill.AGILITY).level(15).build() - ) - ); - - // Shilo Village requires Jungle Potion (and thus Druidic Ritual) plus Crafting 20 and Agility 32【397040585224810†L50-L57】. - REQUIREMENT_MAP.put( - Quest.SHILO_VILLAGE, - Arrays.asList( - QuestTask.builder().quest(Quest.JUNGLE_POTION).build(), - SkillLevelTask.builder().skill(Skill.CRAFTING).level(20).build(), - SkillLevelTask.builder().skill(Skill.AGILITY).level(32).build() - ) - ); - // Jungle Potion requires Druidic Ritual and Herblore 3【532018769049742†L45-L49】. REQUIREMENT_MAP.put( Quest.JUNGLE_POTION, @@ -224,31 +52,6 @@ public class QuestRequirements ) ); - - // Dragon Slayer II requires numerous quests and high skill levels【916074840360349†L82-L92】【916074840360349†L93-L129】. - REQUIREMENT_MAP.put( - Quest.DRAGON_SLAYER_II, - Arrays.asList( - // Quest prerequisites - QuestTask.builder().quest(Quest.LEGENDS_QUEST).build(), - QuestTask.builder().quest(Quest.DREAM_MENTOR).build(), - QuestTask.builder().quest(Quest.A_TAIL_OF_TWO_CATS).build(), - QuestTask.builder().quest(Quest.ANIMAL_MAGNETISM).build(), - QuestTask.builder().quest(Quest.GHOSTS_AHOY).build(), - QuestTask.builder().quest(Quest.BONE_VOYAGE).build(), - QuestTask.builder().quest(Quest.CLIENT_OF_KOUREND).build(), - // Skill requirements - SkillLevelTask.builder().skill(Skill.MAGIC).level(75).build(), - SkillLevelTask.builder().skill(Skill.SMITHING).level(70).build(), - SkillLevelTask.builder().skill(Skill.MINING).level(68).build(), - SkillLevelTask.builder().skill(Skill.CRAFTING).level(62).build(), - SkillLevelTask.builder().skill(Skill.AGILITY).level(60).build(), - SkillLevelTask.builder().skill(Skill.THIEVING).level(60).build(), - SkillLevelTask.builder().skill(Skill.CONSTRUCTION).level(50).build(), - SkillLevelTask.builder().skill(Skill.HITPOINTS).level(50).build() - ) - - ); REQUIREMENT_MAP.put( Quest.BIOHAZARD, Arrays.asList( @@ -256,62 +59,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.BLACK_KNIGHTS_FORTRESS, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.CLOCK_TOWER, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.COOKS_ASSISTANT, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.DEATH_PLATEAU, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.DEMON_SLAYER, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.DORICS_QUEST, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.DRUIDIC_RITUAL, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.DWARF_CANNON, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.EAGLES_PEAK, Arrays.asList( @@ -328,13 +75,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.ERNEST_THE_CHICKEN, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.FISHING_CONTEST, Arrays.asList( @@ -342,55 +82,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.GERTRUDES_CAT, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.GOBLIN_DIPLOMACY, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.HAZEEL_CULT, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.IMP_CATCHER, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.MONKS_FRIEND, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.MURDER_MYSTERY, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.NATURE_SPIRIT, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.OBSERVATORY_QUEST, Arrays.asList( @@ -398,97 +89,13 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.PIRATES_TREASURE, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.PLAGUE_CITY, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.PRIEST_IN_PERIL, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.PRINCE_ALI_RESCUE, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.RAG_AND_BONE_MAN_I, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.RECRUITMENT_DRIVE, - Arrays.asList( + Collections.singletonList( QuestTask.builder().quest(Quest.BLACK_KNIGHTS_FORTRESS).build() ) ); - REQUIREMENT_MAP.put( - Quest.THE_RESTLESS_GHOST, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.ROMEO__JULIET, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.RUNE_MYSTERIES, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.SHEEP_HERDER, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.SHEEP_SHEARER, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.SHIELD_OF_ARRAV, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.A_SOULS_BANE, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.TOWER_OF_LIFE, Arrays.asList( @@ -496,20 +103,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.VAMPYRE_SLAYER, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.WITCHS_POTION, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.ANIMAL_MAGNETISM, Arrays.asList( @@ -642,13 +235,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.THE_FREMENNIK_TRIALS, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.GARDEN_OF_TRANQUILLITY, Arrays.asList( @@ -752,13 +338,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.MERLINS_CRYSTAL, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.MOUNTAIN_DAUGHTER, Arrays.asList( @@ -886,13 +465,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.TREE_GNOME_VILLAGE, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.TRIBAL_TOTEM, Arrays.asList( @@ -921,13 +493,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.WATERFALL_QUEST, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.WHAT_LIES_BELOW, Arrays.asList( @@ -936,13 +501,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.WITCHS_HOUSE, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.ZOGRE_FLESH_EATERS, Arrays.asList( @@ -989,13 +547,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.DRAGON_SLAYER_I, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.EADGARS_RUSE, Arrays.asList( @@ -1014,15 +565,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.FAIRYTALE_I__GROWING_PAINS, - Arrays.asList( - QuestTask.builder().quest(Quest.LOST_CITY).build(), - SkillLevelTask.builder().skill(Skill.FARMING).level(30).build(), - SkillLevelTask.builder().skill(Skill.CRAFTING).level(36).build() - ) - ); - REQUIREMENT_MAP.put( Quest.FAIRYTALE_II__CURE_A_QUEEN, Arrays.asList( @@ -1044,13 +586,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.FIGHT_ARENA, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.THE_FREMENNIK_ISLES, Arrays.asList( @@ -1451,34 +986,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.BELOW_ICE_MOUNTAIN, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.A_PORCINE_OF_INTEREST, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.GETTING_AHEAD, - Arrays.asList( - - ) - ); - - REQUIREMENT_MAP.put( - Quest.MISTHALIN_MYSTERY, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.THE_FREMENNIK_EXILES, Arrays.asList( @@ -1513,13 +1020,6 @@ public class QuestRequirements ) ); - REQUIREMENT_MAP.put( - Quest.THE_GARDEN_OF_DEATH, - Arrays.asList( - - ) - ); - REQUIREMENT_MAP.put( Quest.LAND_OF_THE_GOBLINS, Arrays.asList( From d5ecf9466e3a95b1d89a6c3a0f258117eb75baa5 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Tue, 19 Aug 2025 00:32:13 -0400 Subject: [PATCH 14/53] borked --- .../goaltracker/ui/components/ComboBox.java | 176 ++++++++++-------- .../goaltracker/ui/inputs/ItemTaskInput.java | 58 ++++++ 2 files changed, 152 insertions(+), 82 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java b/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java index 0493db3..ffcc3b2 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java @@ -1,125 +1,137 @@ package com.toofifty.goaltracker.ui.components; -import com.toofifty.goaltracker.GoalTrackerPlugin; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; -import java.awt.image.BufferedImage; import java.util.List; import java.util.function.Function; -import javax.swing.ImageIcon; -import javax.swing.JButton; +import javax.swing.DefaultComboBoxModel; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; +import javax.swing.ListCellRenderer; +import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import javax.swing.plaf.basic.BasicComboBoxUI; -import javax.swing.ListCellRenderer; + import net.runelite.client.ui.ColorScheme; -import net.runelite.client.util.ImageUtil; import net.runelite.client.util.Text; +/** + * Simple generic ComboBox with optional formatter and a renderer + * that matches RuneLite dark styling. Includes a helper to keep the + * popup open after a selection (for rapid multi-add flows). + */ public class ComboBox extends JComboBox { private Function formatter = null; - @SuppressWarnings("unchecked") - public ComboBox(List items) + public ComboBox() { - this((T[]) items.toArray()); + super(); + setRenderer(new ComboBoxListRenderer()); + // Keep consistent look with RuneLite + setBackground(ColorScheme.DARKER_GRAY_COLOR); + setForeground(Color.WHITE); + setUI(new BasicComboBoxUI()); } - public ComboBox(T[] items) { - super(items); - - setForeground(Color.WHITE); - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setFocusable(false); - setRenderer(new ComboBoxListRenderer<>(formatter)); - setUI(new ComboBoxUI()); - setBorder(new EmptyBorder(0, 0, 0, 0)); + this(); + setItems(java.util.Arrays.asList(items)); } - public void setFormatter(Function formatter) + + public ComboBox(List items) { - this.formatter = formatter; - setRenderer(new ComboBoxListRenderer<>(formatter)); + this(); + setItems(items); } - @SuppressWarnings("unchecked") public void setItems(List items) { - removeAllItems(); - for (T item : items) { - addItem(item); - } - if (getItemCount() > 0) { - setSelectedIndex(0); + DefaultComboBoxModel model = new DefaultComboBoxModel<>(); + if (items != null) + { + for (T it : items) + { + model.addElement(it); + } } - } -} - -class ComboBoxUI extends BasicComboBoxUI -{ - private static final ImageIcon ARROW_UP; - private static final ImageIcon ARROW_DOWN; - - static { - final BufferedImage arrowUp = ImageUtil.loadImageResource( - GoalTrackerPlugin.class, "/combo_arrow_up.png"); - ARROW_UP = new ImageIcon(arrowUp); - - final BufferedImage arrowDown = ImageUtil.loadImageResource( - GoalTrackerPlugin.class, "/combo_arrow_down.png"); - ARROW_DOWN = new ImageIcon(arrowDown); + setModel(model); } - @Override - protected JButton createArrowButton() - { - JButton button = new JButton(); - button.setBackground(ColorScheme.DARKER_GRAY_COLOR); - button.setBorder(new EmptyBorder(4, 4, 4, 4)); - button.setBorderPainted(false); - button.add(new JLabel(ARROW_DOWN)); - return button; - } -} - -class ComboBoxListRenderer implements ListCellRenderer -{ - private Function formatter; - - ComboBoxListRenderer(Function formatter) + public void setFormatter(Function formatter) { this.formatter = formatter; + repaint(); } - @Override - public Component getListCellRendererComponent( - JList list, T o, int index, boolean isSelected, - boolean cellHasFocus) + /** + * Re-opens the popup after a selection is made, enabling fast repeated adds. + * Call this once after constructing the combo if you want the behavior. + */ + public void setStayOpenOnSelection(boolean stayOpen) { - JPanel container = new JPanel(new BorderLayout()); - - container.setBorder(new EmptyBorder(0, 4, 0, 4)); - - JLabel label = new JLabel(); - if (formatter != null) { - label.setText(formatter.apply(o)); - } else { - label.setText( - o instanceof Enum ? Text.titleCase((Enum) o) : o.toString()); + if (stayOpen) + { + this.addActionListener(e -> + SwingUtilities.invokeLater(() -> this.setPopupVisible(true)) + ); } - container.add(label, BorderLayout.WEST); + } - if (isSelected) { - container.setBackground(ColorScheme.DARK_GRAY_COLOR); - label.setForeground(Color.WHITE); + private class ComboBoxListRenderer implements ListCellRenderer + { + @Override + public Component getListCellRendererComponent( + JList list, + T value, + int index, + boolean isSelected, + boolean cellHasFocus) + { + JPanel container = new JPanel(new BorderLayout()); + container.setBorder(new EmptyBorder(2, 6, 2, 6)); + + JLabel label = new JLabel(); + label.setOpaque(false); + + if (value != null) + { + if (formatter != null) + { + label.setText(formatter.apply(value)); + } + else if (value instanceof Enum) + { + label.setText(Text.titleCase((Enum) value)); + } + else + { + label.setText(value.toString()); + } + } + else + { + label.setText(""); + } + + container.add(label, BorderLayout.WEST); + + if (isSelected) + { + container.setBackground(ColorScheme.DARK_GRAY_COLOR); + label.setForeground(Color.WHITE); + } + else + { + container.setBackground(ColorScheme.DARKER_GRAY_COLOR); + label.setForeground(Color.WHITE); + } + + return container; } - - return container; } -} \ No newline at end of file +} diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java index ad34fb6..80cb488 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java @@ -13,9 +13,19 @@ import net.runelite.client.ui.components.FlatTextField; import javax.swing.*; +import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; import java.awt.*; +import java.awt.Point; +import java.awt.KeyboardFocusManager; +import java.awt.Window; import java.util.regex.Pattern; +import java.awt.Color; +import javax.swing.JDialog; +import javax.swing.JButton; + + + public class ItemTaskInput extends TaskInput { @@ -53,6 +63,7 @@ public ItemTaskInput(GoalTrackerPlugin plugin, Goal goal) .tooltipText("Choose an item") .onItemSelected(this::setSelectedItem) .build(); + showCloseOverlay(); }); getInputRow().add(searchItemButton, BorderLayout.WEST); @@ -108,6 +119,9 @@ private void setSelectedItem(Integer rawId) revalidate(); repaint(); + + // Immediately add the item task + submit(); }); } @@ -142,5 +156,49 @@ private void clearSelectedItem() revalidate(); repaint(); + + // Immediately add the item task + submit(); + + // Small floating red X button to close the item search popup + private void showCloseOverlay() + { + javax.swing.Timer t = new javax.swing.Timer(120, ev -> { + Window target = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow(); + if (target == null || !target.isShowing()) + { + return; + } + + // Create overlay dialog owned by the search window so it closes with it + final JDialog overlay = new JDialog(target); + overlay.setUndecorated(true); + overlay.setAlwaysOnTop(true); + JPanel header = new JPanel(new BorderLayout()); + header.setBackground(ColorScheme.DARKER_GRAY_COLOR); + JButton close = new JButton("X"); + close.setForeground(Color.WHITE); + close.setBackground(ColorScheme.PROGRESS_ERROR_COLOR); + close.setBorder(new EmptyBorder(2, 6, 2, 6)); + close.setFocusable(false); + close.addActionListener(e2 -> { + overlay.dispose(); + // Try to close the target search window + if (target != null) target.dispose(); + }); + header.add(close, BorderLayout.EAST); + overlay.getContentPane().add(header); + overlay.pack(); + + // Position at top-right of the target window with slight inset + Point loc = target.getLocationOnScreen(); + int x = loc.x + target.getWidth() - overlay.getWidth() - 6; + int y = loc.y + 6; + overlay.setLocation(x, y); + overlay.setVisible(true); + }); + t.setRepeats(false); + t.start(); } +} } \ No newline at end of file From eb9160d7d2c754b4d24f4c3a1030d0485c27412d Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Tue, 19 Aug 2025 12:46:48 -0400 Subject: [PATCH 15/53] working lovely. --- changelog.md | 15 +- .../goaltracker/GoalTrackerPlugin.java | 6 +- .../goaltracker/ui/TaskItemContent.java | 20 -- .../ui/components/ListTaskPanel.java | 86 +++++- .../goaltracker/ui/inputs/ItemTaskInput.java | 257 ++++++++++++++---- .../goaltracker/ui/inputs/TaskInput.java | 15 +- 6 files changed, 311 insertions(+), 88 deletions(-) diff --git a/changelog.md b/changelog.md index b03245a..a78acf5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,3 @@ - - # Changelog ## [Unreleased] - 2025-08-18 @@ -14,3 +12,16 @@ - Pre-req button made more compact (~25% smaller). - Prerequisite insertion now places child tasks directly below their parent instead of at the end of the list. - UI now automatically refreshes after task mutations (add/remove/indent/status change). + +## [Unreleased] - 2025-08-19 + +### Added +- Right-click menu option **Add pre-reqs** for quest tasks with prerequisites. Automatically inserts missing prerequisites as subtasks with proper indentation and prevents duplicates. + +### Changed +- Right-click context menu reorganized: Move actions grouped under a **Move** submenu; Remove action now labeled as **Remove (Shift+Left Click)**. +- Quest tasks now only display the **Add pre-reqs** option if they actually have missing prerequisites. +- Search button now toggles between **Search...** and **Close** to open/close the item search overlay. +- Removed in-panel close button; close control is now handled directly via the Search button toggle. +- Green **+Add** button hidden for item search inputs (still visible for other input types). +- Duplicate prerequisites can no longer be added multiple times to the same quest. diff --git a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java index 9002220..f29b4ea 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java +++ b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java @@ -21,10 +21,10 @@ import net.runelite.client.game.ItemManager; import net.runelite.client.game.SkillIconManager; import net.runelite.client.game.chatbox.ChatboxItemSearch; +import net.runelite.client.game.chatbox.ChatboxPanelManager; import net.runelite.client.plugins.Plugin; import net.runelite.client.plugins.PluginDescriptor; import net.runelite.client.ui.ClientToolbar; -import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.NavigationButton; import net.runelite.client.util.AsyncBufferedImage; @@ -60,6 +60,10 @@ public class GoalTrackerPlugin extends Plugin @Inject private ChatboxItemSearch itemSearch; + @Getter + @Inject + private ChatboxPanelManager chatboxPanelManager; + @Inject private ClientToolbar clientToolbar; diff --git a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java index 2465bbf..f8a97af 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java @@ -23,7 +23,6 @@ public class TaskItemContent extends JPanel implements Refreshable private final TaskIconService iconService; private final JLabel titleLabel = new JLabel(); private final JLabel iconLabel = new JLabel(); - private final JButton prereqButton = new JButton("Add prereqs"); TaskItemContent(GoalTrackerPlugin plugin, Goal goal, Task task) { @@ -35,25 +34,6 @@ public class TaskItemContent extends JPanel implements Refreshable titleLabel.setPreferredSize(new Dimension(0, 24)); add(titleLabel, BorderLayout.CENTER); - if (task instanceof QuestTask) { - prereqButton.setMargin(new Insets(1, 3, 1, 3)); - prereqButton.setFont(prereqButton.getFont().deriveFont(prereqButton.getFont().getSize2D() * 0.75f)); - prereqButton.setFocusable(false); - prereqButton.addActionListener(e -> { - QuestTask qt = (QuestTask) task; - List reqs = QuestRequirements.getRequirements(qt.getQuest(), qt.getIndentLevel()); - // Insert directly after this item - int insertAt = Math.max(0, goal.getTasks().indexOf(task) + 1); - goal.getTasks().addAll(insertAt, reqs); - Container parent = SwingUtilities.getAncestorOfClass(ListPanel.class, TaskItemContent.this); - if (parent instanceof ListPanel) { - ((ListPanel) parent).tryBuildList(); - ((ListPanel) parent).refresh(); - } - }); - add(prereqButton, BorderLayout.EAST); - } - JPanel iconWrapper = new JPanel(new BorderLayout()); iconWrapper.setBorder(new EmptyBorder(4, 0, 0, 4)); iconWrapper.add(iconLabel, BorderLayout.NORTH); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java index 8d04080..865bc46 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java @@ -1,6 +1,7 @@ package com.toofifty.goaltracker.ui.components; import com.toofifty.goaltracker.ui.TaskItemContent; +import com.toofifty.goaltracker.utils.QuestRequirements; import com.toofifty.goaltracker.utils.ReorderableList; import com.toofifty.goaltracker.models.task.Task; @@ -33,7 +34,6 @@ public ListTaskPanel(ReorderableList list, Task item) for (int i = index + 1; i < list.size(); i++) { var child = list.get(i); - System.out.println(String.format("%s >= %s", item.getIndentLevel(), child.getIndentLevel())); // If a child is less indented then this item assume its a parent node and break if (item.getIndentLevel() >= child.getIndentLevel()) break; @@ -41,7 +41,7 @@ public ListTaskPanel(ReorderableList list, Task item) } item.indent(); - this.indentedListener.accept(item); + if (this.indentedListener != null) this.indentedListener.accept(item); refreshParentList(); }); @@ -51,15 +51,14 @@ public ListTaskPanel(ReorderableList list, Task item) for (int i = index + 1; i < list.size(); i++) { var child = list.get(i); - System.out.println(String.format("%s >= %s", item.getIndentLevel(), child.getIndentLevel())); // If a child is less indented then this item assume its a parent node and break if (item.getIndentLevel() >= child.getIndentLevel()) break; - child.unindent();; + child.unindent(); } item.unindent(); - this.unindentedListener.accept(item); + if (this.unindentedListener != null) this.unindentedListener.accept(item); refreshParentList(); }); // Allow shift-click to remove this item and all its indented children @@ -102,17 +101,26 @@ public void mouseClicked(MouseEvent e) { public void refreshMenu() { popupMenu.removeAll(); + javax.swing.JMenu moveMenu = new javax.swing.JMenu("Move"); + boolean hasMove = false; if (!list.isFirst(item)) { - popupMenu.add(moveUp); + moveMenu.add(moveUp); + hasMove = true; } if (!list.isLast(item)) { - popupMenu.add(moveDown); + moveMenu.add(moveDown); + hasMove = true; } if (!list.isFirst(item)) { - popupMenu.add(moveToTop); + moveMenu.add(moveToTop); + hasMove = true; } if (!list.isLast(item)) { - popupMenu.add(moveToBottom); + moveMenu.add(moveToBottom); + hasMove = true; + } + if (hasMove) { + popupMenu.add(moveMenu); } var previousItem = list.getPreviousItem(item); @@ -150,6 +158,66 @@ public void refreshMenu() }); popupMenu.add(toggleStatusItem); + // Add quest pre-reqs menu item only if the quest actually has prereqs + if (item instanceof com.toofifty.goaltracker.models.task.QuestTask) { + com.toofifty.goaltracker.models.task.QuestTask questTask = (com.toofifty.goaltracker.models.task.QuestTask) item; + int baseIndent = item.getIndentLevel(); + // Gather existing direct/descendant children under this quest to avoid duplicates + java.util.Set existingKeys = new java.util.HashSet<>(); + int parentIndex = list.indexOf(item); + for (int i = parentIndex + 1; i < list.size(); i++) { + var child = list.get(i); + if (child.getIndentLevel() <= baseIndent) { + break; // stop at siblings/parents + } + existingKeys.add(child.getClass().getName() + "|" + child.toString()); + } + var rawPrereqs = QuestRequirements.getRequirements(questTask.getQuest(), baseIndent + 1); + java.util.List missingPrereqs = new java.util.ArrayList<>(); + if (rawPrereqs != null) { + for (com.toofifty.goaltracker.models.task.Task p : rawPrereqs) { + String key = p.getClass().getName() + "|" + p.toString(); + if (!existingKeys.contains(key)) { + missingPrereqs.add(p); + } + } + } + if (!missingPrereqs.isEmpty()) { + JMenuItem prereqItem = new JMenuItem("Add pre-reqs"); + prereqItem.addActionListener(e -> { + // Recompute and filter again at click time + var raw = QuestRequirements.getRequirements(questTask.getQuest(), baseIndent + 1); + if (raw != null) { + // Refresh existing keys in case the list changed since menu was built + java.util.Set currentKeys = new java.util.HashSet<>(); + int pIndex = list.indexOf(item); + for (int i = pIndex + 1; i < list.size(); i++) { + var child = list.get(i); + if (child.getIndentLevel() <= baseIndent) break; + currentKeys.add(child.getClass().getName() + "|" + child.toString()); + } + java.util.List filtered = new java.util.ArrayList<>(); + for (com.toofifty.goaltracker.models.task.Task t : raw) { + String key = t.getClass().getName() + "|" + t.toString(); + if (!currentKeys.contains(key)) { + filtered.add(t); + } + } + if (!filtered.isEmpty()) { + int index = list.indexOf(item); + for (com.toofifty.goaltracker.models.task.Task prereq : filtered) { + list.add(index + 1, prereq); + index++; + } + refreshParentList(); + } + } + }); + popupMenu.add(prereqItem); + } + } + + removeItem.setText("Remove (Shift+Left Click)"); popupMenu.add(removeItem); } diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java index 80cb488..cefe64a 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java @@ -23,6 +23,10 @@ import java.awt.Color; import javax.swing.JDialog; import javax.swing.JButton; +import java.awt.event.ActionListener; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.JTextComponent; @@ -34,6 +38,7 @@ public class ItemTaskInput extends TaskInput private final FlatTextField quantityField = new FlatTextField(); private final TextButton searchItemButton = new TextButton("Search..."); + private boolean searchOpen = false; private final JLabel selectedItemLabel = new JLabel(); private final JPanel selectedItemPanel = new JPanel(new BorderLayout()); @@ -43,6 +48,7 @@ public class ItemTaskInput extends TaskInput private String quantityFieldValue = "1"; private ItemComposition selectedItem; + private String lastSearchText = ""; public ItemTaskInput(GoalTrackerPlugin plugin, Goal goal) { @@ -51,19 +57,29 @@ public ItemTaskInput(GoalTrackerPlugin plugin, Goal goal) this.clientThread = plugin.getClientThread(); searchItemButton.onClick(e -> { - if (plugin.getClient().getGameState() != GameState.LOGGED_IN) { - JOptionPane.showMessageDialog(this, - "You must be logged in to choose items", - "UwU", - JOptionPane.ERROR_MESSAGE); - return; - } + if (!searchOpen) { + if (plugin.getClient().getGameState() != GameState.LOGGED_IN) { + JOptionPane.showMessageDialog(this, + "You must be logged in to choose items", + "UwU", + JOptionPane.ERROR_MESSAGE); + return; + } - plugin.getItemSearch() - .tooltipText("Choose an item") - .onItemSelected(this::setSelectedItem) - .build(); - showCloseOverlay(); + plugin.getItemSearch() + .tooltipText("Choose an item") + .onItemSelected(this::setSelectedItem) + .build(); + searchItemButton.setText("Close"); + searchOpen = true; + } + else { + try { + plugin.getChatboxPanelManager().close(); + } catch (Exception ignored) {} + searchItemButton.setText("Search..."); + searchOpen = false; + } }); getInputRow().add(searchItemButton, BorderLayout.WEST); @@ -122,6 +138,15 @@ private void setSelectedItem(Integer rawId) // Immediately add the item task submit(); + + // Reopen search so user can add multiple items + plugin.getItemSearch() + .tooltipText("Choose an item") + .onItemSelected(this::setSelectedItem) + .build(); + + searchItemButton.setText("Close"); + searchOpen = true; }); } @@ -157,48 +182,180 @@ private void clearSelectedItem() revalidate(); repaint(); - // Immediately add the item task - submit(); - + searchItemButton.setText("Search..."); + searchOpen = false; + } + + private boolean containsTextField(Container c) + { + if (c == null) return false; + for (Component comp : c.getComponents()) + { + if (comp instanceof JTextComponent) return true; + if (comp instanceof Container && containsTextField((Container) comp)) return true; + } + return false; + } + + private JTextComponent findTextField(Container c) + { + if (c == null) return null; + for (Component comp : c.getComponents()) + { + if (comp instanceof JTextComponent) return (JTextComponent) comp; + if (comp instanceof Container) + { + JTextComponent tf = findTextField((Container) comp); + if (tf != null) return tf; + } + } + return null; + } + // Small floating red X button to close the item search popup private void showCloseOverlay() { - javax.swing.Timer t = new javax.swing.Timer(120, ev -> { - Window target = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow(); - if (target == null || !target.isShowing()) - { - return; + final int[] attempts = {0}; + final int maxAttempts = 20; + final int delayMs = 150; + + ActionListener tryShow = new ActionListener() { + @Override + public void actionPerformed(java.awt.event.ActionEvent ev) { + Component focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().getFocusOwner(); + final JTextComponent searchField = (focusOwner instanceof JTextComponent) ? (JTextComponent) focusOwner : null; + Window target = (focusOwner != null) ? SwingUtilities.getWindowAncestor(focusOwner) : null; + + if (target == null || searchField == null) + { + if (++attempts[0] < maxAttempts) + { + new javax.swing.Timer(delayMs, this) {{ setRepeats(false); }}.start(); + } + return; + } + + // Create overlay dialog owned by the search window so it closes with it + final JDialog overlay = new JDialog(target); + overlay.setUndecorated(true); + overlay.setAlwaysOnTop(true); + overlay.setFocusableWindowState(false); + overlay.setType(Window.Type.UTILITY); + + JPanel header = new JPanel(new BorderLayout()); + header.setBackground(ColorScheme.DARKER_GRAY_COLOR); + JButton close = new JButton("X"); + close.setForeground(Color.WHITE); + close.setBackground(ColorScheme.PROGRESS_ERROR_COLOR); + close.setBorder(new EmptyBorder(2, 6, 2, 6)); + close.setFocusable(false); + close.addActionListener(e2 -> { + overlay.dispose(); + lastSearchText = ""; + if (target instanceof JDialog) { + ((JDialog) target).dispose(); + } else if (target instanceof javax.swing.JFrame) { + ((javax.swing.JFrame) target).dispose(); + } else { + target.dispose(); + } + }); + header.add(close, BorderLayout.EAST); + overlay.getContentPane().add(header); + overlay.pack(); + + // Position at top-right of the search text field (anchor to the actual box) + Point tfLoc = searchField.getLocationOnScreen(); + int tfRight = tfLoc.x + searchField.getWidth(); + int x = tfRight - overlay.getWidth() - 4; // tuck inside right edge + int y = tfLoc.y + 2; // inside the box near the top + overlay.setLocation(x, y); + overlay.setVisible(true); + overlay.toFront(); + + // Restore and persist the user's query + if (lastSearchText != null && !lastSearchText.isEmpty()) + { + searchField.setText(lastSearchText); + searchField.setCaretPosition(searchField.getText().length()); + searchField.requestFocusInWindow(); + } + searchField.getDocument().addDocumentListener(new DocumentListener() + { + @Override public void insertUpdate(DocumentEvent e) { lastSearchText = searchField.getText(); } + @Override public void removeUpdate(DocumentEvent e) { lastSearchText = searchField.getText(); } + @Override public void changedUpdate(DocumentEvent e) { lastSearchText = searchField.getText(); } + }); + + if (target instanceof Window) { + ((Window) target).addWindowListener(new java.awt.event.WindowAdapter() { + @Override + public void windowClosed(java.awt.event.WindowEvent e) { overlay.dispose(); } + @Override + public void windowClosing(java.awt.event.WindowEvent e) { overlay.dispose(); } + }); + + target.addComponentListener(new java.awt.event.ComponentAdapter() { + @Override + public void componentMoved(java.awt.event.ComponentEvent e) { + try { + Point tfLoc2 = searchField.getLocationOnScreen(); + int tfRight2 = tfLoc2.x + searchField.getWidth(); + int x2 = tfRight2 - overlay.getWidth() - 4; + int y2 = tfLoc2.y + 2; + overlay.setLocation(x2, y2); + } catch (IllegalComponentStateException ignored) { } + } + @Override + public void componentResized(java.awt.event.ComponentEvent e) { + try { + Point tfLoc2 = searchField.getLocationOnScreen(); + int tfRight2 = tfLoc2.x + searchField.getWidth(); + int x2 = tfRight2 - overlay.getWidth() - 4; + int y2 = tfLoc2.y + 2; + overlay.setLocation(x2, y2); + } catch (IllegalComponentStateException ignored) { } + } + }); + } } + }; - // Create overlay dialog owned by the search window so it closes with it - final JDialog overlay = new JDialog(target); - overlay.setUndecorated(true); - overlay.setAlwaysOnTop(true); - JPanel header = new JPanel(new BorderLayout()); - header.setBackground(ColorScheme.DARKER_GRAY_COLOR); - JButton close = new JButton("X"); - close.setForeground(Color.WHITE); - close.setBackground(ColorScheme.PROGRESS_ERROR_COLOR); - close.setBorder(new EmptyBorder(2, 6, 2, 6)); - close.setFocusable(false); - close.addActionListener(e2 -> { - overlay.dispose(); - // Try to close the target search window - if (target != null) target.dispose(); - }); - header.add(close, BorderLayout.EAST); - overlay.getContentPane().add(header); - overlay.pack(); - - // Position at top-right of the target window with slight inset - Point loc = target.getLocationOnScreen(); - int x = loc.x + target.getWidth() - overlay.getWidth() - 6; - int y = loc.y + 6; - overlay.setLocation(x, y); - overlay.setVisible(true); - }); - t.setRepeats(false); - t.start(); + // start first attempt slightly delayed to give the popup time to appear + new javax.swing.Timer(delayMs, tryShow) {{ setRepeats(false); }}.start(); + } + + // Try to locate the RuneLite Item Search popup window reliably + private Window findItemSearchWindow() + { + // Prefer visible JDialogs containing a text field (likely the chatbox item search) + for (Window w : Window.getWindows()) + { + if (w != null && w.isShowing() && w instanceof JDialog) + { + if (w instanceof Container && containsTextField((Container) w)) + { + return w; + } + } + } + Window active = KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow(); + if (active != null && active.isShowing() && active instanceof Container && containsTextField((Container) active)) + { + return active; + } + for (Window w : Window.getWindows()) + { + if (w != null && w.isShowing() && w instanceof Container && containsTextField((Container) w)) + { + return w; + } + } + return null; + } + @Override + protected boolean showAddButton() + { + return false; } -} } \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java index 3e310f9..804231a 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java @@ -52,12 +52,11 @@ public abstract class TaskInput extends JPanel inputRow = new JPanel(new BorderLayout()); inputRow.setBackground(ColorScheme.DARKER_GRAY_COLOR); - - TextButton addButton = new TextButton("Add"); - addButton.onClick(e -> submit()); - - inputRow.add(addButton, BorderLayout.EAST); - + if (showAddButton()) { + TextButton addButton = new TextButton("Add"); + addButton.onClick(e -> submit()); + inputRow.add(addButton, BorderLayout.EAST); + } add(inputRow, constraints); constraints.gridy++; } @@ -91,4 +90,8 @@ public TaskInput onSubmit(Consumer listener) this.listener = listener; return this; } + protected boolean showAddButton() + { + return true; + } } \ No newline at end of file From 63b098f541a88761a3f88f178b444a41af51b24c Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Tue, 19 Aug 2025 12:47:57 -0400 Subject: [PATCH 16/53] working lovely. --- README.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index df9b3c2..0bb2d73 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,9 @@ Track quest progress and completion, just select a quest or miniquest from the d Select an item using the search button and searching via the in-game chatbox, then set the desired quantity. The plugin will keep track of your items and tally up quantities stored in different inventories (bank, player, GIMP storage), and will be automatically completed once you get that amount of the item. -## Changelog +## Changelog + +## [Unreleased] - 2025-08-16 ### Recent Additions - Added right-click menu to parent goals to mark all child tasks as completed or incomplete. @@ -69,3 +71,29 @@ Select an item using the search button and searching via the in-game chatbox, th - Added manual completion toggling for tasks created from presets, allowing users to right-click and mark them complete/incomplete just like quick-added tasks. - Added customizable color setting for task completion messages shown in the chatbox. - Implemented automatic goal status checking upon login to mark goals as completed if requirements are already met. + +## [Unreleased] - 2025-08-18 + +### Added +- Quest prerequisites button: Each quest task now has an **Add prereqs** button to insert its prerequisites directly beneath it. +- Shift+Click removal: Shift+Click a task to remove it and all its indented children at once. +- Completion cascading: Marking a parent quest as complete/incomplete now automatically updates all its child tasks. +- Dropdown quest selector: Replaced fuzzy search with a clean dropdown of all quests, displaying natural names (e.g., "Tree Gnome Village"). + +### Changed +- Pre-req button made more compact (~25% smaller). +- Prerequisite insertion now places child tasks directly below their parent instead of at the end of the list. +- UI now automatically refreshes after task mutations (add/remove/indent/status change). + +## [Unreleased] - 2025-08-19 + +### Added +- Right-click menu option **Add pre-reqs** for quest tasks with prerequisites. Automatically inserts missing prerequisites as subtasks with proper indentation and prevents duplicates. + +### Changed +- Right-click context menu reorganized: Move actions grouped under a **Move** submenu; Remove action now labeled as **Remove (Shift+Left Click)**. +- Quest tasks now only display the **Add pre-reqs** option if they actually have missing prerequisites. +- Search button now toggles between **Search...** and **Close** to open/close the item search overlay. +- Removed in-panel close button; close control is now handled directly via the Search button toggle. +- Green **+Add** button hidden for item search inputs (still visible for other input types). +- Duplicate prerequisites can no longer be added multiple times to the same quest. From 6c24653bdb8507857986771c96633994b0719b9d Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Tue, 19 Aug 2025 13:40:39 -0400 Subject: [PATCH 17/53] - Quest dropdown now uses **RuneScape UF** font at a normal crisp size for improved readability. - ComboBox font scaling updated to use integer point sizes, preventing fuzzy text. - QuestTaskInput updated to rely on shared ComboBox styling for consistency. --- README.md | 3 + changelog.md | 3 + .../goaltracker/ui/components/ComboBox.java | 105 +++++++++++++++++- .../goaltracker/ui/inputs/QuestTaskInput.java | 27 +++-- 4 files changed, 122 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 0bb2d73..ace6956 100644 --- a/README.md +++ b/README.md @@ -97,3 +97,6 @@ Select an item using the search button and searching via the in-game chatbox, th - Removed in-panel close button; close control is now handled directly via the Search button toggle. - Green **+Add** button hidden for item search inputs (still visible for other input types). - Duplicate prerequisites can no longer be added multiple times to the same quest. +- Quest dropdown now uses **RuneScape UF** font at a normal crisp size for improved readability. +- ComboBox font scaling updated to use integer point sizes, preventing fuzzy text. +- QuestTaskInput updated to rely on shared ComboBox styling for consistency. \ No newline at end of file diff --git a/changelog.md b/changelog.md index a78acf5..f79e180 100644 --- a/changelog.md +++ b/changelog.md @@ -25,3 +25,6 @@ - Removed in-panel close button; close control is now handled directly via the Search button toggle. - Green **+Add** button hidden for item search inputs (still visible for other input types). - Duplicate prerequisites can no longer be added multiple times to the same quest. +- Quest dropdown now uses **RuneScape UF** font at a normal crisp size for improved readability. +- ComboBox font scaling updated to use integer point sizes, preventing fuzzy text. +- QuestTaskInput updated to rely on shared ComboBox styling for consistency. \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java b/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java index ffcc3b2..3591d57 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java @@ -3,9 +3,12 @@ import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; +import java.awt.Font; import java.util.List; import java.util.function.Function; import javax.swing.DefaultComboBoxModel; +import javax.swing.ImageIcon; +import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JLabel; import javax.swing.JList; @@ -13,6 +16,8 @@ import javax.swing.ListCellRenderer; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; import javax.swing.plaf.basic.BasicComboBoxUI; import net.runelite.client.ui.ColorScheme; @@ -22,11 +27,20 @@ * Simple generic ComboBox with optional formatter and a renderer * that matches RuneLite dark styling. Includes a helper to keep the * popup open after a selection (for rapid multi-add flows). + * + * This version also supports: + * - Custom up/down arrow icons loaded from resources (combo_arrow_down.png / combo_arrow_up.png) + * - Compact mode (~10% smaller font), or arbitrary font scaling via setFontScale */ public class ComboBox extends JComboBox { private Function formatter = null; + private double fontScale = 1.0; // 1.0 = default; 0.9 = compact + private ImageIcon arrowDownIcon; + private ImageIcon arrowUpIcon; + private JButton arrowButtonRef; // created by UI#createArrowButton + public ComboBox() { super(); @@ -34,21 +48,105 @@ public ComboBox() // Keep consistent look with RuneLite setBackground(ColorScheme.DARKER_GRAY_COLOR); setForeground(Color.WHITE); - setUI(new BasicComboBoxUI()); + + // Load arrow icons from resources if available + // (If null, we just fall back to no icon customization) + arrowDownIcon = loadIcon("/combo_arrow_down.png"); + arrowUpIcon = loadIcon("/combo_arrow_up.png"); + + // Install a BasicComboBoxUI that uses our custom arrow button (if icons are available) + setUI(new BasicComboBoxUI() + { + @Override + protected JButton createArrowButton() + { + JButton b = (arrowDownIcon != null) ? new JButton(arrowDownIcon) : new JButton(); + b.setBorder(new EmptyBorder(0, 6, 0, 6)); + b.setContentAreaFilled(false); + b.setFocusPainted(false); + b.setOpaque(false); + arrowButtonRef = b; + return b; + } + }); + + // Swap arrow icon on popup visibility changes (if we have icons) + addPopupMenuListener(new PopupMenuListener() + { + @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) + { + if (arrowButtonRef != null && arrowUpIcon != null) + { + arrowButtonRef.setIcon(arrowUpIcon); + } + } + @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) + { + if (arrowButtonRef != null && arrowDownIcon != null) + { + arrowButtonRef.setIcon(arrowDownIcon); + } + } + @Override public void popupMenuCanceled(PopupMenuEvent e) + { + if (arrowButtonRef != null && arrowDownIcon != null) + { + arrowButtonRef.setIcon(arrowDownIcon); + } + } + }); + + // Apply initial font scaling + applyFontScale(); } + public ComboBox(T[] items) { this(); setItems(java.util.Arrays.asList(items)); } - public ComboBox(List items) { this(); setItems(items); } + private ImageIcon loadIcon(String path) + { + java.net.URL url = getClass().getResource(path); + return (url != null) ? new ImageIcon(url) : null; + } + + /** + * Compact preset (~10% smaller font). + */ + public void setCompact(boolean compact) + { + setFontScale(compact ? 0.9 : 1.0); + } + + /** + * Arbitrary font scaling (1.0 = normal). + */ + public void setFontScale(double scale) + { + if (scale <= 0) scale = 1.0; + this.fontScale = scale; + applyFontScale(); + repaint(); + } + + private void applyFontScale() + { + Font f = getFont(); + if (f != null) + { + int newSize = Math.max(10, Math.round(f.getSize() * (float) fontScale)); + setFont(new Font(f.getName(), f.getStyle(), newSize)); + } + } + public void setItems(List items) { DefaultComboBoxModel model = new DefaultComboBoxModel<>(); @@ -118,6 +216,9 @@ else if (value instanceof Enum) label.setText(""); } + // Use the exact same font as the combo for crisp text + label.setFont(ComboBox.this.getFont()); + container.add(label, BorderLayout.WEST); if (isSelected) diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java index 46f1a61..2225e6f 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java @@ -5,6 +5,7 @@ import com.toofifty.goaltracker.models.task.QuestTask; import com.toofifty.goaltracker.models.task.Task; import com.toofifty.goaltracker.utils.QuestRequirements; +import com.toofifty.goaltracker.ui.components.ComboBox; import net.runelite.api.Quest; import net.runelite.client.ui.ColorScheme; @@ -22,7 +23,7 @@ public class QuestTaskInput extends TaskInput { private final List allQuests; private Quest bestMatch; - private final JComboBox questDropdown; + private final ComboBox questDropdown; public QuestTaskInput(GoalTrackerPlugin plugin, Goal goal) { @@ -32,19 +33,17 @@ public QuestTaskInput(GoalTrackerPlugin plugin, Goal goal) allQuests = Arrays.asList(Quest.values()); allQuests.sort(Comparator.comparing(Quest::getName)); - // Initialize dropdown with all quests and custom renderer - questDropdown = new JComboBox<>(allQuests.toArray(new Quest[0])); - questDropdown.setRenderer(new DefaultListCellRenderer() { - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { - super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - if (value instanceof Quest) { - String name = ((Quest) value).getName(); - setText(name); - } - return this; - } - }); + // Initialize dropdown via shared ComboBox for consistent arrows and crisp fonts + questDropdown = new ComboBox<>(allQuests); + // questDropdown.setCompact(true); // ~10% smaller font + questDropdown.setFormatter(q -> q != null ? q.getName() : ""); + + // Force RuneScape UF font at normal size + Font rsFont = new Font("RuneScape UF", Font.PLAIN, 10); + questDropdown.setFont(rsFont); + + + questDropdown.setSelectedIndex(-1); // no selection initially questDropdown.addActionListener(e -> { Quest selected = (Quest) questDropdown.getSelectedItem(); From 7d1fe48146674ba8d651523769a7a0f0b46db5bb Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Tue, 19 Aug 2025 14:40:47 -0400 Subject: [PATCH 18/53] - Back and Undo buttons added to a new top control bar in goal view; later removed and replaced with a cleaner single bar design. - Embedded red "< Back" button removed from GoalPanel header, leaving only the goal name input aligned cleanly. - Goal name input updated to support copy, paste, cut, and select-all actions via both context menu and keyboard shortcuts. - Remove menu option enhanced to also delete all indented child tasks when removing a parent. - Remove menu label updated so the "(Shift+Left Click)" hint displays smaller and in gray. --- changelog.md | 7 +- .../toofifty/goaltracker/ui/GoalPanel.java | 84 +++++++++++++++++-- .../goaltracker/ui/GoalTrackerPanel.java | 24 ++++++ .../ui/components/ListTaskPanel.java | 18 +++- .../goaltracker/ui/components/TextButton.java | 3 + 5 files changed, 128 insertions(+), 8 deletions(-) diff --git a/changelog.md b/changelog.md index f79e180..b52a86c 100644 --- a/changelog.md +++ b/changelog.md @@ -27,4 +27,9 @@ - Duplicate prerequisites can no longer be added multiple times to the same quest. - Quest dropdown now uses **RuneScape UF** font at a normal crisp size for improved readability. - ComboBox font scaling updated to use integer point sizes, preventing fuzzy text. -- QuestTaskInput updated to rely on shared ComboBox styling for consistency. \ No newline at end of file +- QuestTaskInput updated to rely on shared ComboBox styling for consistency. +- Back and Undo buttons added to a new top control bar in goal view; later removed and replaced with a cleaner single bar design. +- Embedded red "< Back" button removed from GoalPanel header, leaving only the goal name input aligned cleanly. +- Goal name input updated to support copy, paste, cut, and select-all actions via both context menu and keyboard shortcuts. +- Remove menu option enhanced to also delete all indented child tasks when removing a parent. +- Remove menu label updated so the "(Shift+Left Click)" hint displays smaller and in gray. \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java index 8b73680..3e33dd3 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java @@ -9,11 +9,14 @@ import com.toofifty.goaltracker.ui.components.EditableInput; import com.toofifty.goaltracker.ui.components.ListPanel; import com.toofifty.goaltracker.ui.components.ListTaskPanel; -import com.toofifty.goaltracker.ui.components.TextButton; +import javax.swing.*; +import javax.swing.text.DefaultEditorKit; +import javax.swing.text.JTextComponent; +import java.awt.*; +import java.awt.event.KeyEvent; import java.awt.BorderLayout; import java.util.Objects; import java.util.function.Consumer; -import javax.swing.JPanel; import javax.swing.border.EmptyBorder; import net.runelite.client.ui.ColorScheme; @@ -36,11 +39,7 @@ public class GoalPanel extends JPanel implements Refreshable setLayout(new BorderLayout()); - TextButton backButton = new TextButton("< Back", ColorScheme.PROGRESS_ERROR_COLOR); - backButton.onClick((e) -> closeListener.run()); - JPanel headerPanel = new JPanel(new BorderLayout()); - headerPanel.add(backButton, BorderLayout.WEST); headerPanel.setBorder(new EmptyBorder(0, 0, 8, 0)); add(headerPanel, BorderLayout.NORTH); @@ -49,6 +48,14 @@ public class GoalPanel extends JPanel implements Refreshable this.goalUpdatedListener.accept(goal); }); headerPanel.add(descriptionInput, BorderLayout.CENTER); + SwingUtilities.invokeLater(() -> installClipboardSupport(descriptionInput)); + descriptionInput.addContainerListener(new java.awt.event.ContainerAdapter() + { + @Override public void componentAdded(java.awt.event.ContainerEvent e) + { + SwingUtilities.invokeLater(() -> installClipboardSupport(descriptionInput)); + } + }); taskListPanel = new ListPanel<>(goal.getTasks(), (task) -> { ListTaskPanel taskPanel = new ListTaskPanel(goal.getTasks(), task); @@ -129,4 +136,69 @@ public void onTaskUpdated(Consumer listener) taskListPanel.onUpdated(this.taskUpdatedListener); } + private void installClipboardSupport(Component root) + { + JTextComponent tc = findTextComponent(root); + if (tc == null) + { + return; + } + + // Ensure transfer handler to enable clipboard operations on text property + tc.setTransferHandler(new TransferHandler("text")); + tc.setDragEnabled(true); + + // Context menu with Cut/Copy/Paste/Select All + JPopupMenu menu = new JPopupMenu(); + JMenuItem cut = new JMenuItem(new DefaultEditorKit.CutAction()); + cut.setText("Cut"); + JMenuItem copy = new JMenuItem(new DefaultEditorKit.CopyAction()); + copy.setText("Copy"); + JMenuItem paste = new JMenuItem(new DefaultEditorKit.PasteAction()); + paste.setText("Paste"); + JMenuItem selectAll = new JMenuItem("Select All"); + selectAll.addActionListener(e -> tc.selectAll()); + menu.add(cut); + menu.add(copy); + menu.add(paste); + menu.addSeparator(); + menu.add(selectAll); + tc.setComponentPopupMenu(menu); + + // Keyboard shortcuts (Ctrl/Cmd + C/V/X/A) + int mask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); + InputMap im = tc.getInputMap(JComponent.WHEN_FOCUSED); + ActionMap am = tc.getActionMap(); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, mask), DefaultEditorKit.copyAction); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, mask), DefaultEditorKit.pasteAction); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, mask), DefaultEditorKit.cutAction); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, mask), DefaultEditorKit.selectAllAction); + am.put(DefaultEditorKit.copyAction, new DefaultEditorKit.CopyAction()); + am.put(DefaultEditorKit.pasteAction, new DefaultEditorKit.PasteAction()); + am.put(DefaultEditorKit.cutAction, new DefaultEditorKit.CutAction()); + am.put(DefaultEditorKit.selectAllAction, new AbstractAction() + { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { tc.selectAll(); } + }); + } + + private JTextComponent findTextComponent(Component c) + { + if (c instanceof JTextComponent) + { + return (JTextComponent) c; + } + if (c instanceof Container) + { + for (Component child : ((Container) c).getComponents()) + { + JTextComponent tc = findTextComponent(child); + if (tc != null) + { + return tc; + } + } + } + return null; + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 18715a1..1082056 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -86,6 +86,30 @@ public void view(Goal goal) { removeAll(); + // Top control bar: Back, Undo, Redo + JPanel controlBar = new JPanel(new BorderLayout()); + controlBar.setBackground(ColorScheme.DARK_GRAY_COLOR); + controlBar.setBorder(new EmptyBorder(6, 6, 6, 6)); + + JPanel leftButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); + leftButtons.setOpaque(true); + leftButtons.setBackground(ColorScheme.DARK_GRAY_COLOR); + + TextButton backButton = new TextButton("< Back", e -> this.home()).narrow(); + TextButton undoButton = new TextButton("Undo", e -> { /* hook up in future */ }).narrow(); + TextButton redoButton = new TextButton("Redo", e -> { /* hook up in future */ }).narrow(); + undoButton.setEnabled(false); + redoButton.setEnabled(false); + undoButton.setToolTipText("Undo last change (coming soon)"); + redoButton.setToolTipText("Redo last change (coming soon)"); + + leftButtons.add(backButton); + leftButtons.add(undoButton); + leftButtons.add(redoButton); + controlBar.add(leftButtons, BorderLayout.WEST); + + add(controlBar, BorderLayout.NORTH); + this.goalPanel = new GoalPanel(plugin, goal, this::home); this.goalPanel.onGoalUpdated(this.goalUpdatedListener); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java index 865bc46..2244d8b 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java @@ -217,7 +217,23 @@ public void refreshMenu() } } - removeItem.setText("Remove (Shift+Left Click)"); + // Make the Remove action also delete all indented children of this item + for (var l : removeItem.getActionListeners()) { + removeItem.removeActionListener(l); + } + removeItem.addActionListener(e -> { + int index = list.indexOf(item); + int baseIndent = item.getIndentLevel(); + // Remove all children more indented than this item + while (index + 1 < list.size() && list.get(index + 1).getIndentLevel() > baseIndent) { + list.remove(list.get(index + 1)); + } + // Remove the item itself + list.remove(item); + refreshParentList(); + }); + + removeItem.setText("Remove (Shift+Left Click)"); popupMenu.add(removeItem); } diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java b/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java index be242c2..3adb92a 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java @@ -75,4 +75,7 @@ public TextButton narrow() setBorder(new EmptyBorder(0, 2, 0, 2)); return this; } + + public void setOnClick(Object o) { + } } From 3bd4662053c5e3bab7ca2c3a2467dfd43be907b6 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Tue, 19 Aug 2025 14:41:15 -0400 Subject: [PATCH 19/53] - Back and Undo buttons added to a new top control bar in goal view; later removed and replaced with a cleaner single bar design. - Embedded red "< Back" button removed from GoalPanel header, leaving only the goal name input aligned cleanly. - Goal name input updated to support copy, paste, cut, and select-all actions via both context menu and keyboard shortcuts. - Remove menu option enhanced to also delete all indented child tasks when removing a parent. - Remove menu label updated so the "(Shift+Left Click)" hint displays smaller and in gray. --- .../goaltracker/ui/components/TextButton.java | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java b/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java index 3adb92a..6c2a6a9 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java @@ -43,31 +43,8 @@ public TextButton(String text, Consumer clickListener) onClick(clickListener); } - public TextButton onClick(Consumer clickListener) - { - addMouseListener(new MouseAdapter() - { - @Override - public void mousePressed(MouseEvent e) - { - clickListener.accept(e); - } - - @Override - public void mouseEntered(MouseEvent e) - { - setForeground(mainColor.darker()); - setCursor(new Cursor(Cursor.HAND_CURSOR)); - } - - @Override - public void mouseExited(MouseEvent e) - { - setForeground(mainColor); - setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); - } - }); - return this; + public TextButton onClick(Consumer clickListener) { + return null; } public TextButton narrow() From 9b3b7e320ac6a41a1e9da9f657fcb2ff049e8147 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Tue, 19 Aug 2025 14:50:25 -0400 Subject: [PATCH 20/53] fixed text button issue --- .../goaltracker/ui/components/TextButton.java | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java b/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java index 6c2a6a9..3adb92a 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java @@ -43,8 +43,31 @@ public TextButton(String text, Consumer clickListener) onClick(clickListener); } - public TextButton onClick(Consumer clickListener) { - return null; + public TextButton onClick(Consumer clickListener) + { + addMouseListener(new MouseAdapter() + { + @Override + public void mousePressed(MouseEvent e) + { + clickListener.accept(e); + } + + @Override + public void mouseEntered(MouseEvent e) + { + setForeground(mainColor.darker()); + setCursor(new Cursor(Cursor.HAND_CURSOR)); + } + + @Override + public void mouseExited(MouseEvent e) + { + setForeground(mainColor); + setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); + } + }); + return this; } public TextButton narrow() From 43e2ef43a81c165de6d491395fd2c9e2c0fbbeb3 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Tue, 19 Aug 2025 15:01:59 -0400 Subject: [PATCH 21/53] - Home view updated: "Goal Tracker" title moved to its own header, with a new action bar beneath it containing **+ Add goal**, **Move**, and **Bulk Edit** buttons (the latter two are placeholders marked "Coming soon"). --- changelog.md | 3 +- .../goaltracker/ui/GoalTrackerPanel.java | 51 +++++++++++++++---- 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/changelog.md b/changelog.md index b52a86c..bab8497 100644 --- a/changelog.md +++ b/changelog.md @@ -32,4 +32,5 @@ - Embedded red "< Back" button removed from GoalPanel header, leaving only the goal name input aligned cleanly. - Goal name input updated to support copy, paste, cut, and select-all actions via both context menu and keyboard shortcuts. - Remove menu option enhanced to also delete all indented child tasks when removing a parent. -- Remove menu label updated so the "(Shift+Left Click)" hint displays smaller and in gray. \ No newline at end of file +- Remove menu label updated so the "(Shift+Left Click)" hint displays smaller and in gray. +- Home view updated: "Goal Tracker" title moved to its own header, with a new action bar beneath it containing **+ Add goal**, **Move**, and **Bulk Edit** buttons (the latter two are placeholders marked "Coming soon"). \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 1082056..55691b5 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -46,16 +46,7 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) titlePanel.setLayout(new BorderLayout()); titlePanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - titlePanel.add( - new TextButton("+ Add goal", - e -> { - Goal goal = goalManager.createGoal(); - view(goal); - - if (Objects.nonNull(this.goalAddedListener)) this.goalAddedListener.accept(goal); - if (Objects.nonNull(this.goalUpdatedListener)) this.goalUpdatedListener.accept(goal); - } - ).narrow(), BorderLayout.EAST); + // (Removed "+ Add goal" button from the title panel) JLabel title = new JLabel(); title.setText("Goal Tracker"); @@ -63,6 +54,44 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) title.setFont(FontManager.getRunescapeBoldFont()); titlePanel.add(title, BorderLayout.WEST); + // New action bar below the title + JPanel actionBar = new JPanel(new BorderLayout()); + actionBar.setBorder(new EmptyBorder(6, 10, 6, 10)); + actionBar.setBackground(ColorScheme.DARK_GRAY_COLOR); + + JPanel actionsLeft = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); + actionsLeft.setOpaque(true); + actionsLeft.setBackground(ColorScheme.DARK_GRAY_COLOR); + + TextButton addGoalBtn = new TextButton("+ Add goal", + e -> { + Goal goal = goalManager.createGoal(); + view(goal); + + if (Objects.nonNull(this.goalAddedListener)) this.goalAddedListener.accept(goal); + if (Objects.nonNull(this.goalUpdatedListener)) this.goalUpdatedListener.accept(goal); + } + ).narrow(); + + TextButton moveBtn = new TextButton("Move", e -> {}).narrow(); + moveBtn.setEnabled(false); + moveBtn.setToolTipText("Coming soon"); + + TextButton bulkEditBtn = new TextButton("Bulk Edit", e -> {}).narrow(); + bulkEditBtn.setEnabled(false); + bulkEditBtn.setToolTipText("Coming soon"); + + actionsLeft.add(addGoalBtn); + actionsLeft.add(moveBtn); + actionsLeft.add(bulkEditBtn); + actionBar.add(actionsLeft, BorderLayout.WEST); + + // Stack title and action bar into a single header container + JPanel headerContainer = new JPanel(new BorderLayout()); + headerContainer.setBackground(ColorScheme.DARK_GRAY_COLOR); + headerContainer.add(titlePanel, BorderLayout.NORTH); + headerContainer.add(actionBar, BorderLayout.SOUTH); + goalListPanel = new ListPanel<>(goalManager.getGoals(), (goal) -> { var panel = new ListItemPanel<>(goalManager.getGoals(), goal); @@ -76,7 +105,7 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) goalListPanel.setGap(0); goalListPanel.setPlaceholder("Add a new goal using the button above"); - mainPanel.add(titlePanel, BorderLayout.NORTH); + mainPanel.add(headerContainer, BorderLayout.NORTH); mainPanel.add(goalListPanel, BorderLayout.CENTER); home(); From 94574554915fa1c1054347b06bab7616a2f60d28 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Tue, 19 Aug 2025 17:45:08 -0400 Subject: [PATCH 22/53] added goal panel action bar but right click is now broken. saving for later. --- .../goaltracker/models/ActionHistory.java | 69 ++++++ .../models/IndentChangeAction.java | 35 +++ .../goaltracker/models/RemoveTaskAction.java | 35 +++ .../goaltracker/models/ReorderTaskAction.java | 40 ++++ .../models/ToggleCompleteAction.java | 55 +++++ .../goaltracker/models/UndoStack.java | 100 ++++++++ .../toofifty/goaltracker/ui/GoalPanel.java | 68 ++++++ .../goaltracker/ui/GoalTrackerPanel.java | 100 ++++++-- .../goaltracker/ui/TaskItemContent.java | 43 ++++ .../ui/components/ListItemPanel.java | 18 +- .../ui/components/ListTaskPanel.java | 223 ++++++++++++++---- 11 files changed, 711 insertions(+), 75 deletions(-) create mode 100644 src/main/java/com/toofifty/goaltracker/models/ActionHistory.java create mode 100644 src/main/java/com/toofifty/goaltracker/models/IndentChangeAction.java create mode 100644 src/main/java/com/toofifty/goaltracker/models/RemoveTaskAction.java create mode 100644 src/main/java/com/toofifty/goaltracker/models/ReorderTaskAction.java create mode 100644 src/main/java/com/toofifty/goaltracker/models/ToggleCompleteAction.java create mode 100644 src/main/java/com/toofifty/goaltracker/models/UndoStack.java diff --git a/src/main/java/com/toofifty/goaltracker/models/ActionHistory.java b/src/main/java/com/toofifty/goaltracker/models/ActionHistory.java new file mode 100644 index 0000000..d1ec2c8 --- /dev/null +++ b/src/main/java/com/toofifty/goaltracker/models/ActionHistory.java @@ -0,0 +1,69 @@ +package com.toofifty.goaltracker.models; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Generic history manager for undo/redo of user actions. + * + * Usage: + * - Define actions implementing {@link ActionHistory.Action} + * - Call {@link #push(Action)} whenever an action is performed + * - Call {@link #undo()} or {@link #redo()} from UI buttons + */ +public class ActionHistory +{ + public interface Action + { + void undo(); + void redo(); + } + + private final Deque undoStack = new ArrayDeque<>(); + private final Deque redoStack = new ArrayDeque<>(); + + /** Push a new action, clearing the redo history. */ + public void push(Action action) + { + undoStack.addLast(action); + redoStack.clear(); + } + + /** Undo the most recent action, if any. */ + public void undo() + { + Action a = undoStack.pollLast(); + if (a != null) + { + a.undo(); + redoStack.addLast(a); + } + } + + /** Redo the most recently undone action, if any. */ + public void redo() + { + Action a = redoStack.pollLast(); + if (a != null) + { + a.redo(); + undoStack.addLast(a); + } + } + + public boolean hasUndo() + { + return !undoStack.isEmpty(); + } + + public boolean hasRedo() + { + return !redoStack.isEmpty(); + } + + public void clear() + { + undoStack.clear(); + redoStack.clear(); + } +} diff --git a/src/main/java/com/toofifty/goaltracker/models/IndentChangeAction.java b/src/main/java/com/toofifty/goaltracker/models/IndentChangeAction.java new file mode 100644 index 0000000..357414e --- /dev/null +++ b/src/main/java/com/toofifty/goaltracker/models/IndentChangeAction.java @@ -0,0 +1,35 @@ +package com.toofifty.goaltracker.models; + +import java.util.List; +import com.toofifty.goaltracker.models.task.Task; + +/** + * Action for indenting or outdenting a task in a list. + */ +public class IndentChangeAction implements ActionHistory.Action +{ + private final List tasks; + private final Task task; + private final int oldIndent; + private final int newIndent; + + public IndentChangeAction(List tasks, Task task, int oldIndent, int newIndent) + { + this.tasks = tasks; + this.task = task; + this.oldIndent = oldIndent; + this.newIndent = newIndent; + } + + @Override + public void undo() + { + task.setIndentLevel(oldIndent); + } + + @Override + public void redo() + { + task.setIndentLevel(newIndent); + } +} diff --git a/src/main/java/com/toofifty/goaltracker/models/RemoveTaskAction.java b/src/main/java/com/toofifty/goaltracker/models/RemoveTaskAction.java new file mode 100644 index 0000000..f1138bd --- /dev/null +++ b/src/main/java/com/toofifty/goaltracker/models/RemoveTaskAction.java @@ -0,0 +1,35 @@ +package com.toofifty.goaltracker.models; + +import com.toofifty.goaltracker.models.task.Task; + +import java.util.List; + +/** + * Action for removing a task from a list. + */ +public class RemoveTaskAction implements ActionHistory.Action +{ + private final List tasks; + private final Task task; + private final int index; + + public RemoveTaskAction(List tasks, Task task, int index) + { + this.tasks = tasks; + this.task = task; + this.index = index; + } + + @Override + public void undo() + { + int insertIndex = Math.max(0, Math.min(index, tasks.size())); + tasks.add(insertIndex, task); + } + + @Override + public void redo() + { + tasks.remove(task); + } +} diff --git a/src/main/java/com/toofifty/goaltracker/models/ReorderTaskAction.java b/src/main/java/com/toofifty/goaltracker/models/ReorderTaskAction.java new file mode 100644 index 0000000..6f7c50f --- /dev/null +++ b/src/main/java/com/toofifty/goaltracker/models/ReorderTaskAction.java @@ -0,0 +1,40 @@ +package com.toofifty.goaltracker.models; + +import com.toofifty.goaltracker.models.task.Task; + +import java.util.List; + +/** + * Action for reordering a task within a list (move up/down). + */ +public class ReorderTaskAction implements ActionHistory.Action +{ + private final List tasks; + private final Task task; + private final int oldIndex; + private final int newIndex; + + public ReorderTaskAction(List tasks, Task task, int oldIndex, int newIndex) + { + this.tasks = tasks; + this.task = task; + this.oldIndex = oldIndex; + this.newIndex = newIndex; + } + + @Override + public void undo() + { + tasks.remove(task); + int insertIndex = Math.max(0, Math.min(oldIndex, tasks.size())); + tasks.add(insertIndex, task); + } + + @Override + public void redo() + { + tasks.remove(task); + int insertIndex = Math.max(0, Math.min(newIndex, tasks.size())); + tasks.add(insertIndex, task); + } +} diff --git a/src/main/java/com/toofifty/goaltracker/models/ToggleCompleteAction.java b/src/main/java/com/toofifty/goaltracker/models/ToggleCompleteAction.java new file mode 100644 index 0000000..8416722 --- /dev/null +++ b/src/main/java/com/toofifty/goaltracker/models/ToggleCompleteAction.java @@ -0,0 +1,55 @@ +package com.toofifty.goaltracker.models; + +import com.toofifty.goaltracker.models.task.Task; +import java.lang.reflect.Method; + +/** + * Action for toggling a task's completion state. + */ +public class ToggleCompleteAction implements ActionHistory.Action +{ + private final Task task; + private final boolean oldValue; + private final boolean newValue; + + public ToggleCompleteAction(Task task, boolean oldValue, boolean newValue) + { + this.task = task; + this.oldValue = oldValue; + this.newValue = newValue; + } + + @Override + public void undo() + { + setStatusByName(oldValue ? "COMPLETED" : "NOT_STARTED"); + } + + @Override + public void redo() + { + setStatusByName(newValue ? "COMPLETED" : "NOT_STARTED"); + } + + /** + * Set task status by enum name without importing the enum type. + * This works whether Status is a nested enum or a top-level type. + */ + private void setStatusByName(String name) + { + try + { + Object current = task.getStatus(); + Class enumClass = current.getClass(); + @SuppressWarnings({"unchecked","rawtypes"}) + Enum newStatus = Enum.valueOf((Class) enumClass, name); + + Method m = task.getClass().getMethod("setStatus", enumClass); + m.invoke(task, newStatus); + } + catch (Exception ignored) + { + // If we can't reflectively set it, ignore rather than crash. + } + } +} diff --git a/src/main/java/com/toofifty/goaltracker/models/UndoStack.java b/src/main/java/com/toofifty/goaltracker/models/UndoStack.java new file mode 100644 index 0000000..7a561be --- /dev/null +++ b/src/main/java/com/toofifty/goaltracker/models/UndoStack.java @@ -0,0 +1,100 @@ +package com.toofifty.goaltracker.models; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Objects; + +/** + * Generic undo/redo stack for removed items (e.g., Goals, Tasks). + * + * Flow: + * - When an item is removed from a list, call {@link #pushRemove(Object, int)}. + * This records the item and its original index, and clears the redo stack. + * - To undo the removal: call {@link #popForUndo()} and re-insert the item at the + * returned index. The entry is moved to the redo stack so it can be re-applied. + * - To redo the removal (only valid after an undo): call {@link #popForRedo()} and + * remove that item again. The entry moves back to the undo stack. + */ +public final class UndoStack +{ + public static final class RemovedEntry + { + private final T item; + private final int index; + + public RemovedEntry(T item, int index) + { + this.item = Objects.requireNonNull(item, "item"); + this.index = index; + } + + public T getItem() + { + return item; + } + + public int getIndex() + { + return index; + } + } + + private final Deque> undo = new ArrayDeque<>(); + private final Deque> redo = new ArrayDeque<>(); + + /** Record a removal and clear redo history. */ + public void pushRemove(T item, int index) + { + undo.addLast(new RemovedEntry<>(item, index)); + redo.clear(); + } + + /** + * Prepare to UNDO the most-recent removal. + * + * Moves the entry from the undo stack to the redo stack and returns it. + * Caller should re-insert the item at {@code getIndex()}. + */ + public RemovedEntry popForUndo() + { + RemovedEntry e = undo.pollLast(); + if (e != null) + { + redo.addLast(e); + } + return e; + } + + /** + * Prepare to REDO the most-recent undo (i.e., remove again). + * + * Moves the entry from the redo stack back to the undo stack and returns it. + * Caller should remove the item again. + */ + public RemovedEntry popForRedo() + { + RemovedEntry e = redo.pollLast(); + if (e != null) + { + undo.addLast(e); + } + return e; + } + + public boolean hasUndo() + { + return !undo.isEmpty(); + } + + public boolean hasRedo() + { + return !redo.isEmpty(); + } + + /** Clears all history. */ + public void clear() + { + undo.clear(); + redo.clear(); + } +} diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java index 3e33dd3..a1bf056 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java @@ -2,6 +2,8 @@ import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; +import com.toofifty.goaltracker.models.ActionHistory; +import com.toofifty.goaltracker.models.RemoveTaskAction; import com.toofifty.goaltracker.models.enums.TaskType; import com.toofifty.goaltracker.models.enums.Status; import com.toofifty.goaltracker.models.task.ManualTask; @@ -31,6 +33,10 @@ public class GoalPanel extends JPanel implements Refreshable private Consumer taskAddedListener; private Consumer taskUpdatedListener; + private final ActionHistory actionHistory = new ActionHistory(); + private JButton undoButton; + private JButton redoButton; + GoalPanel(GoalTrackerPlugin plugin, Goal goal, Runnable closeListener) { super(); @@ -43,6 +49,35 @@ public class GoalPanel extends JPanel implements Refreshable headerPanel.setBorder(new EmptyBorder(0, 0, 8, 0)); add(headerPanel, BorderLayout.NORTH); + // Goal List Action Bar: Back, Undo, Redo + JPanel goalListActionBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); + JButton backButton = new JButton("Back"); + undoButton = new JButton("Undo"); + redoButton = new JButton("Redo"); + + Font smallFont = backButton.getFont().deriveFont(Font.PLAIN, backButton.getFont().getSize() - 2f); + Dimension smallSize = new Dimension(65, 22); + + backButton.setFont(smallFont); + undoButton.setFont(smallFont); + redoButton.setFont(smallFont); + + backButton.setPreferredSize(smallSize); + undoButton.setPreferredSize(smallSize); + redoButton.setPreferredSize(smallSize); + + backButton.addActionListener(e -> closeListener.run()); + undoButton.addActionListener(e -> doUndo()); + redoButton.addActionListener(e -> doRedo()); + + goalListActionBar.add(backButton); + goalListActionBar.add(undoButton); + goalListActionBar.add(redoButton); + goalListActionBar.setBorder(new EmptyBorder(0, 0, 10, 0)); // add spacing below + headerPanel.add(goalListActionBar, BorderLayout.NORTH); + + updateUndoRedoButtons(); + descriptionInput = new EditableInput((value) -> { goal.setDescription(value); this.goalUpdatedListener.accept(goal); @@ -59,7 +94,9 @@ public class GoalPanel extends JPanel implements Refreshable taskListPanel = new ListPanel<>(goal.getTasks(), (task) -> { ListTaskPanel taskPanel = new ListTaskPanel(goal.getTasks(), task); + taskPanel.setActionHistory(actionHistory); TaskItemContent taskContent = new TaskItemContent(plugin, goal, task); + taskContent.setActionHistory(actionHistory); taskPanel.add(taskContent); taskPanel.setTaskContent(taskContent); taskContent.refresh(); @@ -78,6 +115,11 @@ public class GoalPanel extends JPanel implements Refreshable this.refresh(); }); + taskPanel.onRemovedWithIndex((removedTask, index) -> { + actionHistory.push(new RemoveTaskAction(goal.getTasks(), removedTask, index)); + updateUndoRedoButtons(); + }); + return taskPanel; }); taskListPanel.setGap(0); @@ -201,4 +243,30 @@ private JTextComponent findTextComponent(Component c) } return null; } + + private void updateUndoRedoButtons() + { + if (undoButton != null) + { + undoButton.setEnabled(actionHistory.hasUndo()); + } + if (redoButton != null) + { + redoButton.setEnabled(actionHistory.hasRedo()); + } + } + + private void doUndo() + { + actionHistory.undo(); + refreshTaskList(); + updateUndoRedoButtons(); + } + + private void doRedo() + { + actionHistory.redo(); + refreshTaskList(); + updateUndoRedoButtons(); + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 55691b5..16cc190 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -3,6 +3,7 @@ import com.toofifty.goaltracker.GoalManager; import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; +import com.toofifty.goaltracker.models.UndoStack; import com.toofifty.goaltracker.models.task.Task; import com.toofifty.goaltracker.ui.components.ListItemPanel; import com.toofifty.goaltracker.ui.components.ListPanel; @@ -25,6 +26,10 @@ public class GoalTrackerPanel extends PluginPanel implements Refreshable private final JPanel mainPanel = new JPanel(new BorderLayout()); private final ListPanel goalListPanel; private final GoalTrackerPlugin plugin; + private final GoalManager goalManager; + private final UndoStack undoStack = new UndoStack<>(); + private TextButton undoButtonRef; + private TextButton redoButtonRef; private GoalPanel goalPanel; private Consumer goalAddedListener; private Consumer goalUpdatedListener; @@ -36,6 +41,7 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) { super(false); this.plugin = plugin; + this.goalManager = goalManager; setBackground(ColorScheme.DARK_GRAY_COLOR); setLayout(new BorderLayout()); @@ -98,6 +104,9 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) panel.onClick(e -> this.view(goal)); panel.add(new GoalItemContent(plugin, goal)); + panel.onRemovedWithIndex((removedGoal, index) -> { + recordGoalRemoval(removedGoal, index); + }); return panel; } @@ -115,30 +124,6 @@ public void view(Goal goal) { removeAll(); - // Top control bar: Back, Undo, Redo - JPanel controlBar = new JPanel(new BorderLayout()); - controlBar.setBackground(ColorScheme.DARK_GRAY_COLOR); - controlBar.setBorder(new EmptyBorder(6, 6, 6, 6)); - - JPanel leftButtons = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); - leftButtons.setOpaque(true); - leftButtons.setBackground(ColorScheme.DARK_GRAY_COLOR); - - TextButton backButton = new TextButton("< Back", e -> this.home()).narrow(); - TextButton undoButton = new TextButton("Undo", e -> { /* hook up in future */ }).narrow(); - TextButton redoButton = new TextButton("Redo", e -> { /* hook up in future */ }).narrow(); - undoButton.setEnabled(false); - redoButton.setEnabled(false); - undoButton.setToolTipText("Undo last change (coming soon)"); - redoButton.setToolTipText("Redo last change (coming soon)"); - - leftButtons.add(backButton); - leftButtons.add(undoButton); - leftButtons.add(redoButton); - controlBar.add(leftButtons, BorderLayout.WEST); - - add(controlBar, BorderLayout.NORTH); - this.goalPanel = new GoalPanel(plugin, goal, this::home); this.goalPanel.onGoalUpdated(this.goalUpdatedListener); @@ -206,4 +191,71 @@ public void onTaskAdded(Consumer listener) this.goalPanel.onTaskAdded(this.taskAddedListener); } } + + private void updateUndoRedoButtons() + { + if (undoButtonRef != null) + { + undoButtonRef.setEnabled(undoStack.hasUndo()); + undoButtonRef.setToolTipText(undoStack.hasUndo() ? null : "Nothing to undo"); + } + if (redoButtonRef != null) + { + redoButtonRef.setEnabled(undoStack.hasRedo()); + redoButtonRef.setToolTipText(undoStack.hasRedo() ? null : "Nothing to redo"); + } + } + + private void doUndo() + { + var entry = undoStack.popForUndo(); + if (entry == null) { updateUndoRedoButtons(); return; } + + java.util.List goals = goalManager.getGoals(); + int idx = Math.max(0, Math.min(entry.getIndex(), goals.size())); + goals.add(idx, entry.getItem()); + + // If we are on the home view, refresh the list; otherwise leave as-is. + if (goalPanel == null) + { + goalListPanel.tryBuildList(); + goalListPanel.refresh(); + revalidate(); + repaint(); + } + updateUndoRedoButtons(); + } + + private void doRedo() + { + var entry = undoStack.popForRedo(); + if (entry == null) { updateUndoRedoButtons(); return; } + + java.util.List goals = goalManager.getGoals(); + int idx = goals.indexOf(entry.getItem()); + if (idx >= 0) + { + goals.remove(idx); + } + // If on home view, refresh + if (goalPanel == null) + { + goalListPanel.tryBuildList(); + goalListPanel.refresh(); + revalidate(); + repaint(); + } + updateUndoRedoButtons(); + } + + /** + * Call this when a goal is removed from the home list to record it for Undo. + * @param goal the goal that was removed + * @param index the index it had before removal + */ + public void recordGoalRemoval(Goal goal, int index) + { + undoStack.pushRemove(goal, index); + updateUndoRedoButtons(); + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java index f8a97af..a0852fc 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java @@ -16,6 +16,11 @@ import static com.toofifty.goaltracker.utils.Constants.STATUS_TO_COLOR; +import com.toofifty.goaltracker.models.ActionHistory; +import com.toofifty.goaltracker.models.ToggleCompleteAction; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + public class TaskItemContent extends JPanel implements Refreshable { private final Task task; @@ -24,9 +29,13 @@ public class TaskItemContent extends JPanel implements Refreshable private final JLabel titleLabel = new JLabel(); private final JLabel iconLabel = new JLabel(); + private final GoalTrackerPlugin plugin; + private ActionHistory actionHistory; + TaskItemContent(GoalTrackerPlugin plugin, Goal goal, Task task) { super(new BorderLayout()); + this.plugin = plugin; this.task = task; this.goal = goal; iconService = plugin.getTaskIconService(); @@ -40,6 +49,40 @@ public class TaskItemContent extends JPanel implements Refreshable add(iconWrapper, BorderLayout.WEST); plugin.getUiStatusManager().addRefresher(task, this::refresh); + + // Right-click to toggle completion with ActionHistory + titleLabel.addMouseListener(new MouseAdapter() + { + private void showMenu(MouseEvent e) + { + if (!e.isPopupTrigger()) return; + + boolean currentlyComplete = "COMPLETED".equals(task.getStatus().toString()); + String label = currentlyComplete ? "Mark as Incomplete" : "Mark as Completed"; + + JPopupMenu menu = new JPopupMenu(); + JMenuItem toggle = new JMenuItem(label); + toggle.addActionListener(a -> { + ToggleCompleteAction act = new ToggleCompleteAction(task, currentlyComplete, !currentlyComplete); + act.redo(); // apply immediately + if (actionHistory != null) + { + actionHistory.push(act); // record for undo/redo + } + plugin.getUiStatusManager().refresh(goal); + }); + menu.add(toggle); + menu.show(titleLabel, e.getX(), e.getY()); + } + + @Override public void mousePressed(MouseEvent e) { showMenu(e); } + @Override public void mouseReleased(MouseEvent e) { showMenu(e); } + }); + } + + public void setActionHistory(ActionHistory history) + { + this.actionHistory = history; } @Override diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java index 71f6429..6827f19 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java @@ -9,6 +9,7 @@ import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.function.Consumer; +import java.util.function.BiConsumer; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; @@ -29,6 +30,7 @@ public class ListItemPanel extends JPanel implements Refreshable protected Consumer reorderedListener; protected Consumer removedListener; + protected BiConsumer removedWithIndexListener; public ListItemPanel(ReorderableList list, T item) { @@ -41,27 +43,29 @@ public ListItemPanel(ReorderableList list, T item) moveUp.addActionListener(e -> { list.moveUp(item); - this.reorderedListener.accept(item); + if (this.reorderedListener != null) this.reorderedListener.accept(item); }); moveDown.addActionListener(e -> { list.moveDown(item); - this.reorderedListener.accept(item); + if (this.reorderedListener != null) this.reorderedListener.accept(item); }); moveToTop.addActionListener(e -> { list.moveToTop(item); - this.reorderedListener.accept(item); + if (this.reorderedListener != null) this.reorderedListener.accept(item); }); moveToBottom.addActionListener(e -> { list.moveToBottom(item); - this.reorderedListener.accept(item); + if (this.reorderedListener != null) this.reorderedListener.accept(item); }); removeItem.addActionListener(e -> { + int index = list.indexOf(item); list.remove(item); - this.removedListener.accept(item); + if (this.removedWithIndexListener != null) this.removedWithIndexListener.accept(item, index); + if (this.removedListener != null) this.removedListener.accept(item); }); popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); @@ -168,4 +172,8 @@ public void onRemoved(Consumer removeListener) { public void onReordered(Consumer reorderListener) { this.reorderedListener = reorderListener; } + + public void onRemovedWithIndex(BiConsumer removeListener) { + this.removedWithIndexListener = removeListener; + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java index 2244d8b..d55b596 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java @@ -13,6 +13,8 @@ import java.awt.Component; import javax.swing.SwingUtilities; import java.awt.Container; +import com.toofifty.goaltracker.models.ActionHistory; +import com.toofifty.goaltracker.models.ReorderTaskAction; public class ListTaskPanel extends ListItemPanel { @@ -24,40 +26,116 @@ public class ListTaskPanel extends ListItemPanel private Consumer indentedListener; private Consumer unindentedListener; + private ActionHistory history; + + private final MouseAdapter shiftClickRemoveListener = new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.isShiftDown() && SwingUtilities.isLeftMouseButton(e)) { + removeItem.doClick(); + } + } + }; + + private void addShiftRemoveListenerRecursive(Component c) + { + c.addMouseListener(shiftClickRemoveListener); + if (c instanceof Container) + { + for (Component child : ((Container) c).getComponents()) + { + addShiftRemoveListenerRecursive(child); + } + } + } + public ListTaskPanel(ReorderableList list, Task item) { super(list, item); indentItem.addActionListener(e -> { - // Indent all of the items children - var index = list.indexOf(item); + int index = list.indexOf(item); + int baseIndent = item.getIndentLevel(); + + // Collect affected tasks (item + descendants until sibling/parent) + java.util.List affected = new java.util.ArrayList<>(); + java.util.List oldIndents = new java.util.ArrayList<>(); + + affected.add(item); + oldIndents.add(baseIndent); for (int i = index + 1; i < list.size(); i++) { var child = list.get(i); + if (child.getIndentLevel() <= baseIndent) break; + affected.add(child); + oldIndents.add(child.getIndentLevel()); + } - // If a child is less indented then this item assume its a parent node and break - if (item.getIndentLevel() >= child.getIndentLevel()) break; + ActionHistory.Action act = new ActionHistory.Action() { + @Override public void undo() { + for (int i = 0; i < affected.size(); i++) { + affected.get(i).setIndentLevel(oldIndents.get(i)); + } + } + @Override public void redo() { + for (int i = 0; i < affected.size(); i++) { + affected.get(i).setIndentLevel(oldIndents.get(i) + 1); + } + } + }; - child.indent(); + if (history != null) { + act.redo(); + history.push(act); + } else { + // Fallback: apply directly + for (int i = 0; i < affected.size(); i++) { + affected.get(i).setIndentLevel(oldIndents.get(i) + 1); + } } - item.indent(); if (this.indentedListener != null) this.indentedListener.accept(item); refreshParentList(); }); unindentItem.addActionListener(e -> { - // Unindent all of the items children - var index = list.indexOf(item); + int index = list.indexOf(item); + int baseIndent = item.getIndentLevel(); + + // Collect affected tasks (item + descendants until sibling/parent) + java.util.List affected = new java.util.ArrayList<>(); + java.util.List oldIndents = new java.util.ArrayList<>(); + + affected.add(item); + oldIndents.add(baseIndent); for (int i = index + 1; i < list.size(); i++) { var child = list.get(i); + if (child.getIndentLevel() <= baseIndent) break; + affected.add(child); + oldIndents.add(child.getIndentLevel()); + } - // If a child is less indented then this item assume its a parent node and break - if (item.getIndentLevel() >= child.getIndentLevel()) break; + ActionHistory.Action act = new ActionHistory.Action() { + @Override public void undo() { + for (int i = 0; i < affected.size(); i++) { + affected.get(i).setIndentLevel(oldIndents.get(i)); + } + } + @Override public void redo() { + for (int i = 0; i < affected.size(); i++) { + affected.get(i).setIndentLevel(Math.max(0, oldIndents.get(i) - 1)); + } + } + }; - child.unindent(); + if (history != null) { + act.redo(); + history.push(act); + } else { + for (int i = 0; i < affected.size(); i++) { + affected.get(i).setIndentLevel(Math.max(0, oldIndents.get(i) - 1)); + } } - item.unindent(); if (this.unindentedListener != null) this.unindentedListener.accept(item); refreshParentList(); }); @@ -66,35 +144,13 @@ public ListTaskPanel(ReorderableList list, Task item) @Override public void mouseClicked(MouseEvent e) { if (e.isShiftDown() && e.getButton() == MouseEvent.BUTTON1) { - int index = list.indexOf(item); - int baseIndent = item.getIndentLevel(); - // Remove all children that are more indented than this item - while (index + 1 < list.size() && list.get(index + 1).getIndentLevel() > baseIndent) { - list.remove(list.get(index + 1)); - } - // Remove the item itself + // Delegate to the Remove action (which cascades children and notifies listeners) removeItem.doClick(); - refreshParentList(); } } }); - // Also apply the same shift-click removal listener to all child components - for (Component child : getComponents()) { - child.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.isShiftDown() && e.getButton() == MouseEvent.BUTTON1) { - int index = list.indexOf(item); - int baseIndent = item.getIndentLevel(); - while (index + 1 < list.size() && list.get(index + 1).getIndentLevel() > baseIndent) { - list.remove(list.get(index + 1)); - } - removeItem.doClick(); - refreshParentList(); - } - } - }); - } + // Apply the same shift-click removal listener to all nested components + addShiftRemoveListenerRecursive(this); } @Override @@ -104,18 +160,56 @@ public void refreshMenu() javax.swing.JMenu moveMenu = new javax.swing.JMenu("Move"); boolean hasMove = false; if (!list.isFirst(item)) { + for (var l : moveUp.getActionListeners()) moveUp.removeActionListener(l); + moveUp.addActionListener(e -> { + int oldIndex = list.indexOf(item); + int newIndex = Math.max(0, oldIndex - 1); + list.moveUp(item); + if (history != null) { + history.push(new ReorderTaskAction(list, item, oldIndex, newIndex)); + } + refreshParentList(); + }); moveMenu.add(moveUp); hasMove = true; } if (!list.isLast(item)) { + for (var l : moveDown.getActionListeners()) moveDown.removeActionListener(l); + moveDown.addActionListener(e -> { + int oldIndex = list.indexOf(item); + int newIndex = Math.min(list.size() - 1, oldIndex + 1); + list.moveDown(item); + if (history != null) { + history.push(new ReorderTaskAction(list, item, oldIndex, newIndex)); + } + refreshParentList(); + }); moveMenu.add(moveDown); hasMove = true; } if (!list.isFirst(item)) { + for (var l : moveToTop.getActionListeners()) moveToTop.removeActionListener(l); + moveToTop.addActionListener(e -> { + int oldIndex = list.indexOf(item); + list.moveToTop(item); + if (history != null) { + history.push(new ReorderTaskAction(list, item, oldIndex, 0)); + } + refreshParentList(); + }); moveMenu.add(moveToTop); hasMove = true; } if (!list.isLast(item)) { + for (var l : moveToBottom.getActionListeners()) moveToBottom.removeActionListener(l); + moveToBottom.addActionListener(e -> { + int oldIndex = list.indexOf(item); + list.moveToBottom(item); + if (history != null) { + history.push(new ReorderTaskAction(list, item, oldIndex, list.size() - 1)); + } + refreshParentList(); + }); moveMenu.add(moveToBottom); hasMove = true; } @@ -136,24 +230,50 @@ public void refreshMenu() String toggleLabel = "Mark as " + (item.getStatus() == Status.COMPLETED ? "Incomplete" : "Completed"); JMenuItem toggleStatusItem = new JMenuItem(toggleLabel); toggleStatusItem.addActionListener(e -> { + // Determine new status for the root item Status newStatus = (item.getStatus() == Status.COMPLETED ? Status.NOT_STARTED : Status.COMPLETED); - item.setStatus(newStatus); - // Cascade status change to all indented children + // Collect affected tasks (item + descendants until sibling/parent) and their old statuses + java.util.List affected = new java.util.ArrayList<>(); + java.util.List oldStatuses = new java.util.ArrayList<>(); + int index = list.indexOf(item); int baseIndent = item.getIndentLevel(); + affected.add(item); + oldStatuses.add(item.getStatus()); for (int i = index + 1; i < list.size(); i++) { Task child = list.get(i); - if (child.getIndentLevel() <= baseIndent) { - break; // stop at siblings or parents + if (child.getIndentLevel() <= baseIndent) break; + affected.add(child); + oldStatuses.add(child.getStatus()); + } + + ActionHistory.Action act = new ActionHistory.Action() { + @Override public void undo() { + for (int i = 0; i < affected.size(); i++) { + affected.get(i).setStatus(oldStatuses.get(i)); + } + } + @Override public void redo() { + for (Task t : affected) { + t.setStatus(newStatus); + } + } + }; + + if (history != null) { + act.redo(); // apply change now + history.push(act); // record for undo/redo + } else { + // Fallback if no history injected + for (Task t : affected) { + t.setStatus(newStatus); } - child.setStatus(newStatus); } if (taskContent != null) { taskContent.refresh(); } - // Refresh the entire list panel so UI updates immediately refreshParentList(); }); popupMenu.add(toggleStatusItem); @@ -222,14 +342,18 @@ public void refreshMenu() removeItem.removeActionListener(l); } removeItem.addActionListener(e -> { - int index = list.indexOf(item); + int removedIndex = list.indexOf(item); int baseIndent = item.getIndentLevel(); // Remove all children more indented than this item - while (index + 1 < list.size() && list.get(index + 1).getIndentLevel() > baseIndent) { - list.remove(list.get(index + 1)); + while (removedIndex + 1 < list.size() && list.get(removedIndex + 1).getIndentLevel() > baseIndent) { + list.remove(list.get(removedIndex + 1)); } // Remove the item itself list.remove(item); + // Notify listeners for Undo/Redo support + if (this.removedWithIndexListener != null) this.removedWithIndexListener.accept(item, removedIndex); + if (this.removedListener != null) this.removedListener.accept(item); + // Refresh UI refreshParentList(); }); @@ -247,6 +371,13 @@ public void onUnindented(Consumer unindentedListener) { public void setTaskContent(TaskItemContent taskContent) { this.taskContent = taskContent; + if (taskContent != null) { + addShiftRemoveListenerRecursive(taskContent); + } + } + + public void setActionHistory(ActionHistory history) { + this.history = history; } private void refreshParentList() From 4277664a1cbfd5914989df127a694c1c9e07cd9c Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Tue, 19 Aug 2025 22:58:57 -0400 Subject: [PATCH 23/53] it's working again!!! --- .../goaltracker/ui/GoalItemContent.java | 37 +++++++++++ .../goaltracker/ui/TaskItemContent.java | 44 +++++++++---- .../ui/components/ListItemPanel.java | 63 ++++++++++++++++++- 3 files changed, 130 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java index 00dc54f..f9eb3d7 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java @@ -3,9 +3,16 @@ import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; +import com.toofifty.goaltracker.ui.Refreshable; + import javax.swing.*; import java.awt.*; +import com.toofifty.goaltracker.ui.components.ListItemPanel; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.SwingUtilities; + import static com.toofifty.goaltracker.utils.Constants.STATUS_TO_COLOR; public class GoalItemContent extends JPanel implements Refreshable @@ -24,6 +31,36 @@ public class GoalItemContent extends JPanel implements Refreshable add(progress, BorderLayout.EAST); plugin.getUiStatusManager().addRefresher(goal, this::refresh); + + // Ensure right-click on any child shows the parent ListItemPanel context menu + MouseAdapter forwardPopup = new MouseAdapter() + { + private void maybeShow(MouseEvent e) + { + if (!(e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))) + { + return; + } + java.awt.Component src = (java.awt.Component) e.getSource(); + javax.swing.JComponent listItem = (javax.swing.JComponent) SwingUtilities.getAncestorOfClass(ListItemPanel.class, src); + if (listItem == null) + { + listItem = (javax.swing.JComponent) SwingUtilities.getAncestorOfClass(ListItemPanel.class, GoalItemContent.this); + } + if (listItem != null && listItem.getComponentPopupMenu() != null) + { + java.awt.Point p = SwingUtilities.convertPoint(src, e.getPoint(), listItem); + listItem.getComponentPopupMenu().show(listItem, p.x, p.y); + } + } + + @Override public void mousePressed(MouseEvent e) { maybeShow(e); } + @Override public void mouseReleased(MouseEvent e) { maybeShow(e); } + }; + + this.addMouseListener(forwardPopup); + title.addMouseListener(forwardPopup); + progress.addMouseListener(forwardPopup); } @Override diff --git a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java index a0852fc..7a78151 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java @@ -9,6 +9,8 @@ import java.util.List; import com.toofifty.goaltracker.ui.components.ListPanel; +import com.toofifty.goaltracker.ui.components.ListItemPanel; +import javax.swing.SwingUtilities; import javax.swing.*; import javax.swing.border.EmptyBorder; @@ -20,6 +22,7 @@ import com.toofifty.goaltracker.models.ToggleCompleteAction; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import com.toofifty.goaltracker.models.enums.Status; public class TaskItemContent extends JPanel implements Refreshable { @@ -51,33 +54,52 @@ public class TaskItemContent extends JPanel implements Refreshable plugin.getUiStatusManager().addRefresher(task, this::refresh); // Right-click to toggle completion with ActionHistory - titleLabel.addMouseListener(new MouseAdapter() + MouseAdapter contextMenuListener = new MouseAdapter() { - private void showMenu(MouseEvent e) + private void showMenuIfNeeded(MouseEvent e) { - if (!e.isPopupTrigger()) return; - - boolean currentlyComplete = "COMPLETED".equals(task.getStatus().toString()); + if (!(e.isPopupTrigger() || javax.swing.SwingUtilities.isRightMouseButton(e))) + { + return; + } + // Prefer the parent ListItemPanel context menu (move up/down/remove, etc.) + Component src = (Component) e.getSource(); + JComponent listItem = (JComponent) SwingUtilities.getAncestorOfClass(ListItemPanel.class, src); + if (listItem != null && listItem.getComponentPopupMenu() != null) + { + Point p = SwingUtilities.convertPoint(src, e.getPoint(), listItem); + listItem.getComponentPopupMenu().show(listItem, p.x, p.y); + return; + } + + // Fallback: show simple toggle menu if no parent popup menu is available + boolean currentlyComplete = task.getStatus() == Status.COMPLETED; String label = currentlyComplete ? "Mark as Incomplete" : "Mark as Completed"; JPopupMenu menu = new JPopupMenu(); JMenuItem toggle = new JMenuItem(label); toggle.addActionListener(a -> { ToggleCompleteAction act = new ToggleCompleteAction(task, currentlyComplete, !currentlyComplete); - act.redo(); // apply immediately + act.redo(); if (actionHistory != null) { - actionHistory.push(act); // record for undo/redo + actionHistory.push(act); } plugin.getUiStatusManager().refresh(goal); }); menu.add(toggle); - menu.show(titleLabel, e.getX(), e.getY()); + Component invoker = (Component) e.getSource(); + menu.show(invoker, e.getX(), e.getY()); } - @Override public void mousePressed(MouseEvent e) { showMenu(e); } - @Override public void mouseReleased(MouseEvent e) { showMenu(e); } - }); + @Override public void mousePressed(MouseEvent e) { showMenuIfNeeded(e); } + @Override public void mouseReleased(MouseEvent e) { showMenuIfNeeded(e); } + }; + + // Attach listener to multiple components to make right-click reliable across platforms + this.addMouseListener(contextMenuListener); + titleLabel.addMouseListener(contextMenuListener); + iconLabel.addMouseListener(contextMenuListener); } public void setActionHistory(ActionHistory history) diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java index 6827f19..760a40c 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java @@ -32,6 +32,48 @@ public class ListItemPanel extends JPanel implements Refreshable protected Consumer removedListener; protected BiConsumer removedWithIndexListener; + private MouseAdapter clickListenerAdapter; + + private void addClickListenerRecursive(Component c) + { + if (clickListenerAdapter == null) return; + c.addMouseListener(clickListenerAdapter); + if (c instanceof java.awt.Container) + { + for (Component child : ((java.awt.Container) c).getComponents()) + { + addClickListenerRecursive(child); + } + } + } + + private void addContextMenuListenerRecursive(Component c) + { + c.addMouseListener(contextMenuListener); + if (c instanceof java.awt.Container) + { + for (Component child : ((java.awt.Container) c).getComponents()) + { + addContextMenuListenerRecursive(child); + } + } + } + + private final MouseAdapter contextMenuListener = new MouseAdapter() + { + private void maybeShow(MouseEvent e) + { + if (!(e.isPopupTrigger() || javax.swing.SwingUtilities.isRightMouseButton(e))) + { + return; + } + popupMenu.show((Component) e.getSource(), e.getX(), e.getY()); + } + + @Override public void mousePressed(MouseEvent e) { maybeShow(e); } + @Override public void mouseReleased(MouseEvent e) { maybeShow(e); } + }; + public ListItemPanel(ReorderableList list, T item) { super(new BorderLayout()); @@ -71,6 +113,11 @@ public ListItemPanel(ReorderableList list, T item) popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); setComponentPopupMenu(popupMenu); + // Also show context menu on press/release to handle platform differences + this.addMouseListener(contextMenuListener); + // Ensure popup and click listeners cover all descendants initially + addContextMenuListenerRecursive(this); + setOpaque(true); } @Override @@ -136,17 +183,20 @@ public void refreshMenu() public ListItemPanel add(Component comp) { super.add(comp, BorderLayout.CENTER); + addContextMenuListenerRecursive(comp); + if (clickListenerAdapter != null) addClickListenerRecursive(comp); return this; } public void onClick(Consumer clickListener) { - addMouseListener(new MouseAdapter() + clickListenerAdapter = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { - if (e.getButton() == MouseEvent.BUTTON1) { + if (e.getButton() == MouseEvent.BUTTON1) + { clickListener.accept(e); } } @@ -162,7 +212,14 @@ public void mouseExited(MouseEvent e) { setBackground(ColorScheme.DARK_GRAY_COLOR); } - }); + }; + + // Attach to this panel and all current children + addMouseListener(clickListenerAdapter); + addClickListenerRecursive(this); + + // Optional: use a hand cursor to indicate clickability + setCursor(new java.awt.Cursor(java.awt.Cursor.HAND_CURSOR)); } public void onRemoved(Consumer removeListener) { From 321fc724da0758471f58813fe18a78e78f1c2142 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Wed, 20 Aug 2025 00:19:45 -0400 Subject: [PATCH 24/53] Lots O Changes --- changelog.md | 18 +++- .../com/toofifty/goaltracker/GoalManager.java | 26 +++--- .../toofifty/goaltracker/ui/GoalPanel.java | 38 +++------ .../goaltracker/ui/GoalTrackerPanel.java | 62 +++++++------- .../goaltracker/ui/components/ActionBar.java | 35 ++++++++ .../ui/components/ActionBarButton.java | 82 +++++++++++++++++++ .../goaltracker/ui/components/ListPanel.java | 11 +-- 7 files changed, 199 insertions(+), 73 deletions(-) create mode 100644 src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java create mode 100644 src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java diff --git a/changelog.md b/changelog.md index bab8497..ee22c2b 100644 --- a/changelog.md +++ b/changelog.md @@ -17,6 +17,8 @@ ### Added - Right-click menu option **Add pre-reqs** for quest tasks with prerequisites. Automatically inserts missing prerequisites as subtasks with proper indentation and prevents duplicates. +- New `ActionBar` and `ActionBarButton` UI components introduced for consistent toolbar styling across panels. +- Cursor hover state and hand cursor indicator for goal list items to improve usability. ### Changed - Right-click context menu reorganized: Move actions grouped under a **Move** submenu; Remove action now labeled as **Remove (Shift+Left Click)**. @@ -33,4 +35,18 @@ - Goal name input updated to support copy, paste, cut, and select-all actions via both context menu and keyboard shortcuts. - Remove menu option enhanced to also delete all indented child tasks when removing a parent. - Remove menu label updated so the "(Shift+Left Click)" hint displays smaller and in gray. -- Home view updated: "Goal Tracker" title moved to its own header, with a new action bar beneath it containing **+ Add goal**, **Move**, and **Bulk Edit** buttons (the latter two are placeholders marked "Coming soon"). \ No newline at end of file +- Home view updated: "Goal Tracker" title moved to its own header, with a new action bar beneath it containing **+ Add goal**, **Move**, and **Bulk Edit** buttons (the latter two are placeholders marked "Coming soon"). +- Home panel action bar refactored to use new `ActionBar` and `ActionBarButton` components for consistent styling. +- Goal view header updated to use unified `ActionBar` with Back (left) and Undo/Redo (right) buttons. +- ActionBar now includes a center spacer to properly separate left and right button groups. +- Right-click context menus fixed to reliably open across platforms (Windows, macOS, Linux). +- Task right-click now defaults to showing full ListItemPanel menu (move, remove, etc.), with fallback to toggle-only menu if no parent menu exists. +- Goal item context menus updated to forward right-clicks to parent ListItemPanel menus for consistent options. +- Cursor/hover detection improved on home goal list: listeners now attach recursively to all child components for accurate selection and highlighting. + +### Fixed +- Home panel Undo/Redo buttons removed; these controls now exist only in Goal view. +- ActionBarButton painting fixed: clears background correctly, text always drawn on top, and hover state no longer causes overlapping artifacts. +- GoalTrackerPanel `home()` method fixed so returning from Goal view refreshes and displays the goal list instead of a blank panel. +- ListPanel `refresh()` updated to rebuild list before refreshing children, preventing stale or empty views. +- Improved accuracy of mouse selection in home goal list; click/hover listeners now consistently cover the entire item area. \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/GoalManager.java b/src/main/java/com/toofifty/goaltracker/GoalManager.java index 790bf2f..14e0c17 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalManager.java +++ b/src/main/java/com/toofifty/goaltracker/GoalManager.java @@ -36,38 +36,40 @@ public Goal createGoal() } @SuppressWarnings("unchecked") - public List getTasksByTypeAndAnyStatus(TaskType type, Status ...statuses) + public List getTasksByTypeAndAnyStatus(TaskType type, Status... statuses) { List tasks = new ArrayList<>(); - for (Goal goal : goals) { + for (Goal goal : goals) + { tasks.addAll((List) goal.getTasks().stream() - .filter((task) -> task.getType() == type && Arrays.stream(statuses).anyMatch((status) -> status == task.getStatus())) - .collect(Collectors.toList()) - ); + .filter(task -> task.getType() == type && Arrays.stream(statuses).anyMatch(status -> status == task.getStatus())) + .collect(Collectors.toList())); } return tasks; } - public List getIncompleteTasksByType(TaskType type) { + public List getIncompleteTasksByType(TaskType type) + { return this.getTasksByTypeAndAnyStatus(type, Status.NOT_STARTED, Status.IN_PROGRESS); } public void save() { config.goalTrackerData(goalSerializer.serialize(goals)); - log.info("Saved " + goals.size() + " goals"); } public void load() { - try { + try + { this.goals.clear(); this.goals.addAll(goalSerializer.deserialize(config.goalTrackerData())); - log.info("Loaded " + this.goals.size() + " goals"); - } catch (Exception e) { - log.error("Failed to load goals!"); + } + catch (Exception e) + { + log.error("Failed to load goals!", e); } } -} +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java index a1bf056..17439c3 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java @@ -11,6 +11,8 @@ import com.toofifty.goaltracker.ui.components.EditableInput; import com.toofifty.goaltracker.ui.components.ListPanel; import com.toofifty.goaltracker.ui.components.ListTaskPanel; +import com.toofifty.goaltracker.ui.components.ActionBar; +import com.toofifty.goaltracker.ui.components.ActionBarButton; import javax.swing.*; import javax.swing.text.DefaultEditorKit; import javax.swing.text.JTextComponent; @@ -34,8 +36,8 @@ public class GoalPanel extends JPanel implements Refreshable private Consumer taskUpdatedListener; private final ActionHistory actionHistory = new ActionHistory(); - private JButton undoButton; - private JButton redoButton; + private ActionBarButton undoButton; + private ActionBarButton redoButton; GoalPanel(GoalTrackerPlugin plugin, Goal goal, Runnable closeListener) { @@ -49,32 +51,18 @@ public class GoalPanel extends JPanel implements Refreshable headerPanel.setBorder(new EmptyBorder(0, 0, 8, 0)); add(headerPanel, BorderLayout.NORTH); - // Goal List Action Bar: Back, Undo, Redo - JPanel goalListActionBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); - JButton backButton = new JButton("Back"); - undoButton = new JButton("Undo"); - redoButton = new JButton("Redo"); + // Goal List Action Bar: Back (left), Undo/Redo (right) + ActionBar actionBar = new ActionBar(); - Font smallFont = backButton.getFont().deriveFont(Font.PLAIN, backButton.getFont().getSize() - 2f); - Dimension smallSize = new Dimension(65, 22); + ActionBarButton backButton = new ActionBarButton("Back", closeListener::run); + undoButton = new ActionBarButton("Undo", this::doUndo); + redoButton = new ActionBarButton("Redo", this::doRedo); - backButton.setFont(smallFont); - undoButton.setFont(smallFont); - redoButton.setFont(smallFont); + actionBar.left().add(backButton); + actionBar.right().add(undoButton); + actionBar.right().add(redoButton); - backButton.setPreferredSize(smallSize); - undoButton.setPreferredSize(smallSize); - redoButton.setPreferredSize(smallSize); - - backButton.addActionListener(e -> closeListener.run()); - undoButton.addActionListener(e -> doUndo()); - redoButton.addActionListener(e -> doRedo()); - - goalListActionBar.add(backButton); - goalListActionBar.add(undoButton); - goalListActionBar.add(redoButton); - goalListActionBar.setBorder(new EmptyBorder(0, 0, 10, 0)); // add spacing below - headerPanel.add(goalListActionBar, BorderLayout.NORTH); + headerPanel.add(actionBar, BorderLayout.NORTH); updateUndoRedoButtons(); diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 16cc190..7e33c10 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -5,9 +5,10 @@ import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.UndoStack; import com.toofifty.goaltracker.models.task.Task; +import com.toofifty.goaltracker.ui.components.ActionBar; +import com.toofifty.goaltracker.ui.components.ActionBarButton; import com.toofifty.goaltracker.ui.components.ListItemPanel; import com.toofifty.goaltracker.ui.components.ListPanel; -import com.toofifty.goaltracker.ui.components.TextButton; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.FontManager; import net.runelite.client.ui.PluginPanel; @@ -28,8 +29,8 @@ public class GoalTrackerPanel extends PluginPanel implements Refreshable private final GoalTrackerPlugin plugin; private final GoalManager goalManager; private final UndoStack undoStack = new UndoStack<>(); - private TextButton undoButtonRef; - private TextButton redoButtonRef; + private ActionBarButton undoButtonRef; + private ActionBarButton redoButtonRef; private GoalPanel goalPanel; private Consumer goalAddedListener; private Consumer goalUpdatedListener; @@ -60,43 +61,33 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) title.setFont(FontManager.getRunescapeBoldFont()); titlePanel.add(title, BorderLayout.WEST); - // New action bar below the title - JPanel actionBar = new JPanel(new BorderLayout()); - actionBar.setBorder(new EmptyBorder(6, 10, 6, 10)); - actionBar.setBackground(ColorScheme.DARK_GRAY_COLOR); + // Action bar (shared style) + ActionBar actionBar = new ActionBar(); - JPanel actionsLeft = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); - actionsLeft.setOpaque(true); - actionsLeft.setBackground(ColorScheme.DARK_GRAY_COLOR); - - TextButton addGoalBtn = new TextButton("+ Add goal", - e -> { - Goal goal = goalManager.createGoal(); - view(goal); - - if (Objects.nonNull(this.goalAddedListener)) this.goalAddedListener.accept(goal); - if (Objects.nonNull(this.goalUpdatedListener)) this.goalUpdatedListener.accept(goal); - } - ).narrow(); - - TextButton moveBtn = new TextButton("Move", e -> {}).narrow(); + // Left-side actions + ActionBarButton addGoalBtn = new ActionBarButton("+ Add goal", () -> + { + Goal goal = goalManager.createGoal(); + view(goal); + if (goalAddedListener != null) goalAddedListener.accept(goal); + if (goalUpdatedListener != null) goalUpdatedListener.accept(goal); + }); + ActionBarButton moveBtn = new ActionBarButton("Move", () -> {}); moveBtn.setEnabled(false); moveBtn.setToolTipText("Coming soon"); - TextButton bulkEditBtn = new TextButton("Bulk Edit", e -> {}).narrow(); + ActionBarButton bulkEditBtn = new ActionBarButton("Bulk Edit", () -> {}); bulkEditBtn.setEnabled(false); bulkEditBtn.setToolTipText("Coming soon"); - actionsLeft.add(addGoalBtn); - actionsLeft.add(moveBtn); - actionsLeft.add(bulkEditBtn); - actionBar.add(actionsLeft, BorderLayout.WEST); + actionBar.left().add(addGoalBtn); + actionBar.left().add(moveBtn); + actionBar.left().add(bulkEditBtn); - // Stack title and action bar into a single header container JPanel headerContainer = new JPanel(new BorderLayout()); headerContainer.setBackground(ColorScheme.DARK_GRAY_COLOR); headerContainer.add(titlePanel, BorderLayout.NORTH); - headerContainer.add(actionBar, BorderLayout.SOUTH); + headerContainer.add(actionBar, BorderLayout.SOUTH ); goalListPanel = new ListPanel<>(goalManager.getGoals(), (goal) -> { @@ -139,11 +130,22 @@ public void view(Goal goal) public void home() { + // Clear the GoalTrackerPanel content and switch back to the main panel removeAll(); - add(mainPanel, BorderLayout.CENTER); + + // Rebuild the list BEFORE attaching the main panel to ensure layout has components goalListPanel.tryBuildList(); goalListPanel.refresh(); + // Make sure the list is the CENTER of the main panel (in case layout got disturbed) + mainPanel.remove(goalListPanel); + mainPanel.add(goalListPanel, BorderLayout.CENTER); + + // Attach main panel and validate + add(mainPanel, BorderLayout.CENTER); + mainPanel.revalidate(); + mainPanel.repaint(); + revalidate(); repaint(); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java new file mode 100644 index 0000000..6a4fbd5 --- /dev/null +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java @@ -0,0 +1,35 @@ +package com.toofifty.goaltracker.ui.components; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import javax.swing.JPanel; +import javax.swing.border.EmptyBorder; +import net.runelite.client.ui.ColorScheme; + +public class ActionBar extends JPanel +{ + private final JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); + private final JPanel right = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); + private final JPanel spacer = new JPanel(); + + public ActionBar() + { + super(new BorderLayout()); + setBackground(ColorScheme.DARK_GRAY_COLOR); + setBorder(new EmptyBorder(6, 10, 6, 10)); + + left.setOpaque(true); + left.setBackground(ColorScheme.DARK_GRAY_COLOR); + + right.setOpaque(true); + right.setBackground(ColorScheme.DARK_GRAY_COLOR); + + spacer.setOpaque(false); + add(left, BorderLayout.WEST); + add(right, BorderLayout.EAST); + add(spacer, BorderLayout.CENTER); + } + + public JPanel left() { return left; } + public JPanel right() { return right; } +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java new file mode 100644 index 0000000..118f68f --- /dev/null +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java @@ -0,0 +1,82 @@ +package com.toofifty.goaltracker.ui.components; + +import java.awt.Cursor; +import java.awt.Graphics; +import java.awt.Insets; +import java.awt.RenderingHints; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.JButton; +import javax.swing.border.EmptyBorder; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Dimension; + +public class ActionBarButton extends JButton +{ + private boolean hover; + + public ActionBarButton(String text, Runnable onClick) + { + super(text); + setFont(FontManager.getRunescapeSmallFont()); + setForeground(Color.WHITE); + setFocusable(false); + setOpaque(false); + setBorder(new EmptyBorder(4, 10, 4, 10)); + setMargin(new Insets(0,0,0,0)); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + setContentAreaFilled(false); + + addActionListener(e -> { if (onClick != null) onClick.run(); }); + + addMouseListener(new MouseAdapter() + { + @Override public void mouseEntered(MouseEvent e) { hover = true; repaint(); } + @Override public void mouseExited(MouseEvent e) { hover = false; repaint(); } + }); + } + + @Override + protected void paintComponent(Graphics g) + { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + Color bg = hover ? ColorScheme.DARKER_GRAY_HOVER_COLOR : ColorScheme.DARK_GRAY_COLOR.brighter(); + Color outline = ColorScheme.DARKER_GRAY_HOVER_COLOR.darker(); + + int arc = 8; + int w = getWidth(); + int h = getHeight(); + + g2.setColor(bg); + g2.fillRoundRect(0, 0, w, h, arc, arc); + + g2.setColor(outline); + g2.drawRoundRect(0, 0, w - 1, h - 1, arc, arc); + + g2.dispose(); + super.paintComponent(g); + } + + @Override + public boolean isContentAreaFilled() + { + return false; + } + + @Override + public boolean isOpaque() + { + return false; + } + + @Override + public Dimension getMinimumSize() + { + return getPreferredSize(); + } +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java index d514ffc..9ed6cb6 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java @@ -78,7 +78,10 @@ private List> buildItemPanels() @Override public void refresh() { - // refresh all children + // Rebuild the list first in case the underlying data changed + tryBuildList(); + + // Then refresh all children for (Component component : listPanel.getComponents()) { if (component instanceof Refreshable) { ((Refreshable) component).refresh(); @@ -96,14 +99,12 @@ private ListItemPanel buildItemPanel(T item) itemPanel.onReordered((updatedItem) -> { tryBuildList(); - - this.updatedListener.accept(updatedItem); + if (this.updatedListener != null) this.updatedListener.accept(updatedItem); }); itemPanel.onRemoved((updatedItem) -> { tryBuildList(); - - this.updatedListener.accept(updatedItem); + if (this.updatedListener != null) this.updatedListener.accept(updatedItem); }); itemPanelMap.put(item, itemPanel); From b50f78447ddbda9b8a9c93d1281dc666457ea733 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Wed, 20 Aug 2025 07:45:45 -0400 Subject: [PATCH 25/53] copy\paste working. --- changelog.md | 3 +- .../goaltracker/ui/GoalItemContent.java | 26 +++++++++- .../ui/components/EditableInput.java | 48 +++++++++++++++++-- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/changelog.md b/changelog.md index ee22c2b..d1a8a2b 100644 --- a/changelog.md +++ b/changelog.md @@ -49,4 +49,5 @@ - ActionBarButton painting fixed: clears background correctly, text always drawn on top, and hover state no longer causes overlapping artifacts. - GoalTrackerPanel `home()` method fixed so returning from Goal view refreshes and displays the goal list instead of a blank panel. - ListPanel `refresh()` updated to rebuild list before refreshing children, preventing stale or empty views. -- Improved accuracy of mouse selection in home goal list; click/hover listeners now consistently cover the entire item area. \ No newline at end of file +- Improved accuracy of mouse selection in home goal list; click/hover listeners now consistently cover the entire item area. +- Goal name input now fully supports keyboard shortcuts (Ctrl/Cmd+C, V, X, A, Insert/Delete variants) and right-click context menu for copy/paste across all platforms. \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java index f9eb3d7..29fded5 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java @@ -17,7 +17,7 @@ public class GoalItemContent extends JPanel implements Refreshable { - private final JLabel title = new JLabel(); + private final JTextField title = new JTextField(); private final JLabel progress = new JLabel(); private final Goal goal; @@ -28,6 +28,29 @@ public class GoalItemContent extends JPanel implements Refreshable this.goal = goal; add(title, BorderLayout.WEST); + // Make goal title editable with standard copy/paste + title.setBorder(null); + title.setOpaque(false); + title.setEditable(true); + title.setDragEnabled(true); + title.setCaretPosition(0); + // Commit edits on Enter and when focus is lost + title.addActionListener(e -> { + String newText = title.getText(); + if (newText != null) { + goal.setDescription(newText); + } + }); + title.addFocusListener(new java.awt.event.FocusAdapter() { + @Override + public void focusLost(java.awt.event.FocusEvent e) { + String newText = title.getText(); + if (newText != null) { + goal.setDescription(newText); + } + } + }); + add(progress, BorderLayout.EAST); plugin.getUiStatusManager().addRefresher(goal, this::refresh); @@ -70,6 +93,7 @@ public void refresh() title.setText(goal.getDescription()); title.setForeground(color); + title.setCaretColor(color); progress.setText( goal.getComplete().size() + "/" + goal.getTasks().size()); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/EditableInput.java b/src/main/java/com/toofifty/goaltracker/ui/components/EditableInput.java index c86da01..771fc63 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/EditableInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/EditableInput.java @@ -12,6 +12,13 @@ import javax.swing.border.Border; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; +import javax.swing.JPopupMenu; +import javax.swing.JMenuItem; +import javax.swing.KeyStroke; +import javax.swing.ActionMap; +import javax.swing.InputMap; +import javax.swing.text.DefaultEditorKit; +import java.awt.Toolkit; import lombok.Setter; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.components.FlatTextField; @@ -54,7 +61,7 @@ public EditableInput(Consumer saveAction) cancel.onClick(e -> this.cancel()); edit.onClick(e -> { - inputField.setEditable(true); + inputField.getTextField().setEditable(true); updateActions(true); }); @@ -64,12 +71,43 @@ public EditableInput(Consumer saveAction) inputField.setText(localValue); inputField.setBorder(null); - inputField.setEditable(false); + inputField.getTextField().setEditable(false); + inputField.getTextField().setFocusable(true); inputField.setBackground(ColorScheme.DARKER_GRAY_COLOR); inputField.setPreferredSize(new Dimension(50, 24)); inputField.getTextField().setForeground(Color.WHITE); inputField.getTextField().setBorder(new EmptyBorder(0, 8, 0, 0)); - inputField.addKeyListener(new KeyAdapter() + // Ensure standard copy/paste/cut/select-all shortcuts work on all platforms + final var tf = inputField.getTextField(); + InputMap im = tf.getInputMap(); + ActionMap am = tf.getActionMap(); + int menuMask = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx(); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, menuMask), DefaultEditorKit.copyAction); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, menuMask), DefaultEditorKit.pasteAction); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, menuMask), DefaultEditorKit.cutAction); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, menuMask), DefaultEditorKit.selectAllAction); + // Also support Insert/Delete variants + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, KeyEvent.SHIFT_DOWN_MASK), DefaultEditorKit.pasteAction); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, KeyEvent.CTRL_DOWN_MASK), DefaultEditorKit.copyAction); + im.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.SHIFT_DOWN_MASK), DefaultEditorKit.cutAction); + + // Context menu for mouse-based copy/paste + JPopupMenu popup = new JPopupMenu(); + JMenuItem cut = new JMenuItem("Cut"); + cut.addActionListener(e -> tf.cut()); + popup.add(cut); + JMenuItem copy = new JMenuItem("Copy"); + copy.addActionListener(e -> tf.copy()); + popup.add(copy); + JMenuItem paste = new JMenuItem("Paste"); + paste.addActionListener(e -> tf.paste()); + popup.add(paste); + JMenuItem selectAll = new JMenuItem("Select All"); + selectAll.addActionListener(e -> tf.selectAll()); + popup.add(selectAll); + tf.setComponentPopupMenu(popup); + tf.setDragEnabled(true); + inputField.getTextField().addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) @@ -91,14 +129,14 @@ private void save() localValue = inputField.getText(); saveAction.accept(localValue); - inputField.setEditable(false); + inputField.getTextField().setEditable(false); updateActions(false); requestFocusInWindow(); } private void cancel() { - inputField.setEditable(false); + inputField.getTextField().setEditable(false); inputField.setText(localValue); updateActions(false); From 7023bc4fdbdd883bbc988f46aad741add06b1a1f Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Wed, 20 Aug 2025 08:25:27 -0400 Subject: [PATCH 26/53] copy\paste working. --- .../toofifty/goaltracker/ui/GoalTrackerPanel.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 7e33c10..03634ad 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -36,6 +36,7 @@ public class GoalTrackerPanel extends PluginPanel implements Refreshable private Consumer goalUpdatedListener; private Consumer taskAddedListener; private Consumer taskUpdatedListener; + private Goal pendingNewGoal; @Inject public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) @@ -68,6 +69,7 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) ActionBarButton addGoalBtn = new ActionBarButton("+ Add goal", () -> { Goal goal = goalManager.createGoal(); + pendingNewGoal = goal; view(goal); if (goalAddedListener != null) goalAddedListener.accept(goal); if (goalUpdatedListener != null) goalUpdatedListener.accept(goal); @@ -130,6 +132,17 @@ public void view(Goal goal) public void home() { + // Auto-remove an empty goal created via "+ Add goal" if user backs out without adding tasks + if (pendingNewGoal != null) + { + try { + if (pendingNewGoal.getTasks() == null || pendingNewGoal.getTasks().isEmpty()) { + goalManager.getGoals().remove(pendingNewGoal); + } + } finally { + pendingNewGoal = null; + } + } // Clear the GoalTrackerPanel content and switch back to the main panel removeAll(); From 597830bf43f8929b5b2d4508330c6f30fad1db78 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Wed, 20 Aug 2025 08:25:56 -0400 Subject: [PATCH 27/53] updated changelog and readme. --- README.md | 84 ++++++++++++++++++++++++++++------------------------ changelog.md | 76 ++++++++++++++++++++++++++++------------------- 2 files changed, 91 insertions(+), 69 deletions(-) diff --git a/README.md b/README.md index ace6956..11a4b9f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,47 @@ +# RuneLite Goal Tracker Plugin v2 + +This is a redesigned version of the RuneLite Goal Tracker plugin with major updates and improvements to enhance your experience managing OSRS goals. + +## New Features + +- Quest prerequisites button for easy access to quest requirements +- Shift+Click removal of tasks for faster task management +- Completion cascading to automatically complete related tasks +- Dropdown quest selector for quicker quest task addition +- Right-click menus for prereq and child completion options +- Manual toggling for preset tasks to customize your workflow +- Customizable chatbox colors for notifications +- Automatic goal status checks for up-to-date progress +- New ActionBar and ActionBarButton UI components +- Hover states for better visual feedback +- New context menu organization for streamlined interaction +- Search toggle improvements for easier task searching + +## Improvements + +- More compact prereq button for a cleaner interface +- Refreshed UI with updated design elements +- Font and ComboBox readability enhancements +- Consistent ActionBar UI throughout the plugin +- Unified goal view header for a cohesive look +- Improved context menus with better usability +- Enhanced cursor and hover detection accuracy +- Copy and paste support in the goal name input field + +## Fixes + +- Undo/Redo functionality cleanup for smoother editing +- ActionBarButton painting fixes to prevent visual glitches +- GoalTrackerPanel `home()` method refresh improvements +- Correct refresh behavior in ListPanel +- Improved mouse selection accuracy +- Keyboard shortcut fixes and enhancements +- Automatic removal of empty goals to keep lists tidy +- Visual refresh issue resolved on login + +
+

Goal Tracker v1 Readme originally by dillydill123

+ # Runelite Goal Tracker Plugin Keep track of your OSRS goals and complete them automatically. @@ -61,42 +105,4 @@ Track quest progress and completion, just select a quest or miniquest from the d Select an item using the search button and searching via the in-game chatbox, then set the desired quantity. The plugin will keep track of your items and tally up quantities stored in different inventories (bank, player, GIMP storage), and will be automatically completed once you get that amount of the item. -## Changelog - -## [Unreleased] - 2025-08-16 - -### Recent Additions -- Added right-click menu to parent goals to mark all child tasks as completed or incomplete. -- Fixed visual refresh issue where quest task statuses didn’t show correctly on login unless re-entering the goal. -- Added manual completion toggling for tasks created from presets, allowing users to right-click and mark them complete/incomplete just like quick-added tasks. -- Added customizable color setting for task completion messages shown in the chatbox. -- Implemented automatic goal status checking upon login to mark goals as completed if requirements are already met. - -## [Unreleased] - 2025-08-18 - -### Added -- Quest prerequisites button: Each quest task now has an **Add prereqs** button to insert its prerequisites directly beneath it. -- Shift+Click removal: Shift+Click a task to remove it and all its indented children at once. -- Completion cascading: Marking a parent quest as complete/incomplete now automatically updates all its child tasks. -- Dropdown quest selector: Replaced fuzzy search with a clean dropdown of all quests, displaying natural names (e.g., "Tree Gnome Village"). - -### Changed -- Pre-req button made more compact (~25% smaller). -- Prerequisite insertion now places child tasks directly below their parent instead of at the end of the list. -- UI now automatically refreshes after task mutations (add/remove/indent/status change). - -## [Unreleased] - 2025-08-19 - -### Added -- Right-click menu option **Add pre-reqs** for quest tasks with prerequisites. Automatically inserts missing prerequisites as subtasks with proper indentation and prevents duplicates. - -### Changed -- Right-click context menu reorganized: Move actions grouped under a **Move** submenu; Remove action now labeled as **Remove (Shift+Left Click)**. -- Quest tasks now only display the **Add pre-reqs** option if they actually have missing prerequisites. -- Search button now toggles between **Search...** and **Close** to open/close the item search overlay. -- Removed in-panel close button; close control is now handled directly via the Search button toggle. -- Green **+Add** button hidden for item search inputs (still visible for other input types). -- Duplicate prerequisites can no longer be added multiple times to the same quest. -- Quest dropdown now uses **RuneScape UF** font at a normal crisp size for improved readability. -- ComboBox font scaling updated to use integer point sizes, preventing fuzzy text. -- QuestTaskInput updated to rely on shared ComboBox styling for consistency. \ No newline at end of file +
\ No newline at end of file diff --git a/changelog.md b/changelog.md index d1a8a2b..8f5ce33 100644 --- a/changelog.md +++ b/changelog.md @@ -1,27 +1,32 @@ # Changelog -## [Unreleased] - 2025-08-18 +## [Unreleased] ### Added -- Quest prerequisites button: Each quest task now has an **Add prereqs** button to insert its prerequisites directly beneath it. +- Quest prerequisites button: Each quest task now has an **Add prereqs** button to insert its prerequisites + directly beneath it. - Shift+Click removal: Shift+Click a task to remove it and all its indented children at once. -- Completion cascading: Marking a parent quest as complete/incomplete now automatically updates all its child tasks. -- Dropdown quest selector: Replaced fuzzy search with a clean dropdown of all quests, displaying natural names (e.g., "Tree Gnome Village"). +- Completion cascading: Marking a parent quest as complete/incomplete now automatically updates all its + child tasks. +- Dropdown quest selector: Replaced fuzzy search with a clean dropdown of all quests, displaying natural + names (e.g., "Tree Gnome Village"). +- Right-click menu option **Add pre-reqs** for quest tasks with prerequisites. Automatically inserts + missing prerequisites as subtasks with proper indentation and prevents duplicates. +- New `ActionBar` and `ActionBarButton` UI components introduced for consistent toolbar styling across + panels. +- Cursor hover state and hand cursor indicator for goal list items to improve usability. +- Added right-click menu to parent goals to mark all child tasks as completed or incomplete. +- Added manual completion toggling for tasks created from presets, allowing users to right-click and mark them complete/incomplete just like quick-added tasks. +- Added customizable color setting for task completion messages shown in the chatbox. +- Implemented automatic goal status checking upon login to mark goals as completed if requirements are already met. ### Changed - Pre-req button made more compact (~25% smaller). -- Prerequisite insertion now places child tasks directly below their parent instead of at the end of the list. +- Prerequisite insertion now places child tasks directly below their parent instead of at the end of the + list. - UI now automatically refreshes after task mutations (add/remove/indent/status change). - -## [Unreleased] - 2025-08-19 - -### Added -- Right-click menu option **Add pre-reqs** for quest tasks with prerequisites. Automatically inserts missing prerequisites as subtasks with proper indentation and prevents duplicates. -- New `ActionBar` and `ActionBarButton` UI components introduced for consistent toolbar styling across panels. -- Cursor hover state and hand cursor indicator for goal list items to improve usability. - -### Changed -- Right-click context menu reorganized: Move actions grouped under a **Move** submenu; Remove action now labeled as **Remove (Shift+Left Click)**. +- Right-click context menu reorganized: Move actions grouped under a **Move** submenu; Remove action now + labeled as **Remove (Shift+Left Click)**. - Quest tasks now only display the **Add pre-reqs** option if they actually have missing prerequisites. - Search button now toggles between **Search...** and **Close** to open/close the item search overlay. - Removed in-panel close button; close control is now handled directly via the Search button toggle. @@ -29,20 +34,29 @@ - Duplicate prerequisites can no longer be added multiple times to the same quest. - Quest dropdown now uses **RuneScape UF** font at a normal crisp size for improved readability. - ComboBox font scaling updated to use integer point sizes, preventing fuzzy text. -- QuestTaskInput updated to rely on shared ComboBox styling for consistency. -- Back and Undo buttons added to a new top control bar in goal view; later removed and replaced with a cleaner single bar design. -- Embedded red "< Back" button removed from GoalPanel header, leaving only the goal name input aligned cleanly. -- Goal name input updated to support copy, paste, cut, and select-all actions via both context menu and keyboard shortcuts. -- Remove menu option enhanced to also delete all indented child tasks when removing a parent. -- Remove menu label updated so the "(Shift+Left Click)" hint displays smaller and in gray. -- Home view updated: "Goal Tracker" title moved to its own header, with a new action bar beneath it containing **+ Add goal**, **Move**, and **Bulk Edit** buttons (the latter two are placeholders marked "Coming soon"). -- Home panel action bar refactored to use new `ActionBar` and `ActionBarButton` components for consistent styling. -- Goal view header updated to use unified `ActionBar` with Back (left) and Undo/Redo (right) buttons. -- ActionBar now includes a center spacer to properly separate left and right button groups. -- Right-click context menus fixed to reliably open across platforms (Windows, macOS, Linux). -- Task right-click now defaults to showing full ListItemPanel menu (move, remove, etc.), with fallback to toggle-only menu if no parent menu exists. -- Goal item context menus updated to forward right-clicks to parent ListItemPanel menus for consistent options. -- Cursor/hover detection improved on home goal list: listeners now attach recursively to all child components for accurate selection and highlighting. + + - Back and Undo buttons added to a new top control bar in goal view; later removed and replaced with a + cleaner single bar design. + - Embedded red "< Back" button removed from GoalPanel header, leaving only the goal name input aligned + cleanly. + - Goal name input updated to support copy, paste, cut, and select-all actions via both context menu and + keyboard shortcuts. + - Remove menu option enhanced to also delete all indented child tasks when removing a parent. + - Remove menu label updated so the "(Shift+Left Click)" hint displays smaller and in gray. + - Home view updated: "Goal Tracker" title moved to its own header, with a new action bar beneath it + containing **+ Add goal**, **Move**, and **Bulk Edit** buttons (the latter two are placeholders marked + "Coming soon"). + - Home panel action bar refactored to use new `ActionBar` and `ActionBarButton` components for consistent + styling. + - Goal view header updated to use unified `ActionBar` with Back (left) and Undo/Redo (right) buttons. + - ActionBar now includes a center spacer to properly separate left and right button groups. + - Right-click context menus fixed to reliably open across platforms (Windows, macOS, Linux). + - Task right-click now defaults to showing full ListItemPanel menu (move, remove, etc.), with fallback to + toggle-only menu if no parent menu exists. + - Goal item context menus updated to forward right-clicks to parent ListItemPanel menus for consistent + options. + - Cursor/hover detection improved on home goal list: listeners now attach recursively to all child + components for accurate selection and highlighting. ### Fixed - Home panel Undo/Redo buttons removed; these controls now exist only in Goal view. @@ -50,4 +64,6 @@ - GoalTrackerPanel `home()` method fixed so returning from Goal view refreshes and displays the goal list instead of a blank panel. - ListPanel `refresh()` updated to rebuild list before refreshing children, preventing stale or empty views. - Improved accuracy of mouse selection in home goal list; click/hover listeners now consistently cover the entire item area. -- Goal name input now fully supports keyboard shortcuts (Ctrl/Cmd+C, V, X, A, Insert/Delete variants) and right-click context menu for copy/paste across all platforms. \ No newline at end of file +- Goal name input now fully supports keyboard shortcuts (Ctrl/Cmd+C, V, X, A, Insert/Delete variants) and right-click context menu for copy/paste across all platforms. +- Empty goals (0/0 tasks) created via **+ Add goal** are now automatically removed when backing out without adding tasks, preventing clutter in saved goal lists. +- Fixed visual refresh issue where quest task statuses didn’t show correctly on login unless re-entering the goal. From 51a7775469fe1f859055b86bb9f55a4817b1f580 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Wed, 20 Aug 2025 09:01:18 -0400 Subject: [PATCH 28/53] updated readme, license and home panel header. --- LICENSE | 1 + README.md | 125 ++++++++++++------ .../goaltracker/ui/GoalTrackerPanel.java | 15 ++- 3 files changed, 97 insertions(+), 44 deletions(-) diff --git a/LICENSE b/LICENSE index 180ab2e..97a109d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ BSD 2-Clause License Copyright (c) 2019, dillydill123 +Copyright (c) 2025, AhDoozy All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/README.md b/README.md index 11a4b9f..4c2e12e 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,89 @@ -# RuneLite Goal Tracker Plugin v2 - -This is a redesigned version of the RuneLite Goal Tracker plugin with major updates and improvements to enhance your experience managing OSRS goals. - -## New Features - -- Quest prerequisites button for easy access to quest requirements -- Shift+Click removal of tasks for faster task management -- Completion cascading to automatically complete related tasks -- Dropdown quest selector for quicker quest task addition -- Right-click menus for prereq and child completion options -- Manual toggling for preset tasks to customize your workflow -- Customizable chatbox colors for notifications -- Automatic goal status checks for up-to-date progress -- New ActionBar and ActionBarButton UI components -- Hover states for better visual feedback -- New context menu organization for streamlined interaction -- Search toggle improvements for easier task searching - -## Improvements - -- More compact prereq button for a cleaner interface -- Refreshed UI with updated design elements -- Font and ComboBox readability enhancements -- Consistent ActionBar UI throughout the plugin -- Unified goal view header for a cohesive look -- Improved context menus with better usability -- Enhanced cursor and hover detection accuracy -- Copy and paste support in the goal name input field - -## Fixes - -- Undo/Redo functionality cleanup for smoother editing -- ActionBarButton painting fixes to prevent visual glitches -- GoalTrackerPanel `home()` method refresh improvements -- Correct refresh behavior in ListPanel -- Improved mouse selection accuracy -- Keyboard shortcut fixes and enhancements -- Automatic removal of empty goals to keep lists tidy -- Visual refresh issue resolved on login +# 🏆 RuneLite Goal Tracker v2 +> A complete reimagining of the Goal Tracker plugin — rebuilt with a more modern UI, powerful new features, and improved stability to help you plan, track, and achieve your Old School RuneScape goals with ease. + +--- + +## ✨ New Features + +- 🌟 Quest prerequisites button for easy access to quest requirements +- ⚡ Shift+Click removal of tasks for faster task management +- 🔗 Completion cascading to automatically complete related tasks +- 🎯 Dropdown quest selector for quicker quest task addition +- 🖱️ Right-click menus for prereq and child completion options +- 🔄 Manual toggling for preset tasks to customize your workflow +- 🎨 Customizable chatbox colors for notifications +- ⏱️ Automatic goal status checks for up-to-date progress +- 🆕 New ActionBar and ActionBarButton UI components +- 👆 Hover states for better visual feedback +- 📋 New context menu organization for streamlined interaction +- 🔍 Search toggle improvements for easier task searching + +--- + +## 🔧 Improvements + +- 🏷️ More compact prereq button for a cleaner interface +- ✨ Refreshed UI with updated design elements +- 🔠 Font and ComboBox readability enhancements +- 🧩 Consistent ActionBar UI throughout the plugin +- 🧑‍💻 Unified goal view header for a cohesive look +- 🖱️ Improved context menus with better usability +- 🎯 Enhanced cursor and hover detection accuracy +- 📋 Copy and paste support in the goal name input field + +--- +## 🐛 Fixes + +- ♻️ Undo/Redo functionality cleanup for smoother editing +- 🎨 ActionBarButton painting fixes to prevent visual glitches +- 🏠 GoalTrackerPanel `home()` method refresh improvements +- 🔄 Correct refresh behavior in ListPanel +- 🖱️ Improved mouse selection accuracy +- ⌨️ Keyboard shortcut fixes and enhancements +- 🗑️ Automatic removal of empty goals to keep lists tidy +- 🔄 Visual refresh issue resolved on login + +--- + +## 📥 Installation + +1. Open RuneLite. +2. Go to the Plugin Hub. +3. Search for "Goal Tracker v2". +4. Click **Install**. + +--- + +## 🚀 Getting Started + +- Open the plugin panel in RuneLite once installed. +- Use **+ Add goal** to create a new goal. +- Add tasks (quests, skills, items, or manual) via the goal view. +- Use the new **ActionBar** buttons for navigation, undo/redo, and bulk actions. +- Right-click goals or tasks for advanced options like adding prerequisites, marking all children complete, or removing tasks quickly. + +--- + +## 🖼️ Screenshots + +_Coming soon_ +*(Screenshots and GIFs will be added here to showcase the new UI and features.)* + +--- + +## 🙏 Acknowledgements + +- Original plugin created by **dillydill123**. +- Fully renovated and maintained by **AhDoozy**. + +--- +## 📄 License +Licensed under the [BSD 2-Clause License](LICENSE). + +-----
-

Goal Tracker v1 Readme originally by dillydill123

+

📜 Original Goal Tracker v1 Readme & Documentation (created by dillydill123)

# Runelite Goal Tracker Plugin diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 03634ad..7f640e1 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -56,11 +56,20 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) // (Removed "+ Add goal" button from the title panel) - JLabel title = new JLabel(); - title.setText("Goal Tracker"); + JLabel title = new JLabel("Goal Tracker v2"); title.setForeground(Color.WHITE); title.setFont(FontManager.getRunescapeBoldFont()); - titlePanel.add(title, BorderLayout.WEST); + + JLabel author = new JLabel("By: AhDoozy"); + author.setForeground(Color.LIGHT_GRAY); + author.setFont(title.getFont().deriveFont(title.getFont().getSize2D() - 3f)); + + JPanel titleTextPanel = new JPanel(new GridLayout(2, 1)); + titleTextPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); + titleTextPanel.add(title); + titleTextPanel.add(author); + + titlePanel.add(titleTextPanel, BorderLayout.WEST); // Action bar (shared style) ActionBar actionBar = new ActionBar(); From 79f24dbb84b59a7f1f72bba0e4c14bdbced20b19 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Wed, 20 Aug 2025 10:43:54 -0400 Subject: [PATCH 29/53] lots of UI updates --- .../goaltracker/ui/GoalItemContent.java | 5 ++++ .../toofifty/goaltracker/ui/GoalPanel.java | 8 ++++++- .../goaltracker/ui/GoalTrackerPanel.java | 9 -------- .../ui/components/ListItemPanel.java | 23 +++++++++++++++---- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java index 29fded5..263b895 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java @@ -1,4 +1,5 @@ package com.toofifty.goaltracker.ui; +import net.runelite.client.ui.ColorScheme; import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; @@ -27,6 +28,10 @@ public class GoalItemContent extends JPanel implements Refreshable super(new BorderLayout()); this.goal = goal; + setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); // padding for centered text + setBackground(ColorScheme.DARKER_GRAY_COLOR); + setOpaque(true); + add(title, BorderLayout.WEST); // Make goal title editable with standard copy/paste title.setBorder(null); diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java index 17439c3..7bb54a1 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java @@ -22,6 +22,7 @@ import java.util.Objects; import java.util.function.Consumer; import javax.swing.border.EmptyBorder; +import javax.swing.BorderFactory; import net.runelite.client.ui.ColorScheme; public class GoalPanel extends JPanel implements Refreshable @@ -88,7 +89,12 @@ public class GoalPanel extends JPanel implements Refreshable taskPanel.add(taskContent); taskPanel.setTaskContent(taskContent); taskContent.refresh(); - taskPanel.setBorder(new EmptyBorder(2, 4, 2, 4)); + taskPanel.setOpaque(true); + taskPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); + taskPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(4, 6, 0, 6, ColorScheme.DARKER_GRAY_COLOR), // darker line for contrast + new EmptyBorder(2, 4, 2, 4) + )); taskPanel.onIndented(e -> { diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 7f640e1..0dd123b 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -83,17 +83,8 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) if (goalAddedListener != null) goalAddedListener.accept(goal); if (goalUpdatedListener != null) goalUpdatedListener.accept(goal); }); - ActionBarButton moveBtn = new ActionBarButton("Move", () -> {}); - moveBtn.setEnabled(false); - moveBtn.setToolTipText("Coming soon"); - - ActionBarButton bulkEditBtn = new ActionBarButton("Bulk Edit", () -> {}); - bulkEditBtn.setEnabled(false); - bulkEditBtn.setToolTipText("Coming soon"); actionBar.left().add(addGoalBtn); - actionBar.left().add(moveBtn); - actionBar.left().add(bulkEditBtn); JPanel headerContainer = new JPanel(new BorderLayout()); headerContainer.setBackground(ColorScheme.DARK_GRAY_COLOR); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java index 760a40c..bafd246 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java @@ -80,8 +80,19 @@ public ListItemPanel(ReorderableList list, T item) this.list = list; this.item = item; - setBorder(new EmptyBorder(8, 8, 8, 8)); - setBackground(ColorScheme.DARK_GRAY_COLOR); + if (item instanceof Goal) { + setBorder(javax.swing.BorderFactory.createCompoundBorder( + new EmptyBorder(4, 2, 4, 2), // minimal outer spacing + javax.swing.BorderFactory.createCompoundBorder( + javax.swing.BorderFactory.createLineBorder(java.awt.Color.DARK_GRAY, 1, true), // rounded card outline + new EmptyBorder(2, 4, 2, 4) // minimal inner padding + ) + )); + setBackground(ColorScheme.DARKER_GRAY_COLOR); + } else { + setBorder(new EmptyBorder(2, 4, 2, 4)); // add horizontal and vertical spacing for tasks + setBackground(ColorScheme.DARK_GRAY_COLOR); + } moveUp.addActionListener(e -> { list.moveUp(item); @@ -204,13 +215,17 @@ public void mousePressed(MouseEvent e) @Override public void mouseEntered(MouseEvent e) { - setBackground(ColorScheme.DARK_GRAY_HOVER_COLOR); + if (item instanceof Goal) { + setBackground(ColorScheme.DARK_GRAY_HOVER_COLOR); + } } @Override public void mouseExited(MouseEvent e) { - setBackground(ColorScheme.DARK_GRAY_COLOR); + if (item instanceof Goal) { + setBackground(ColorScheme.DARKER_GRAY_COLOR); + } } }; From 12f753a183161e78f66ec76a8c8bed743e1ab331 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Wed, 20 Aug 2025 11:43:01 -0400 Subject: [PATCH 30/53] lots of UI updates --- .../goaltracker/ui/GoalTrackerPanel.java | 24 ++++++- .../ui/components/ListItemPanel.java | 64 ++++++++++++++++--- 2 files changed, 75 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 0dd123b..7df236c 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -86,10 +86,28 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) actionBar.left().add(addGoalBtn); + // Right-side actions: Undo/Redo + undoButtonRef = new ActionBarButton("Undo", this::doUndo); + redoButtonRef = new ActionBarButton("Redo", this::doRedo); + actionBar.right().add(undoButtonRef); + actionBar.right().add(redoButtonRef); + updateUndoRedoButtons(); + + // Wrap the title panel with a subtle bottom separator for visual polish + JPanel titleWrapper = new JPanel(new BorderLayout()); + titleWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR); + titleWrapper.add(titlePanel, BorderLayout.CENTER); + + // 1px divider under the header title + JPanel headerSeparator = new JPanel(); + headerSeparator.setBackground(ColorScheme.DARKER_GRAY_COLOR); + headerSeparator.setPreferredSize(new Dimension(1, 4)); + titleWrapper.add(headerSeparator, BorderLayout.SOUTH); + JPanel headerContainer = new JPanel(new BorderLayout()); headerContainer.setBackground(ColorScheme.DARK_GRAY_COLOR); - headerContainer.add(titlePanel, BorderLayout.NORTH); - headerContainer.add(actionBar, BorderLayout.SOUTH ); + headerContainer.add(titleWrapper, BorderLayout.NORTH); + headerContainer.add(actionBar, BorderLayout.SOUTH); goalListPanel = new ListPanel<>(goalManager.getGoals(), (goal) -> { @@ -105,7 +123,7 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) } ); goalListPanel.setGap(0); - goalListPanel.setPlaceholder("Add a new goal using the button above"); + goalListPanel.setPlaceholder("
No goals yet.
Click + Add goal above to create your first one.
"); mainPanel.add(headerContainer, BorderLayout.NORTH); mainPanel.add(goalListPanel, BorderLayout.CENTER); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java index bafd246..ac5ac99 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java @@ -13,7 +13,9 @@ import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import javax.swing.JComponent; import javax.swing.border.EmptyBorder; +import javax.swing.border.MatteBorder; import net.runelite.client.ui.ColorScheme; public class ListItemPanel extends JPanel implements Refreshable @@ -81,14 +83,7 @@ public ListItemPanel(ReorderableList list, T item) this.item = item; if (item instanceof Goal) { - setBorder(javax.swing.BorderFactory.createCompoundBorder( - new EmptyBorder(4, 2, 4, 2), // minimal outer spacing - javax.swing.BorderFactory.createCompoundBorder( - javax.swing.BorderFactory.createLineBorder(java.awt.Color.DARK_GRAY, 1, true), // rounded card outline - new EmptyBorder(2, 4, 2, 4) // minimal inner padding - ) - )); - setBackground(ColorScheme.DARKER_GRAY_COLOR); + applyGoalCardDefaultStyle(); } else { setBorder(new EmptyBorder(2, 4, 2, 4)); // add horizontal and vertical spacing for tasks setBackground(ColorScheme.DARK_GRAY_COLOR); @@ -194,6 +189,12 @@ public void refreshMenu() public ListItemPanel add(Component comp) { super.add(comp, BorderLayout.CENTER); + // For goal cards, strip inner borders from the content to avoid double outlines + if (item instanceof Goal && comp instanceof JComponent) + { + ((JComponent) comp).setBorder(new EmptyBorder(0, 0, 0, 0)); + ((JComponent) comp).setOpaque(false); + } addContextMenuListenerRecursive(comp); if (clickListenerAdapter != null) addClickListenerRecursive(comp); return this; @@ -208,6 +209,9 @@ public void mousePressed(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { + if (item instanceof Goal) { + applyGoalCardPressedStyle(); + } clickListener.accept(e); } } @@ -216,7 +220,7 @@ public void mousePressed(MouseEvent e) public void mouseEntered(MouseEvent e) { if (item instanceof Goal) { - setBackground(ColorScheme.DARK_GRAY_HOVER_COLOR); + applyGoalCardHoverStyle(); } } @@ -224,7 +228,19 @@ public void mouseEntered(MouseEvent e) public void mouseExited(MouseEvent e) { if (item instanceof Goal) { - setBackground(ColorScheme.DARKER_GRAY_COLOR); + applyGoalCardDefaultStyle(); + } + } + + @Override + public void mouseReleased(MouseEvent e) + { + if (item instanceof Goal) { + if (contains(e.getPoint())) { + applyGoalCardHoverStyle(); + } else { + applyGoalCardDefaultStyle(); + } } } }; @@ -248,4 +264,32 @@ public void onReordered(Consumer reorderListener) { public void onRemovedWithIndex(BiConsumer removeListener) { this.removedWithIndexListener = removeListener; } + + private void applyGoalCardDefaultStyle() + { + // Outer gap -> subtle drop shadow (bottom/right) -> rounded outline -> inner padding + setBorder(javax.swing.BorderFactory.createCompoundBorder( + new EmptyBorder(8, 6, 8, 6), + javax.swing.BorderFactory.createCompoundBorder( + new MatteBorder(2, 2, 4, 4, new Color(0, 0, 0, 60)), // shadow on all sides + javax.swing.BorderFactory.createCompoundBorder( + javax.swing.BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR, 1, true), + new EmptyBorder(6, 8, 6, 8) + ) + ) + )); + setBackground(ColorScheme.DARK_GRAY_COLOR); + } + + private void applyGoalCardHoverStyle() + { + // Keep border; just brighten background on hover + setBackground(ColorScheme.DARK_GRAY_HOVER_COLOR); + } + + private void applyGoalCardPressedStyle() + { + // Slightly darker than hover to indicate press + setBackground(ColorScheme.DARK_GRAY_COLOR); + } } From d4cd93d64bf48575ed6f630fe06c92a483a488e9 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Wed, 20 Aug 2025 13:04:23 -0400 Subject: [PATCH 31/53] appears everythings working --- .../goaltracker/ui/GoalItemContent.java | 15 +- .../ui/components/ListItemPanel.java | 190 ++++++++++++------ .../goaltracker/ui/components/ListPanel.java | 2 +- .../ui/components/ListTaskPanel.java | 175 +++++++++++++++- 4 files changed, 314 insertions(+), 68 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java index 263b895..d7df086 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java @@ -29,8 +29,9 @@ public class GoalItemContent extends JPanel implements Refreshable this.goal = goal; setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); // padding for centered text - setBackground(ColorScheme.DARKER_GRAY_COLOR); - setOpaque(true); + // Let the parent card body paint the background; avoid double fills + setOpaque(false); + setBackground(null); add(title, BorderLayout.WEST); // Make goal title editable with standard copy/paste @@ -58,6 +59,16 @@ public void focusLost(java.awt.event.FocusEvent e) { add(progress, BorderLayout.EAST); + // Initialize visible text and colors immediately (before first refresh) + { + Color color = STATUS_TO_COLOR.get(goal.getStatus()); + title.setText(goal.getDescription()); + title.setForeground(color); + title.setCaretColor(color); + progress.setText(goal.getComplete().size() + "/" + goal.getTasks().size()); + progress.setForeground(color); + } + plugin.getUiStatusManager().addRefresher(goal, this::refresh); // Ensure right-click on any child shows the parent ListItemPanel context menu diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java index ac5ac99..e2bdc16 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java @@ -36,6 +36,9 @@ public class ListItemPanel extends JPanel implements Refreshable private MouseAdapter clickListenerAdapter; + // Inner face of the goal card; only this area changes color on hover/press + private JPanel cardBody; + private void addClickListenerRecursive(Component c) { if (clickListenerAdapter == null) return; @@ -82,6 +85,12 @@ public ListItemPanel(ReorderableList list, T item) this.list = list; this.item = item; + // Create inner card surface to isolate hover/press background changes + cardBody = new JPanel(new BorderLayout()); + cardBody.setOpaque(true); + // Add the cardBody as the main content area + super.add(cardBody, BorderLayout.CENTER); + if (item instanceof Goal) { applyGoalCardDefaultStyle(); } else { @@ -127,73 +136,47 @@ public ListItemPanel(ReorderableList list, T item) } @Override - public void setBackground(Color bg) + public void add(Component comp, Object constraints) { - super.setBackground(bg); - for (Component component : getComponents()) { - component.setBackground(bg); + if (item instanceof Goal) + { + cardBody.add(comp, BorderLayout.CENTER); + // For goal cards, strip inner borders from the content to avoid double outlines + if (comp instanceof JComponent) + { + ((JComponent) comp).setBorder(new EmptyBorder(0, 0, 0, 0)); + } + addContextMenuListenerRecursive(comp); + if (clickListenerAdapter != null) addClickListenerRecursive(comp); + return; } + super.add(comp, constraints); } @Override - public void refresh() + public Component add(String name, Component comp) { - refreshMenu(); - - for (Component component : getComponents()) { - if (component instanceof Refreshable) { - ((Refreshable) component).refresh(); + if (item instanceof Goal) + { + cardBody.add(comp, BorderLayout.CENTER); + if (comp instanceof JComponent) + { + ((JComponent) comp).setBorder(new EmptyBorder(0, 0, 0, 0)); } + addContextMenuListenerRecursive(comp); + if (clickListenerAdapter != null) addClickListenerRecursive(comp); + return comp; } - } - - public void refreshMenu() - { - popupMenu.removeAll(); - if (!list.isFirst(item)) { - popupMenu.add(moveUp); - } - if (!list.isLast(item)) { - popupMenu.add(moveDown); - } - if (!list.isFirst(item)) { - popupMenu.add(moveToTop); - } - if (!list.isLast(item)) { - popupMenu.add(moveToBottom); - } - popupMenu.add(removeItem); - - if (item instanceof Goal) { - JMenuItem markAllComplete = new JMenuItem("Mark all as completed"); - JMenuItem markAllIncomplete = new JMenuItem("Mark all as incomplete"); - - markAllComplete.addActionListener(e -> { - Goal goal = (Goal) item; - goal.setAllTasksCompleted(true); - refresh(); - }); - - markAllIncomplete.addActionListener(e -> { - Goal goal = (Goal) item; - goal.setAllTasksCompleted(false); - refresh(); - }); - - popupMenu.addSeparator(); - popupMenu.add(markAllComplete); - popupMenu.add(markAllIncomplete); - } + return super.add(name, comp); } public ListItemPanel add(Component comp) { - super.add(comp, BorderLayout.CENTER); + cardBody.add(comp, BorderLayout.CENTER); // For goal cards, strip inner borders from the content to avoid double outlines if (item instanceof Goal && comp instanceof JComponent) { ((JComponent) comp).setBorder(new EmptyBorder(0, 0, 0, 0)); - ((JComponent) comp).setOpaque(false); } addContextMenuListenerRecursive(comp); if (clickListenerAdapter != null) addClickListenerRecursive(comp); @@ -267,29 +250,108 @@ public void onRemovedWithIndex(BiConsumer removeListener) { private void applyGoalCardDefaultStyle() { - // Outer gap -> subtle drop shadow (bottom/right) -> rounded outline -> inner padding + // Outer spacing + shadow on the container panel setBorder(javax.swing.BorderFactory.createCompoundBorder( new EmptyBorder(8, 6, 8, 6), - javax.swing.BorderFactory.createCompoundBorder( - new MatteBorder(2, 2, 4, 4, new Color(0, 0, 0, 60)), // shadow on all sides - javax.swing.BorderFactory.createCompoundBorder( - javax.swing.BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR, 1, true), - new EmptyBorder(6, 8, 6, 8) - ) - ) + new MatteBorder(2, 2, 4, 4, new Color(0, 0, 0, 60)) // shadow on all sides )); - setBackground(ColorScheme.DARK_GRAY_COLOR); + setBackground(ColorScheme.DARK_GRAY_COLOR); // keep outer area stable + + // Inner card face: rounded outline + inner padding + if (cardBody != null) + { + cardBody.setBorder(javax.swing.BorderFactory.createCompoundBorder( + javax.swing.BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR, 1, true), + new EmptyBorder(6, 8, 6, 8) + )); + cardBody.setBackground(ColorScheme.DARK_GRAY_COLOR); + } } private void applyGoalCardHoverStyle() { - // Keep border; just brighten background on hover - setBackground(ColorScheme.DARK_GRAY_HOVER_COLOR); + if (cardBody != null) + { + cardBody.setBackground(ColorScheme.DARK_GRAY_HOVER_COLOR); + } } private void applyGoalCardPressedStyle() { - // Slightly darker than hover to indicate press - setBackground(ColorScheme.DARK_GRAY_COLOR); + if (cardBody != null) + { + cardBody.setBackground(ColorScheme.DARK_GRAY_COLOR); + } + } + + @Override + public void refresh() + { + // Refresh the context menu + popupMenu.removeAll(); + if (!list.isFirst(item)) { + popupMenu.add(moveUp); + } + if (!list.isLast(item)) { + popupMenu.add(moveDown); + } + if (!list.isFirst(item)) { + popupMenu.add(moveToTop); + } + if (!list.isLast(item)) { + popupMenu.add(moveToBottom); + } + popupMenu.add(removeItem); + + buildAdditionalMenu(); + + // Refresh all descendants that implement Refreshable + for (Component component : getComponents()) { + refreshDescendants(component); + } + revalidate(); + repaint(); + } + + /** + * Hook for subclasses to append extra context‑menu items. + * Base implementation adds Goal‑specific actions only. + */ + protected void buildAdditionalMenu() + { + if (item instanceof Goal) + { + JMenuItem markAllComplete = new JMenuItem("Mark all as completed"); + JMenuItem markAllIncomplete = new JMenuItem("Mark all as incomplete"); + + markAllComplete.addActionListener(e -> { + Goal goal = (Goal) item; + goal.setAllTasksCompleted(true); + refresh(); + }); + + markAllIncomplete.addActionListener(e -> { + Goal goal = (Goal) item; + goal.setAllTasksCompleted(false); + refresh(); + }); + + popupMenu.addSeparator(); + popupMenu.add(markAllComplete); + popupMenu.add(markAllIncomplete); + } + // Non‑Goal rows (e.g., Tasks) should add their own items in subclasses like ListTaskPanel + } + + private void refreshDescendants(Component c) + { + if (c instanceof Refreshable) { + ((Refreshable) c).refresh(); + } + if (c instanceof java.awt.Container) { + for (Component child : ((java.awt.Container) c).getComponents()) { + refreshDescendants(child); + } + } } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java index 9ed6cb6..8fecf26 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java @@ -132,7 +132,7 @@ private void refreshChildMenus() { for (Component component : listPanel.getComponents()) { if (component instanceof ListItemPanel) { - ((ListItemPanel) component).refreshMenu(); + ((Refreshable) component).refresh(); } } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java index d55b596..33689d7 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java @@ -153,9 +153,9 @@ public void mouseClicked(MouseEvent e) { addShiftRemoveListenerRecursive(this); } - @Override public void refreshMenu() { + super.refresh(); popupMenu.removeAll(); javax.swing.JMenu moveMenu = new javax.swing.JMenu("Move"); boolean hasMove = false; @@ -380,6 +380,179 @@ public void setActionHistory(ActionHistory history) { this.history = history; } + /** + * Appends Task-specific context menu items. + */ + @Override + protected void buildAdditionalMenu() + { + javax.swing.JMenu moveMenu = new javax.swing.JMenu("Move"); + boolean hasMove = false; + // Rewire generic move actions to include ActionHistory entries + if (!list.isFirst(item)) { + for (var l : moveUp.getActionListeners()) moveUp.removeActionListener(l); + moveUp.addActionListener(e -> { + int oldIndex = list.indexOf(item); + int newIndex = Math.max(0, oldIndex - 1); + list.moveUp(item); + if (history != null) { + history.push(new ReorderTaskAction(list, item, oldIndex, newIndex)); + } + refreshParentList(); + }); + moveMenu.add(moveUp); + hasMove = true; + } + if (!list.isLast(item)) { + for (var l : moveDown.getActionListeners()) moveDown.removeActionListener(l); + moveDown.addActionListener(e -> { + int oldIndex = list.indexOf(item); + int newIndex = Math.min(list.size() - 1, oldIndex + 1); + list.moveDown(item); + if (history != null) { + history.push(new ReorderTaskAction(list, item, oldIndex, newIndex)); + } + refreshParentList(); + }); + moveMenu.add(moveDown); + hasMove = true; + } + if (!list.isFirst(item)) { + for (var l : moveToTop.getActionListeners()) moveToTop.removeActionListener(l); + moveToTop.addActionListener(e -> { + int oldIndex = list.indexOf(item); + list.moveToTop(item); + if (history != null) { + history.push(new ReorderTaskAction(list, item, oldIndex, 0)); + } + refreshParentList(); + }); + moveMenu.add(moveToTop); + hasMove = true; + } + if (!list.isLast(item)) { + for (var l : moveToBottom.getActionListeners()) moveToBottom.removeActionListener(l); + moveToBottom.addActionListener(e -> { + int oldIndex = list.indexOf(item); + list.moveToBottom(item); + if (history != null) { + history.push(new ReorderTaskAction(list, item, oldIndex, list.size() - 1)); + } + refreshParentList(); + }); + moveMenu.add(moveToBottom); + hasMove = true; + } + if (hasMove) { + popupMenu.add(moveMenu); + } + + // Indent / Unindent + var previousItem = list.getPreviousItem(item); + if (item.isNotFullyIndented() && previousItem != null && previousItem.getIndentLevel() >= item.getIndentLevel()) { + popupMenu.add(indentItem); + } + if (item.isIndented()) { + popupMenu.add(unindentItem); + } + + // Toggle Completed/Incomplete for this task and its descendants + String toggleLabel = "Mark as " + (item.getStatus() == Status.COMPLETED ? "Incomplete" : "Completed"); + JMenuItem toggleStatusItem = new JMenuItem(toggleLabel); + toggleStatusItem.addActionListener(e -> { + Status newStatus = (item.getStatus() == Status.COMPLETED ? Status.NOT_STARTED : Status.COMPLETED); + java.util.List affected = new java.util.ArrayList<>(); + java.util.List oldStatuses = new java.util.ArrayList<>(); + int index = list.indexOf(item); + int baseIndent = item.getIndentLevel(); + affected.add(item); + oldStatuses.add(item.getStatus()); + for (int i = index + 1; i < list.size(); i++) { + Task child = list.get(i); + if (child.getIndentLevel() <= baseIndent) break; + affected.add(child); + oldStatuses.add(child.getStatus()); + } + ActionHistory.Action act = new ActionHistory.Action() { + @Override public void undo() { for (int i = 0; i < affected.size(); i++) affected.get(i).setStatus(oldStatuses.get(i)); } + @Override public void redo() { for (Task t : affected) t.setStatus(newStatus); } + }; + if (history != null) { act.redo(); history.push(act); } + else { for (Task t : affected) t.setStatus(newStatus); } + if (taskContent != null) taskContent.refresh(); + refreshParentList(); + }); + popupMenu.add(toggleStatusItem); + + // Quest pre-reqs (if available) + if (item instanceof com.toofifty.goaltracker.models.task.QuestTask) { + com.toofifty.goaltracker.models.task.QuestTask questTask = (com.toofifty.goaltracker.models.task.QuestTask) item; + int baseIndent = item.getIndentLevel(); + java.util.Set existingKeys = new java.util.HashSet<>(); + int parentIndex = list.indexOf(item); + for (int i = parentIndex + 1; i < list.size(); i++) { + var child = list.get(i); + if (child.getIndentLevel() <= baseIndent) break; + existingKeys.add(child.getClass().getName() + "|" + child.toString()); + } + var rawPrereqs = QuestRequirements.getRequirements(questTask.getQuest(), baseIndent + 1); + java.util.List missingPrereqs = new java.util.ArrayList<>(); + if (rawPrereqs != null) { + for (com.toofifty.goaltracker.models.task.Task p : rawPrereqs) { + String key = p.getClass().getName() + "|" + p.toString(); + if (!existingKeys.contains(key)) missingPrereqs.add(p); + } + } + if (!missingPrereqs.isEmpty()) { + JMenuItem prereqItem = new JMenuItem("Add pre-reqs"); + prereqItem.addActionListener(e -> { + var raw = QuestRequirements.getRequirements(questTask.getQuest(), baseIndent + 1); + if (raw != null) { + java.util.Set currentKeys = new java.util.HashSet<>(); + int pIndex = list.indexOf(item); + for (int i = pIndex + 1; i < list.size(); i++) { + var child = list.get(i); + if (child.getIndentLevel() <= baseIndent) break; + currentKeys.add(child.getClass().getName() + "|" + child.toString()); + } + java.util.List filtered = new java.util.ArrayList<>(); + for (com.toofifty.goaltracker.models.task.Task t : raw) { + String key = t.getClass().getName() + "|" + t.toString(); + if (!currentKeys.contains(key)) filtered.add(t); + } + if (!filtered.isEmpty()) { + int insertIndex = list.indexOf(item); + for (com.toofifty.goaltracker.models.task.Task prereq : filtered) { + list.add(insertIndex + 1, prereq); + insertIndex++; + } + refreshParentList(); + } + } + }); + popupMenu.add(prereqItem); + } + } + + // Rewire Remove to cascade children and update label + for (var l : removeItem.getActionListeners()) removeItem.removeActionListener(l); + removeItem.addActionListener(e -> { + int removedIndex = list.indexOf(item); + int baseIndent = item.getIndentLevel(); + while (removedIndex + 1 < list.size() && list.get(removedIndex + 1).getIndentLevel() > baseIndent) { + list.remove(list.get(removedIndex + 1)); + } + list.remove(item); + if (this.removedWithIndexListener != null) this.removedWithIndexListener.accept(item, removedIndex); + if (this.removedListener != null) this.removedListener.accept(item); + refreshParentList(); + }); + removeItem.setText("Remove (Shift+Left Click)"); + // Ensure remove is at the bottom: remove and re-add it + popupMenu.remove(removeItem); + popupMenu.add(removeItem); + } + private void refreshParentList() { Container parent = SwingUtilities.getAncestorOfClass(ListPanel.class, this); From 8a4e20dd7e49f1315cb6dd56e999e424807d6b18 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Wed, 20 Aug 2025 17:02:12 -0400 Subject: [PATCH 32/53] appears everythings working --- README.md | 10 ++++++++++ changelog.md | 10 ++++++++++ .../com/toofifty/goaltracker/GoalTrackerConfig.java | 2 +- .../com/toofifty/goaltracker/GoalTrackerPlugin.java | 9 +++------ .../com/toofifty/goaltracker/ui/GoalItemContent.java | 10 ++++++++++ 5 files changed, 34 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4c2e12e..2decc6a 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ - 👆 Hover states for better visual feedback - 📋 New context menu organization for streamlined interaction - 🔍 Search toggle improvements for easier task searching +- 📂 Task right‑click menu now includes a grouped **Move** submenu and cascading complete/incomplete toggle that applies to children. +- 🖼️ Goal cards redesigned with lighter fills, full shadows, and hover/press effects only on the card face. +- 📏 Thicker header divider under “Goal Tracker” for clearer separation. +- 🔄 Automatic refresh propagation so Home goals update instantly when tasks change. +- 💬 Completion chat messages delivered as proper Game messages with configurable colors. --- @@ -31,6 +36,8 @@ - 🖱️ Improved context menus with better usability - 🎯 Enhanced cursor and hover detection accuracy - 📋 Copy and paste support in the goal name input field +- 🔄 Context menu entries rebuilt dynamically before opening to always reflect the latest state. +- 🖼️ Task and goal content now force an initial refresh so icons and text render correctly at login. --- ## 🐛 Fixes @@ -43,6 +50,9 @@ - ⌨️ Keyboard shortcut fixes and enhancements - 🗑️ Automatic removal of empty goals to keep lists tidy - 🔄 Visual refresh issue resolved on login +- ✅ Fixed child task refresh issues by recursively refreshing all descendants. +- 🗂️ Fixed blank panel issue when switching from Home to Goal view. +- 💬 Fixed completion chat message not appearing on task completion. --- diff --git a/changelog.md b/changelog.md index 8f5ce33..ac0a144 100644 --- a/changelog.md +++ b/changelog.md @@ -19,6 +19,10 @@ - Added manual completion toggling for tasks created from presets, allowing users to right-click and mark them complete/incomplete just like quick-added tasks. - Added customizable color setting for task completion messages shown in the chatbox. - Implemented automatic goal status checking upon login to mark goals as completed if requirements are already met. +- Context menu entries now rebuilt dynamically right before opening to ensure the latest state. +- Task list panel context menus reorganized with a **Move** submenu and cascading complete/incomplete toggle that applies to children. +- Initial refresh calls for Task and Goal content so icons/text render correctly on login. +- Automatic ancestor `ListPanel` refresh propagation to keep Home goal list in sync when tasks change. ### Changed - Pre-req button made more compact (~25% smaller). @@ -57,6 +61,9 @@ options. - Cursor/hover detection improved on home goal list: listeners now attach recursively to all child components for accurate selection and highlighting. +- Goal cards now use a lighter fill with a full shadow around borders, and hover/press only affect the card face. +- Header divider under “Goal Tracker” made thicker (4px) for stronger separation. +- Right-click menus refactored so Tasks build their own menu and Goals build theirs, preventing duplicate/unusable items. ### Fixed - Home panel Undo/Redo buttons removed; these controls now exist only in Goal view. @@ -67,3 +74,6 @@ - Goal name input now fully supports keyboard shortcuts (Ctrl/Cmd+C, V, X, A, Insert/Delete variants) and right-click context menu for copy/paste across all platforms. - Empty goals (0/0 tasks) created via **+ Add goal** are now automatically removed when backing out without adding tasks, preventing clutter in saved goal lists. - Fixed visual refresh issue where quest task statuses didn’t show correctly on login unless re-entering the goal. +- Fixed child task refresh issues after parent complete/incomplete cascades by recursively refreshing all descendants. +- Fixed blank panel issue when switching from Home to Goal view by only using inner card body for Goal rows. +- Fixed completion chat message not appearing; now delivered as a proper `GAMEMESSAGE` with customizable config color. diff --git a/src/main/java/com/toofifty/goaltracker/GoalTrackerConfig.java b/src/main/java/com/toofifty/goaltracker/GoalTrackerConfig.java index d008d91..3409cd2 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalTrackerConfig.java +++ b/src/main/java/com/toofifty/goaltracker/GoalTrackerConfig.java @@ -45,6 +45,6 @@ default String goalTrackerItemNoteMapCache() @Alpha default Color completionMessageColor() { - return new Color(0xFF16ABE5, true); + return new Color(0xF227A509, true); } } diff --git a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java index f29b4ea..74dcbe6 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java +++ b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java @@ -27,6 +27,7 @@ import net.runelite.client.ui.ClientToolbar; import net.runelite.client.ui.NavigationButton; import net.runelite.client.util.AsyncBufferedImage; +import net.runelite.client.util.ColorUtil; import javax.inject.Inject; import java.util.List; @@ -189,12 +190,8 @@ public void notifyTask(Task task) log.debug("Notify: [Goal Tracker] You have completed a task: " + task + "!"); String message = "[Goal Tracker] You have completed a task: " + task + "!"; - String formattedMessage = new ChatMessageBuilder().append(config.completionMessageColor(), message).build(); - chatMessageManager.queue(QueuedMessage.builder() - .type(ChatMessageType.CONSOLE) - .name("Goal Tracker") - .runeLiteFormattedMessage(formattedMessage) - .build()); + String formattedMessage = ColorUtil.wrapWithColorTag(message, config.completionMessageColor()); + client.addChatMessage(ChatMessageType.GAMEMESSAGE, "", formattedMessage, null); task.setNotified(true); } diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java index d7df086..3e96de6 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java @@ -100,6 +100,16 @@ private void maybeShow(MouseEvent e) this.addMouseListener(forwardPopup); title.addMouseListener(forwardPopup); progress.addMouseListener(forwardPopup); + // Ensure item icon/text initialize on first render (e.g., on login) + javax.swing.SwingUtilities.invokeLater(this::refresh); + } + + @Override + public void addNotify() + { + super.addNotify(); + // In case construction happened before UI was realized, refresh when shown + javax.swing.SwingUtilities.invokeLater(this::refresh); } @Override From 911d62b8133f7724816eeb795bc14fd3fc359cc1 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Wed, 20 Aug 2025 18:39:20 -0400 Subject: [PATCH 33/53] appears everythings working --- README.md | 130 ++++++++++-------- .../goaltracker/ui/GoalItemContent.java | 47 ++++++- 2 files changed, 116 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 2decc6a..593c523 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,105 @@ -# 🏆 RuneLite Goal Tracker v2 +# 🏆 Goal Tracker v2 > A complete reimagining of the Goal Tracker plugin — rebuilt with a more modern UI, powerful new features, and improved stability to help you plan, track, and achieve your Old School RuneScape goals with ease. --- -## ✨ New Features - -- 🌟 Quest prerequisites button for easy access to quest requirements -- ⚡ Shift+Click removal of tasks for faster task management -- 🔗 Completion cascading to automatically complete related tasks -- 🎯 Dropdown quest selector for quicker quest task addition -- 🖱️ Right-click menus for prereq and child completion options -- 🔄 Manual toggling for preset tasks to customize your workflow -- 🎨 Customizable chatbox colors for notifications -- ⏱️ Automatic goal status checks for up-to-date progress -- 🆕 New ActionBar and ActionBarButton UI components -- 👆 Hover states for better visual feedback -- 📋 New context menu organization for streamlined interaction -- 🔍 Search toggle improvements for easier task searching -- 📂 Task right‑click menu now includes a grouped **Move** submenu and cascading complete/incomplete toggle that applies to children. -- 🖼️ Goal cards redesigned with lighter fills, full shadows, and hover/press effects only on the card face. -- 📏 Thicker header divider under “Goal Tracker” for clearer separation. -- 🔄 Automatic refresh propagation so Home goals update instantly when tasks change. -- 💬 Completion chat messages delivered as proper Game messages with configurable colors. - ---- +
+

✨ New Features

+ +- Shift+Click removal of tasks for faster task management +- Automatic goal status checks for up-to-date progress +- New ActionBar and ActionBarButton UI components +- Hover states for better visual feedback +- New context menu organization for streamlined interaction +- Search toggle improvements for easier task searching +- New task right‑click menu with grouped **Move** submenu and cascading complete/incomplete toggle that applies to children (expanded beyond v1 functionality). +- Goal cards redesigned with lighter fills, full shadows, and hover/press effects only on the card face. +- Thicker header divider under “Goal Tracker” for clearer separation. +- Automatic refresh propagation so Home goals update instantly when tasks change. +- Completion chat messages delivered as proper Game messages with configurable colors. + +### ♻️ Redesigned Features + +- Redesigned quest prerequisites button for quick access to quest requirements +- Redesigned completion cascading to automatically complete related tasks +- Redesigned dropdown quest selector for faster quest task addition +- Redesigned right-click menus for prerequisites and child completion options +- Redesigned manual toggling for preset tasks to customize workflow +- Redesigned chatbox notification colors (now fully customizable) + +
-## 🔧 Improvements +
+

🔧 Improvements

-- 🏷️ More compact prereq button for a cleaner interface -- ✨ Refreshed UI with updated design elements -- 🔠 Font and ComboBox readability enhancements -- 🧩 Consistent ActionBar UI throughout the plugin -- 🧑‍💻 Unified goal view header for a cohesive look -- 🖱️ Improved context menus with better usability -- 🎯 Enhanced cursor and hover detection accuracy -- 📋 Copy and paste support in the goal name input field -- 🔄 Context menu entries rebuilt dynamically before opening to always reflect the latest state. -- 🖼️ Task and goal content now force an initial refresh so icons and text render correctly at login. +- More compact prereq button for a cleaner interface +- Refreshed UI with updated design elements +- Font and ComboBox readability enhancements +- Consistent ActionBar UI throughout the plugin +- Unified goal view header for a cohesive look +- Improved context menus with better usability +- Enhanced cursor and hover detection accuracy +- Copy and paste support in the goal name input field +- Context menu entries rebuilt dynamically before opening to always reflect the latest state. +- Task and goal content now force an initial refresh so icons and text render correctly at login. ---- -## 🐛 Fixes - -- ♻️ Undo/Redo functionality cleanup for smoother editing -- 🎨 ActionBarButton painting fixes to prevent visual glitches -- 🏠 GoalTrackerPanel `home()` method refresh improvements -- 🔄 Correct refresh behavior in ListPanel -- 🖱️ Improved mouse selection accuracy -- ⌨️ Keyboard shortcut fixes and enhancements -- 🗑️ Automatic removal of empty goals to keep lists tidy -- 🔄 Visual refresh issue resolved on login -- ✅ Fixed child task refresh issues by recursively refreshing all descendants. -- 🗂️ Fixed blank panel issue when switching from Home to Goal view. -- 💬 Fixed completion chat message not appearing on task completion. +
---- +
+

🐛 Fixes

+ +- Undo/Redo functionality cleanup for smoother editing +- ActionBarButton painting fixes to prevent visual glitches +- GoalTrackerPanel `home()` method refresh improvements +- Correct refresh behavior in ListPanel +- Improved mouse selection accuracy +- Keyboard shortcut fixes and enhancements +- Automatic removal of empty goals to keep lists tidy +- Visual refresh issue resolved on login +- Fixed child task refresh issues by recursively refreshing all descendants. +- Fixed blank panel issue when switching from Home to Goal view. +- Fixed completion chat message not appearing on task completion. + +
-## 📥 Installation +
+

📥 Installation

1. Open RuneLite. 2. Go to the Plugin Hub. 3. Search for "Goal Tracker v2". 4. Click **Install**. ---- +
-## 🚀 Getting Started +
+

🚀 Getting Started

- Open the plugin panel in RuneLite once installed. - Use **+ Add goal** to create a new goal. - Add tasks (quests, skills, items, or manual) via the goal view. - Use the new **ActionBar** buttons for navigation, undo/redo, and bulk actions. -- Right-click goals or tasks for advanced options like adding prerequisites, marking all children complete, or removing tasks quickly. ---- +
-## 🖼️ Screenshots +
+

🖼️ Screenshots

-_Coming soon_ -*(Screenshots and GIFs will be added here to showcase the new UI and features.)* +[screenshot] Home panel with goal cards +[screenshot] Inside a goal with task list +[screenshot] Right‑click menu on a task +[screenshot] Config panel with customizable chat color ---- +
-## 🙏 Acknowledgements +
+

🙏 Acknowledgements

- Original plugin created by **dillydill123**. - Fully renovated and maintained by **AhDoozy**. ---- +
## 📄 License Licensed under the [BSD 2-Clause License](LICENSE). diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java index 3e96de6..673fa8d 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java @@ -20,6 +20,7 @@ public class GoalItemContent extends JPanel implements Refreshable { private final JTextField title = new JTextField(); private final JLabel progress = new JLabel(); + private final SlimBar progressBar = new SlimBar(); private final Goal goal; @@ -33,7 +34,9 @@ public class GoalItemContent extends JPanel implements Refreshable setOpaque(false); setBackground(null); - add(title, BorderLayout.WEST); + JPanel topRow = new JPanel(new BorderLayout()); + topRow.setOpaque(false); + topRow.add(title, BorderLayout.WEST); // Make goal title editable with standard copy/paste title.setBorder(null); title.setOpaque(false); @@ -57,7 +60,13 @@ public void focusLost(java.awt.event.FocusEvent e) { } }); - add(progress, BorderLayout.EAST); + topRow.add(progress, BorderLayout.EAST); + add(topRow, BorderLayout.CENTER); + + // Slim custom progress bar under the title row + progressBar.setBorder(BorderFactory.createEmptyBorder(4, 0, 0, 0)); // gap above the bar + progressBar.setPreferredSize(new Dimension(0, 6)); // 6px tall bar + add(progressBar, BorderLayout.SOUTH); // Initialize visible text and colors immediately (before first refresh) { @@ -100,6 +109,7 @@ private void maybeShow(MouseEvent e) this.addMouseListener(forwardPopup); title.addMouseListener(forwardPopup); progress.addMouseListener(forwardPopup); + progressBar.addMouseListener(forwardPopup); // Ensure item icon/text initialize on first render (e.g., on login) javax.swing.SwingUtilities.invokeLater(this::refresh); } @@ -124,5 +134,38 @@ public void refresh() progress.setText( goal.getComplete().size() + "/" + goal.getTasks().size()); progress.setForeground(color); + + int total = goal.getTasks().size(); + int done = goal.getComplete().size(); + progressBar.setVisible(total > 0); + progressBar.setProgress(done, total, color); + } + private static class SlimBar extends JComponent { + private int done = 0; + private int total = 1; + private Color fill = Color.GREEN; + + void setProgress(int done, int total, Color fill) { + this.done = done; + this.total = Math.max(1, total); + this.fill = fill; + repaint(); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + Graphics2D g2 = (Graphics2D) g.create(); + int w = getWidth(); + int h = getHeight(); + // track + g2.setColor(ColorScheme.DARKER_GRAY_COLOR); + g2.fillRect(0, 0, w, h); + // fill + int barW = (int) ((done / (double) total) * w); + g2.setColor(fill != null ? fill : Color.GREEN); + g2.fillRect(0, 0, barW, h); + g2.dispose(); + } } } From 6ba8db039b5c5b0cd60a1f7f9df35c0e434a709b Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Thu, 21 Aug 2025 13:39:37 -0400 Subject: [PATCH 34/53] weird overlapping on undo is gone. --- .../goaltracker/ui/GoalTrackerPanel.java | 28 ++++++++++--------- .../goaltracker/ui/components/ActionBar.java | 4 +++ .../ui/components/ActionBarButton.java | 9 ++++-- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 7df236c..b944e08 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -73,24 +73,23 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) // Action bar (shared style) ActionBar actionBar = new ActionBar(); - - // Left-side actions - ActionBarButton addGoalBtn = new ActionBarButton("+ Add goal", () -> - { - Goal goal = goalManager.createGoal(); - pendingNewGoal = goal; - view(goal); - if (goalAddedListener != null) goalAddedListener.accept(goal); - if (goalUpdatedListener != null) goalUpdatedListener.accept(goal); - }); - - actionBar.left().add(addGoalBtn); + actionBar.right().setBorder(new EmptyBorder(0, 4, 0, 0)); // Right-side actions: Undo/Redo undoButtonRef = new ActionBarButton("Undo", this::doUndo); redoButtonRef = new ActionBarButton("Redo", this::doRedo); actionBar.right().add(undoButtonRef); actionBar.right().add(redoButtonRef); + + ActionBarButton exportButton = new ActionBarButton("Export", () -> { + // TODO: implement export + }); + ActionBarButton importButton = new ActionBarButton("Import", () -> { + // TODO: implement import + }); + actionBar.right().add(exportButton); + actionBar.right().add(importButton); + updateUndoRedoButtons(); // Wrap the title panel with a subtle bottom separator for visual polish @@ -107,7 +106,10 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) JPanel headerContainer = new JPanel(new BorderLayout()); headerContainer.setBackground(ColorScheme.DARK_GRAY_COLOR); headerContainer.add(titleWrapper, BorderLayout.NORTH); - headerContainer.add(actionBar, BorderLayout.SOUTH); + JPanel actionBarWrapper = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0)); + actionBarWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR); + actionBarWrapper.add(actionBar); + headerContainer.add(actionBarWrapper, BorderLayout.SOUTH); goalListPanel = new ListPanel<>(goalManager.getGoals(), (goal) -> { diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java index 6a4fbd5..678a663 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java @@ -2,6 +2,7 @@ import java.awt.BorderLayout; import java.awt.FlowLayout; +import java.awt.Dimension; import javax.swing.JPanel; import javax.swing.border.EmptyBorder; import net.runelite.client.ui.ColorScheme; @@ -20,11 +21,14 @@ public ActionBar() left.setOpaque(true); left.setBackground(ColorScheme.DARK_GRAY_COLOR); + left.setBorder(new EmptyBorder(0, 0, 0, 4)); right.setOpaque(true); right.setBackground(ColorScheme.DARK_GRAY_COLOR); + right.setBorder(new EmptyBorder(0, 4, 0, 0)); spacer.setOpaque(false); + spacer.setPreferredSize(new Dimension(8, 1)); add(left, BorderLayout.WEST); add(right, BorderLayout.EAST); add(spacer, BorderLayout.CENTER); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java index 118f68f..70b6219 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java @@ -52,11 +52,16 @@ protected void paintComponent(Graphics g) int w = getWidth(); int h = getHeight(); + int x = 0; + int y = 0; + int drawW = w; + int drawH = h; + g2.setColor(bg); - g2.fillRoundRect(0, 0, w, h, arc, arc); + g2.fillRoundRect(x, y, drawW, drawH, arc, arc); g2.setColor(outline); - g2.drawRoundRect(0, 0, w - 1, h - 1, arc, arc); + g2.drawRoundRect(x, y, drawW - 1, drawH - 1, arc, arc); g2.dispose(); super.paintComponent(g); From 18c5cd011c70913bc83bc0fc68e3237459c3a1cb Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Thu, 21 Aug 2025 14:27:40 -0400 Subject: [PATCH 35/53] new action bar completed. --- .../goaltracker/ui/GoalTrackerPanel.java | 24 ++++++++++++++----- .../goaltracker/ui/components/ActionBar.java | 24 ++++++++----------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index b944e08..60ca658 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -5,6 +5,7 @@ import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.UndoStack; import com.toofifty.goaltracker.models.task.Task; +import com.toofifty.goaltracker.utils.ReorderableList; import com.toofifty.goaltracker.ui.components.ActionBar; import com.toofifty.goaltracker.ui.components.ActionBarButton; import com.toofifty.goaltracker.ui.components.ListItemPanel; @@ -71,6 +72,13 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) titlePanel.add(titleTextPanel, BorderLayout.WEST); + // Re-add "+ Add goal" to the header (right side) + JPanel addGoalPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0)); + addGoalPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); + ActionBarButton addGoalBtn = new ActionBarButton("+ Add goal", this::addNewGoal); + addGoalPanel.add(addGoalBtn); + titlePanel.add(addGoalPanel, BorderLayout.EAST); + // Action bar (shared style) ActionBar actionBar = new ActionBar(); actionBar.right().setBorder(new EmptyBorder(0, 4, 0, 0)); @@ -78,8 +86,8 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) // Right-side actions: Undo/Redo undoButtonRef = new ActionBarButton("Undo", this::doUndo); redoButtonRef = new ActionBarButton("Redo", this::doRedo); - actionBar.right().add(undoButtonRef); - actionBar.right().add(redoButtonRef); + actionBar.left().add(undoButtonRef); + actionBar.left().add(redoButtonRef); ActionBarButton exportButton = new ActionBarButton("Export", () -> { // TODO: implement export @@ -106,10 +114,7 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) JPanel headerContainer = new JPanel(new BorderLayout()); headerContainer.setBackground(ColorScheme.DARK_GRAY_COLOR); headerContainer.add(titleWrapper, BorderLayout.NORTH); - JPanel actionBarWrapper = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0)); - actionBarWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR); - actionBarWrapper.add(actionBar); - headerContainer.add(actionBarWrapper, BorderLayout.SOUTH); + headerContainer.add(actionBar, BorderLayout.SOUTH); goalListPanel = new ListPanel<>(goalManager.getGoals(), (goal) -> { @@ -293,4 +298,11 @@ public void recordGoalRemoval(Goal goal, int index) undoStack.pushRemove(goal, index); updateUndoRedoButtons(); } + private void addNewGoal() + { + Goal goal = Goal.builder().tasks(ReorderableList.from()).build(); + goalManager.getGoals().add(0, goal); + pendingNewGoal = goal; + view(goal); + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java index 678a663..c919ff0 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java @@ -1,37 +1,33 @@ package com.toofifty.goaltracker.ui.components; -import java.awt.BorderLayout; import java.awt.FlowLayout; -import java.awt.Dimension; import javax.swing.JPanel; import javax.swing.border.EmptyBorder; +import javax.swing.BoxLayout; import net.runelite.client.ui.ColorScheme; public class ActionBar extends JPanel { - private final JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 0)); - private final JPanel right = new JPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0)); - private final JPanel spacer = new JPanel(); + private final JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 3, 0)); + private final JPanel right = new JPanel(new FlowLayout(FlowLayout.RIGHT, 3, 0)); public ActionBar() { - super(new BorderLayout()); + super(); + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); setBackground(ColorScheme.DARK_GRAY_COLOR); - setBorder(new EmptyBorder(6, 10, 6, 10)); + setBorder(new EmptyBorder(6, 6, 6, 6)); left.setOpaque(true); left.setBackground(ColorScheme.DARK_GRAY_COLOR); - left.setBorder(new EmptyBorder(0, 0, 0, 4)); right.setOpaque(true); right.setBackground(ColorScheme.DARK_GRAY_COLOR); - right.setBorder(new EmptyBorder(0, 4, 0, 0)); - spacer.setOpaque(false); - spacer.setPreferredSize(new Dimension(8, 1)); - add(left, BorderLayout.WEST); - add(right, BorderLayout.EAST); - add(spacer, BorderLayout.CENTER); + add(left); + // Removed strut to eliminate extra spacing + // add(javax.swing.Box.createHorizontalStrut(2)); + add(right); } public JPanel left() { return left; } From c293f65168414051d52f8185a19da17ea8f77125 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Thu, 21 Aug 2025 15:05:04 -0400 Subject: [PATCH 36/53] fixed actionbar layout, refresing of icons upon load in. --- .../com/toofifty/goaltracker/GoalManager.java | 44 ++++++ .../goaltracker/GoalTrackerPlugin.java | 31 ++++ .../goaltracker/ui/GoalTrackerPanel.java | 144 ++++++++++-------- 3 files changed, 159 insertions(+), 60 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/GoalManager.java b/src/main/java/com/toofifty/goaltracker/GoalManager.java index 14e0c17..f6b4615 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalManager.java +++ b/src/main/java/com/toofifty/goaltracker/GoalManager.java @@ -28,6 +28,8 @@ public class GoalManager @Getter private final ReorderableList goals = new ReorderableList<>(); + private final List goalsChangedListeners = new ArrayList<>(); + public Goal createGoal() { Goal goal = Goal.builder().build(); @@ -57,6 +59,7 @@ public void save() { config.goalTrackerData(goalSerializer.serialize(goals)); log.info("Saved " + goals.size() + " goals"); + notifyGoalsChanged(); } public void load() @@ -65,6 +68,7 @@ public void load() { this.goals.clear(); this.goals.addAll(goalSerializer.deserialize(config.goalTrackerData())); + notifyGoalsChanged(); log.info("Loaded " + this.goals.size() + " goals"); } catch (Exception e) @@ -72,4 +76,44 @@ public void load() log.error("Failed to load goals!", e); } } + /** + * Return the current goals as JSON for export. + * @param pretty pretty-print output + */ + public String exportJson(boolean pretty) + { + return goalSerializer.serialize(goals, pretty); + } + + /** + * Import goals from JSON and persist them. + */ + public void importJson(String json) + { + try + { + this.goals.clear(); + this.goals.addAll(goalSerializer.deserialize(json)); + save(); + } + catch (Exception e) + { + log.error("Failed to import goals!", e); + } + } + public void addGoalsChangedListener(Runnable listener) + { + if (listener != null && !goalsChangedListeners.contains(listener)) + { + goalsChangedListeners.add(listener); + } + } + + private void notifyGoalsChanged() + { + for (Runnable r : goalsChangedListeners) + { + try { r.run(); } catch (Exception ignored) {} + } + } } \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java index 74dcbe6..86478bf 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java +++ b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java @@ -107,6 +107,8 @@ public class GoalTrackerPlugin extends Plugin @Setter private boolean validateAll = true; + private boolean warmedIcons = false; + @Override protected void startUp() { @@ -140,6 +142,10 @@ protected void startUp() goalManager.save(); }); goalTrackerPanel.onTaskUpdated((task) -> goalManager.save()); + + // Preload item icons at plugin startup so they are visible immediately + warmItemIcons(); + warmedIcons = true; // avoid re-warming on first login tick } @Override @@ -196,10 +202,29 @@ public void notifyTask(Task task) task.setNotified(true); } + public void warmItemIcons() + { + // Iterate all tasks and ensure item icons are requested so they appear without opening search + goalManager.getGoals().forEach(goal -> { + if (goal.getTasks() == null) return; + goal.getTasks().forEach(task -> { + if (task instanceof ItemTask) + { + try { + taskIconService.get((ItemTask) task); + } catch (Exception ignored) { } + } + }); + }); + // Refresh UI after warming to repaint icons + javax.swing.SwingUtilities.invokeLater(goalTrackerPanel::refresh); + } + @Subscribe public void onGameStateChanged(GameStateChanged event) { if (client.getGameState() != GameState.LOGGED_IN) { + warmedIcons = false; return; } @@ -223,6 +248,12 @@ public void onGameTick(GameTick event) // so this need to be done in this subscriber goalTrackerPanel.refresh(); + if (!warmedIcons) + { + warmItemIcons(); + warmedIcons = true; + } + goalManager.getGoals().stream() .flatMap(goal -> goal.getTasks().stream()) .filter(task -> !task.getStatus().isCompleted()) diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 60ca658..4dd9c58 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -15,14 +15,12 @@ import net.runelite.client.ui.PluginPanel; import javax.inject.Inject; -import javax.inject.Singleton; import javax.swing.*; import javax.swing.border.EmptyBorder; +import javax.swing.filechooser.FileNameExtensionFilter; import java.awt.*; -import java.util.Objects; import java.util.function.Consumer; -@Singleton public class GoalTrackerPanel extends PluginPanel implements Refreshable { private final JPanel mainPanel = new JPanel(new BorderLayout()); @@ -45,18 +43,18 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) super(false); this.plugin = plugin; this.goalManager = goalManager; + this.goalManager.addGoalsChangedListener(() -> javax.swing.SwingUtilities.invokeLater(this::refreshHomeListIfVisible)); setBackground(ColorScheme.DARK_GRAY_COLOR); setLayout(new BorderLayout()); setBorder(new EmptyBorder(8, 8, 8, 8)); + // Header with title and + Add goal on right JPanel titlePanel = new JPanel(); titlePanel.setBorder(new EmptyBorder(10, 10, 10, 10)); titlePanel.setLayout(new BorderLayout()); titlePanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - // (Removed "+ Add goal" button from the title panel) - JLabel title = new JLabel("Goal Tracker v2"); title.setForeground(Color.WHITE); title.setFont(FontManager.getRunescapeBoldFont()); @@ -69,43 +67,34 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) titleTextPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); titleTextPanel.add(title); titleTextPanel.add(author); - titlePanel.add(titleTextPanel, BorderLayout.WEST); - // Re-add "+ Add goal" to the header (right side) JPanel addGoalPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0)); addGoalPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); ActionBarButton addGoalBtn = new ActionBarButton("+ Add goal", this::addNewGoal); addGoalPanel.add(addGoalBtn); titlePanel.add(addGoalPanel, BorderLayout.EAST); - // Action bar (shared style) + // Action bar ActionBar actionBar = new ActionBar(); actionBar.right().setBorder(new EmptyBorder(0, 4, 0, 0)); - // Right-side actions: Undo/Redo undoButtonRef = new ActionBarButton("Undo", this::doUndo); redoButtonRef = new ActionBarButton("Redo", this::doRedo); actionBar.left().add(undoButtonRef); actionBar.left().add(redoButtonRef); - ActionBarButton exportButton = new ActionBarButton("Export", () -> { - // TODO: implement export - }); - ActionBarButton importButton = new ActionBarButton("Import", () -> { - // TODO: implement import - }); + ActionBarButton exportButton = new ActionBarButton("Export", this::exportGoalsToFile); + ActionBarButton importButton = new ActionBarButton("Import", this::importGoalsFromFile); actionBar.right().add(exportButton); actionBar.right().add(importButton); updateUndoRedoButtons(); - // Wrap the title panel with a subtle bottom separator for visual polish JPanel titleWrapper = new JPanel(new BorderLayout()); titleWrapper.setBackground(ColorScheme.DARK_GRAY_COLOR); titleWrapper.add(titlePanel, BorderLayout.CENTER); - // 1px divider under the header title JPanel headerSeparator = new JPanel(); headerSeparator.setBackground(ColorScheme.DARKER_GRAY_COLOR); headerSeparator.setPreferredSize(new Dimension(1, 4)); @@ -116,19 +105,13 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) headerContainer.add(titleWrapper, BorderLayout.NORTH); headerContainer.add(actionBar, BorderLayout.SOUTH); - goalListPanel = new ListPanel<>(goalManager.getGoals(), - (goal) -> { - var panel = new ListItemPanel<>(goalManager.getGoals(), goal); - - panel.onClick(e -> this.view(goal)); - panel.add(new GoalItemContent(plugin, goal)); - panel.onRemovedWithIndex((removedGoal, index) -> { - recordGoalRemoval(removedGoal, index); - }); - - return panel; - } - ); + goalListPanel = new ListPanel<>(goalManager.getGoals(), (goal) -> { + var panel = new ListItemPanel<>(goalManager.getGoals(), goal); + panel.onClick(e -> this.view(goal)); + panel.add(new GoalItemContent(plugin, goal)); + panel.onRemovedWithIndex(this::recordGoalRemoval); + return panel; + }); goalListPanel.setGap(0); goalListPanel.setPlaceholder("
No goals yet.
Click + Add goal above to create your first one.
"); @@ -138,26 +121,32 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) home(); } + private void refreshHomeListIfVisible() + { + if (goalPanel == null) + { + goalListPanel.tryBuildList(); + goalListPanel.refresh(); + revalidate(); + repaint(); + } + } + public void view(Goal goal) { removeAll(); - this.goalPanel = new GoalPanel(plugin, goal, this::home); - this.goalPanel.onGoalUpdated(this.goalUpdatedListener); this.goalPanel.onTaskAdded(this.taskAddedListener); this.goalPanel.onTaskUpdated(this.taskUpdatedListener); - add(this.goalPanel, BorderLayout.CENTER); this.goalPanel.refresh(); - revalidate(); repaint(); } public void home() { - // Auto-remove an empty goal created via "+ Add goal" if user backs out without adding tasks if (pendingNewGoal != null) { try { @@ -168,47 +157,34 @@ public void home() pendingNewGoal = null; } } - // Clear the GoalTrackerPanel content and switch back to the main panel removeAll(); - - // Rebuild the list BEFORE attaching the main panel to ensure layout has components goalListPanel.tryBuildList(); goalListPanel.refresh(); - - // Make sure the list is the CENTER of the main panel (in case layout got disturbed) mainPanel.remove(goalListPanel); mainPanel.add(goalListPanel, BorderLayout.CENTER); - - // Attach main panel and validate add(mainPanel, BorderLayout.CENTER); mainPanel.revalidate(); mainPanel.repaint(); - revalidate(); repaint(); - this.goalPanel = null; } @Override public void refresh() { - // refresh single-view goal for (Component component : getComponents()) { if (component instanceof Refreshable) { ((Refreshable) component).refresh(); } } - goalListPanel.refresh(); } public void onGoalUpdated(Consumer listener) { this.goalUpdatedListener = listener; - this.goalListPanel.onUpdated(this.goalUpdatedListener); - if (this.goalPanel != null) { this.goalPanel.onGoalUpdated(this.goalUpdatedListener); } @@ -217,7 +193,6 @@ public void onGoalUpdated(Consumer listener) public void onTaskUpdated(Consumer listener) { this.taskUpdatedListener = listener; - if (this.goalPanel != null) { this.goalPanel.onTaskUpdated(this.taskUpdatedListener); } @@ -226,7 +201,6 @@ public void onTaskUpdated(Consumer listener) public void onTaskAdded(Consumer listener) { this.taskAddedListener = listener; - if (this.goalPanel != null) { this.goalPanel.onTaskAdded(this.taskAddedListener); } @@ -250,12 +224,9 @@ private void doUndo() { var entry = undoStack.popForUndo(); if (entry == null) { updateUndoRedoButtons(); return; } - java.util.List goals = goalManager.getGoals(); int idx = Math.max(0, Math.min(entry.getIndex(), goals.size())); goals.add(idx, entry.getItem()); - - // If we are on the home view, refresh the list; otherwise leave as-is. if (goalPanel == null) { goalListPanel.tryBuildList(); @@ -270,14 +241,12 @@ private void doRedo() { var entry = undoStack.popForRedo(); if (entry == null) { updateUndoRedoButtons(); return; } - java.util.List goals = goalManager.getGoals(); int idx = goals.indexOf(entry.getItem()); if (idx >= 0) { goals.remove(idx); } - // If on home view, refresh if (goalPanel == null) { goalListPanel.tryBuildList(); @@ -288,16 +257,12 @@ private void doRedo() updateUndoRedoButtons(); } - /** - * Call this when a goal is removed from the home list to record it for Undo. - * @param goal the goal that was removed - * @param index the index it had before removal - */ public void recordGoalRemoval(Goal goal, int index) { undoStack.pushRemove(goal, index); updateUndoRedoButtons(); } + private void addNewGoal() { Goal goal = Goal.builder().tasks(ReorderableList.from()).build(); @@ -305,4 +270,63 @@ private void addNewGoal() pendingNewGoal = goal; view(goal); } + + private void exportGoalsToFile() + { + JFileChooser chooser = new JFileChooser(); + chooser.setAcceptAllFileFilterUsed(false); + chooser.setMultiSelectionEnabled(false); + chooser.setFileFilter(new FileNameExtensionFilter("JSON files (*.json)", "json")); + chooser.setDialogTitle("Export goals to JSON"); + chooser.setSelectedFile(new java.io.File("goals.json")); + int res = chooser.showSaveDialog(this); + if (res != JFileChooser.APPROVE_OPTION) { return; } + java.io.File file = chooser.getSelectedFile(); + if (!file.getName().toLowerCase().endsWith(".json")) + { + file = new java.io.File(file.getParentFile(), file.getName() + ".json"); + } + try (java.io.FileWriter fw = new java.io.FileWriter(file)) + { + String json = goalManager.exportJson(true); + fw.write(json); + fw.flush(); + JOptionPane.showMessageDialog(this, "Exported " + goalManager.getGoals().size() + " goal(s) to\n" + file.getAbsolutePath(), "Export Complete", JOptionPane.INFORMATION_MESSAGE); + } + catch (Exception ex) + { + JOptionPane.showMessageDialog(this, "Failed to export goals: " + ex.getMessage(), "Export Error", JOptionPane.ERROR_MESSAGE); + } + } + + private void importGoalsFromFile() + { + JFileChooser chooser = new JFileChooser(); + chooser.setAcceptAllFileFilterUsed(false); + chooser.setMultiSelectionEnabled(false); + chooser.setFileFilter(new FileNameExtensionFilter("JSON files (*.json)", "json")); + chooser.setDialogTitle("Import goals from JSON"); + int res = chooser.showOpenDialog(this); + if (res != JFileChooser.APPROVE_OPTION) { return; } + java.io.File file = chooser.getSelectedFile(); + try + { + String json = new String(java.nio.file.Files.readAllBytes(file.toPath())); + goalManager.importJson(json); + plugin.warmItemIcons(); + if (goalPanel != null) { + home(); + } else { + goalListPanel.tryBuildList(); + goalListPanel.refresh(); + revalidate(); + repaint(); + } + JOptionPane.showMessageDialog(this, "Imported goals from\n" + file.getAbsolutePath(), "Import Complete", JOptionPane.INFORMATION_MESSAGE); + } + catch (Exception ex) + { + JOptionPane.showMessageDialog(this, "Failed to import goals: " + ex.getMessage(), "Import Error", JOptionPane.ERROR_MESSAGE); + } + } } From a2538eb02e00c9bb5f62a44133c25ba7b1efdf12 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Thu, 21 Aug 2025 15:09:20 -0400 Subject: [PATCH 37/53] updates --- README.md | 8 ++++++++ changelog.md | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/README.md b/README.md index 593c523..ecd1347 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,9 @@ - Thicker header divider under “Goal Tracker” for clearer separation. - Automatic refresh propagation so Home goals update instantly when tasks change. - Completion chat messages delivered as proper Game messages with configurable colors. +- Export and Import functionality: save your goals to a JSON file and import them back with full UI refresh. +- Automatic warming of item icons so they load at startup, on login, and after import. +- GoalsChangedListener system to auto-refresh the Home panel when goals change. ### ♻️ Redesigned Features @@ -43,6 +46,7 @@ - Copy and paste support in the goal name input field - Context menu entries rebuilt dynamically before opening to always reflect the latest state. - Task and goal content now force an initial refresh so icons and text render correctly at login. +- Refined ActionBar spacing to ensure Export and Import buttons fit without overlap.
@@ -60,6 +64,10 @@ - Fixed child task refresh issues by recursively refreshing all descendants. - Fixed blank panel issue when switching from Home to Goal view. - Fixed completion chat message not appearing on task completion. +- Export/Import buttons were previously non-functional; now they work correctly. +- Item icons sometimes failed to appear until entering a goal; now fixed by preloading at startup/login and after import. +- Home panel not refreshing after completing a task; fixed with a new listener system. +- Overlapping issue around the Export button fixed by layout adjustments. diff --git a/changelog.md b/changelog.md index ac0a144..9f0687c 100644 --- a/changelog.md +++ b/changelog.md @@ -23,6 +23,10 @@ - Task list panel context menus reorganized with a **Move** submenu and cascading complete/incomplete toggle that applies to children. - Initial refresh calls for Task and Goal content so icons/text render correctly on login. - Automatic ancestor `ListPanel` refresh propagation to keep Home goal list in sync when tasks change. +- Export and Import functionality: Goals can now be exported to JSON and imported back with full UI refresh. +- JSON file filter in Import/Export dialogs for safer file selection. +- Automatic warming of ItemTask icons on plugin startup and login tick, and after JSON import, ensuring icons display immediately. +- GoalsChangedListener system in GoalManager to notify panels and auto-refresh home view when goals are saved or loaded. ### Changed - Pre-req button made more compact (~25% smaller). @@ -64,6 +68,8 @@ - Goal cards now use a lighter fill with a full shadow around borders, and hover/press only affect the card face. - Header divider under “Goal Tracker” made thicker (4px) for stronger separation. - Right-click menus refactored so Tasks build their own menu and Goals build theirs, preventing duplicate/unusable items. +- ActionBar spacing refined between Redo and Export buttons to prevent overlap and fit Import button. +- Plugin startup now triggers item icon warm-up so icons are ready before login. ### Fixed - Home panel Undo/Redo buttons removed; these controls now exist only in Goal view. @@ -77,3 +83,7 @@ - Fixed child task refresh issues after parent complete/incomplete cascades by recursively refreshing all descendants. - Fixed blank panel issue when switching from Home to Goal view by only using inner card body for Goal rows. - Fixed completion chat message not appearing; now delivered as a proper `GAMEMESSAGE` with customizable config color. +- Export/Import buttons previously non-functional; now wired to save/load JSON correctly. +- Item icons not appearing until entering a goal; fixed by warming icons at startup/login and after import. +- Home panel not refreshing after task completion; fixed with GoalsChangedListener refresh hook. +- Overlapping UI issue around Export button resolved by adjusting panel borders and layout. From f187258cc4e0ce37e1b6f28279287212a28128bb Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Thu, 21 Aug 2025 23:26:29 -0400 Subject: [PATCH 38/53] massive improvements. --- .../java/com/toofifty/goaltracker/GoalManager.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main/java/com/toofifty/goaltracker/GoalManager.java b/src/main/java/com/toofifty/goaltracker/GoalManager.java index f6b4615..b8ee876 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalManager.java +++ b/src/main/java/com/toofifty/goaltracker/GoalManager.java @@ -37,6 +37,19 @@ public Goal createGoal() return goal; } + /** + * Append a batch of goals and persist + notify listeners. + */ + public void addGoals(List newGoals) + { + if (newGoals == null || newGoals.isEmpty()) + { + return; + } + goals.addAll(newGoals); + save(); + } + @SuppressWarnings("unchecked") public List getTasksByTypeAndAnyStatus(TaskType type, Status... statuses) { From 44625cb56ca547b8572bc937e0d49ab309e62417 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Thu, 21 Aug 2025 23:26:37 -0400 Subject: [PATCH 39/53] massive improvements. --- README.md | 7 + changelog.md | 8 + .../presets/GoalPresetRepository.java | 172 ++++++++++++++++++ .../goaltracker/ui/GoalItemContent.java | 139 +++++++++++--- .../goaltracker/ui/GoalTrackerPanel.java | 56 +++++- .../goaltracker/ui/TaskItemContent.java | 98 +++++++++- .../ui/components/ListTaskPanel.java | 46 +++++ 7 files changed, 490 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java diff --git a/README.md b/README.md index ecd1347..2e93f31 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,10 @@ - Export and Import functionality: save your goals to a JSON file and import them back with full UI refresh. - Automatic warming of item icons so they load at startup, on login, and after import. - GoalsChangedListener system to auto-refresh the Home panel when goals change. +- Preset Goal Lists: add goals from built-in presets (Quest Cape Core, Early Game Ironman) via a new “Add from Preset…” button. +- Presets automatically expand quest prerequisites using the existing right-click prereq logic. +- Goal titles and ManualTask descriptions now support click-to-edit with inline text fields. +- Long goal and task titles are ellipsized with … and show full text on hover. ### ♻️ Redesigned Features @@ -47,6 +51,9 @@ - Context menu entries rebuilt dynamically before opening to always reflect the latest state. - Task and goal content now force an initial refresh so icons and text render correctly at login. - Refined ActionBar spacing to ensure Export and Import buttons fit without overlap. +- Progress text (e.g., “1/10”) on goal cards now has a reserved width and never clips. +- + Add goal and Add from Preset buttons restyled and stacked vertically for cleaner layout. +- Task rows updated to match goal cards with ellipsized titles and consistent styling. diff --git a/changelog.md b/changelog.md index 9f0687c..e81c79d 100644 --- a/changelog.md +++ b/changelog.md @@ -27,6 +27,11 @@ - JSON file filter in Import/Export dialogs for safer file selection. - Automatic warming of ItemTask icons on plugin startup and login tick, and after JSON import, ensuring icons display immediately. - GoalsChangedListener system in GoalManager to notify panels and auto-refresh home view when goals are saved or loaded. +- Preset Goal Lists: Added “Add from Preset…” button in the Goal Tracker panel, with initial presets (Quest Cape Core, Early Game Ironman). Presets now expand to include prerequisite quests automatically. +- Centralized presets into a new `GoalPresetRepository` class for easier management and expansion. +- Automatic prerequisite expansion for presets leverages the same logic as the quest right-click **Add prereqs** option. +- Ellipsized titles: Goal cards and Task rows now ellipsize long titles with `…` and show full text on hover via tooltip. +- Click-to-edit: Goal titles and ManualTask descriptions can now be edited by clicking their label; label swaps to an inline text field and saves on Enter or blur. ### Changed - Pre-req button made more compact (~25% smaller). @@ -70,6 +75,9 @@ - Right-click menus refactored so Tasks build their own menu and Goals build theirs, preventing duplicate/unusable items. - ActionBar spacing refined between Redo and Export buttons to prevent overlap and fit Import button. - Plugin startup now triggers item icon warm-up so icons are ready before login. +- “+ Add goal” and “Add from Preset…” buttons now stacked vertically in the Goal Tracker panel header for cleaner layout. +- Goal card progress text (e.g., “1/10”) reserved fixed width and no longer clips; typography is consistent and does not shrink. +- Task rows updated with consistent styling and ellipsis/edit behavior, matching Goal cards for a unified UI. ### Fixed - Home panel Undo/Redo buttons removed; these controls now exist only in Goal view. diff --git a/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java b/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java new file mode 100644 index 0000000..3f18c1f --- /dev/null +++ b/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java @@ -0,0 +1,172 @@ +package com.toofifty.goaltracker.presets; + +import com.toofifty.goaltracker.models.Goal; +import com.toofifty.goaltracker.models.enums.Status; +import com.toofifty.goaltracker.models.task.ManualTask; +import com.toofifty.goaltracker.models.task.QuestTask; +import net.runelite.api.Quest; +import com.toofifty.goaltracker.utils.ReorderableList; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.HashSet; + +/** + * Central repository for built-in goal presets. + * + * Keep this tiny and dependency‑free so it’s easy to expand. + */ +public class GoalPresetRepository { + + /** Simple container representing a named preset. */ + public static class Preset { + private final String name; + private final String description; + private final List goals; + + public Preset(String name, String description, List goals) { + this.name = name; + this.description = description; + this.goals = goals; + } + + public String getName() { return name; } + public String getDescription() { return description; } + public List getGoals() { return goals; } + + @Override public String toString() { return name; } + } + + // Minimal prerequisite graph for the quests we reference + private static final Map> PREREQS = new HashMap<>(); + static { + // Simple chains + PREREQS.put(Quest.FAIRYTALE_II__CURE_A_QUEEN, Arrays.asList(Quest.FAIRYTALE_I__GROWING_PAINS)); + PREREQS.put(Quest.MOURNINGS_END_PART_II, Arrays.asList(Quest.MOURNINGS_END_PART_I)); + PREREQS.put(Quest.MOURNINGS_END_PART_I, Arrays.asList(Quest.ROVING_ELVES)); + PREREQS.put(Quest.ROVING_ELVES, Arrays.asList(Quest.REGICIDE, Quest.WATERFALL_QUEST)); + PREREQS.put(Quest.REGICIDE, Arrays.asList(Quest.UNDERGROUND_PASS)); + // Desert Treasure I chain (trimmed to common quest prereqs) + PREREQS.put(Quest.DESERT_TREASURE_I, Arrays.asList( + Quest.PRIEST_IN_PERIL, + Quest.THE_DIG_SITE, + Quest.THE_TOURIST_TRAP, + Quest.TEMPLE_OF_IKOV, + Quest.TROLL_STRONGHOLD + )); + PREREQS.put(Quest.TROLL_STRONGHOLD, Arrays.asList(Quest.DEATH_PLATEAU)); + // Animal Magnetism common prereqs + PREREQS.put(Quest.ANIMAL_MAGNETISM, Arrays.asList( + Quest.THE_RESTLESS_GHOST, + Quest.ERNEST_THE_CHICKEN, + Quest.PRIEST_IN_PERIL + )); + // Lunar Diplomacy common prereqs + PREREQS.put(Quest.LUNAR_DIPLOMACY, Arrays.asList( + Quest.THE_FREMENNIK_TRIALS + )); + // Song of the Elves depends on the elf line; ensure ME2 present via above mappings + PREREQS.put(Quest.SONG_OF_THE_ELVES, Arrays.asList(Quest.MOURNINGS_END_PART_II)); + // Sins of the Father (trimmed main chain) + PREREQS.put(Quest.SINS_OF_THE_FATHER, Arrays.asList( + Quest.A_TASTE_OF_HOPE + )); + } + + private static void addQuestWithPrereqs(Quest quest, Set seen, ReorderableList out) + { + if (seen.contains(quest)) return; + List reqs = PREREQS.get(quest); + if (reqs != null) { + for (Quest q : reqs) { + addQuestWithPrereqs(q, seen, out); + } + } + // After ensuring prereqs are added, add the quest itself if missing + if (!seen.contains(quest)) { + out.add(QuestTask.builder().quest(quest).build()); + seen.add(quest); + } + } + + private static void expandWithPrereqs(ReorderableList tasks) + { + Set present = new HashSet<>(); + for (com.toofifty.goaltracker.models.task.Task t : tasks) { + if (t instanceof QuestTask) { + present.add(((QuestTask) t).getQuest()); + } + } + // Build a new ordered list with prereqs inserted before dependents + ReorderableList expanded = new ReorderableList<>(); + Set added = new HashSet<>(); + for (com.toofifty.goaltracker.models.task.Task t : new ArrayList<>(tasks)) { + if (t instanceof QuestTask) { + addQuestWithPrereqs(((QuestTask) t).getQuest(), added, expanded); + } else { + expanded.add(t); + } + } + tasks.clear(); + tasks.addAll(expanded); + } + + /** Return all available presets. */ + public static List getAll() { + List list = new ArrayList<>(); + list.add(buildQuestCapeCore()); + list.add(buildEarlyIronman()); + return list; + } + + private static Preset buildQuestCapeCore() { + ReorderableList tasks = ReorderableList.from( + QuestTask.builder().quest(Quest.ANIMAL_MAGNETISM).build(), + QuestTask.builder().quest(Quest.FAIRYTALE_I__GROWING_PAINS).build(), + QuestTask.builder().quest(Quest.FAIRYTALE_II__CURE_A_QUEEN).build(), + QuestTask.builder().quest(Quest.DESERT_TREASURE_I).build(), + QuestTask.builder().quest(Quest.DRAGON_SLAYER_II).build(), + QuestTask.builder().quest(Quest.LUNAR_DIPLOMACY).build(), + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), + QuestTask.builder().quest(Quest.UNDERGROUND_PASS).build(), + QuestTask.builder().quest(Quest.REGICIDE).build(), + QuestTask.builder().quest(Quest.ROVING_ELVES).build(), + QuestTask.builder().quest(Quest.MOURNINGS_END_PART_I).build(), + QuestTask.builder().quest(Quest.MOURNINGS_END_PART_II).build(), + QuestTask.builder().quest(Quest.SONG_OF_THE_ELVES).build(), + QuestTask.builder().quest(Quest.SINS_OF_THE_FATHER).build() + ); + expandWithPrereqs(tasks); + Goal goal = Goal.builder() + .description("Quest Cape — Core unlocks") + .tasks(tasks) + .build(); + return new Preset( + "Quest Cape (Core)", + "Essential quests and unlocks toward Quest Cape.", + Arrays.asList(goal) + ); + } + + private static Preset buildEarlyIronman() { + Goal early = Goal.builder() + .description("Early Game Ironman — Foundation") + .tasks(ReorderableList.from( + ManualTask.builder().description("Unlock Ardougne cloak 1 (monastery teleport)").status(Status.NOT_STARTED).build(), + ManualTask.builder().description("Do Wintertodt to ~70 Firemaking").status(Status.NOT_STARTED).build(), + ManualTask.builder().description("Get graceful set").status(Status.NOT_STARTED).build(), + ManualTask.builder().description("Obtain Dramen staff (Fairy rings)").status(Status.NOT_STARTED).build(), + ManualTask.builder().description("Start Barrows for early gear and runes").status(Status.NOT_STARTED).build() + )) + .build(); + return new Preset( + "Early Game Ironman", + "Starter progression path for fresh iron accounts.", + Arrays.asList(early) + ); + } +} diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java index 673fa8d..36e6216 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java @@ -14,16 +14,25 @@ import java.awt.event.MouseEvent; import javax.swing.SwingUtilities; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; + import static com.toofifty.goaltracker.utils.Constants.STATUS_TO_COLOR; public class GoalItemContent extends JPanel implements Refreshable { - private final JTextField title = new JTextField(); + private final JLabel titleLabel = new JLabel(); + private final JTextField titleEdit = new JTextField(); private final JLabel progress = new JLabel(); private final SlimBar progressBar = new SlimBar(); + private final JPanel titleStack = new JPanel(new CardLayout()); private final Goal goal; + private static final int MIN_TITLE_FONT = 11; + private static final int MIN_PROGRESS_FONT = 10; + private JPanel topRow; + GoalItemContent(GoalTrackerPlugin plugin, Goal goal) { super(new BorderLayout()); @@ -34,33 +43,43 @@ public class GoalItemContent extends JPanel implements Refreshable setOpaque(false); setBackground(null); - JPanel topRow = new JPanel(new BorderLayout()); + topRow = new JPanel(new BorderLayout()); topRow.setOpaque(false); - topRow.add(title, BorderLayout.WEST); - // Make goal title editable with standard copy/paste - title.setBorder(null); - title.setOpaque(false); - title.setEditable(true); - title.setDragEnabled(true); - title.setCaretPosition(0); - // Commit edits on Enter and when focus is lost - title.addActionListener(e -> { - String newText = title.getText(); - if (newText != null) { - goal.setDescription(newText); - } + + // Title label (display mode) + titleLabel.setBorder(null); + titleLabel.setOpaque(false); + titleLabel.setFocusable(false); + titleLabel.setToolTipText(null); + + // Title edit (edit mode) + titleEdit.setBorder(null); + titleEdit.setOpaque(false); + titleEdit.setDragEnabled(true); + + titleStack.setOpaque(false); + titleStack.add(titleLabel, "label"); + titleStack.add(titleEdit, "edit"); + topRow.add(titleStack, BorderLayout.WEST); + + // Swap to edit on label click + titleLabel.addMouseListener(new MouseAdapter() { + @Override public void mouseClicked(MouseEvent e) { enterEdit(); } }); - title.addFocusListener(new java.awt.event.FocusAdapter() { - @Override - public void focusLost(java.awt.event.FocusEvent e) { - String newText = title.getText(); - if (newText != null) { - goal.setDescription(newText); - } - } + // Commit edits on Enter and when focus is lost + titleEdit.addActionListener(e -> exitEdit(true)); + titleEdit.addFocusListener(new java.awt.event.FocusAdapter() { + @Override public void focusLost(java.awt.event.FocusEvent e) { exitEdit(true); } }); topRow.add(progress, BorderLayout.EAST); + + // Reserve fixed width for progress like 999/999 so it never clips + int progW = getFontMetrics(progress.getFont()).stringWidth("999/999"); + Dimension progSize = new Dimension(progW, progress.getPreferredSize().height); + progress.setPreferredSize(progSize); + progress.setMinimumSize(progSize); + add(topRow, BorderLayout.CENTER); // Slim custom progress bar under the title row @@ -71,13 +90,16 @@ public void focusLost(java.awt.event.FocusEvent e) { // Initialize visible text and colors immediately (before first refresh) { Color color = STATUS_TO_COLOR.get(goal.getStatus()); - title.setText(goal.getDescription()); - title.setForeground(color); - title.setCaretColor(color); + updateTitleLabel(); + titleEdit.setCaretColor(color); progress.setText(goal.getComplete().size() + "/" + goal.getTasks().size()); progress.setForeground(color); } + topRow.addComponentListener(new ComponentAdapter() { + @Override public void componentResized(ComponentEvent e) { updateTitleLabel(); } + }); + plugin.getUiStatusManager().addRefresher(goal, this::refresh); // Ensure right-click on any child shows the parent ListItemPanel context menu @@ -107,7 +129,8 @@ private void maybeShow(MouseEvent e) }; this.addMouseListener(forwardPopup); - title.addMouseListener(forwardPopup); + titleLabel.addMouseListener(forwardPopup); + titleEdit.addMouseListener(forwardPopup); progress.addMouseListener(forwardPopup); progressBar.addMouseListener(forwardPopup); // Ensure item icon/text initialize on first render (e.g., on login) @@ -127,9 +150,8 @@ public void refresh() { Color color = STATUS_TO_COLOR.get(goal.getStatus()); - title.setText(goal.getDescription()); - title.setForeground(color); - title.setCaretColor(color); + updateTitleLabel(); + titleEdit.setCaretColor(color); progress.setText( goal.getComplete().size() + "/" + goal.getTasks().size()); @@ -168,4 +190,61 @@ protected void paintComponent(Graphics g) { g2.dispose(); } } + + private void updateTitleLabel() + { + if (topRow == null) return; + String full = goal.getDescription() != null ? goal.getDescription() : ""; + titleLabel.setToolTipText(full.isEmpty() ? null : full); + + // Compute available width for title (row width minus progress preferred width and a small gap) + int rowW = topRow.getWidth(); + if (rowW <= 0) { titleLabel.setText(full); return; } + int gap = 8; + int avail = Math.max(16, rowW - progress.getPreferredSize().width - gap); + + FontMetrics fm = titleLabel.getFontMetrics(titleLabel.getFont()); + if (fm.stringWidth(full) <= avail) { + titleLabel.setText(full); + return; + } + String ellipsis = "…"; + int ellW = fm.stringWidth(ellipsis); + String text = full; + // Binary-like trim from the end until it fits + int lo = 0, hi = full.length(); + int cut = hi; + while (lo <= hi) { + int mid = (lo + hi) >>> 1; + String candidate = full.substring(0, Math.max(0, mid)) + ellipsis; + if (fm.stringWidth(candidate) <= avail) { + cut = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + text = full.substring(0, Math.max(0, cut)) + ellipsis; + titleLabel.setText(text); + } + + private void enterEdit() + { + titleEdit.setText(goal.getDescription() != null ? goal.getDescription() : ""); + ((CardLayout) titleStack.getLayout()).show(titleStack, "edit"); + titleEdit.requestFocusInWindow(); + titleEdit.selectAll(); + } + + private void exitEdit(boolean save) + { + if (save) { + String newText = titleEdit.getText(); + if (newText != null) { + goal.setDescription(newText); + } + } + ((CardLayout) titleStack.getLayout()).show(titleStack, "label"); + updateTitleLabel(); + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 4dd9c58..e6963d6 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -4,6 +4,8 @@ import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.UndoStack; +import com.toofifty.goaltracker.presets.GoalPresetRepository; +import com.toofifty.goaltracker.presets.GoalPresetRepository.Preset; import com.toofifty.goaltracker.models.task.Task; import com.toofifty.goaltracker.utils.ReorderableList; import com.toofifty.goaltracker.ui.components.ActionBar; @@ -69,10 +71,17 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) titleTextPanel.add(author); titlePanel.add(titleTextPanel, BorderLayout.WEST); - JPanel addGoalPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0)); + JPanel addGoalPanel = new JPanel(); + addGoalPanel.setLayout(new BoxLayout(addGoalPanel, BoxLayout.Y_AXIS)); addGoalPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); + JPanel addGoalRow = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); + addGoalRow.setBackground(ColorScheme.DARK_GRAY_COLOR); ActionBarButton addGoalBtn = new ActionBarButton("+ Add goal", this::addNewGoal); - addGoalPanel.add(addGoalBtn); + addGoalRow.add(addGoalBtn); + addGoalPanel.add(addGoalRow); + addGoalPanel.add(Box.createVerticalStrut(4)); + ActionBarButton addFromPresetBtn = new ActionBarButton("Add from Preset…", this::addFromPreset); + addGoalPanel.add(addFromPresetBtn); titlePanel.add(addGoalPanel, BorderLayout.EAST); // Action bar @@ -329,4 +338,47 @@ private void importGoalsFromFile() JOptionPane.showMessageDialog(this, "Failed to import goals: " + ex.getMessage(), "Import Error", JOptionPane.ERROR_MESSAGE); } } + + private void addFromPreset() + { + java.util.List options = GoalPresetRepository.getAll(); + + JComboBox combo = new JComboBox<>(options.toArray(new Preset[0])); + JTextArea details = new JTextArea(6, 36); + details.setWrapStyleWord(true); + details.setLineWrap(true); + details.setEditable(false); + details.setBackground(new JPanel().getBackground()); + + java.util.function.Consumer update = (opt) -> { + if (opt == null) { details.setText(""); return; } + int goals = opt.getGoals().size(); + int tasks = opt.getGoals().stream().mapToInt(g -> g.getTasks().size()).sum(); + details.setText(opt.getDescription() + "\n\nThis will add " + goals + " goal(s) with " + tasks + " task(s)."); + }; + combo.addActionListener(e -> update.accept((Preset) combo.getSelectedItem())); + update.accept((Preset) combo.getSelectedItem()); + + JPanel panel = new JPanel(new BorderLayout(6,6)); + panel.add(new JLabel("Choose a preset:"), BorderLayout.NORTH); + panel.add(combo, BorderLayout.CENTER); + panel.add(new JScrollPane(details), BorderLayout.SOUTH); + + int res = JOptionPane.showConfirmDialog(this, panel, "Add from Preset…", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); + if (res != JOptionPane.OK_OPTION) { return; } + + Preset selected = (Preset) combo.getSelectedItem(); + if (selected == null) { return; } + + goalManager.addGoals(selected.getGoals()); + + if (goalPanel != null) { + home(); + } else { + goalListPanel.tryBuildList(); + goalListPanel.refresh(); + revalidate(); + repaint(); + } + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java index 7a78151..fca21c4 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java @@ -24,13 +24,23 @@ import java.awt.event.MouseEvent; import com.toofifty.goaltracker.models.enums.Status; +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; +import java.awt.CardLayout; + +import com.toofifty.goaltracker.models.task.ManualTask; + public class TaskItemContent extends JPanel implements Refreshable { private final Task task; private final Goal goal; private final TaskIconService iconService; private final JLabel titleLabel = new JLabel(); + private final JTextField titleEdit = new JTextField(); + private final JPanel titleStack = new JPanel(new CardLayout()); private final JLabel iconLabel = new JLabel(); + private JPanel iconWrapper; + private boolean titleEditable; private final GoalTrackerPlugin plugin; private ActionHistory actionHistory; @@ -44,15 +54,39 @@ public class TaskItemContent extends JPanel implements Refreshable iconService = plugin.getTaskIconService(); titleLabel.setPreferredSize(new Dimension(0, 24)); - add(titleLabel, BorderLayout.CENTER); - - JPanel iconWrapper = new JPanel(new BorderLayout()); + titleLabel.setBorder(null); + titleLabel.setOpaque(false); + titleEdit.setBorder(null); + titleEdit.setOpaque(false); + titleEdit.setDragEnabled(true); + + titleStack.setOpaque(false); + titleStack.add(titleLabel, "label"); + titleStack.add(titleEdit, "edit"); + add(titleStack, BorderLayout.CENTER); + + iconWrapper = new JPanel(new BorderLayout()); iconWrapper.setBorder(new EmptyBorder(4, 0, 0, 4)); iconWrapper.add(iconLabel, BorderLayout.NORTH); add(iconWrapper, BorderLayout.WEST); plugin.getUiStatusManager().addRefresher(task, this::refresh); + this.addComponentListener(new ComponentAdapter() { + @Override public void componentResized(ComponentEvent e) { updateTitleLabel(); } + }); + + titleEditable = (task instanceof ManualTask); + if (titleEditable) { + titleLabel.addMouseListener(new MouseAdapter() { + @Override public void mouseClicked(MouseEvent e) { enterEdit(); } + }); + titleEdit.addActionListener(e -> exitEdit(true)); + titleEdit.addFocusListener(new java.awt.event.FocusAdapter() { + @Override public void focusLost(java.awt.event.FocusEvent e) { exitEdit(true); } + }); + } + // Right-click to toggle completion with ActionHistory MouseAdapter contextMenuListener = new MouseAdapter() { @@ -98,7 +132,9 @@ private void showMenuIfNeeded(MouseEvent e) // Attach listener to multiple components to make right-click reliable across platforms this.addMouseListener(contextMenuListener); + titleStack.addMouseListener(contextMenuListener); titleLabel.addMouseListener(contextMenuListener); + titleEdit.addMouseListener(contextMenuListener); iconLabel.addMouseListener(contextMenuListener); } @@ -110,8 +146,8 @@ public void setActionHistory(ActionHistory history) @Override public void refresh() { - titleLabel.setText(task.toString()); titleLabel.setForeground(STATUS_TO_COLOR.get(task.getStatus())); + updateTitleLabel(); int indent = 16 * task.getIndentLevel(); iconLabel.setIcon(iconService.get(task)); @@ -128,4 +164,58 @@ public void setBackground(Color bg) component.setBackground(bg); } } + + private void updateTitleLabel() + { + String full = task.toString(); + titleLabel.setToolTipText((full == null || full.isEmpty()) ? null : full); + if (getWidth() <= 0) { titleLabel.setText(full); return; } + + int insets = 0; + if (getBorder() != null) { + Insets ins = getBorder().getBorderInsets(this); + insets = (ins.left + ins.right); + } + int iconW = iconWrapper != null ? iconWrapper.getPreferredSize().width : 0; + int gap = 8; + int avail = Math.max(16, getWidth() - insets - iconW - gap); + + FontMetrics fm = titleLabel.getFontMetrics(titleLabel.getFont()); + if (full == null) full = ""; + if (fm.stringWidth(full) <= avail) { titleLabel.setText(full); return; } + + String ellipsis = "…"; + int lo = 0, hi = full.length(); + int cut = hi; + while (lo <= hi) { + int mid = (lo + hi) >>> 1; + String candidate = full.substring(0, Math.max(0, mid)) + ellipsis; + if (fm.stringWidth(candidate) <= avail) { cut = mid; lo = mid + 1; } + else { hi = mid - 1; } + } + titleLabel.setText(full.substring(0, Math.max(0, cut)) + ellipsis); + } + + private void enterEdit() + { + if (!titleEditable) return; + titleEdit.setText(task.toString()); + ((CardLayout) titleStack.getLayout()).show(titleStack, "edit"); + titleEdit.requestFocusInWindow(); + titleEdit.selectAll(); + } + + private void exitEdit(boolean save) + { + if (!titleEditable) { ((CardLayout) titleStack.getLayout()).show(titleStack, "label"); return; } + if (save) { + String newText = titleEdit.getText(); + if (newText != null && task instanceof ManualTask) { + ((ManualTask) task).setDescription(newText); + } + } + ((CardLayout) titleStack.getLayout()).show(titleStack, "label"); + updateTitleLabel(); + plugin.getUiStatusManager().refresh(goal); + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java index 33689d7..ac397f3 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java @@ -565,4 +565,50 @@ private void refreshParentList() repaint(); } } + /** + * Programmatically add quest prerequisites using the same logic as the context menu action. + * This lets callers (e.g., presets) trigger prereq insertion without simulating a right-click. + */ + public static void addPrereqsForTask(ReorderableList list, Task item) + { + if (!(item instanceof com.toofifty.goaltracker.models.task.QuestTask)) { + return; + } + com.toofifty.goaltracker.models.task.QuestTask questTask = (com.toofifty.goaltracker.models.task.QuestTask) item; + int baseIndent = item.getIndentLevel(); + + // Gather existing children under this task to avoid duplicates + java.util.Set existingKeys = new java.util.HashSet<>(); + int parentIndex = list.indexOf(item); + for (int i = parentIndex + 1; i < list.size(); i++) { + Task child = list.get(i); + if (child.getIndentLevel() <= baseIndent) break; + existingKeys.add(child.getClass().getName() + "|" + child.toString()); + } + + java.util.List raw = QuestRequirements.getRequirements(questTask.getQuest(), baseIndent + 1); + if (raw == null || raw.isEmpty()) { + return; + } + + // Filter out any already-present entries + java.util.List filtered = new java.util.ArrayList<>(); + for (com.toofifty.goaltracker.models.task.Task t : raw) { + String key = t.getClass().getName() + "|" + t.toString(); + if (!existingKeys.contains(key)) { + filtered.add(t); + } + } + + if (filtered.isEmpty()) { + return; + } + + // Insert directly after the parent item, preserving order from QuestRequirements + int insertIndex = list.indexOf(item); + for (com.toofifty.goaltracker.models.task.Task prereq : filtered) { + list.add(insertIndex + 1, prereq); + insertIndex++; + } + } } From 4d5d1c00c45ce464f721affd9d38d28895c5c2ba Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Fri, 22 Aug 2025 00:11:38 -0400 Subject: [PATCH 40/53] Pinning working, but right click is odd. --- .../com/toofifty/goaltracker/models/Goal.java | 3 + .../goaltracker/ui/GoalItemContent.java | 71 +++++++++++++++++-- .../goaltracker/ui/GoalTrackerPanel.java | 15 ++++ 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/models/Goal.java b/src/main/java/com/toofifty/goaltracker/models/Goal.java index 649301e..dcdf50d 100644 --- a/src/main/java/com/toofifty/goaltracker/models/Goal.java +++ b/src/main/java/com/toofifty/goaltracker/models/Goal.java @@ -25,6 +25,9 @@ public class Goal @Builder.Default private int displayOrder = -1; + @Builder.Default + private boolean pinned = false; + @SerializedName("items") @Builder.Default private ReorderableList tasks = new ReorderableList<>(); diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java index 36e6216..c2229ed 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java @@ -7,6 +7,9 @@ import com.toofifty.goaltracker.ui.Refreshable; import javax.swing.*; +import javax.swing.JMenuItem; +import javax.swing.event.PopupMenuListener; +import javax.swing.event.PopupMenuEvent; import java.awt.*; import com.toofifty.goaltracker.ui.components.ListItemPanel; @@ -29,10 +32,11 @@ public class GoalItemContent extends JPanel implements Refreshable private final Goal goal; - private static final int MIN_TITLE_FONT = 11; - private static final int MIN_PROGRESS_FONT = 10; private JPanel topRow; + //private static final int PIN_STRIPE_W = 3; // px + private static final Color PINNED_BG_COLOR = new Color(45, 45, 45); // darker gray background + GoalItemContent(GoalTrackerPlugin plugin, Goal goal) { super(new BorderLayout()); @@ -60,7 +64,7 @@ public class GoalItemContent extends JPanel implements Refreshable titleStack.setOpaque(false); titleStack.add(titleLabel, "label"); titleStack.add(titleEdit, "edit"); - topRow.add(titleStack, BorderLayout.WEST); + topRow.add(titleStack, BorderLayout.CENTER); // Swap to edit on label click titleLabel.addMouseListener(new MouseAdapter() { @@ -120,7 +124,51 @@ private void maybeShow(MouseEvent e) if (listItem != null && listItem.getComponentPopupMenu() != null) { java.awt.Point p = SwingUtilities.convertPoint(src, e.getPoint(), listItem); - listItem.getComponentPopupMenu().show(listItem, p.x, p.y); + JPopupMenu menu = listItem.getComponentPopupMenu(); + + // --- Pin / Unpin (temporary additions) --- + JSeparator sep = new JSeparator(); + ((JComponent) sep).putClientProperty("pinToggle", Boolean.TRUE); + menu.add(sep); + + JMenuItem pinToggle = new JMenuItem(goal.isPinned() ? "Unpin" : "Pin"); + ((JComponent) pinToggle).putClientProperty("pinToggle", Boolean.TRUE); + pinToggle.addActionListener(ev -> { + goal.setPinned(!goal.isPinned()); + try { + plugin.getGoalManager().save(); + } catch (Throwable t) { + plugin.getUiStatusManager().refresh(goal); + } + GoalItemContent.this.revalidate(); + GoalItemContent.this.repaint(); + }); + menu.add(pinToggle); + + PopupMenuListener cleanup = new PopupMenuListener() { + @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { } + @Override public void popupMenuCanceled(PopupMenuEvent e) { cleanup(menu, this); } + @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { cleanup(menu, this); } + private void cleanup(JPopupMenu m, PopupMenuListener self) { + // Remove only the components we added this time + java.util.List toRemove = new java.util.ArrayList<>(); + for (java.awt.Component c : m.getComponents()) { + if (c instanceof JComponent) { + Object flag = ((JComponent) c).getClientProperty("pinToggle"); + if (Boolean.TRUE.equals(flag)) { + toRemove.add(c); + } + } + } + for (java.awt.Component c : toRemove) { + m.remove(c); + } + m.removePopupMenuListener(self); + } + }; + menu.addPopupMenuListener(cleanup); + + menu.show(listItem, p.x, p.y); } } @@ -161,6 +209,7 @@ public void refresh() int done = goal.getComplete().size(); progressBar.setVisible(total > 0); progressBar.setProgress(done, total, color); + javax.swing.SwingUtilities.invokeLater(this::updateTitleLabel); } private static class SlimBar extends JComponent { private int done = 0; @@ -209,7 +258,6 @@ private void updateTitleLabel() return; } String ellipsis = "…"; - int ellW = fm.stringWidth(ellipsis); String text = full; // Binary-like trim from the end until it fits int lo = 0, hi = full.length(); @@ -247,4 +295,17 @@ private void exitEdit(boolean save) ((CardLayout) titleStack.getLayout()).show(titleStack, "label"); updateTitleLabel(); } + + @Override + protected void paintComponent(Graphics g) + { + if (goal != null && goal.isPinned()) + { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setColor(PINNED_BG_COLOR); + g2.fillRect(0, 0, getWidth(), getHeight()); + g2.dispose(); + } + super.paintComponent(g); + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index e6963d6..8527696 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -22,6 +22,8 @@ import javax.swing.filechooser.FileNameExtensionFilter; import java.awt.*; import java.util.function.Consumer; +import java.util.Collections; +import java.util.Comparator; public class GoalTrackerPanel extends PluginPanel implements Refreshable { @@ -134,6 +136,7 @@ private void refreshHomeListIfVisible() { if (goalPanel == null) { + sortGoalsForHome(); goalListPanel.tryBuildList(); goalListPanel.refresh(); revalidate(); @@ -167,6 +170,7 @@ public void home() } } removeAll(); + sortGoalsForHome(); goalListPanel.tryBuildList(); goalListPanel.refresh(); mainPanel.remove(goalListPanel); @@ -229,6 +233,17 @@ private void updateUndoRedoButtons() } } + private void sortGoalsForHome() + { + java.util.List goals = goalManager.getGoals(); + Collections.sort(goals, Comparator + .comparing(Goal::isPinned).reversed() + .thenComparing(g -> { + String d = g.getDescription(); + return d == null ? "" : d.toLowerCase(); + })); + } + private void doUndo() { var entry = undoStack.popForUndo(); From c5cbe1ab337e61559257b913273ffc9ba83105be Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Sun, 24 Aug 2025 10:28:45 -0400 Subject: [PATCH 41/53] Pinning working, also added pre-reqs button and revamp presets. --- changelog.md | 9 + .../presets/GoalPresetRepository.java | 671 ++++++++++++++---- .../toofifty/goaltracker/ui/GoalPanel.java | 88 ++- .../goaltracker/ui/GoalTrackerPanel.java | 3 +- .../goaltracker/ui/TaskItemContent.java | 59 ++ 5 files changed, 692 insertions(+), 138 deletions(-) diff --git a/changelog.md b/changelog.md index e81c79d..5f63782 100644 --- a/changelog.md +++ b/changelog.md @@ -32,6 +32,15 @@ - Automatic prerequisite expansion for presets leverages the same logic as the quest right-click **Add prereqs** option. - Ellipsized titles: Goal cards and Task rows now ellipsize long titles with `…` and show full text on hover via tooltip. - Click-to-edit: Goal titles and ManualTask descriptions can now be edited by clicking their label; label swaps to an inline text field and saves on Enter or blur. +- Added per-lane Ladlor presets (Melee, Ranged, Magic, Utility, Void Set, God Capes, Crystal & Bowfa, Jewelry & Boots, Slayer & Undead, Imbued Rings, God Wars Armor, Elite Void, Prayer Scrolls, Raids Uniques, Milestones & Capes), alongside the full combined Ladlor Ironman preset. +- Added Early, Mid, and Late Ironman presets as consolidated single-goal lists rather than split by category. +- Added a new **Add pre-reqs** button to the Goal view header, next to Undo/Redo. This button triggers the same logic as the quest right-click **Add prerequisites** option, but applies to all quest tasks in the goal at once. +- Converted all Early, Mid, and Late Ironman preset skill milestones from ManualTask to SkillLevelTask for accurate level tracking. +- Expanded Early Ironman preset with Birdhouse run unlocks (Dig Site, Bone Voyage, Hunter 5, Crafting 8, Construction 16) and Seaweed run unlock (Farming 23). +- Reworked Early, Mid, and Late Ironman presets with more appropriate content and progression pacing (e.g., Early emphasizes graceful set and mobility, Mid includes Iban’s staff, Ava’s, Barrows gloves, Late includes Blowpipe, Bowfa, Bandos, endgame quests). +- Commented out Ladlor presets from default loading in GoalPresetRepository to simplify active preset list. +- Fixed incorrect ItemID constants for Salve amulet (ei) in Slayer & Undead presets (SALVE_AMULET_EI). +- Removed stray selection marker and corrected field declaration in GoalPresetRepository. ### Changed - Pre-req button made more compact (~25% smaller). diff --git a/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java b/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java index 3f18c1f..b74658d 100644 --- a/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java +++ b/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java @@ -1,172 +1,581 @@ package com.toofifty.goaltracker.presets; import com.toofifty.goaltracker.models.Goal; -import com.toofifty.goaltracker.models.enums.Status; -import com.toofifty.goaltracker.models.task.ManualTask; +import com.toofifty.goaltracker.models.task.ItemTask; import com.toofifty.goaltracker.models.task.QuestTask; -import net.runelite.api.Quest; +import com.toofifty.goaltracker.models.task.SkillLevelTask; import com.toofifty.goaltracker.utils.ReorderableList; +import lombok.Getter; +import net.runelite.api.Quest; +import net.runelite.api.Skill; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Map; -import java.util.HashMap; -import java.util.Set; -import java.util.HashSet; - -/** - * Central repository for built-in goal presets. - * - * Keep this tiny and dependency‑free so it’s easy to expand. - */ + +import static net.runelite.api.ItemID.*; + public class GoalPresetRepository { - /** Simple container representing a named preset. */ - public static class Preset { + @Getter + public static final class Preset { private final String name; private final String description; private final List goals; - public Preset(String name, String description, List goals) { this.name = name; this.description = description; this.goals = goals; } - public String getName() { return name; } - public String getDescription() { return description; } - public List getGoals() { return goals; } - @Override public String toString() { return name; } } - // Minimal prerequisite graph for the quests we reference - private static final Map> PREREQS = new HashMap<>(); - static { - // Simple chains - PREREQS.put(Quest.FAIRYTALE_II__CURE_A_QUEEN, Arrays.asList(Quest.FAIRYTALE_I__GROWING_PAINS)); - PREREQS.put(Quest.MOURNINGS_END_PART_II, Arrays.asList(Quest.MOURNINGS_END_PART_I)); - PREREQS.put(Quest.MOURNINGS_END_PART_I, Arrays.asList(Quest.ROVING_ELVES)); - PREREQS.put(Quest.ROVING_ELVES, Arrays.asList(Quest.REGICIDE, Quest.WATERFALL_QUEST)); - PREREQS.put(Quest.REGICIDE, Arrays.asList(Quest.UNDERGROUND_PASS)); - // Desert Treasure I chain (trimmed to common quest prereqs) - PREREQS.put(Quest.DESERT_TREASURE_I, Arrays.asList( - Quest.PRIEST_IN_PERIL, - Quest.THE_DIG_SITE, - Quest.THE_TOURIST_TRAP, - Quest.TEMPLE_OF_IKOV, - Quest.TROLL_STRONGHOLD - )); - PREREQS.put(Quest.TROLL_STRONGHOLD, Arrays.asList(Quest.DEATH_PLATEAU)); - // Animal Magnetism common prereqs - PREREQS.put(Quest.ANIMAL_MAGNETISM, Arrays.asList( - Quest.THE_RESTLESS_GHOST, - Quest.ERNEST_THE_CHICKEN, - Quest.PRIEST_IN_PERIL - )); - // Lunar Diplomacy common prereqs - PREREQS.put(Quest.LUNAR_DIPLOMACY, Arrays.asList( - Quest.THE_FREMENNIK_TRIALS - )); - // Song of the Elves depends on the elf line; ensure ME2 present via above mappings - PREREQS.put(Quest.SONG_OF_THE_ELVES, Arrays.asList(Quest.MOURNINGS_END_PART_II)); - // Sins of the Father (trimmed main chain) - PREREQS.put(Quest.SINS_OF_THE_FATHER, Arrays.asList( - Quest.A_TASTE_OF_HOPE - )); - } - - private static void addQuestWithPrereqs(Quest quest, Set seen, ReorderableList out) - { - if (seen.contains(quest)) return; - List reqs = PREREQS.get(quest); - if (reqs != null) { - for (Quest q : reqs) { - addQuestWithPrereqs(q, seen, out); - } - } - // After ensuring prereqs are added, add the quest itself if missing - if (!seen.contains(quest)) { - out.add(QuestTask.builder().quest(quest).build()); - seen.add(quest); - } - } - - private static void expandWithPrereqs(ReorderableList tasks) - { - Set present = new HashSet<>(); - for (com.toofifty.goaltracker.models.task.Task t : tasks) { - if (t instanceof QuestTask) { - present.add(((QuestTask) t).getQuest()); - } - } - // Build a new ordered list with prereqs inserted before dependents - ReorderableList expanded = new ReorderableList<>(); - Set added = new HashSet<>(); - for (com.toofifty.goaltracker.models.task.Task t : new ArrayList<>(tasks)) { - if (t instanceof QuestTask) { - addQuestWithPrereqs(((QuestTask) t).getQuest(), added, expanded); - } else { - expanded.add(t); - } - } - tasks.clear(); - tasks.addAll(expanded); - } - - /** Return all available presets. */ public static List getAll() { List list = new ArrayList<>(); - list.add(buildQuestCapeCore()); + // list.add(buildLadlorIronman()); + // list.add(buildLadlorMelee()); + // list.add(buildLadlorRanged()); + // list.add(buildLadlorMagic()); + // list.add(buildLadlorUtility()); + // list.add(buildLadlorVoidSet()); + // list.add(buildLadlorGodCapes()); + // list.add(buildLadlorCrystalBowfa()); + // list.add(buildLadlorJewelryBoots()); + // list.add(buildLadlorSlayerUndead()); + // list.add(buildLadlorImbuedRings()); + // list.add(buildLadlorGwdArmor()); + // list.add(buildLadlorEliteVoid()); + // list.add(buildLadlorPrayerScrolls()); + // list.add(buildLadlorRaidsUniques()); + // list.add(buildLadlorMilestones()); list.add(buildEarlyIronman()); + list.add(buildMidIronman()); + list.add(buildLateIronman()); return list; } - private static Preset buildQuestCapeCore() { - ReorderableList tasks = ReorderableList.from( - QuestTask.builder().quest(Quest.ANIMAL_MAGNETISM).build(), - QuestTask.builder().quest(Quest.FAIRYTALE_I__GROWING_PAINS).build(), - QuestTask.builder().quest(Quest.FAIRYTALE_II__CURE_A_QUEEN).build(), - QuestTask.builder().quest(Quest.DESERT_TREASURE_I).build(), - QuestTask.builder().quest(Quest.DRAGON_SLAYER_II).build(), - QuestTask.builder().quest(Quest.LUNAR_DIPLOMACY).build(), - QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), - QuestTask.builder().quest(Quest.UNDERGROUND_PASS).build(), - QuestTask.builder().quest(Quest.REGICIDE).build(), - QuestTask.builder().quest(Quest.ROVING_ELVES).build(), - QuestTask.builder().quest(Quest.MOURNINGS_END_PART_I).build(), - QuestTask.builder().quest(Quest.MOURNINGS_END_PART_II).build(), - QuestTask.builder().quest(Quest.SONG_OF_THE_ELVES).build(), - QuestTask.builder().quest(Quest.SINS_OF_THE_FATHER).build() - ); - expandWithPrereqs(tasks); - Goal goal = Goal.builder() - .description("Quest Cape — Core unlocks") - .tasks(tasks) + /* + private static Preset buildLadlorIronman() { + // Melee lane + Goal melee = Goal.builder() + .description("Ladlor — Melee Progression") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(DRAGON_SCIMITAR).itemName("Dragon scimitar").quantity(1).build(), + ItemTask.builder().itemId(DRAGON_DEFENDER).itemName("Dragon defender").quantity(1).build(), + ItemTask.builder().itemId(FIGHTER_TORSO).itemName("Fighter torso").quantity(1).build(), + ItemTask.builder().itemId(DRAGON_BOOTS).itemName("Dragon boots").quantity(1).build(), + ItemTask.builder().itemId(ABYSSAL_WHIP).itemName("Abyssal whip").quantity(1).build(), + ItemTask.builder().itemId(BERSERKER_RING).itemName("Berserker ring").quantity(1).build(), + ItemTask.builder().itemId(FIRE_CAPE).itemName("Fire cape").quantity(1).build(), + ItemTask.builder().itemId(FEROCIOUS_GLOVES).itemName("Ferocious gloves").quantity(1).build(), + ItemTask.builder().itemId(ABYSSAL_TENTACLE).itemName("Abyssal tentacle").quantity(1).build() + )) + .build(); + + // Ranged lane + Goal ranged = Goal.builder() + .description("Ladlor — Ranged Progression") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(MAGIC_SHORTBOW).itemName("Magic shortbow").quantity(1).build(), + ItemTask.builder().itemId(RUNE_CROSSBOW).itemName("Rune crossbow").quantity(1).build(), + ItemTask.builder().itemId(BLACK_DHIDE_BODY).itemName("Black d'hide body").quantity(1).build(), + ItemTask.builder().itemId(AVAS_ACCUMULATOR).itemName("Ava's accumulator").quantity(1).build(), + ItemTask.builder().itemId(KARILS_LEATHERTOP).itemName("Karil's leathertop").quantity(1).build(), + ItemTask.builder().itemId(KARILS_LEATHERSKIRT).itemName("Karil's leatherskirt").quantity(1).build(), + ItemTask.builder().itemId(TOXIC_BLOWPIPE).itemName("Toxic blowpipe").quantity(1).build(), + ItemTask.builder().itemId(ARMADYL_CROSSBOW).itemName("Armadyl crossbow").quantity(1).build(), + ItemTask.builder().itemId(AVAS_ASSEMBLER).itemName("Ava's assembler").quantity(1).build() + )) + .build(); + + // Magic lane + Goal magic = Goal.builder() + .description("Ladlor — Magic Progression") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(MYSTIC_ROBE_TOP).itemName("Mystic robe top").quantity(1).build(), + ItemTask.builder().itemId(MYSTIC_ROBE_BOTTOM).itemName("Mystic robe bottom").quantity(1).build(), + ItemTask.builder().itemId(OCCULT_NECKLACE).itemName("Occult necklace").quantity(1).build(), + ItemTask.builder().itemId(MAGES_BOOK).itemName("Mage's book").quantity(1).build(), + ItemTask.builder().itemId(TRIDENT_OF_THE_SEAS).itemName("Trident of the seas").quantity(1).build(), + ItemTask.builder().itemId(UNCHARGED_TOXIC_TRIDENT).itemName("Trident of the swamp").quantity(1).build(), + ItemTask.builder().itemId(ANCESTRAL_ROBE_TOP).itemName("Ancestral robe top").quantity(1).build(), + ItemTask.builder().itemId(ANCESTRAL_ROBE_BOTTOM).itemName("Ancestral robe bottom").quantity(1).build(), + ItemTask.builder().itemId(ANCESTRAL_HAT).itemName("Ancestral hat").quantity(1).build() + )) + .build(); + + // Utility / Unlocks lane + Goal utility = Goal.builder() + .description("Ladlor — Utility & Unlocks") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(GRACEFUL_HOOD).itemName("Graceful hood").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_TOP).itemName("Graceful top").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_LEGS).itemName("Graceful legs").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_GLOVES).itemName("Graceful gloves").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_BOOTS).itemName("Graceful boots").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_CAPE).itemName("Graceful cape").quantity(1).build(), + ItemTask.builder().itemId(BARROWS_GLOVES).itemName("Barrows gloves").quantity(1).build() + )) + .build(); + + // Void set lane + Goal voidSet = Goal.builder() + .description("Ladlor — Void Set") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(VOID_KNIGHT_TOP).itemName("Void knight top").quantity(1).build(), + ItemTask.builder().itemId(VOID_KNIGHT_ROBE).itemName("Void knight robe").quantity(1).build(), + ItemTask.builder().itemId(VOID_KNIGHT_GLOVES).itemName("Void knight gloves").quantity(1).build(), + ItemTask.builder().itemId(VOID_MAGE_HELM).itemName("Void mage helm").quantity(1).build(), + ItemTask.builder().itemId(VOID_RANGER_HELM).itemName("Void ranger helm").quantity(1).build(), + ItemTask.builder().itemId(VOID_MELEE_HELM).itemName("Void melee helm").quantity(1).build() + )) + .build(); + + // God capes imbued lane + Goal godCapes = Goal.builder() + .description("Ladlor — Imbued God Capes") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(IMBUED_SARADOMIN_CAPE).itemName("Imbued Saradomin cape").quantity(1).build(), + ItemTask.builder().itemId(IMBUED_GUTHIX_CAPE).itemName("Imbued Guthix cape").quantity(1).build(), + ItemTask.builder().itemId(IMBUED_ZAMORAK_CAPE).itemName("Imbued Zamorak cape").quantity(1).build() + )) + .build(); + + // Crystal / Bofa lane + Goal crystal = Goal.builder() + .description("Ladlor — Crystal & Bowfa") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(CRYSTAL_HELM).itemName("Crystal helm").quantity(1).build(), + ItemTask.builder().itemId(CRYSTAL_BODY).itemName("Crystal body").quantity(1).build(), + ItemTask.builder().itemId(CRYSTAL_LEGS).itemName("Crystal legs").quantity(1).build(), + ItemTask.builder().itemId(BOW_OF_FAERDHINEN).itemName("Bow of Faerdhinen").quantity(1).build() + )) .build(); + + // Jewelry / Boots upgrades lane + Goal jewelry = Goal.builder() + .description("Ladlor — Jewelry & Boots Upgrades") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(AMULET_OF_TORTURE).itemName("Amulet of torture").quantity(1).build(), + ItemTask.builder().itemId(NECKLACE_OF_ANGUISH).itemName("Necklace of anguish").quantity(1).build(), + ItemTask.builder().itemId(PEGASIAN_BOOTS).itemName("Pegasian boots").quantity(1).build(), + ItemTask.builder().itemId(RANGER_BOOTS).itemName("Ranger boots").quantity(1).build(), + ItemTask.builder().itemId(PRIMORDIAL_BOOTS).itemName("Primordial boots").quantity(1).build(), + ItemTask.builder().itemId(ETERNAL_BOOTS).itemName("Eternal boots").quantity(1).build() + )) + .build(); + + // Slayer & Undead lane + Goal slayerUndead = Goal.builder() + .description("Ladlor — Slayer & Undead") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(BLACK_MASK).itemName("Black mask").quantity(1).build(), + ItemTask.builder().itemId(SLAYER_HELMET).itemName("Slayer helmet").quantity(1).build(), + ItemTask.builder().itemId(SLAYER_HELMET_I).itemName("Slayer helmet (i)").quantity(1).build(), + ItemTask.builder().itemId(SALVE_AMULET_E).itemName("Salve amulet (e)").quantity(1).build(), + ItemTask.builder().itemId(SALVE_AMULET_EI).itemName("Salve amulet (ei)").quantity(1).build(), + ItemTask.builder().itemId(IMBUED_HEART).itemName("Imbued heart").quantity(1).build(), + ItemTask.builder().itemId(RUNE_POUCH).itemName("Rune pouch").quantity(1).build(), + ItemTask.builder().itemId(RING_OF_SUFFERING).itemName("Ring of suffering").quantity(1).build(), + ItemTask.builder().itemId(RING_OF_SUFFERING_I).itemName("Ring of suffering (i)").quantity(1).build() + )) + .build(); + + // Imbued Rings lane + Goal imbuedRings = Goal.builder() + .description("Ladlor — Imbued Rings") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(BERSERKER_RING_I).itemName("Berserker ring (i)").quantity(1).build(), + ItemTask.builder().itemId(ARCHERS_RING_I).itemName("Archer's ring (i)").quantity(1).build(), + ItemTask.builder().itemId(SEERS_RING_I).itemName("Seers ring (i)").quantity(1).build(), + ItemTask.builder().itemId(WARRIOR_RING_I).itemName("Warrior ring (i)").quantity(1).build() + )) + .build(); + + // God Wars Dungeon Armor lane + Goal gwdArmor = Goal.builder() + .description("Ladlor — God Wars Armor") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(BANDOS_CHESTPLATE).itemName("Bandos chestplate").quantity(1).build(), + ItemTask.builder().itemId(BANDOS_TASSETS).itemName("Bandos tassets").quantity(1).build(), + ItemTask.builder().itemId(ARMADYL_CHESTPLATE).itemName("Armadyl chestplate").quantity(1).build(), + ItemTask.builder().itemId(ARMADYL_CHAINSKIRT).itemName("Armadyl chainskirt").quantity(1).build() + )) + .build(); + + // Elite Void upgrades lane + Goal eliteVoid = Goal.builder() + .description("Ladlor — Elite Void") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(ELITE_VOID_TOP).itemName("Elite void top").quantity(1).build(), + ItemTask.builder().itemId(ELITE_VOID_ROBE).itemName("Elite void robe").quantity(1).build() + )) + .build(); + + // Prayer scrolls lane (Rigour & Augury) + Goal prayerScrolls = Goal.builder() + .description("Ladlor — Prayer Scrolls") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(DEXTEROUS_PRAYER_SCROLL).itemName("Dexterous prayer scroll (Rigour)").quantity(1).build(), + ItemTask.builder().itemId(ARCANE_PRAYER_SCROLL).itemName("Arcane prayer scroll (Augury)").quantity(1).build() + )) + .build(); + + // Raids uniques lane + Goal raidsUniques = Goal.builder() + .description("Ladlor — Raids Uniques") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(DRAGON_HUNTER_CROSSBOW).itemName("Dragon hunter crossbow").quantity(1).build(), + ItemTask.builder().itemId(TWISTED_BOW).itemName("Twisted bow").quantity(1).build(), + ItemTask.builder().itemId(SCYTHE_OF_VITUR).itemName("Scythe of Vitur").quantity(1).build(), + ItemTask.builder().itemId(GHRAZI_RAPIER).itemName("Ghrazi rapier").quantity(1).build(), + ItemTask.builder().itemId(OSMUMTENS_FANG).itemName("Osmumten's fang").quantity(1).build(), + ItemTask.builder().itemId(MASORI_BODY).itemName("Masori body").quantity(1).build(), + ItemTask.builder().itemId(MASORI_CHAPS).itemName("Masori chaps").quantity(1).build(), + ItemTask.builder().itemId(TUMEKENS_SHADOW).itemName("Tumeken's shadow").quantity(1).build() + )) + .build(); + + // Boss / cape milestones (quests mixed in) + Goal milestones = Goal.builder() + .description("Ladlor — Milestones & Capes") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(FIRE_CAPE).itemName("Fire cape").quantity(1).build(), + QuestTask.builder().quest(Quest.SONG_OF_THE_ELVES).build(), + QuestTask.builder().quest(Quest.SINS_OF_THE_FATHER).build() + )) + .build(); + + List goals = Arrays.asList( + melee, ranged, magic, utility, + voidSet, eliteVoid, godCapes, crystal, jewelry, + slayerUndead, imbuedRings, gwdArmor, prayerScrolls, raidsUniques, + milestones + ); return new Preset( - "Quest Cape (Core)", - "Essential quests and unlocks toward Quest Cape.", - Arrays.asList(goal) + "Ladlor Ironman Progression", + "A multi-lane item-based progression inspired by Ladlor's Ironman chart.", + goals ); } + private static Preset buildLadlorMelee() { + Goal melee = Goal.builder() + .description("Ladlor — Melee Progression") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(DRAGON_SCIMITAR).itemName("Dragon scimitar").quantity(1).build(), + ItemTask.builder().itemId(DRAGON_DEFENDER).itemName("Dragon defender").quantity(1).build(), + ItemTask.builder().itemId(FIGHTER_TORSO).itemName("Fighter torso").quantity(1).build(), + ItemTask.builder().itemId(DRAGON_BOOTS).itemName("Dragon boots").quantity(1).build(), + ItemTask.builder().itemId(ABYSSAL_WHIP).itemName("Abyssal whip").quantity(1).build(), + ItemTask.builder().itemId(BERSERKER_RING).itemName("Berserker ring").quantity(1).build(), + ItemTask.builder().itemId(FIRE_CAPE).itemName("Fire cape").quantity(1).build(), + ItemTask.builder().itemId(FEROCIOUS_GLOVES).itemName("Ferocious gloves").quantity(1).build(), + ItemTask.builder().itemId(ABYSSAL_TENTACLE).itemName("Abyssal tentacle").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Melee", "Melee progression lane from Ladlor chart.", Arrays.asList(melee)); + } + + private static Preset buildLadlorRanged() { + Goal ranged = Goal.builder() + .description("Ladlor — Ranged Progression") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(MAGIC_SHORTBOW).itemName("Magic shortbow").quantity(1).build(), + ItemTask.builder().itemId(RUNE_CROSSBOW).itemName("Rune crossbow").quantity(1).build(), + ItemTask.builder().itemId(BLACK_DHIDE_BODY).itemName("Black d'hide body").quantity(1).build(), + ItemTask.builder().itemId(AVAS_ACCUMULATOR).itemName("Ava's accumulator").quantity(1).build(), + ItemTask.builder().itemId(KARILS_LEATHERTOP).itemName("Karil's leathertop").quantity(1).build(), + ItemTask.builder().itemId(KARILS_LEATHERSKIRT).itemName("Karil's leatherskirt").quantity(1).build(), + ItemTask.builder().itemId(TOXIC_BLOWPIPE).itemName("Toxic blowpipe").quantity(1).build(), + ItemTask.builder().itemId(ARMADYL_CROSSBOW).itemName("Armadyl crossbow").quantity(1).build(), + ItemTask.builder().itemId(AVAS_ASSEMBLER).itemName("Ava's assembler").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Ranged", "Ranged progression lane from Ladlor chart.", Arrays.asList(ranged)); + } + + private static Preset buildLadlorMagic() { + Goal magic = Goal.builder() + .description("Ladlor — Magic Progression") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(MYSTIC_ROBE_TOP).itemName("Mystic robe top").quantity(1).build(), + ItemTask.builder().itemId(MYSTIC_ROBE_BOTTOM).itemName("Mystic robe bottom").quantity(1).build(), + ItemTask.builder().itemId(OCCULT_NECKLACE).itemName("Occult necklace").quantity(1).build(), + ItemTask.builder().itemId(MAGES_BOOK).itemName("Mage's book").quantity(1).build(), + ItemTask.builder().itemId(TRIDENT_OF_THE_SEAS).itemName("Trident of the seas").quantity(1).build(), + ItemTask.builder().itemId(UNCHARGED_TOXIC_TRIDENT).itemName("Trident of the swamp").quantity(1).build(), + ItemTask.builder().itemId(ANCESTRAL_ROBE_TOP).itemName("Ancestral robe top").quantity(1).build(), + ItemTask.builder().itemId(ANCESTRAL_ROBE_BOTTOM).itemName("Ancestral robe bottom").quantity(1).build(), + ItemTask.builder().itemId(ANCESTRAL_HAT).itemName("Ancestral hat").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Magic", "Magic progression lane from Ladlor chart.", Arrays.asList(magic)); + } + + private static Preset buildLadlorUtility() { + Goal utility = Goal.builder() + .description("Ladlor — Utility & Unlocks") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(GRACEFUL_HOOD).itemName("Graceful hood").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_TOP).itemName("Graceful top").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_LEGS).itemName("Graceful legs").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_GLOVES).itemName("Graceful gloves").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_BOOTS).itemName("Graceful boots").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_CAPE).itemName("Graceful cape").quantity(1).build(), + ItemTask.builder().itemId(BARROWS_GLOVES).itemName("Barrows gloves").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Utility & Unlocks", "Utility, movement and QoL lane from Ladlor chart.", Arrays.asList(utility)); + } + + private static Preset buildLadlorVoidSet() { + Goal voidSet = Goal.builder() + .description("Ladlor — Void Set") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(VOID_KNIGHT_TOP).itemName("Void knight top").quantity(1).build(), + ItemTask.builder().itemId(VOID_KNIGHT_ROBE).itemName("Void knight robe").quantity(1).build(), + ItemTask.builder().itemId(VOID_KNIGHT_GLOVES).itemName("Void knight gloves").quantity(1).build(), + ItemTask.builder().itemId(VOID_MAGE_HELM).itemName("Void mage helm").quantity(1).build(), + ItemTask.builder().itemId(VOID_RANGER_HELM).itemName("Void ranger helm").quantity(1).build(), + ItemTask.builder().itemId(VOID_MELEE_HELM).itemName("Void melee helm").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Void Set", "Standard Void set lane from Ladlor chart.", Arrays.asList(voidSet)); + } + + private static Preset buildLadlorGodCapes() { + Goal godCapes = Goal.builder() + .description("Ladlor — Imbued God Capes") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(IMBUED_SARADOMIN_CAPE).itemName("Imbued Saradomin cape").quantity(1).build(), + ItemTask.builder().itemId(IMBUED_GUTHIX_CAPE).itemName("Imbued Guthix cape").quantity(1).build(), + ItemTask.builder().itemId(IMBUED_ZAMORAK_CAPE).itemName("Imbued Zamorak cape").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Imbued God Capes", "Mage Arena II imbued god capes.", Arrays.asList(godCapes)); + } + + private static Preset buildLadlorCrystalBowfa() { + Goal crystal = Goal.builder() + .description("Ladlor — Crystal & Bowfa") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(CRYSTAL_HELM).itemName("Crystal helm").quantity(1).build(), + ItemTask.builder().itemId(CRYSTAL_BODY).itemName("Crystal body").quantity(1).build(), + ItemTask.builder().itemId(CRYSTAL_LEGS).itemName("Crystal legs").quantity(1).build(), + ItemTask.builder().itemId(BOW_OF_FAERDHINEN).itemName("Bow of Faerdhinen").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Crystal & Bowfa", "Crystal armour and Bow of Faerdhinen lane.", Arrays.asList(crystal)); + } + + private static Preset buildLadlorJewelryBoots() { + Goal jewelry = Goal.builder() + .description("Ladlor — Jewelry & Boots Upgrades") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(AMULET_OF_TORTURE).itemName("Amulet of torture").quantity(1).build(), + ItemTask.builder().itemId(NECKLACE_OF_ANGUISH).itemName("Necklace of anguish").quantity(1).build(), + ItemTask.builder().itemId(PEGASIAN_BOOTS).itemName("Pegasian boots").quantity(1).build(), + ItemTask.builder().itemId(RANGER_BOOTS).itemName("Ranger boots").quantity(1).build(), + ItemTask.builder().itemId(PRIMORDIAL_BOOTS).itemName("Primordial boots").quantity(1).build(), + ItemTask.builder().itemId(ETERNAL_BOOTS).itemName("Eternal boots").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Jewelry & Boots", "Jewelry and footwear upgrades lane.", Arrays.asList(jewelry)); + } + + private static Preset buildLadlorSlayerUndead() { + Goal slayerUndead = Goal.builder() + .description("Ladlor — Slayer & Undead") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(BLACK_MASK).itemName("Black mask").quantity(1).build(), + ItemTask.builder().itemId(SLAYER_HELMET).itemName("Slayer helmet").quantity(1).build(), + ItemTask.builder().itemId(SLAYER_HELMET_I).itemName("Slayer helmet (i)").quantity(1).build(), + ItemTask.builder().itemId(SALVE_AMULET_E).itemName("Salve amulet (e)").quantity(1).build(), + ItemTask.builder().itemId(SALVE_AMULET_EI).itemName("Salve amulet (ei)").quantity(1).build(), + ItemTask.builder().itemId(IMBUED_HEART).itemName("Imbued heart").quantity(1).build(), + ItemTask.builder().itemId(RUNE_POUCH).itemName("Rune pouch").quantity(1).build(), + ItemTask.builder().itemId(RING_OF_SUFFERING).itemName("Ring of suffering").quantity(1).build(), + ItemTask.builder().itemId(RING_OF_SUFFERING_I).itemName("Ring of suffering (i)").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Slayer & Undead", "Slayer and undead-focused unlocks.", Arrays.asList(slayerUndead)); + } + + private static Preset buildLadlorImbuedRings() { + Goal imbuedRings = Goal.builder() + .description("Ladlor — Imbued Rings") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(BERSERKER_RING_I).itemName("Berserker ring (i)").quantity(1).build(), + ItemTask.builder().itemId(ARCHERS_RING_I).itemName("Archer's ring (i)").quantity(1).build(), + ItemTask.builder().itemId(SEERS_RING_I).itemName("Seers ring (i)").quantity(1).build(), + ItemTask.builder().itemId(WARRIOR_RING_I).itemName("Warrior ring (i)").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Imbued Rings", "Imbued Dagannoth Kings rings.", Arrays.asList(imbuedRings)); + } + + private static Preset buildLadlorGwdArmor() { + Goal gwdArmor = Goal.builder() + .description("Ladlor — God Wars Armor") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(BANDOS_CHESTPLATE).itemName("Bandos chestplate").quantity(1).build(), + ItemTask.builder().itemId(BANDOS_TASSETS).itemName("Bandos tassets").quantity(1).build(), + ItemTask.builder().itemId(ARMADYL_CHESTPLATE).itemName("Armadyl chestplate").quantity(1).build(), + ItemTask.builder().itemId(ARMADYL_CHAINSKIRT).itemName("Armadyl chainskirt").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — God Wars Armor", "Bandos and Armadyl armour lane.", Arrays.asList(gwdArmor)); + } + + private static Preset buildLadlorEliteVoid() { + Goal eliteVoid = Goal.builder() + .description("Ladlor — Elite Void") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(ELITE_VOID_TOP).itemName("Elite void top").quantity(1).build(), + ItemTask.builder().itemId(ELITE_VOID_ROBE).itemName("Elite void robe").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Elite Void", "Elite void upgrade lane.", Arrays.asList(eliteVoid)); + } + + private static Preset buildLadlorPrayerScrolls() { + Goal prayerScrolls = Goal.builder() + .description("Ladlor — Prayer Scrolls") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(DEXTEROUS_PRAYER_SCROLL).itemName("Dexterous prayer scroll (Rigour)").quantity(1).build(), + ItemTask.builder().itemId(ARCANE_PRAYER_SCROLL).itemName("Arcane prayer scroll (Augury)").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Prayer Scrolls", "Rigour and Augury unlock scrolls.", Arrays.asList(prayerScrolls)); + } + + private static Preset buildLadlorRaidsUniques() { + Goal raidsUniques = Goal.builder() + .description("Ladlor — Raids Uniques") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(DRAGON_HUNTER_CROSSBOW).itemName("Dragon hunter crossbow").quantity(1).build(), + ItemTask.builder().itemId(TWISTED_BOW).itemName("Twisted bow").quantity(1).build(), + ItemTask.builder().itemId(SCYTHE_OF_VITUR).itemName("Scythe of Vitur").quantity(1).build(), + ItemTask.builder().itemId(GHRAZI_RAPIER).itemName("Ghrazi rapier").quantity(1).build(), + ItemTask.builder().itemId(OSMUMTENS_FANG).itemName("Osmumten's fang").quantity(1).build(), + ItemTask.builder().itemId(MASORI_BODY).itemName("Masori body").quantity(1).build(), + ItemTask.builder().itemId(MASORI_CHAPS).itemName("Masori chaps").quantity(1).build(), + ItemTask.builder().itemId(TUMEKENS_SHADOW).itemName("Tumeken's shadow").quantity(1).build() + )) + .build(); + return new Preset("Ladlor — Raids Uniques", "Raids 1/2/3 uniques lane.", Arrays.asList(raidsUniques)); + } + + private static Preset buildLadlorMilestones() { + Goal milestones = Goal.builder() + .description("Ladlor — Milestones & Capes") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(FIRE_CAPE).itemName("Fire cape").quantity(1).build(), + QuestTask.builder().quest(Quest.SONG_OF_THE_ELVES).build(), + QuestTask.builder().quest(Quest.SINS_OF_THE_FATHER).build() + )) + .build(); + return new Preset("Ladlor — Milestones & Capes", "Key capes and late-game quest milestones.", Arrays.asList(milestones)); + } + */ private static Preset buildEarlyIronman() { Goal early = Goal.builder() - .description("Early Game Ironman — Foundation") + .description("Early Ironman Progression") .tasks(ReorderableList.from( - ManualTask.builder().description("Unlock Ardougne cloak 1 (monastery teleport)").status(Status.NOT_STARTED).build(), - ManualTask.builder().description("Do Wintertodt to ~70 Firemaking").status(Status.NOT_STARTED).build(), - ManualTask.builder().description("Get graceful set").status(Status.NOT_STARTED).build(), - ManualTask.builder().description("Obtain Dramen staff (Fairy rings)").status(Status.NOT_STARTED).build(), - ManualTask.builder().description("Start Barrows for early gear and runes").status(Status.NOT_STARTED).build() + // Core Early Skills (unlock movement & teleports) + SkillLevelTask.builder().skill(Skill.AGILITY).level(30).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(37).build(), // Falador/Camelot teles + SkillLevelTask.builder().skill(Skill.THIEVING).level(20).build(), + // Early set-up gear + ItemTask.builder().itemId(GRACEFUL_HOOD).itemName("Graceful hood").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_TOP).itemName("Graceful top").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_LEGS).itemName("Graceful legs").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_GLOVES).itemName("Graceful gloves").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_BOOTS).itemName("Graceful boots").quantity(1).build(), + ItemTask.builder().itemId(GRACEFUL_CAPE).itemName("Graceful cape").quantity(1).build(), + // Foundational quests for stats/unlocks + QuestTask.builder().quest(Quest.DRUIDIC_RITUAL).build(), + QuestTask.builder().quest(Quest.WATERFALL_QUEST).build(), + QuestTask.builder().quest(Quest.PRIEST_IN_PERIL).build(), + QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build(), + QuestTask.builder().quest(Quest.THE_GRAND_TREE).build(), + // Birdhouse runs (Fossil Island access) + QuestTask.builder().quest(Quest.THE_DIG_SITE).build(), + QuestTask.builder().quest(Quest.BONE_VOYAGE).build(), + SkillLevelTask.builder().skill(Skill.HUNTER).level(5).build(), + SkillLevelTask.builder().skill(Skill.CRAFTING).level(8).build(), // clockwork + SkillLevelTask.builder().skill(Skill.CONSTRUCTION).level(16).build(), // clockmaker's bench + + // Seaweed runs (Giant seaweed patches on Fossil Island) + SkillLevelTask.builder().skill(Skill.FARMING).level(23).build() )) .build(); - return new Preset( - "Early Game Ironman", - "Starter progression path for fresh iron accounts.", - Arrays.asList(early) - ); + return new Preset("Early Ironman Progression", "Stats, gear, and quest goals for early game Ironman.", Arrays.asList(early)); + } + + private static Preset buildMidIronman() { + Goal mid = Goal.builder() + .description("Mid Ironman Progression") + .tasks(ReorderableList.from( + // Midgame Skill Targets + SkillLevelTask.builder().skill(Skill.ATTACK).level(60).build(), + SkillLevelTask.builder().skill(Skill.STRENGTH).level(60).build(), + SkillLevelTask.builder().skill(Skill.DEFENCE).level(60).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(60).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(55).build(), // High Alch + SkillLevelTask.builder().skill(Skill.PRAYER).level(43).build(), // Protect prayers + SkillLevelTask.builder().skill(Skill.AGILITY).level(60).build(), + // Midgame Gear & Upgrades + ItemTask.builder().itemId(DRAGON_SCIMITAR).itemName("Dragon scimitar").quantity(1).build(), + ItemTask.builder().itemId(DRAGON_DEFENDER).itemName("Dragon defender").quantity(1).build(), + ItemTask.builder().itemId(FIGHTER_TORSO).itemName("Fighter torso").quantity(1).build(), + ItemTask.builder().itemId(RUNE_CROSSBOW).itemName("Rune crossbow").quantity(1).build(), + ItemTask.builder().itemId(IBANS_STAFF_U).itemName("Iban's staff (u)").quantity(1).build(), + ItemTask.builder().itemId(BARROWS_GLOVES).itemName("Barrows gloves").quantity(1).build(), + ItemTask.builder().itemId(AVAS_ACCUMULATOR).itemName("Ava's accumulator").quantity(1).build(), + // Midgame Quests / Unlocks + QuestTask.builder().quest(Quest.RECIPE_FOR_DISASTER).build(), + QuestTask.builder().quest(Quest.FAIRYTALE_II__CURE_A_QUEEN).build(), // Fairy rings access (partial) + QuestTask.builder().quest(Quest.LUNAR_DIPLOMACY).build(), + QuestTask.builder().quest(Quest.ANIMAL_MAGNETISM).build(), + QuestTask.builder().quest(Quest.UNDERGROUND_PASS).build() + )) + .build(); + return new Preset("Mid Ironman Progression", "Stats, gear, and quest goals for mid game Ironman.", Arrays.asList(mid)); + } + + private static Preset buildLateIronman() { + Goal late = Goal.builder() + .description("Late Ironman Progression") + .tasks(ReorderableList.from( + // Late Skill Targets + SkillLevelTask.builder().skill(Skill.ATTACK).level(85).build(), + SkillLevelTask.builder().skill(Skill.STRENGTH).level(85).build(), + SkillLevelTask.builder().skill(Skill.DEFENCE).level(85).build(), + SkillLevelTask.builder().skill(Skill.MAGIC).level(94).build(), // Vengeance/Ice Barrage + SkillLevelTask.builder().skill(Skill.PRAYER).level(77).build(), // Rigour/Augury + SkillLevelTask.builder().skill(Skill.RANGED).level(85).build(), + // Late Gear Goals + ItemTask.builder().itemId(ABYSSAL_TENTACLE).itemName("Abyssal tentacle").quantity(1).build(), + ItemTask.builder().itemId(TOXIC_BLOWPIPE).itemName("Toxic blowpipe").quantity(1).build(), + ItemTask.builder().itemId(ARMADYL_CROSSBOW).itemName("Armadyl crossbow").quantity(1).build(), + ItemTask.builder().itemId(BOW_OF_FAERDHINEN).itemName("Bow of Faerdhinen").quantity(1).build(), + ItemTask.builder().itemId(BANDOS_CHESTPLATE).itemName("Bandos chestplate").quantity(1).build(), + ItemTask.builder().itemId(BANDOS_TASSETS).itemName("Bandos tassets").quantity(1).build(), + ItemTask.builder().itemId(TRIDENT_OF_THE_SEAS).itemName("Trident of the seas").quantity(1).build(), + ItemTask.builder().itemId(RING_OF_SUFFERING).itemName("Ring of suffering").quantity(1).build(), + ItemTask.builder().itemId(RING_OF_SUFFERING_I).itemName("Ring of suffering (i)").quantity(1).build(), + ItemTask.builder().itemId(SALVE_AMULETEI).itemName("Salve amulet (ei)").quantity(1).build(), + ItemTask.builder().itemId(INFERNAL_CAPE).itemName("Infernal cape").quantity(1).build(), + // Late Quests / Diaries + QuestTask.builder().quest(Quest.SONG_OF_THE_ELVES).build(), + QuestTask.builder().quest(Quest.SINS_OF_THE_FATHER).build(), + QuestTask.builder().quest(Quest.MONKEY_MADNESS_II).build(), + QuestTask.builder().quest(Quest.DESERT_TREASURE_I).build() + )) + .build(); + return new Preset("Late Ironman Progression", "Stats, gear, and quest goals for late game Ironman.", Arrays.asList(late)); } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java index 7bb54a1..f31ed7d 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java @@ -39,6 +39,7 @@ public class GoalPanel extends JPanel implements Refreshable private final ActionHistory actionHistory = new ActionHistory(); private ActionBarButton undoButton; private ActionBarButton redoButton; + private ActionBarButton prereqsButton; GoalPanel(GoalTrackerPlugin plugin, Goal goal, Runnable closeListener) { @@ -52,18 +53,33 @@ public class GoalPanel extends JPanel implements Refreshable headerPanel.setBorder(new EmptyBorder(0, 0, 8, 0)); add(headerPanel, BorderLayout.NORTH); - // Goal List Action Bar: Back (left), Undo/Redo (right) + // Back row above the action bar + JPanel backRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + backRow.setBackground(ColorScheme.DARK_GRAY_COLOR); + ActionBarButton backButton = new ActionBarButton("Back", closeListener::run); + backRow.add(backButton); + + // Action bar with Add pre-reqs on the left of Undo/Redo ActionBar actionBar = new ActionBar(); + prereqsButton = new ActionBarButton("Add pre-reqs", this::addPrereqs); + prereqsButton.setToolTipText("Add prerequisite quests/tasks for this goal"); + prereqsButton.setEnabled(true); - ActionBarButton backButton = new ActionBarButton("Back", closeListener::run); undoButton = new ActionBarButton("Undo", this::doUndo); redoButton = new ActionBarButton("Redo", this::doRedo); - actionBar.left().add(backButton); - actionBar.right().add(undoButton); - actionBar.right().add(redoButton); + actionBar.left().add(prereqsButton); + actionBar.left().add(undoButton); + actionBar.left().add(redoButton); - headerPanel.add(actionBar, BorderLayout.NORTH); + // Stack back row above the action bar in the header's NORTH + JPanel headerTop = new JPanel(); + headerTop.setBackground(ColorScheme.DARK_GRAY_COLOR); + headerTop.setLayout(new BoxLayout(headerTop, BoxLayout.Y_AXIS)); + headerTop.add(backRow); + headerTop.add(actionBar); + + headerPanel.add(headerTop, BorderLayout.NORTH); updateUndoRedoButtons(); @@ -263,4 +279,64 @@ private void doRedo() refreshTaskList(); updateUndoRedoButtons(); } + + private void addPrereqs() + { + int processed = 0; + int invoked = 0; + + java.util.List contents = new java.util.ArrayList<>(); + collectTaskItemContents(taskListPanel, contents); + + for (TaskItemContent tic : contents) + { + Task t = tic.getTask(); + if (t != null && t.getType() == TaskType.QUEST) + { + processed++; + if (tic.addPrereqsFromContext()) + { + invoked++; + } + } + } + + // Refresh UI/state after batch + plugin.setValidateAll(true); + plugin.getUiStatusManager().refresh(goal); + refreshTaskList(); + updateUndoRedoButtons(); + + if (processed == 0) + { + JOptionPane.showMessageDialog(this, + "No quest tasks found in this goal.", + "Add pre-reqs", + JOptionPane.INFORMATION_MESSAGE); + return; + } + + if (invoked == 0) + { + JOptionPane.showMessageDialog(this, + "All pre-reqs have already been added.\n", + "Add pre-reqs", + JOptionPane.WARNING_MESSAGE); + } + } + + private static void collectTaskItemContents(Component root, java.util.List out) + { + if (root instanceof TaskItemContent) + { + out.add((TaskItemContent) root); + } + if (root instanceof Container) + { + for (Component child : ((Container) root).getComponents()) + { + collectTaskItemContents(child, out); + } + } + } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 8527696..b2b71d7 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -183,6 +183,7 @@ public void home() this.goalPanel = null; } + @Override public void refresh() { @@ -396,4 +397,4 @@ private void addFromPreset() repaint(); } } -} +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java index fca21c4..5bf0f96 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java @@ -7,6 +7,7 @@ import com.toofifty.goaltracker.models.task.QuestTask; import com.toofifty.goaltracker.utils.QuestRequirements; import java.util.List; +import java.util.Locale; import com.toofifty.goaltracker.ui.components.ListPanel; import com.toofifty.goaltracker.ui.components.ListItemPanel; @@ -218,4 +219,62 @@ private void exitEdit(boolean save) updateTitleLabel(); plugin.getUiStatusManager().refresh(goal); } + + public Task getTask() + { + return task; + } + + /** + * Try to invoke the same context menu action as right-click -> "Add prerequisites". + * @return true if the action was found and invoked, false otherwise + */ + public boolean addPrereqsFromContext() + { + JComponent listItem = (JComponent) SwingUtilities.getAncestorOfClass(ListItemPanel.class, this); + if (listItem == null || listItem.getComponentPopupMenu() == null) + { + return false; + } + + JPopupMenu menu = listItem.getComponentPopupMenu(); + // Accept several label variants (case-insensitive) + String[] targets = new String[] { + "add prerequisites", "add pre-reqs", "add prereqs", "prerequisites" + }; + + return clickMenuItemByLabels(menu.getSubElements(), targets); + } + + // Recursively search menu/submenus for a matching label and click it. + private static boolean clickMenuItemByLabels(MenuElement[] items, String[] needles) + { + for (MenuElement me : items) + { + if (me instanceof JMenuItem) + { + JMenuItem it = (JMenuItem) me; + String txt = it.getText(); + if (txt != null) + { + String lower = txt.toLowerCase(Locale.ROOT).trim(); + for (String needle : needles) + { + if (lower.contains(needle)) + { + it.doClick(); + return true; + } + } + } + } + // Recurse into submenus and containers + MenuElement[] children = me.getSubElements(); + if (children != null && children.length > 0 && clickMenuItemByLabels(children, needles)) + { + return true; + } + } + return false; + } } From b5a881daf7ec1814b49e77c342fad4d11201b109 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Sun, 24 Aug 2025 11:55:16 -0400 Subject: [PATCH 42/53] more presets --- README.md | 2 +- changelog.md | 7 +- .../presets/GoalPresetRepository.java | 116 ++++++++++++++++++ 3 files changed, 122 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2e93f31..a3e69a7 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ - Export and Import functionality: save your goals to a JSON file and import them back with full UI refresh. - Automatic warming of item icons so they load at startup, on login, and after import. - GoalsChangedListener system to auto-refresh the Home panel when goals change. -- Preset Goal Lists: add goals from built-in presets (Quest Cape Core, Early Game Ironman) via a new “Add from Preset…” button. +- Preset Goal Lists: add goals from built-in presets via a new “Add from Preset…” button (Quest Cape Core, Early/Mid/Late Ironman, Full Void Armor, Free-to-Play Quests, Fast Travel Unlocks). - Presets automatically expand quest prerequisites using the existing right-click prereq logic. - Goal titles and ManualTask descriptions now support click-to-edit with inline text fields. - Long goal and task titles are ellipsized with … and show full text on hover. diff --git a/changelog.md b/changelog.md index 5f63782..6517de7 100644 --- a/changelog.md +++ b/changelog.md @@ -2,7 +2,7 @@ ## [Unreleased] -### Added +-### Added - Quest prerequisites button: Each quest task now has an **Add prereqs** button to insert its prerequisites directly beneath it. - Shift+Click removal: Shift+Click a task to remove it and all its indented children at once. @@ -41,6 +41,9 @@ - Commented out Ladlor presets from default loading in GoalPresetRepository to simplify active preset list. - Fixed incorrect ItemID constants for Salve amulet (ei) in Slayer & Undead presets (SALVE_AMULET_EI). - Removed stray selection marker and corrected field declaration in GoalPresetRepository. +- Added Full Void Armor preset (base Void top, robe, gloves, melee/range/mage helms). +- Added Free-to-Play Quests preset including all 20 F2P quests, ordered from quick to long, with partner note for Shield of Arrav. +- Added Fast Travel Unlocks preset covering quests that unlock teleport networks, transportation methods, and teleport items (spirit trees, gliders, fairy rings, balloons, Kourend memoirs, Drakan's medallion, etc.). ### Changed - Pre-req button made more compact (~25% smaller). @@ -88,7 +91,7 @@ - Goal card progress text (e.g., “1/10”) reserved fixed width and no longer clips; typography is consistent and does not shrink. - Task rows updated with consistent styling and ellipsis/edit behavior, matching Goal cards for a unified UI. -### Fixed +-### Fixed - Home panel Undo/Redo buttons removed; these controls now exist only in Goal view. - ActionBarButton painting fixed: clears background correctly, text always drawn on top, and hover state no longer causes overlapping artifacts. - GoalTrackerPanel `home()` method fixed so returning from Goal view refreshes and displays the goal list instead of a blank panel. diff --git a/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java b/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java index b74658d..0a74187 100644 --- a/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java +++ b/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java @@ -2,6 +2,7 @@ import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.task.ItemTask; +import com.toofifty.goaltracker.models.task.ManualTask; import com.toofifty.goaltracker.models.task.QuestTask; import com.toofifty.goaltracker.models.task.SkillLevelTask; import com.toofifty.goaltracker.utils.ReorderableList; @@ -52,6 +53,9 @@ public static List getAll() { list.add(buildEarlyIronman()); list.add(buildMidIronman()); list.add(buildLateIronman()); + list.add(buildFullVoidArmor()); + list.add(buildFastTravelUnlocks()); + list.add(buildFreeToPlayQuests()); return list; } @@ -259,6 +263,28 @@ private static Preset buildLadlorIronman() { goals ); } + + // Fast travel unlocks (not a full preset, just for reference) + private static List buildFastTravelUnlocks() { + return Arrays.asList( + // Teleport items from quests + QuestTask.builder().quest(Quest.MONKEY_MADNESS_II).build(), // Royal seed pod + QuestTask.builder().quest(Quest.A_TASTE_OF_HOPE).build(), // Drakan's medallion teleports in Morytania + QuestTask.builder().quest(Quest.DESERT_TREASURE_I).build(), // Ring of visibility (not a teleport itself, but for fairy rings) + QuestTask.builder().quest(Quest.FAIRYTALE_II__CURE_A_QUEEN).build(), // Fairy rings + QuestTask.builder().quest(Quest.LUNAR_DIPLOMACY).build(), // Lunar spellbook teleport + QuestTask.builder().quest(Quest.ANIMAL_MAGNETISM).build(), // Ectophial + QuestTask.builder().quest(Quest.ANCIENT_CAVERN).build(), // Games necklace (Barbarian Outpost) + // Other travel networks + QuestTask.builder().quest(Quest.THE_GRAND_TREE).build(), // Gnome glider network + QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build(), // Spirit tree network + QuestTask.builder().quest(Quest.THE_GIANT_DWARF).build(), // Keldagrim mine carts + QuestTask.builder().quest(Quest.ANOTHER_SLICE_OF_HAM).build(), // Dorgesh-Kaan ↔ Keldagrim train + QuestTask.builder().quest(Quest.KINGS_RANSOM).build(), // Camelot teleport + QuestTask.builder().quest(Quest.PLAGUE_CITY).build(), // Ardougne teleport + QuestTask.builder().quest(Quest.RUM_DEAL).build() // Harmony Island teleport (Morytania) + ); + } private static Preset buildLadlorMelee() { Goal melee = Goal.builder() .description("Ladlor — Melee Progression") @@ -578,4 +604,94 @@ private static Preset buildLateIronman() { .build(); return new Preset("Late Ironman Progression", "Stats, gear, and quest goals for late game Ironman.", Arrays.asList(late)); } + private static Preset buildFullVoidArmor() { + Goal voidSet = Goal.builder() + .description("Full Void Armor Set") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(VOID_KNIGHT_TOP).itemName("Void knight top").quantity(1).build(), + ItemTask.builder().itemId(VOID_KNIGHT_ROBE).itemName("Void knight robe").quantity(1).build(), + ItemTask.builder().itemId(VOID_KNIGHT_GLOVES).itemName("Void knight gloves").quantity(1).build(), + ItemTask.builder().itemId(VOID_MELEE_HELM).itemName("Void melee helm").quantity(1).build(), + ItemTask.builder().itemId(VOID_RANGER_HELM).itemName("Void ranger helm").quantity(1).build(), + ItemTask.builder().itemId(VOID_MAGE_HELM).itemName("Void mage helm").quantity(1).build() + )) + .build(); + return new Preset("Full Void Armor", "All base Void pieces: top, robe, gloves, and all three helms.", Arrays.asList(voidSet)); + } + private static Preset buildFreeToPlayQuests() { + Goal f2p = Goal.builder() + .description("All Free-to-Play Quests") + .tasks(ReorderableList.from( + // Suggested quick-to-hard order + QuestTask.builder().quest(Quest.COOKS_ASSISTANT).build(), + QuestTask.builder().quest(Quest.SHEEP_SHEARER).build(), + QuestTask.builder().quest(Quest.ROMEO__JULIET).build(), + QuestTask.builder().quest(Quest.THE_RESTLESS_GHOST).build(), + QuestTask.builder().quest(Quest.IMP_CATCHER).build(), + QuestTask.builder().quest(Quest.DORICS_QUEST).build(), + QuestTask.builder().quest(Quest.GOBLIN_DIPLOMACY).build(), + QuestTask.builder().quest(Quest.ERNEST_THE_CHICKEN).build(), + QuestTask.builder().quest(Quest.THE_KNIGHTS_SWORD).build(), + QuestTask.builder().quest(Quest.PIRATES_TREASURE).build(), + QuestTask.builder().quest(Quest.PRINCE_ALI_RESCUE).build(), + QuestTask.builder().quest(Quest.BLACK_KNIGHTS_FORTRESS).build(), + QuestTask.builder().quest(Quest.VAMPYRE_SLAYER).build(), + QuestTask.builder().quest(Quest.DEMON_SLAYER).build(), + QuestTask.builder().quest(Quest.RUNE_MYSTERIES).build(), + QuestTask.builder().quest(Quest.MISTHALIN_MYSTERY).build(), + QuestTask.builder().quest(Quest.THE_CORSAIR_CURSE).build(), + // Note: Shield of Arrav needs a partner + ManualTask.builder().description("Find a partner for Shield of Arrav (Phoenix/Black Arm) or coordinate in a clan chat").build(), + QuestTask.builder().quest(Quest.SHIELD_OF_ARRAV).build(), + // Capstone F2P quest + QuestTask.builder().quest(Quest.DRAGON_SLAYER_I).build() + )) + .build(); + return new Preset("Free-to-Play Quests", "Every F2P quest in OSRS (20 total) as of today.", Arrays.asList(f2p)); + } + private static Preset buildFastTravelUnlocks() { + Goal travel = Goal.builder() + .description("Fast Travel Unlocks (Quest-gated)") + .tasks(ReorderableList.from( + // Spirit trees & gliders + QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build(), + QuestTask.builder().quest(Quest.THE_GRAND_TREE).build(), + // Fairy rings (partial completion allows use with dramen/lunar staff) + QuestTask.builder().quest(Quest.FAIRYTALE_II__CURE_A_QUEEN).build(), + // Balloons + QuestTask.builder().quest(Quest.ENLIGHTENED_JOURNEY).build(), + // City teleport spells unlocked by quests + QuestTask.builder().quest(Quest.WATCHTOWER).build(), + QuestTask.builder().quest(Quest.PLAGUE_CITY).build(), + QuestTask.builder().quest(Quest.EADGARS_RUSE).build(), + // Teleport items from quests + QuestTask.builder().quest(Quest.GHOSTS_AHOY).build(), // Ectophial + QuestTask.builder().quest(Quest.THE_DIG_SITE).build(), // Digsite pendant access + QuestTask.builder().quest(Quest.BONE_VOYAGE).build(), // Fossil Island travel + QuestTask.builder().quest(Quest.MONKEY_MADNESS_II).build(), // Royal seed pod + QuestTask.builder().quest(Quest.A_TASTE_OF_HOPE).build(), // Drakan's medallion teleports in Morytania + // Kourend memoirs teleports + QuestTask.builder().quest(Quest.CLIENT_OF_KOUREND).build(), + QuestTask.builder().quest(Quest.THE_DEPTHS_OF_DESPAIR).build(), + QuestTask.builder().quest(Quest.THE_QUEEN_OF_THIEVES).build(), + QuestTask.builder().quest(Quest.TALE_OF_THE_RIGHTEOUS).build(), + QuestTask.builder().quest(Quest.THE_FORSAKEN_TOWER).build(), + QuestTask.builder().quest(Quest.THE_ASCENT_OF_ARCEUUS).build(), + // Other travel networks + QuestTask.builder().quest(Quest.EAGLES_PEAK).build(), // Eagle transport + QuestTask.builder().quest(Quest.SHILO_VILLAGE).build(), // Brimhaven–Shilo carts + QuestTask.builder().quest(Quest.THE_GIANT_DWARF).build(), // Keldagrim mine carts + QuestTask.builder().quest(Quest.ANOTHER_SLICE_OF_HAM).build(), // Dorgesh-Kaan ↔ Keldagrim train + QuestTask.builder().quest(Quest.THE_FREMENNIK_TRIALS).build(), // Enchanted lyre to Rellekka + // Spellbook unlocks with many teleports + QuestTask.builder().quest(Quest.DESERT_TREASURE_I).build(), // Ancient Magicks + QuestTask.builder().quest(Quest.LUNAR_DIPLOMACY).build() // Lunar teleports + )) + .build(); + return new Preset( + "Fast Travel Unlocks", + "Quests that unlock major transportation methods: spirit trees, gliders, fairy rings, balloons, city teleports, carts, memoirs, seed pod, and more.", + Arrays.asList(travel) + ); + } } From aa8cdb63ff49ce0ef4b05a0436edae5b98dee9c0 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 25 Aug 2025 14:06:25 -0400 Subject: [PATCH 43/53] Imported my V2 version into this branch and updated the paths to go back to the OG path and started removing any\all v2. Currently some stuff still lingers but it's running and working. --- README.md | 141 +++-- build.gradle | 18 +- changelog.md | 144 ++--- gradlew.bat | 178 +++---- img/add_from_preset_v2.jpg | Bin 0 -> 30891 bytes img/goal_panel_v2.jpg | Bin 0 -> 69957 bytes img/goal_task_panel_v2.jpg | Bin 0 -> 65973 bytes img/right_click_goals_v2.jpg | Bin 0 -> 27127 bytes runelite-plugin.properties | 13 +- settings.gradle | 2 +- .../com/toofifty/goaltracker/GoalManager.java | 6 +- .../toofifty/goaltracker/GoalSerializer.java | 9 +- .../goaltracker/GoalTrackerConfig.java | 14 +- .../goaltracker/GoalTrackerPlugin.java | 452 +++++++++++----- .../com/toofifty/goaltracker/ItemCache.java | 15 +- .../goaltracker/TaskUIStatusManager.java | 6 +- .../goaltracker/adapters/QuestAdapter.java | 6 +- .../goaltracker/adapters/SkillAdapter.java | 6 +- .../goaltracker/adapters/TaskAdapter.java | 8 +- .../goaltracker/models/ActionHistory.java | 33 +- .../com/toofifty/goaltracker/models/Goal.java | 48 +- .../models/IndentChangeAction.java | 6 +- .../goaltracker/models/RemoveTaskAction.java | 3 +- .../goaltracker/models/ReorderTaskAction.java | 3 +- .../models/ToggleCompleteAction.java | 4 +- .../goaltracker/models/UndoStack.java | 9 +- .../goaltracker/models/enums/Status.java | 4 + .../goaltracker/models/enums/TaskType.java | 4 + .../goaltracker/models/task/ItemTask.java | 50 +- .../goaltracker/models/task/ManualTask.java | 12 +- .../goaltracker/models/task/QuestTask.java | 50 +- .../models/task/SkillLevelTask.java | 12 +- .../goaltracker/models/task/SkillXpTask.java | 12 +- .../goaltracker/models/task/Task.java | 11 +- .../presets/GoalPresetRepository.java | 498 ++---------------- .../goaltracker/services/TaskIconService.java | 67 ++- .../services/TaskUpdateService.java | 156 +++--- .../goaltracker/ui/GoalItemContent.java | 43 +- .../toofifty/goaltracker/ui/GoalPanel.java | 23 +- .../goaltracker/ui/GoalTrackerPanel.java | 61 ++- .../toofifty/goaltracker/ui/NewTaskPanel.java | 23 +- .../goaltracker/ui/TaskItemContent.java | 35 +- .../goaltracker/ui/components/ActionBar.java | 13 +- .../ui/components/ActionBarButton.java | 22 +- .../goaltracker/ui/components/ComboBox.java | 30 +- .../ui/components/EditableInput.java | 34 +- .../ui/components/ListItemPanel.java | 26 +- .../goaltracker/ui/components/ListPanel.java | 6 +- .../ui/components/ListTaskPanel.java | 69 +-- .../goaltracker/ui/components/TextButton.java | 18 +- .../goaltracker/ui/inputs/ItemTaskInput.java | 29 +- .../ui/inputs/ManualTaskInput.java | 6 +- .../goaltracker/ui/inputs/QuestTaskInput.java | 15 +- .../ui/inputs/SkillLevelTaskInput.java | 6 +- .../ui/inputs/SkillXpTaskInput.java | 6 +- .../goaltracker/ui/inputs/TaskInput.java | 17 +- .../toofifty/goaltracker/utils/Constants.java | 5 +- .../goaltracker/utils/QuestRequirements.java | 16 +- .../goaltracker/utils/ReorderableList.java | 7 +- .../goaltracker/GoalSerializerTest.java | 10 +- .../toofifty/goaltracker/models/GoalTest.java | 2 +- .../models/enums/TaskTypeTest.java | 3 +- .../goaltracker/models/task/ItemTaskTest.java | 2 +- .../models/task/ManualTaskTest.java | 2 +- .../models/task/QuestTaskTest.java | 2 +- .../models/task/SkillLevelTaskTest.java | 2 +- .../models/task/SkillXpTaskTest.java | 2 +- .../goaltracker/models/task/TaskTest.java | 3 +- .../services/TaskIconServiceTest.java | 8 +- .../services/TaskUpdateServiceTest.java | 12 +- 70 files changed, 1224 insertions(+), 1334 deletions(-) create mode 100644 img/add_from_preset_v2.jpg create mode 100644 img/goal_panel_v2.jpg create mode 100644 img/goal_task_panel_v2.jpg create mode 100644 img/right_click_goals_v2.jpg diff --git a/README.md b/README.md index a3e69a7..e4bf96b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🏆 Goal Tracker v2 +# 🏆 Goal Tracker v2 > A complete reimagining of the Goal Tracker plugin — rebuilt with a more modern UI, powerful new features, and improved stability to help you plan, track, and achieve your Old School RuneScape goals with ease. @@ -7,104 +7,89 @@

✨ New Features

-- Shift+Click removal of tasks for faster task management -- Automatic goal status checks for up-to-date progress -- New ActionBar and ActionBarButton UI components -- Hover states for better visual feedback -- New context menu organization for streamlined interaction -- Search toggle improvements for easier task searching -- New task right‑click menu with grouped **Move** submenu and cascading complete/incomplete toggle that applies to children (expanded beyond v1 functionality). -- Goal cards redesigned with lighter fills, full shadows, and hover/press effects only on the card face. -- Thicker header divider under “Goal Tracker” for clearer separation. -- Automatic refresh propagation so Home goals update instantly when tasks change. -- Completion chat messages delivered as proper Game messages with configurable colors. -- Export and Import functionality: save your goals to a JSON file and import them back with full UI refresh. -- Automatic warming of item icons so they load at startup, on login, and after import. -- GoalsChangedListener system to auto-refresh the Home panel when goals change. -- Preset Goal Lists: add goals from built-in presets via a new “Add from Preset…” button (Quest Cape Core, Early/Mid/Late Ironman, Full Void Armor, Free-to-Play Quests, Fast Travel Unlocks). -- Presets automatically expand quest prerequisites using the existing right-click prereq logic. -- Goal titles and ManualTask descriptions now support click-to-edit with inline text fields. -- Long goal and task titles are ellipsized with … and show full text on hover. +- Shift+Click removal of tasks for faster task management +- New ActionBar and ActionBarButton UI components +- Hover states for better visual feedback +- New context menu organization for streamlined interaction +- Search toggle improvements for easier task searching +- New task right‑click menu with grouped **Move** submenu and cascading complete/incomplete toggle that applies to children +- Redesigned goal cards with lighter fills, full shadows, and hover/press effects on the card face +- Export and Import functionality to save and load goals via JSON files +- Presets automatically expand quest prerequisites +- Goal titles and ManualTask descriptions support click-to-edit with inline text fields +- Long goal and task titles are ellipsized with … and show full text on hover ### ♻️ Redesigned Features -- Redesigned quest prerequisites button for quick access to quest requirements -- Redesigned completion cascading to automatically complete related tasks -- Redesigned dropdown quest selector for faster quest task addition -- Redesigned right-click menus for prerequisites and child completion options -- Redesigned manual toggling for preset tasks to customize workflow -- Redesigned chatbox notification colors (now fully customizable) +- Redesigned quest prerequisites button for quick access +- Redesigned completion cascading to automatically complete related tasks +- Redesigned dropdown quest selector for faster quest task addition +- Redesigned right-click menus for prerequisites and child completion options +- Redesigned manual toggling for preset tasks +- Redesigned chatbox notification colors with customization options +- Redesigned quest status detection with improved auto-refresh +- Redesigned completion chat messages delivered as proper Game messages

🔧 Improvements

-- More compact prereq button for a cleaner interface -- Refreshed UI with updated design elements -- Font and ComboBox readability enhancements -- Consistent ActionBar UI throughout the plugin -- Unified goal view header for a cohesive look -- Improved context menus with better usability -- Enhanced cursor and hover detection accuracy -- Copy and paste support in the goal name input field -- Context menu entries rebuilt dynamically before opening to always reflect the latest state. -- Task and goal content now force an initial refresh so icons and text render correctly at login. -- Refined ActionBar spacing to ensure Export and Import buttons fit without overlap. -- Progress text (e.g., “1/10”) on goal cards now has a reserved width and never clips. -- + Add goal and Add from Preset buttons restyled and stacked vertically for cleaner layout. -- Task rows updated to match goal cards with ellipsized titles and consistent styling. +- Refreshed UI with updated design elements and improved readability +- Consistent ActionBar UI throughout the plugin +- Unified goal view header for a cohesive look +- Improved context menus and hover detection accuracy +- Copy and paste support in the goal name input field +- Refined ActionBar spacing to fit Export and Import buttons without overlap +- Progress text (e.g., “1/10”) on goal cards now has reserved width and never clips +- + Add goal and Add from Preset buttons restyled and stacked vertically +- Task rows updated to match goal cards with icons and ellipsized titles +- Debounced quest detection and scheduled UI refresh to prevent lag +- Goal/task panels auto-refresh reliably after quest or item detection +- Extended item task detection to include equipment, seed vault, and group storage

🐛 Fixes

-- Undo/Redo functionality cleanup for smoother editing -- ActionBarButton painting fixes to prevent visual glitches -- GoalTrackerPanel `home()` method refresh improvements -- Correct refresh behavior in ListPanel -- Improved mouse selection accuracy -- Keyboard shortcut fixes and enhancements -- Automatic removal of empty goals to keep lists tidy -- Visual refresh issue resolved on login -- Fixed child task refresh issues by recursively refreshing all descendants. -- Fixed blank panel issue when switching from Home to Goal view. -- Fixed completion chat message not appearing on task completion. -- Export/Import buttons were previously non-functional; now they work correctly. -- Item icons sometimes failed to appear until entering a goal; now fixed by preloading at startup/login and after import. -- Home panel not refreshing after completing a task; fixed with a new listener system. -- Overlapping issue around the Export button fixed by layout adjustments. +- Undo/Redo functionality cleanup for smoother editing +- ActionBarButton painting fixes to prevent visual glitches +- Improved refresh behavior in Home and List panels +- Enhanced mouse selection accuracy +- Keyboard shortcut fixes and enhancements +- Automatic removal of empty goals to keep lists tidy +- Visual refresh issue resolved on login +- Fixed child task refresh issues +- Fixed blank panel issue when switching views +- Fixed completion chat messages to appear properly with configured colors +- Export/Import buttons now work correctly +- Item icons preload correctly on startup, login, and after import +- Home panel refreshes correctly after completing a task +- Layout adjustments to fix overlapping Export button +- Fixed Shield of Arrav preset indentation for partner-finding step +- Sidebar refreshes automatically after quest or item detection
-

📥 Installation

- -1. Open RuneLite. -2. Go to the Plugin Hub. -3. Search for "Goal Tracker v2". -4. Click **Install**. - -
- -
-

🚀 Getting Started

+

🖼️ Screenshots

-- Open the plugin panel in RuneLite once installed. -- Use **+ Add goal** to create a new goal. -- Add tasks (quests, skills, items, or manual) via the goal view. -- Use the new **ActionBar** buttons for navigation, undo/redo, and bulk actions. +### Home panel with goal cards +*Redesigned goal cards with progress bars, hover effects, and instant refresh — plus the new “Add from Preset” button and Export/Import features for easy sharing and backups.* +![Goal Tracker Home Panel](img/goal_panel_v2.jpg) -
+### Home panel right‑click menu +*New options: move goal up/down, mark all complete/incomplete, and pin/unpin goals.* +![Goal Tracker Right Click Menu](img/right_click_goals_v2.jpg) -
-

🖼️ Screenshots

+### Inside a goal with task list +*Task rows now show icons, ellipsized text, and inline editing — with redesigned item search & quest features, plus an Add Pre-reqs button for quests (using our built-in list), along with Undo/Redo buttons.* +![Goal Tracker Task List](img/goal_task_panel_v2.jpg) -[screenshot] Home panel with goal cards -[screenshot] Inside a goal with task list -[screenshot] Right‑click menu on a task -[screenshot] Config panel with customizable chat color +### Add from Preset +*Quickly insert curated presets like Ironman Progression, Void set, or Barrows Gear.* +![Goal Tracker Add From Preset](img/add_from_preset_v2.jpg)
@@ -112,7 +97,7 @@

🙏 Acknowledgements

- Original plugin created by **dillydill123**. -- Fully renovated and maintained by **AhDoozy**. +- Renovated and maintained by **AhDoozy**. ## 📄 License @@ -185,4 +170,4 @@ Track quest progress and completion, just select a quest or miniquest from the d Select an item using the search button and searching via the in-game chatbox, then set the desired quantity. The plugin will keep track of your items and tally up quantities stored in different inventories (bank, player, GIMP storage), and will be automatically completed once you get that amount of the item. - \ No newline at end of file + diff --git a/build.gradle b/build.gradle index cc76a13..b565110 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,12 @@ plugins { id 'jacoco' } +java { + toolchain { + languageVersion = JavaLanguageVersion.of(11) + } +} + repositories { mavenLocal() maven { @@ -11,7 +17,7 @@ repositories { mavenCentral() } -def runeLiteVersion = 'latest.release' +def runeLiteVersion = findProperty('rlVersion') ?: 'latest.release' dependencies { compileOnly "net.runelite:client:${runeLiteVersion}" @@ -31,13 +37,12 @@ dependencies { testImplementation "net.runelite:jshell:${runeLiteVersion}" } -group = 'com.toofifty.goaltracker' +group = 'com.ahdoozy.goaltrackerv2' version = '1.0.4' -sourceCompatibility = '11' -targetCompatibility = JavaVersion.VERSION_11 tasks.withType(JavaCompile) { options.encoding = 'UTF-8' + options.release = 11 } tasks.test { @@ -52,4 +57,9 @@ jacocoTestReport { reports { xml.required = true } +} + +tasks.withType(Jar) { + preserveFileTimestamps = false + reproducibleFileOrder = true } \ No newline at end of file diff --git a/changelog.md b/changelog.md index 6517de7..261e94e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,109 +1,45 @@ # Changelog -## [Unreleased] - --### Added -- Quest prerequisites button: Each quest task now has an **Add prereqs** button to insert its prerequisites - directly beneath it. -- Shift+Click removal: Shift+Click a task to remove it and all its indented children at once. -- Completion cascading: Marking a parent quest as complete/incomplete now automatically updates all its - child tasks. -- Dropdown quest selector: Replaced fuzzy search with a clean dropdown of all quests, displaying natural - names (e.g., "Tree Gnome Village"). -- Right-click menu option **Add pre-reqs** for quest tasks with prerequisites. Automatically inserts - missing prerequisites as subtasks with proper indentation and prevents duplicates. -- New `ActionBar` and `ActionBarButton` UI components introduced for consistent toolbar styling across - panels. -- Cursor hover state and hand cursor indicator for goal list items to improve usability. -- Added right-click menu to parent goals to mark all child tasks as completed or incomplete. -- Added manual completion toggling for tasks created from presets, allowing users to right-click and mark them complete/incomplete just like quick-added tasks. -- Added customizable color setting for task completion messages shown in the chatbox. -- Implemented automatic goal status checking upon login to mark goals as completed if requirements are already met. -- Context menu entries now rebuilt dynamically right before opening to ensure the latest state. -- Task list panel context menus reorganized with a **Move** submenu and cascading complete/incomplete toggle that applies to children. -- Initial refresh calls for Task and Goal content so icons/text render correctly on login. -- Automatic ancestor `ListPanel` refresh propagation to keep Home goal list in sync when tasks change. -- Export and Import functionality: Goals can now be exported to JSON and imported back with full UI refresh. -- JSON file filter in Import/Export dialogs for safer file selection. -- Automatic warming of ItemTask icons on plugin startup and login tick, and after JSON import, ensuring icons display immediately. -- GoalsChangedListener system in GoalManager to notify panels and auto-refresh home view when goals are saved or loaded. -- Preset Goal Lists: Added “Add from Preset…” button in the Goal Tracker panel, with initial presets (Quest Cape Core, Early Game Ironman). Presets now expand to include prerequisite quests automatically. -- Centralized presets into a new `GoalPresetRepository` class for easier management and expansion. -- Automatic prerequisite expansion for presets leverages the same logic as the quest right-click **Add prereqs** option. -- Ellipsized titles: Goal cards and Task rows now ellipsize long titles with `…` and show full text on hover via tooltip. -- Click-to-edit: Goal titles and ManualTask descriptions can now be edited by clicking their label; label swaps to an inline text field and saves on Enter or blur. -- Added per-lane Ladlor presets (Melee, Ranged, Magic, Utility, Void Set, God Capes, Crystal & Bowfa, Jewelry & Boots, Slayer & Undead, Imbued Rings, God Wars Armor, Elite Void, Prayer Scrolls, Raids Uniques, Milestones & Capes), alongside the full combined Ladlor Ironman preset. -- Added Early, Mid, and Late Ironman presets as consolidated single-goal lists rather than split by category. -- Added a new **Add pre-reqs** button to the Goal view header, next to Undo/Redo. This button triggers the same logic as the quest right-click **Add prerequisites** option, but applies to all quest tasks in the goal at once. -- Converted all Early, Mid, and Late Ironman preset skill milestones from ManualTask to SkillLevelTask for accurate level tracking. -- Expanded Early Ironman preset with Birdhouse run unlocks (Dig Site, Bone Voyage, Hunter 5, Crafting 8, Construction 16) and Seaweed run unlock (Farming 23). -- Reworked Early, Mid, and Late Ironman presets with more appropriate content and progression pacing (e.g., Early emphasizes graceful set and mobility, Mid includes Iban’s staff, Ava’s, Barrows gloves, Late includes Blowpipe, Bowfa, Bandos, endgame quests). -- Commented out Ladlor presets from default loading in GoalPresetRepository to simplify active preset list. -- Fixed incorrect ItemID constants for Salve amulet (ei) in Slayer & Undead presets (SALVE_AMULET_EI). -- Removed stray selection marker and corrected field declaration in GoalPresetRepository. -- Added Full Void Armor preset (base Void top, robe, gloves, melee/range/mage helms). -- Added Free-to-Play Quests preset including all 20 F2P quests, ordered from quick to long, with partner note for Shield of Arrav. -- Added Fast Travel Unlocks preset covering quests that unlock teleport networks, transportation methods, and teleport items (spirit trees, gliders, fairy rings, balloons, Kourend memoirs, Drakan's medallion, etc.). +### Added +- Item task auto-detection across inventory, equipment, bank, seed vault, and group storage. +- Equivalent item detection for Barrows gear: degraded versions (100/75/50/25/0) now treated as base items. +- New All Barrows Gear preset including every Ahrim’s, Dharok’s, Guthan’s, Karil’s, Torag’s, and Verac’s item. +- Quest prerequisites button: each quest task has an **Add prereqs** option to insert requirements beneath it. +- Shift+Click removal to delete a task and its children. +- Completion cascading: parent quest completion toggles propagate to children. +- Dropdown quest selector with natural quest names. +- Right-click **Add prereqs** for quest tasks with automatic insertion and indentation. +- Right-click menu to mark all child tasks complete or incomplete. +- Manual completion toggling for preset tasks. +- Configurable color setting for completion chat messages. +- Export and import goals to JSON with file filtering and full UI refresh. +- Preset Goal Lists with an “Add from Preset…” button (including Quest Cape, Ironman progressions, Void, F2P, Fast Travel, etc.). +- Full Void Armor, Free-to-Play Quests, and Fast Travel Unlocks presets. +- Expanded and reworked Early, Mid, and Late Ironman presets. +- Inline editing support for goal titles and manual task descriptions. +- Ellipsized titles with tooltips on hover for goal cards and task rows. ### Changed -- Pre-req button made more compact (~25% smaller). -- Prerequisite insertion now places child tasks directly below their parent instead of at the end of the - list. -- UI now automatically refreshes after task mutations (add/remove/indent/status change). -- Right-click context menu reorganized: Move actions grouped under a **Move** submenu; Remove action now - labeled as **Remove (Shift+Left Click)**. -- Quest tasks now only display the **Add pre-reqs** option if they actually have missing prerequisites. -- Search button now toggles between **Search...** and **Close** to open/close the item search overlay. -- Removed in-panel close button; close control is now handled directly via the Search button toggle. -- Green **+Add** button hidden for item search inputs (still visible for other input types). -- Duplicate prerequisites can no longer be added multiple times to the same quest. -- Quest dropdown now uses **RuneScape UF** font at a normal crisp size for improved readability. -- ComboBox font scaling updated to use integer point sizes, preventing fuzzy text. - - - Back and Undo buttons added to a new top control bar in goal view; later removed and replaced with a - cleaner single bar design. - - Embedded red "< Back" button removed from GoalPanel header, leaving only the goal name input aligned - cleanly. - - Goal name input updated to support copy, paste, cut, and select-all actions via both context menu and - keyboard shortcuts. - - Remove menu option enhanced to also delete all indented child tasks when removing a parent. - - Remove menu label updated so the "(Shift+Left Click)" hint displays smaller and in gray. - - Home view updated: "Goal Tracker" title moved to its own header, with a new action bar beneath it - containing **+ Add goal**, **Move**, and **Bulk Edit** buttons (the latter two are placeholders marked - "Coming soon"). - - Home panel action bar refactored to use new `ActionBar` and `ActionBarButton` components for consistent - styling. - - Goal view header updated to use unified `ActionBar` with Back (left) and Undo/Redo (right) buttons. - - ActionBar now includes a center spacer to properly separate left and right button groups. - - Right-click context menus fixed to reliably open across platforms (Windows, macOS, Linux). - - Task right-click now defaults to showing full ListItemPanel menu (move, remove, etc.), with fallback to - toggle-only menu if no parent menu exists. - - Goal item context menus updated to forward right-clicks to parent ListItemPanel menus for consistent - options. - - Cursor/hover detection improved on home goal list: listeners now attach recursively to all child - components for accurate selection and highlighting. -- Goal cards now use a lighter fill with a full shadow around borders, and hover/press only affect the card face. -- Header divider under “Goal Tracker” made thicker (4px) for stronger separation. -- Right-click menus refactored so Tasks build their own menu and Goals build theirs, preventing duplicate/unusable items. -- ActionBar spacing refined between Redo and Export buttons to prevent overlap and fit Import button. -- Plugin startup now triggers item icon warm-up so icons are ready before login. -- “+ Add goal” and “Add from Preset…” buttons now stacked vertically in the Goal Tracker panel header for cleaner layout. -- Goal card progress text (e.g., “1/10”) reserved fixed width and no longer clips; typography is consistent and does not shrink. -- Task rows updated with consistent styling and ellipsis/edit behavior, matching Goal cards for a unified UI. +- Quest detection stabilized and debounced with scheduled UI refresh to prevent login lag. +- Task and goal panels auto-refresh after quest or item detection without requiring navigation. +- Chat completion messages rewritten as proper `GAMEMESSAGE`s with color customization support. +- UI polish including a more compact prerequisites button, consistent ActionBar styling, unified headers, and improved readability. +- Right-click context menus reorganized with a grouped **Move** submenu and cleaner option labels. +- Search input redesigned to toggle open/close behavior. +- Preset prerequisite expansion now prevents duplicate additions. +- Task rows updated with consistent styling, icon warming, and inline editing capabilities. +- Home panel buttons stacked vertically; header divider thickness increased for clarity. +- Goal card typography fixed with reserved progress text width to prevent clipping. --### Fixed -- Home panel Undo/Redo buttons removed; these controls now exist only in Goal view. -- ActionBarButton painting fixed: clears background correctly, text always drawn on top, and hover state no longer causes overlapping artifacts. -- GoalTrackerPanel `home()` method fixed so returning from Goal view refreshes and displays the goal list instead of a blank panel. -- ListPanel `refresh()` updated to rebuild list before refreshing children, preventing stale or empty views. -- Improved accuracy of mouse selection in home goal list; click/hover listeners now consistently cover the entire item area. -- Goal name input now fully supports keyboard shortcuts (Ctrl/Cmd+C, V, X, A, Insert/Delete variants) and right-click context menu for copy/paste across all platforms. -- Empty goals (0/0 tasks) created via **+ Add goal** are now automatically removed when backing out without adding tasks, preventing clutter in saved goal lists. -- Fixed visual refresh issue where quest task statuses didn’t show correctly on login unless re-entering the goal. -- Fixed child task refresh issues after parent complete/incomplete cascades by recursively refreshing all descendants. -- Fixed blank panel issue when switching from Home to Goal view by only using inner card body for Goal rows. -- Fixed completion chat message not appearing; now delivered as a proper `GAMEMESSAGE` with customizable config color. -- Export/Import buttons previously non-functional; now wired to save/load JSON correctly. -- Item icons not appearing until entering a goal; fixed by warming icons at startup/login and after import. -- Home panel not refreshing after task completion; fixed with GoalsChangedListener refresh hook. -- Overlapping UI issue around Export button resolved by adjusting panel borders and layout. +### Fixed +- Fixed chat completion messages: now appear reliably as `GAMEMESSAGE`s with configured colors; raw `` tags removed. +- Shield of Arrav preset partner-finding step properly indented as a subtask. +- Fixed sidebar and panels not refreshing after quest or item detection (no longer requires re-entering a goal or relogging). +- Fixed Home and Goal panel refresh issues, including blank panels, stale data, and child task refresh not propagating. +- Fixed item icons failing to appear until goal entry; now preload on startup, login, and after JSON import. +- Fixed Export and Import buttons (now fully functional). +- Fixed Undo/Redo buttons: restricted to Goal view and visual overlaps resolved. +- Fixed mouse and keyboard input handling for selection, copy/paste, and shortcuts. +- Fixed empty goals remaining in list; now automatically removed if left without tasks. +- Fixed overlapping Export button layout issue. +- Fixed quest tasks not showing correct status on login until entering the goal. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f..107acd3 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/img/add_from_preset_v2.jpg b/img/add_from_preset_v2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6210af5be66b6715e6105031b955b4931ca6b807 GIT binary patch literal 30891 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
V>U4O6aVGhp>bhQE5>?n?Wszj9uS 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 WjMU~>|1@KBMdptAqOXm{yZ;ShTJ}W% literal 0 HcmV?d00001 diff --git a/img/goal_panel_v2.jpg b/img/goal_panel_v2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dce5882f64059e23b419da309058ce2452f318ce GIT binary patch literal 69957 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 literal 0 HcmV?d00001 diff --git a/img/right_click_goals_v2.jpg b/img/right_click_goals_v2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..084468d192c30d99644ca521ded22439fa4a0ce1 GIT binary patch literal 27127 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 SwingUtilities.invokeLater(goalTrackerPanel::refresh)); + uiRefreshTimer.setRepeats(false); + uiRefreshTimer.start(); + } + + /** + * Preloads commonly used item icons so they render instantly in the UI. + */ + public void warmItemIcons() + { + try + { + // Example: warm up the TODO_LIST icon at minimum + itemManager.getImage(ItemID.TODO_LIST); + + // Warm up skill icons + for (Skill skill : Skill.values()) + { + skillIconManager.getSkillImage(skill); + } + } + catch (Exception ex) + { + log.warn("warmItemIcons failed", ex); + } + } + @Override protected void startUp() { - goalManager.load(); - itemCache.load(); - goalTrackerPanel.home(); + // Defensive guards to avoid NPEs during test bootstrap if DI bindings are missing + if (goalManager == null || itemCache == null || goalTrackerPanel == null || itemManager == null || clientToolbar == null) + { + log.warn("GoalTrackerV2Plugin: skipping full startup because a dependency was null. goalManager={}, itemCache={}, panel={}, itemManager={}, toolbar={}", + goalManager != null, itemCache != null, goalTrackerPanel != null, itemManager != null, clientToolbar != null); + return; + } - final AsyncBufferedImage icon = itemManager.getImage(ItemID.TODO_LIST); + try { + goalManager.load(); + itemCache.load(); + } catch (Exception ex) { + log.error("GoalTrackerV2Plugin: failed to load persisted state", ex); + } - icon.onLoaded(() -> { - uiNavigationButton = NavigationButton.builder() - .tooltip("Goal Tracker") - .icon(icon) - .priority(7) - .panel(goalTrackerPanel) - .build(); + goalTrackerPanel.home(); - clientToolbar.addNavigation(uiNavigationButton); - }); + final AsyncBufferedImage icon = itemManager.getImage(ItemID.TODO_LIST); + if (icon == null) + { + log.warn("GoalTrackerV2Plugin: icon was null; skipping sidebar button creation"); + } + else + { + icon.onLoaded(() -> { + uiNavigationButton = NavigationButton.builder() + .tooltip("Goal Tracker") + .icon(icon) + .priority(7) + .panel(goalTrackerPanel) + .build(); + + clientToolbar.addNavigation(uiNavigationButton); + }); + } goalTrackerPanel.onGoalUpdated((goal) -> goalManager.save()); goalTrackerPanel.onTaskAdded((task) -> { @@ -151,14 +250,22 @@ protected void startUp() @Override protected void shutDown() { - clientToolbar.removeNavigation(uiNavigationButton); + if (uiNavigationButton != null) + { + clientToolbar.removeNavigation(uiNavigationButton); + uiNavigationButton = null; + } } @Subscribe public void onSessionOpen(SessionOpen event) { - goalManager.load(); - goalTrackerPanel.refresh(); + if (goalManager != null) { + try { goalManager.load(); } catch (Exception ex) { log.error("Failed to load goals on session open", ex); } + } + if (goalTrackerPanel != null) { + goalTrackerPanel.refresh(); + } } @Subscribe @@ -167,164 +274,223 @@ public void onStatChanged(StatChanged event) List skillLevelTasks = goalManager.getIncompleteTasksByType(TaskType.SKILL_LEVEL); for (SkillLevelTask task : skillLevelTasks) { if (!taskUpdateService.update(task, event)) continue; - - if (task.getStatus().isCompleted()) { - notifyTask(task); - } - - uiStatusManager.refresh(task); - this.goalManager.save(); - } - - List skillXpTasks = goalManager.getIncompleteTasksByType(TaskType.SKILL_XP); - for (SkillXpTask task : skillXpTasks) { - if (!taskUpdateService.update(task, event)) continue; - - if (task.getStatus().isCompleted()) { - notifyTask(task); - } - - uiStatusManager.refresh(task); - this.goalManager.save(); } } - public void notifyTask(Task task) + @Subscribe + public void onGameStateChanged(GameStateChanged event) { - if (client.getGameState() != GameState.LOGGED_IN || task.isNotified()) return; - - log.debug("Notify: [Goal Tracker] You have completed a task: " + task + "!"); - - String message = "[Goal Tracker] You have completed a task: " + task + "!"; - String formattedMessage = ColorUtil.wrapWithColorTag(message, config.completionMessageColor()); - client.addChatMessage(ChatMessageType.GAMEMESSAGE, "", formattedMessage, null); - - task.setNotified(true); - } + if (event.getGameState() == GameState.LOGGED_IN) + { + // Re-check quest tasks after login + clientThread.invokeLater(() -> refreshQuestTasks()); - public void warmItemIcons() - { - // Iterate all tasks and ensure item icons are requested so they appear without opening search - goalManager.getGoals().forEach(goal -> { - if (goal.getTasks() == null) return; - goal.getTasks().forEach(task -> { - if (task instanceof ItemTask) - { - try { - taskIconService.get((ItemTask) task); - } catch (Exception ignored) { } - } - }); - }); - // Refresh UI after warming to repaint icons - javax.swing.SwingUtilities.invokeLater(goalTrackerPanel::refresh); + // Refresh the panel once, 10s after login, after detection settles + schedulePanelRefresh(10_000); + } } @Subscribe - public void onGameStateChanged(GameStateChanged event) + public void onVarbitChanged(VarbitChanged event) { - if (client.getGameState() != GameState.LOGGED_IN) { - warmedIcons = false; - return; - } + // Quest progress often updates via varbits/varps + clientThread.invokeLater(() -> refreshQuestTasks()); - // redo the login check on the next game tick - validateAll = true; + // Debounce UI refresh during rapid quest varbit updates + schedulePanelRefresh(750); } @Subscribe - public void onGameTick(GameTick event) + public void onItemContainerChanged(ItemContainerChanged event) { - if (!validateAll) { - return; - } - if (client.getGameState() != GameState.LOGGED_IN) { + // Only react to the player's inventory/equipment/bank-like containers + if (!isPlayerInventoryContainer(event.getContainerId())) + { return; } - validateAll = false; - // perform a full refresh just once on login - // onGameStateChanged reports incorrect quest statuses, - // so this need to be done in this subscriber - goalTrackerPanel.refresh(); - - if (!warmedIcons) + List itemTasks = goalManager.getIncompleteTasksByType(TaskType.ITEM); + for (ItemTask task : itemTasks) { - warmItemIcons(); - warmedIcons = true; - } - - goalManager.getGoals().stream() - .flatMap(goal -> goal.getTasks().stream()) - .filter(task -> !task.getStatus().isCompleted()) - .forEach(task -> { - if (taskUpdateService.update(task)) { - if (task.getStatus().isCompleted()) { - notifyTask(task); - } - uiStatusManager.refresh(task); - } - }); - - List questTasks = goalManager.getIncompleteTasksByType(TaskType.QUEST); - for (QuestTask task : questTasks) { - if (!taskUpdateService.update(task)) continue; + final int itemId = task.getItemId(); + if (itemId <= 0) + { + continue; + } - if (task.getStatus().isCompleted()) { - notifyTask(task); + final int count = countHeldEquivalent(itemId, task.getItemName()); + final boolean changed = task.recomputeFromCount(count); + if (!changed) + { + continue; } uiStatusManager.refresh(task); + if (task.getStatus().isCompleted()) + { + notifyTask(task); + } } - goalManager.save(); - goalTrackerPanel.refresh(); + // Debounce panel refresh; coalesce multiple rapid inventory changes + schedulePanelRefresh(400); } - @Subscribe - public void onChatMessage(ChatMessage event) + private static boolean isPlayerInventoryContainer(int containerId) { - if (event.getType() != ChatMessageType.GAMEMESSAGE || !event.getMessage().contains("Quest complete")) return; - - List questTasks = goalManager.getIncompleteTasksByType(TaskType.QUEST); - for (QuestTask task : questTasks) { - if (!taskUpdateService.update(task)) continue; - - if (task.getStatus().isCompleted()) { - notifyTask(task); + for (int id : PLAYER_INVENTORIES) + { + if (id == containerId) + { + return true; } + } + return false; + } - uiStatusManager.refresh(task); - this.goalManager.save(); + private int countHeld(final int itemId) + { + int total = 0; + final ItemContainer inv = client.getItemContainer(InventoryID.INVENTORY); + if (inv != null) + { + for (Item i : inv.getItems()) + { + if (i != null && i.getId() == itemId) + { + total += Math.max(1, i.getQuantity()); + } + } + } + final ItemContainer equip = client.getItemContainer(InventoryID.EQUIPMENT); + if (equip != null) + { + for (Item i : equip.getItems()) + { + if (i != null && i.getId() == itemId) + { + total += Math.max(1, i.getQuantity()); + } + } + } + final ItemContainer bank = client.getItemContainer(InventoryID.BANK); + if (bank != null) + { + for (Item i : bank.getItems()) + { + if (i != null && i.getId() == itemId) + { + total += Math.max(1, i.getQuantity()); + } + } } - goalTrackerPanel.refresh(); + final ItemContainer seedVault = client.getItemContainer(InventoryID.SEED_VAULT); + if (seedVault != null) + { + for (Item i : seedVault.getItems()) + { + if (i != null && i.getId() == itemId) + { + total += Math.max(1, i.getQuantity()); + } + } + } + final ItemContainer groupStorage = client.getItemContainer(InventoryID.GROUP_STORAGE); + if (groupStorage != null) + { + for (Item i : groupStorage.getItems()) + { + if (i != null && i.getId() == itemId) + { + total += Math.max(1, i.getQuantity()); + } + } + } + return total; } - @Subscribe - public void onItemContainerChanged(ItemContainerChanged event) + private int countHeldEquivalent(final int targetItemId, final String targetItemName) { - if (IntStream.of(GoalTrackerPlugin.PLAYER_INVENTORIES).noneMatch((id) -> id == event.getContainerId())) return; + // Fallback to ID-only counting if we don't have a name + if (targetItemName == null || targetItemName.isEmpty() || itemManager == null) + { + return countHeld(targetItemId); + } - itemCache.update(event.getContainerId(), event.getItemContainer().getItems()); + final String baseName = normalizeBarrowsName(targetItemName); + int total = 0; - List itemTasks = goalManager.getIncompleteTasksByType(TaskType.ITEM); - for (ItemTask task : itemTasks) { - if (!taskUpdateService.update(task)) continue; + total += countContainerByName(InventoryID.INVENTORY, baseName); + total += countContainerByName(InventoryID.EQUIPMENT, baseName); + total += countContainerByName(InventoryID.BANK, baseName); + total += countContainerByName(InventoryID.SEED_VAULT, baseName); + total += countContainerByName(InventoryID.GROUP_STORAGE, baseName); - if (task.getStatus().isCompleted()) { - notifyTask(task); + // Include the exact base ID too, in case some pieces don't use numeric suffixes + total += countHeld(targetItemId); + return total; + } + + private int countContainerByName(final InventoryID id, final String baseName) + { + final ItemContainer c = client.getItemContainer(id); + if (c == null) + { + return 0; + } + int subtotal = 0; + for (Item i : c.getItems()) + { + if (i == null) + { + continue; + } + try + { + final String name = normalizeBarrowsName(itemManager.getItemComposition(i.getId()).getName()); + if (name.equals(baseName)) + { + subtotal += Math.max(1, i.getQuantity()); + } } + catch (Exception ignored) { } + } + return subtotal; + } - // always refresh item tasks, since the acquired - // count could have changed - uiStatusManager.refresh(task); - this.goalManager.save(); + /** + * Normalize Barrows item names so that degraded variants (e.g., "Torag's platelegs 100/75/50/25/0") + * all map to the same base string (e.g., "Torag's platelegs"). + */ + private static String normalizeBarrowsName(final String raw) + { + if (raw == null) + { + return ""; } + // Strip trailing space+digits (e.g., " 100") and collapse double spaces + String s = raw.replaceAll("\\s[0-9]{1,3}$", "").trim(); + s = s.replaceAll("\\s+", " "); + return s; } + private void refreshQuestTasks() + { + if (goalManager == null || client == null) return; + List questTasks = goalManager.getIncompleteTasksByType(TaskType.QUEST); + for (QuestTask task : questTasks) + { + task.refreshStatus(client); + uiStatusManager.refresh(task); + if (task.getStatus().isCompleted()) + { + notifyTask(task); + } + } + } @Provides - GoalTrackerConfig getGoalTrackerConfig(ConfigManager configManager) + public GoalTrackerConfig provideConfig(ConfigManager configManager) { return configManager.getConfig(GoalTrackerConfig.class); } -} +} \ No newline at end of file diff --git a/src/main/java/com/toofifty/goaltracker/ItemCache.java b/src/main/java/com/toofifty/goaltracker/ItemCache.java index 1f7ccf8..3833178 100644 --- a/src/main/java/com/toofifty/goaltracker/ItemCache.java +++ b/src/main/java/com/toofifty/goaltracker/ItemCache.java @@ -2,16 +2,21 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; -import java.util.HashMap; -import java.util.Map; -import javax.inject.Inject; -import javax.inject.Singleton; import net.runelite.api.Client; import net.runelite.api.Item; import net.runelite.client.game.ItemManager; +import javax.inject.Inject; +import javax.inject.Singleton; +import java.util.HashMap; +import java.util.Map; + @Singleton -public class ItemCache +/** + * Tracks player inventory contents across sessions. + * Persists item totals and note-link mappings for offline use. + */ +public final class ItemCache { private Map inventories; private Map itemTotals; diff --git a/src/main/java/com/toofifty/goaltracker/TaskUIStatusManager.java b/src/main/java/com/toofifty/goaltracker/TaskUIStatusManager.java index bb743a6..ae7e881 100644 --- a/src/main/java/com/toofifty/goaltracker/TaskUIStatusManager.java +++ b/src/main/java/com/toofifty/goaltracker/TaskUIStatusManager.java @@ -11,7 +11,11 @@ @Slf4j @Singleton -public class TaskUIStatusManager +/** + * Keeps track of UI refresh callbacks for tasks and goals. + * Ensures updates run on the Swing event dispatch thread. + */ +public final class TaskUIStatusManager { private final Map taskRefreshers = new HashMap<>(); private final Map goalRefreshers = new HashMap<>(); diff --git a/src/main/java/com/toofifty/goaltracker/adapters/QuestAdapter.java b/src/main/java/com/toofifty/goaltracker/adapters/QuestAdapter.java index cb5f375..133c2da 100644 --- a/src/main/java/com/toofifty/goaltracker/adapters/QuestAdapter.java +++ b/src/main/java/com/toofifty/goaltracker/adapters/QuestAdapter.java @@ -6,7 +6,11 @@ import java.lang.reflect.Type; import java.util.Arrays; -public class QuestAdapter implements JsonSerializer, JsonDeserializer { +/** + * Gson adapter for RuneLite Quests. + * Serializes/deserializes quests by their unique ID. + */ +public final class QuestAdapter implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(Quest src, Type typeOfSrc, JsonSerializationContext context) { diff --git a/src/main/java/com/toofifty/goaltracker/adapters/SkillAdapter.java b/src/main/java/com/toofifty/goaltracker/adapters/SkillAdapter.java index 0e801f1..d3ff145 100644 --- a/src/main/java/com/toofifty/goaltracker/adapters/SkillAdapter.java +++ b/src/main/java/com/toofifty/goaltracker/adapters/SkillAdapter.java @@ -6,7 +6,11 @@ import java.lang.reflect.Type; import java.util.Arrays; -public class SkillAdapter implements JsonSerializer, JsonDeserializer { +/** + * Gson adapter for RuneLite Skills. + * Serializes/deserializes skills by their display name. + */ +public final class SkillAdapter implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(Skill src, Type typeOfSrc, JsonSerializationContext context) { return context.serialize(src.getName()); diff --git a/src/main/java/com/toofifty/goaltracker/adapters/TaskAdapter.java b/src/main/java/com/toofifty/goaltracker/adapters/TaskAdapter.java index d17c9f0..94b74eb 100644 --- a/src/main/java/com/toofifty/goaltracker/adapters/TaskAdapter.java +++ b/src/main/java/com/toofifty/goaltracker/adapters/TaskAdapter.java @@ -7,8 +7,12 @@ import java.lang.reflect.Type; +/** + * Gson adapter for polymorphic Task serialization. + * Adds a "type" field and deserializes into the correct Task subclass. + */ @Slf4j -public class TaskAdapter implements JsonSerializer, JsonDeserializer { +public final class TaskAdapter implements JsonSerializer, JsonDeserializer { @Override public JsonElement serialize(Task src, Type typeOfSrc, JsonSerializationContext context) { @@ -19,7 +23,7 @@ public JsonElement serialize(Task src, Type typeOfSrc, JsonSerializationContext @Override public Task deserialize(JsonElement json, Type typeOfT, - JsonDeserializationContext context) throws JsonParseException { + JsonDeserializationContext context) throws JsonParseException { TaskType taskType = TaskType.fromString(json.getAsJsonObject().get("type").getAsString()); switch (taskType) { case SKILL_XP: diff --git a/src/main/java/com/toofifty/goaltracker/models/ActionHistory.java b/src/main/java/com/toofifty/goaltracker/models/ActionHistory.java index d1ec2c8..ede8fc4 100644 --- a/src/main/java/com/toofifty/goaltracker/models/ActionHistory.java +++ b/src/main/java/com/toofifty/goaltracker/models/ActionHistory.java @@ -2,16 +2,13 @@ import java.util.ArrayDeque; import java.util.Deque; +import java.util.Objects; /** - * Generic history manager for undo/redo of user actions. - * - * Usage: - * - Define actions implementing {@link ActionHistory.Action} - * - Call {@link #push(Action)} whenever an action is performed - * - Call {@link #undo()} or {@link #redo()} from UI buttons + * Simple undo/redo stack for user actions. + * Push new actions with {@link #push(Action)}; call {@link #undo()} / {@link #redo()} from UI. */ -public class ActionHistory +public final class ActionHistory { public interface Action { @@ -22,9 +19,10 @@ public interface Action private final Deque undoStack = new ArrayDeque<>(); private final Deque redoStack = new ArrayDeque<>(); - /** Push a new action, clearing the redo history. */ - public void push(Action action) + /** Push a new action and clear the redo history. */ + public void push(final Action action) { + Objects.requireNonNull(action, "action"); undoStack.addLast(action); redoStack.clear(); } @@ -32,22 +30,22 @@ public void push(Action action) /** Undo the most recent action, if any. */ public void undo() { - Action a = undoStack.pollLast(); - if (a != null) + final Action action = undoStack.pollLast(); + if (action != null) { - a.undo(); - redoStack.addLast(a); + action.undo(); + redoStack.addLast(action); } } /** Redo the most recently undone action, if any. */ public void redo() { - Action a = redoStack.pollLast(); - if (a != null) + final Action action = redoStack.pollLast(); + if (action != null) { - a.redo(); - undoStack.addLast(a); + action.redo(); + undoStack.addLast(action); } } @@ -61,6 +59,7 @@ public boolean hasRedo() return !redoStack.isEmpty(); } + /** Clear both undo and redo history. */ public void clear() { undoStack.clear(); diff --git a/src/main/java/com/toofifty/goaltracker/models/Goal.java b/src/main/java/com/toofifty/goaltracker/models/Goal.java index dcdf50d..bcfcf66 100644 --- a/src/main/java/com/toofifty/goaltracker/models/Goal.java +++ b/src/main/java/com/toofifty/goaltracker/models/Goal.java @@ -1,9 +1,9 @@ package com.toofifty.goaltracker.models; import com.google.gson.annotations.SerializedName; -import com.toofifty.goaltracker.utils.ReorderableList; import com.toofifty.goaltracker.models.enums.Status; import com.toofifty.goaltracker.models.task.Task; +import com.toofifty.goaltracker.utils.ReorderableList; import lombok.Builder; import lombok.Getter; import lombok.Setter; @@ -14,10 +14,14 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +/** + * Represents a player goal containing one or more tasks. + * Provides helpers for status aggregation and task updates. + */ @Setter @Getter @SuperBuilder -public class Goal +public final class Goal { @Builder.Default private String description = "New goal"; @@ -34,35 +38,47 @@ public class Goal private List filterBy(Predicate predicate) { - return this.getTasks().stream().filter(predicate).collect(Collectors.toList()); + return tasks.stream().filter(predicate).collect(Collectors.toList()); } - public boolean isStatus(Status status) { - return this.getTasks().stream().allMatch((task) -> task.getStatus() == status); + /** True if all tasks are of the given status. */ + public boolean isStatus(Status status) + { + return tasks.stream().allMatch(task -> task.getStatus() == status); } - public boolean isAnyStatus(Status ...statuses) { - return this.getTasks().stream().anyMatch((task) -> Arrays.stream(statuses).anyMatch((status) -> status == task.getStatus())); + /** True if any task matches one of the given statuses. */ + public boolean isAnyStatus(Status... statuses) + { + return tasks.stream().anyMatch(task -> + Arrays.stream(statuses).anyMatch(s -> s == task.getStatus())); } - public List getComplete() { - return this.filterBy(Task::isDone); + /** List of completed tasks. */ + public List getComplete() + { + return filterBy(Task::isDone); } - public Status getStatus() { - if (this.isStatus(Status.COMPLETED)) { + /** Aggregated status of this goal. */ + public Status getStatus() + { + if (isStatus(Status.COMPLETED)) + { return Status.COMPLETED; } - - if (this.isAnyStatus(Status.IN_PROGRESS, Status.COMPLETED)) { + if (isAnyStatus(Status.IN_PROGRESS, Status.COMPLETED)) + { return Status.IN_PROGRESS; } - return Status.NOT_STARTED; } - public void setAllTasksCompleted(boolean completed) { - for (Task task : tasks) { + /** Mark all tasks as complete or not started. */ + public void setAllTasksCompleted(boolean completed) + { + for (Task task : tasks) + { task.setStatus(completed ? Status.COMPLETED : Status.NOT_STARTED); } } diff --git a/src/main/java/com/toofifty/goaltracker/models/IndentChangeAction.java b/src/main/java/com/toofifty/goaltracker/models/IndentChangeAction.java index 357414e..d4da20a 100644 --- a/src/main/java/com/toofifty/goaltracker/models/IndentChangeAction.java +++ b/src/main/java/com/toofifty/goaltracker/models/IndentChangeAction.java @@ -1,12 +1,14 @@ package com.toofifty.goaltracker.models; -import java.util.List; import com.toofifty.goaltracker.models.task.Task; +import java.util.List; + /** * Action for indenting or outdenting a task in a list. + * Stores old and new indent levels for undo/redo. */ -public class IndentChangeAction implements ActionHistory.Action +public final class IndentChangeAction implements ActionHistory.Action { private final List tasks; private final Task task; diff --git a/src/main/java/com/toofifty/goaltracker/models/RemoveTaskAction.java b/src/main/java/com/toofifty/goaltracker/models/RemoveTaskAction.java index f1138bd..24a57e1 100644 --- a/src/main/java/com/toofifty/goaltracker/models/RemoveTaskAction.java +++ b/src/main/java/com/toofifty/goaltracker/models/RemoveTaskAction.java @@ -6,8 +6,9 @@ /** * Action for removing a task from a list. + * Stores the original index for correct undo placement. */ -public class RemoveTaskAction implements ActionHistory.Action +public final class RemoveTaskAction implements ActionHistory.Action { private final List tasks; private final Task task; diff --git a/src/main/java/com/toofifty/goaltracker/models/ReorderTaskAction.java b/src/main/java/com/toofifty/goaltracker/models/ReorderTaskAction.java index 6f7c50f..1211518 100644 --- a/src/main/java/com/toofifty/goaltracker/models/ReorderTaskAction.java +++ b/src/main/java/com/toofifty/goaltracker/models/ReorderTaskAction.java @@ -6,8 +6,9 @@ /** * Action for reordering a task within a list (move up/down). + * Stores old and new indices for undo/redo. */ -public class ReorderTaskAction implements ActionHistory.Action +public final class ReorderTaskAction implements ActionHistory.Action { private final List tasks; private final Task task; diff --git a/src/main/java/com/toofifty/goaltracker/models/ToggleCompleteAction.java b/src/main/java/com/toofifty/goaltracker/models/ToggleCompleteAction.java index 8416722..d608794 100644 --- a/src/main/java/com/toofifty/goaltracker/models/ToggleCompleteAction.java +++ b/src/main/java/com/toofifty/goaltracker/models/ToggleCompleteAction.java @@ -1,12 +1,14 @@ package com.toofifty.goaltracker.models; import com.toofifty.goaltracker.models.task.Task; + import java.lang.reflect.Method; /** * Action for toggling a task's completion state. + * Uses reflection to avoid a hard dependency on the Status enum. */ -public class ToggleCompleteAction implements ActionHistory.Action +public final class ToggleCompleteAction implements ActionHistory.Action { private final Task task; private final boolean oldValue; diff --git a/src/main/java/com/toofifty/goaltracker/models/UndoStack.java b/src/main/java/com/toofifty/goaltracker/models/UndoStack.java index 7a561be..d5df322 100644 --- a/src/main/java/com/toofifty/goaltracker/models/UndoStack.java +++ b/src/main/java/com/toofifty/goaltracker/models/UndoStack.java @@ -6,14 +6,7 @@ /** * Generic undo/redo stack for removed items (e.g., Goals, Tasks). - * - * Flow: - * - When an item is removed from a list, call {@link #pushRemove(Object, int)}. - * This records the item and its original index, and clears the redo stack. - * - To undo the removal: call {@link #popForUndo()} and re-insert the item at the - * returned index. The entry is moved to the redo stack so it can be re-applied. - * - To redo the removal (only valid after an undo): call {@link #popForRedo()} and - * remove that item again. The entry moves back to the undo stack. + * Stores the removed item and its original index for correct restoration. */ public final class UndoStack { diff --git a/src/main/java/com/toofifty/goaltracker/models/enums/Status.java b/src/main/java/com/toofifty/goaltracker/models/enums/Status.java index c24f3e8..bef80a6 100644 --- a/src/main/java/com/toofifty/goaltracker/models/enums/Status.java +++ b/src/main/java/com/toofifty/goaltracker/models/enums/Status.java @@ -5,6 +5,10 @@ import lombok.Getter; import net.runelite.api.QuestState; +/** + * Generic task/goal status enum (not_started, in_progress, completed). + * Provides mapping from RuneLite QuestState and convenience checks. + */ @Getter @AllArgsConstructor public enum Status diff --git a/src/main/java/com/toofifty/goaltracker/models/enums/TaskType.java b/src/main/java/com/toofifty/goaltracker/models/enums/TaskType.java index bd8b504..021768b 100644 --- a/src/main/java/com/toofifty/goaltracker/models/enums/TaskType.java +++ b/src/main/java/com/toofifty/goaltracker/models/enums/TaskType.java @@ -6,6 +6,10 @@ @Getter @AllArgsConstructor +/** + * Enumeration of supported task types for goals. + * Used in JSON serialization and deserialization (manual, skill, quest, item). + */ public enum TaskType { @SerializedName("manual") diff --git a/src/main/java/com/toofifty/goaltracker/models/task/ItemTask.java b/src/main/java/com/toofifty/goaltracker/models/task/ItemTask.java index 448481f..0ea17b4 100644 --- a/src/main/java/com/toofifty/goaltracker/models/task/ItemTask.java +++ b/src/main/java/com/toofifty/goaltracker/models/task/ItemTask.java @@ -1,5 +1,7 @@ package com.toofifty.goaltracker.models.task; +import com.google.gson.annotations.SerializedName; +import com.toofifty.goaltracker.models.enums.Status; import com.toofifty.goaltracker.models.enums.TaskType; import lombok.Builder; import lombok.Getter; @@ -11,16 +13,52 @@ @Setter @Getter @SuperBuilder -public class -ItemTask extends Task +/** + * Task representing acquisition of an item. + * Tracks item ID, name, desired quantity, acquired count, and cached icon. + */ +public final class ItemTask extends Task { private transient BufferedImage cachedIcon; - private int quantity; @Builder.Default + @SerializedName("quantity") + private int quantity = 1; + @Builder.Default + @SerializedName("acquired") private int acquired = 0; + @SerializedName("item_id") private int itemId; + @SerializedName("item_name") private String itemName; + /** + * Recompute acquired count and task status from a current item count. + * @param count how many of this item are currently held (inventory/equipment/bank, as defined by the caller) + * @return true if either acquired or status changed; false otherwise + */ + public boolean recomputeFromCount(final int count) + { + final int oldAcquired = this.acquired; + final Status oldStatus = getStatus(); + + this.acquired = Math.max(0, Math.min(count, Math.max(1, quantity))); + + if (this.acquired >= this.quantity) + { + setStatus(Status.COMPLETED); + } + else if (this.acquired > 0) + { + setStatus(Status.IN_PROGRESS); + } + else + { + setStatus(Status.NOT_STARTED); + } + + return oldAcquired != this.acquired || oldStatus != getStatus(); + } + @Override public String toString() { @@ -35,6 +73,12 @@ public String toString() return String.format("%d x %s", quantity, itemName); } + @Override + public String getDisplayName() + { + return itemName; + } + @Override public TaskType getType() { diff --git a/src/main/java/com/toofifty/goaltracker/models/task/ManualTask.java b/src/main/java/com/toofifty/goaltracker/models/task/ManualTask.java index ce177cb..cb32b9b 100644 --- a/src/main/java/com/toofifty/goaltracker/models/task/ManualTask.java +++ b/src/main/java/com/toofifty/goaltracker/models/task/ManualTask.java @@ -9,7 +9,11 @@ @Setter @Getter @SuperBuilder -public class ManualTask extends Task +/** + * Task representing a manually tracked entry (checklist-style). + * Toggled complete/incomplete by the user. + */ +public final class ManualTask extends Task { private String description; @@ -24,6 +28,12 @@ public String toString() return description; } + @Override + public String getDisplayName() + { + return description; + } + @Override public TaskType getType() { diff --git a/src/main/java/com/toofifty/goaltracker/models/task/QuestTask.java b/src/main/java/com/toofifty/goaltracker/models/task/QuestTask.java index f911ec5..35fb28d 100644 --- a/src/main/java/com/toofifty/goaltracker/models/task/QuestTask.java +++ b/src/main/java/com/toofifty/goaltracker/models/task/QuestTask.java @@ -1,26 +1,74 @@ package com.toofifty.goaltracker.models.task; import com.google.gson.annotations.SerializedName; +import com.toofifty.goaltracker.models.enums.Status; import com.toofifty.goaltracker.models.enums.TaskType; import lombok.Getter; import lombok.Setter; import lombok.experimental.SuperBuilder; +import net.runelite.api.Client; import net.runelite.api.Quest; +import net.runelite.api.QuestState; +/** + * Task representing completion of a quest. + * Stores the RuneLite Quest reference and returns its name as label. + */ @Getter @Setter @SuperBuilder -public class QuestTask extends Task +public final class QuestTask extends Task { @SerializedName("quest_id") private Quest quest; + /** + * Map RuneLite's QuestState to this plugin's Task.Status. + */ + private static Status mapQuestState(final QuestState state) + { + if (state == null) + { + return Status.NOT_STARTED; + } + switch (state) + { + case FINISHED: + return Status.COMPLETED; + case IN_PROGRESS: + return Status.IN_PROGRESS; + case NOT_STARTED: + default: + return Status.NOT_STARTED; + } + } + + /** + * Re-evaluate this quest's completion status from the live client and update the Task.status field. + * Safe to call on login and whenever relevant varbits/varps change. + */ + public void refreshStatus(final Client client) + { + if (client == null || quest == null) + { + return; + } + final QuestState qs = quest.getState(client); + setStatus(mapQuestState(qs)); + } + @Override public String toString() { return quest.getName(); } + @Override + public String getDisplayName() + { + return quest.getName(); + } + @Override public TaskType getType() { diff --git a/src/main/java/com/toofifty/goaltracker/models/task/SkillLevelTask.java b/src/main/java/com/toofifty/goaltracker/models/task/SkillLevelTask.java index 4aefb48..c8d07f3 100644 --- a/src/main/java/com/toofifty/goaltracker/models/task/SkillLevelTask.java +++ b/src/main/java/com/toofifty/goaltracker/models/task/SkillLevelTask.java @@ -9,7 +9,11 @@ @Setter @Getter @SuperBuilder -public class SkillLevelTask extends Task +/** + * Task representing reaching a target level in a skill. + * Stores the RuneLite Skill and required level. + */ +public final class SkillLevelTask extends Task { private Skill skill; private int level; @@ -20,6 +24,12 @@ public String toString() return String.format("%s %s", level, skill.getName()); } + @Override + public String getDisplayName() + { + return String.format("Reach level %d %s", level, skill.getName()); + } + @Override public TaskType getType() { diff --git a/src/main/java/com/toofifty/goaltracker/models/task/SkillXpTask.java b/src/main/java/com/toofifty/goaltracker/models/task/SkillXpTask.java index f5162f7..d9f1318 100644 --- a/src/main/java/com/toofifty/goaltracker/models/task/SkillXpTask.java +++ b/src/main/java/com/toofifty/goaltracker/models/task/SkillXpTask.java @@ -9,7 +9,11 @@ @Getter @Setter @SuperBuilder -public class SkillXpTask extends Task +/** + * Task representing earning a target XP amount in a skill. + * Stores the RuneLite Skill and required XP. + */ +public final class SkillXpTask extends Task { private Skill skill; private int xp; @@ -20,6 +24,12 @@ public String toString() return String.format("%d %s XP", xp, skill.getName()); } + @Override + public String getDisplayName() + { + return String.format("%d %s XP", xp, skill.getName()); + } + @Override public TaskType getType() { diff --git a/src/main/java/com/toofifty/goaltracker/models/task/Task.java b/src/main/java/com/toofifty/goaltracker/models/task/Task.java index 0402a72..3f9204d 100644 --- a/src/main/java/com/toofifty/goaltracker/models/task/Task.java +++ b/src/main/java/com/toofifty/goaltracker/models/task/Task.java @@ -11,10 +11,14 @@ @Getter @Setter @SuperBuilder +/** + * Base class for all goal tasks. + * Provides status tracking, indent level management, and type identification. + */ public abstract class Task { @Builder.Default - @SerializedName("previous_result") + @SerializedName(value = "status", alternate = {"previous_result"}) private Status status = Status.NOT_STARTED; @Builder.Default @@ -57,6 +61,11 @@ public boolean isNotFullyIndented() { return !isFullyIndented(); } + /** + * Returns a human-readable name for the task. + */ + public abstract String getDisplayName(); + @Override abstract public String toString(); diff --git a/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java b/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java index 0a74187..bdeeb4c 100644 --- a/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java +++ b/src/main/java/com/toofifty/goaltracker/presets/GoalPresetRepository.java @@ -34,477 +34,16 @@ public Preset(String name, String description, List goals) { public static List getAll() { List list = new ArrayList<>(); - // list.add(buildLadlorIronman()); - // list.add(buildLadlorMelee()); - // list.add(buildLadlorRanged()); - // list.add(buildLadlorMagic()); - // list.add(buildLadlorUtility()); - // list.add(buildLadlorVoidSet()); - // list.add(buildLadlorGodCapes()); - // list.add(buildLadlorCrystalBowfa()); - // list.add(buildLadlorJewelryBoots()); - // list.add(buildLadlorSlayerUndead()); - // list.add(buildLadlorImbuedRings()); - // list.add(buildLadlorGwdArmor()); - // list.add(buildLadlorEliteVoid()); - // list.add(buildLadlorPrayerScrolls()); - // list.add(buildLadlorRaidsUniques()); - // list.add(buildLadlorMilestones()); list.add(buildEarlyIronman()); list.add(buildMidIronman()); list.add(buildLateIronman()); list.add(buildFullVoidArmor()); list.add(buildFastTravelUnlocks()); list.add(buildFreeToPlayQuests()); + list.add(buildAllBarrowsGear()); return list; } - /* - private static Preset buildLadlorIronman() { - // Melee lane - Goal melee = Goal.builder() - .description("Ladlor — Melee Progression") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(DRAGON_SCIMITAR).itemName("Dragon scimitar").quantity(1).build(), - ItemTask.builder().itemId(DRAGON_DEFENDER).itemName("Dragon defender").quantity(1).build(), - ItemTask.builder().itemId(FIGHTER_TORSO).itemName("Fighter torso").quantity(1).build(), - ItemTask.builder().itemId(DRAGON_BOOTS).itemName("Dragon boots").quantity(1).build(), - ItemTask.builder().itemId(ABYSSAL_WHIP).itemName("Abyssal whip").quantity(1).build(), - ItemTask.builder().itemId(BERSERKER_RING).itemName("Berserker ring").quantity(1).build(), - ItemTask.builder().itemId(FIRE_CAPE).itemName("Fire cape").quantity(1).build(), - ItemTask.builder().itemId(FEROCIOUS_GLOVES).itemName("Ferocious gloves").quantity(1).build(), - ItemTask.builder().itemId(ABYSSAL_TENTACLE).itemName("Abyssal tentacle").quantity(1).build() - )) - .build(); - - // Ranged lane - Goal ranged = Goal.builder() - .description("Ladlor — Ranged Progression") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(MAGIC_SHORTBOW).itemName("Magic shortbow").quantity(1).build(), - ItemTask.builder().itemId(RUNE_CROSSBOW).itemName("Rune crossbow").quantity(1).build(), - ItemTask.builder().itemId(BLACK_DHIDE_BODY).itemName("Black d'hide body").quantity(1).build(), - ItemTask.builder().itemId(AVAS_ACCUMULATOR).itemName("Ava's accumulator").quantity(1).build(), - ItemTask.builder().itemId(KARILS_LEATHERTOP).itemName("Karil's leathertop").quantity(1).build(), - ItemTask.builder().itemId(KARILS_LEATHERSKIRT).itemName("Karil's leatherskirt").quantity(1).build(), - ItemTask.builder().itemId(TOXIC_BLOWPIPE).itemName("Toxic blowpipe").quantity(1).build(), - ItemTask.builder().itemId(ARMADYL_CROSSBOW).itemName("Armadyl crossbow").quantity(1).build(), - ItemTask.builder().itemId(AVAS_ASSEMBLER).itemName("Ava's assembler").quantity(1).build() - )) - .build(); - - // Magic lane - Goal magic = Goal.builder() - .description("Ladlor — Magic Progression") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(MYSTIC_ROBE_TOP).itemName("Mystic robe top").quantity(1).build(), - ItemTask.builder().itemId(MYSTIC_ROBE_BOTTOM).itemName("Mystic robe bottom").quantity(1).build(), - ItemTask.builder().itemId(OCCULT_NECKLACE).itemName("Occult necklace").quantity(1).build(), - ItemTask.builder().itemId(MAGES_BOOK).itemName("Mage's book").quantity(1).build(), - ItemTask.builder().itemId(TRIDENT_OF_THE_SEAS).itemName("Trident of the seas").quantity(1).build(), - ItemTask.builder().itemId(UNCHARGED_TOXIC_TRIDENT).itemName("Trident of the swamp").quantity(1).build(), - ItemTask.builder().itemId(ANCESTRAL_ROBE_TOP).itemName("Ancestral robe top").quantity(1).build(), - ItemTask.builder().itemId(ANCESTRAL_ROBE_BOTTOM).itemName("Ancestral robe bottom").quantity(1).build(), - ItemTask.builder().itemId(ANCESTRAL_HAT).itemName("Ancestral hat").quantity(1).build() - )) - .build(); - - // Utility / Unlocks lane - Goal utility = Goal.builder() - .description("Ladlor — Utility & Unlocks") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(GRACEFUL_HOOD).itemName("Graceful hood").quantity(1).build(), - ItemTask.builder().itemId(GRACEFUL_TOP).itemName("Graceful top").quantity(1).build(), - ItemTask.builder().itemId(GRACEFUL_LEGS).itemName("Graceful legs").quantity(1).build(), - ItemTask.builder().itemId(GRACEFUL_GLOVES).itemName("Graceful gloves").quantity(1).build(), - ItemTask.builder().itemId(GRACEFUL_BOOTS).itemName("Graceful boots").quantity(1).build(), - ItemTask.builder().itemId(GRACEFUL_CAPE).itemName("Graceful cape").quantity(1).build(), - ItemTask.builder().itemId(BARROWS_GLOVES).itemName("Barrows gloves").quantity(1).build() - )) - .build(); - - // Void set lane - Goal voidSet = Goal.builder() - .description("Ladlor — Void Set") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(VOID_KNIGHT_TOP).itemName("Void knight top").quantity(1).build(), - ItemTask.builder().itemId(VOID_KNIGHT_ROBE).itemName("Void knight robe").quantity(1).build(), - ItemTask.builder().itemId(VOID_KNIGHT_GLOVES).itemName("Void knight gloves").quantity(1).build(), - ItemTask.builder().itemId(VOID_MAGE_HELM).itemName("Void mage helm").quantity(1).build(), - ItemTask.builder().itemId(VOID_RANGER_HELM).itemName("Void ranger helm").quantity(1).build(), - ItemTask.builder().itemId(VOID_MELEE_HELM).itemName("Void melee helm").quantity(1).build() - )) - .build(); - - // God capes imbued lane - Goal godCapes = Goal.builder() - .description("Ladlor — Imbued God Capes") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(IMBUED_SARADOMIN_CAPE).itemName("Imbued Saradomin cape").quantity(1).build(), - ItemTask.builder().itemId(IMBUED_GUTHIX_CAPE).itemName("Imbued Guthix cape").quantity(1).build(), - ItemTask.builder().itemId(IMBUED_ZAMORAK_CAPE).itemName("Imbued Zamorak cape").quantity(1).build() - )) - .build(); - - // Crystal / Bofa lane - Goal crystal = Goal.builder() - .description("Ladlor — Crystal & Bowfa") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(CRYSTAL_HELM).itemName("Crystal helm").quantity(1).build(), - ItemTask.builder().itemId(CRYSTAL_BODY).itemName("Crystal body").quantity(1).build(), - ItemTask.builder().itemId(CRYSTAL_LEGS).itemName("Crystal legs").quantity(1).build(), - ItemTask.builder().itemId(BOW_OF_FAERDHINEN).itemName("Bow of Faerdhinen").quantity(1).build() - )) - .build(); - - // Jewelry / Boots upgrades lane - Goal jewelry = Goal.builder() - .description("Ladlor — Jewelry & Boots Upgrades") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(AMULET_OF_TORTURE).itemName("Amulet of torture").quantity(1).build(), - ItemTask.builder().itemId(NECKLACE_OF_ANGUISH).itemName("Necklace of anguish").quantity(1).build(), - ItemTask.builder().itemId(PEGASIAN_BOOTS).itemName("Pegasian boots").quantity(1).build(), - ItemTask.builder().itemId(RANGER_BOOTS).itemName("Ranger boots").quantity(1).build(), - ItemTask.builder().itemId(PRIMORDIAL_BOOTS).itemName("Primordial boots").quantity(1).build(), - ItemTask.builder().itemId(ETERNAL_BOOTS).itemName("Eternal boots").quantity(1).build() - )) - .build(); - - // Slayer & Undead lane - Goal slayerUndead = Goal.builder() - .description("Ladlor — Slayer & Undead") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(BLACK_MASK).itemName("Black mask").quantity(1).build(), - ItemTask.builder().itemId(SLAYER_HELMET).itemName("Slayer helmet").quantity(1).build(), - ItemTask.builder().itemId(SLAYER_HELMET_I).itemName("Slayer helmet (i)").quantity(1).build(), - ItemTask.builder().itemId(SALVE_AMULET_E).itemName("Salve amulet (e)").quantity(1).build(), - ItemTask.builder().itemId(SALVE_AMULET_EI).itemName("Salve amulet (ei)").quantity(1).build(), - ItemTask.builder().itemId(IMBUED_HEART).itemName("Imbued heart").quantity(1).build(), - ItemTask.builder().itemId(RUNE_POUCH).itemName("Rune pouch").quantity(1).build(), - ItemTask.builder().itemId(RING_OF_SUFFERING).itemName("Ring of suffering").quantity(1).build(), - ItemTask.builder().itemId(RING_OF_SUFFERING_I).itemName("Ring of suffering (i)").quantity(1).build() - )) - .build(); - - // Imbued Rings lane - Goal imbuedRings = Goal.builder() - .description("Ladlor — Imbued Rings") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(BERSERKER_RING_I).itemName("Berserker ring (i)").quantity(1).build(), - ItemTask.builder().itemId(ARCHERS_RING_I).itemName("Archer's ring (i)").quantity(1).build(), - ItemTask.builder().itemId(SEERS_RING_I).itemName("Seers ring (i)").quantity(1).build(), - ItemTask.builder().itemId(WARRIOR_RING_I).itemName("Warrior ring (i)").quantity(1).build() - )) - .build(); - - // God Wars Dungeon Armor lane - Goal gwdArmor = Goal.builder() - .description("Ladlor — God Wars Armor") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(BANDOS_CHESTPLATE).itemName("Bandos chestplate").quantity(1).build(), - ItemTask.builder().itemId(BANDOS_TASSETS).itemName("Bandos tassets").quantity(1).build(), - ItemTask.builder().itemId(ARMADYL_CHESTPLATE).itemName("Armadyl chestplate").quantity(1).build(), - ItemTask.builder().itemId(ARMADYL_CHAINSKIRT).itemName("Armadyl chainskirt").quantity(1).build() - )) - .build(); - - // Elite Void upgrades lane - Goal eliteVoid = Goal.builder() - .description("Ladlor — Elite Void") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(ELITE_VOID_TOP).itemName("Elite void top").quantity(1).build(), - ItemTask.builder().itemId(ELITE_VOID_ROBE).itemName("Elite void robe").quantity(1).build() - )) - .build(); - - // Prayer scrolls lane (Rigour & Augury) - Goal prayerScrolls = Goal.builder() - .description("Ladlor — Prayer Scrolls") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(DEXTEROUS_PRAYER_SCROLL).itemName("Dexterous prayer scroll (Rigour)").quantity(1).build(), - ItemTask.builder().itemId(ARCANE_PRAYER_SCROLL).itemName("Arcane prayer scroll (Augury)").quantity(1).build() - )) - .build(); - - // Raids uniques lane - Goal raidsUniques = Goal.builder() - .description("Ladlor — Raids Uniques") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(DRAGON_HUNTER_CROSSBOW).itemName("Dragon hunter crossbow").quantity(1).build(), - ItemTask.builder().itemId(TWISTED_BOW).itemName("Twisted bow").quantity(1).build(), - ItemTask.builder().itemId(SCYTHE_OF_VITUR).itemName("Scythe of Vitur").quantity(1).build(), - ItemTask.builder().itemId(GHRAZI_RAPIER).itemName("Ghrazi rapier").quantity(1).build(), - ItemTask.builder().itemId(OSMUMTENS_FANG).itemName("Osmumten's fang").quantity(1).build(), - ItemTask.builder().itemId(MASORI_BODY).itemName("Masori body").quantity(1).build(), - ItemTask.builder().itemId(MASORI_CHAPS).itemName("Masori chaps").quantity(1).build(), - ItemTask.builder().itemId(TUMEKENS_SHADOW).itemName("Tumeken's shadow").quantity(1).build() - )) - .build(); - - // Boss / cape milestones (quests mixed in) - Goal milestones = Goal.builder() - .description("Ladlor — Milestones & Capes") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(FIRE_CAPE).itemName("Fire cape").quantity(1).build(), - QuestTask.builder().quest(Quest.SONG_OF_THE_ELVES).build(), - QuestTask.builder().quest(Quest.SINS_OF_THE_FATHER).build() - )) - .build(); - - List goals = Arrays.asList( - melee, ranged, magic, utility, - voidSet, eliteVoid, godCapes, crystal, jewelry, - slayerUndead, imbuedRings, gwdArmor, prayerScrolls, raidsUniques, - milestones - ); - return new Preset( - "Ladlor Ironman Progression", - "A multi-lane item-based progression inspired by Ladlor's Ironman chart.", - goals - ); - } - - // Fast travel unlocks (not a full preset, just for reference) - private static List buildFastTravelUnlocks() { - return Arrays.asList( - // Teleport items from quests - QuestTask.builder().quest(Quest.MONKEY_MADNESS_II).build(), // Royal seed pod - QuestTask.builder().quest(Quest.A_TASTE_OF_HOPE).build(), // Drakan's medallion teleports in Morytania - QuestTask.builder().quest(Quest.DESERT_TREASURE_I).build(), // Ring of visibility (not a teleport itself, but for fairy rings) - QuestTask.builder().quest(Quest.FAIRYTALE_II__CURE_A_QUEEN).build(), // Fairy rings - QuestTask.builder().quest(Quest.LUNAR_DIPLOMACY).build(), // Lunar spellbook teleport - QuestTask.builder().quest(Quest.ANIMAL_MAGNETISM).build(), // Ectophial - QuestTask.builder().quest(Quest.ANCIENT_CAVERN).build(), // Games necklace (Barbarian Outpost) - // Other travel networks - QuestTask.builder().quest(Quest.THE_GRAND_TREE).build(), // Gnome glider network - QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build(), // Spirit tree network - QuestTask.builder().quest(Quest.THE_GIANT_DWARF).build(), // Keldagrim mine carts - QuestTask.builder().quest(Quest.ANOTHER_SLICE_OF_HAM).build(), // Dorgesh-Kaan ↔ Keldagrim train - QuestTask.builder().quest(Quest.KINGS_RANSOM).build(), // Camelot teleport - QuestTask.builder().quest(Quest.PLAGUE_CITY).build(), // Ardougne teleport - QuestTask.builder().quest(Quest.RUM_DEAL).build() // Harmony Island teleport (Morytania) - ); - } - private static Preset buildLadlorMelee() { - Goal melee = Goal.builder() - .description("Ladlor — Melee Progression") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(DRAGON_SCIMITAR).itemName("Dragon scimitar").quantity(1).build(), - ItemTask.builder().itemId(DRAGON_DEFENDER).itemName("Dragon defender").quantity(1).build(), - ItemTask.builder().itemId(FIGHTER_TORSO).itemName("Fighter torso").quantity(1).build(), - ItemTask.builder().itemId(DRAGON_BOOTS).itemName("Dragon boots").quantity(1).build(), - ItemTask.builder().itemId(ABYSSAL_WHIP).itemName("Abyssal whip").quantity(1).build(), - ItemTask.builder().itemId(BERSERKER_RING).itemName("Berserker ring").quantity(1).build(), - ItemTask.builder().itemId(FIRE_CAPE).itemName("Fire cape").quantity(1).build(), - ItemTask.builder().itemId(FEROCIOUS_GLOVES).itemName("Ferocious gloves").quantity(1).build(), - ItemTask.builder().itemId(ABYSSAL_TENTACLE).itemName("Abyssal tentacle").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Melee", "Melee progression lane from Ladlor chart.", Arrays.asList(melee)); - } - - private static Preset buildLadlorRanged() { - Goal ranged = Goal.builder() - .description("Ladlor — Ranged Progression") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(MAGIC_SHORTBOW).itemName("Magic shortbow").quantity(1).build(), - ItemTask.builder().itemId(RUNE_CROSSBOW).itemName("Rune crossbow").quantity(1).build(), - ItemTask.builder().itemId(BLACK_DHIDE_BODY).itemName("Black d'hide body").quantity(1).build(), - ItemTask.builder().itemId(AVAS_ACCUMULATOR).itemName("Ava's accumulator").quantity(1).build(), - ItemTask.builder().itemId(KARILS_LEATHERTOP).itemName("Karil's leathertop").quantity(1).build(), - ItemTask.builder().itemId(KARILS_LEATHERSKIRT).itemName("Karil's leatherskirt").quantity(1).build(), - ItemTask.builder().itemId(TOXIC_BLOWPIPE).itemName("Toxic blowpipe").quantity(1).build(), - ItemTask.builder().itemId(ARMADYL_CROSSBOW).itemName("Armadyl crossbow").quantity(1).build(), - ItemTask.builder().itemId(AVAS_ASSEMBLER).itemName("Ava's assembler").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Ranged", "Ranged progression lane from Ladlor chart.", Arrays.asList(ranged)); - } - - private static Preset buildLadlorMagic() { - Goal magic = Goal.builder() - .description("Ladlor — Magic Progression") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(MYSTIC_ROBE_TOP).itemName("Mystic robe top").quantity(1).build(), - ItemTask.builder().itemId(MYSTIC_ROBE_BOTTOM).itemName("Mystic robe bottom").quantity(1).build(), - ItemTask.builder().itemId(OCCULT_NECKLACE).itemName("Occult necklace").quantity(1).build(), - ItemTask.builder().itemId(MAGES_BOOK).itemName("Mage's book").quantity(1).build(), - ItemTask.builder().itemId(TRIDENT_OF_THE_SEAS).itemName("Trident of the seas").quantity(1).build(), - ItemTask.builder().itemId(UNCHARGED_TOXIC_TRIDENT).itemName("Trident of the swamp").quantity(1).build(), - ItemTask.builder().itemId(ANCESTRAL_ROBE_TOP).itemName("Ancestral robe top").quantity(1).build(), - ItemTask.builder().itemId(ANCESTRAL_ROBE_BOTTOM).itemName("Ancestral robe bottom").quantity(1).build(), - ItemTask.builder().itemId(ANCESTRAL_HAT).itemName("Ancestral hat").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Magic", "Magic progression lane from Ladlor chart.", Arrays.asList(magic)); - } - - private static Preset buildLadlorUtility() { - Goal utility = Goal.builder() - .description("Ladlor — Utility & Unlocks") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(GRACEFUL_HOOD).itemName("Graceful hood").quantity(1).build(), - ItemTask.builder().itemId(GRACEFUL_TOP).itemName("Graceful top").quantity(1).build(), - ItemTask.builder().itemId(GRACEFUL_LEGS).itemName("Graceful legs").quantity(1).build(), - ItemTask.builder().itemId(GRACEFUL_GLOVES).itemName("Graceful gloves").quantity(1).build(), - ItemTask.builder().itemId(GRACEFUL_BOOTS).itemName("Graceful boots").quantity(1).build(), - ItemTask.builder().itemId(GRACEFUL_CAPE).itemName("Graceful cape").quantity(1).build(), - ItemTask.builder().itemId(BARROWS_GLOVES).itemName("Barrows gloves").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Utility & Unlocks", "Utility, movement and QoL lane from Ladlor chart.", Arrays.asList(utility)); - } - - private static Preset buildLadlorVoidSet() { - Goal voidSet = Goal.builder() - .description("Ladlor — Void Set") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(VOID_KNIGHT_TOP).itemName("Void knight top").quantity(1).build(), - ItemTask.builder().itemId(VOID_KNIGHT_ROBE).itemName("Void knight robe").quantity(1).build(), - ItemTask.builder().itemId(VOID_KNIGHT_GLOVES).itemName("Void knight gloves").quantity(1).build(), - ItemTask.builder().itemId(VOID_MAGE_HELM).itemName("Void mage helm").quantity(1).build(), - ItemTask.builder().itemId(VOID_RANGER_HELM).itemName("Void ranger helm").quantity(1).build(), - ItemTask.builder().itemId(VOID_MELEE_HELM).itemName("Void melee helm").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Void Set", "Standard Void set lane from Ladlor chart.", Arrays.asList(voidSet)); - } - - private static Preset buildLadlorGodCapes() { - Goal godCapes = Goal.builder() - .description("Ladlor — Imbued God Capes") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(IMBUED_SARADOMIN_CAPE).itemName("Imbued Saradomin cape").quantity(1).build(), - ItemTask.builder().itemId(IMBUED_GUTHIX_CAPE).itemName("Imbued Guthix cape").quantity(1).build(), - ItemTask.builder().itemId(IMBUED_ZAMORAK_CAPE).itemName("Imbued Zamorak cape").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Imbued God Capes", "Mage Arena II imbued god capes.", Arrays.asList(godCapes)); - } - - private static Preset buildLadlorCrystalBowfa() { - Goal crystal = Goal.builder() - .description("Ladlor — Crystal & Bowfa") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(CRYSTAL_HELM).itemName("Crystal helm").quantity(1).build(), - ItemTask.builder().itemId(CRYSTAL_BODY).itemName("Crystal body").quantity(1).build(), - ItemTask.builder().itemId(CRYSTAL_LEGS).itemName("Crystal legs").quantity(1).build(), - ItemTask.builder().itemId(BOW_OF_FAERDHINEN).itemName("Bow of Faerdhinen").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Crystal & Bowfa", "Crystal armour and Bow of Faerdhinen lane.", Arrays.asList(crystal)); - } - - private static Preset buildLadlorJewelryBoots() { - Goal jewelry = Goal.builder() - .description("Ladlor — Jewelry & Boots Upgrades") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(AMULET_OF_TORTURE).itemName("Amulet of torture").quantity(1).build(), - ItemTask.builder().itemId(NECKLACE_OF_ANGUISH).itemName("Necklace of anguish").quantity(1).build(), - ItemTask.builder().itemId(PEGASIAN_BOOTS).itemName("Pegasian boots").quantity(1).build(), - ItemTask.builder().itemId(RANGER_BOOTS).itemName("Ranger boots").quantity(1).build(), - ItemTask.builder().itemId(PRIMORDIAL_BOOTS).itemName("Primordial boots").quantity(1).build(), - ItemTask.builder().itemId(ETERNAL_BOOTS).itemName("Eternal boots").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Jewelry & Boots", "Jewelry and footwear upgrades lane.", Arrays.asList(jewelry)); - } - - private static Preset buildLadlorSlayerUndead() { - Goal slayerUndead = Goal.builder() - .description("Ladlor — Slayer & Undead") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(BLACK_MASK).itemName("Black mask").quantity(1).build(), - ItemTask.builder().itemId(SLAYER_HELMET).itemName("Slayer helmet").quantity(1).build(), - ItemTask.builder().itemId(SLAYER_HELMET_I).itemName("Slayer helmet (i)").quantity(1).build(), - ItemTask.builder().itemId(SALVE_AMULET_E).itemName("Salve amulet (e)").quantity(1).build(), - ItemTask.builder().itemId(SALVE_AMULET_EI).itemName("Salve amulet (ei)").quantity(1).build(), - ItemTask.builder().itemId(IMBUED_HEART).itemName("Imbued heart").quantity(1).build(), - ItemTask.builder().itemId(RUNE_POUCH).itemName("Rune pouch").quantity(1).build(), - ItemTask.builder().itemId(RING_OF_SUFFERING).itemName("Ring of suffering").quantity(1).build(), - ItemTask.builder().itemId(RING_OF_SUFFERING_I).itemName("Ring of suffering (i)").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Slayer & Undead", "Slayer and undead-focused unlocks.", Arrays.asList(slayerUndead)); - } - - private static Preset buildLadlorImbuedRings() { - Goal imbuedRings = Goal.builder() - .description("Ladlor — Imbued Rings") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(BERSERKER_RING_I).itemName("Berserker ring (i)").quantity(1).build(), - ItemTask.builder().itemId(ARCHERS_RING_I).itemName("Archer's ring (i)").quantity(1).build(), - ItemTask.builder().itemId(SEERS_RING_I).itemName("Seers ring (i)").quantity(1).build(), - ItemTask.builder().itemId(WARRIOR_RING_I).itemName("Warrior ring (i)").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Imbued Rings", "Imbued Dagannoth Kings rings.", Arrays.asList(imbuedRings)); - } - - private static Preset buildLadlorGwdArmor() { - Goal gwdArmor = Goal.builder() - .description("Ladlor — God Wars Armor") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(BANDOS_CHESTPLATE).itemName("Bandos chestplate").quantity(1).build(), - ItemTask.builder().itemId(BANDOS_TASSETS).itemName("Bandos tassets").quantity(1).build(), - ItemTask.builder().itemId(ARMADYL_CHESTPLATE).itemName("Armadyl chestplate").quantity(1).build(), - ItemTask.builder().itemId(ARMADYL_CHAINSKIRT).itemName("Armadyl chainskirt").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — God Wars Armor", "Bandos and Armadyl armour lane.", Arrays.asList(gwdArmor)); - } - - private static Preset buildLadlorEliteVoid() { - Goal eliteVoid = Goal.builder() - .description("Ladlor — Elite Void") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(ELITE_VOID_TOP).itemName("Elite void top").quantity(1).build(), - ItemTask.builder().itemId(ELITE_VOID_ROBE).itemName("Elite void robe").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Elite Void", "Elite void upgrade lane.", Arrays.asList(eliteVoid)); - } - - private static Preset buildLadlorPrayerScrolls() { - Goal prayerScrolls = Goal.builder() - .description("Ladlor — Prayer Scrolls") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(DEXTEROUS_PRAYER_SCROLL).itemName("Dexterous prayer scroll (Rigour)").quantity(1).build(), - ItemTask.builder().itemId(ARCANE_PRAYER_SCROLL).itemName("Arcane prayer scroll (Augury)").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Prayer Scrolls", "Rigour and Augury unlock scrolls.", Arrays.asList(prayerScrolls)); - } - - private static Preset buildLadlorRaidsUniques() { - Goal raidsUniques = Goal.builder() - .description("Ladlor — Raids Uniques") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(DRAGON_HUNTER_CROSSBOW).itemName("Dragon hunter crossbow").quantity(1).build(), - ItemTask.builder().itemId(TWISTED_BOW).itemName("Twisted bow").quantity(1).build(), - ItemTask.builder().itemId(SCYTHE_OF_VITUR).itemName("Scythe of Vitur").quantity(1).build(), - ItemTask.builder().itemId(GHRAZI_RAPIER).itemName("Ghrazi rapier").quantity(1).build(), - ItemTask.builder().itemId(OSMUMTENS_FANG).itemName("Osmumten's fang").quantity(1).build(), - ItemTask.builder().itemId(MASORI_BODY).itemName("Masori body").quantity(1).build(), - ItemTask.builder().itemId(MASORI_CHAPS).itemName("Masori chaps").quantity(1).build(), - ItemTask.builder().itemId(TUMEKENS_SHADOW).itemName("Tumeken's shadow").quantity(1).build() - )) - .build(); - return new Preset("Ladlor — Raids Uniques", "Raids 1/2/3 uniques lane.", Arrays.asList(raidsUniques)); - } - - private static Preset buildLadlorMilestones() { - Goal milestones = Goal.builder() - .description("Ladlor — Milestones & Capes") - .tasks(ReorderableList.from( - ItemTask.builder().itemId(FIRE_CAPE).itemName("Fire cape").quantity(1).build(), - QuestTask.builder().quest(Quest.SONG_OF_THE_ELVES).build(), - QuestTask.builder().quest(Quest.SINS_OF_THE_FATHER).build() - )) - .build(); - return new Preset("Ladlor — Milestones & Capes", "Key capes and late-game quest milestones.", Arrays.asList(milestones)); - } - */ private static Preset buildEarlyIronman() { Goal early = Goal.builder() @@ -641,8 +180,8 @@ private static Preset buildFreeToPlayQuests() { QuestTask.builder().quest(Quest.MISTHALIN_MYSTERY).build(), QuestTask.builder().quest(Quest.THE_CORSAIR_CURSE).build(), // Note: Shield of Arrav needs a partner - ManualTask.builder().description("Find a partner for Shield of Arrav (Phoenix/Black Arm) or coordinate in a clan chat").build(), QuestTask.builder().quest(Quest.SHIELD_OF_ARRAV).build(), + ManualTask.builder().description("Find a partner for Shield of Arrav (Phoenix/Black Arm) or coordinate in a clan chat").indentLevel(1).build(), // Capstone F2P quest QuestTask.builder().quest(Quest.DRAGON_SLAYER_I).build() )) @@ -694,4 +233,37 @@ private static Preset buildFastTravelUnlocks() { Arrays.asList(travel) ); } + + private static Preset buildAllBarrowsGear() { + Goal barrows = Goal.builder() + .description("All Barrows Gear") + .tasks(ReorderableList.from( + ItemTask.builder().itemId(AHRIMS_HOOD).itemName("Ahrim's hood").quantity(1).build(), + ItemTask.builder().itemId(AHRIMS_ROBETOP).itemName("Ahrim's robetop").quantity(1).build(), + ItemTask.builder().itemId(AHRIMS_ROBESKIRT).itemName("Ahrim's robeskirt").quantity(1).build(), + ItemTask.builder().itemId(AHRIMS_STAFF).itemName("Ahrim's staff").quantity(1).build(), + ItemTask.builder().itemId(DHAROKS_HELM).itemName("Dharok's helm").quantity(1).build(), + ItemTask.builder().itemId(DHAROKS_PLATEBODY).itemName("Dharok's platebody").quantity(1).build(), + ItemTask.builder().itemId(DHAROKS_PLATELEGS).itemName("Dharok's platelegs").quantity(1).build(), + ItemTask.builder().itemId(DHAROKS_GREATAXE).itemName("Dharok's greataxe").quantity(1).build(), + ItemTask.builder().itemId(GUTHANS_HELM).itemName("Guthan's helm").quantity(1).build(), + ItemTask.builder().itemId(GUTHANS_PLATEBODY).itemName("Guthan's platebody").quantity(1).build(), + ItemTask.builder().itemId(GUTHANS_CHAINSKIRT).itemName("Guthan's chainskirt").quantity(1).build(), + ItemTask.builder().itemId(GUTHANS_WARSPEAR).itemName("Guthan's warspear").quantity(1).build(), + ItemTask.builder().itemId(KARILS_COIF).itemName("Karil's coif").quantity(1).build(), + ItemTask.builder().itemId(KARILS_LEATHERTOP).itemName("Karil's leathertop").quantity(1).build(), + ItemTask.builder().itemId(KARILS_LEATHERSKIRT).itemName("Karil's leatherskirt").quantity(1).build(), + ItemTask.builder().itemId(KARILS_CROSSBOW).itemName("Karil's crossbow").quantity(1).build(), + ItemTask.builder().itemId(TORAGS_HELM).itemName("Torag's helm").quantity(1).build(), + ItemTask.builder().itemId(TORAGS_PLATEBODY).itemName("Torag's platebody").quantity(1).build(), + ItemTask.builder().itemId(TORAGS_PLATELEGS).itemName("Torag's platelegs").quantity(1).build(), + ItemTask.builder().itemId(TORAGS_HAMMERS).itemName("Torag's hammers").quantity(1).build(), + ItemTask.builder().itemId(VERACS_HELM).itemName("Verac's helm").quantity(1).build(), + ItemTask.builder().itemId(VERACS_BRASSARD).itemName("Verac's brassard").quantity(1).build(), + ItemTask.builder().itemId(VERACS_PLATESKIRT).itemName("Verac's plateskirt").quantity(1).build(), + ItemTask.builder().itemId(VERACS_FLAIL).itemName("Verac's flail").quantity(1).build() + )) + .build(); + return new Preset("All Barrows Gear", "Collect a full set of each Barrows brothers' equipment.", Arrays.asList(barrows)); + } } diff --git a/src/main/java/com/toofifty/goaltracker/services/TaskIconService.java b/src/main/java/com/toofifty/goaltracker/services/TaskIconService.java index 732a0de..d481431 100644 --- a/src/main/java/com/toofifty/goaltracker/services/TaskIconService.java +++ b/src/main/java/com/toofifty/goaltracker/services/TaskIconService.java @@ -8,65 +8,76 @@ import net.runelite.client.util.ImageUtil; import javax.inject.Inject; +import javax.inject.Singleton; import javax.swing.*; +import java.awt.*; import java.awt.image.BufferedImage; -public class TaskIconService +/** + * Resolves small 16x16 icons for tasks (items, skills, quests, manual checks). + */ +@Singleton +public final class TaskIconService { + private static final int ICON_SIZE = 16; + public static final ImageIcon CROSS_MARK_ICON; public static final ImageIcon CHECK_MARK_ICON; public static final ImageIcon QUEST_ICON; public static final ImageIcon QUEST_COMPLETE_ICON; public static final ImageIcon UNKNOWN_ICON; - static { + static + { CROSS_MARK_ICON = new ImageIcon( - ImageUtil.loadImageResource(GoalTrackerPlugin.class, "/cross_mark.png") + ImageUtil.loadImageResource(GoalTrackerPlugin.class, "/cross_mark.png") ); CHECK_MARK_ICON = new ImageIcon( - ImageUtil.loadImageResource(GoalTrackerPlugin.class, "/check_mark.png") + ImageUtil.loadImageResource(GoalTrackerPlugin.class, "/check_mark.png") ); QUEST_ICON = new ImageIcon( - ImageUtil.loadImageResource(GoalTrackerPlugin.class, "/quest_icon.png") + ImageUtil.loadImageResource(GoalTrackerPlugin.class, "/quest_icon.png") ); QUEST_COMPLETE_ICON = new ImageIcon( - ImageUtil.loadImageResource(GoalTrackerPlugin.class, "/quest_complete.png") + ImageUtil.loadImageResource(GoalTrackerPlugin.class, "/quest_complete.png") ); UNKNOWN_ICON = new ImageIcon( - ImageUtil.loadImageResource(GoalTrackerPlugin.class, "/question_mark.png") + ImageUtil.loadImageResource(GoalTrackerPlugin.class, "/question_mark.png") ); } - @Inject - private ItemManager itemManager; - - @Inject - private SkillIconManager skillIconManager; - - @Inject - private Client client; + @Inject private ItemManager itemManager; + @Inject private SkillIconManager skillIconManager; + @Inject private Client client; public ImageIcon get(Task task) { - if (task instanceof ManualTask) { + if (task instanceof ManualTask) + { return get((ManualTask) task); } - - if (task instanceof QuestTask) { + if (task instanceof QuestTask) + { return get((QuestTask) task); } BufferedImage image = null; - if (task instanceof SkillLevelTask) { + if (task instanceof SkillLevelTask) + { image = get((SkillLevelTask) task); - } else if (task instanceof SkillXpTask) { + } + else if (task instanceof SkillXpTask) + { image = get((SkillXpTask) task); - } else if (task instanceof ItemTask) { + } + else if (task instanceof ItemTask) + { image = get((ItemTask) task); } - if (image != null) { - return new ImageIcon(image.getScaledInstance(16, 16, 32)); + if (image != null) + { + return iconify(image); } return UNKNOWN_ICON; @@ -94,10 +105,16 @@ public ImageIcon get(QuestTask task) public BufferedImage get(ItemTask task) { - if (task.getCachedIcon() == null && client.isClientThread()) { + if (task.getCachedIcon() == null && client.isClientThread()) + { task.setCachedIcon(itemManager.getImage(task.getItemId())); } - return task.getCachedIcon(); } + + private ImageIcon iconify(BufferedImage img) + { + Image scaled = img.getScaledInstance(ICON_SIZE, ICON_SIZE, Image.SCALE_SMOOTH); + return new ImageIcon(scaled); + } } diff --git a/src/main/java/com/toofifty/goaltracker/services/TaskUpdateService.java b/src/main/java/com/toofifty/goaltracker/services/TaskUpdateService.java index 522180d..6e30d14 100644 --- a/src/main/java/com/toofifty/goaltracker/services/TaskUpdateService.java +++ b/src/main/java/com/toofifty/goaltracker/services/TaskUpdateService.java @@ -8,137 +8,121 @@ import net.runelite.api.events.StatChanged; import javax.inject.Inject; +import javax.inject.Singleton; -public class TaskUpdateService +/** + * Updates task status based on current client state and events. + */ +@Singleton +public final class TaskUpdateService { - @Inject - private Client client; - - @Inject - private ItemCache itemCache; - - public boolean update(Task task) { - switch (task.getType()) { - case SKILL_LEVEL: return this.update((SkillLevelTask) task); - case SKILL_XP: return this.update((SkillXpTask) task); - case QUEST: return this.update((QuestTask) task); - case ITEM: return this.update((ItemTask) task); - default: return false; - } - } + @Inject private Client client; + @Inject private ItemCache itemCache; /** - * Returns true if an update has occurred - * @param task the skill level task to validate + * Dispatch update for a generic task, returning true if status/values changed. */ - public boolean update(SkillLevelTask task) + public boolean update(Task task) { - if (client.getGameState() != GameState.LOGGED_IN) return false; + switch (task.getType()) + { + case SKILL_LEVEL: return update((SkillLevelTask) task); + case SKILL_XP: return update((SkillXpTask) task); + case QUEST: return update((QuestTask) task); + case ITEM: return update((ItemTask) task); + default: return false; + } + } + + // -------------------- Skill Level -------------------- - return this.update(task, client.getRealSkillLevel(task.getSkill())); + /** Returns true if an update has occurred. */ + public boolean update(SkillLevelTask task) + { + if (client.getGameState() != GameState.LOGGED_IN || !client.isClientThread()) + { + return false; + } + return update(task, client.getRealSkillLevel(task.getSkill())); } - /** - * Returns true if an update has occurred - * @param task the skill level task to validate - * @param event the stat changed event this update is tied to - */ + /** Returns true if an update has occurred (event-driven). */ public boolean update(SkillLevelTask task, StatChanged event) { - if (event.getSkill() != task.getSkill()) return false; - - return this.update(task, event.getLevel()); + if (event.getSkill() != task.getSkill()) + { + return false; + } + return update(task, event.getLevel()); } - /** - * Returns true if an update has occurred - * @param task the skill level task to validate - */ + /** Returns true if an update has occurred given a specific level. */ public boolean update(SkillLevelTask task, int level) { - Status oldStatus = task.getStatus(); - - task.setStatus( - level >= task.getLevel() - ? Status.COMPLETED - : Status.NOT_STARTED - ); - + final Status oldStatus = task.getStatus(); + task.setStatus(level >= task.getLevel() ? Status.COMPLETED : Status.NOT_STARTED); return oldStatus != task.getStatus(); } - /** - * Returns true if an update has occurred - * @param task the skill level task to validate - */ + // -------------------- Skill XP -------------------- + + /** Returns true if an update has occurred. */ public boolean update(SkillXpTask task) { - if (client.getGameState() != GameState.LOGGED_IN) return false; - - return this.update(task, client.getSkillExperience(task.getSkill())); + if (client.getGameState() != GameState.LOGGED_IN || !client.isClientThread()) + { + return false; + } + return update(task, client.getSkillExperience(task.getSkill())); } - /** - * Returns true if an update has occurred - * @param task the skill level task to validate - * @param event the stat changed event this update is tied to - */ + /** Returns true if an update has occurred (event-driven). */ public boolean update(SkillXpTask task, StatChanged event) { - if (event.getSkill() != task.getSkill()) return false; - - return this.update(task, event.getXp()); + if (event.getSkill() != task.getSkill()) + { + return false; + } + return update(task, event.getXp()); } - /** - * Returns true if an update has occurred - * @param task the skill xp task to validate - */ + /** Returns true if an update has occurred given a specific XP value. */ public boolean update(SkillXpTask task, int xp) { - Status oldStatus = task.getStatus(); - - task.setStatus( - xp >= task.getXp() - ? Status.COMPLETED - : Status.NOT_STARTED - ); - + final Status oldStatus = task.getStatus(); + task.setStatus(xp >= task.getXp() ? Status.COMPLETED : Status.NOT_STARTED); return oldStatus != task.getStatus(); } - /** - * Returns true if an update has occurred - * @param task the quest task to validate - */ + // -------------------- Quest -------------------- + + /** Returns true if an update has occurred. */ public boolean update(QuestTask task) { - if (client.getGameState() != GameState.LOGGED_IN || !client.isClientThread()) return false; - - Status oldStatus = task.getStatus(); - + if (client.getGameState() != GameState.LOGGED_IN || !client.isClientThread()) + { + return false; + } + final Status oldStatus = task.getStatus(); task.setStatus(Status.fromQuestState(task.getQuest().getState(client))); - return oldStatus != task.getStatus(); } - /** - * Returns true if an update has occurred - * @param task the item task to validate - */ + // -------------------- Item -------------------- + + /** Returns true if an update has occurred. */ public boolean update(ItemTask task) { - int oldAcquired = task.getAcquired(); - Status oldStatus = task.getStatus(); + final int oldAcquired = task.getAcquired(); + final Status oldStatus = task.getStatus(); task.setAcquired(Math.min(itemCache.getTotalQuantity(task.getItemId()), task.getQuantity())); task.setStatus( task.getAcquired() >= task.getQuantity() ? Status.COMPLETED - : (task.getAcquired() > 0 - ? Status.IN_PROGRESS - : Status.NOT_STARTED)); + : (task.getAcquired() > 0 ? Status.IN_PROGRESS : Status.NOT_STARTED) + ); return oldAcquired != task.getAcquired() || oldStatus != task.getStatus(); } diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java index c2229ed..e92cca0 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java @@ -1,28 +1,27 @@ package com.toofifty.goaltracker.ui; -import net.runelite.client.ui.ColorScheme; import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; - -import com.toofifty.goaltracker.ui.Refreshable; +import com.toofifty.goaltracker.ui.components.ListItemPanel; +import net.runelite.client.ui.ColorScheme; import javax.swing.*; -import javax.swing.JMenuItem; -import javax.swing.event.PopupMenuListener; import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; import java.awt.*; - -import com.toofifty.goaltracker.ui.components.ListItemPanel; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import javax.swing.SwingUtilities; - import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; import static com.toofifty.goaltracker.utils.Constants.STATUS_TO_COLOR; -public class GoalItemContent extends JPanel implements Refreshable +/** + * UI component for rendering a Goal in a list. + * Shows the title (editable), progress count, and a slim progress bar. + * Also supports pinning, context menus, and refresh on goal status change. + */ +public final class GoalItemContent extends JPanel implements Refreshable { private final JLabel titleLabel = new JLabel(); private final JTextField titleEdit = new JTextField(); @@ -115,15 +114,15 @@ private void maybeShow(MouseEvent e) { return; } - java.awt.Component src = (java.awt.Component) e.getSource(); - javax.swing.JComponent listItem = (javax.swing.JComponent) SwingUtilities.getAncestorOfClass(ListItemPanel.class, src); + Component src = (Component) e.getSource(); + JComponent listItem = (JComponent) SwingUtilities.getAncestorOfClass(ListItemPanel.class, src); if (listItem == null) { - listItem = (javax.swing.JComponent) SwingUtilities.getAncestorOfClass(ListItemPanel.class, GoalItemContent.this); + listItem = (JComponent) SwingUtilities.getAncestorOfClass(ListItemPanel.class, GoalItemContent.this); } if (listItem != null && listItem.getComponentPopupMenu() != null) { - java.awt.Point p = SwingUtilities.convertPoint(src, e.getPoint(), listItem); + Point p = SwingUtilities.convertPoint(src, e.getPoint(), listItem); JPopupMenu menu = listItem.getComponentPopupMenu(); // --- Pin / Unpin (temporary additions) --- @@ -151,8 +150,8 @@ private void maybeShow(MouseEvent e) @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { cleanup(menu, this); } private void cleanup(JPopupMenu m, PopupMenuListener self) { // Remove only the components we added this time - java.util.List toRemove = new java.util.ArrayList<>(); - for (java.awt.Component c : m.getComponents()) { + java.util.List toRemove = new java.util.ArrayList<>(); + for (Component c : m.getComponents()) { if (c instanceof JComponent) { Object flag = ((JComponent) c).getClientProperty("pinToggle"); if (Boolean.TRUE.equals(flag)) { @@ -160,7 +159,7 @@ private void cleanup(JPopupMenu m, PopupMenuListener self) { } } } - for (java.awt.Component c : toRemove) { + for (Component c : toRemove) { m.remove(c); } m.removePopupMenuListener(self); @@ -182,7 +181,7 @@ private void cleanup(JPopupMenu m, PopupMenuListener self) { progress.addMouseListener(forwardPopup); progressBar.addMouseListener(forwardPopup); // Ensure item icon/text initialize on first render (e.g., on login) - javax.swing.SwingUtilities.invokeLater(this::refresh); + SwingUtilities.invokeLater(this::refresh); } @Override @@ -190,7 +189,7 @@ public void addNotify() { super.addNotify(); // In case construction happened before UI was realized, refresh when shown - javax.swing.SwingUtilities.invokeLater(this::refresh); + SwingUtilities.invokeLater(this::refresh); } @Override @@ -209,7 +208,7 @@ public void refresh() int done = goal.getComplete().size(); progressBar.setVisible(total > 0); progressBar.setProgress(done, total, color); - javax.swing.SwingUtilities.invokeLater(this::updateTitleLabel); + SwingUtilities.invokeLater(this::updateTitleLabel); } private static class SlimBar extends JComponent { private int done = 0; diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java index f31ed7d..8f9e392 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java @@ -1,31 +1,28 @@ package com.toofifty.goaltracker.ui; import com.toofifty.goaltracker.GoalTrackerPlugin; -import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.ActionHistory; +import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.RemoveTaskAction; import com.toofifty.goaltracker.models.enums.TaskType; -import com.toofifty.goaltracker.models.enums.Status; -import com.toofifty.goaltracker.models.task.ManualTask; import com.toofifty.goaltracker.models.task.Task; -import com.toofifty.goaltracker.ui.components.EditableInput; -import com.toofifty.goaltracker.ui.components.ListPanel; -import com.toofifty.goaltracker.ui.components.ListTaskPanel; -import com.toofifty.goaltracker.ui.components.ActionBar; -import com.toofifty.goaltracker.ui.components.ActionBarButton; +import com.toofifty.goaltracker.ui.components.*; +import net.runelite.client.ui.ColorScheme; + import javax.swing.*; +import javax.swing.border.EmptyBorder; import javax.swing.text.DefaultEditorKit; import javax.swing.text.JTextComponent; import java.awt.*; import java.awt.event.KeyEvent; -import java.awt.BorderLayout; import java.util.Objects; import java.util.function.Consumer; -import javax.swing.border.EmptyBorder; -import javax.swing.BorderFactory; -import net.runelite.client.ui.ColorScheme; -public class GoalPanel extends JPanel implements Refreshable +/** + * Panel showing a single Goal: header, action bar (undo/redo, prereqs), + * editable description, and the task list with add-new controls. + */ +public final class GoalPanel extends JPanel implements Refreshable { private final GoalTrackerPlugin plugin; private final Goal goal; diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index b2b71d7..8b9f031 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -1,17 +1,17 @@ package com.toofifty.goaltracker.ui; +import com.toofifty.goaltracker.presets.GoalPresetRepository.Preset; import com.toofifty.goaltracker.GoalManager; import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.UndoStack; -import com.toofifty.goaltracker.presets.GoalPresetRepository; -import com.toofifty.goaltracker.presets.GoalPresetRepository.Preset; import com.toofifty.goaltracker.models.task.Task; -import com.toofifty.goaltracker.utils.ReorderableList; +import com.toofifty.goaltracker.presets.GoalPresetRepository; import com.toofifty.goaltracker.ui.components.ActionBar; import com.toofifty.goaltracker.ui.components.ActionBarButton; import com.toofifty.goaltracker.ui.components.ListItemPanel; import com.toofifty.goaltracker.ui.components.ListPanel; +import com.toofifty.goaltracker.utils.ReorderableList; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.FontManager; import net.runelite.client.ui.PluginPanel; @@ -21,11 +21,16 @@ import javax.swing.border.EmptyBorder; import javax.swing.filechooser.FileNameExtensionFilter; import java.awt.*; -import java.util.function.Consumer; import java.util.Collections; import java.util.Comparator; +import java.util.function.Consumer; -public class GoalTrackerPanel extends PluginPanel implements Refreshable +/** + * Main plugin panel for Goal Tracker. + * Shows header (title, add/import/export controls), goal list with undo/redo, + * and switches between home view and individual goal panels. + */ +public final class GoalTrackerPanel extends PluginPanel implements Refreshable { private final JPanel mainPanel = new JPanel(new BorderLayout()); private final ListPanel goalListPanel; @@ -47,7 +52,7 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) super(false); this.plugin = plugin; this.goalManager = goalManager; - this.goalManager.addGoalsChangedListener(() -> javax.swing.SwingUtilities.invokeLater(this::refreshHomeListIfVisible)); + this.goalManager.addGoalsChangedListener(() -> SwingUtilities.invokeLater(this::refreshHomeListIfVisible)); setBackground(ColorScheme.DARK_GRAY_COLOR); setLayout(new BorderLayout()); @@ -59,32 +64,32 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) titlePanel.setLayout(new BorderLayout()); titlePanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - JLabel title = new JLabel("Goal Tracker v2"); + JLabel title = new JLabel("Goal Tracker"); title.setForeground(Color.WHITE); title.setFont(FontManager.getRunescapeBoldFont()); - JLabel author = new JLabel("By: AhDoozy"); - author.setForeground(Color.LIGHT_GRAY); - author.setFont(title.getFont().deriveFont(title.getFont().getSize2D() - 3f)); - - JPanel titleTextPanel = new JPanel(new GridLayout(2, 1)); - titleTextPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - titleTextPanel.add(title); - titleTextPanel.add(author); - titlePanel.add(titleTextPanel, BorderLayout.WEST); - - JPanel addGoalPanel = new JPanel(); - addGoalPanel.setLayout(new BoxLayout(addGoalPanel, BoxLayout.Y_AXIS)); - addGoalPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - JPanel addGoalRow = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); - addGoalRow.setBackground(ColorScheme.DARK_GRAY_COLOR); + // Title row (top) + JPanel titleRow = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); + titleRow.setBackground(ColorScheme.DARK_GRAY_COLOR); + titleRow.add(title); + + // Buttons row (bottom) + JPanel buttonsRow = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); + buttonsRow.setBackground(ColorScheme.DARK_GRAY_COLOR); ActionBarButton addGoalBtn = new ActionBarButton("+ Add goal", this::addNewGoal); - addGoalRow.add(addGoalBtn); - addGoalPanel.add(addGoalRow); - addGoalPanel.add(Box.createVerticalStrut(4)); ActionBarButton addFromPresetBtn = new ActionBarButton("Add from Preset…", this::addFromPreset); - addGoalPanel.add(addFromPresetBtn); - titlePanel.add(addGoalPanel, BorderLayout.EAST); + buttonsRow.add(addGoalBtn); + buttonsRow.add(addFromPresetBtn); + + // Stack rows vertically + JPanel headerTop = new JPanel(); + headerTop.setLayout(new BoxLayout(headerTop, BoxLayout.Y_AXIS)); + headerTop.setBackground(ColorScheme.DARK_GRAY_COLOR); + headerTop.add(titleRow); + headerTop.add(Box.createVerticalStrut(4)); + headerTop.add(buttonsRow); + + titlePanel.add(headerTop, BorderLayout.CENTER); // Action bar ActionBar actionBar = new ActionBar(); @@ -366,7 +371,7 @@ private void addFromPreset() details.setEditable(false); details.setBackground(new JPanel().getBackground()); - java.util.function.Consumer update = (opt) -> { + Consumer update = (opt) -> { if (opt == null) { details.setText(""); return; } int goals = opt.getGoals().size(); int tasks = opt.getGoals().stream().mapToInt(g -> g.getTasks().size()).sum(); diff --git a/src/main/java/com/toofifty/goaltracker/ui/NewTaskPanel.java b/src/main/java/com/toofifty/goaltracker/ui/NewTaskPanel.java index 10c586e..024446e 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/NewTaskPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/NewTaskPanel.java @@ -4,20 +4,19 @@ import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.task.Task; import com.toofifty.goaltracker.ui.components.TextButton; -import com.toofifty.goaltracker.ui.inputs.ItemTaskInput; -import com.toofifty.goaltracker.ui.inputs.ManualTaskInput; -import com.toofifty.goaltracker.ui.inputs.QuestTaskInput; -import com.toofifty.goaltracker.ui.inputs.SkillLevelTaskInput; -import com.toofifty.goaltracker.ui.inputs.SkillXpTaskInput; -import java.awt.BorderLayout; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.util.function.Consumer; -import javax.swing.JPanel; -import javax.swing.border.EmptyBorder; +import com.toofifty.goaltracker.ui.inputs.*; import net.runelite.client.ui.ColorScheme; -public class NewTaskPanel extends JPanel +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.util.function.Consumer; + +/** + * Panel for adding new tasks: quick Manual input and expandable options (skills, quests, items). + * Toggles a "More options" section and forwards created tasks to a listener. + */ +public final class NewTaskPanel extends JPanel { private final TextButton moreOptionsButton; private final GoalTrackerPlugin plugin; diff --git a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java index 5bf0f96..73bc89a 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java @@ -1,37 +1,32 @@ package com.toofifty.goaltracker.ui; import com.toofifty.goaltracker.GoalTrackerPlugin; +import com.toofifty.goaltracker.models.ActionHistory; import com.toofifty.goaltracker.models.Goal; +import com.toofifty.goaltracker.models.ToggleCompleteAction; +import com.toofifty.goaltracker.models.enums.Status; +import com.toofifty.goaltracker.models.task.ManualTask; import com.toofifty.goaltracker.models.task.Task; import com.toofifty.goaltracker.services.TaskIconService; -import com.toofifty.goaltracker.models.task.QuestTask; -import com.toofifty.goaltracker.utils.QuestRequirements; -import java.util.List; -import java.util.Locale; - -import com.toofifty.goaltracker.ui.components.ListPanel; import com.toofifty.goaltracker.ui.components.ListItemPanel; -import javax.swing.SwingUtilities; import javax.swing.*; import javax.swing.border.EmptyBorder; import java.awt.*; - -import static com.toofifty.goaltracker.utils.Constants.STATUS_TO_COLOR; - -import com.toofifty.goaltracker.models.ActionHistory; -import com.toofifty.goaltracker.models.ToggleCompleteAction; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import com.toofifty.goaltracker.models.enums.Status; - import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; -import java.awt.CardLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.Locale; -import com.toofifty.goaltracker.models.task.ManualTask; +import static com.toofifty.goaltracker.utils.Constants.STATUS_TO_COLOR; -public class TaskItemContent extends JPanel implements Refreshable +/** + * UI component for rendering a single Task row inside a Goal. + * Shows icon, title (editable for manual tasks), right-click context menu, + * and handles refreshing on task updates. + */ +public final class TaskItemContent extends JPanel implements Refreshable { private final Task task; private final Goal goal; @@ -93,7 +88,7 @@ public class TaskItemContent extends JPanel implements Refreshable { private void showMenuIfNeeded(MouseEvent e) { - if (!(e.isPopupTrigger() || javax.swing.SwingUtilities.isRightMouseButton(e))) + if (!(e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))) { return; } diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java index c919ff0..fb29c12 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBar.java @@ -1,12 +1,15 @@ package com.toofifty.goaltracker.ui.components; -import java.awt.FlowLayout; -import javax.swing.JPanel; -import javax.swing.border.EmptyBorder; -import javax.swing.BoxLayout; import net.runelite.client.ui.ColorScheme; -public class ActionBar extends JPanel +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; + +/** + * Horizontal container with left- and right-aligned sections for action buttons. + */ +public final class ActionBar extends JPanel { private final JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 3, 0)); private final JPanel right = new JPanel(new FlowLayout(FlowLayout.RIGHT, 3, 0)); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java index 70b6219..efca3bf 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ActionBarButton.java @@ -1,20 +1,18 @@ package com.toofifty.goaltracker.ui.components; -import java.awt.Cursor; -import java.awt.Graphics; -import java.awt.Insets; -import java.awt.RenderingHints; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import javax.swing.JButton; -import javax.swing.border.EmptyBorder; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.FontManager; -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.Dimension; -public class ActionBarButton extends JButton +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +/** + * Styled small button used inside ActionBar, with hover background effect. + */ +public final class ActionBarButton extends JButton { private boolean hover; diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java b/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java index 3591d57..c86c4fe 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ComboBox.java @@ -1,27 +1,16 @@ package com.toofifty.goaltracker.ui.components; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Font; -import java.util.List; -import java.util.function.Function; -import javax.swing.DefaultComboBoxModel; -import javax.swing.ImageIcon; -import javax.swing.JButton; -import javax.swing.JComboBox; -import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.ListCellRenderer; -import javax.swing.SwingUtilities; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.util.Text; + +import javax.swing.*; import javax.swing.border.EmptyBorder; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; import javax.swing.plaf.basic.BasicComboBoxUI; - -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.util.Text; +import java.awt.*; +import java.util.List; +import java.util.function.Function; /** * Simple generic ComboBox with optional formatter and a renderer @@ -32,7 +21,10 @@ * - Custom up/down arrow icons loaded from resources (combo_arrow_down.png / combo_arrow_up.png) * - Compact mode (~10% smaller font), or arbitrary font scaling via setFontScale */ -public class ComboBox extends JComboBox +/** + * Generic RuneLite-styled combo box with optional formatting, custom arrow icons, and font scaling. + */ +public final class ComboBox extends JComboBox { private Function formatter = null; diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/EditableInput.java b/src/main/java/com/toofifty/goaltracker/ui/components/EditableInput.java index 771fc63..a6bc764 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/EditableInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/EditableInput.java @@ -1,29 +1,25 @@ package com.toofifty.goaltracker.ui.components; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.event.KeyAdapter; -import java.awt.event.KeyEvent; -import java.awt.event.MouseListener; -import java.util.function.Consumer; -import javax.swing.BorderFactory; -import javax.swing.JPanel; +import lombok.Setter; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.components.FlatTextField; + +import javax.swing.*; import javax.swing.border.Border; import javax.swing.border.CompoundBorder; import javax.swing.border.EmptyBorder; -import javax.swing.JPopupMenu; -import javax.swing.JMenuItem; -import javax.swing.KeyStroke; -import javax.swing.ActionMap; -import javax.swing.InputMap; import javax.swing.text.DefaultEditorKit; -import java.awt.Toolkit; -import lombok.Setter; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.components.FlatTextField; +import java.awt.*; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseListener; +import java.util.function.Consumer; -public class EditableInput extends JPanel +/** + * Editable text field with Save, Cancel, and Edit controls. + * Provides copy/paste shortcuts and context menu support. + */ +public final class EditableInput extends JPanel { private static final Border INPUT_BOTTOM_BORDER = new CompoundBorder( diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java index e2bdc16..88c8520 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java @@ -1,23 +1,23 @@ package com.toofifty.goaltracker.ui.components; -import com.toofifty.goaltracker.utils.ReorderableList; -import com.toofifty.goaltracker.ui.Refreshable; import com.toofifty.goaltracker.models.Goal; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; +import com.toofifty.goaltracker.ui.Refreshable; +import com.toofifty.goaltracker.utils.ReorderableList; +import net.runelite.client.ui.ColorScheme; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import javax.swing.border.MatteBorder; +import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.util.function.Consumer; import java.util.function.BiConsumer; -import javax.swing.JMenuItem; -import javax.swing.JPanel; -import javax.swing.JPopupMenu; -import javax.swing.JComponent; -import javax.swing.border.EmptyBorder; -import javax.swing.border.MatteBorder; -import net.runelite.client.ui.ColorScheme; +import java.util.function.Consumer; +/** + * Generic panel representing a list item (Goal or Task). + * Provides context menu actions (move, remove) and hover/press styling. + */ public class ListItemPanel extends JPanel implements Refreshable { protected final JMenuItem moveUp = new JMenuItem("Move up"); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java index 8fecf26..960a108 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java @@ -14,7 +14,11 @@ import java.util.function.Function; import java.util.stream.Collectors; -public class ListPanel extends JScrollPane implements Refreshable +/** + * Scrollable container that renders and manages a list of items with optional placeholder text. + * Supports reordering, removal, and refresh of child panels. + */ +public final class ListPanel extends JScrollPane implements Refreshable { private final JPanel listPanel = new JPanel(new GridBagLayout()); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java index ac397f3..5dc296e 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListTaskPanel.java @@ -1,22 +1,25 @@ package com.toofifty.goaltracker.ui.components; +import com.toofifty.goaltracker.models.ActionHistory; +import com.toofifty.goaltracker.models.ReorderTaskAction; +import com.toofifty.goaltracker.models.enums.Status; +import com.toofifty.goaltracker.models.task.QuestTask; +import com.toofifty.goaltracker.models.task.Task; import com.toofifty.goaltracker.ui.TaskItemContent; import com.toofifty.goaltracker.utils.QuestRequirements; - import com.toofifty.goaltracker.utils.ReorderableList; -import com.toofifty.goaltracker.models.task.Task; -import com.toofifty.goaltracker.models.enums.Status; -import java.util.function.Consumer; -import javax.swing.JMenuItem; + +import javax.swing.*; +import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; -import java.awt.Component; -import javax.swing.SwingUtilities; -import java.awt.Container; -import com.toofifty.goaltracker.models.ActionHistory; -import com.toofifty.goaltracker.models.ReorderTaskAction; +import java.util.function.Consumer; -public class ListTaskPanel extends ListItemPanel +/** + * Task row panel with context menu (move, indent, toggle), + * cascading child handling, and shift-click remove support. + */ +public final class ListTaskPanel extends ListItemPanel { private TaskItemContent taskContent; @@ -58,7 +61,7 @@ public ListTaskPanel(ReorderableList list, Task item) int baseIndent = item.getIndentLevel(); // Collect affected tasks (item + descendants until sibling/parent) - java.util.List affected = new java.util.ArrayList<>(); + java.util.List affected = new java.util.ArrayList<>(); java.util.List oldIndents = new java.util.ArrayList<>(); affected.add(item); @@ -102,7 +105,7 @@ public ListTaskPanel(ReorderableList list, Task item) int baseIndent = item.getIndentLevel(); // Collect affected tasks (item + descendants until sibling/parent) - java.util.List affected = new java.util.ArrayList<>(); + java.util.List affected = new java.util.ArrayList<>(); java.util.List oldIndents = new java.util.ArrayList<>(); affected.add(item); @@ -279,8 +282,8 @@ public void refreshMenu() popupMenu.add(toggleStatusItem); // Add quest pre-reqs menu item only if the quest actually has prereqs - if (item instanceof com.toofifty.goaltracker.models.task.QuestTask) { - com.toofifty.goaltracker.models.task.QuestTask questTask = (com.toofifty.goaltracker.models.task.QuestTask) item; + if (item instanceof QuestTask) { + QuestTask questTask = (QuestTask) item; int baseIndent = item.getIndentLevel(); // Gather existing direct/descendant children under this quest to avoid duplicates java.util.Set existingKeys = new java.util.HashSet<>(); @@ -293,9 +296,9 @@ public void refreshMenu() existingKeys.add(child.getClass().getName() + "|" + child.toString()); } var rawPrereqs = QuestRequirements.getRequirements(questTask.getQuest(), baseIndent + 1); - java.util.List missingPrereqs = new java.util.ArrayList<>(); + java.util.List missingPrereqs = new java.util.ArrayList<>(); if (rawPrereqs != null) { - for (com.toofifty.goaltracker.models.task.Task p : rawPrereqs) { + for (Task p : rawPrereqs) { String key = p.getClass().getName() + "|" + p.toString(); if (!existingKeys.contains(key)) { missingPrereqs.add(p); @@ -316,8 +319,8 @@ public void refreshMenu() if (child.getIndentLevel() <= baseIndent) break; currentKeys.add(child.getClass().getName() + "|" + child.toString()); } - java.util.List filtered = new java.util.ArrayList<>(); - for (com.toofifty.goaltracker.models.task.Task t : raw) { + java.util.List filtered = new java.util.ArrayList<>(); + for (Task t : raw) { String key = t.getClass().getName() + "|" + t.toString(); if (!currentKeys.contains(key)) { filtered.add(t); @@ -325,7 +328,7 @@ public void refreshMenu() } if (!filtered.isEmpty()) { int index = list.indexOf(item); - for (com.toofifty.goaltracker.models.task.Task prereq : filtered) { + for (Task prereq : filtered) { list.add(index + 1, prereq); index++; } @@ -485,8 +488,8 @@ protected void buildAdditionalMenu() popupMenu.add(toggleStatusItem); // Quest pre-reqs (if available) - if (item instanceof com.toofifty.goaltracker.models.task.QuestTask) { - com.toofifty.goaltracker.models.task.QuestTask questTask = (com.toofifty.goaltracker.models.task.QuestTask) item; + if (item instanceof QuestTask) { + QuestTask questTask = (QuestTask) item; int baseIndent = item.getIndentLevel(); java.util.Set existingKeys = new java.util.HashSet<>(); int parentIndex = list.indexOf(item); @@ -496,9 +499,9 @@ protected void buildAdditionalMenu() existingKeys.add(child.getClass().getName() + "|" + child.toString()); } var rawPrereqs = QuestRequirements.getRequirements(questTask.getQuest(), baseIndent + 1); - java.util.List missingPrereqs = new java.util.ArrayList<>(); + java.util.List missingPrereqs = new java.util.ArrayList<>(); if (rawPrereqs != null) { - for (com.toofifty.goaltracker.models.task.Task p : rawPrereqs) { + for (Task p : rawPrereqs) { String key = p.getClass().getName() + "|" + p.toString(); if (!existingKeys.contains(key)) missingPrereqs.add(p); } @@ -515,14 +518,14 @@ protected void buildAdditionalMenu() if (child.getIndentLevel() <= baseIndent) break; currentKeys.add(child.getClass().getName() + "|" + child.toString()); } - java.util.List filtered = new java.util.ArrayList<>(); - for (com.toofifty.goaltracker.models.task.Task t : raw) { + java.util.List filtered = new java.util.ArrayList<>(); + for (Task t : raw) { String key = t.getClass().getName() + "|" + t.toString(); if (!currentKeys.contains(key)) filtered.add(t); } if (!filtered.isEmpty()) { int insertIndex = list.indexOf(item); - for (com.toofifty.goaltracker.models.task.Task prereq : filtered) { + for (Task prereq : filtered) { list.add(insertIndex + 1, prereq); insertIndex++; } @@ -571,10 +574,10 @@ private void refreshParentList() */ public static void addPrereqsForTask(ReorderableList list, Task item) { - if (!(item instanceof com.toofifty.goaltracker.models.task.QuestTask)) { + if (!(item instanceof QuestTask)) { return; } - com.toofifty.goaltracker.models.task.QuestTask questTask = (com.toofifty.goaltracker.models.task.QuestTask) item; + QuestTask questTask = (QuestTask) item; int baseIndent = item.getIndentLevel(); // Gather existing children under this task to avoid duplicates @@ -586,14 +589,14 @@ public static void addPrereqsForTask(ReorderableList list, Task item) existingKeys.add(child.getClass().getName() + "|" + child.toString()); } - java.util.List raw = QuestRequirements.getRequirements(questTask.getQuest(), baseIndent + 1); + java.util.List raw = QuestRequirements.getRequirements(questTask.getQuest(), baseIndent + 1); if (raw == null || raw.isEmpty()) { return; } // Filter out any already-present entries - java.util.List filtered = new java.util.ArrayList<>(); - for (com.toofifty.goaltracker.models.task.Task t : raw) { + java.util.List filtered = new java.util.ArrayList<>(); + for (Task t : raw) { String key = t.getClass().getName() + "|" + t.toString(); if (!existingKeys.contains(key)) { filtered.add(t); @@ -606,7 +609,7 @@ public static void addPrereqsForTask(ReorderableList list, Task item) // Insert directly after the parent item, preserving order from QuestRequirements int insertIndex = list.indexOf(item); - for (com.toofifty.goaltracker.models.task.Task prereq : filtered) { + for (Task prereq : filtered) { list.add(insertIndex + 1, prereq); insertIndex++; } diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java b/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java index 3adb92a..36b2d0c 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/TextButton.java @@ -1,16 +1,20 @@ package com.toofifty.goaltracker.ui.components; -import java.awt.Color; -import java.awt.Cursor; +import net.runelite.client.ui.ColorScheme; +import net.runelite.client.ui.FontManager; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.function.Consumer; -import javax.swing.JLabel; -import javax.swing.border.EmptyBorder; -import net.runelite.client.ui.ColorScheme; -import net.runelite.client.ui.FontManager; -public class TextButton extends JLabel +/** + * Small clickable label styled as a button, used for text-based actions. + * Supports hover effects and consumer-based click listeners. + */ +public final class TextButton extends JLabel { private Color mainColor = ColorScheme.PROGRESS_COMPLETE_COLOR; diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java index cefe64a..9a2161d 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/ItemTaskInput.java @@ -13,25 +13,22 @@ import net.runelite.client.ui.components.FlatTextField; import javax.swing.*; -import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; -import java.awt.*; -import java.awt.Point; -import java.awt.KeyboardFocusManager; -import java.awt.Window; -import java.util.regex.Pattern; -import java.awt.Color; -import javax.swing.JDialog; -import javax.swing.JButton; -import java.awt.event.ActionListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.JTextComponent; +import java.awt.*; +import java.awt.event.ActionListener; +import java.util.regex.Pattern; - -public class ItemTaskInput extends TaskInput +/** + * Input panel for creating Item tasks. + * Provides search with close overlay, quantity field with k/m suffix support, + * and automatic task submission on selection. + */ +public final class ItemTaskInput extends TaskInput { private final ItemManager itemManager; private final ClientThread clientThread; @@ -230,7 +227,7 @@ public void actionPerformed(java.awt.event.ActionEvent ev) { { if (++attempts[0] < maxAttempts) { - new javax.swing.Timer(delayMs, this) {{ setRepeats(false); }}.start(); + new Timer(delayMs, this) {{ setRepeats(false); }}.start(); } return; } @@ -254,8 +251,8 @@ public void actionPerformed(java.awt.event.ActionEvent ev) { lastSearchText = ""; if (target instanceof JDialog) { ((JDialog) target).dispose(); - } else if (target instanceof javax.swing.JFrame) { - ((javax.swing.JFrame) target).dispose(); + } else if (target instanceof JFrame) { + ((JFrame) target).dispose(); } else { target.dispose(); } @@ -322,7 +319,7 @@ public void componentResized(java.awt.event.ComponentEvent e) { }; // start first attempt slightly delayed to give the popup time to appear - new javax.swing.Timer(delayMs, tryShow) {{ setRepeats(false); }}.start(); + new Timer(delayMs, tryShow) {{ setRepeats(false); }}.start(); } // Try to locate the RuneLite Item Search popup window reliably diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/ManualTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/ManualTaskInput.java index 39fe519..bb5e479 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/ManualTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/ManualTaskInput.java @@ -10,7 +10,11 @@ import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; -public class ManualTaskInput extends TaskInput +/** + * Input panel for creating simple manual tasks. + * Provides a text field with Enter-to-submit behavior. + */ +public final class ManualTaskInput extends TaskInput { private final FlatTextField titleField; diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java index 2225e6f..003c846 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/QuestTaskInput.java @@ -3,23 +3,22 @@ import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.task.QuestTask; -import com.toofifty.goaltracker.models.task.Task; -import com.toofifty.goaltracker.utils.QuestRequirements; import com.toofifty.goaltracker.ui.components.ComboBox; import net.runelite.api.Quest; -import net.runelite.client.ui.ColorScheme; import javax.swing.*; -import javax.swing.border.EmptyBorder; import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; import java.util.Arrays; import java.util.Comparator; import java.util.List; -import java.util.ArrayList; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -public class QuestTaskInput extends TaskInput +/** + * Input panel for creating Quest tasks. + * Provides a searchable dropdown with RuneScape UF font and Enter-to-submit support. + */ +public final class QuestTaskInput extends TaskInput { private final List allQuests; private Quest bestMatch; diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillLevelTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillLevelTaskInput.java index fc29ba8..b8e9e54 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillLevelTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillLevelTaskInput.java @@ -14,7 +14,11 @@ import java.awt.*; import java.util.regex.Pattern; -public class SkillLevelTaskInput extends TaskInput +/** + * Input panel for creating Skill Level tasks. + * Provides a numeric level field (validated 1–99) and a skill dropdown. + */ +public final class SkillLevelTaskInput extends TaskInput { private FlatTextField levelField; private String levelFieldValue = "99"; diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillXpTaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillXpTaskInput.java index f02a9a4..6d2275e 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillXpTaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/SkillXpTaskInput.java @@ -14,7 +14,11 @@ import java.awt.*; import java.util.regex.Pattern; -public class SkillXpTaskInput extends TaskInput +/** + * Input panel for creating Skill XP tasks. + * Provides a numeric XP field (supports k/m suffixes, capped at 200m) and a skill dropdown. + */ +public final class SkillXpTaskInput extends TaskInput { private final FlatTextField xpField; private final ComboBox skillField; diff --git a/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java b/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java index 804231a..bed95c1 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java +++ b/src/main/java/com/toofifty/goaltracker/ui/inputs/TaskInput.java @@ -1,21 +1,24 @@ + package com.toofifty.goaltracker.ui.inputs; import com.toofifty.goaltracker.GoalTrackerPlugin; import com.toofifty.goaltracker.models.Goal; import com.toofifty.goaltracker.models.task.Task; import com.toofifty.goaltracker.ui.components.TextButton; -import java.awt.BorderLayout; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.border.EmptyBorder; import lombok.Getter; import net.runelite.client.ui.ColorScheme; import net.runelite.client.ui.FontManager; -import java.util.function.Consumer; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; import java.util.Collection; +import java.util.function.Consumer; +/** + * Abstract base panel for creating new tasks. + * Provides a title label, input row, optional Add button, and submit handling. + */ public abstract class TaskInput extends JPanel { protected final int PREFERRED_INPUT_HEIGHT = 16; diff --git a/src/main/java/com/toofifty/goaltracker/utils/Constants.java b/src/main/java/com/toofifty/goaltracker/utils/Constants.java index 84b91c3..f03e34c 100644 --- a/src/main/java/com/toofifty/goaltracker/utils/Constants.java +++ b/src/main/java/com/toofifty/goaltracker/utils/Constants.java @@ -6,7 +6,10 @@ import java.awt.*; import java.util.Map; -public class Constants { +/** + * Shared plugin-wide constants (colors, keys, etc.). + */ +public final class Constants { public static final Map STATUS_TO_COLOR = Map.of( Status.NOT_STARTED, ColorScheme.PROGRESS_ERROR_COLOR, Status.IN_PROGRESS, ColorScheme.PROGRESS_INPROGRESS_COLOR, diff --git a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java index b468856..9ca9285 100644 --- a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java +++ b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java @@ -5,14 +5,14 @@ import com.toofifty.goaltracker.models.task.Task; import net.runelite.api.Quest; import net.runelite.api.Skill; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class QuestRequirements + +import java.util.*; + +/** + * Static lookup of quest prerequisites (quests + skill levels) and a helper to + * expand nested requirements with appropriate indent levels. + */ +public final class QuestRequirements { // Map of quests to their requirements (quests and skills) private static final Map> REQUIREMENT_MAP; diff --git a/src/main/java/com/toofifty/goaltracker/utils/ReorderableList.java b/src/main/java/com/toofifty/goaltracker/utils/ReorderableList.java index 918df7c..9be4327 100644 --- a/src/main/java/com/toofifty/goaltracker/utils/ReorderableList.java +++ b/src/main/java/com/toofifty/goaltracker/utils/ReorderableList.java @@ -1,3 +1,4 @@ + package com.toofifty.goaltracker.utils; import java.util.ArrayList; @@ -5,7 +6,11 @@ import java.util.Collections; import java.util.List; -public class ReorderableList extends ArrayList +/** + * ArrayList with helper methods to move elements up, down, top, or bottom. + * Used to maintain user-specified ordering for tasks and goals. + */ +public final class ReorderableList extends ArrayList { private void move(T item, int offset) { int i = this.indexOf(item); diff --git a/src/test/java/com/toofifty/goaltracker/GoalSerializerTest.java b/src/test/java/com/toofifty/goaltracker/GoalSerializerTest.java index 150f499..8faf56b 100644 --- a/src/test/java/com/toofifty/goaltracker/GoalSerializerTest.java +++ b/src/test/java/com/toofifty/goaltracker/GoalSerializerTest.java @@ -1,11 +1,15 @@ package com.toofifty.goaltracker; -import com.google.common.io.Resources; +import com.toofifty.goaltracker.models.Goal; +import com.toofifty.goaltracker.models.task.ManualTask; +import com.toofifty.goaltracker.models.task.SkillLevelTask; +import com.toofifty.goaltracker.models.task.SkillXpTask; +import com.toofifty.goaltracker.models.task.QuestTask; +import com.toofifty.goaltracker.models.task.ItemTask; import com.toofifty.goaltracker.models.enums.Status; import com.toofifty.goaltracker.models.enums.TaskType; -import com.toofifty.goaltracker.models.Goal; -import com.toofifty.goaltracker.models.task.*; import com.toofifty.goaltracker.utils.ReorderableList; +import com.google.common.io.Resources; import net.runelite.api.Quest; import net.runelite.api.Skill; import org.junit.jupiter.api.Test; diff --git a/src/test/java/com/toofifty/goaltracker/models/GoalTest.java b/src/test/java/com/toofifty/goaltracker/models/GoalTest.java index ea4cdab..4df7d57 100644 --- a/src/test/java/com/toofifty/goaltracker/models/GoalTest.java +++ b/src/test/java/com/toofifty/goaltracker/models/GoalTest.java @@ -1,9 +1,9 @@ package com.toofifty.goaltracker.models; -import com.toofifty.goaltracker.utils.ReorderableList; import com.toofifty.goaltracker.models.enums.Status; import com.toofifty.goaltracker.models.task.ItemTask; import com.toofifty.goaltracker.models.task.ManualTask; +import com.toofifty.goaltracker.utils.ReorderableList; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; diff --git a/src/test/java/com/toofifty/goaltracker/models/enums/TaskTypeTest.java b/src/test/java/com/toofifty/goaltracker/models/enums/TaskTypeTest.java index 124b8a8..93bd2ab 100644 --- a/src/test/java/com/toofifty/goaltracker/models/enums/TaskTypeTest.java +++ b/src/test/java/com/toofifty/goaltracker/models/enums/TaskTypeTest.java @@ -2,7 +2,8 @@ import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; class TaskTypeTest { @Test diff --git a/src/test/java/com/toofifty/goaltracker/models/task/ItemTaskTest.java b/src/test/java/com/toofifty/goaltracker/models/task/ItemTaskTest.java index ef40ccf..f9bdb33 100644 --- a/src/test/java/com/toofifty/goaltracker/models/task/ItemTaskTest.java +++ b/src/test/java/com/toofifty/goaltracker/models/task/ItemTaskTest.java @@ -3,7 +3,7 @@ import com.toofifty.goaltracker.models.enums.TaskType; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class ItemTaskTest { diff --git a/src/test/java/com/toofifty/goaltracker/models/task/ManualTaskTest.java b/src/test/java/com/toofifty/goaltracker/models/task/ManualTaskTest.java index 300d6a1..bf31a3b 100644 --- a/src/test/java/com/toofifty/goaltracker/models/task/ManualTaskTest.java +++ b/src/test/java/com/toofifty/goaltracker/models/task/ManualTaskTest.java @@ -4,7 +4,7 @@ import com.toofifty.goaltracker.models.enums.TaskType; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class ManualTaskTest { diff --git a/src/test/java/com/toofifty/goaltracker/models/task/QuestTaskTest.java b/src/test/java/com/toofifty/goaltracker/models/task/QuestTaskTest.java index d3f7117..92f3572 100644 --- a/src/test/java/com/toofifty/goaltracker/models/task/QuestTaskTest.java +++ b/src/test/java/com/toofifty/goaltracker/models/task/QuestTaskTest.java @@ -4,7 +4,7 @@ import net.runelite.api.Quest; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class QuestTaskTest { diff --git a/src/test/java/com/toofifty/goaltracker/models/task/SkillLevelTaskTest.java b/src/test/java/com/toofifty/goaltracker/models/task/SkillLevelTaskTest.java index 572fae2..d3d1675 100644 --- a/src/test/java/com/toofifty/goaltracker/models/task/SkillLevelTaskTest.java +++ b/src/test/java/com/toofifty/goaltracker/models/task/SkillLevelTaskTest.java @@ -4,7 +4,7 @@ import net.runelite.api.Skill; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class SkillLevelTaskTest { diff --git a/src/test/java/com/toofifty/goaltracker/models/task/SkillXpTaskTest.java b/src/test/java/com/toofifty/goaltracker/models/task/SkillXpTaskTest.java index ff5e777..5f92f4f 100644 --- a/src/test/java/com/toofifty/goaltracker/models/task/SkillXpTaskTest.java +++ b/src/test/java/com/toofifty/goaltracker/models/task/SkillXpTaskTest.java @@ -4,7 +4,7 @@ import net.runelite.api.Skill; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; class SkillXpTaskTest { @Test diff --git a/src/test/java/com/toofifty/goaltracker/models/task/TaskTest.java b/src/test/java/com/toofifty/goaltracker/models/task/TaskTest.java index 415f019..7c4c048 100644 --- a/src/test/java/com/toofifty/goaltracker/models/task/TaskTest.java +++ b/src/test/java/com/toofifty/goaltracker/models/task/TaskTest.java @@ -3,7 +3,8 @@ import com.toofifty.goaltracker.models.enums.Status; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; class TaskTest { @Test diff --git a/src/test/java/com/toofifty/goaltracker/services/TaskIconServiceTest.java b/src/test/java/com/toofifty/goaltracker/services/TaskIconServiceTest.java index 61984dc..05de33a 100644 --- a/src/test/java/com/toofifty/goaltracker/services/TaskIconServiceTest.java +++ b/src/test/java/com/toofifty/goaltracker/services/TaskIconServiceTest.java @@ -1,7 +1,13 @@ package com.toofifty.goaltracker.services; +import com.toofifty.goaltracker.models.task.Task; +import com.toofifty.goaltracker.models.task.ManualTask; +import com.toofifty.goaltracker.models.task.QuestTask; +import com.toofifty.goaltracker.models.task.SkillLevelTask; +import com.toofifty.goaltracker.models.task.SkillXpTask; +import com.toofifty.goaltracker.models.task.ItemTask; + import com.toofifty.goaltracker.models.enums.Status; -import com.toofifty.goaltracker.models.task.*; import net.runelite.api.Client; import net.runelite.api.Skill; import net.runelite.client.game.ItemManager; diff --git a/src/test/java/com/toofifty/goaltracker/services/TaskUpdateServiceTest.java b/src/test/java/com/toofifty/goaltracker/services/TaskUpdateServiceTest.java index beadf6a..e8ddf3a 100644 --- a/src/test/java/com/toofifty/goaltracker/services/TaskUpdateServiceTest.java +++ b/src/test/java/com/toofifty/goaltracker/services/TaskUpdateServiceTest.java @@ -1,8 +1,14 @@ package com.toofifty.goaltracker.services; +import com.toofifty.goaltracker.models.task.Task; +import com.toofifty.goaltracker.models.task.SkillLevelTask; +import com.toofifty.goaltracker.models.task.SkillXpTask; +import com.toofifty.goaltracker.models.task.QuestTask; +import com.toofifty.goaltracker.models.task.ItemTask; +import com.toofifty.goaltracker.models.task.ManualTask; + import com.toofifty.goaltracker.ItemCache; import com.toofifty.goaltracker.models.enums.Status; -import com.toofifty.goaltracker.models.task.*; import net.runelite.api.*; import net.runelite.api.events.StatChanged; import org.junit.jupiter.api.Test; @@ -11,7 +17,9 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) From 3bf90767a67d8235048c7a710d6640e94c13325a Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 25 Aug 2025 14:12:17 -0400 Subject: [PATCH 44/53] Imported my V2 version into this branch and updated the paths to go back to the OG path and started removing any\all v2. Currently some stuff still lingers but it's running and working. --- README.md | 10 +++++----- ...d_from_preset_v2.jpg => add_from_preset.jpg} | Bin img/goal_panel_v2.jpg | Bin 69957 -> 0 bytes ...al_task_panel_v2.jpg => goal_task_panel.jpg} | Bin img/home_panel_new.jpg | Bin 0 -> 61816 bytes ...click_goals_v2.jpg => right_click_goals.jpg} | Bin runelite-plugin.properties | 6 +++--- 7 files changed, 8 insertions(+), 8 deletions(-) rename img/{add_from_preset_v2.jpg => add_from_preset.jpg} (100%) delete mode 100644 img/goal_panel_v2.jpg rename img/{goal_task_panel_v2.jpg => goal_task_panel.jpg} (100%) create mode 100644 img/home_panel_new.jpg rename img/{right_click_goals_v2.jpg => right_click_goals.jpg} (100%) diff --git a/README.md b/README.md index e4bf96b..6a52ef6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🏆 Goal Tracker v2 +# 🏆 Goal Tracker > A complete reimagining of the Goal Tracker plugin — rebuilt with a more modern UI, powerful new features, and improved stability to help you plan, track, and achieve your Old School RuneScape goals with ease. @@ -77,19 +77,19 @@ ### Home panel with goal cards *Redesigned goal cards with progress bars, hover effects, and instant refresh — plus the new “Add from Preset” button and Export/Import features for easy sharing and backups.* -![Goal Tracker Home Panel](img/goal_panel_v2.jpg) +![Goal Tracker Home Panel](img/home_panel_new.jpg) ### Home panel right‑click menu *New options: move goal up/down, mark all complete/incomplete, and pin/unpin goals.* -![Goal Tracker Right Click Menu](img/right_click_goals_v2.jpg) +![Goal Tracker Right Click Menu](img/right_click_goals.jpg) ### Inside a goal with task list *Task rows now show icons, ellipsized text, and inline editing — with redesigned item search & quest features, plus an Add Pre-reqs button for quests (using our built-in list), along with Undo/Redo buttons.* -![Goal Tracker Task List](img/goal_task_panel_v2.jpg) +![Goal Tracker Task List](img/goal_task_panel.jpg) ### Add from Preset *Quickly insert curated presets like Ironman Progression, Void set, or Barrows Gear.* -![Goal Tracker Add From Preset](img/add_from_preset_v2.jpg) +![Goal Tracker Add From Preset](img/add_from_preset.jpg) diff --git a/img/add_from_preset_v2.jpg b/img/add_from_preset.jpg similarity index 100% rename from img/add_from_preset_v2.jpg rename to img/add_from_preset.jpg diff --git a/img/goal_panel_v2.jpg b/img/goal_panel_v2.jpg deleted file mode 100644 index dce5882f64059e23b419da309058ce2452f318ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69957 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 literal 0 HcmV?d00001 diff --git a/img/right_click_goals_v2.jpg b/img/right_click_goals.jpg similarity index 100% rename from img/right_click_goals_v2.jpg rename to img/right_click_goals.jpg diff --git a/runelite-plugin.properties b/runelite-plugin.properties index 5c96863..bbe70bb 100644 --- a/runelite-plugin.properties +++ b/runelite-plugin.properties @@ -1,7 +1,7 @@ -displayName=Goal Tracker v2 +displayName=Goal Tracker author=AhDoozy -support=https://github.com/AhDoozy/goal-tracker-v2 +support=https://github.com/toofifty/goaltracker description=Track and organize RuneScape goals with progress bars, presets, and more. tags=task,to-do,todo,Track,organize,progress -plugins=com.ahdoozy.goaltrackerv2.GoalTrackerV2Plugin +plugins=com.toofifty.goaltracker.GoalTrackerPlugin version=1.0.0 From 0868b8dc34f8315b9887508ba35afda93d571ca4 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 25 Aug 2025 14:18:29 -0400 Subject: [PATCH 45/53] Imported my V2 version into this branch and updated the paths to go back to the OG path and started removing any\all v2. Currently some stuff still lingers but it's running and working. --- README.md | 14 ++------------ changelog.md | 2 +- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6a52ef6..6ff157b 100644 --- a/README.md +++ b/README.md @@ -88,21 +88,11 @@ ![Goal Tracker Task List](img/goal_task_panel.jpg) ### Add from Preset -*Quickly insert curated presets like Ironman Progression, Void set, or Barrows Gear.* -![Goal Tracker Add From Preset](img/add_from_preset.jpg) +*Quickly insert curated presets like Ironman Progression, Void set, or Barrows Gear.* +![Goal Tracker Add From Preset](img/add_from_preset.jpg) -
-

🙏 Acknowledgements

- -- Original plugin created by **dillydill123**. -- Renovated and maintained by **AhDoozy**. - -
-## 📄 License -Licensed under the [BSD 2-Clause License](LICENSE). - -----

📜 Original Goal Tracker v1 Readme & Documentation (created by dillydill123)

diff --git a/changelog.md b/changelog.md index 261e94e..ee33b72 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,4 @@ -# Changelog +# AhDoozy's Changelog ### Added - Item task auto-detection across inventory, equipment, bank, seed vault, and group storage. From a7520b7870d370c098f9a931f0505caed092343c Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 25 Aug 2025 14:48:58 -0400 Subject: [PATCH 46/53] Imported my V2 version into this branch and updated the paths to go back to the OG path and started removing any\all v2. Currently some stuff still lingers but it's running and working. --- LICENSE | 1 - runelite-plugin.properties | 2 +- settings.gradle | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 97a109d..180ab2e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,6 @@ BSD 2-Clause License Copyright (c) 2019, dillydill123 -Copyright (c) 2025, AhDoozy All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/runelite-plugin.properties b/runelite-plugin.properties index bbe70bb..7530cc8 100644 --- a/runelite-plugin.properties +++ b/runelite-plugin.properties @@ -4,4 +4,4 @@ support=https://github.com/toofifty/goaltracker description=Track and organize RuneScape goals with progress bars, presets, and more. tags=task,to-do,todo,Track,organize,progress plugins=com.toofifty.goaltracker.GoalTrackerPlugin -version=1.0.0 +version=2.0.0 diff --git a/settings.gradle b/settings.gradle index 551432b..d3f8def 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'goal-tracker-v2' +rootProject.name = 'rl-goal-tracker' From ad212a6a26fd9c5bfe5b2a5b87d270744873bf07 Mon Sep 17 00:00:00 2001 From: AhDoozy <90292189+AhDoozy@users.noreply.github.com> Date: Mon, 25 Aug 2025 14:53:06 -0400 Subject: [PATCH 47/53] Remove V2 naming and ahdoozy paths --- build.gradle | 4 ++-- changelog.md | 2 +- runelite-plugin.properties | 2 +- .../java/com/toofifty/goaltracker/GoalTrackerPlugin.java | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index b565110..cd134de 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ dependencies { testImplementation "net.runelite:jshell:${runeLiteVersion}" } -group = 'com.ahdoozy.goaltrackerv2' +group = 'com.toofifty.goaltracker' version = '1.0.4' tasks.withType(JavaCompile) { @@ -62,4 +62,4 @@ jacocoTestReport { tasks.withType(Jar) { preserveFileTimestamps = false reproducibleFileOrder = true -} \ No newline at end of file +} diff --git a/changelog.md b/changelog.md index ee33b72..261e94e 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,4 @@ -# AhDoozy's Changelog +# Changelog ### Added - Item task auto-detection across inventory, equipment, bank, seed vault, and group storage. diff --git a/runelite-plugin.properties b/runelite-plugin.properties index bbe70bb..3e5ab87 100644 --- a/runelite-plugin.properties +++ b/runelite-plugin.properties @@ -1,5 +1,5 @@ displayName=Goal Tracker -author=AhDoozy +author=toofifty support=https://github.com/toofifty/goaltracker description=Track and organize RuneScape goals with progress bars, presets, and more. tags=task,to-do,todo,Track,organize,progress diff --git a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java index 48af6c6..7c6ad35 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java +++ b/src/main/java/com/toofifty/goaltracker/GoalTrackerPlugin.java @@ -41,7 +41,7 @@ import java.util.List; @Slf4j -@PluginDescriptor(name = "Goal Tracker V2", description = "Keep track of your goals and complete them automatically") +@PluginDescriptor(name = "Goal Tracker", description = "Keep track of your goals and complete them automatically") /** * Main entry point for the Goal Tracker plugin. * Handles lifecycle (startup/shutdown), UI registration, and listens for @@ -195,7 +195,7 @@ protected void startUp() // Defensive guards to avoid NPEs during test bootstrap if DI bindings are missing if (goalManager == null || itemCache == null || goalTrackerPanel == null || itemManager == null || clientToolbar == null) { - log.warn("GoalTrackerV2Plugin: skipping full startup because a dependency was null. goalManager={}, itemCache={}, panel={}, itemManager={}, toolbar={}", + log.warn("GoalTrackerPlugin: skipping full startup because a dependency was null. goalManager={}, itemCache={}, panel={}, itemManager={}, toolbar={}", goalManager != null, itemCache != null, goalTrackerPanel != null, itemManager != null, clientToolbar != null); return; } @@ -204,7 +204,7 @@ protected void startUp() goalManager.load(); itemCache.load(); } catch (Exception ex) { - log.error("GoalTrackerV2Plugin: failed to load persisted state", ex); + log.error("GoalTrackerPlugin: failed to load persisted state", ex); } goalTrackerPanel.home(); @@ -212,7 +212,7 @@ protected void startUp() final AsyncBufferedImage icon = itemManager.getImage(ItemID.TODO_LIST); if (icon == null) { - log.warn("GoalTrackerV2Plugin: icon was null; skipping sidebar button creation"); + log.warn("GoalTrackerPlugin: icon was null; skipping sidebar button creation"); } else { From 49aa9073bd23c9f4b23a831b51ff670aac5e048e Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 25 Aug 2025 14:55:54 -0400 Subject: [PATCH 48/53] Imported my V2 version into this branch and updated the paths to go back to the OG path and started removing any\all v2. Currently some stuff still lingers but it's running and working. --- changelog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 261e94e..0eee3bb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,4 +1,5 @@ -# Changelog +# AhDoozy's Changelog +Please leave for my sanity. thanks :) ### Added - Item task auto-detection across inventory, equipment, bank, seed vault, and group storage. From 1193ee4c10ee83ed684666a373130b3bd4b98e49 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 25 Aug 2025 15:34:55 -0400 Subject: [PATCH 49/53] readme update --- README.md | 134 ++++++++++++++++++------------------------------------ 1 file changed, 44 insertions(+), 90 deletions(-) diff --git a/README.md b/README.md index 6ff157b..d33cfc8 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,51 @@ # 🏆 Goal Tracker -> A complete reimagining of the Goal Tracker plugin — rebuilt with a more modern UI, powerful new features, and improved stability to help you plan, track, and achieve your Old School RuneScape goals with ease. +> A reimagining of the Goal Tracker plugin — rebuilt with a more modern UI, powerful new features, and improved stability to help you plan, track, and achieve your Old School RuneScape goals with ease. +> It is now faster, cleaner, and more reliable — introducing a Quest Requirements system, presets, improved visuals, and smoother task/goal management. --- -
-

✨ New Features

- -- Shift+Click removal of tasks for faster task management -- New ActionBar and ActionBarButton UI components -- Hover states for better visual feedback -- New context menu organization for streamlined interaction -- Search toggle improvements for easier task searching -- New task right‑click menu with grouped **Move** submenu and cascading complete/incomplete toggle that applies to children -- Redesigned goal cards with lighter fills, full shadows, and hover/press effects on the card face -- Export and Import functionality to save and load goals via JSON files -- Presets automatically expand quest prerequisites -- Goal titles and ManualTask descriptions support click-to-edit with inline text fields -- Long goal and task titles are ellipsized with … and show full text on hover - -### ♻️ Redesigned Features - -- Redesigned quest prerequisites button for quick access -- Redesigned completion cascading to automatically complete related tasks -- Redesigned dropdown quest selector for faster quest task addition -- Redesigned right-click menus for prerequisites and child completion options -- Redesigned manual toggling for preset tasks -- Redesigned chatbox notification colors with customization options -- Redesigned quest status detection with improved auto-refresh -- Redesigned completion chat messages delivered as proper Game messages - -
- -
-

🔧 Improvements

- -- Refreshed UI with updated design elements and improved readability -- Consistent ActionBar UI throughout the plugin -- Unified goal view header for a cohesive look -- Improved context menus and hover detection accuracy -- Copy and paste support in the goal name input field -- Refined ActionBar spacing to fit Export and Import buttons without overlap -- Progress text (e.g., “1/10”) on goal cards now has reserved width and never clips -- + Add goal and Add from Preset buttons restyled and stacked vertically -- Task rows updated to match goal cards with icons and ellipsized titles -- Debounced quest detection and scheduled UI refresh to prevent lag -- Goal/task panels auto-refresh reliably after quest or item detection -- Extended item task detection to include equipment, seed vault, and group storage - -
- -
-

🐛 Fixes

- -- Undo/Redo functionality cleanup for smoother editing -- ActionBarButton painting fixes to prevent visual glitches -- Improved refresh behavior in Home and List panels -- Enhanced mouse selection accuracy -- Keyboard shortcut fixes and enhancements -- Automatic removal of empty goals to keep lists tidy -- Visual refresh issue resolved on login -- Fixed child task refresh issues -- Fixed blank panel issue when switching views -- Fixed completion chat messages to appear properly with configured colors -- Export/Import buttons now work correctly -- Item icons preload correctly on startup, login, and after import -- Home panel refreshes correctly after completing a task -- Layout adjustments to fix overlapping Export button -- Fixed Shield of Arrav preset indentation for partner-finding step -- Sidebar refreshes automatically after quest or item detection - -
- -
-

🖼️ Screenshots

- -### Home panel with goal cards -*Redesigned goal cards with progress bars, hover effects, and instant refresh — plus the new “Add from Preset” button and Export/Import features for easy sharing and backups.* -![Goal Tracker Home Panel](img/home_panel_new.jpg) - -### Home panel right‑click menu -*New options: move goal up/down, mark all complete/incomplete, and pin/unpin goals.* -![Goal Tracker Right Click Menu](img/right_click_goals.jpg) - -### Inside a goal with task list -*Task rows now show icons, ellipsized text, and inline editing — with redesigned item search & quest features, plus an Add Pre-reqs button for quests (using our built-in list), along with Undo/Redo buttons.* -![Goal Tracker Task List](img/goal_task_panel.jpg) - -### Add from Preset -*Quickly insert curated presets like Ironman Progression, Void set, or Barrows Gear.* -![Goal Tracker Add From Preset](img/add_from_preset.jpg) - -
+✨ New Features +- Shift+Click task removal for faster management +- Automatic goal status checks for real-time progress +- ActionBar & ActionBarButton UI components for navigation and bulk actions +- Progress bars added under each goal card on the Home panel for quick visual tracking +- Quest Requirements file built and integrated for quick access to quest prerequisites +- Preset Goal Lists +- Export/Import goals to JSON with full UI refresh +- Inline editing for goal titles and manual tasks +- Long titles now ellipsized with hover-to-view full text + +♻️ Redesigned +- Quest prerequisites button leveraging the new Quest Requirements file +- Cascading completion to auto-complete related tasks +- Dropdown quest selector for faster task addition +- Right-click menus streamlined for prerequisites and child task options +- Goal/task cards with lighter fills, shadows, hover/press effects, and clear header dividers + +🐛 Fixes +- Undo/Redo functionality cleanup +- ActionBarButton painting glitches resolved +- Proper refresh handling in GoalTrackerPanel and ListPanel +- Fixed blank/refresh issues when switching panels or logging in +- Item icons preload correctly at startup/login/import +- Home panel refreshes immediately after task completion +- Layout fixes around Export button to avoid overlap +- Recursive refresh of child tasks ensures status consistency + +### 🖼️ Screenshots + +- **Home panel with goal cards** — redesigned buttons with hover/press effects, new progress bars under each goal card, plus the “Add from Preset” button and Export/Import to JSON buttons for easy sharing. + Home panel screenshot showing goal cards + +- **Inside a goal with task list** — supports auto-fill quest prerequisites, right-click task options, a new item search box, and a dropdown quest selector. + Goal task panel screenshot with tasks list + +- **Right‑click menu on a Home panel goal card** — manage goals quickly with options to move, pin, mark complete/incomplete, or delete. + Right-click menu screenshot on Home panel goal card + +- **Add from preset** — includes built-in support for Barrows gear, Void armor sets, and basic Ironman progression presets. + Add from preset screenshot with preset options -----
@@ -160,4 +114,4 @@ Track quest progress and completion, just select a quest or miniquest from the d Select an item using the search button and searching via the in-game chatbox, then set the desired quantity. The plugin will keep track of your items and tally up quantities stored in different inventories (bank, player, GIMP storage), and will be automatically completed once you get that amount of the item. -
+
\ No newline at end of file From d143862a3f1786f6ce4a7a9b3de57817c9917f75 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 1 Sep 2025 10:05:09 -0400 Subject: [PATCH 50/53] fixed empty panel text wrapping and progress bar from cutting off. Mark all complete/incomplete is working. --- changelog.md | 1 + .../goaltracker/ui/GoalItemContent.java | 46 +++++-------------- .../goaltracker/ui/GoalTrackerPanel.java | 43 +++++++++++++++-- .../ui/components/ListItemPanel.java | 13 ++---- .../goaltracker/ui/components/ListPanel.java | 29 ++++++++---- 5 files changed, 76 insertions(+), 56 deletions(-) diff --git a/changelog.md b/changelog.md index 0eee3bb..7d88ef4 100644 --- a/changelog.md +++ b/changelog.md @@ -31,6 +31,7 @@ Please leave for my sanity. thanks :) - Task rows updated with consistent styling, icon warming, and inline editing capabilities. - Home panel buttons stacked vertically; header divider thickness increased for clarity. - Goal card typography fixed with reserved progress text width to prevent clipping. +- Home panel placeholder updated to use wrapping text; empty text now displays cleanly without clipping. ### Fixed - Fixed chat completion messages: now appear reliably as `GAMEMESSAGE`s with configured colors; raw `` tags removed. diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java index e92cca0..8e9319f 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java @@ -23,7 +23,7 @@ */ public final class GoalItemContent extends JPanel implements Refreshable { - private final JLabel titleLabel = new JLabel(); + private final JTextArea titleLabel = new JTextArea(); private final JTextField titleEdit = new JTextField(); private final JLabel progress = new JLabel(); private final SlimBar progressBar = new SlimBar(); @@ -41,7 +41,7 @@ public final class GoalItemContent extends JPanel implements Refreshable super(new BorderLayout()); this.goal = goal; - setBorder(BorderFactory.createEmptyBorder(6, 8, 6, 8)); // padding for centered text + setBorder(BorderFactory.createEmptyBorder(6, 0, 6, 0)); // remove extra left/right padding so title can span edge-to-edge within the card body // Let the parent card body paint the background; avoid double fills setOpaque(false); setBackground(null); @@ -50,10 +50,15 @@ public final class GoalItemContent extends JPanel implements Refreshable topRow.setOpaque(false); // Title label (display mode) - titleLabel.setBorder(null); + titleLabel.setLineWrap(true); + titleLabel.setWrapStyleWord(true); + titleLabel.setEditable(false); titleLabel.setOpaque(false); + titleLabel.setBorder(null); titleLabel.setFocusable(false); - titleLabel.setToolTipText(null); + titleLabel.setFont(getFont()); + titleLabel.setMinimumSize(new Dimension(0, titleLabel.getPreferredSize().height)); + titleLabel.setMargin(new Insets(0, 0, 0, 0)); // Title edit (edit mode) titleEdit.setBorder(null); @@ -75,6 +80,8 @@ public final class GoalItemContent extends JPanel implements Refreshable @Override public void focusLost(java.awt.event.FocusEvent e) { exitEdit(true); } }); + // Place progress at the far right with a small left gap and a yellow border + progress.setBorder(new javax.swing.border.EmptyBorder(0, 3, 0, 0)); topRow.add(progress, BorderLayout.EAST); // Reserve fixed width for progress like 999/999 so it never clips @@ -82,7 +89,6 @@ public final class GoalItemContent extends JPanel implements Refreshable Dimension progSize = new Dimension(progW, progress.getPreferredSize().height); progress.setPreferredSize(progSize); progress.setMinimumSize(progSize); - add(topRow, BorderLayout.CENTER); // Slim custom progress bar under the title row @@ -243,36 +249,8 @@ private void updateTitleLabel() { if (topRow == null) return; String full = goal.getDescription() != null ? goal.getDescription() : ""; + titleLabel.setText(full.isBlank() ? " " : full); titleLabel.setToolTipText(full.isEmpty() ? null : full); - - // Compute available width for title (row width minus progress preferred width and a small gap) - int rowW = topRow.getWidth(); - if (rowW <= 0) { titleLabel.setText(full); return; } - int gap = 8; - int avail = Math.max(16, rowW - progress.getPreferredSize().width - gap); - - FontMetrics fm = titleLabel.getFontMetrics(titleLabel.getFont()); - if (fm.stringWidth(full) <= avail) { - titleLabel.setText(full); - return; - } - String ellipsis = "…"; - String text = full; - // Binary-like trim from the end until it fits - int lo = 0, hi = full.length(); - int cut = hi; - while (lo <= hi) { - int mid = (lo + hi) >>> 1; - String candidate = full.substring(0, Math.max(0, mid)) + ellipsis; - if (fm.stringWidth(candidate) <= avail) { - cut = mid; - lo = mid + 1; - } else { - hi = mid - 1; - } - } - text = full.substring(0, Math.max(0, cut)) + ellipsis; - titleLabel.setText(text); } private void enterEdit() diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index 8b9f031..b54d5d6 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -123,13 +123,24 @@ public GoalTrackerPanel(GoalTrackerPlugin plugin, GoalManager goalManager) goalListPanel = new ListPanel<>(goalManager.getGoals(), (goal) -> { var panel = new ListItemPanel<>(goalManager.getGoals(), goal); - panel.onClick(e -> this.view(goal)); + panel.onClick(e -> this.safeView(goal)); panel.add(new GoalItemContent(plugin, goal)); panel.onRemovedWithIndex(this::recordGoalRemoval); return panel; }); goalListPanel.setGap(0); - goalListPanel.setPlaceholder("
No goals yet.
Click + Add goal above to create your first one.
"); + goalListPanel.setBorder(new EmptyBorder(0, 0, 0, 0)); + JTextArea placeholder = new JTextArea("No goals yet.\nClick + Add goal above to create your first one."); + placeholder.setLineWrap(true); + placeholder.setWrapStyleWord(true); + placeholder.setEditable(false); + placeholder.setOpaque(false); + placeholder.setForeground(new Color(0xBFBFBF)); + placeholder.setFont(FontManager.getRunescapeSmallFont()); + placeholder.setBorder(new EmptyBorder(8, 0, 8, 0)); + placeholder.setColumns(30); + placeholder.setMaximumSize(new Dimension(Integer.MAX_VALUE, placeholder.getPreferredSize().height)); + goalListPanel.setPlaceholder(placeholder); mainPanel.add(headerContainer, BorderLayout.NORTH); mainPanel.add(goalListPanel, BorderLayout.CENTER); @@ -162,6 +173,33 @@ public void view(Goal goal) repaint(); } + private void safeView(Goal goal) + { + try + { + removeAll(); + this.goalPanel = new GoalPanel(plugin, goal, this::home); + this.goalPanel.onGoalUpdated(this.goalUpdatedListener); + this.goalPanel.onTaskAdded(this.taskAddedListener); + this.goalPanel.onTaskUpdated(this.taskUpdatedListener); + add(this.goalPanel, BorderLayout.CENTER); + this.goalPanel.refresh(); + revalidate(); + repaint(); + } + catch (Throwable t) + { + // Log to console and show a user-visible error so the panel isn't left blank + t.printStackTrace(); + JOptionPane.showMessageDialog(this, + "Sorry, something went wrong opening that goal. Returning to Home.", + "Goal Open Error", + JOptionPane.ERROR_MESSAGE); + this.goalPanel = null; + home(); + } + } + public void home() { if (pendingNewGoal != null) @@ -178,7 +216,6 @@ public void home() sortGoalsForHome(); goalListPanel.tryBuildList(); goalListPanel.refresh(); - mainPanel.remove(goalListPanel); mainPanel.add(goalListPanel, BorderLayout.CENTER); add(mainPanel, BorderLayout.CENTER); mainPanel.revalidate(); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java index 88c8520..ac5005a 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java @@ -88,6 +88,7 @@ public ListItemPanel(ReorderableList list, T item) // Create inner card surface to isolate hover/press background changes cardBody = new JPanel(new BorderLayout()); cardBody.setOpaque(true); + cardBody.setMaximumSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); // Add the cardBody as the main content area super.add(cardBody, BorderLayout.CENTER); @@ -250,20 +251,14 @@ public void onRemovedWithIndex(BiConsumer removeListener) { private void applyGoalCardDefaultStyle() { - // Outer spacing + shadow on the container panel - setBorder(javax.swing.BorderFactory.createCompoundBorder( - new EmptyBorder(8, 6, 8, 6), - new MatteBorder(2, 2, 4, 4, new Color(0, 0, 0, 60)) // shadow on all sides - )); + // Outer spacing only (no colored line border) + setBorder(new EmptyBorder(8, 6, 8, 6)); setBackground(ColorScheme.DARK_GRAY_COLOR); // keep outer area stable // Inner card face: rounded outline + inner padding if (cardBody != null) { - cardBody.setBorder(javax.swing.BorderFactory.createCompoundBorder( - javax.swing.BorderFactory.createLineBorder(ColorScheme.DARK_GRAY_COLOR, 1, true), - new EmptyBorder(6, 8, 6, 8) - )); + cardBody.setBorder(new EmptyBorder(4, 6, 4, 6)); cardBody.setBackground(ColorScheme.DARK_GRAY_COLOR); } } diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java index 960a108..b197e31 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java @@ -28,7 +28,7 @@ public final class ListPanel extends JScrollPane implements Refreshable private final Map> itemPanelMap = new HashMap<>(); private int gap = 2; - private String placeholder = "Nothing interesting happens."; + private JComponent placeholder = new JLabel("Nothing interesting happens."); private Consumer updatedListener; public ListPanel( @@ -39,15 +39,16 @@ public ListPanel( this.reorderableList = reorderableList; this.renderItem = renderItem; - setBorder(new EmptyBorder(10, 10, 10, 10)); listPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); + // Add left/right padding so card content aligns with the header + listPanel.setBorder(new EmptyBorder(0, 10, 0, 10)); JPanel wrapperPanel = new JPanel(new BorderLayout()); wrapperPanel.setBackground(ColorScheme.DARKER_GRAY_COLOR); wrapperPanel.add(listPanel, BorderLayout.NORTH); - setBorder(new EmptyBorder(4, 4, 4 - gap, 4)); + setBorder(new EmptyBorder(0, 0, 0, 0)); setBackground(ColorScheme.DARKER_GRAY_COLOR); getVerticalScrollBar().setPreferredSize(new Dimension(12, 0)); getVerticalScrollBar().setBorder(new EmptyBorder(0, 4, 0, 0)); @@ -61,13 +62,19 @@ public ListPanel( public void setGap(int gap) { this.gap = gap; - setBorder(new EmptyBorder(4, 4, 4 - gap, 4)); + setBorder(new EmptyBorder(0, 0, 0, 0)); tryBuildList(); } - public void setPlaceholder(String placeholder) + public void setPlaceholder(String placeholderText) { - this.placeholder = placeholder; + this.placeholder = new JLabel(placeholderText); + tryBuildList(); + } + + public void setPlaceholder(JComponent placeholderComponent) + { + this.placeholder = placeholderComponent; tryBuildList(); } @@ -128,7 +135,7 @@ private GridBagConstraints getConstraints(int gridy) constraints.weightx = 1; constraints.gridy = gridy; constraints.gridx = 0; - constraints.insets = new Insets(0, 0, gap, 0); + constraints.insets = new Insets(4, -4, gap, -4); return constraints; } @@ -149,11 +156,13 @@ public void tryBuildList() if (reorderableList.isEmpty()) { listPanel.removeAll(); - JLabel placeholderLabel = new JLabel(placeholder); - placeholderLabel.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + if (placeholder instanceof JLabel) { + ((JLabel) placeholder).setForeground(ColorScheme.LIGHT_GRAY_COLOR); + } JPanel placeholderPanel = new JPanel(); placeholderPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - placeholderPanel.add(placeholderLabel); + placeholderPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0)); + placeholderPanel.add(placeholder); listPanel.add(placeholderPanel, getConstraints()); } else { listPanel.removeAll(); From e569a22356f1cd6965cabd34c15cb726f0e589ee Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 1 Sep 2025 20:40:57 -0400 Subject: [PATCH 51/53] fixes to goal card sizes and added border when pinned. --- .../toofifty/goaltracker/ui/GoalPanel.java | 94 +++++++++------ .../goaltracker/ui/TaskItemContent.java | 12 +- .../ui/components/ListItemPanel.java | 108 +++++++++++++++--- .../goaltracker/ui/components/ListPanel.java | 14 ++- 4 files changed, 174 insertions(+), 54 deletions(-) diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java index 8f9e392..5df2ce5 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java @@ -58,8 +58,8 @@ public final class GoalPanel extends JPanel implements Refreshable // Action bar with Add pre-reqs on the left of Undo/Redo ActionBar actionBar = new ActionBar(); - prereqsButton = new ActionBarButton("Add pre-reqs", this::addPrereqs); - prereqsButton.setToolTipText("Add prerequisite quests/tasks for this goal"); + prereqsButton = new ActionBarButton("Add Quest pre-reqs", this::addPrereqs); + prereqsButton.setToolTipText("Add prerequisite quests for this goal"); prereqsButton.setEnabled(true); undoButton = new ActionBarButton("Undo", this::doUndo); @@ -82,7 +82,7 @@ public final class GoalPanel extends JPanel implements Refreshable descriptionInput = new EditableInput((value) -> { goal.setDescription(value); - this.goalUpdatedListener.accept(goal); + if (this.goalUpdatedListener != null) this.goalUpdatedListener.accept(goal); }); headerPanel.add(descriptionInput, BorderLayout.CENTER); SwingUtilities.invokeLater(() -> installClipboardSupport(descriptionInput)); @@ -95,39 +95,54 @@ public final class GoalPanel extends JPanel implements Refreshable }); taskListPanel = new ListPanel<>(goal.getTasks(), (task) -> { - ListTaskPanel taskPanel = new ListTaskPanel(goal.getTasks(), task); - taskPanel.setActionHistory(actionHistory); - TaskItemContent taskContent = new TaskItemContent(plugin, goal, task); - taskContent.setActionHistory(actionHistory); - taskPanel.add(taskContent); - taskPanel.setTaskContent(taskContent); - taskContent.refresh(); - taskPanel.setOpaque(true); - taskPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); - taskPanel.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(4, 6, 0, 6, ColorScheme.DARKER_GRAY_COLOR), // darker line for contrast - new EmptyBorder(2, 4, 2, 4) - )); - - - taskPanel.onIndented(e -> { - this.goalUpdatedListener.accept(goal); - plugin.getUiStatusManager().refresh(goal); - this.refresh(); - }); - - taskPanel.onUnindented(e -> { - this.goalUpdatedListener.accept(goal); - plugin.getUiStatusManager().refresh(goal); - this.refresh(); - }); - - taskPanel.onRemovedWithIndex((removedTask, index) -> { - actionHistory.push(new RemoveTaskAction(goal.getTasks(), removedTask, index)); - updateUndoRedoButtons(); - }); - - return taskPanel; + try { + ListTaskPanel taskPanel = new ListTaskPanel(goal.getTasks(), task); + taskPanel.setActionHistory(actionHistory); + TaskItemContent taskContent = new TaskItemContent(plugin, goal, task); + taskContent.setActionHistory(actionHistory); + taskPanel.add(taskContent); + taskPanel.setTaskContent(taskContent); + taskContent.refresh(); + taskPanel.setOpaque(true); + taskPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); + taskPanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createMatteBorder(4, 6, 0, 6, ColorScheme.DARKER_GRAY_COLOR), // darker line for contrast + new EmptyBorder(2, 4, 2, 4) + )); + + + taskPanel.onIndented(e -> { + if (this.goalUpdatedListener != null) this.goalUpdatedListener.accept(goal); + plugin.getUiStatusManager().refresh(goal); + this.refresh(); + }); + + taskPanel.onUnindented(e -> { + if (this.goalUpdatedListener != null) this.goalUpdatedListener.accept(goal); + plugin.getUiStatusManager().refresh(goal); + this.refresh(); + }); + + taskPanel.onRemovedWithIndex((removedTask, index) -> { + actionHistory.push(new RemoveTaskAction(goal.getTasks(), removedTask, index)); + updateUndoRedoButtons(); + }); + + return taskPanel; + } catch (Throwable t) { + t.printStackTrace(); + // Render a minimal, visible error row instead of breaking the entire list + ListTaskPanel fallback = new ListTaskPanel(goal.getTasks(), task); + JPanel error = new JPanel(new BorderLayout()); + error.setOpaque(true); + error.setBackground(ColorScheme.DARK_GRAY_COLOR); + JLabel msg = new JLabel("\u26A0\uFE0F Failed to render task; see log."); + msg.setForeground(ColorScheme.LIGHT_GRAY_COLOR); + msg.setBorder(new EmptyBorder(2, 4, 2, 4)); + error.add(msg, BorderLayout.CENTER); + fallback.add(error); + return fallback; + } }); taskListPanel.setGap(0); taskListPanel.setPlaceholder("No tasks added yet"); @@ -136,6 +151,13 @@ public final class GoalPanel extends JPanel implements Refreshable NewTaskPanel newTaskPanel = new NewTaskPanel(plugin, goal); newTaskPanel.onTaskAdded(this::updateFromNewTask); add(newTaskPanel, BorderLayout.SOUTH); + + // Global ESC to go back + getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT) + .put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ESCAPE, 0), "gt.back"); + getActionMap().put("gt.back", new javax.swing.AbstractAction() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { closeListener.run(); } + }); } public void updateFromNewTask(Task task) diff --git a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java index 73bc89a..6315c00 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/TaskItemContent.java @@ -28,6 +28,8 @@ */ public final class TaskItemContent extends JPanel implements Refreshable { + private static final int INDENT_PER_LEVEL = 12; // pixels per indent level + private final Task task; private final Goal goal; private final TaskIconService iconService; @@ -145,9 +147,15 @@ public void refresh() titleLabel.setForeground(STATUS_TO_COLOR.get(task.getStatus())); updateTitleLabel(); - int indent = 16 * task.getIndentLevel(); + int level = Math.max(0, task.getIndentLevel()); + // Shift rendering so level 0 = 0px, level 1 = 0px, level 2 = 12px, etc. + // This avoids the first child appearing with an extra indent when prereqs are added. + int indent = (level <= 1) ? 0 : (level - 1) * INDENT_PER_LEVEL; + iconLabel.setIcon(iconService.get(task)); - iconLabel.setBorder(new EmptyBorder(0, indent, 0, 0)); + // Apply indent to the wrapper instead of the label to avoid double padding + iconLabel.setBorder(new EmptyBorder(0, 0, 0, 0)); + iconWrapper.setBorder(new EmptyBorder(4, indent, 0, 4)); revalidate(); } diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java index ac5005a..e793f0a 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java @@ -6,6 +6,7 @@ import net.runelite.client.ui.ColorScheme; import javax.swing.*; +import javax.swing.border.Border; import javax.swing.border.EmptyBorder; import javax.swing.border.MatteBorder; import java.awt.*; @@ -20,6 +21,13 @@ */ public class ListItemPanel extends JPanel implements Refreshable { + private static final int BASE_TOP = 8, BASE_LEFT = 6, BASE_BOTTOM = 8, BASE_RIGHT = 6; + private static final int TAN_THICKNESS = 1; + private static final int INNER_TOP = 4, INNER_LEFT = 6, INNER_BOTTOM = 4, INNER_RIGHT = 6; + + private static final Border UNPINNED_BORDER = new EmptyBorder(BASE_TOP, BASE_LEFT, BASE_BOTTOM, BASE_RIGHT); + private static final Border PINNED_BORDER = new MatteBorder(TAN_THICKNESS, TAN_THICKNESS, TAN_THICKNESS, TAN_THICKNESS, new Color(210, 180, 140)); + protected final JMenuItem moveUp = new JMenuItem("Move up"); protected final JMenuItem moveDown = new JMenuItem("Move down"); protected final JMenuItem moveToTop = new JMenuItem("Move to top"); @@ -68,11 +76,10 @@ private void addContextMenuListenerRecursive(Component c) { private void maybeShow(MouseEvent e) { - if (!(e.isPopupTrigger() || javax.swing.SwingUtilities.isRightMouseButton(e))) + if (e.isPopupTrigger()) { - return; + popupMenu.show((Component) e.getSource(), e.getX(), e.getY()); } - popupMenu.show((Component) e.getSource(), e.getX(), e.getY()); } @Override public void mousePressed(MouseEvent e) { maybeShow(e); } @@ -127,13 +134,28 @@ public ListItemPanel(ReorderableList list, T item) }); popupMenu.setBorder(new EmptyBorder(5, 5, 5, 5)); - + cardBody.setComponentPopupMenu(popupMenu); setComponentPopupMenu(popupMenu); // Also show context menu on press/release to handle platform differences - this.addMouseListener(contextMenuListener); + // this.addMouseListener(contextMenuListener); // Removed to avoid duplicate popup invocations // Ensure popup and click listeners cover all descendants initially - addContextMenuListenerRecursive(this); + // addContextMenuListenerRecursive(this); // Removed to avoid multiple menus + setOpaque(true); + + // Attach context-menu and click listeners automatically to any components added later inside the card + cardBody.addContainerListener(new java.awt.event.ContainerAdapter() { + @Override + public void componentAdded(java.awt.event.ContainerEvent e) { + java.awt.Component child = e.getChild(); + if (child instanceof JComponent) { + ((JComponent) child).setComponentPopupMenu(popupMenu); + } + if (clickListenerAdapter != null) { + addClickListenerRecursive(child); + } + } + }); } @Override @@ -146,8 +168,9 @@ public void add(Component comp, Object constraints) if (comp instanceof JComponent) { ((JComponent) comp).setBorder(new EmptyBorder(0, 0, 0, 0)); + ((JComponent) comp).setComponentPopupMenu(popupMenu); } - addContextMenuListenerRecursive(comp); + // addContextMenuListenerRecursive(comp); if (clickListenerAdapter != null) addClickListenerRecursive(comp); return; } @@ -163,8 +186,9 @@ public Component add(String name, Component comp) if (comp instanceof JComponent) { ((JComponent) comp).setBorder(new EmptyBorder(0, 0, 0, 0)); + ((JComponent) comp).setComponentPopupMenu(popupMenu); } - addContextMenuListenerRecursive(comp); + // addContextMenuListenerRecursive(comp); if (clickListenerAdapter != null) addClickListenerRecursive(comp); return comp; } @@ -179,7 +203,10 @@ public ListItemPanel add(Component comp) { ((JComponent) comp).setBorder(new EmptyBorder(0, 0, 0, 0)); } - addContextMenuListenerRecursive(comp); + if (comp instanceof JComponent) { + ((JComponent) comp).setComponentPopupMenu(popupMenu); + } + // addContextMenuListenerRecursive(comp); if (clickListenerAdapter != null) addClickListenerRecursive(comp); return this; } @@ -249,30 +276,63 @@ public void onRemovedWithIndex(BiConsumer removeListener) { this.removedWithIndexListener = removeListener; } + private void applyBaseBorderWithOptionalTan() + { + // Outer panel: keep spacing constant + setBorder(UNPINNED_BORDER); + + if (cardBody == null) + { + return; + } + + // Ensure the inner content always has consistent padding + cardBody.setBorder(new EmptyBorder(INNER_TOP, INNER_LEFT, INNER_BOTTOM, INNER_RIGHT)); + + if (item instanceof Goal && ((Goal) item).isPinned()) + { + // Apply tan to the OUTER card border; keep total insets stable + int top = Math.max(0, BASE_TOP - TAN_THICKNESS); + int left = Math.max(0, BASE_LEFT - TAN_THICKNESS); + int bottom = Math.max(0, BASE_BOTTOM - TAN_THICKNESS); + int right = Math.max(0, BASE_RIGHT - TAN_THICKNESS); + Border inner = new EmptyBorder(top, left, bottom, right); + Color tan = new Color(210, 180, 140); + setBorder(BorderFactory.createCompoundBorder( + new MatteBorder(TAN_THICKNESS, TAN_THICKNESS, TAN_THICKNESS, TAN_THICKNESS, tan), + inner + )); + } + // else: leave UNPINNED_BORDER already applied above + } + private void applyGoalCardDefaultStyle() { - // Outer spacing only (no colored line border) - setBorder(new EmptyBorder(8, 6, 8, 6)); - setBackground(ColorScheme.DARK_GRAY_COLOR); // keep outer area stable + // Borders + applyBaseBorderWithOptionalTan(); - // Inner card face: rounded outline + inner padding + // Backgrounds + setBackground(ColorScheme.DARK_GRAY_COLOR); // keep outer area stable if (cardBody != null) { - cardBody.setBorder(new EmptyBorder(4, 6, 4, 6)); cardBody.setBackground(ColorScheme.DARK_GRAY_COLOR); } } private void applyGoalCardHoverStyle() { + // Keep borders as set by applyBaseBorderWithOptionalTan(); + applyBaseBorderWithOptionalTan(); if (cardBody != null) { - cardBody.setBackground(ColorScheme.DARK_GRAY_HOVER_COLOR); + cardBody.setBackground(ColorScheme.DARKER_GRAY_HOVER_COLOR); } } private void applyGoalCardPressedStyle() { + // Keep borders as set by applyBaseBorderWithOptionalTan(); + applyBaseBorderWithOptionalTan(); if (cardBody != null) { cardBody.setBackground(ColorScheme.DARK_GRAY_COLOR); @@ -300,6 +360,24 @@ public void refresh() buildAdditionalMenu(); + // Deduplicate Pin/Unpin items if multiple sources added them + for (int i = popupMenu.getComponentCount() - 1, seenPin = 0, seenUnpin = 0; i >= 0; i--) + { + java.awt.Component c = popupMenu.getComponent(i); + if (c instanceof javax.swing.JMenuItem) + { + String text = ((javax.swing.JMenuItem) c).getText(); + if ("Pin goal".equals(text)) + { + if (seenPin++ > 0) popupMenu.remove(i); + } + else if ("Unpin goal".equals(text)) + { + if (seenUnpin++ > 0) popupMenu.remove(i); + } + } + } + // Refresh all descendants that implement Refreshable for (Component component : getComponents()) { refreshDescendants(component); diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java index b197e31..9411730 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListPanel.java @@ -31,6 +31,9 @@ public final class ListPanel extends JScrollPane implements Refreshable private JComponent placeholder = new JLabel("Nothing interesting happens."); private Consumer updatedListener; + private int rowLeftInset = 0; + private int rowRightInset = 0; + public ListPanel( ReorderableList reorderableList, Function> renderItem @@ -66,6 +69,13 @@ public void setGap(int gap) tryBuildList(); } + public void setRowSideInsets(int left, int right) + { + this.rowLeftInset = Math.max(0, left); + this.rowRightInset = Math.max(0, right); + tryBuildList(); + } + public void setPlaceholder(String placeholderText) { this.placeholder = new JLabel(placeholderText); @@ -135,7 +145,7 @@ private GridBagConstraints getConstraints(int gridy) constraints.weightx = 1; constraints.gridy = gridy; constraints.gridx = 0; - constraints.insets = new Insets(4, -4, gap, -4); + constraints.insets = new Insets(4, rowLeftInset, gap, rowRightInset); return constraints; } @@ -153,6 +163,8 @@ private void refreshChildMenus() */ public void tryBuildList() { + itemPanelMap.clear(); + if (reorderableList.isEmpty()) { listPanel.removeAll(); From a00284a8a7f965bb70b4cb0def425cdaafb8e6de Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 1 Sep 2025 22:41:15 -0400 Subject: [PATCH 52/53] Polish rename, pin/unpin, move, and import/export flows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename box no longer stretches the card and only activates when it should - Added a right-click Rename option with proper commit/cancel (ESC to cancel) - Cleaned up Pin/Unpin so there’s only one menu item and unpin refreshes instantly - Move up/down/top/bottom now actually reorders cards; removed forced A–Z sorting - Export now asks before overwriting a file - Import now asks whether to Overwrite, Merge, or Cancel --- changelog.md | 9 ++++ .../com/toofifty/goaltracker/GoalManager.java | 17 +++++-- .../goaltracker/ui/GoalItemContent.java | 45 +++++++++-------- .../toofifty/goaltracker/ui/GoalPanel.java | 39 ++++++++++++++- .../goaltracker/ui/GoalTrackerPanel.java | 48 ++++++++++++++++--- .../ui/components/ListItemPanel.java | 47 +++++++++++++++++- .../goaltracker/utils/QuestRequirements.java | 15 ++++++ 7 files changed, 186 insertions(+), 34 deletions(-) diff --git a/changelog.md b/changelog.md index 7d88ef4..1d900a0 100644 --- a/changelog.md +++ b/changelog.md @@ -32,6 +32,9 @@ Please leave for my sanity. thanks :) - Home panel buttons stacked vertically; header divider thickness increased for clarity. - Goal card typography fixed with reserved progress text width to prevent clipping. - Home panel placeholder updated to use wrapping text; empty text now displays cleanly without clipping. +- Export to file now prompts before overwriting existing files. +- Import from file now prompts with Overwrite / Merge / Cancel options instead of silently replacing goals. +- Home panel no longer forces alphabetical order; manual Move actions now persist with pinned-first grouping only. ### Fixed - Fixed chat completion messages: now appear reliably as `GAMEMESSAGE`s with configured colors; raw `` tags removed. @@ -45,3 +48,9 @@ Please leave for my sanity. thanks :) - Fixed empty goals remaining in list; now automatically removed if left without tasks. - Fixed overlapping Export button layout issue. - Fixed quest tasks not showing correct status on login until entering the goal. +- Fixed card rename editor stretching beyond panel width; editor now constrained to card body. +- Fixed inconsistent rename activation; description text is read-only unless rename explicitly invoked. +- Added right-click Rename option with bounded editor, commit/cancel, and ESC cancel support. +- Fixed duplicate Pin/Unpin menu items by centralizing handling in ListItemPanel. +- Fixed unpinning a card not refreshing the Home panel immediately; Home panel now rebuilds/reorders on pin state toggle. +- Fixed Move right-click options (Up/Down/Top/Bottom) not changing card position; moves now reorder cards properly. diff --git a/src/main/java/com/toofifty/goaltracker/GoalManager.java b/src/main/java/com/toofifty/goaltracker/GoalManager.java index 98f7ac7..b62a0a9 100644 --- a/src/main/java/com/toofifty/goaltracker/GoalManager.java +++ b/src/main/java/com/toofifty/goaltracker/GoalManager.java @@ -102,14 +102,25 @@ public String exportJson(boolean pretty) return goalSerializer.serialize(goals, pretty); } + public void importJson(String json) + { + // Backwards-compatible default: overwrite existing goals + importJson(json, true); + } + /** - * Import goals from JSON and persist them. + * Import goals from JSON, optionally merging with existing goals. + * @param json the JSON payload + * @param overwrite if true, clears existing goals first; if false, appends imported goals */ - public void importJson(String json) + public void importJson(String json, boolean overwrite) { try { - this.goals.clear(); + if (overwrite) + { + this.goals.clear(); + } this.goals.addAll(goalSerializer.deserialize(json)); save(); } diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java index 8e9319f..01f38b7 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalItemContent.java @@ -65,14 +65,31 @@ public final class GoalItemContent extends JPanel implements Refreshable titleEdit.setOpaque(false); titleEdit.setDragEnabled(true); + // Constrain edit field so it never expands the card width + titleEdit.setColumns(1); // keep preferred width minimal; BorderLayout will stretch it as needed + int editH = titleEdit.getPreferredSize().height; + titleEdit.setMinimumSize(new Dimension(0, editH)); + titleEdit.setPreferredSize(new Dimension(10, editH)); // tiny preferred width; parent determines real width + // ESC cancels rename without saving + InputMap im = titleEdit.getInputMap(JComponent.WHEN_FOCUSED); + ActionMap am = titleEdit.getActionMap(); + im.put(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_ESCAPE, 0), "gt.cancelEdit"); + am.put("gt.cancelEdit", new AbstractAction() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { exitEdit(false); } + }); + titleStack.setOpaque(false); titleStack.add(titleLabel, "label"); titleStack.add(titleEdit, "edit"); topRow.add(titleStack, BorderLayout.CENTER); - // Swap to edit on label click + // Swap to edit only on LEFT double‑click (not on right‑click) titleLabel.addMouseListener(new MouseAdapter() { - @Override public void mouseClicked(MouseEvent e) { enterEdit(); } + @Override public void mouseClicked(MouseEvent e) { + if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { + enterEdit(); + } + } }); // Commit edits on Enter and when focus is lost titleEdit.addActionListener(e -> exitEdit(true)); @@ -116,7 +133,7 @@ public final class GoalItemContent extends JPanel implements Refreshable { private void maybeShow(MouseEvent e) { - if (!(e.isPopupTrigger() || SwingUtilities.isRightMouseButton(e))) + if (!e.isPopupTrigger()) { return; } @@ -131,24 +148,10 @@ private void maybeShow(MouseEvent e) Point p = SwingUtilities.convertPoint(src, e.getPoint(), listItem); JPopupMenu menu = listItem.getComponentPopupMenu(); - // --- Pin / Unpin (temporary additions) --- - JSeparator sep = new JSeparator(); - ((JComponent) sep).putClientProperty("pinToggle", Boolean.TRUE); - menu.add(sep); - - JMenuItem pinToggle = new JMenuItem(goal.isPinned() ? "Unpin" : "Pin"); - ((JComponent) pinToggle).putClientProperty("pinToggle", Boolean.TRUE); - pinToggle.addActionListener(ev -> { - goal.setPinned(!goal.isPinned()); - try { - plugin.getGoalManager().save(); - } catch (Throwable t) { - plugin.getUiStatusManager().refresh(goal); - } - GoalItemContent.this.revalidate(); - GoalItemContent.this.repaint(); - }); - menu.add(pinToggle); + JMenuItem rename = new JMenuItem("Rename"); + ((JComponent) rename).putClientProperty("pinToggle", Boolean.TRUE); // reuse cleanup flag + rename.addActionListener(ev -> enterEdit()); + menu.add(rename); PopupMenuListener cleanup = new PopupMenuListener() { @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) { } diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java index 5df2ce5..ac8381f 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalPanel.java @@ -69,6 +69,43 @@ public final class GoalPanel extends JPanel implements Refreshable actionBar.left().add(undoButton); actionBar.left().add(redoButton); + ActionBarButton importButton = new ActionBarButton("Import", () -> { + String input = javax.swing.JOptionPane.showInputDialog(GoalPanel.this, "Paste goals JSON:"); + if (input == null) { + return; // canceled + } + input = input.trim(); + if (input.isEmpty()) { + return; // nothing to import + } + + Object[] options = {"Overwrite", "Merge", "Cancel"}; + int choice = javax.swing.JOptionPane.showOptionDialog( + GoalPanel.this, + "Importing will change your current goals.\nDo you want to overwrite existing goals or merge with them?", + "Import Goals", + javax.swing.JOptionPane.YES_NO_CANCEL_OPTION, + javax.swing.JOptionPane.QUESTION_MESSAGE, + null, + options, + options[2] + ); + + if (choice == javax.swing.JOptionPane.YES_OPTION) { + plugin.getGoalManager().importJson(input, true); // Overwrite + } else if (choice == javax.swing.JOptionPane.NO_OPTION) { + plugin.getGoalManager().importJson(input, false); // Merge + } else { + return; // canceled + } + + // Refresh current view after import + plugin.setValidateAll(true); + plugin.getUiStatusManager().refresh(goal); + refreshTaskList(); + }); + actionBar.left().add(importButton); + // Stack back row above the action bar in the header's NORTH JPanel headerTop = new JPanel(); headerTop.setBackground(ColorScheme.DARK_GRAY_COLOR); @@ -106,7 +143,7 @@ public final class GoalPanel extends JPanel implements Refreshable taskPanel.setOpaque(true); taskPanel.setBackground(ColorScheme.DARK_GRAY_COLOR); taskPanel.setBorder(BorderFactory.createCompoundBorder( - BorderFactory.createMatteBorder(4, 6, 0, 6, ColorScheme.DARKER_GRAY_COLOR), // darker line for contrast + BorderFactory.createMatteBorder(1, 6, 0, 6, ColorScheme.DARKER_GRAY_COLOR), // thinner divider new EmptyBorder(2, 4, 2, 4) )); diff --git a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java index b54d5d6..47c4622 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/GoalTrackerPanel.java @@ -278,13 +278,10 @@ private void updateUndoRedoButtons() private void sortGoalsForHome() { + // Keep pinned goals first, but do NOT alphabetize within groups. + // Collections.sort is stable (TimSort), so existing manual order is preserved within each group. java.util.List goals = goalManager.getGoals(); - Collections.sort(goals, Comparator - .comparing(Goal::isPinned).reversed() - .thenComparing(g -> { - String d = g.getDescription(); - return d == null ? "" : d.toLowerCase(); - })); + Collections.sort(goals, Comparator.comparing(Goal::isPinned).reversed()); } private void doUndo() @@ -353,6 +350,22 @@ private void exportGoalsToFile() { file = new java.io.File(file.getParentFile(), file.getName() + ".json"); } + // Confirm overwrite if file already exists + if (file.exists()) + { + int confirm = JOptionPane.showConfirmDialog( + this, + "File already exists:\n" + file.getAbsolutePath() + "\n\nOverwrite it?", + "Confirm Overwrite", + JOptionPane.YES_NO_OPTION, + JOptionPane.WARNING_MESSAGE + ); + if (confirm != JOptionPane.YES_OPTION) + { + return; // user chose not to overwrite + } + } + try (java.io.FileWriter fw = new java.io.FileWriter(file)) { String json = goalManager.exportJson(true); @@ -379,8 +392,28 @@ private void importGoalsFromFile() try { String json = new String(java.nio.file.Files.readAllBytes(file.toPath())); - goalManager.importJson(json); + + Object[] options = {"Overwrite", "Merge", "Cancel"}; + int choice = JOptionPane.showOptionDialog( + this, + "Importing will change your current goals.\nDo you want to overwrite existing goals or merge with them?", + "Import Goals", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.QUESTION_MESSAGE, + null, + options, + options[2] + ); + + if (choice == JOptionPane.CANCEL_OPTION || choice == JOptionPane.CLOSED_OPTION) + { + return; // user canceled + } + + boolean overwrite = (choice == JOptionPane.YES_OPTION); + goalManager.importJson(json, overwrite); plugin.warmItemIcons(); + if (goalPanel != null) { home(); } else { @@ -389,6 +422,7 @@ private void importGoalsFromFile() revalidate(); repaint(); } + JOptionPane.showMessageDialog(this, "Imported goals from\n" + file.getAbsolutePath(), "Import Complete", JOptionPane.INFORMATION_MESSAGE); } catch (Exception ex) diff --git a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java index e793f0a..cca5d88 100644 --- a/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java +++ b/src/main/java/com/toofifty/goaltracker/ui/components/ListItemPanel.java @@ -109,21 +109,25 @@ public ListItemPanel(ReorderableList list, T item) moveUp.addActionListener(e -> { list.moveUp(item); if (this.reorderedListener != null) this.reorderedListener.accept(item); + requestContainerRefresh(); }); moveDown.addActionListener(e -> { list.moveDown(item); if (this.reorderedListener != null) this.reorderedListener.accept(item); + requestContainerRefresh(); }); moveToTop.addActionListener(e -> { list.moveToTop(item); if (this.reorderedListener != null) this.reorderedListener.accept(item); + requestContainerRefresh(); }); moveToBottom.addActionListener(e -> { list.moveToBottom(item); if (this.reorderedListener != null) this.reorderedListener.accept(item); + requestContainerRefresh(); }); removeItem.addActionListener(e -> { @@ -276,6 +280,23 @@ public void onRemovedWithIndex(BiConsumer removeListener) { this.removedWithIndexListener = removeListener; } + private void requestContainerRefresh() + { + java.awt.Container p = getParent(); + while (p != null) + { + if (p instanceof com.toofifty.goaltracker.ui.GoalTrackerPanel) + { + ((com.toofifty.goaltracker.ui.GoalTrackerPanel) p).refresh(); + return; + } + p = p.getParent(); + } + // Fallback if no GoalTrackerPanel ancestor is found + revalidate(); + repaint(); + } + private void applyBaseBorderWithOptionalTan() { // Outer panel: keep spacing constant @@ -394,17 +415,39 @@ protected void buildAdditionalMenu() { if (item instanceof Goal) { + Goal goal = (Goal) item; + JMenuItem pinToggle = new JMenuItem(goal.isPinned() ? "Unpin goal" : "Pin goal"); + pinToggle.addActionListener(e -> { + // Toggle pin state + boolean nowPinned = !goal.isPinned(); + goal.setPinned(nowPinned); + + // Reorder this item in the visible list immediately so it moves without waiting on a rebuild + try { + if (nowPinned) { + list.moveToTop(item); + } else { + list.moveToBottom(item); + } + } catch (Throwable ignore) { + // If list doesn't support it for some reason, we still continue with refresh + } + + // Refresh this card and ask the container to repaint/rebuild + refresh(); + requestContainerRefresh(); + }); + popupMenu.add(pinToggle); + JMenuItem markAllComplete = new JMenuItem("Mark all as completed"); JMenuItem markAllIncomplete = new JMenuItem("Mark all as incomplete"); markAllComplete.addActionListener(e -> { - Goal goal = (Goal) item; goal.setAllTasksCompleted(true); refresh(); }); markAllIncomplete.addActionListener(e -> { - Goal goal = (Goal) item; goal.setAllTasksCompleted(false); refresh(); }); diff --git a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java index 9ca9285..376158c 100644 --- a/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java +++ b/src/main/java/com/toofifty/goaltracker/utils/QuestRequirements.java @@ -214,6 +214,21 @@ public final class QuestRequirements ) ); + REQUIREMENT_MAP.put( + Quest.THE_PATH_OF_GLOUPHRIE, + Arrays.asList( + QuestTask.builder().quest(Quest.WATERFALL_QUEST).build(), + QuestTask.builder().quest(Quest.THE_EYES_OF_GLOUPHRIE).build(), + QuestTask.builder().quest(Quest.TREE_GNOME_VILLAGE).build(), + QuestTask.builder().quest(Quest.THE_GRAND_TREE).build(), + SkillLevelTask.builder().skill(Skill.STRENGTH).level(60).build(), + SkillLevelTask.builder().skill(Skill.SLAYER).level(56).build(), + SkillLevelTask.builder().skill(Skill.THIEVING).level(56).build(), + SkillLevelTask.builder().skill(Skill.RANGED).level(47).build(), + SkillLevelTask.builder().skill(Skill.AGILITY).level(45).build() + ) + ); + REQUIREMENT_MAP.put( Quest.THE_FEUD, Arrays.asList( From d00e0fe01e789a8801918a3cc1a4a2ea31620790 Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Fri, 5 Sep 2025 09:04:24 -0400 Subject: [PATCH 53/53] - Updated readme - Fixed issue in .properties --- README.md | 117 +++--------------- changelog.md | 2 +- runelite-plugin.properties | 4 +- .../models/task/SkillLevelTask.java | 8 +- .../goaltracker/models/task/Task.java | 7 +- 5 files changed, 26 insertions(+), 112 deletions(-) diff --git a/README.md b/README.md index d33cfc8..2e7ce9d 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,30 @@ # 🏆 Goal Tracker -> A reimagining of the Goal Tracker plugin — rebuilt with a more modern UI, powerful new features, and improved stability to help you plan, track, and achieve your Old School RuneScape goals with ease. -> It is now faster, cleaner, and more reliable — introducing a Quest Requirements system, presets, improved visuals, and smoother task/goal management. +> A RuneLite plugin that helps you plan, track, and achieve your Old School RuneScape goals with ease. +> Goal Tracker provides presets, quest requirements, progress bars, and a modern UI to make managing your tasks and goals simple and efficient. --- +## Plugin Features -✨ New Features -- Shift+Click task removal for faster management -- Automatic goal status checks for real-time progress -- ActionBar & ActionBarButton UI components for navigation and bulk actions -- Progress bars added under each goal card on the Home panel for quick visual tracking -- Quest Requirements file built and integrated for quick access to quest prerequisites -- Preset Goal Lists -- Export/Import goals to JSON with full UI refresh -- Inline editing for goal titles and manual tasks -- Long titles now ellipsized with hover-to-view full text +- Preset Goal Lists for quick setup of common goals +- Pin important goals to the top of the Home panel +- Automatically add prerequisites for quests +- Track progress with visual progress bars +- Import and Export goals via JSON for easy sharing or backups +- Create and manage custom goals with tasks -♻️ Redesigned -- Quest prerequisites button leveraging the new Quest Requirements file -- Cascading completion to auto-complete related tasks -- Dropdown quest selector for faster task addition -- Right-click menus streamlined for prerequisites and child task options -- Goal/task cards with lighter fills, shadows, hover/press effects, and clear header dividers +## Screenshots -🐛 Fixes -- Undo/Redo functionality cleanup -- ActionBarButton painting glitches resolved -- Proper refresh handling in GoalTrackerPanel and ListPanel -- Fixed blank/refresh issues when switching panels or logging in -- Item icons preload correctly at startup/login/import -- Home panel refreshes immediately after task completion -- Layout fixes around Export button to avoid overlap -- Recursive refresh of child tasks ensures status consistency - -### 🖼️ Screenshots - -- **Home panel with goal cards** — redesigned buttons with hover/press effects, new progress bars under each goal card, plus the “Add from Preset” button and Export/Import to JSON buttons for easy sharing. +- **Home panel with goal cards** — shows progress bars, presets, and export/import buttons. Home panel screenshot showing goal cards -- **Inside a goal with task list** — supports auto-fill quest prerequisites, right-click task options, a new item search box, and a dropdown quest selector. +- **Inside a goal with task list** — displays quest prerequisites, right-click task options, item search, and a quest selector. Goal task panel screenshot with tasks list -- **Right‑click menu on a Home panel goal card** — manage goals quickly with options to move, pin, mark complete/incomplete, or delete. +- **Right‑click menu on a goal card** — options to move, pin, mark complete, or delete. Right-click menu screenshot on Home panel goal card -- **Add from preset** — includes built-in support for Barrows gear, Void armor sets, and basic Ironman progression presets. +- **Add from preset** — includes built-in options for Barrows gear, Void armor, and Ironman progression. Add from preset screenshot with preset options ------ -
-

📜 Original Goal Tracker v1 Readme & Documentation (created by dillydill123)

- -# Runelite Goal Tracker Plugin - -Keep track of your OSRS goals and complete them automatically. - -## Features - -- Track different types of tasks - - Manual tasks - - Skill tasks - - Quests - - Item tasks -- Organise tasks lists into goals -- Reorder and manage goal and task lists -- Chat notification on task completion - -### Planned - -- More task types - - Achievement diaries - - Minigame rewards - - Kourend favour - - NPC kills - -Suggestions are welcome - please submit an issue :) - -## Usage - -### Goals - -Goals are lists of tasks, and at a glance provide a quick way to view your progress towards the goal. - -![Goals list](img/goals_list.png) - -You can add a new goal with the "+ Add goal" button, and you can reorder/remove goals using right click. Clicking a goal will show the tasks within: - -![Goal view](img/goal_view.png) - -From here, you can add tasks to the goal. - -### Adding tasks - -![Task inputs](img/task_inputs.png) - -#### Manual tasks - -Basically a simple to-do list item. You can add these via the "Quick add" text box. - -You can toggle them on and off manually just by clicking them. - -Use the "+ More options" button to reveal the automatic task options. - -#### Skill level/XP tasks - -Use these tasks to automatically track skill progress. Just select a skill, and the desired level or XP amount. The task will automatically complete once you hit that level/xp. - -#### Quest tasks - -Track quest progress and completion, just select a quest or miniquest from the dropdown. Will also display in progress quests as orange. - -#### Item tasks - -Select an item using the search button and searching via the in-game chatbox, then set the desired quantity. The plugin will keep track of your items and tally up quantities stored in different inventories (bank, player, GIMP storage), and will be automatically completed once you get that amount of the item. - -
\ No newline at end of file +----- \ No newline at end of file diff --git a/changelog.md b/changelog.md index 1d900a0..b01556e 100644 --- a/changelog.md +++ b/changelog.md @@ -14,7 +14,7 @@ Please leave for my sanity. thanks :) - Manual completion toggling for preset tasks. - Configurable color setting for completion chat messages. - Export and import goals to JSON with file filtering and full UI refresh. -- Preset Goal Lists with an “Add from Preset…” button (including Quest Cape, Ironman progressions, Void, F2P, Fast Travel, etc.). +- Preset Goal Lists with an “Add from Preset…” button. - Full Void Armor, Free-to-Play Quests, and Fast Travel Unlocks presets. - Expanded and reworked Early, Mid, and Late Ironman presets. - Inline editing support for goal titles and manual task descriptions. diff --git a/runelite-plugin.properties b/runelite-plugin.properties index fbf7e90..81ba3a4 100644 --- a/runelite-plugin.properties +++ b/runelite-plugin.properties @@ -1,6 +1,6 @@ displayName=Goal Tracker -author=toofifty -support=https://github.com/toofifty/goaltracker +author=Toofifty +support=https://github.com/Toofifty/rl-goal-tracker description=Track and organize RuneScape goals with progress bars, presets, and more. tags=task,to-do,todo,Track,organize,progress plugins=com.toofifty.goaltracker.GoalTrackerPlugin diff --git a/src/main/java/com/toofifty/goaltracker/models/task/SkillLevelTask.java b/src/main/java/com/toofifty/goaltracker/models/task/SkillLevelTask.java index c8d07f3..46ed71c 100644 --- a/src/main/java/com/toofifty/goaltracker/models/task/SkillLevelTask.java +++ b/src/main/java/com/toofifty/goaltracker/models/task/SkillLevelTask.java @@ -19,15 +19,15 @@ public final class SkillLevelTask extends Task private int level; @Override - public String toString() + public String getDisplayName() { - return String.format("%s %s", level, skill.getName()); + return String.format("Reach level %d %s", level, skill.getName()); } @Override - public String getDisplayName() + public String toString() { - return String.format("Reach level %d %s", level, skill.getName()); + return String.format("%s %s", level, skill.getName()); } @Override diff --git a/src/main/java/com/toofifty/goaltracker/models/task/Task.java b/src/main/java/com/toofifty/goaltracker/models/task/Task.java index 3f9204d..9842f5d 100644 --- a/src/main/java/com/toofifty/goaltracker/models/task/Task.java +++ b/src/main/java/com/toofifty/goaltracker/models/task/Task.java @@ -61,13 +61,14 @@ public boolean isNotFullyIndented() { return !isFullyIndented(); } + + @Override + abstract public String toString(); + /** * Returns a human-readable name for the task. */ public abstract String getDisplayName(); - @Override - abstract public String toString(); - abstract public TaskType getType(); }