From 3731232c4916c83a694cf752dde76c2383f563ba Mon Sep 17 00:00:00 2001 From: AhDoozy Date: Mon, 28 Jul 2025 11:39:42 -0400 Subject: [PATCH 01/41] 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/41] 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/41] - 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] - 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/41] - 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/41] - 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/41] 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/41] - 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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; + } }