diff --git a/src/main/java/net/reldo/taskstracker/TasksTrackerConfig.java b/src/main/java/net/reldo/taskstracker/TasksTrackerConfig.java index fbaa4a2..e7a2831 100644 --- a/src/main/java/net/reldo/taskstracker/TasksTrackerConfig.java +++ b/src/main/java/net/reldo/taskstracker/TasksTrackerConfig.java @@ -4,15 +4,30 @@ import net.runelite.client.config.Config; import net.runelite.client.config.ConfigGroup; import net.runelite.client.config.ConfigItem; +import net.runelite.client.config.ConfigSection; +import net.runelite.client.config.Range; @ConfigGroup(TasksTrackerPlugin.CONFIG_GROUP_NAME) public interface TasksTrackerConfig extends Config { + + /*==================== + -- General settings -- + ====================*/ + + @ConfigSection( + name = "General", + description = "General settings", + position = 0 + ) + String generalSettings = "generalSettings"; + @ConfigItem( - position = 10, - keyName = "untrackUponCompletion", - name = "Untrack Tasks Upon Completion", - description = "Configures whether completed tasks should also automatically untracked when the task is completed." + position = 10, + keyName = "untrackUponCompletion", + name = "Untrack Tasks Upon Completion", + description = "Configures whether completed tasks should also automatically untracked when the task is completed.", + section = generalSettings ) default boolean untrackUponCompletion() { @@ -23,7 +38,8 @@ default boolean untrackUponCompletion() position = 11, keyName = "filterPanelCollapsible", name = "Filter Panels Collapsible", - description = "Shows button that allows filter panels to be hidden." + description = "Shows button that allows filter panels to be hidden.", + section = generalSettings ) default boolean filterPanelCollapsible() { @@ -35,13 +51,37 @@ default boolean filterPanelCollapsible() keyName = "saveSubFilterState", //@todo generalise this to all sub-filters name = "Save Filter State", description = "Configures whether the state of area filters should be saved and recalled when switching task type or restarting the plugin.", - hidden = true + hidden = true //todo This is hidden because it currently doesn't do anything ) default boolean saveSubFilterState() { return true; } + + @ConfigSection( + name = "Internal Config", + description = "These settings change the internal behaviour of the plugin. Reset them if any issues occur.", + position = 10, + closedByDefault = true + ) + String internalConfig = "internalConfig"; + + @Range( + min = 10 + ) + @ConfigItem( + position = 13, + keyName = "taskPanelBatchSize", + name = "Task Panel Batch Size", + description = "Configures the number of task panels to create in each batch when redrawing the task list panel.", + section = internalConfig + ) + default int taskPanelBatchSize() + { + return 50; + } + @ConfigItem( position = 100, keyName = "completedFilter", @@ -87,15 +127,15 @@ default ConfigValues.IgnoredFilterValues ignoredFilter() ) default ConfigValues.TaskListTabs taskListTab() { - return ConfigValues.TaskListTabs.ALL; + return ConfigValues.TaskListTabs.TAB_TWO; } @ConfigItem( - position = 106, - keyName = "taskTypeJsonName", - name = "Task Type", - description = "Configures the task type which is displayed in the panel.", - hidden = true + position = 106, + keyName = "taskTypeJsonName", + name = "Task Type", + description = "Configures the task type which is displayed in the panel.", + hidden = true ) default String taskTypeJsonName() { @@ -103,11 +143,11 @@ default String taskTypeJsonName() } @ConfigItem( - position = 109, - keyName = "dropdownFilter", - name = "Dropdown Filter", - description = "Configures the dropdown to filter tasks on.", - hidden = true + position = 109, + keyName = "dropdownFilter", + name = "Dropdown Filter", + description = "Configures the dropdown to filter tasks on.", + hidden = true ) default String dropdownFilter() { @@ -137,4 +177,295 @@ default ConfigValues.SortDirections sortDirection() { return ConfigValues.SortDirections.ASCENDING; } + + /*========================== + -- Task List Tab settings -- + ==========================*/ + + /*================== + -- Tab 1 settings -- + ==================*/ + + @ConfigSection( + name = "Task List Tab 1", + description = "Tab 1 settings", + position = 1 + ) + String tab1Settings = "tab1Settings"; + + @ConfigItem( + position = 1, + keyName = "tab1Name", + name = "Name", + description = "The name of the tab. Default: Tracked Tasks", + section = tab1Settings + ) + default String tab1Name() + { + return "Tracked"; + } + + @ConfigItem( + position = 2, + keyName = "tab1CompletedLock", + name = "Lock Completed Filter", + description = "Locks the completed tasks filter button to the configured value when tab 1 is selected.", + section = tab1Settings + ) + default boolean tab1CompletedLock() + { + return false; + } + + @ConfigItem( + position = 3, + keyName = "tab1CompletedValue", + name = "Completed Filter", + description = "The configured completed tasks filter button value.", + section = tab1Settings + ) + default ConfigValues.CompletedFilterValues tab1CompletedValue() + { + return ConfigValues.CompletedFilterValues.INCOMPLETE; + } + + @ConfigItem( + position = 4, + keyName = "tab1TrackedLock", + name = "Lock Tracked Filter", + description = "Locks the tracked tasks filter button to the configured value when tab 1 is selected.", + section = tab1Settings + ) + default boolean tab1TrackedLock() + { + return false; + } + + @ConfigItem( + position = 5, + keyName = "tab1TrackedValue", + name = "Tracked Filter", + description = "The configured tracked tasks filter button value.", + section = tab1Settings + ) + default ConfigValues.TrackedFilterValues tab1TrackedValue() + { + return ConfigValues.TrackedFilterValues.TRACKED; + } + + @ConfigItem( + position = 6, + keyName = "tab1IgnoredLock", + name = "Lock Ignored Filter", + description = "Locks the ignored tasks filter button to the configured value when tab 1 is selected.", + section = tab1Settings + ) + default boolean tab1IgnoredLock() + { + return false; + } + + @ConfigItem( + position = 7, + keyName = "tab1IgnoredValue", + name = "Ignored Filter", + description = "The configured ignored tasks filter button value.", + section = tab1Settings + ) + default ConfigValues.IgnoredFilterValues tab1IgnoredValue() + { + return ConfigValues.IgnoredFilterValues.NOT_IGNORED; + } + + /*================== + -- Tab 2 settings -- + ==================*/ + + @ConfigSection( + name = "Task List Tab 2", + description = "Tab 2 settings", + position = 2 + ) + String tab2Settings = "tab2Settings"; + + @ConfigItem( + position = 1, + keyName = "tab2Name", + name = "Name", + description = "The name of the tab. Default: All Tasks", + section = tab2Settings + ) + default String tab2Name() + { + return "Incomplete"; + } + + @ConfigItem( + position = 2, + keyName = "tab2CompletedLock", + name = "Lock Completed Filter", + description = "Locks the completed tasks filter button to the configured value when tab 2 is selected.", + section = tab2Settings + ) + default boolean tab2CompletedLock() + { + return false; + } + + @ConfigItem( + position = 3, + keyName = "tab2CompletedValue", + name = "Completed Filter", + description = "The configured completed tasks filter button value.", + section = tab2Settings + ) + default ConfigValues.CompletedFilterValues tab2CompletedValue() + { + return ConfigValues.CompletedFilterValues.INCOMPLETE; + } + + @ConfigItem( + position = 4, + keyName = "tab2TrackedLock", + name = "Lock Tracked Filter", + description = "Locks the tracked tasks filter button to the configured value when tab 2 is selected.", + section = tab2Settings + ) + default boolean tab2TrackedLock() + { + return false; + } + + @ConfigItem( + position = 5, + keyName = "tab2TrackedValue", + name = "Tracked Filter", + description = "The configured tracked tasks filter button value.", + section = tab2Settings + ) + default ConfigValues.TrackedFilterValues tab2TrackedValue() + { + return ConfigValues.TrackedFilterValues.TRACKED_AND_UNTRACKED; + } + + @ConfigItem( + position = 6, + keyName = "tab2IgnoredLock", + name = "Lock Ignored Filter", + description = "Locks the ignored tasks filter button to the configured value when tab 2 is selected.", + section = tab2Settings + ) + default boolean tab2IgnoredLock() + { + return false; + } + + @ConfigItem( + position = 7, + keyName = "tab2IgnoredValue", + name = "Ignored Filter", + description = "The configured ignored tasks filter button value.", + section = tab2Settings + ) + default ConfigValues.IgnoredFilterValues tab2IgnoredValue() + { + return ConfigValues.IgnoredFilterValues.NOT_IGNORED; + } + + /*================== + -- Tab 3 settings -- + ==================*/ + + @ConfigSection( + name = "Task List Tab 3", + description = "Tab 3 settings", + position = 3 + ) + String tab3Settings = "tab3Settings"; + + @ConfigItem( + position = 1, + keyName = "tab3Name", + name = "Name", + description = "The name of the tab. Default: Custom", + section = tab3Settings + ) + default String tab3Name() + { + return "All Tasks"; + } + + @ConfigItem( + position = 2, + keyName = "tab3CompletedLock", + name = "Lock Completed Filter", + description = "Locks the completed tasks filter button to the configured value when tab 3 is selected.", + section = tab3Settings + ) + default boolean tab3CompletedLock() + { + return false; + } + + @ConfigItem( + position = 3, + keyName = "tab3CompletedValue", + name = "Completed Filter", + description = "The configured completed tasks filter button value.", + section = tab3Settings + ) + default ConfigValues.CompletedFilterValues tab3CompletedValue() + { + return ConfigValues.CompletedFilterValues.COMPLETE_AND_INCOMPLETE; + } + + @ConfigItem( + position = 4, + keyName = "tab3TrackedLock", + name = "Lock Tracked Filter", + description = "Locks the tracked tasks filter button to the configured value when tab 3 is selected.", + section = tab3Settings + ) + default boolean tab3TrackedLock() + { + return false; + } + + @ConfigItem( + position = 5, + keyName = "tab3TrackedValue", + name = "Tracked Filter", + description = "The configured tracked tasks filter button value.", + section = tab3Settings + ) + default ConfigValues.TrackedFilterValues tab3TrackedValue() + { + return ConfigValues.TrackedFilterValues.TRACKED_AND_UNTRACKED; + } + + @ConfigItem( + position = 6, + keyName = "tab3IgnoredLock", + name = "Lock Ignored Filter", + description = "Locks the ignored tasks filter button to the configured value when tab 3 is selected.", + section = tab3Settings + ) + default boolean tab3IgnoredLock() + { + return false; + } + + @ConfigItem( + position = 7, + keyName = "tab3IgnoredValue", + name = "Ignored Filter", + description = "The configured ignored tasks filter button value.", + section = tab3Settings + ) + default ConfigValues.IgnoredFilterValues tab3IgnoredValue() + { + return ConfigValues.IgnoredFilterValues.IGNORED_AND_NOT_IGNORED; + } + + } diff --git a/src/main/java/net/reldo/taskstracker/TasksTrackerPlugin.java b/src/main/java/net/reldo/taskstracker/TasksTrackerPlugin.java index 1683b9b..f9d525e 100644 --- a/src/main/java/net/reldo/taskstracker/TasksTrackerPlugin.java +++ b/src/main/java/net/reldo/taskstracker/TasksTrackerPlugin.java @@ -1,575 +1,588 @@ -package net.reldo.taskstracker; - -import com.google.gson.Gson; -import com.google.inject.Binder; -import com.google.inject.Provides; -import java.awt.Color; -import java.awt.Toolkit; -import java.awt.datatransfer.StringSelection; -import java.awt.image.BufferedImage; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import javax.annotation.Nullable; -import javax.inject.Inject; -import javax.inject.Named; -import javax.swing.JDialog; -import javax.swing.JOptionPane; -import javax.swing.SwingUtilities; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import net.reldo.taskstracker.data.Export; -import net.reldo.taskstracker.data.LongSerializer; -import net.reldo.taskstracker.data.TasksSummary; -import net.reldo.taskstracker.data.TrackerConfigStore; -import net.reldo.taskstracker.data.jsondatastore.reader.DataStoreReader; -import net.reldo.taskstracker.data.jsondatastore.reader.HttpDataStoreReader; -import net.reldo.taskstracker.data.reldo.ReldoImport; -import net.reldo.taskstracker.data.task.TaskFromStruct; -import net.reldo.taskstracker.data.task.TaskService; -import net.reldo.taskstracker.data.task.TaskType; -import net.reldo.taskstracker.data.task.filters.FilterService; -import net.reldo.taskstracker.panel.TasksTrackerPluginPanel; -import net.runelite.api.ChatMessageType; -import net.runelite.api.Client; -import net.runelite.api.Experience; -import net.runelite.api.GameState; -import net.runelite.api.Skill; -import net.runelite.api.events.CommandExecuted; -import net.runelite.api.events.GameStateChanged; -import net.runelite.api.events.GameTick; -import net.runelite.api.events.StatChanged; -import net.runelite.api.events.VarbitChanged; -import net.runelite.client.callback.ClientThread; -import net.runelite.client.chat.ChatMessageBuilder; -import net.runelite.client.chat.ChatMessageManager; -import net.runelite.client.chat.QueuedMessage; -import net.runelite.client.config.ConfigManager; -import net.runelite.client.config.RuneScapeProfileType; -import net.runelite.client.eventbus.Subscribe; -import net.runelite.client.events.ConfigChanged; -import net.runelite.client.events.ProfileChanged; -import net.runelite.client.game.SpriteManager; -import net.runelite.client.plugins.Plugin; -import net.runelite.client.plugins.PluginDescriptor; -import net.runelite.client.plugins.PluginManager; -import net.runelite.client.ui.ClientToolbar; -import net.runelite.client.ui.NavigationButton; -import net.runelite.client.util.ImageUtil; -import net.runelite.client.util.LinkBrowser; - -@Slf4j -@PluginDescriptor( - name = "Tasks Tracker" -) -public class TasksTrackerPlugin extends Plugin -{ - public static final String CONFIG_GROUP_NAME = "tasks-tracker"; - - public int[] playerSkills; - - public String taskTextFilter; - - public TasksTrackerPluginPanel pluginPanel; - - private static final long VARP_UPDATE_THROTTLE_DELAY_MS = 7 * 1000; - - private boolean forceUpdateVarpsFlag = false; - private Set varpIdsToUpdate = new HashSet<>(); - private long lastVarpUpdate = 0; - private NavigationButton navButton; - private RuneScapeProfileType currentProfileType; - private final Map oldExperience = new EnumMap<>(Skill.class); - - @Inject @Named("runelite.version") private String runeliteVersion; - @Inject private Gson gson; - @Inject private Client client; - @Inject private SpriteManager spriteManager; - @Inject private PluginManager pluginManager; - @Inject private ClientToolbar clientToolbar; - @Inject private ClientThread clientThread; - @Inject private ChatMessageManager chatMessageManager; - @Getter @Inject private ConfigManager configManager; - @Getter @Inject private TasksTrackerConfig config; - - @Inject private TrackerConfigStore trackerConfigStore; - @Inject private TaskService taskService; - @Inject private FilterService filterService; - - @Override - public void configure(Binder binder) - { - binder.bind(DataStoreReader.class).to(HttpDataStoreReader.class); - super.configure(binder); - } - - @Provides - TasksTrackerConfig getConfig(ConfigManager configManager) - { - return configManager.getConfig(TasksTrackerConfig.class); - } - - @Override - protected void startUp() - { - try - { - String taskTypeJsonName = config.taskTypeJsonName(); - taskService.setTaskType(taskTypeJsonName); - } - catch (Exception ex) - { - log.error("error setting task type in startUp", ex); - } - - forceUpdateVarpsFlag = false; - - pluginPanel = new TasksTrackerPluginPanel(this, config, spriteManager, taskService); - - boolean isLoggedIn = isLoggedInState(client.getGameState()); - pluginPanel.setLoggedIn(isLoggedIn); - if (isLoggedIn) - { - forceUpdateVarpsFlag = true; - } - - final BufferedImage icon = ImageUtil.loadImageResource(getClass(), "panel_icon.png"); - navButton = NavigationButton.builder() - .tooltip("Task Tracker") - .icon(icon) - .priority(5) - .panel(pluginPanel) - .build(); - clientToolbar.addNavigation(navButton); - - log.info("Tasks Tracker started!"); - } - - @Override - protected void shutDown() - { - pluginPanel = null; - taskService.clearTaskTypes(); - clientToolbar.removeNavigation(navButton); - log.info("Tasks Tracker stopped!"); - } - - @Subscribe - public void onCommandExecuted(CommandExecuted commandExecuted) - { - if (!commandExecuted.getCommand().startsWith("tt")) return; - - if (commandExecuted.getCommand().equalsIgnoreCase("tt-process-varp")) - { - String[] args = commandExecuted.getArguments(); - if (args.length == 0) return; - - try - { - int varpId = Integer.parseInt(args[0]); - log.debug("Processing varpId " + varpId); - processVarpAndUpdateTasks(varpId); - } - catch (NumberFormatException e) - { - log.debug("Invalid varpId, provide a valid integer"); - } - } - } - - @Subscribe - public void onVarbitChanged(VarbitChanged varbitChanged) - { - if (forceUpdateVarpsFlag || taskService.isTaskTypeChanged()) - { - // Force update is coming on next game tick, so ignore varbit change events - return; - } - int varpId = varbitChanged.getVarpId(); - if (!taskService.isVarpInCurrentTaskType(varpId)) - { - return; - } - varpIdsToUpdate.add(varbitChanged.getVarpId()); - } - - @Subscribe - public void onConfigChanged(ConfigChanged configChanged) - { - if (!configChanged.getGroup().equals(CONFIG_GROUP_NAME)) - { - return; - } - log.debug("onConfigChanged {} {}", configChanged.getKey(), configChanged.getNewValue()); - if (configChanged.getKey().equals("untrackUponCompletion") && config.untrackUponCompletion()) - { - forceVarpUpdate(); - } - - if (configChanged.getKey().equals("filterPanelCollapsible")) - { - SwingUtilities.invokeLater(pluginPanel::redraw); - } - } - - @Subscribe - public void onGameStateChanged(GameStateChanged gameStateChanged) - { - log.debug("onGameStateChanged {}", gameStateChanged.getGameState().toString()); - GameState newGameState = gameStateChanged.getGameState(); - RuneScapeProfileType newProfileType = RuneScapeProfileType.getCurrent(client); - - SwingUtilities.invokeLater(() -> pluginPanel.setLoggedIn(isLoggedInState(newGameState))); - - // Logged in - if (newGameState == GameState.LOGGING_IN) - { - forceUpdateVarpsFlag = true; - } - // Changed game mode - if (isLoggedInState(newGameState) && currentProfileType != null && currentProfileType != newProfileType) - { - forceUpdateVarpsFlag = true; - } - - currentProfileType = newProfileType; - } - - private boolean isLoggedInState(GameState gameState) - { - return gameState == GameState.LOGGED_IN || gameState == GameState.HOPPING || gameState == GameState.LOADING; - } - - @Subscribe - public void onGameTick(GameTick gameTick) - { - if (forceUpdateVarpsFlag || taskService.isTaskTypeChanged()) - { - log.debug("forceUpdateVarpsFlag game tick"); - trackerConfigStore.loadCurrentTaskTypeFromConfig(); - forceVarpUpdate(); - SwingUtilities.invokeLater(() -> pluginPanel.redraw()); - forceUpdateVarpsFlag = false; - taskService.setTaskTypeChanged(false); - } - - // Flush throttled varp updates - long currentTimeEpoch = System.currentTimeMillis(); - if (currentTimeEpoch - lastVarpUpdate > VARP_UPDATE_THROTTLE_DELAY_MS) - { - flushVarpUpdates(varpIdsToUpdate); - varpIdsToUpdate = new HashSet<>(); - lastVarpUpdate = currentTimeEpoch; - } - } - - @Subscribe - public void onStatChanged(StatChanged statChanged) - { - // @todo deprecate one of these, we don't need to track player skills twice. - // Cache current player skills - int[] newSkills = client.getRealSkillLevels(); - boolean changed = !Arrays.equals(playerSkills, newSkills); - if (changed) - { - playerSkills = client.getRealSkillLevels(); - } - - final Skill skill = statChanged.getSkill(); - - // Modified from m0bilebtw's modification from Nightfirecat's virtual level ups plugin - final int xpAfter = client.getSkillExperience(skill); - final int levelAfter = Experience.getLevelForXp(xpAfter); - final int xpBefore = oldExperience.getOrDefault(skill, -1); - final int levelBefore = xpBefore == -1 ? -1 : Experience.getLevelForXp(xpBefore); - - oldExperience.put(skill, xpAfter); - - // Do not proceed if any of the following are true: - // * xpBefore == -1 (don't fire when first setting new known value) - // * xpAfter <= xpBefore (do not allow 200m -> 200m exp drops) - // * levelBefore >= levelAfter (stop if we're not actually reaching a new level) - // * levelAfter > MAX_REAL_LEVEL (stop if above 99) - if (xpBefore == -1 || xpAfter <= xpBefore || levelBefore >= levelAfter || levelAfter > Experience.MAX_REAL_LEVEL) - { - return; - } - - // If we get here, 'skill' was leveled up! - SwingUtilities.invokeLater(() -> pluginPanel.taskListPanel.refreshTaskPanelsWithSkill(skill)); - } - - @Subscribe - public void onProfileChanged(ProfileChanged profileChanged) - { - final Optional taskTrackerPlugin = pluginManager.getPlugins().stream().filter(p -> p.getName().equals("Tasks Tracker")).findFirst(); - if (taskTrackerPlugin.isPresent() && pluginManager.isPluginEnabled(taskTrackerPlugin.get())) - { - reloadTaskType(); - } - } - - public void refresh() - { - SwingUtilities.invokeLater(() -> pluginPanel.refresh(null)); - } - - public void reloadTaskType() { - taskService.clearTaskTypes(); - filterService.clearFilterConfigs(); - try { - String taskTypeJsonName = config.taskTypeJsonName(); - taskService.setTaskType(taskTypeJsonName).thenAccept(isSet -> { - if (!isSet) { - return; - } - SwingUtilities.invokeLater(() -> - { - pluginPanel.redraw(); - pluginPanel.refresh(null); - }); - }); - } catch (Exception ex) { - log.error("error setting task type in reload", ex); - } - - } - - public void saveCurrentTaskTypeData() - { - log.debug("saveCurrentTaskTypeData"); - trackerConfigStore.saveCurrentTaskTypeData(); - } - - public void openImportJsonDialog() - { - JOptionPane optionPane = new JOptionPane("Paste import data into the text field below to import task tracker data.", JOptionPane.INFORMATION_MESSAGE); - optionPane.setWantsInput(true); - JDialog inputDialog = optionPane.createDialog(this.pluginPanel, "Import Tasks Input"); - inputDialog.setAlwaysOnTop(true); - inputDialog.setVisible(true); - - if (optionPane.getInputValue().equals("") || optionPane.getInputValue().equals("uninitializedValue")) - { - this.showMessageBox("Import Tasks Error", "Input was empty so no data has been imported.", JOptionPane.ERROR_MESSAGE, false); - return; - } - - String json = ""; - ReldoImport reldoImport; - try - { - json = (String) optionPane.getInputValue(); - reldoImport = this.gson.fromJson(json, ReldoImport.class); - } - catch (Exception ex) - { - this.showMessageBox("Import Tasks Error", "There was an issue importing task tracker data. " + ex.getMessage(), JOptionPane.ERROR_MESSAGE, false); - log.error("There was an issue importing task tracker data.", ex); - log.debug("reldoImport json: {}", json); - return; - } - - if (!reldoImport.taskTypeName.equalsIgnoreCase(config.taskTypeJsonName())) - { - this.showMessageBox("Import Tasks Error", String.format("Wrong task type. Select the %s task type to import this data.", reldoImport.taskTypeName), JOptionPane.ERROR_MESSAGE, false); - return; - } - - optionPane = new JOptionPane("Importing tasks will overwrite task tracker settings and cannot be undone. Are you sure you want to import tasks?", JOptionPane.WARNING_MESSAGE, JOptionPane.YES_NO_OPTION); - JDialog confirmDialog = optionPane.createDialog(this.pluginPanel, "Import Tasks Overwrite Confirmation"); - confirmDialog.setAlwaysOnTop(true); - confirmDialog.setVisible(true); - - Object selectedValue = optionPane.getValue(); - if (selectedValue == null) - { - return; - } - - if (selectedValue.equals(JOptionPane.YES_OPTION)) - { - HashMap tasksById = new HashMap<>(); - taskService.getTasks().forEach((task) -> tasksById.put(task.getIntParam("id"), task)); - - reldoImport.getTasks().forEach((id, reldoTaskSave) -> { - TaskFromStruct task = tasksById.get(id); - task.loadReldoSave(reldoTaskSave); - }); - - trackerConfigStore.saveCurrentTaskTypeData(); - pluginPanel.redraw(); - } - } - - public void sendTotalsToChat() - { - TasksSummary summary = new TasksSummary(taskService.getTasks()); - int trackedTasks = summary.trackedTasksCount; - int trackedPoints = summary.trackedTasksPoints; - - final String message = new ChatMessageBuilder() - .append(Color.BLACK, String.format("Task Tracker - Tracked Tasks: %s | Tracked Points: %s", trackedTasks, trackedPoints)) - .build(); - - chatMessageManager.queue( - QueuedMessage.builder() - .type(ChatMessageType.CONSOLE) - .runeLiteFormattedMessage(message) - .build()); - } - - public void copyJsonToClipboard() - { - clientThread.invokeLater(() -> { - // Not worried with this complexity on the client thread because it's from an infrequent button press - String json = getCurrentTaskTypeExportJson(); - final StringSelection stringSelection = new StringSelection(json); - Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, null); - - String message = "Copied " + taskService.getCurrentTaskType().getName() + " data to clipboard!"; - showMessageBox("Data Exported!", message, JOptionPane.INFORMATION_MESSAGE, true); - }); - } - - private void forceVarpUpdate() - { - log.debug("forceVarpUpdate"); - processVarpAndUpdateTasks(null).thenAccept((processed) -> { - if (processed) - { - log.debug("forceVarpUpdate processed complete, saving"); - saveCurrentTaskTypeData(); - } - }); - } - - private void flushVarpUpdates(Set varpIds) - { - log.debug("Flushing throttled varp updates {}", varpIds); - varpIds.forEach((id) -> processVarpAndUpdateTasks(id).thenAccept(processed -> { - if (processed) - { - log.debug("flushVarpUpdates processed complete, saving"); - saveCurrentTaskTypeData(); - } - })); - } - - private CompletableFuture processTaskStatus(TaskFromStruct task) - { - CompletableFuture future = new CompletableFuture<>(); - clientThread.invoke(() -> { - int taskId = task.getIntParam("id"); - int varbitIndex = taskId / 32; - int bitIndex = taskId % 32; - try - { - int varpId = task.getTaskType().getTaskVarps().get(varbitIndex); - BigInteger varpValue = BigInteger.valueOf(client.getVarpValue(varpId)); - boolean isTaskCompleted = varpValue.testBit(bitIndex); - task.setCompleted(isTaskCompleted); - if (isTaskCompleted && config.untrackUponCompletion()) - { - task.setTracked(false); - } - log.debug("process taskFromStruct {} ({}) {}", task.getStringParam("name"), task.getIntParam("id"), isTaskCompleted); - SwingUtilities.invokeLater(() -> pluginPanel.refresh(task)); - future.complete(isTaskCompleted); - } - catch (Exception ex) - { - log.error("Error processing task status {}", taskId, ex); - future.completeExceptionally(ex); - } - }); - return future; - } - - /** - * Update task completion status. If no varpId is specified, it updates all tasks in the current task type - * @param varpId varp id to update (optional) - * @return An observable that emits true if all tasks were processed - */ - private CompletableFuture processVarpAndUpdateTasks(@Nullable Integer varpId) - { - log.info("processVarpAndUpdateTasks: " + (varpId != null ? varpId : "all")); - - List tasks = varpId != null ? - taskService.getTasksFromVarpId(varpId) : - taskService.getTasks(); - - List> taskFutures = new ArrayList<>(); - for (TaskFromStruct task : tasks) - { - CompletableFuture taskFuture = processTaskStatus(task); - taskFutures.add(taskFuture); - } - - CompletableFuture allTasksFuture = CompletableFuture.allOf(taskFutures.toArray(new CompletableFuture[0])); - return allTasksFuture.thenApply(v -> true); - } - - private String getCurrentTaskTypeExportJson() - { - TaskType taskType = taskService.getCurrentTaskType(); - Gson gson = this.gson.newBuilder() - .excludeFieldsWithoutExposeAnnotation() - .registerTypeAdapter(float.class, new LongSerializer()) - .create(); - - if (taskType == null) - { - String error = "Cannot export to JSON; no task type selected."; - log.error(error); - return error; - } - else - { - Export export = new Export(taskType, taskService.getTasks(), runeliteVersion, client); - return gson.toJson(export); - } - } - - private void showMessageBox(final String title, final String message, int messageType, boolean showOpenLeagueTools) - { - SwingUtilities.invokeLater(() -> { - JOptionPane optionPane; - JDialog dialog; - - if (showOpenLeagueTools) - { - String[] options = {"Open OS League Tools", "Ok"}; - - optionPane = new JOptionPane(message, messageType, JOptionPane.YES_NO_OPTION, null, options, options[1]); - } - else - { - optionPane = new JOptionPane(message, messageType); - } - - dialog = optionPane.createDialog(pluginPanel, title); - dialog.setAlwaysOnTop(true); - dialog.setVisible(true); - - Object selectedValue = optionPane.getValue(); - if (selectedValue == null) - { - return; - } - - if (selectedValue.equals("Open OS League Tools")) - { - LinkBrowser.browse("https://www.osleague.tools/tracker?open=import&tab=tasks"); - } - }); - } -} +package net.reldo.taskstracker; + +import com.google.gson.Gson; +import com.google.inject.Binder; +import com.google.inject.Provides; +import java.awt.Color; +import java.awt.Toolkit; +import java.awt.datatransfer.StringSelection; +import java.awt.image.BufferedImage; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; +import javax.inject.Inject; +import javax.inject.Named; +import javax.swing.JDialog; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.reldo.taskstracker.data.Export; +import net.reldo.taskstracker.data.LongSerializer; +import net.reldo.taskstracker.data.TasksSummary; +import net.reldo.taskstracker.data.TrackerConfigStore; +import net.reldo.taskstracker.data.jsondatastore.reader.DataStoreReader; +import net.reldo.taskstracker.data.jsondatastore.reader.HttpDataStoreReader; +import net.reldo.taskstracker.data.reldo.ReldoImport; +import net.reldo.taskstracker.data.task.TaskFromStruct; +import net.reldo.taskstracker.data.task.TaskService; +import net.reldo.taskstracker.data.task.TaskType; +import net.reldo.taskstracker.data.task.filters.FilterService; +import net.reldo.taskstracker.panel.TasksTrackerPluginPanel; +import net.runelite.api.ChatMessageType; +import net.runelite.api.Client; +import net.runelite.api.Experience; +import net.runelite.api.GameState; +import net.runelite.api.Skill; +import net.runelite.api.events.CommandExecuted; +import net.runelite.api.events.GameStateChanged; +import net.runelite.api.events.GameTick; +import net.runelite.api.events.StatChanged; +import net.runelite.api.events.VarbitChanged; +import net.runelite.client.callback.ClientThread; +import net.runelite.client.chat.ChatMessageBuilder; +import net.runelite.client.chat.ChatMessageManager; +import net.runelite.client.chat.QueuedMessage; +import net.runelite.client.config.ConfigManager; +import net.runelite.client.config.RuneScapeProfileType; +import net.runelite.client.eventbus.Subscribe; +import net.runelite.client.events.ConfigChanged; +import net.runelite.client.events.ProfileChanged; +import net.runelite.client.game.SpriteManager; +import net.runelite.client.plugins.Plugin; +import net.runelite.client.plugins.PluginDescriptor; +import net.runelite.client.plugins.PluginManager; +import net.runelite.client.ui.ClientToolbar; +import net.runelite.client.ui.NavigationButton; +import net.runelite.client.util.ImageUtil; +import net.runelite.client.util.LinkBrowser; + +@Slf4j +@PluginDescriptor( + name = "Tasks Tracker" +) +public class TasksTrackerPlugin extends Plugin +{ + public static final String CONFIG_GROUP_NAME = "tasks-tracker"; + + public int[] playerSkills; + + public String taskTextFilter; + + public TasksTrackerPluginPanel pluginPanel; + + private static final long VARP_UPDATE_THROTTLE_DELAY_MS = 7 * 1000; + + private boolean forceUpdateVarpsFlag = false; + private Set varpIdsToUpdate = new HashSet<>(); + private long lastVarpUpdate = 0; + private NavigationButton navButton; + private RuneScapeProfileType currentProfileType; + private final Map oldExperience = new EnumMap<>(Skill.class); + + @Inject @Named("runelite.version") private String runeliteVersion; + @Inject private Gson gson; + @Inject private Client client; + @Inject private SpriteManager spriteManager; + @Inject private PluginManager pluginManager; + @Inject private ClientToolbar clientToolbar; + @Inject private ClientThread clientThread; + @Inject private ChatMessageManager chatMessageManager; + @Getter @Inject private ConfigManager configManager; + @Getter @Inject private TasksTrackerConfig config; + + @Inject private TrackerConfigStore trackerConfigStore; + @Inject private TaskService taskService; + @Inject private FilterService filterService; + + @Override + public void configure(Binder binder) + { + binder.bind(DataStoreReader.class).to(HttpDataStoreReader.class); + super.configure(binder); + } + + @Provides + TasksTrackerConfig getConfig(ConfigManager configManager) + { + return configManager.getConfig(TasksTrackerConfig.class); + } + + @Override + protected void startUp() + { + try + { + String taskTypeJsonName = config.taskTypeJsonName(); + taskService.setTaskType(taskTypeJsonName); + } + catch (Exception ex) + { + log.error("error setting task type in startUp", ex); + } + + forceUpdateVarpsFlag = false; + + pluginPanel = new TasksTrackerPluginPanel(this, config, spriteManager, taskService); + + boolean isLoggedIn = isLoggedInState(client.getGameState()); + pluginPanel.setLoggedIn(isLoggedIn); + if (isLoggedIn) + { + forceUpdateVarpsFlag = true; + } + + final BufferedImage icon = ImageUtil.loadImageResource(getClass(), "panel_icon.png"); + navButton = NavigationButton.builder() + .tooltip("Task Tracker") + .icon(icon) + .priority(5) + .panel(pluginPanel) + .build(); + clientToolbar.addNavigation(navButton); + + log.info("Tasks Tracker started!"); + } + + @Override + protected void shutDown() + { + pluginPanel = null; + taskService.clearTaskTypes(); + clientToolbar.removeNavigation(navButton); + log.info("Tasks Tracker stopped!"); + } + + @Subscribe + public void onCommandExecuted(CommandExecuted commandExecuted) + { + if (!commandExecuted.getCommand().startsWith("tt")) return; + + if (commandExecuted.getCommand().equalsIgnoreCase("tt-process-varp")) + { + String[] args = commandExecuted.getArguments(); + if (args.length == 0) return; + + try + { + int varpId = Integer.parseInt(args[0]); + log.debug("Processing varpId " + varpId); + processVarpAndUpdateTasks(varpId); + } + catch (NumberFormatException e) + { + log.debug("Invalid varpId, provide a valid integer"); + } + } + } + + @Subscribe + public void onVarbitChanged(VarbitChanged varbitChanged) + { + if (forceUpdateVarpsFlag || taskService.isTaskTypeChanged()) + { + // Force update is coming on next game tick, so ignore varbit change events + return; + } + int varpId = varbitChanged.getVarpId(); + if (!taskService.isVarpInCurrentTaskType(varpId)) + { + return; + } + varpIdsToUpdate.add(varbitChanged.getVarpId()); + } + + @Subscribe + public void onConfigChanged(ConfigChanged configChanged) + { + if (!configChanged.getGroup().equals(CONFIG_GROUP_NAME)) + { + return; + } + + log.debug("onConfigChanged {} {}", configChanged.getKey(), configChanged.getNewValue()); + if (configChanged.getKey().equals("untrackUponCompletion") && config.untrackUponCompletion()) + { + forceVarpUpdate(); + } + + if (configChanged.getKey().equals("filterPanelCollapsible")) + { + SwingUtilities.invokeLater(pluginPanel::redraw); + } + + if (configChanged.getKey().startsWith("tab")) // task list tab config items all start 'tab#' + { + pluginPanel.refreshFilterButtonsFromConfig(config.taskListTab()); + refresh(); + } + + if (configChanged.getKey().equals("taskPanelBatchSize")) + { + pluginPanel.taskListPanel.setBatchSize(config.taskPanelBatchSize()); + } + } + + @Subscribe + public void onGameStateChanged(GameStateChanged gameStateChanged) + { + log.debug("onGameStateChanged {}", gameStateChanged.getGameState().toString()); + GameState newGameState = gameStateChanged.getGameState(); + RuneScapeProfileType newProfileType = RuneScapeProfileType.getCurrent(client); + + SwingUtilities.invokeLater(() -> pluginPanel.setLoggedIn(isLoggedInState(newGameState))); + + // Logged in + if (newGameState == GameState.LOGGING_IN) + { + forceUpdateVarpsFlag = true; + } + // Changed game mode + if (isLoggedInState(newGameState) && currentProfileType != null && currentProfileType != newProfileType) + { + forceUpdateVarpsFlag = true; + } + + currentProfileType = newProfileType; + } + + private boolean isLoggedInState(GameState gameState) + { + return gameState == GameState.LOGGED_IN || gameState == GameState.HOPPING || gameState == GameState.LOADING; + } + + @Subscribe + public void onGameTick(GameTick gameTick) + { + if (forceUpdateVarpsFlag || taskService.isTaskTypeChanged()) + { + log.debug("forceUpdateVarpsFlag game tick"); + trackerConfigStore.loadCurrentTaskTypeFromConfig(); + forceVarpUpdate(); + SwingUtilities.invokeLater(() -> pluginPanel.drawNewTaskType()); + forceUpdateVarpsFlag = false; + taskService.setTaskTypeChanged(false); + } + + // Flush throttled varp updates + long currentTimeEpoch = System.currentTimeMillis(); + if (currentTimeEpoch - lastVarpUpdate > VARP_UPDATE_THROTTLE_DELAY_MS) + { + flushVarpUpdates(varpIdsToUpdate); + varpIdsToUpdate = new HashSet<>(); + lastVarpUpdate = currentTimeEpoch; + } + } + + @Subscribe + public void onStatChanged(StatChanged statChanged) + { + // @todo deprecate one of these, we don't need to track player skills twice. + // Cache current player skills + int[] newSkills = client.getRealSkillLevels(); + boolean changed = !Arrays.equals(playerSkills, newSkills); + if (changed) + { + playerSkills = client.getRealSkillLevels(); + } + + final Skill skill = statChanged.getSkill(); + + // Modified from m0bilebtw's modification from Nightfirecat's virtual level ups plugin + final int xpAfter = client.getSkillExperience(skill); + final int levelAfter = Experience.getLevelForXp(xpAfter); + final int xpBefore = oldExperience.getOrDefault(skill, -1); + final int levelBefore = xpBefore == -1 ? -1 : Experience.getLevelForXp(xpBefore); + + oldExperience.put(skill, xpAfter); + + // Do not proceed if any of the following are true: + // * xpBefore == -1 (don't fire when first setting new known value) + // * xpAfter <= xpBefore (do not allow 200m -> 200m exp drops) + // * levelBefore >= levelAfter (stop if we're not actually reaching a new level) + // * levelAfter > MAX_REAL_LEVEL (stop if above 99) + if (xpBefore == -1 || xpAfter <= xpBefore || levelBefore >= levelAfter || levelAfter > Experience.MAX_REAL_LEVEL) + { + return; + } + + // If we get here, 'skill' was leveled up! + SwingUtilities.invokeLater(() -> pluginPanel.taskListPanel.refreshTaskPanelsWithSkill(skill)); + } + + @Subscribe + public void onProfileChanged(ProfileChanged profileChanged) + { + final Optional taskTrackerPlugin = pluginManager.getPlugins().stream().filter(p -> p.getName().equals("Tasks Tracker")).findFirst(); + if (taskTrackerPlugin.isPresent() && pluginManager.isPluginEnabled(taskTrackerPlugin.get())) + { + reloadTaskType(); + } + } + + public void refresh() + { + SwingUtilities.invokeLater(() -> pluginPanel.refresh(null)); + } + + public void reloadTaskType() { + taskService.clearTaskTypes(); + filterService.clearFilterConfigs(); + try { + String taskTypeJsonName = config.taskTypeJsonName(); + taskService.setTaskType(taskTypeJsonName).thenAccept(isSet -> { + if (!isSet) { + return; + } + SwingUtilities.invokeLater(() -> + { + pluginPanel.drawNewTaskType(); + pluginPanel.refreshFilterButtonsFromConfig(config.taskListTab()); + pluginPanel.refresh(null); + }); + }); + } catch (Exception ex) { + log.error("error setting task type in reload", ex); + } + + } + + public void saveCurrentTaskTypeData() + { + log.debug("saveCurrentTaskTypeData"); + trackerConfigStore.saveCurrentTaskTypeData(); + } + + public void openImportJsonDialog() + { + JOptionPane optionPane = new JOptionPane("Paste import data into the text field below to import task tracker data.", JOptionPane.INFORMATION_MESSAGE); + optionPane.setWantsInput(true); + JDialog inputDialog = optionPane.createDialog(this.pluginPanel, "Import Tasks Input"); + inputDialog.setAlwaysOnTop(true); + inputDialog.setVisible(true); + + if (optionPane.getInputValue().equals("") || optionPane.getInputValue().equals("uninitializedValue")) + { + this.showMessageBox("Import Tasks Error", "Input was empty so no data has been imported.", JOptionPane.ERROR_MESSAGE, false); + return; + } + + String json = ""; + ReldoImport reldoImport; + try + { + json = (String) optionPane.getInputValue(); + reldoImport = this.gson.fromJson(json, ReldoImport.class); + } + catch (Exception ex) + { + this.showMessageBox("Import Tasks Error", "There was an issue importing task tracker data. " + ex.getMessage(), JOptionPane.ERROR_MESSAGE, false); + log.error("There was an issue importing task tracker data.", ex); + log.debug("reldoImport json: {}", json); + return; + } + + if (!reldoImport.taskTypeName.equalsIgnoreCase(config.taskTypeJsonName())) + { + this.showMessageBox("Import Tasks Error", String.format("Wrong task type. Select the %s task type to import this data.", reldoImport.taskTypeName), JOptionPane.ERROR_MESSAGE, false); + return; + } + + optionPane = new JOptionPane("Importing tasks will overwrite task tracker settings and cannot be undone. Are you sure you want to import tasks?", JOptionPane.WARNING_MESSAGE, JOptionPane.YES_NO_OPTION); + JDialog confirmDialog = optionPane.createDialog(this.pluginPanel, "Import Tasks Overwrite Confirmation"); + confirmDialog.setAlwaysOnTop(true); + confirmDialog.setVisible(true); + + Object selectedValue = optionPane.getValue(); + if (selectedValue == null) + { + return; + } + + if (selectedValue.equals(JOptionPane.YES_OPTION)) + { + HashMap tasksById = new HashMap<>(); + taskService.getTasks().forEach((task) -> tasksById.put(task.getIntParam("id"), task)); + + reldoImport.getTasks().forEach((id, reldoTaskSave) -> { + TaskFromStruct task = tasksById.get(id); + task.loadReldoSave(reldoTaskSave); + }); + + trackerConfigStore.saveCurrentTaskTypeData(); + pluginPanel.redraw(); + } + } + + public void sendTotalsToChat() + { + TasksSummary summary = new TasksSummary(taskService.getTasks()); + int trackedTasks = summary.trackedTasksCount; + int trackedPoints = summary.trackedTasksPoints; + + final String message = new ChatMessageBuilder() + .append(Color.BLACK, String.format("Task Tracker - Tracked Tasks: %s | Tracked Points: %s", trackedTasks, trackedPoints)) + .build(); + + chatMessageManager.queue( + QueuedMessage.builder() + .type(ChatMessageType.CONSOLE) + .runeLiteFormattedMessage(message) + .build()); + } + + public void copyJsonToClipboard() + { + clientThread.invokeLater(() -> { + // Not worried with this complexity on the client thread because it's from an infrequent button press + String json = getCurrentTaskTypeExportJson(); + final StringSelection stringSelection = new StringSelection(json); + Toolkit.getDefaultToolkit().getSystemClipboard().setContents(stringSelection, null); + + String message = "Copied " + taskService.getCurrentTaskType().getName() + " data to clipboard!"; + showMessageBox("Data Exported!", message, JOptionPane.INFORMATION_MESSAGE, true); + }); + } + + private void forceVarpUpdate() + { + log.debug("forceVarpUpdate"); + processVarpAndUpdateTasks(null).thenAccept((processed) -> { + if (processed) + { + log.debug("forceVarpUpdate processed complete, saving"); + saveCurrentTaskTypeData(); + } + }); + } + + private void flushVarpUpdates(Set varpIds) + { + log.debug("Flushing throttled varp updates {}", varpIds); + varpIds.forEach((id) -> processVarpAndUpdateTasks(id).thenAccept(processed -> { + if (processed) + { + log.debug("flushVarpUpdates processed complete, saving"); + saveCurrentTaskTypeData(); + } + })); + } + + private CompletableFuture processTaskStatus(TaskFromStruct task) + { + CompletableFuture future = new CompletableFuture<>(); + clientThread.invoke(() -> { + int taskId = task.getIntParam("id"); + int varbitIndex = taskId / 32; + int bitIndex = taskId % 32; + try + { + int varpId = task.getTaskType().getTaskVarps().get(varbitIndex); + BigInteger varpValue = BigInteger.valueOf(client.getVarpValue(varpId)); + boolean isTaskCompleted = varpValue.testBit(bitIndex); + task.setCompleted(isTaskCompleted); + if (isTaskCompleted && config.untrackUponCompletion()) + { + task.setTracked(false); + } + log.debug("process taskFromStruct {} ({}) {}", task.getStringParam("name"), task.getIntParam("id"), isTaskCompleted); + SwingUtilities.invokeLater(() -> pluginPanel.refresh(task)); + future.complete(isTaskCompleted); + } + catch (Exception ex) + { + log.error("Error processing task status {}", taskId, ex); + future.completeExceptionally(ex); + } + }); + return future; + } + + /** + * Update task completion status. If no varpId is specified, it updates all tasks in the current task type + * @param varpId varp id to update (optional) + * @return An observable that emits true if all tasks were processed + */ + private CompletableFuture processVarpAndUpdateTasks(@Nullable Integer varpId) + { + log.info("processVarpAndUpdateTasks: " + (varpId != null ? varpId : "all")); + + List tasks = varpId != null ? + taskService.getTasksFromVarpId(varpId) : + taskService.getTasks(); + + List> taskFutures = new ArrayList<>(); + for (TaskFromStruct task : tasks) + { + CompletableFuture taskFuture = processTaskStatus(task); + taskFutures.add(taskFuture); + } + + CompletableFuture allTasksFuture = CompletableFuture.allOf(taskFutures.toArray(new CompletableFuture[0])); + return allTasksFuture.thenApply(v -> true); + } + + private String getCurrentTaskTypeExportJson() + { + TaskType taskType = taskService.getCurrentTaskType(); + Gson gson = this.gson.newBuilder() + .excludeFieldsWithoutExposeAnnotation() + .registerTypeAdapter(float.class, new LongSerializer()) + .create(); + + if (taskType == null) + { + String error = "Cannot export to JSON; no task type selected."; + log.error(error); + return error; + } + else + { + Export export = new Export(taskType, taskService.getTasks(), runeliteVersion, client); + return gson.toJson(export); + } + } + + private void showMessageBox(final String title, final String message, int messageType, boolean showOpenLeagueTools) + { + SwingUtilities.invokeLater(() -> { + JOptionPane optionPane; + JDialog dialog; + + if (showOpenLeagueTools) + { + String[] options = {"Open OS League Tools", "Ok"}; + + optionPane = new JOptionPane(message, messageType, JOptionPane.YES_NO_OPTION, null, options, options[1]); + } + else + { + optionPane = new JOptionPane(message, messageType); + } + + dialog = optionPane.createDialog(pluginPanel, title); + dialog.setAlwaysOnTop(true); + dialog.setVisible(true); + + Object selectedValue = optionPane.getValue(); + if (selectedValue == null) + { + return; + } + + if (selectedValue.equals("Open OS League Tools")) + { + LinkBrowser.browse("https://www.osleague.tools/tracker?open=import&tab=tasks"); + } + }); + } +} diff --git a/src/main/java/net/reldo/taskstracker/config/ConfigValues.java b/src/main/java/net/reldo/taskstracker/config/ConfigValues.java index 21c7ac2..8135c99 100644 --- a/src/main/java/net/reldo/taskstracker/config/ConfigValues.java +++ b/src/main/java/net/reldo/taskstracker/config/ConfigValues.java @@ -5,30 +5,57 @@ public class ConfigValues { public enum CompletedFilterValues { - COMPLETE_AND_INCOMPLETE, - COMPLETE, - INCOMPLETE; + COMPLETE_AND_INCOMPLETE("Both"), + COMPLETE("Complete"), + INCOMPLETE("Incomplete"); + + public final String name; + + public String toString() { return name; } + + CompletedFilterValues(String name) + { + this.name = name; + } } public enum TrackedFilterValues { - TRACKED_AND_UNTRACKED, - TRACKED, - UNTRACKED; + TRACKED_AND_UNTRACKED("Both"), + TRACKED("Tracked"), + UNTRACKED("Untracked"); + + public final String name; + + public String toString() { return name; } + + TrackedFilterValues(String name) + { + this.name = name; + } } public enum IgnoredFilterValues { - NOT_IGNORED, - IGNORED_AND_NOT_IGNORED, - IGNORED; + NOT_IGNORED("Not Ignored"), + IGNORED_AND_NOT_IGNORED("Both"), + IGNORED("Ignored"); + + public final String name; + + public String toString() { return name; } + + IgnoredFilterValues(String name) + { + this.name = name; + } } public enum TaskListTabs { - TRACKED, - ALL, - CUSTOM; + TAB_ONE, + TAB_TWO, + TAB_THREE; } public enum SortDirections diff --git a/src/main/java/net/reldo/taskstracker/panel/Icons.java b/src/main/java/net/reldo/taskstracker/panel/Icons.java index c8e90a0..707d7f7 100644 --- a/src/main/java/net/reldo/taskstracker/panel/Icons.java +++ b/src/main/java/net/reldo/taskstracker/panel/Icons.java @@ -15,9 +15,9 @@ public class Icons private static final String ignoredBtnPath = "panel/components/ignored_button/"; public static final BufferedImage semivisibleimg = ImageUtil.loadImageResource(TasksTrackerPlugin.class, ignoredBtnPath + "semivisible_icon.png"); - public static final Icon SEMIVISIBLE_ICON = new ImageIcon(ImageUtil.alphaOffset(semivisibleimg, -180)); - public static final Icon INVISIBLE_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, ignoredBtnPath + "invisible_icon.png")); - public static final Icon VISIBLE_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, ignoredBtnPath + "visible_icon.png")); + public static final Icon UNIGNORED_ONLY_ICON = new ImageIcon(ImageUtil.alphaOffset(semivisibleimg, -180)); + public static final Icon IGNORED_ONLY_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, ignoredBtnPath + "invisible_icon.png")); + public static final Icon IGNORED_UNIGNORED_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, ignoredBtnPath + "visible_icon.png")); private static final String trackedBtnPath = "panel/components/tracked_button/"; public static final Icon UNTRACKED_ONLY_ICON = new ImageIcon(ImageUtil.loadImageResource(TasksTrackerPlugin.class, trackedBtnPath + "untracked_icon.png")); diff --git a/src/main/java/net/reldo/taskstracker/panel/LoggedInPanel.java b/src/main/java/net/reldo/taskstracker/panel/LoggedInPanel.java index 0019d81..877ec61 100644 --- a/src/main/java/net/reldo/taskstracker/panel/LoggedInPanel.java +++ b/src/main/java/net/reldo/taskstracker/panel/LoggedInPanel.java @@ -5,8 +5,6 @@ import java.awt.Dimension; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; import javax.swing.Box; import javax.swing.BoxLayout; @@ -63,6 +61,9 @@ public class LoggedInPanel extends JPanel private SubFilterPanel subFilterPanel; private SortPanel sortPanel; private final JToggleButton collapseBtn = new JToggleButton(); + private JToggleButton tabOne; + private JToggleButton tabTwo; + private JToggleButton tabThree; public LoggedInPanel(TasksTrackerPlugin plugin, TasksTrackerConfig config, TaskService taskService) { @@ -80,7 +81,7 @@ public Dimension getPreferredSize() return getParent().getSize(); } - public void redraw() + public void drawNewTaskType() { // taskTypeDropdown may become de-synced after profile change String selectedTaskTypeJsonName = taskTypeDropdown.getItemAt(taskTypeDropdown.getSelectedIndex()).getValue().getTaskJsonName(); @@ -98,6 +99,17 @@ public void redraw() } } } + + subFilterPanel.redraw(); + sortPanel.redraw(); + updateCollapseButtonText(); + + taskListPanel.drawNewTaskType(); + refreshFilterButtonsFromConfig(config.taskListTab()); + } + + public void redraw() + { subFilterPanel.redraw(); sortPanel.redraw(); updateCollapseButtonText(); @@ -110,8 +122,8 @@ public void refresh(TaskFromStruct task) if(task == null) { updateCollapseButtonText(); + refreshTabNames(); } - taskListPanel.refresh(task); } @@ -125,14 +137,6 @@ private void createPanel() add(getNorthPanel(), BorderLayout.NORTH); add(getCenterPanel(), BorderLayout.CENTER); add(getSouthPanel(), BorderLayout.SOUTH); - - loadAndApplyFilters(config.taskListTab()); - if(config.taskListTab().equals(ConfigValues.TaskListTabs.TRACKED)) - { - trackedFilterBtn.setState(1); - trackedFilterBtn.setEnabled(false); - plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "trackedFilter", ConfigValues.TrackedFilterValues.TRACKED); - } } private JPanel getCenterPanel() { @@ -146,84 +150,103 @@ private JPanel getCenterPanel() { tabPane.setBorder(new EmptyBorder(0,0,0,0)); tabPane.setPreferredSize(new Dimension(PluginPanel.PANEL_WIDTH,24)); - JToggleButton trackedTab = tabButton("Tracked Tasks", ConfigValues.TaskListTabs.TRACKED); - JToggleButton allTab = tabButton("All Tasks", ConfigValues.TaskListTabs.ALL); - JToggleButton customTab = tabButton("Custom", ConfigValues.TaskListTabs.CUSTOM); + tabOne = tabButton(config.tab1Name(), ConfigValues.TaskListTabs.TAB_ONE); + tabTwo = tabButton(config.tab2Name(), ConfigValues.TaskListTabs.TAB_TWO); + tabThree = tabButton(config.tab3Name(), ConfigValues.TaskListTabs.TAB_THREE); ButtonGroup tabGroup = new ButtonGroup(); - tabGroup.add(trackedTab); - tabGroup.add(allTab); - tabGroup.add(customTab); + tabGroup.add(tabOne); + tabGroup.add(tabTwo); + tabGroup.add(tabThree); tabPane.add(Box.createHorizontalGlue()); - tabPane.add(trackedTab); + tabPane.add(tabOne); tabPane.add(Box.createHorizontalGlue()); - tabPane.add(allTab); + tabPane.add(tabTwo); tabPane.add(Box.createHorizontalGlue()); - tabPane.add(customTab); + tabPane.add(tabThree); tabPane.add(Box.createHorizontalGlue()); taskListPanel.add(tabPane, BorderLayout.NORTH); taskListPanel.add(this.taskListPanel, BorderLayout.CENTER); - // set initial filter states to "complete and incomplete", "tracked and untracked", "not ignored" - Map filterStates = new HashMap<>(); - filterStates.put("completed",0); - filterStates.put("tracked",0); - filterStates.put("ignored",0); - for(ConfigValues.TaskListTabs tab : ConfigValues.TaskListTabs.values()) + return taskListPanel; + } + + public void tabChanged(ConfigValues.TaskListTabs newTab) + { + if(newTab != null) + { + saveCurrentTabFilters(); + plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "taskListTab", newTab); + refreshFilterButtonsFromConfig(newTab); + plugin.refresh(); + } + } + + private void saveCurrentTabFilters() + { + String tab = "tab" + (config.taskListTab().ordinal() + 1); // tabs are 1-indexed + + Enum configValue; + + if (plugin.getConfigManager().getConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, tab + "CompletedLock").equals("false")) { - filterStore.put(tab, filterStates); + configValue = ConfigValues.CompletedFilterValues.values()[completedFilterBtn.getState()]; + plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, tab + "CompletedValue", configValue); } - switch (config.taskListTab()) + if (plugin.getConfigManager().getConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, tab + "TrackedLock").equals("false")) { - case TRACKED: - trackedTab.setSelected(true); - break; - case ALL: - allTab.setSelected(true); - break; - case CUSTOM: - customTab.setSelected(true); - break; + configValue = ConfigValues.TrackedFilterValues.values()[trackedFilterBtn.getState()]; + plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, tab + "TrackedValue", configValue); } - tabChanged(config.taskListTab()); - return taskListPanel; + if (plugin.getConfigManager().getConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, tab + "IgnoredLock").equals("false")) + { + configValue = ConfigValues.IgnoredFilterValues.values()[ignoredFilterBtn.getState()]; + plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, tab + "IgnoredValue", configValue); + } } - public void tabChanged(ConfigValues.TaskListTabs newTab) + // TODO reduce duplication + public void refreshFilterButtonsFromConfig(ConfigValues.TaskListTabs tab) { - if(newTab != null) { - changeTab(newTab); - - switch (newTab) { - case TRACKED: - trackedFilterBtn.setState(1); - trackedFilterBtn.setEnabled(false); - plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "taskListTab", newTab); - filterButtonAction("tracked"); - break; - case ALL: - trackedFilterBtn.setState(0); - trackedFilterBtn.setEnabled(false); - completedFilterBtn.setState(0); - completedFilterBtn.setEnabled(false); - ignoredFilterBtn.setState(1); - ignoredFilterBtn.setEnabled(false); - plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "taskListTab", newTab); - actionAllFilterButtons(); - break; - case CUSTOM: - plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "taskListTab", newTab); - plugin.refresh(); - break; - default: - plugin.refresh(); - break; - } + switch (tab) + { + case TAB_ONE: + if(!tabOne.isSelected()) tabOne.setSelected(true); + trackedFilterBtn.setState(config.tab1TrackedValue().ordinal()); + trackedFilterBtn.setEnabled(!config.tab1TrackedLock()); + completedFilterBtn.setState(config.tab1CompletedValue().ordinal()); + completedFilterBtn.setEnabled(!config.tab1CompletedLock()); + ignoredFilterBtn.setState(config.tab1IgnoredValue().ordinal()); + ignoredFilterBtn.setEnabled(!config.tab1IgnoredLock()); + actionAllFilterButtonsNoRefresh(); + break; + case TAB_TWO: + if(!tabTwo.isSelected()) tabTwo.setSelected(true); + trackedFilterBtn.setState(config.tab2TrackedValue().ordinal()); + trackedFilterBtn.setEnabled(!config.tab2TrackedLock()); + completedFilterBtn.setState(config.tab2CompletedValue().ordinal()); + completedFilterBtn.setEnabled(!config.tab2CompletedLock()); + ignoredFilterBtn.setState(config.tab2IgnoredValue().ordinal()); + ignoredFilterBtn.setEnabled(!config.tab2IgnoredLock()); + actionAllFilterButtonsNoRefresh(); + break; + case TAB_THREE: + if(!tabThree.isSelected()) tabThree.setSelected(true); + trackedFilterBtn.setState(config.tab3TrackedValue().ordinal()); + trackedFilterBtn.setEnabled(!config.tab3TrackedLock()); + completedFilterBtn.setState(config.tab3CompletedValue().ordinal()); + completedFilterBtn.setEnabled(!config.tab3CompletedLock()); + ignoredFilterBtn.setState(config.tab3IgnoredValue().ordinal()); + ignoredFilterBtn.setEnabled(!config.tab3IgnoredLock()); + actionAllFilterButtonsNoRefresh(); + break; + default: + break; } } @@ -239,54 +262,24 @@ private JToggleButton tabButton(String label, ConfigValues.TaskListTabs tab) return button; } - private void changeTab(ConfigValues.TaskListTabs newTab) - { - saveFilters(); - resetFilters(); - loadAndApplyFilters(newTab); - } - - private final Map> filterStore = new HashMap<>(); - - private void saveFilters() - { - ConfigValues.TaskListTabs tab = config.taskListTab(); - - Map filterStates = new HashMap<>(); - filterStates.put("completed", config.completedFilter().ordinal()); - filterStates.put("tracked", config.trackedFilter().ordinal()); - filterStates.put("ignored", config.ignoredFilter().ordinal()); - - filterStore.put(tab, filterStates); - } - - private void resetFilters() - { - completedFilterBtn.setEnabled(true); - trackedFilterBtn.setEnabled(true); - ignoredFilterBtn.setEnabled(true); - } - - private void loadAndApplyFilters(ConfigValues.TaskListTabs tab) + private void refreshTabNames() { - Map filterStates = filterStore.get(tab); - - if(filterStates == null) return; - - Enum configValue; - - completedFilterBtn.setState(filterStates.get("completed")); - trackedFilterBtn.setState(filterStates.get("tracked")); - ignoredFilterBtn.setState(filterStates.get("ignored")); - - configValue = ConfigValues.CompletedFilterValues.values()[completedFilterBtn.getState()]; - plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "completedFilter", configValue); - - configValue = ConfigValues.TrackedFilterValues.values()[trackedFilterBtn.getState()]; - plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "trackedFilter", configValue); - - configValue = ConfigValues.IgnoredFilterValues.values()[ignoredFilterBtn.getState()]; - plugin.getConfigManager().setConfiguration(TasksTrackerPlugin.CONFIG_GROUP_NAME, "ignoredFilter", configValue); + String tabName; + if (!tabOne.getText().equals(config.tab1Name())) + { + tabName = config.tab1Name().isBlank() ? "Tab 1" : config.tab1Name(); + tabOne.setText(tabName); + } + if (!tabTwo.getText().equals(config.tab2Name())) + { + tabName = config.tab2Name().isBlank() ? "Tab 2" : config.tab2Name(); + tabTwo.setText(tabName); + } + if (!tabThree.getText().equals(config.tab3Name())) + { + tabName = config.tab3Name().isBlank() ? "Tab 3" : config.tab3Name(); + tabThree.setText(tabName); + } } private JPanel getSouthPanel() @@ -339,7 +332,6 @@ private JPanel getNorthPanel() if (wasTaskTypeChanged) { SwingUtilities.invokeLater(() -> { - redraw(); refresh(null); }); } @@ -438,7 +430,7 @@ private JPanel getTitleAndButtonPanel() // Ignored tasks filter button SwingUtil.removeButtonDecorations(ignoredFilterBtn); - ignoredFilterBtn.setIcons(Icons.SEMIVISIBLE_ICON, Icons.VISIBLE_ICON, Icons.INVISIBLE_ICON); + ignoredFilterBtn.setIcons(Icons.UNIGNORED_ONLY_ICON, Icons.IGNORED_UNIGNORED_ICON, Icons.IGNORED_ONLY_ICON); ignoredFilterBtn.setToolTips("Hide ignored tasks", "All tasks", "Ignored tasks only"); ignoredFilterBtn.setBackground(ColorScheme.DARK_GRAY_COLOR); ignoredFilterBtn.setStateChangedAction(e -> filterButtonAction("ignored")); @@ -487,11 +479,16 @@ private void filterButtonAction(String filter) plugin.refresh(); } - private void actionAllFilterButtons() + private void actionAllFilterButtonsNoRefresh() { filterButtonActionNoRefresh("tracked"); filterButtonActionNoRefresh("ignored"); filterButtonActionNoRefresh("completed"); + } + + private void actionAllFilterButtons() + { + actionAllFilterButtonsNoRefresh(); plugin.refresh(); } diff --git a/src/main/java/net/reldo/taskstracker/panel/SortPanel.java b/src/main/java/net/reldo/taskstracker/panel/SortPanel.java index 76db7c4..30cc1bf 100644 --- a/src/main/java/net/reldo/taskstracker/panel/SortPanel.java +++ b/src/main/java/net/reldo/taskstracker/panel/SortPanel.java @@ -56,10 +56,7 @@ public void redraw() sortDropdown.setSelectedIndex(0); sortDropdown.addActionListener(e -> { updateConfig(); - SwingUtilities.invokeLater(() -> { - taskListPanel.redraw(); - taskListPanel.refresh(null); - }); + SwingUtilities.invokeLater(taskListPanel::redraw); }); sortDropdown.setFocusable(false); @@ -70,10 +67,7 @@ public void redraw() directionButton.setBackground(ColorScheme.DARK_GRAY_COLOR); directionButton.setStateChangedAction(e -> { updateConfig(); - SwingUtilities.invokeLater(() -> { - taskListPanel.redraw(); - taskListPanel.refresh(null); - }); + SwingUtilities.invokeLater(taskListPanel::redraw); }); add(sortDropdown); diff --git a/src/main/java/net/reldo/taskstracker/panel/TaskListPanel.java b/src/main/java/net/reldo/taskstracker/panel/TaskListPanel.java index 10ebb54..aee7bbe 100644 --- a/src/main/java/net/reldo/taskstracker/panel/TaskListPanel.java +++ b/src/main/java/net/reldo/taskstracker/panel/TaskListPanel.java @@ -4,12 +4,14 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.IntConsumer; import javax.swing.BoxLayout; import javax.swing.JLabel; import javax.swing.JScrollPane; import javax.swing.ScrollPaneConstants; import javax.swing.SwingUtilities; import javax.swing.border.EmptyBorder; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import net.reldo.taskstracker.TasksTrackerPlugin; import net.reldo.taskstracker.config.ConfigValues; @@ -24,30 +26,78 @@ public class TaskListPanel extends JScrollPane { public TasksTrackerPlugin plugin; - public final ArrayList taskPanels = new ArrayList<>(); - private final TaskListListPanel taskList; + private final int TASK_LIST_BUFFER_COUNT = 2; + public ArrayList taskPanels = new ArrayList<>(); + private final ArrayList taskListBuffers = new ArrayList<>(TASK_LIST_BUFFER_COUNT); + private int currentTaskListBufferIndex; private final TaskService taskService; private final JLabel emptyTasks = new JLabel(); + @Setter + private int batchSize; public TaskListPanel(TasksTrackerPlugin plugin, TaskService taskService) { this.plugin = plugin; - - taskList = new TaskListListPanel(plugin); this.taskService = taskService; + batchSize = plugin.getConfig().taskPanelBatchSize(); + + FixedWidthPanel taskListListPanelWrapper = new FixedWidthPanel(); + + for(int i = 0; i < TASK_LIST_BUFFER_COUNT; i++) + { + taskListBuffers.add(new TaskListListPanel(plugin)); + taskListListPanelWrapper.add(taskListBuffers.get(i)); + } - setViewportView(taskList); + setViewportView(taskListListPanelWrapper); setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); + + setCurrentTaskListListPanel(0); } - public String getEmptyTaskListMessage() + private void setCurrentTaskListListPanel(int index) { - return "No tasks match the current filters."; + if (index != currentTaskListBufferIndex) + { + taskListBuffers.get(currentTaskListBufferIndex).setVisible(false); + taskListBuffers.get(index).setVisible(true); + currentTaskListBufferIndex = index; + } + } + + private TaskListListPanel getCurrentTaskListListPanel() + { + return taskListBuffers.get(currentTaskListBufferIndex); + } + + private TaskListListPanel getNextTaskListListPanel() + { + return taskListBuffers.get(getNextBufferIndex()); + } + + private void showNextTaskListListPanel() + { + log.debug("Showing next task list list panel: {}", getNextBufferIndex()); + TaskListListPanel previousPanel = getCurrentTaskListListPanel(); + setCurrentTaskListListPanel(getNextBufferIndex()); + previousPanel.prepEmptyTaskListPanel(); + } + + private int getNextBufferIndex() + { + return (currentTaskListBufferIndex + 1) % TASK_LIST_BUFFER_COUNT; + } + + public void drawNewTaskType() + { + log.debug("Drawing new Task Type taskListListPanel"); + getNextTaskListListPanel().drawNewTaskType(); } public void redraw() { - taskList.redraw(); + log.debug("Redrawing taskListListPanel"); + getCurrentTaskListListPanel().redraw(); } public void refresh(TaskFromStruct task) @@ -71,14 +121,7 @@ public void refresh(TaskFromStruct task) } } - Optional visibleTaskPanel = taskPanels.stream() - .filter(TaskPanel::isVisible) - .findFirst(); - - if (visibleTaskPanel.isEmpty()) - { - emptyTasks.setVisible(true); - } + refreshEmptyPanel(); } else { @@ -86,30 +129,47 @@ public void refresh(TaskFromStruct task) } } + private void refreshEmptyPanel() + { + Optional visibleTaskPanel = taskPanels.stream() + .filter(TaskPanel::isVisible) + .findFirst(); + + if (visibleTaskPanel.isEmpty()) + { + emptyTasks.setVisible(true); + } + } + public void refreshTaskPanelsWithSkill(Skill skill) { // Refresh all task panels for tasks with 'skill' or // 'SKILLS' (any skill) or 'TOTAL LEVEL' as a requirement. taskPanels.stream() - .filter(tp -> + .filter(tp -> + { + List skillsList = tp.task.getTaskDefinition().getSkills(); + if(skillsList == null || skillsList.isEmpty()) { - List skillsList = tp.task.getTaskDefinition().getSkills(); - if(skillsList == null || skillsList.isEmpty()) - { - return false; - } - - return skillsList.stream() - .map(TaskDefinitionSkill::getSkill) - .anyMatch(s -> s.equalsIgnoreCase(skill.getName()) || - s.equalsIgnoreCase("SKILLS") || - s.equalsIgnoreCase("TOTAL LEVEL") - ); - }) - .forEach(TaskPanel::refresh); + return false; + } + + return skillsList.stream() + .map(TaskDefinitionSkill::getSkill) + .anyMatch(s -> s.equalsIgnoreCase(skill.getName()) || + s.equalsIgnoreCase("SKILLS") || + s.equalsIgnoreCase("TOTAL LEVEL") + ); + }) + .forEach(TaskPanel::refresh); } - private class TaskListListPanel extends FixedWidthPanel + public String getEmptyTaskListMessage() + { + return "No tasks match the current filters."; + } + + private class TaskListListPanel extends FixedWidthPanel { private final TasksTrackerPlugin plugin; @@ -130,17 +190,20 @@ public TaskListListPanel(TasksTrackerPlugin plugin) emptyTasks.setVisible(false); } - public void redraw() + public void prepEmptyTaskListPanel() { - log.debug("TaskListPanel.redraw"); + SwingUtilities.invokeLater(this::removeAll); + } + + public void drawNewTaskType() + { + log.debug("TaskListPanel.drawNewTaskType"); if(SwingUtilities.isEventDispatchThread()) { - removeAll(); - taskPanels.clear(); + log.debug("TaskListPanel creating panels"); + add(emptyTasks); - emptyTasks.setVisible(false); - log.debug("TaskListPanel creating panels"); List tasks = taskService.getTasks(); if (tasks == null || tasks.isEmpty()) { @@ -148,24 +211,90 @@ public void redraw() return; } - for (int indexPosition = 0; indexPosition < tasks.size(); indexPosition++) + emptyTasks.setVisible(false); + + // Buffer to hold newly created task panels before they are swapped in + ArrayList newTaskPanels = new ArrayList<>(tasks.size()); + + processInBatches(tasks.size(), indexPosition -> + { + TaskPanel taskPanel = new TaskPanel(plugin, tasks.get(indexPosition)); + add(taskPanel); + newTaskPanels.add(taskPanel); + if (indexPosition == (batchSize - 1)) taskPanels = newTaskPanels; // replace taskPanels list at end of first batch + }); + } + else + { + log.error("Task list panel drawNewTaskType failed - not event dispatch thread."); + } + } + + public void redraw() + { + log.debug("TaskListPanel.redraw"); + if(SwingUtilities.isEventDispatchThread()) + { + if (taskPanels == null || taskPanels.isEmpty()) + { + emptyTasks.setVisible(true); + return; + } + + for (int indexPosition = 0; indexPosition < taskPanels.size(); indexPosition++) { int adjustedIndexPosition = indexPosition; if (plugin.getConfig().sortDirection().equals(ConfigValues.SortDirections.DESCENDING)) - adjustedIndexPosition = tasks.size() - (adjustedIndexPosition + 1); - TaskPanel taskPanel = new TaskPanel(plugin, tasks.get(taskService.getSortedTaskIndex(plugin.getConfig().sortCriteria(), adjustedIndexPosition))); - add(taskPanel); - taskPanels.add(taskPanel); + adjustedIndexPosition = taskPanels.size() - (adjustedIndexPosition + 1); + TaskPanel taskPanel = taskPanels.get(taskService.getSortedTaskIndex(plugin.getConfig().sortCriteria(), adjustedIndexPosition)); + setComponentZOrder(taskPanel, indexPosition); } - log.debug("TaskListPanel validate and repaint"); - validate(); - repaint(); + SwingUtilities.invokeLater(() -> refresh(null)); } else { log.error("Task list panel redraw failed - not event dispatch thread."); } } + + private void processInBatches(int objectCount, IntConsumer method) + { + processBatch(0, objectCount, method); + showNextTaskListListPanel(); + } + + private void processBatch(int batch, int objectCount, IntConsumer method) + { + log.debug("TaskListPanel.processBatch {}", batch); + + for (int index = 0; index < batchSize; index++) + { + int indexPosition = index + (batch * batchSize); + if (indexPosition < objectCount) + { + method.accept(indexPosition); + } + else + { + break; + } + } + + refreshEmptyPanel(); + validate(); + repaint(); + + // queue next batch if not done or refresh after last batch + int batchIndex = batch + 1; + if (batchIndex * batchSize < objectCount) + { + SwingUtilities.invokeLater(() -> processBatch(batchIndex, objectCount, method)); + } + else + { + SwingUtilities.invokeLater(() -> refresh(null)); + } + } } } diff --git a/src/main/java/net/reldo/taskstracker/panel/TasksTrackerPluginPanel.java b/src/main/java/net/reldo/taskstracker/panel/TasksTrackerPluginPanel.java index a1429b2..a52eaa6 100644 --- a/src/main/java/net/reldo/taskstracker/panel/TasksTrackerPluginPanel.java +++ b/src/main/java/net/reldo/taskstracker/panel/TasksTrackerPluginPanel.java @@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j; import net.reldo.taskstracker.TasksTrackerConfig; import net.reldo.taskstracker.TasksTrackerPlugin; +import net.reldo.taskstracker.config.ConfigValues; import net.reldo.taskstracker.data.task.TaskFromStruct; import net.reldo.taskstracker.data.task.TaskService; import net.runelite.client.game.SpriteManager; @@ -64,6 +65,14 @@ public void refresh(TaskFromStruct task) } } + public void refreshFilterButtonsFromConfig(ConfigValues.TaskListTabs tab) + { + if (loggedIn) + { + loggedInPanel.refreshFilterButtonsFromConfig(tab); + } + } + public void setLoggedIn(boolean loggedIn) { if(SwingUtilities.isEventDispatchThread()) @@ -91,4 +100,13 @@ public void setLoggedIn(boolean loggedIn) log.error("Failed to update loggedIn state - not event dispatch thread."); } } + + public void drawNewTaskType() + { + if (loggedIn) + { + loggedInPanel.drawNewTaskType(); + } + + } } \ No newline at end of file diff --git a/src/main/java/net/reldo/taskstracker/panel/components/MultiToggleButton.java b/src/main/java/net/reldo/taskstracker/panel/components/MultiToggleButton.java index 6c4fc47..0ce07f9 100644 --- a/src/main/java/net/reldo/taskstracker/panel/components/MultiToggleButton.java +++ b/src/main/java/net/reldo/taskstracker/panel/components/MultiToggleButton.java @@ -46,7 +46,7 @@ public void popupMenuEnabled(boolean enabled) } else { - this.remove(popupMenu); + this.setComponentPopupMenu(null); } } } @@ -142,7 +142,13 @@ public void setStateThenAction(int state) private void addPopupMenuItem(String text, int state) { JMenuItem menuItem = new JMenuItem(text); - menuItem.addActionListener(e -> {if(isEnabled())setState(state);}); + menuItem.addActionListener(e -> {if(isEnabled())setStateThenAction(state);}); popupMenu.add(menuItem); } + + public void setEnabled(boolean enabled) + { + super.setEnabled(enabled); + popupMenuEnabled(enabled); + } }