diff --git a/.github/scripts/build_release_notes.sh b/.github/scripts/build_release_notes.sh deleted file mode 100644 index f24ef5a..0000000 --- a/.github/scripts/build_release_notes.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail -# build_release_notes.sh -# Generates release notes combining annotated tag message and CHANGELOG.md section. -# Usage: build_release_notes.sh -# Outputs markdown to STDOUT. - -TAG=${1:?tag required} -REPO_OWNER=${2:?owner required} -REPO_NAME=${3:?name required} -TAG_COMMIT=${4:?commit sha required} -GITHUB_SERVER_URL=${5:-https://github.com} - -# Normalize versions (strip leading v) -# VERSION_FULL keeps suffixes like -rc1, VERSION_CORE strips them -VERSION_FULL=$(echo "$TAG" | sed -E 's/^v//') -VERSION_CORE=$(echo "$VERSION_FULL" | sed -E 's/^([0-9]+(\.[0-9]+)*).*/\1/') - -# Get tag message. Try annotated tag contents first, then fall back to the tagged commit message -# but only if the tag actually exists. If the tag name doesn't resolve to a tag ref, do nothing. -TAG_MESSAGE="" -# Check whether the tag ref exists -if git rev-parse --verify --quiet "refs/tags/${TAG}" >/dev/null 2>&1; then - # Try annotated tag contents (works for annotated tags) - TAG_MESSAGE=$(git for-each-ref --format='%(contents)' "refs/tags/${TAG}" 2>/dev/null || true) - - # If empty, try to read tag object (cat-file) - may include annotation - if [ -z "${TAG_MESSAGE}" ]; then - TAG_MESSAGE=$(git cat-file -p "refs/tags/${TAG}" 2>/dev/null | sed -n '1,/^$/p' || true) - fi - - # If still empty, fall back to commit message of the tagged commit (works for lightweight tags) - if [ -z "${TAG_MESSAGE}" ]; then - if git rev-parse "${TAG}^{commit}" >/dev/null 2>&1; then - TAG_MESSAGE=$(git show -s --format='%B' "${TAG}^{commit}" 2>/dev/null || true) - else - TAG_MESSAGE=$(git show -s --format='%B' "${TAG}" 2>/dev/null || true) - fi - fi -else - # Tag reference does not exist; leave tag message empty - TAG_MESSAGE="" -fi - -# Remove potential trailing whitespace in tag message -TAG_MESSAGE=$(printf '%s' "${TAG_MESSAGE}" | sed -E ':a; /\n$/ {N; ba}; s/[[:space:]]+$//') -# Remove potential trailing whitespace in tag message -TAG_MESSAGE=$(printf '%s' "${TAG_MESSAGE}" | sed -E ':a; /\n$/ {N; ba}; s/[[:space:]]+$//') - -# Extract the section from CHANGELOG.md belonging to this version -CHANGELOG_CONTENT="" -if git show "${TAG_COMMIT}:CHANGELOG.md" > /tmp/CHANGELOG_ALL 2>/dev/null; then - # Grab section starting after heading line matching version until next heading - # Try full version first (e.g., 1.0.0-rc1), then fall back to core (e.g., 1.0.0) - EXTRACTED="" - for try_ver in "${VERSION_FULL}" "${VERSION_CORE}"; do - EXTRACTED=$(awk -v ver="${try_ver}" ' - BEGIN{found=0} - $0 ~ "^##[[:space:]]*\\[" ver "\\]" {found=1; next} - found && $0 ~ "^##[[:space:]]*\\[" {exit} - found {print} - ' /tmp/CHANGELOG_ALL) - if [ -n "${EXTRACTED}" ]; then - break - fi - done - if [ -n "${EXTRACTED}" ]; then - CHANGELOG_CONTENT="${EXTRACTED}" - fi -fi - -# Filter to sections Added / Changed / Fixed / Security, keep their content blocks until next heading of same depth (### ) -if [ -n "${CHANGELOG_CONTENT}" ]; then - FILTERED=$(awk ' - BEGIN{cur=""; keep=0} - /^###[[:space:]]+Added/ {cur="Added"; keep=1; print; next} - /^###[[:space:]]+Changed/ {cur="Changed"; keep=1; print; next} - /^###[[:space:]]+Fixed/ {cur="Fixed"; keep=1; print; next} - /^###[[:space:]]+Security/ {cur="Security"; keep=1; print; next} - /^###/ {cur=""; keep=0} - {if(keep){print}} - ' <<<"${CHANGELOG_CONTENT}") - # If filtering yielded something non-empty use it, else keep original - if [ -n "${FILTERED}" ]; then - CHANGELOG_CONTENT="${FILTERED}" - fi -fi - -# Trim leading/trailing blank lines -trim_blank() { sed -E '/^[[:space:]]*$/{$d;}; 1{/^[[:space:]]*$/d;}' ; } -CHANGELOG_CONTENT=$(printf '%s\n' "${CHANGELOG_CONTENT}" | sed -E ':a;/^$/{$d;N;ba};' | sed -E '1{/^$/d;}') || true - -# Build compare link (try to determine a previous tag) -COMPARE_LINK="" - -# Try several ways to find a previous tag: by creation date, then by semantic/version sort, -# then by using git describe on the parent commit. This improves chances of producing a -# meaningful compare URL on repositories with different tag styles. -PREV_TAG="" -PREV_TAG=$(git tag --sort=-creatordate | grep -Fvx "${TAG}" | head -n1 || true) -if [ -z "${PREV_TAG}" ]; then - PREV_TAG=$(git tag --sort=-v:refname | grep -Fvx "${TAG}" | head -n1 || true) -fi -if [ -z "${PREV_TAG}" ]; then - PREV_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true) -fi -if [ -n "${PREV_TAG}" ]; then - # Only build a compare URL if the requested TAG actually resolves to something the - # git server can compare to: either a tag ref or a commit-ish. - if git rev-parse --verify --quiet "refs/tags/${TAG}" >/dev/null 2>&1 || git rev-parse --verify --quiet "${TAG}" >/dev/null 2>&1; then - COMPARE_URL="${GITHUB_SERVER_URL}/${REPO_OWNER}/${REPO_NAME}/compare/${PREV_TAG}...${TAG}" - COMPARE_LINK="Full changelog: ${COMPARE_URL}" - fi -fi - -# Assemble final notes without embedding literal \n sequences -segments=() -if [ -n "${TAG_MESSAGE}" ]; then - segments+=("${TAG_MESSAGE}") -fi -if [ -n "${CHANGELOG_CONTENT}" ]; then - segments+=("${CHANGELOG_CONTENT}") -fi -if [ -n "${COMPARE_LINK}" ]; then - segments+=("${COMPARE_LINK}") -fi - -# Join segments with a blank line -if [ ${#segments[@]} -gt 0 ]; then - # Print each segment separated by a blank line - { - for i in "${!segments[@]}"; do - printf '%s' "${segments[$i]}" - if [ "$i" -lt $((${#segments[@]} - 1)) ]; then - printf '\n\n' - else - printf '\n' - fi - done - } | sed -E ':a;/^$/{$d;N;ba};' # trim trailing blank lines -fi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 402dbdc..fe2e5e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,7 +2,14 @@ name: build on: push: + branches: + - '**' + tags: + - '**' pull_request: + branches: + - '**' + workflow_dispatch: # Allow manual triggering from GitHub UI permissions: contents: write @@ -36,9 +43,6 @@ jobs: - name: Make gradlew executable run: chmod +x gradlew - - name: Make scripts executable - run: chmod +x .github/scripts/*.sh - - name: Build run: | ./gradlew --version @@ -48,7 +52,7 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: cell-terminal-jar + name: jars path: build/libs/*.jar - name: Publish GitHub Release (tags only) @@ -57,44 +61,36 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | TAG="${GITHUB_REF##*/}" - TAG_COMMIT=$(git rev-list -n 1 "$TAG") + + # Get version and release type from Gradle + VERSION=$(./gradlew -q printVersion) + FULL_VERSION=$(./gradlew -q printFullVersion) + RELEASE_TYPE=$(./gradlew -q printReleaseType) + + # Get changelog from Gradle NOTES_FILE="/tmp/release_notes.md" - .github/scripts/build_release_notes.sh "$TAG" "${GITHUB_REPOSITORY%/*}" "${GITHUB_REPOSITORY#*/}" "$TAG_COMMIT" "${GITHUB_SERVER_URL:-https://github.com}" > "$NOTES_FILE" + ./gradlew -q printChangelog > "$NOTES_FILE" + + echo "Version: $VERSION, Release Type: $RELEASE_TYPE" >&2 echo "Generated release notes:" >&2 sed 's/^/ /' "$NOTES_FILE" >&2 + + # TODO: get "C.E.L.L.S" from gradle gh release create "$TAG" build/libs/*.jar \ - --title "Cell Terminal $TAG" \ + --title "$FULL_VERSION" \ --notes-file "$NOTES_FILE" \ - $(if [[ "$TAG" =~ [ab]|rc|pre ]]; then echo "--prerelease"; fi) + $(if [[ "$RELEASE_TYPE" != "release" ]]; then echo "--prerelease"; fi) - - name: Publish to CurseForge (tags only; infer release type) + - name: Publish to CurseForge (tags only) if: ${{ env.CURSEFORGE_TOKEN != '' && startsWith(github.ref, 'refs/tags/') }} - working-directory: . env: CURSEFORGE_TOKEN: ${{ env.CURSEFORGE_TOKEN }} - GIT_TAG: ${{ github.ref_name }} run: | - set -euo pipefail - TAG="${GIT_TAG:-${GITHUB_REF##*/}}" - VERSION_DISPLAY=$(echo "$TAG" | sed -E 's/^v//') - LOWER=$(echo "$TAG" | tr '[:upper:]' '[:lower:]') - RELEASE_TYPE=release - if [[ "$LOWER" == *alpha* ]]; then - RELEASE_TYPE=alpha - elif [[ "$LOWER" == *beta* || "$LOWER" == *rc* || "$LOWER" == *pre* ]]; then - RELEASE_TYPE=beta - else - CORE=$(echo "$LOWER" | sed -E 's/^v//') - if [[ "$CORE" =~ ^[0-9]+(\.[0-9]+)*a([0-9]*)?$ ]]; then - RELEASE_TYPE=alpha - elif [[ "$CORE" =~ ^[0-9]+(\.[0-9]+)*b([0-9]*)?$ ]]; then - RELEASE_TYPE=beta - fi - fi - TAG_COMMIT=$(git rev-list -n 1 "$TAG") - CL=$(.github/scripts/build_release_notes.sh "$TAG" "${GITHUB_REPOSITORY%/*}" "${GITHUB_REPOSITORY#*/}" "$TAG_COMMIT" "${GITHUB_SERVER_URL:-https://github.com}") + # Get release type from Gradle + RELEASE_TYPE=$(./gradlew -q printReleaseType) + + # Gradle handles changelog and display name from mod_version ./gradlew curseforge --no-daemon \ -Ppublish_to_curseforge=true \ - -Prelease_type="$RELEASE_TYPE" \ - -Pcurseforge_changelog="$CL" \ - -Pcurseforge_display_name="Cell Terminal $VERSION_DISPLAY" + -Ppublish_with_changelog=true \ + -Prelease_type="$RELEASE_TYPE" diff --git a/CHANGELOG.md b/CHANGELOG.md index 2333088..bef561c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,31 @@ The format is based on Keep a Changelog and this project adheres to Semantic Ver - Semantic Versioning: https://semver.org/spec/v2.0.0.html +## [1.5.1] - 2026-03-14 +### Fixed +- Fix Subnets Overview not handling Fluids. + + +## [1.5.0] - 2026-03-13 +### Added +- Add proper content/partition handling to the Subnet Overview, mirroring the behavior of the Temporary Area tab. +- Add tooltip for Temporary Cells stored in the Wireless Terminal. + +### Fixed +- Fix Upgrade Cards not being handled at all in the Temporary Area tab. +- Fix the Temporary Area not working at all with the Wireless Universal Terminal. +- (and probably many other small bugs I may add later as I compare in parallel) + +### Changed +- Rewrite the whole GUI part of the mod, with proper icons and missing features implemented. Similar widgets have been unified for a more consistent experience across the different tabs. + +### Technical +- Convert the heavy-handed context passing into a tree-like widgets structure. Tab Manager -> Tabs (Terminal/Inventory/Partition) -> Headers (Drive/Storage Bus) -> Tree Lines (Cell entries, partition entries, inventory entries). This allows to share a lot of the logic and code between the different tabs, as they are now mostly just different renderings of the same underlying data, and to avoid passing a huge amount of context parameters everywhere. +- The new widgets unify the behaviors by separating the implementation into the relevant widget classes, sharing the common logic and only implementing the specific rendering and interactions in the relevant classes. For example, the inventory and partition entries are the same widget with slightly different data (same data but different key), slightly different rendering, and slightly different interactions. +- The widgets handle clicks, hovering, and keybinds themselves at the lowest level, instead of delegating to a central handler in the main GUI class. This allows to move the logic for each interaction into the relevant widget, and avoid having a huge central handler with a lot of context parameters and special cases for different tabs. The main GUI delegates interactions to the top level widgets which then decide what to do with them, e.g. delegating further down to the relevant child widgets. +- The renaming logic has been moved to a singleton manager that just handles the state of the currently renaming entry and the renaming actions, the widgets themselves just call the manager when they need to trigger a rename. This allows to easily add renaming to *pretty much anything* in the GUI just by calling the manager with the relevant parameters and adding a new type in confirmEditing() for the network packet. +- The priority fields have also been (partially) moved to a singleton manager controlled by the headers. This case is slightly different, as they still need to keep state and they use a native text field for convenience. + ## [1.4.0] - 2026-03-04 ### Added - Add keybind for Subnet Overview toggle (default: back). diff --git a/gradle.properties b/gradle.properties index eec78a8..34d9b52 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ enable_junit_testing = true show_testing_output = false # Mod Information -mod_version = 1.4.0 +mod_version = 1.5.0 root_package = com.cellterminal mod_id = cellterminal mod_name = Cell Terminal diff --git a/gradle/scripts/extra.gradle b/gradle/scripts/extra.gradle index ca59920..fc1b54e 100644 --- a/gradle/scripts/extra.gradle +++ b/gradle/scripts/extra.gradle @@ -5,3 +5,68 @@ apply from: 'gradle/scripts/helpers.gradle' base { archivesName.set('cell-terminal') } + +// Task to print the changelog for CI usage (GitHub releases, etc.) +// Usage: ./gradlew -q printChangelog +tasks.register('printChangelog') { + group 'publishing' + description 'Prints the changelog for the current mod_version to stdout' + doLast { + if (!file('CHANGELOG.md').exists()) { + throw new GradleException('CHANGELOG.md does not exist!') + } + + + // Access the OutputType enum via the plugin's classloader at execution time + def changelogClass = project.buildscript.classLoader.loadClass('org.jetbrains.changelog.Changelog$OutputType') + def outputTypeMarkdown = changelogClass.enumConstants.find { it.name() == 'MARKDOWN' } + + def cl = changelog.renderItem( + changelog.get(propertyString('mod_version')).withHeader(false).withEmptySections(false), + outputTypeMarkdown + ) + + if (cl.isEmpty()) { + throw new GradleException("No changelog section found for version ${propertyString('mod_version')}") + } + + println cl + } +} + +// Task to print the mod version for CI usage +// Usage: ./gradlew -q printVersion +tasks.register('printVersion') { + group 'publishing' + description 'Prints the mod_version to stdout' + doLast { + println propertyString('mod_version') + } +} + +// Task to print the mod full version for CI usage +// Usage: ./gradlew -q printFullVersion +tasks.register('printFullVersion') { + group 'publishing' + description 'Prints the full mod version (mod_name + mod_version) to stdout' + doLast { + println "${propertyString('mod_name')} ${propertyString('mod_version')}" + } +} + +// Task to infer release type from mod_version (release, beta, alpha) +// Usage: ./gradlew -q printReleaseType +tasks.register('printReleaseType') { + group 'publishing' + description 'Prints the inferred release type (release/beta/alpha) based on mod_version' + doLast { + def version = propertyString('mod_version').toLowerCase() + def releaseType = 'release' + if (version.contains('alpha') || version =~ /^\d+(\.\d+)*a\d*$/) { + releaseType = 'alpha' + } else if (version.contains('beta') || version.contains('rc') || version.contains('pre') || version =~ /^\d+(\.\d+)*b\d*$/) { + releaseType = 'beta' + } + println releaseType + } +} \ No newline at end of file diff --git a/src/main/java/com/cellterminal/client/AdvancedSearchParser.java b/src/main/java/com/cellterminal/client/AdvancedSearchParser.java index 80e8968..4483abc 100644 --- a/src/main/java/com/cellterminal/client/AdvancedSearchParser.java +++ b/src/main/java/com/cellterminal/client/AdvancedSearchParser.java @@ -328,9 +328,9 @@ private static boolean isStringIdentifier(String id) { } private static boolean isOperator(String s) { - return "=".equals(s) || "!=".equals(s) || "<".equals(s) || ">".equals(s) + return s.startsWith("=") || "!=".equals(s) || s.startsWith("<") || s.startsWith(">") || "<=".equals(s) || ">=".equals(s) || "~".equals(s) - || s.startsWith("=") || s.startsWith("!") || s.startsWith("<") || s.startsWith(">") || s.startsWith("~"); + || s.startsWith("!") || s.startsWith("~"); } private static String normalizeOperator(String s) { diff --git a/src/main/java/com/cellterminal/client/BlockHighlightRenderer.java b/src/main/java/com/cellterminal/client/BlockHighlightRenderer.java index 0c39115..53336db 100644 --- a/src/main/java/com/cellterminal/client/BlockHighlightRenderer.java +++ b/src/main/java/com/cellterminal/client/BlockHighlightRenderer.java @@ -2,7 +2,6 @@ import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; @@ -83,10 +82,7 @@ public void onRenderWorldLast(RenderWorldLastEvent event) { long now = System.currentTimeMillis(); // Remove expired standard highlights - Iterator> iter = highlightedBlocks.entrySet().iterator(); - while (iter.hasNext()) { - if (iter.next().getValue() < now) iter.remove(); - } + highlightedBlocks.entrySet().removeIf(blockPosLongEntry -> blockPosLongEntry.getValue() < now); if (highlightedBlocks.isEmpty()) return; diff --git a/src/main/java/com/cellterminal/client/CellInfo.java b/src/main/java/com/cellterminal/client/CellInfo.java index a8a191a..3cc5b0d 100644 --- a/src/main/java/com/cellterminal/client/CellInfo.java +++ b/src/main/java/com/cellterminal/client/CellInfo.java @@ -9,7 +9,6 @@ import net.minecraft.nbt.NBTTagList; import net.minecraftforge.common.util.Constants; -import appeng.api.config.Upgrades; import appeng.api.implementations.items.IUpgradeModule; import com.cellterminal.gui.rename.Renameable; @@ -138,10 +137,6 @@ public int getSlot() { return slot; } - public int getStatus() { - return status; - } - public boolean isFluid() { return isFluid; } @@ -166,18 +161,10 @@ public long getTotalBytes() { return totalBytes; } - public long getUsedTypes() { - return usedTypes; - } - public long getTotalTypes() { return totalTypes; } - public long getStoredItemCount() { - return storedItemCount; - } - public List getPartition() { return partition; } @@ -198,31 +185,12 @@ public float getByteUsagePercent() { return (float) usedBytes / totalBytes; } - public float getTypeUsagePercent() { - if (totalTypes == 0) return 0; - - return (float) usedTypes / totalTypes; - } - public String getDisplayName() { if (!cellItem.isEmpty()) return cellItem.getDisplayName(); return I18n.format("gui.cellterminal.cell_empty"); } - // TODO: replace with the ItemStack to avoid Type Squatting - public boolean hasSticky() { - return getInstalledUpgrades(Upgrades.STICKY) > 0; - } - - public boolean hasFuzzy() { - return getInstalledUpgrades(Upgrades.FUZZY) > 0; - } - - public boolean hasInverter() { - return getInstalledUpgrades(Upgrades.INVERTER) > 0; - } - public List getUpgrades() { return upgrades; } @@ -238,39 +206,18 @@ public int getUpgradeSlotIndex(int index) { return upgradeSlotIndices.get(index); } + // TODO: does it make sense to have client-side upgrades checks when we moved all validation server-side? + public int getUpgradeSlotCount() { // Standard AE2 cells have 2 upgrade slots // TODO: adjust if supporting cells with different upgrade slot counts return 2; } - public int getInstalledUpgradeCount() { - return upgrades.size(); - } - public boolean hasUpgradeSpace() { return upgrades.size() < getUpgradeSlotCount(); } - /** - * Check how many of a specific upgrade type are currently installed. - * @param upgradeType The upgrade type to count - * @return The number of this upgrade currently installed - */ - public int getInstalledUpgrades(Upgrades upgradeType) { - if (upgradeType == null) return 0; - - int count = 0; - for (ItemStack upgrade : upgrades) { - if (upgrade.getItem() instanceof IUpgradeModule) { - Upgrades type = ((IUpgradeModule) upgrade.getItem()).getType(upgrade); - if (type == upgradeType) count++; - } - } - - return count; - } - /** * Check if this cell can potentially accept the given upgrade item. * This is a client-side heuristic only - actual validation happens server-side. @@ -282,6 +229,10 @@ public boolean canAcceptUpgrade(ItemStack upgradeStack) { if (upgradeStack.isEmpty()) return false; if (!(upgradeStack.getItem() instanceof IUpgradeModule)) return false; + // Distinguish real upgrades from storage components that also implement IUpgradeModule + // Real upgrades (speed card, capacity card, etc.) return a non-null Upgrades type + if (((IUpgradeModule) upgradeStack.getItem()).getType(upgradeStack) == null) return false; + return hasUpgradeSpace(); } diff --git a/src/main/java/com/cellterminal/client/Prioritizable.java b/src/main/java/com/cellterminal/client/Prioritizable.java new file mode 100644 index 0000000..7f96e68 --- /dev/null +++ b/src/main/java/com/cellterminal/client/Prioritizable.java @@ -0,0 +1,21 @@ +package com.cellterminal.client; + + +/** + * Shared interface for data objects that support AE2 priority editing. + *

+ * Implemented by {@link StorageInfo} (drives/chests) and {@link StorageBusInfo} + * (storage buses). Used by the priority field system to avoid duplicating + * field logic for each data type. + */ +public interface Prioritizable { + + /** Unique identifier used for priority packet addressing. */ + long getId(); + + /** Current priority value. */ + int getPriority(); + + /** Whether this object supports priority editing (some storages don't). */ + boolean supportsPriority(); +} diff --git a/src/main/java/com/cellterminal/client/StorageBusInfo.java b/src/main/java/com/cellterminal/client/StorageBusInfo.java index fcbb8d6..1789b02 100644 --- a/src/main/java/com/cellterminal/client/StorageBusInfo.java +++ b/src/main/java/com/cellterminal/client/StorageBusInfo.java @@ -22,7 +22,7 @@ * Client-side data holder for storage bus information received from server. * Similar to CellInfo but for storage buses which connect to external inventories. */ -public class StorageBusInfo implements Renameable { +public class StorageBusInfo implements Renameable, Prioritizable { /** * Base number of config slots without any capacity upgrades. @@ -39,11 +39,6 @@ public class StorageBusInfo implements Renameable { */ public static final int MAX_CONFIG_SLOTS = 63; - /** - * Maximum capacity upgrades a storage bus can have. - */ - public static final int MAX_CAPACITY_UPGRADES = 5; - /** * Calculate the number of available config slots for a given capacity upgrade count. * FIXME: This should be provided by the scanner implementation instead of being hardcoded here. @@ -73,7 +68,6 @@ public static int calculateAvailableSlots(int capacityUpgrades) { private final List contentCounts = new ArrayList<>(); private final List upgrades = new ArrayList<>(); private final List upgradeSlotIndices = new ArrayList<>(); - private boolean expanded = true; private final boolean supportsPriorityFlag; private final boolean supportsIOModeFlag; @@ -201,19 +195,7 @@ public int getPriority() { public int getAvailableConfigSlots() { int raw = baseConfigSlots + slotsPerUpgrade * Math.max(0, getInstalledUpgrades(Upgrades.CAPACITY)); - return raw > maxConfigSlots ? maxConfigSlots : raw; - } - - public boolean hasInverter() { - return getInstalledUpgrades(Upgrades.INVERTER) > 0; - } - - public boolean hasSticky() { - return getInstalledUpgrades(Upgrades.STICKY) > 0; - } - - public boolean hasFuzzy() { - return getInstalledUpgrades(Upgrades.FUZZY) > 0; + return Math.min(raw, maxConfigSlots); } public boolean isFluid() { @@ -292,18 +274,6 @@ public int getUpgradeSlotIndex(int index) { return upgradeSlotIndices.get(index); } - public boolean isExpanded() { - return expanded; - } - - public void setExpanded(boolean expanded) { - this.expanded = expanded; - } - - public void toggleExpanded() { - this.expanded = !this.expanded; - } - public String getDisplayName() { return I18n.format("gui.cellterminal.storage_bus.name"); } @@ -386,7 +356,18 @@ public long getTotalItemCount() { * Storage buses have 5 upgrade slots. */ public boolean hasUpgradeSpace() { - return upgrades.size() < 5; + return upgrades.size() < getUpgradeSlotCount(); + } + + /** + * Get the total number of upgrade slots available. + * Standard AE2 storage buses have 5 upgrade slots. + * @return The total upgrade slot count + */ + public int getUpgradeSlotCount() { + // Standard AE2 storage buses have 5 upgrade slots + // TODO: adjust if supporting buses with different upgrade slot counts + return 5; } /** @@ -419,6 +400,10 @@ public boolean canAcceptUpgrade(ItemStack upgradeStack) { if (upgradeStack.isEmpty()) return false; if (!(upgradeStack.getItem() instanceof IUpgradeModule)) return false; + // Distinguish real upgrades from storage components that also implement IUpgradeModule + // Real upgrades (speed card, capacity card, etc.) return a non-null Upgrades type + if (((IUpgradeModule) upgradeStack.getItem()).getType(upgradeStack) == null) return false; + return hasUpgradeSpace(); } diff --git a/src/main/java/com/cellterminal/client/StorageInfo.java b/src/main/java/com/cellterminal/client/StorageInfo.java index 5779f4e..5c4b2a6 100644 --- a/src/main/java/com/cellterminal/client/StorageInfo.java +++ b/src/main/java/com/cellterminal/client/StorageInfo.java @@ -17,7 +17,7 @@ /** * Client-side data holder for drive/chest storage information received from server. */ -public class StorageInfo implements Renameable { +public class StorageInfo implements Renameable, Prioritizable { private final long id; private final BlockPos pos; @@ -28,7 +28,6 @@ public class StorageInfo implements Renameable { private final int priority; private final boolean supportsPriorityFlag; private final List cells = new ArrayList<>(); - private boolean expanded = true; public StorageInfo(NBTTagCompound nbt) { this.id = nbt.getLong("id"); @@ -87,18 +86,6 @@ public List getCells() { return cells; } - public boolean isExpanded() { - return expanded; - } - - public void setExpanded(boolean expanded) { - this.expanded = expanded; - } - - public void toggleExpanded() { - this.expanded = !this.expanded; - } - public String getLocationString() { return I18n.format("gui.cellterminal.location_format", pos.getX(), pos.getY(), pos.getZ(), dimension); } diff --git a/src/main/java/com/cellterminal/client/SubnetConnectionEntry.java b/src/main/java/com/cellterminal/client/SubnetConnectionEntry.java new file mode 100644 index 0000000..c82a01c --- /dev/null +++ b/src/main/java/com/cellterminal/client/SubnetConnectionEntry.java @@ -0,0 +1,50 @@ +package com.cellterminal.client; + + +/** + * Represents a single connection point as a header-level entry in the subnet overview. + *

+ * Each connection between the main network and a subnet gets its own header row, + * showing the direction arrow (→/←), icon, subnet name, connection position, and Load button. + *

+ * The flattened display list for subnets is: + *

+ *   SubnetInfo (main network only, no connections)
+ *   SubnetConnectionEntry → connection header (with arrow, per-connection)
+ *     SubnetConnectionRow (isPartitionRow=false) → SlotsLine (content)
+ *     SubnetConnectionRow (isPartitionRow=true) → SlotsLine (partition/filter)
+ * 
+ * + * @see SubnetInfo + * @see SubnetInfo.ConnectionPoint + * @see SubnetConnectionRow + */ +public class SubnetConnectionEntry { + + private final SubnetInfo subnet; + private final SubnetInfo.ConnectionPoint connection; + private final int connectionIndex; + + /** + * @param subnet The subnet this connection belongs to + * @param connection The specific connection point + * @param connectionIndex Index of this connection in the subnet's connection list + */ + public SubnetConnectionEntry(SubnetInfo subnet, SubnetInfo.ConnectionPoint connection, int connectionIndex) { + this.subnet = subnet; + this.connection = connection; + this.connectionIndex = connectionIndex; + } + + public SubnetInfo getSubnet() { + return subnet; + } + + public SubnetInfo.ConnectionPoint getConnection() { + return connection; + } + + public int getConnectionIndex() { + return connectionIndex; + } +} diff --git a/src/main/java/com/cellterminal/client/SubnetConnectionRow.java b/src/main/java/com/cellterminal/client/SubnetConnectionRow.java index 5bf04f9..6e59b9d 100644 --- a/src/main/java/com/cellterminal/client/SubnetConnectionRow.java +++ b/src/main/java/com/cellterminal/client/SubnetConnectionRow.java @@ -1,36 +1,56 @@ package com.cellterminal.client; +import java.util.List; + import net.minecraft.item.ItemStack; /** - * Represents a row of connection filter items for display in the subnet overview. - * Each SubnetConnectionRow shows up to 9 filter items from a connection point, starting at a given index. + * Represents a row of connection content or partition items for display in the subnet overview. + * Each SubnetConnectionRow shows up to 9 items from a connection point, starting at a given index. + *

+ * This is analogous to {@link CellContentRow} but for subnet connections. Like CellContentRow, + * each row has an {@code isPartitionRow} flag to distinguish content rows (read-only, items + * flowing through the connection) from partition rows (editable, storage bus filter config). *

- * This is analogous to {@link StorageBusContentRow} but for subnet connections. + * The flattened display under a {@link SubnetConnectionEntry} header is: + *

+ *   SubnetConnectionRow (isPartitionRow=false) → content rows (subnet's whole storage)
+ *   SubnetConnectionRow (isPartitionRow=true)  → partition rows (storage bus filter config)
+ * 
+ * + * @see SubnetConnectionEntry + * @see SubnetInfo.ConnectionPoint */ public class SubnetConnectionRow { private final SubnetInfo subnet; private final SubnetInfo.ConnectionPoint connection; - private final int connectionIndex; // Index of this connection in the subnet's connection list - private final int filterStartIndex; // Starting index of filter items to display (0, 9, 18, etc.) - private final boolean isFirstRowForConnection; + private final int connectionIndex; + private final int startIndex; + private final boolean isFirstRow; + private final boolean isPartitionRow; + private final boolean usesSubnetInventory; /** * @param subnet The subnet this row belongs to - * @param connection The connection point this row displays filters for + * @param connection The connection point this row displays items for * @param connectionIndex Index of this connection in subnet's connection list - * @param filterStartIndex The starting index of filter items to display (0, 9, 18, etc.) - * @param isFirstRowForConnection Whether this is the first row for this connection + * @param startIndex The starting index of items to display (0, 9, 18, etc.) + * @param isFirstRow Whether this is the first row for this content/partition section + * @param isPartitionRow Whether this row displays partition data (vs content data) + * @param usesSubnetInventory Whether content comes from the subnet's ME storage inventory */ public SubnetConnectionRow(SubnetInfo subnet, SubnetInfo.ConnectionPoint connection, - int connectionIndex, int filterStartIndex, boolean isFirstRowForConnection) { + int connectionIndex, int startIndex, boolean isFirstRow, + boolean isPartitionRow, boolean usesSubnetInventory) { this.subnet = subnet; this.connection = connection; this.connectionIndex = connectionIndex; - this.filterStartIndex = filterStartIndex; - this.isFirstRowForConnection = isFirstRowForConnection; + this.startIndex = startIndex; + this.isFirstRow = isFirstRow; + this.isPartitionRow = isPartitionRow; + this.usesSubnetInventory = usesSubnetInventory; } public SubnetInfo getSubnet() { @@ -45,42 +65,58 @@ public int getConnectionIndex() { return connectionIndex; } - public int getFilterStartIndex() { - return filterStartIndex; + public int getStartIndex() { + return startIndex; } - public boolean isFirstRowForConnection() { - return isFirstRowForConnection; + public boolean isFirstRow() { + return isFirstRow; } /** - * Get the filter item at the given slot index (0-8 for this row). - * @param slotIndex Slot index within this row (0-8) - * @return The filter item, or EMPTY if none + * Whether this row displays partition data (storage bus filter config). + * False means this row displays content data (items flowing through connection). */ - public ItemStack getFilterAt(int slotIndex) { - int actualIndex = filterStartIndex + slotIndex; - - if (actualIndex < connection.getFilter().size()) { - return connection.getFilter().get(actualIndex); - } + public boolean isPartitionRow() { + return isPartitionRow; + } - return ItemStack.EMPTY; + /** + * Whether this row's content comes from the subnet's aggregated ME storage inventory + * rather than from the per-connection content. + * Only meaningful for content rows (isPartitionRow=false) on outbound connections. + */ + public boolean usesSubnetInventory() { + return usesSubnetInventory; } /** - * Get the total number of filter items in this connection. + * Get the items for this row's data source. + *
    + *
  • Partition rows → connection partition
  • + *
  • Content rows with subnet inventory → subnet's ME storage
  • + *
  • Content rows without subnet inventory → connection's content
  • + *
*/ - public int getTotalFilterCount() { - return connection.getFilter().size(); + public List getItems() { + if (isPartitionRow) return connection.getPartition(); + if (usesSubnetInventory) return subnet.getInventory(); + + return connection.getContent(); } /** - * Get the number of filter items displayed in this row (up to 9). + * Get the total number of items (in the relevant list, content or partition). */ - public int getFilterCountInRow() { - int remaining = connection.getFilter().size() - filterStartIndex; + public int getTotalItemCount() { + return getItems().size(); + } - return Math.min(remaining, 9); + /** + * Get the max number of slots available for partition editing. + * Content has no meaningful max (backend decides). + */ + public int getMaxSlots() { + return isPartitionRow ? connection.getMaxPartitionSlots() : Integer.MAX_VALUE; } } diff --git a/src/main/java/com/cellterminal/client/SubnetInfo.java b/src/main/java/com/cellterminal/client/SubnetInfo.java index 0f66210..aeb508c 100644 --- a/src/main/java/com/cellterminal/client/SubnetInfo.java +++ b/src/main/java/com/cellterminal/client/SubnetInfo.java @@ -11,6 +11,9 @@ import net.minecraft.util.math.BlockPos; import net.minecraftforge.common.util.Constants; +import com.cellterminal.gui.rename.Renameable; +import com.cellterminal.gui.rename.RenameTargetType; + /** * Client-side data holder for subnet connection information received from server. @@ -33,7 +36,7 @@ *

* Note: P2P Tunnels do NOT create subnets - they teleport channels within the same grid. */ -public class SubnetInfo { +public class SubnetInfo implements Renameable { /** * Represents a single connection point between main network and subnet. @@ -42,26 +45,49 @@ public class SubnetInfo { public static class ConnectionPoint { private final BlockPos pos; // Position on main network + private final int dimension; // Dimension of the connection (can differ from subnet primary dim via Quantum Bridge) private final EnumFacing side; // Side of the part private final boolean isOutbound; // true = Storage Bus on main, false = Interface on main private final ItemStack localIcon; // Icon of the block on main network private final ItemStack remoteIcon; // Icon of the block on subnet - private final List filter; // Filter configuration (for Storage Bus connections) + + // Content items (items flowing through the connection). Empty list if no content key in data. + private final List content; + private final boolean hasContentKey; // Whether the backend sent a "content" key at all + + // Partition items (storage bus filter config). Empty list if no partition key in data. + private final List partition; + private final boolean hasPartitionKey; // Whether the backend sent a "filter" key at all + private final int maxPartitionSlots; // Maximum number of partition slots (e.g. 63 for storage bus) public ConnectionPoint(NBTTagCompound nbt) { this.pos = BlockPos.fromLong(nbt.getLong("pos")); + this.dimension = nbt.getInteger("dim"); this.side = EnumFacing.byIndex(nbt.getInteger("side")); this.isOutbound = nbt.getBoolean("outbound"); this.localIcon = nbt.hasKey("localIcon") ? new ItemStack(nbt.getCompoundTag("localIcon")) : ItemStack.EMPTY; this.remoteIcon = nbt.hasKey("remoteIcon") ? new ItemStack(nbt.getCompoundTag("remoteIcon")) : ItemStack.EMPTY; - this.filter = new ArrayList<>(); - if (nbt.hasKey("filter")) { + // Content items (future: backend will send "content" key with subnet inventory) + this.hasContentKey = nbt.hasKey("content"); + this.content = new ArrayList<>(); + if (hasContentKey) { + NBTTagList contentList = nbt.getTagList("content", Constants.NBT.TAG_COMPOUND); + for (int i = 0; i < contentList.tagCount(); i++) { + content.add(new ItemStack(contentList.getCompoundTagAt(i))); + } + } + + // Partition items (storage bus filter config, with empty slots preserved for position-aware editing) + this.hasPartitionKey = nbt.hasKey("filter"); + this.partition = new ArrayList<>(); + if (hasPartitionKey) { NBTTagList filterList = nbt.getTagList("filter", Constants.NBT.TAG_COMPOUND); for (int i = 0; i < filterList.tagCount(); i++) { - filter.add(new ItemStack(filterList.getCompoundTagAt(i))); + partition.add(new ItemStack(filterList.getCompoundTagAt(i))); } } + this.maxPartitionSlots = nbt.hasKey("maxPartitionSlots") ? nbt.getInteger("maxPartitionSlots") : 63; } public BlockPos getPos() { @@ -72,6 +98,14 @@ public EnumFacing getSide() { return side; } + /** + * Dimension of this connection point. + * May differ from the subnet's primary dimension when Quantum Bridges are involved. + */ + public int getDimension() { + return dimension; + } + /** * True if this is an outbound connection (Storage Bus on main → Interface on subnet). * False if this is an inbound connection (Interface on main ← Storage Bus on subnet). @@ -95,17 +129,49 @@ public ItemStack getRemoteIcon() { } /** - * Filter configuration (for Storage Bus connections only). + * Content items flowing through this connection. + * Empty if backend doesn't send content data yet. + */ + public List getContent() { + return content; + } + + /** + * Whether the backend sent a "content" key at all. + * When true, content rows should be shown (even if the list is empty). */ - public List getFilter() { - return filter; + public boolean hasContentKey() { + return hasContentKey; } /** - * Check if this connection has a non-empty filter configured. + * Partition items (storage bus filter configuration). + * Includes empty slots to preserve slot positions for editing. + */ + public List getPartition() { + return partition; + } + + /** + * Whether the backend sent a "filter" key at all. + * When true, partition rows should be shown (even if the list is empty). + */ + public boolean hasPartitionKey() { + return hasPartitionKey; + } + + /** + * Maximum number of partition slots (e.g. 63 for an AE2 storage bus). + */ + public int getMaxPartitionSlots() { + return maxPartitionSlots; + } + + /** + * Check if this connection has a non-empty filter/partition configured. */ public boolean hasFilter() { - for (ItemStack stack : filter) { + for (ItemStack stack : partition) { if (!stack.isEmpty()) return true; } @@ -114,7 +180,7 @@ public boolean hasFilter() { } private final long id; // Unique identifier for this subnet (based on grid hash or primary position) - private final int dimension; + private final int dimension; // Primary dimension (from primary node, used for sorting) private final BlockPos primaryPos; // Primary position for sorting/highlighting (first interface position) private final String defaultName; // Auto-generated name (e.g., "Subnet @ X, Y, Z") private String customName; // User-defined name @@ -126,6 +192,10 @@ public boolean hasFilter() { // All connection points between main network and this subnet private final List connections = new ArrayList<>(); + // Subnet inventory: all items/fluids stored in the subnet's ME storage + private final List inventory = new ArrayList<>(); + private final List inventoryCounts = new ArrayList<>(); + // Whether this represents the main network (ID = 0) private final boolean isMainNetwork; @@ -179,6 +249,18 @@ public SubnetInfo(NBTTagCompound nbt) { connections.add(new ConnectionPoint(connList.getCompoundTagAt(i))); } } + + // Load subnet inventory (items/fluids in the subnet's ME storage) + if (nbt.hasKey("inventory")) { + NBTTagList invList = nbt.getTagList("inventory", Constants.NBT.TAG_COMPOUND); + for (int i = 0; i < invList.tagCount(); i++) { + NBTTagCompound stackNbt = invList.getCompoundTagAt(i); + ItemStack stack = new ItemStack(stackNbt); + long count = stackNbt.hasKey("Cnt") ? stackNbt.getLong("Cnt") : stack.getCount(); + inventory.add(stack); + inventoryCounts.add(count); + } + } } public long getId() { @@ -268,6 +350,29 @@ public List getConnections() { return connections; } + /** + * Get the subnet's inventory items (all items/fluids stored in the subnet's ME storage). + */ + public List getInventory() { + return inventory; + } + + /** + * Get the count for an inventory item at the given index. + */ + public long getInventoryCount(int index) { + if (index < 0 || index >= inventoryCounts.size()) return 0; + + return inventoryCounts.get(index); + } + + /** + * Whether this subnet has any inventory data. + */ + public boolean hasInventory() { + return !inventory.isEmpty(); + } + /** * Get the number of outbound connections (Storage Bus on main → Interface on subnet). */ @@ -300,7 +405,7 @@ public List getAllFilterItems(int maxItems) { List items = new ArrayList<>(); for (ConnectionPoint cp : connections) { - for (ItemStack stack : cp.getFilter()) { + for (ItemStack stack : cp.getPartition()) { if (stack.isEmpty()) continue; if (items.size() >= maxItems) return items; @@ -344,32 +449,90 @@ public NBTTagCompound writeActionNBT() { } /** - * Build a list of connection rows for displaying filters under this subnet's header. - * Each connection with filters gets one or more rows (9 items per row). - * Connections without filters still get one row to show the connection info. + * Build content and partition rows for a single connection, following the same layout + * as Temp Area: content rows first, then partition rows. + *

+ * Content rows are only emitted if the backend sent a "content" key (even if empty). + * Partition rows are only emitted if the backend sent a "filter" key (even if empty). + * When present, at least one row is always shown (even for empty data). + *

+ * Partition display stops at the last non-empty slot, but adds one more row when + * the last column is occupied (so the user can add more items). * - * @param maxFilterItemsPerRow Maximum filter items shown per row (typically 9) - * @return List of connection rows for this subnet + * @param slotsPerRow Number of slots per row (typically 9) + * @return List of connection rows for this connection */ - public List buildConnectionRows(int maxFilterItemsPerRow) { + public static List buildConnectionContentRows( + SubnetInfo subnet, ConnectionPoint conn, int connIdx, int slotsPerRow) { List rows = new ArrayList<>(); - for (int connIdx = 0; connIdx < connections.size(); connIdx++) { - ConnectionPoint conn = connections.get(connIdx); - int filterCount = conn.getFilter().size(); - - if (filterCount == 0) { - // Connection with no filter - show one row with connection info only - rows.add(new SubnetConnectionRow(this, conn, connIdx, 0, true)); - } else { - // Connection with filters - create rows for each batch of items - for (int startIdx = 0; startIdx < filterCount; startIdx += maxFilterItemsPerRow) { - boolean isFirst = (startIdx == 0); - rows.add(new SubnetConnectionRow(this, conn, connIdx, startIdx, isFirst)); - } + // For outbound connections (Storage Bus on main → Interface on subnet), + // show the subnet's entire ME storage as "content" so the user can see + // what's available and use "Partition All" to set filters. + if (conn.isOutbound() && subnet.hasInventory()) { + int contentCount = subnet.getInventory().size(); + int contentRows = Math.max(1, (contentCount + slotsPerRow - 1) / slotsPerRow); + for (int row = 0; row < contentRows; row++) { + rows.add(new SubnetConnectionRow(subnet, conn, connIdx, + row * slotsPerRow, row == 0, false, true)); + } + + // For inbound connections, use per-connection content (if backend sends it) + } else if (conn.hasContentKey()) { + int contentCount = conn.getContent().size(); + int contentRows = Math.max(1, (contentCount + slotsPerRow - 1) / slotsPerRow); + for (int row = 0; row < contentRows; row++) { + rows.add(new SubnetConnectionRow(subnet, conn, connIdx, + row * slotsPerRow, row == 0, false, false)); + } + } + + // Partition rows (storage bus filter config) + if (conn.hasPartitionKey()) { + int highestSlot = getHighestNonEmptySlot(conn.getPartition()); + int partitionRows = Math.max(1, (highestSlot + slotsPerRow) / slotsPerRow); + + // If the last visible column is occupied and there's room for more, + // add one more row so the user can expand + if (highestSlot >= 0 && (highestSlot + 1) % slotsPerRow == 0 + && (highestSlot + 1) < conn.getMaxPartitionSlots()) { + partitionRows++; + } + + for (int row = 0; row < partitionRows; row++) { + rows.add(new SubnetConnectionRow(subnet, conn, connIdx, + row * slotsPerRow, row == 0, true, false)); } } return rows; } + + /** + * Find the highest non-empty slot index in a list, or -1 if all empty. + */ + private static int getHighestNonEmptySlot(List items) { + for (int i = items.size() - 1; i >= 0; i--) { + if (!items.get(i).isEmpty()) return i; + } + + return -1; + } + + // ---- Renameable implementation ---- + + @Override + public boolean isRenameable() { + return !isMainNetwork; + } + + @Override + public RenameTargetType getRenameTargetType() { + return RenameTargetType.SUBNET; + } + + @Override + public long getRenameId() { + return id; + } } diff --git a/src/main/java/com/cellterminal/client/TabStateManager.java b/src/main/java/com/cellterminal/client/TabStateManager.java index 6926452..8ba77d2 100644 --- a/src/main/java/com/cellterminal/client/TabStateManager.java +++ b/src/main/java/com/cellterminal/client/TabStateManager.java @@ -21,7 +21,8 @@ public enum TabType { INVENTORY(1), PARTITION(2), STORAGE_BUS_INVENTORY(3), - STORAGE_BUS_PARTITION(4); + STORAGE_BUS_PARTITION(4), + SUBNET_OVERVIEW(-1); private final int index; @@ -61,6 +62,10 @@ public static TabStateManager getInstance() { return INSTANCE; } + public static boolean isSubnetTab(int tabIndex) { + return tabIndex == TabType.SUBNET_OVERVIEW.getIndex(); + } + /** * Check if a storage entry is expanded for the given tab. * diff --git a/src/main/java/com/cellterminal/client/UpgradeTooltipHandler.java b/src/main/java/com/cellterminal/client/UpgradeTooltipHandler.java index 0c61da5..6d6aabb 100644 --- a/src/main/java/com/cellterminal/client/UpgradeTooltipHandler.java +++ b/src/main/java/com/cellterminal/client/UpgradeTooltipHandler.java @@ -30,8 +30,10 @@ public void onItemTooltip(ItemTooltipEvent event) { ItemStack stack = event.getItemStack(); if (stack.isEmpty()) return; - // Only add hints to upgrade items + // Only add hints to real upgrade items, not storage components that + // also implement IUpgradeModule but return null from getType() if (!(stack.getItem() instanceof IUpgradeModule)) return; + if (((IUpgradeModule) stack.getItem()).getType(stack) == null) return; List tooltip = event.getToolTip(); diff --git a/src/main/java/com/cellterminal/client/WUTTooltipHandler.java b/src/main/java/com/cellterminal/client/WUTTooltipHandler.java new file mode 100644 index 0000000..7afc788 --- /dev/null +++ b/src/main/java/com/cellterminal/client/WUTTooltipHandler.java @@ -0,0 +1,57 @@ +package com.cellterminal.client; + +import java.util.List; + +import net.minecraft.client.resources.I18n; +import net.minecraft.item.ItemStack; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.relauncher.Side; +import net.minecraftforge.fml.relauncher.SideOnly; + +import com.cellterminal.integration.AE2WUTIntegration; +import com.cellterminal.items.ItemWirelessCellTerminal; + + +/** + * Adds temp cell information to Wireless Universal Terminal item tooltips. + * Uses Forge's ItemTooltipEvent to inject tooltip lines without requiring a mixin. + */ +@SideOnly(Side.CLIENT) +public class WUTTooltipHandler { + + @SubscribeEvent + public void onItemTooltip(ItemTooltipEvent event) { + // Skip if AE2WUT isn't loaded + if (!AE2WUTIntegration.isModLoaded()) return; + + ItemStack stack = event.getItemStack(); + if (stack.isEmpty()) return; + + // Only process WUT items + if (!AE2WUTIntegration.isWirelessUniversalTerminal(stack)) return; + + // Check if this WUT has the Cell Terminal mode installed + int[] modes = AE2WUTIntegration.getWUTModes(stack); + if (modes == null) return; + + boolean hasCellTerminalMode = false; + byte cellMode = AE2WUTIntegration.getCellTerminalMode(); + for (int mode : modes) { + if (mode == cellMode) { + hasCellTerminalMode = true; + break; + } + } + + if (!hasCellTerminalMode) return; + + // Get temp cell count from the WUT's NBT + int tempCellCount = ItemWirelessCellTerminal.getTempCellCount(stack); + if (tempCellCount <= 0) return; + + // Add temp cell count to tooltip + List tooltip = event.getToolTip(); + tooltip.add(I18n.format("item.cellterminal.wireless_cell_terminal.temp_cells", tempCellCount)); + } +} diff --git a/src/main/java/com/cellterminal/config/CellTerminalClientConfig.java b/src/main/java/com/cellterminal/config/CellTerminalClientConfig.java index 63d3934..33cc2c4 100644 --- a/src/main/java/com/cellterminal/config/CellTerminalClientConfig.java +++ b/src/main/java/com/cellterminal/config/CellTerminalClientConfig.java @@ -54,6 +54,7 @@ public class CellTerminalClientConfig { private final Property searchModeProperty; private final Property cellSlotLimitProperty; private final Property busSlotLimitProperty; + private final Property subnetSlotLimitProperty; private final Property lastViewedNetworkIdProperty; private final Property subnetVisibilityProperty; private int selectedTab = GuiConstants.TAB_TERMINAL; @@ -62,6 +63,7 @@ public class CellTerminalClientConfig { private SearchFilterMode searchMode = SearchFilterMode.MIXED; private SlotLimit cellSlotLimit = SlotLimit.UNLIMITED; private SlotLimit busSlotLimit = SlotLimit.UNLIMITED; + private SlotLimit subnetSlotLimit = SlotLimit.LIMIT_64; private long lastViewedNetworkId = 0; // 0 = main network private SubnetVisibility subnetVisibility = SubnetVisibility.DONT_SHOW; @@ -149,6 +151,11 @@ private CellTerminalClientConfig() { this.busSlotLimitProperty.setLanguageKey("config.cellterminal.gui.busSlotLimit"); this.busSlotLimit = SlotLimit.fromName(this.busSlotLimitProperty.getString()); + this.subnetSlotLimitProperty = config.get(CATEGORY_GUI, "subnetSlotLimit", SlotLimit.LIMIT_64.name(), + "Slot limit for subnet inventory content display: LIMIT_8, LIMIT_32, LIMIT_64, or UNLIMITED"); + this.subnetSlotLimitProperty.setLanguageKey("config.cellterminal.gui.subnetSlotLimit"); + this.subnetSlotLimit = SlotLimit.fromName(this.subnetSlotLimitProperty.getString()); + this.lastViewedNetworkIdProperty = config.get(CATEGORY_GUI, "lastViewedNetworkId", "0", "The last viewed network ID (0 = main network, other = subnet ID). Stored as string to support long values."); this.lastViewedNetworkIdProperty.setLanguageKey("config.cellterminal.gui.lastViewedNetworkId"); @@ -322,6 +329,35 @@ public SlotLimit cycleBusSlotLimit() { return next; } + /** + * Get the slot limit for subnet inventory display. + */ + public SlotLimit getSubnetSlotLimit() { + return subnetSlotLimit; + } + + /** + * Set the slot limit for subnet inventory display. + */ + public void setSubnetSlotLimit(SlotLimit limit) { + if (this.subnetSlotLimit == limit) return; + + this.subnetSlotLimit = limit; + this.subnetSlotLimitProperty.set(limit.name()); + config.save(); + } + + /** + * Cycle to the next subnet slot limit. + * @return The new slot limit after cycling + */ + public SlotLimit cycleSubnetSlotLimit() { + SlotLimit next = subnetSlotLimit.next(); + setSubnetSlotLimit(next); + + return next; + } + /** * Get the last viewed network ID (0 = main network). */ @@ -377,6 +413,19 @@ public SlotLimit getSlotLimit(boolean forStorageBus) { return forStorageBus ? busSlotLimit : cellSlotLimit; } + /** + * Get the appropriate slot limit for the given tab index. + * Handles cell tabs, storage bus tabs, and subnet overview. + * @param tabIndex The tab index (-1 for subnet overview) + */ + public SlotLimit getSlotLimitForTab(int tabIndex) { + // FIXME: use the indexes defined in GuiConstants + if (tabIndex < 0) return subnetSlotLimit; + if (tabIndex >= GuiConstants.TAB_TEMP_AREA) return busSlotLimit; + + return cellSlotLimit; + } + /** * Get the filter state for a specific filter. * @param filter The filter type diff --git a/src/main/java/com/cellterminal/config/CellTerminalServerConfig.java b/src/main/java/com/cellterminal/config/CellTerminalServerConfig.java index bbf086f..c541db0 100644 --- a/src/main/java/com/cellterminal/config/CellTerminalServerConfig.java +++ b/src/main/java/com/cellterminal/config/CellTerminalServerConfig.java @@ -297,6 +297,7 @@ public void syncFromConfig() { this.tabTerminalEnabled = this.tabTerminalEnabledProperty.getBoolean(); this.tabInventoryEnabled = this.tabInventoryEnabledProperty.getBoolean(); this.tabPartitionEnabled = this.tabPartitionEnabledProperty.getBoolean(); + this.tabTempAreaEnabled = this.tabTempAreaEnabledProperty.getBoolean(); this.tabStorageBusInventoryEnabled = this.tabStorageBusInventoryEnabledProperty.getBoolean(); this.tabStorageBusPartitionEnabled = this.tabStorageBusPartitionEnabledProperty.getBoolean(); this.tabNetworkToolsEnabled = this.tabNetworkToolsEnabledProperty.getBoolean(); @@ -363,34 +364,11 @@ public ConfigCategory getCategory(String name) { // Tab getters - public boolean isTabTerminalEnabled() { - return tabTerminalEnabled; - } - - public boolean isTabInventoryEnabled() { - return tabInventoryEnabled; - } - - public boolean isTabPartitionEnabled() { - return tabPartitionEnabled; - } - public boolean isTabTempAreaEnabled() { return tabTempAreaEnabled; } - public boolean isTabStorageBusInventoryEnabled() { - return tabStorageBusInventoryEnabled; - } - - public boolean isTabStorageBusPartitionEnabled() { - return tabStorageBusPartitionEnabled; - } - - public boolean isTabNetworkToolsEnabled() { - return tabNetworkToolsEnabled; - } - + // TODO: index is not really realiable /** * Check if a specific tab is enabled by its index. * @param tabIndex The tab index (0-6) diff --git a/src/main/java/com/cellterminal/container/ContainerCellTerminalBase.java b/src/main/java/com/cellterminal/container/ContainerCellTerminalBase.java index d06a728..c956a35 100644 --- a/src/main/java/com/cellterminal/container/ContainerCellTerminalBase.java +++ b/src/main/java/com/cellterminal/container/ContainerCellTerminalBase.java @@ -60,6 +60,7 @@ import com.cellterminal.network.PacketPartitionAction; import com.cellterminal.network.PacketStorageBusPartitionAction; import com.cellterminal.network.PacketSubnetListUpdate; +import com.cellterminal.network.PacketSubnetPartitionAction; import com.cellterminal.network.PacketTempCellAction; import com.cellterminal.network.PacketTempCellPartitionAction; import com.cellterminal.util.PlayerMessageHelper; @@ -87,6 +88,7 @@ public abstract class ContainerCellTerminalBase extends AEBaseContainer { // Slot limits for controlling how many types are serialized (synced from client) protected int cellSlotLimit = Integer.MAX_VALUE; protected int busSlotLimit = Integer.MAX_VALUE; + protected int subnetSlotLimit = 64; // Default to 64 for subnet inventory // Tick counter for storage bus polling (only poll every N ticks when on storage bus tab) protected int storageBusPollCounter = 0; @@ -267,7 +269,6 @@ protected void handleStorageBusPolling() { * Triggers storage bus refresh only on first switch to a storage bus tab. */ public void setActiveTab(int tab) { - boolean wasOnStorageBusTab = (activeTab == GuiConstants.TAB_STORAGE_BUS_INVENTORY || activeTab == GuiConstants.TAB_STORAGE_BUS_PARTITION); boolean isOnStorageBusTab = (tab == GuiConstants.TAB_STORAGE_BUS_INVENTORY || tab == GuiConstants.TAB_STORAGE_BUS_PARTITION); this.activeTab = tab; @@ -284,14 +285,17 @@ public void setActiveTab(int tab) { * Triggers a full refresh so the new limits take effect. * @param cellLimit Limit for cell contents (-1 for unlimited) * @param busLimit Limit for storage bus contents (-1 for unlimited) + * @param subnetLimit Limit for subnet inventory contents (-1 for unlimited) */ - public void setSlotLimits(int cellLimit, int busLimit) { + public void setSlotLimits(int cellLimit, int busLimit, int subnetLimit) { // Convert -1 (unlimited) to MAX_VALUE for easier comparison this.cellSlotLimit = cellLimit < 0 ? Integer.MAX_VALUE : cellLimit; this.busSlotLimit = busLimit < 0 ? Integer.MAX_VALUE : busLimit; + this.subnetSlotLimit = subnetLimit < 0 ? Integer.MAX_VALUE : subnetLimit; // Trigger refresh to apply new limits this.needsFullRefresh = true; + this.needsSubnetRefresh = true; } /** @@ -563,10 +567,28 @@ public void handleUpgradeCell(EntityPlayer player, long storageId, int cellSlot, ItemStack upgradeStack = fromSlot >= 0 ? player.inventory.getStackInSlot(fromSlot) : player.inventory.getItemStack(); - // For shift-click, iterate through all cells to find one that actually accepts this upgrade - // The client's guess might be wrong if the cell doesn't support this specific upgrade type - // Sort by distance to match visual order on client + // For shift-click with a specific storage ID: iterate cells within that storage + // For shift-click without storage ID: iterate all storages and cells + // This handles: (1) shift-click on empty space, (2) click on storage header if (shiftClick) { + // If a specific storage is targeted, only iterate its cells + StorageTracker targetTracker = this.byId.get(storageId); + if (targetTracker != null) { + IItemHandler cellInventory = CellDataHandler.getCellInventory(targetTracker.storage); + if (cellInventory != null) { + for (int slot = 0; slot < cellInventory.getSlots(); slot++) { + if (CellActionHandler.upgradeCell(targetTracker.storage, targetTracker.tile, slot, upgradeStack, player, fromSlot)) { + this.needsFullRefresh = true; + + return; + } + } + } + + return; // Targeted storage but no cell accepted + } + + // No specific storage: iterate all storages and cells (global shift-click) int terminalDim = getTerminalDimension(); List sortedTrackers = new ArrayList<>(this.byId.values()); sortedTrackers.sort(createTrackerComparator(BlockPos.ORIGIN, terminalDim)); @@ -596,13 +618,6 @@ public void handleUpgradeCell(EntityPlayer player, long storageId, int cellSlot, } } - /** - * Handle upgrade cell requests (legacy signature for compatibility). - */ - public void handleUpgradeCell(EntityPlayer player, long storageId, int cellSlot, boolean shiftClick) { - handleUpgradeCell(player, storageId, cellSlot, shiftClick, -1); - } - /** * Handle upgrade storage bus requests from client. * Takes the upgrade from the player's held item or a specific inventory slot and inserts it into the storage bus. @@ -728,6 +743,23 @@ public void handleExtractUpgrade(EntityPlayer player, PacketExtractUpgrade.Targe upgradesInv = cellItem.getUpgradesInventory(cellStack); tile = tracker.tile; + } else if (targetType == PacketExtractUpgrade.TargetType.TEMP_CELL) { + // Extract upgrade from a temp area cell + IItemHandlerModifiable tempInv = getTempCellInventory(); + if (tempInv == null) return; + + int tempSlotIndex = (int) targetId; + if (tempSlotIndex < 0 || tempSlotIndex >= tempInv.getSlots()) return; + + ItemStack cellStack = tempInv.getStackInSlot(tempSlotIndex); + if (cellStack.isEmpty() || !(cellStack.getItem() instanceof ICellWorkbenchItem)) return; + + ICellWorkbenchItem cellItem = (ICellWorkbenchItem) cellStack.getItem(); + if (!cellItem.isEditable(cellStack)) return; + + upgradesInv = cellItem.getUpgradesInventory(cellStack); + // No tile for temp cells - NBT update is handled via setStackInSlot below + tile = null; } else { StorageBusTracker tracker = this.storageBusById.get(targetId); if (tracker == null) return; @@ -774,6 +806,19 @@ public void handleExtractUpgrade(EntityPlayer player, PacketExtractUpgrade.Targe if (tile != null) tile.markDirty(); if (targetType == PacketExtractUpgrade.TargetType.CELL) { + this.needsFullRefresh = true; + } else if (targetType == PacketExtractUpgrade.TargetType.TEMP_CELL) { + // Temp cell upgrade extraction: update the cell in temp inventory + // (NBT was modified when the upgrade was extracted via IItemHandler) + IItemHandlerModifiable tempInv = getTempCellInventory(); + if (tempInv != null) { + int tempSlotIndex = (int) targetId; + if (tempSlotIndex >= 0 && tempSlotIndex < tempInv.getSlots()) { + ItemStack cellStack = tempInv.getStackInSlot(tempSlotIndex); + tempInv.setStackInSlot(tempSlotIndex, cellStack); + } + } + this.needsFullRefresh = true; } else { this.needsStorageBusRefresh = true; @@ -1057,7 +1102,7 @@ protected void regenSubnetList() { int playerId = this.getPlayerInv().player.getEntityId(); NBTTagCompound data = new NBTTagCompound(); - data.setTag("subnets", SubnetDataHandler.collectSubnets(this.grid, this.subnetById, playerId)); + data.setTag("subnets", SubnetDataHandler.collectSubnets(this.grid, this.subnetById, playerId, this.subnetSlotLimit)); // Send subnet list as a separate packet if (this.getPlayerInv().player instanceof EntityPlayerMP) { @@ -1077,7 +1122,7 @@ public void handleSubnetAction(long subnetId, SubnetDataHandler.SubnetAction act // Ensure subnet trackers are populated before handling action // This can happen if the client sends an action before the server has scanned subnets if (this.subnetById.isEmpty() && this.grid != null) { - SubnetDataHandler.collectSubnets(this.grid, this.subnetById, player.getEntityId()); + SubnetDataHandler.collectSubnets(this.grid, this.subnetById, player.getEntityId(), this.subnetSlotLimit); } if (SubnetDataHandler.handleSubnetAction(this.subnetById, subnetId, action, data, player)) { @@ -1086,6 +1131,32 @@ public void handleSubnetAction(long subnetId, SubnetDataHandler.SubnetAction act } } + /** + * Handle subnet connection partition modification from client. + * Finds the storage bus at (pos, side) within the subnet and modifies its config. + */ + public void handleSubnetPartitionAction(long subnetId, long pos, int side, + PacketSubnetPartitionAction.Action action, + int partitionSlot, ItemStack itemStack) { + if (!CellTerminalServerConfig.getInstance().isPartitionEditEnabled()) { + PlayerMessageHelper.error(this.getPlayerInv().player, "cellterminal.error.partition_edit_disabled"); + + return; + } + + EntityPlayer player = this.getPlayerInv().player; + + // Ensure subnet trackers are populated + if (this.subnetById.isEmpty() && this.grid != null) { + SubnetDataHandler.collectSubnets(this.grid, this.subnetById, player.getEntityId(), this.subnetSlotLimit); + } + + if (SubnetDataHandler.handleSubnetPartitionAction(this.subnetById, subnetId, pos, side, + action, partitionSlot, itemStack)) { + this.needsSubnetRefresh = true; + } + } + /** * Switch the terminal view to a different network (main or subnet). * @@ -1121,20 +1192,6 @@ public void switchNetwork(long networkId) { this.needsStorageBusRefresh = true; } - /** - * Get the current network ID (0 = main, >0 = subnet). - */ - public long getCurrentNetworkId() { - return currentNetworkId; - } - - /** - * Check if currently viewing a subnet. - */ - public boolean isViewingSubnet() { - return currentNetworkId != 0; - } - /** * Get the display name of the current network (main network or subnet). *

@@ -1163,7 +1220,7 @@ private String getSubnetName(IGrid grid) { // Find the primary interface host (same logic as SubnetDataHandler) IInterfaceHost interfaceHost = SubnetDataHandler.findPrimaryInterfaceHost(grid); - if (interfaceHost != null && interfaceHost instanceof ICustomNameObject) { + if (interfaceHost instanceof ICustomNameObject) { ICustomNameObject nameable = (ICustomNameObject) interfaceHost; if (nameable.hasCustomInventoryName()) return nameable.getCustomInventoryName(); } diff --git a/src/main/java/com/cellterminal/container/ContainerWirelessCellTerminal.java b/src/main/java/com/cellterminal/container/ContainerWirelessCellTerminal.java index b7f0635..012949c 100644 --- a/src/main/java/com/cellterminal/container/ContainerWirelessCellTerminal.java +++ b/src/main/java/com/cellterminal/container/ContainerWirelessCellTerminal.java @@ -77,8 +77,6 @@ private ItemStack getTerminalStack() { return getPlayerInv().getStackInSlot(slot); } - - @Override protected boolean canSendUpdates() { ItemStack currentStack = getTerminalStack(); diff --git a/src/main/java/com/cellterminal/container/WirelessTempCellInventory.java b/src/main/java/com/cellterminal/container/WirelessTempCellInventory.java index b02d9eb..52ab7f2 100644 --- a/src/main/java/com/cellterminal/container/WirelessTempCellInventory.java +++ b/src/main/java/com/cellterminal/container/WirelessTempCellInventory.java @@ -6,6 +6,7 @@ import appeng.api.storage.ICellWorkbenchItem; +import com.cellterminal.integration.AE2WUTIntegration; import com.cellterminal.items.ItemWirelessCellTerminal; @@ -16,6 +17,8 @@ *

* Unlike the tile-based PartCellTerminal which has its own inventory, * this wrapper reads/writes directly to the wireless terminal ItemStack's NBT. + *

+ * Works with both Wireless Cell Terminal and Wireless Universal Terminal (WUT). */ public class WirelessTempCellInventory implements IItemHandlerModifiable { @@ -46,13 +49,17 @@ private ItemStack getTerminalStack() { } /** - * Get the wireless terminal item instance. + * Check if the terminal stack is valid (Wireless Cell Terminal or WUT with Cell Terminal mode). */ - private ItemWirelessCellTerminal getTerminalItem() { + private boolean isValidTerminal() { ItemStack stack = getTerminalStack(); - if (stack.isEmpty() || !(stack.getItem() instanceof ItemWirelessCellTerminal)) return null; + if (stack.isEmpty()) return false; + + // Check for Wireless Cell Terminal + if (stack.getItem() instanceof ItemWirelessCellTerminal) return true; - return (ItemWirelessCellTerminal) stack.getItem(); + // Check for WUT with Cell Terminal mode + return AE2WUTIntegration.isWirelessUniversalTerminal(stack); } /** @@ -69,30 +76,28 @@ private ItemStack getBaubleStack() { @Override public int getSlots() { - ItemWirelessCellTerminal item = getTerminalItem(); + if (!isValidTerminal()) return 0; - return item != null ? item.getMaxTempCells() : 0; + return ItemWirelessCellTerminal.getMaxTempCells(); } @Override public ItemStack getStackInSlot(int slot) { - ItemWirelessCellTerminal item = getTerminalItem(); - if (item == null) return ItemStack.EMPTY; + if (!isValidTerminal()) return ItemStack.EMPTY; - return item.getTempCell(getTerminalStack(), slot); + return ItemWirelessCellTerminal.getTempCell(getTerminalStack(), slot); } @Override public void setStackInSlot(int slot, ItemStack stack) { - ItemWirelessCellTerminal item = getTerminalItem(); - if (item == null) return; + if (!isValidTerminal()) return; - item.setTempCell(getTerminalStack(), slot, stack); + ItemWirelessCellTerminal.setTempCell(getTerminalStack(), slot, stack); } @Override public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { - if (stack.isEmpty()) return ItemStack.EMPTY; + if (stack.isEmpty() || !isValidTerminal()) return stack; // Only accept valid cell items if (!(stack.getItem() instanceof ICellWorkbenchItem)) return stack; @@ -107,6 +112,8 @@ public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) { @Override public ItemStack extractItem(int slot, int amount, boolean simulate) { + if (!isValidTerminal()) return ItemStack.EMPTY; + ItemStack existing = getStackInSlot(slot); if (existing.isEmpty() || amount <= 0) return ItemStack.EMPTY; diff --git a/src/main/java/com/cellterminal/container/handler/CellDataHandler.java b/src/main/java/com/cellterminal/container/handler/CellDataHandler.java index 124e7d7..65f3b99 100644 --- a/src/main/java/com/cellterminal/container/handler/CellDataHandler.java +++ b/src/main/java/com/cellterminal/container/handler/CellDataHandler.java @@ -33,18 +33,6 @@ */ public class CellDataHandler { - /** - * Create NBT data for a storage device (ME Drive or ME Chest). - * @param storage The storage device - * @param defaultName The default localization key for the storage name - * @param trackerCallback Callback to register the storage tracker - * @return NBT data for the storage - */ - public static NBTTagCompound createStorageData(IChestOrDrive storage, String defaultName, - StorageTrackerCallback trackerCallback) { - return createStorageData(storage, defaultName, trackerCallback, Integer.MAX_VALUE); - } - /** * Create NBT data for a storage device (ME Drive or ME Chest). * @param storage The storage device @@ -99,13 +87,6 @@ public static NBTTagCompound createStorageData(IChestOrDrive storage, String def return storageData; } - /** - * Create NBT data for a single cell. - */ - public static NBTTagCompound createCellData(int slot, ItemStack cellStack, int status) { - return createCellData(slot, cellStack, status, Integer.MAX_VALUE); - } - /** * Create NBT data for a single cell. * @param slot The slot index in the storage device @@ -310,9 +291,7 @@ public static IItemHandler getCellInventory(IChestOrDrive storage) { // to return only the cell inventory portion. if (storage instanceof TileChest) { TileChest chest = (TileChest) storage; - IItemHandler wrapper = new TileChestCellInventoryWrapper(chest); - - return wrapper; + return new TileChestCellInventoryWrapper(chest); } // Modular handling of Storage Drive-like tiles via AENetworkInvTile @@ -334,11 +313,9 @@ public static IItemHandler getCellInventory(IChestOrDrive storage) { * This wrapper maps slot 0 to the cell slot (slot 1 of the internal inventory). */ private static class TileChestCellInventoryWrapper implements IItemHandler { - private final TileChest chest; private final IItemHandler internalInv; public TileChestCellInventoryWrapper(TileChest chest) { - this.chest = chest; this.internalInv = chest.getInternalInventory(); } diff --git a/src/main/java/com/cellterminal/container/handler/NetworkToolActionHandler.java b/src/main/java/com/cellterminal/container/handler/NetworkToolActionHandler.java index 6f8c3fa..34ac6d6 100644 --- a/src/main/java/com/cellterminal/container/handler/NetworkToolActionHandler.java +++ b/src/main/java/com/cellterminal/container/handler/NetworkToolActionHandler.java @@ -2,6 +2,7 @@ import java.math.BigInteger; import java.util.ArrayList; +import java.util.Comparator; import java.util.EnumMap; import java.util.HashSet; import java.util.List; @@ -431,9 +432,7 @@ private static RedistributionResult redistributeCellTypeWithSimulation(CellType // Sort by probed capacity (SMALLEST first for best-fit algorithm) // Best-fit assigns items to the smallest cell that can hold them, // preventing huge cells from being wasted on small stacks - targetCells.sort((a, b) -> Long.compare( - cellCapacities.getOrDefault(a, 0L), - cellCapacities.getOrDefault(b, 0L))); + targetCells.sort(Comparator.comparingLong(a -> cellCapacities.getOrDefault(a, 0L))); // Log sorted order CellTerminal.LOGGER.info("[AttributeUnique] Cells sorted by capacity (smallest first for best-fit):"); diff --git a/src/main/java/com/cellterminal/container/handler/StorageBusDataHandler.java b/src/main/java/com/cellterminal/container/handler/StorageBusDataHandler.java index 471adcf..70a2359 100644 --- a/src/main/java/com/cellterminal/container/handler/StorageBusDataHandler.java +++ b/src/main/java/com/cellterminal/container/handler/StorageBusDataHandler.java @@ -44,6 +44,7 @@ import com.cellterminal.integration.StorageDrawersIntegration; import com.cellterminal.integration.ThaumicEnergisticsIntegration; import com.cellterminal.network.PacketStorageBusPartitionAction; +import com.cellterminal.network.PacketSubnetPartitionAction; import com.cellterminal.integration.storagebus.StorageBusScannerRegistry; @@ -375,9 +376,8 @@ private static int computeAvailableSlotsFrom(NBTTagCompound busData, int capacit int max = busData.hasKey("maxConfigSlots") ? busData.getInteger("maxConfigSlots") : StorageBusInfo.MAX_CONFIG_SLOTS; int raw = base + perUpg * Math.max(0, capacityUpgrades); - if (raw > max) return max; + return Math.min(raw, max); - return raw; } private static void addUpgradesData(NBTTagCompound busData, IItemHandler upgradesInv) { @@ -420,6 +420,83 @@ public static boolean handlePartitionAction(StorageBusTracker tracker, return false; } + /** + * Execute a subnet partition action on a storage bus. + * Converts the subnet action enum to storage bus action enum and delegates to the existing logic. + */ + public static void executeSubnetPartitionAction(IItemHandler configInv, int slotsToUse, + PacketSubnetPartitionAction.Action action, + int partitionSlot, ItemStack itemStack, + PartStorageBus bus) { + // Map subnet action to storage bus action (they have the same operations) + PacketStorageBusPartitionAction.Action mappedAction; + switch (action) { + case ADD_ITEM: + mappedAction = PacketStorageBusPartitionAction.Action.ADD_ITEM; + break; + case REMOVE_ITEM: + mappedAction = PacketStorageBusPartitionAction.Action.REMOVE_ITEM; + break; + case TOGGLE_ITEM: + mappedAction = PacketStorageBusPartitionAction.Action.TOGGLE_ITEM; + break; + case SET_ALL_FROM_CONTENTS: + mappedAction = PacketStorageBusPartitionAction.Action.SET_ALL_FROM_CONTENTS; + break; + case CLEAR_ALL: + mappedAction = PacketStorageBusPartitionAction.Action.CLEAR_ALL; + break; + default: + return; + } + + executeItemPartitionAction(configInv, slotsToUse, mappedAction, partitionSlot, itemStack, bus); + } + + /** + * Execute a subnet partition action on a fluid storage bus. + * Converts the subnet action enum to storage bus action enum and delegates to fluid partition logic. + * + * @return true if the partition was modified + */ + public static boolean executeSubnetFluidPartitionAction(PartFluidStorageBus bus, + PacketSubnetPartitionAction.Action action, + int partitionSlot, ItemStack itemStack) { + IFluidHandler configInv = bus.getFluidInventoryByName("config"); + if (configInv == null) return false; + + int slotsToUse = StorageBusInfo.calculateAvailableSlots(bus.getInstalledUpgrades(Upgrades.CAPACITY)); + FluidStack fluid = extractFluidFromItem(itemStack); + + PacketStorageBusPartitionAction.Action mappedAction; + switch (action) { + case ADD_ITEM: + mappedAction = PacketStorageBusPartitionAction.Action.ADD_ITEM; + break; + case REMOVE_ITEM: + mappedAction = PacketStorageBusPartitionAction.Action.REMOVE_ITEM; + break; + case TOGGLE_ITEM: + mappedAction = PacketStorageBusPartitionAction.Action.TOGGLE_ITEM; + break; + case SET_ALL_FROM_CONTENTS: + mappedAction = PacketStorageBusPartitionAction.Action.SET_ALL_FROM_CONTENTS; + break; + case CLEAR_ALL: + mappedAction = PacketStorageBusPartitionAction.Action.CLEAR_ALL; + break; + default: + return false; + } + + executeFluidPartitionAction(configInv, slotsToUse, mappedAction, partitionSlot, fluid, bus); + + TileEntity hostTile = bus.getHost().getTile(); + if (hostTile != null) hostTile.markDirty(); + + return true; + } + private static boolean handleItemBusPartition(PartStorageBus bus, PacketStorageBusPartitionAction.Action action, int partitionSlot, ItemStack itemStack, @@ -719,10 +796,10 @@ public static boolean insertUpgrade(StorageBusTracker tracker, ItemStack upgrade } private static ItemStack getBlockAsItemStack(IBlockState state, World world, BlockPos pos) { - if (state == null || state.getBlock() == null) return ItemStack.EMPTY; + if (state == null) return ItemStack.EMPTY; Item item = Item.getItemFromBlock(state.getBlock()); - if (item != null && item != Items.AIR) { + if (item != Items.AIR) { int meta = state.getBlock().damageDropped(state); return new ItemStack(item, 1, meta); diff --git a/src/main/java/com/cellterminal/container/handler/SubnetDataHandler.java b/src/main/java/com/cellterminal/container/handler/SubnetDataHandler.java index 0aa53ff..7bf8eb0 100644 --- a/src/main/java/com/cellterminal/container/handler/SubnetDataHandler.java +++ b/src/main/java/com/cellterminal/container/handler/SubnetDataHandler.java @@ -5,20 +5,39 @@ import java.util.Map; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.EnumFacing; +import net.minecraft.util.math.BlockPos; +import net.minecraftforge.items.IItemHandler; + +import appeng.api.AEApi; import appeng.api.networking.IGrid; import appeng.api.networking.IGridNode; +import appeng.api.networking.storage.IStorageGrid; +import appeng.api.parts.IPart; +import appeng.api.parts.IPartHost; +import appeng.api.storage.IMEMonitor; +import appeng.api.storage.channels.IFluidStorageChannel; +import appeng.api.storage.channels.IItemStorageChannel; +import appeng.api.storage.data.IAEFluidStack; +import appeng.api.storage.data.IAEItemStack; +import appeng.api.storage.data.IItemList; +import appeng.api.util.AEPartLocation; import appeng.api.util.DimensionalCoord; import appeng.helpers.ICustomNameObject; import appeng.helpers.IInterfaceHost; import appeng.parts.misc.PartInterface; +import appeng.fluids.parts.PartFluidStorageBus; +import appeng.parts.misc.PartStorageBus; import appeng.tile.misc.TileInterface; +import appeng.util.helpers.ItemHandlerUtil; import com.cellterminal.integration.subnet.SubnetScannerRegistry; +import com.cellterminal.network.PacketSubnetPartitionAction; /** @@ -57,7 +76,7 @@ public SubnetTracker(long id, IGrid targetGrid) { */ public void addConnection(Object part, TileEntity hostTile) { connectionParts.add(part); - if (!hostTiles.contains(hostTile)) hostTiles.add(hostTile); + hostTiles.add(hostTile); // Must stay parallel with connectionParts isOutbound.add(true); connectionSides.add(null); // Side is derived from the part } @@ -67,7 +86,7 @@ public void addConnection(Object part, TileEntity hostTile) { */ public void addInboundConnection(Object part, TileEntity hostTile, EnumFacing side) { connectionParts.add(part); - if (!hostTiles.contains(hostTile)) hostTiles.add(hostTile); + hostTiles.add(hostTile); // Must stay parallel with connectionParts isOutbound.add(false); connectionSides.add(side); } @@ -79,15 +98,16 @@ public void addInboundConnection(Object part, TileEntity hostTile, EnumFacing si * @param grid The main ME network grid to scan * @param trackerMap Map to populate with subnet trackers (keyed by subnet ID) * @param playerId The player ID for security permission checks + * @param slotLimit Maximum number of inventory item types to include per subnet * @return NBTTagList containing all subnet data */ - public static NBTTagList collectSubnets(IGrid grid, Map trackerMap, int playerId) { + public static NBTTagList collectSubnets(IGrid grid, Map trackerMap, int playerId, int slotLimit) { NBTTagList subnetList = new NBTTagList(); if (grid == null) return subnetList; // Delegate to registered scanners - SubnetScannerRegistry.scanAll(grid, subnetList, trackerMap, playerId); + SubnetScannerRegistry.scanAll(grid, subnetList, trackerMap, playerId, slotLimit); return subnetList; } @@ -239,6 +259,220 @@ public static IInterfaceHost findPrimaryInterfaceHost(IGrid grid) { return null; } + /** + * Handle subnet connection partition modification. + * Finds the storage bus at (pos, side) within the subnet and modifies its config. + * + * @param trackerMap Current subnet trackers + * @param subnetId The subnet ID containing the connection + * @param pos The packed block position of the connection host tile + * @param side The side ordinal of the storage bus + * @param action The partition action to perform + * @param partitionSlot The target slot index (-1 for bulk actions) + * @param itemStack The item for ADD/TOGGLE actions + * @return true if the partition was modified + */ + public static boolean handleSubnetPartitionAction(Map trackerMap, + long subnetId, long pos, int side, + PacketSubnetPartitionAction.Action action, + int partitionSlot, ItemStack itemStack) { + SubnetTracker tracker = trackerMap.get(subnetId); + if (tracker == null) return false; + + // SET_ALL_FROM_SUBNET_INVENTORY requires special handling: + // query the subnet's ME storage and fill the bus config from it + if (action == PacketSubnetPartitionAction.Action.SET_ALL_FROM_SUBNET_INVENTORY) { + return handleSetAllFromSubnetInventory(tracker, pos, side); + } + + Object busPart = findBusForConnection(tracker, pos, side); + if (busPart == null) return false; + + // Delegate to the appropriate handler based on bus type + if (busPart instanceof PartStorageBus) { + return handleItemBusSubnetPartition((PartStorageBus) busPart, action, partitionSlot, itemStack); + } else if (busPart instanceof PartFluidStorageBus) { + return StorageBusDataHandler.executeSubnetFluidPartitionAction( + (PartFluidStorageBus) busPart, action, partitionSlot, itemStack); + } + + return false; + } + + private static boolean handleItemBusSubnetPartition(PartStorageBus bus, + PacketSubnetPartitionAction.Action action, + int partitionSlot, ItemStack itemStack) { + IItemHandler configInv = bus.getInventoryByName("config"); + if (configInv == null) return false; + + StorageBusDataHandler.executeSubnetPartitionAction(configInv, configInv.getSlots(), action, + partitionSlot, itemStack, bus); + + TileEntity hostTile = bus.getHost().getTile(); + if (hostTile != null) hostTile.markDirty(); + + return true; + } + + /** + * Handle SET_ALL_FROM_SUBNET_INVENTORY: query the subnet's ME storage grid + * and fill the storage bus config with item types found in the subnet's inventory. + */ + private static boolean handleSetAllFromSubnetInventory(SubnetTracker tracker, long pos, int side) { + Object busPart = findBusForConnection(tracker, pos, side); + if (busPart == null) return false; + + IGrid subnetGrid = tracker.targetGrid; + IStorageGrid storageGrid = subnetGrid.getCache(IStorageGrid.class); + if (storageGrid == null) return false; + + if (busPart instanceof PartStorageBus) { + return fillItemBusFromSubnetInventory((PartStorageBus) busPart, storageGrid); + } else if (busPart instanceof PartFluidStorageBus) { + return fillFluidBusFromSubnetInventory((PartFluidStorageBus) busPart, storageGrid); + } + + return false; + } + + /** + * Fill an item storage bus config from the subnet's ME item inventory. + */ + private static boolean fillItemBusFromSubnetInventory(PartStorageBus bus, IStorageGrid storageGrid) { + IItemHandler configInv = bus.getInventoryByName("config"); + if (configInv == null) return false; + + int slotsToUse = configInv.getSlots(); + + // Clear existing config + for (int i = 0; i < slotsToUse; i++) { + ItemHandlerUtil.setStackInSlot(configInv, i, ItemStack.EMPTY); + } + + // Query the subnet's item inventory + IItemStorageChannel itemChannel = AEApi.instance().storage().getStorageChannel(IItemStorageChannel.class); + IMEMonitor itemMonitor = storageGrid.getInventory(itemChannel); + IItemList items = itemMonitor.getStorageList(); + + int slot = 0; + for (IAEItemStack aeStack : items) { + if (slot >= slotsToUse) break; + if (aeStack.getStackSize() <= 0) continue; + + ItemStack configStack = aeStack.createItemStack(); + configStack.setCount(1); + ItemHandlerUtil.setStackInSlot(configInv, slot++, configStack); + } + + TileEntity hostTile = bus.getHost().getTile(); + if (hostTile != null) hostTile.markDirty(); + + return true; + } + + /** + * Fill a fluid storage bus config from the subnet's ME fluid inventory. + */ + private static boolean fillFluidBusFromSubnetInventory(PartFluidStorageBus bus, IStorageGrid storageGrid) { + // TODO: Implement fluid bus partition from subnet inventory. + // This requires accessing the fluid config via IAEFluidTank and + // IFluidStorageChannel, similar to executeFluidPartitionAction logic. + return false; + } + + /** + * Find the storage bus (item or fluid) for a connection described by (pos, side). + * For outbound connections, the storage bus is the connection part itself. + * For inbound connections, the storage bus is on the subnet side (offset from the interface). + * + * @return PartStorageBus or PartFluidStorageBus, or null if not found + */ + private static Object findBusForConnection(SubnetTracker tracker, long pos, int side) { + BlockPos targetPos = BlockPos.fromLong(pos); + + for (int i = 0; i < tracker.connectionParts.size(); i++) { + Object part = tracker.connectionParts.get(i); + boolean outbound = i < tracker.isOutbound.size() && tracker.isOutbound.get(i); + + if (outbound && part instanceof PartStorageBus) { + PartStorageBus bus = (PartStorageBus) part; + TileEntity hostTile = bus.getHost().getTile(); + + if (hostTile != null && hostTile.getPos().equals(targetPos) + && bus.getSide().ordinal() == side) { + return bus; + } + } else if (outbound && part instanceof PartFluidStorageBus) { + PartFluidStorageBus bus = (PartFluidStorageBus) part; + TileEntity hostTile = bus.getHost().getTile(); + + if (hostTile != null && hostTile.getPos().equals(targetPos) + && bus.getSide().ordinal() == side) { + return bus; + } + } else if (!outbound && part instanceof TileInterface) { + // Inbound: full-block interface on the main network, storage bus is on subnet side + TileInterface iface = (TileInterface) part; + EnumFacing connSide = i < tracker.connectionSides.size() + ? tracker.connectionSides.get(i) : null; + + // The pos/side sent by the client match the interface tile + connection side + if (!iface.getPos().equals(targetPos) || connSide == null) continue; + if (connSide.ordinal() != side) continue; + + // Find the storage bus on the subnet side + BlockPos remoteTilePos = iface.getPos().offset(connSide); + TileEntity remoteTile = iface.getWorld().getTileEntity(remoteTilePos); + if (!(remoteTile instanceof IPartHost)) continue; + + IPartHost partHost = (IPartHost) remoteTile; + EnumFacing oppositeDir = connSide.getOpposite(); + + for (AEPartLocation loc : AEPartLocation.SIDE_LOCATIONS) { + if (loc.getFacing() != oppositeDir) continue; + + IPart remotePart = partHost.getPart(loc); + + // Return whichever bus type is found on the remote side + if (remotePart instanceof PartStorageBus + || remotePart instanceof PartFluidStorageBus) { + return remotePart; + } + } + } else if (!outbound && part instanceof PartInterface) { + // Inbound: cable-attached interface on the main network, storage bus is on subnet side + PartInterface iface = (PartInterface) part; + TileEntity ifaceTile = iface.getTileEntity(); + EnumFacing connSide = i < tracker.connectionSides.size() + ? tracker.connectionSides.get(i) : null; + + if (ifaceTile == null || !ifaceTile.getPos().equals(targetPos) || connSide == null) continue; + if (connSide.ordinal() != side) continue; + + // Find the storage bus on the subnet side + BlockPos remoteTilePos = ifaceTile.getPos().offset(connSide); + TileEntity remoteTile = ifaceTile.getWorld().getTileEntity(remoteTilePos); + if (!(remoteTile instanceof IPartHost)) continue; + + IPartHost partHost = (IPartHost) remoteTile; + EnumFacing oppositeDir = connSide.getOpposite(); + + for (AEPartLocation loc : AEPartLocation.SIDE_LOCATIONS) { + if (loc.getFacing() != oppositeDir) continue; + + IPart remotePart = partHost.getPart(loc); + + if (remotePart instanceof PartStorageBus + || remotePart instanceof PartFluidStorageBus) { + return remotePart; + } + } + } + } + + return null; + } + /** * Actions that can be performed on subnets. */ diff --git a/src/main/java/com/cellterminal/container/handler/TempCellActionHandler.java b/src/main/java/com/cellterminal/container/handler/TempCellActionHandler.java index 6b8c585..0a1cd0c 100644 --- a/src/main/java/com/cellterminal/container/handler/TempCellActionHandler.java +++ b/src/main/java/com/cellterminal/container/handler/TempCellActionHandler.java @@ -23,8 +23,9 @@ public class TempCellActionHandler { /** - * Handle temp cell action (insert, extract, send). + * Handle temp cell action (insert, extract, send, upgrade). * @param toInventory For EXTRACT: if true, send directly to inventory instead of cursor (shift-click) + * For UPGRADE: if true, server finds first cell that can accept */ public static void handleAction(ContainerCellTerminalBase container, PacketTempCellAction.Action action, @@ -40,6 +41,12 @@ public static void handleAction(ContainerCellTerminalBase container, case SEND: handleSend(container, tempSlotIndex, player); break; + case UPGRADE: + handleUpgrade(container, tempSlotIndex, playerSlotIndex, player, toInventory); + break; + case SWAP: + handleSwap(container, tempSlotIndex, player); + break; } } @@ -229,6 +236,51 @@ private static void handleExtract(ContainerCellTerminalBase container, int tempS markDirty(container); } + /** + * Swap the cell in a temp area slot with the cell on the player's cursor. + * The existing cell goes to the cursor, and the cursor cell goes into the slot. + */ + private static void handleSwap(ContainerCellTerminalBase container, int tempSlotIndex, EntityPlayer player) { + IItemHandlerModifiable tempInv = getTempCellInventory(container); + if (tempInv == null) return; + + if (tempSlotIndex < 0 || tempSlotIndex >= tempInv.getSlots()) return; + + ItemStack existingCell = tempInv.getStackInSlot(tempSlotIndex); + if (existingCell.isEmpty()) return; + + ItemStack cursorStack = player.inventory.getItemStack(); + if (cursorStack.isEmpty()) return; + + // Validate the cursor item is a valid cell + if (!(cursorStack.getItem() instanceof ICellWorkbenchItem)) { + PlayerMessageHelper.error(player, "gui.cellterminal.temp_area.not_cell"); + + return; + } + + // Only swap single items (cells don't stack, but just in case) + ItemStack newCell = cursorStack.splitStack(1); + + // Put existing cell on cursor, new cell in slot + tempInv.setStackInSlot(tempSlotIndex, newCell); + + // Give the old cell back to the cursor + if (cursorStack.isEmpty()) { + // Cursor is now empty after splitting - just set it to the existing cell + player.inventory.setItemStack(existingCell); + } else { + // Cursor still has items (shouldn't happen for cells, but handle gracefully) + // Try to add the existing cell to inventory, or drop it + if (!player.inventory.addItemStackToInventory(existingCell)) { + player.dropItem(existingCell, false); + } + } + + ((EntityPlayerMP) player).updateHeldItem(); + markDirty(container); + } + /** * Send a temp cell to the first available slot in the ME network. */ @@ -257,6 +309,89 @@ private static void handleSend(ContainerCellTerminalBase container, int tempSlot PlayerMessageHelper.success(player, "cellterminal.temp_area.sent", gridName); } + /** + * Insert an upgrade card into a temp cell. + * + * @param tempSlotIndex The temp slot to upgrade (-1 = find first that accepts) + * @param fromSlot The player inventory slot to take the upgrade from (-1 = cursor) + * @param shiftClick If true, find first cell that can accept this upgrade + */ + private static void handleUpgrade(ContainerCellTerminalBase container, int tempSlotIndex, + int fromSlot, EntityPlayer player, boolean shiftClick) { + IItemHandlerModifiable tempInv = getTempCellInventory(container); + if (tempInv == null) return; + + ItemStack upgradeStack = fromSlot >= 0 + ? player.inventory.getStackInSlot(fromSlot) + : player.inventory.getItemStack(); + + if (upgradeStack.isEmpty()) return; + + if (!(upgradeStack.getItem() instanceof appeng.api.implementations.items.IUpgradeModule)) return; + + // Shift-click: find first cell that can accept this upgrade + if (shiftClick || tempSlotIndex < 0) { + for (int i = 0; i < tempInv.getSlots(); i++) { + if (tryInsertUpgradeIntoTempCell(tempInv, i, upgradeStack, player, fromSlot)) { + markDirty(container); + + return; + } + } + + return; + } + + // Regular click: use specific temp slot + if (tempSlotIndex >= tempInv.getSlots()) return; + + if (tryInsertUpgradeIntoTempCell(tempInv, tempSlotIndex, upgradeStack, player, fromSlot)) { + markDirty(container); + } + } + + /** + * Try to insert an upgrade into a temp cell at the given slot. + * + * @return true if the upgrade was successfully inserted + */ + private static boolean tryInsertUpgradeIntoTempCell(IItemHandlerModifiable tempInv, int tempSlotIndex, + ItemStack upgradeStack, EntityPlayer player, int fromSlot) { + ItemStack cellStack = tempInv.getStackInSlot(tempSlotIndex); + if (cellStack.isEmpty()) return false; + + if (!(cellStack.getItem() instanceof ICellWorkbenchItem)) return false; + + ICellWorkbenchItem cellItem = (ICellWorkbenchItem) cellStack.getItem(); + if (!cellItem.isEditable(cellStack)) return false; + + IItemHandler upgradesInv = cellItem.getUpgradesInventory(cellStack); + if (upgradesInv == null) return false; + + ItemStack toInsert = upgradeStack.copy(); + toInsert.setCount(1); + + for (int slot = 0; slot < upgradesInv.getSlots(); slot++) { + ItemStack remainder = upgradesInv.insertItem(slot, toInsert, false); + if (remainder.isEmpty()) { + upgradeStack.shrink(1); + + if (fromSlot >= 0) { + player.inventory.markDirty(); + } else { + ((EntityPlayerMP) player).updateHeldItem(); + } + + // Update the cell in temp inventory (NBT was modified) + tempInv.setStackInSlot(tempSlotIndex, cellStack); + + return true; + } + } + + return false; + } + /** * Get the temp cell inventory from the container. */ diff --git a/src/main/java/com/cellterminal/gui/GuiCellTerminalBase.java b/src/main/java/com/cellterminal/gui/GuiCellTerminalBase.java index 32a5476..0df22ff 100644 --- a/src/main/java/com/cellterminal/gui/GuiCellTerminalBase.java +++ b/src/main/java/com/cellterminal/gui/GuiCellTerminalBase.java @@ -12,7 +12,6 @@ import org.lwjgl.input.Keyboard; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Gui; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.renderer.GlStateManager; @@ -22,8 +21,9 @@ import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.math.BlockPos; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; -import appeng.api.AEApi; import appeng.api.implementations.items.IUpgradeModule; import appeng.client.gui.AEBaseGui; import appeng.client.gui.widgets.GuiScrollbar; @@ -34,65 +34,35 @@ import mezz.jei.api.gui.IGhostIngredientHandler; import com.cellterminal.client.CellInfo; -import com.cellterminal.client.SubnetConnectionRow; -import com.cellterminal.client.SubnetInfo; import com.cellterminal.client.SubnetVisibility; import com.cellterminal.client.TabStateManager; -import com.cellterminal.client.TempCellInfo; import com.cellterminal.config.CellTerminalClientConfig; import com.cellterminal.config.CellTerminalClientConfig.TerminalStyle; import com.cellterminal.container.ContainerCellTerminalBase; +import com.cellterminal.client.CellContentRow; import com.cellterminal.client.KeyBindings; import com.cellterminal.client.SearchFilterMode; -import com.cellterminal.client.StorageBusInfo; +import com.cellterminal.client.StorageBusContentRow; import com.cellterminal.client.StorageInfo; import com.cellterminal.config.CellTerminalServerConfig; -import com.cellterminal.gui.handler.JeiGhostHandler; -import com.cellterminal.gui.handler.JeiGhostHandler.PartitionSlotTarget; -import com.cellterminal.gui.handler.JeiGhostHandler.StorageBusPartitionSlotTarget; -import com.cellterminal.gui.handler.StorageBusClickHandler; +import com.cellterminal.gui.buttons.*; +import com.cellterminal.gui.handler.TabManager; import com.cellterminal.gui.handler.TabRenderingHandler; -import com.cellterminal.gui.handler.TerminalClickHandler; import com.cellterminal.gui.handler.TerminalDataManager; import com.cellterminal.gui.handler.TooltipHandler; -import com.cellterminal.gui.handler.UpgradeClickHandler; import com.cellterminal.gui.networktools.INetworkTool; import com.cellterminal.gui.networktools.GuiToolConfirmationModal; import com.cellterminal.gui.overlay.MessageHelper; import com.cellterminal.gui.overlay.OverlayMessageRenderer; -import com.cellterminal.gui.render.InventoryTabRenderer; -import com.cellterminal.gui.render.NetworkToolsTabRenderer; -import com.cellterminal.gui.render.PartitionTabRenderer; -import com.cellterminal.gui.render.RenderContext; -import com.cellterminal.gui.render.StorageBusInventoryTabRenderer; -import com.cellterminal.gui.render.StorageBusPartitionTabRenderer; -import com.cellterminal.gui.render.TempAreaTabRenderer; -import com.cellterminal.gui.render.TerminalTabRenderer; -import com.cellterminal.gui.tab.ITabController; -import com.cellterminal.gui.tab.PartitionTabController; -import com.cellterminal.gui.tab.StorageBusPartitionTabController; -import com.cellterminal.gui.tab.TabContext; -import com.cellterminal.gui.tab.TempAreaTabController; -import com.cellterminal.gui.subnet.GuiBackButton; -import com.cellterminal.gui.subnet.SubnetOverviewRenderer; -import com.cellterminal.gui.tab.TabControllerRegistry; +import com.cellterminal.gui.widget.tab.AbstractTabWidget; +import com.cellterminal.gui.widget.tab.NetworkToolsTabWidget; +import com.cellterminal.gui.widget.tab.SubnetOverviewTabWidget; import com.cellterminal.network.CellTerminalNetwork; -import com.cellterminal.network.PacketExtractUpgrade; import com.cellterminal.network.PacketHighlightBlock; -import com.cellterminal.network.PacketSubnetAction; import com.cellterminal.network.PacketSwitchNetwork; -import com.cellterminal.network.PacketPartitionAction; -import com.cellterminal.network.PacketRenameAction; import com.cellterminal.network.PacketSlotLimitChange; -import com.cellterminal.network.PacketStorageBusPartitionAction; import com.cellterminal.network.PacketTabChange; -import com.cellterminal.network.PacketTempCellAction; -import com.cellterminal.network.PacketTempCellPartitionAction; -import com.cellterminal.network.PacketUpgradeCell; -import com.cellterminal.network.PacketUpgradeStorageBus; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.rename.InlineRenameEditor; -import com.cellterminal.gui.rename.Renameable; +import com.cellterminal.gui.rename.InlineRenameManager; /** @@ -100,38 +70,24 @@ * Contains shared functionality for displaying storage drives/chests with their cells. * Supports three tabs: Terminal (list view), Inventory (cell slots with contents), Partition (cell slots with partition). */ -public abstract class GuiCellTerminalBase extends AEBaseGui implements IJEIGhostIngredients { - - private static final int TAB_WIDTH = 22; - private static final int TAB_HEIGHT = 22; - private static final int TAB_Y_OFFSET = -22; +public abstract class GuiCellTerminalBase extends AEBaseGui implements IJEIGhostIngredients, NetworkToolsTabWidget.NetworkToolGuiContext, SubnetOverviewTabWidget.SubnetOverviewContext, TabManager.TabSwitchListener { // Layout constants - protected static final int ROW_HEIGHT = 18; - protected static final int MIN_ROWS = 6; - protected static final int DEFAULT_ROWS = 8; + protected static final int ROW_HEIGHT = GuiConstants.ROW_HEIGHT; + protected static final int MIN_ROWS = GuiConstants.MIN_ROWS; + protected static final int DEFAULT_ROWS = GuiConstants.DEFAULT_ROWS; // Magic height number for tall mode calculation (header + footer heights) - private static final int MAGIC_HEIGHT_NUMBER = 18 + 98; + private static final int MAGIC_HEIGHT_NUMBER = GuiConstants.MAGIC_HEIGHT_NUMBER; // Dynamic row count (computed based on terminal style) protected int rowsVisible = DEFAULT_ROWS; - // Tab renderers - protected TerminalTabRenderer terminalRenderer; - protected InventoryTabRenderer inventoryRenderer; - protected PartitionTabRenderer partitionRenderer; - protected TempAreaTabRenderer tempAreaRenderer; - protected StorageBusInventoryTabRenderer storageBusInventoryRenderer; - protected StorageBusPartitionTabRenderer storageBusPartitionRenderer; - protected NetworkToolsTabRenderer networkToolsRenderer; - protected SubnetOverviewRenderer subnetOverviewRenderer; - protected RenderContext renderContext; + // Tab management (widget lifecycle, rendering, clicking, switching) + protected TabManager tabManager; // Handlers protected TerminalDataManager dataManager; - protected TerminalClickHandler clickHandler; - protected StorageBusClickHandler storageBusClickHandler; // Terminal style button protected GuiTerminalStyleButton terminalStyleButton; @@ -149,120 +105,46 @@ public abstract class GuiCellTerminalBase extends AEBaseGui implements IJEIGhost protected SearchFilterMode currentSearchMode = SearchFilterMode.MIXED; protected SubnetVisibility currentSubnetVisibility = SubnetVisibility.DONT_SHOW; - // Current tab - protected int currentTab; - - // Tab icons (lazy initialized) - protected ItemStack tabIconTerminal = null; - protected ItemStack tabIconInventory = null; - protected ItemStack tabIconPartition = null; - protected ItemStack tabIconTempArea = null; // Temp Area tab icon (64k cell) - protected ItemStack tabIconStorageBus = null; // Storage bus icon for composite rendering - protected ItemStack tabIconNetworkTool = null; // Network Tools tab icon - // Popup states protected PopupCellInventory inventoryPopup = null; protected PopupCellPartition partitionPopup = null; - protected CellInfo hoveredCell = null; - protected int hoverType = 0; // 0=none, 1=inventory, 2=partition, 3=eject - protected StorageInfo hoveredStorageLine = null; // Storage header being hovered - - // Priority field manager (inline editable fields) - protected PriorityFieldManager priorityFieldManager = null; - - // Inline rename editor for storage, cell, and storage bus renaming - protected InlineRenameEditor inlineRenameEditor = null; - - // Tab hover for tooltips - protected int hoveredTab = -1; - - // Hover tracking for background highlight - protected int hoveredLineIndex = -1; - - // Tab 2/3 hover state - protected CellInfo hoveredCellCell = null; - protected StorageInfo hoveredCellStorage = null; - protected int hoveredCellSlotIndex = -1; - protected ItemStack hoveredContentStack = ItemStack.EMPTY; - protected int hoveredContentX = 0; - protected int hoveredContentY = 0; - protected int hoveredContentSlotIndex = -1; - - // Partition slot tracking for JEI ghost ingredients and click handling - protected int hoveredPartitionSlotIndex = -1; - protected CellInfo hoveredPartitionCell = null; - protected final List partitionSlotTargets = new ArrayList<>(); - - // Cell partition button tracking - protected CellInfo hoveredPartitionAllButtonCell = null; // Tab 2: partition all button - protected CellInfo hoveredClearPartitionButtonCell = null; // Tab 3: clear partition button - - // Storage bus hover and selection tracking - protected StorageBusInfo hoveredStorageBus = null; - protected int hoveredStorageBusPartitionSlot = -1; - protected int hoveredStorageBusContentSlot = -1; - protected StorageBusInfo hoveredClearButtonStorageBus = null; - protected StorageBusInfo hoveredIOModeButtonStorageBus = null; - protected StorageBusInfo hoveredPartitionAllButtonStorageBus = null; // Tab 4: partition all button - protected final Set selectedStorageBusIds = new HashSet<>(); // For Tab 5 - multi-selection of buses for keybind - protected final List storageBusPartitionSlotTargets = new ArrayList<>(); - - // Temp area hover and selection tracking - protected int hoveredTempCellSlot = -1; // Hovered temp cell header slot index - protected int hoveredTempCellPartitionSlot = -1; // Hovered partition slot within a temp cell - protected final Set selectedTempCellSlots = new HashSet<>(); // For Tab 3 - multi-selection of temp cells - protected final List tempCellPartitionSlotTargets = new ArrayList<>(); + + // Storage bus and temp area selection tracking (for quick-add keybinds) + protected final Set selectedStorageBusIds = new HashSet<>(); + protected final Set selectedTempCellSlots = new HashSet<>(); // Modal search bar for expanded editing protected GuiModalSearchBar modalSearchBar = null; + // Search field click handler (right-click clear, double-click modal) + protected SearchFieldHandler searchFieldHandler = null; + // Guard to prevent style button from being retriggered while mouse is still down private long lastStyleButtonClickTime = 0; private static final long STYLE_BUTTON_COOLDOWN = 100; // ms - protected long lastSearchFieldClickTime = 0; - protected static final long DOUBLE_CLICK_THRESHOLD = 500; // ms - - // Hovered upgrade icon for tooltip and extraction - protected RenderContext.UpgradeIconTarget hoveredUpgradeIcon = null; - // Whether we've restored the saved scroll after the first data update private boolean initialScrollRestored = false; - // Subnet view state - protected final List subnetList = new ArrayList<>(); - protected final List subnetLines = new ArrayList<>(); // Flattened list of SubnetInfo + SubnetConnectionRow - protected boolean isInSubnetOverviewMode = false; + // Subnet view state (network routing, kept here since it affects data updates) protected long currentNetworkId = 0; // 0 = main network, >0 = subnet ID - protected int hoveredSubnetEntryIndex = -1; // Index of hovered subnet in overview mode - protected long lastSubnetClickTime = 0; // For double-click detection - protected long lastSubnetClickId = -1; // Track which subnet was clicked for double-click // When true, we're waiting for a network switch response - ignore incoming data until confirmed protected boolean awaitingNetworkSwitch = false; public GuiCellTerminalBase(Container container) { super(container); - this.xSize = 208; + this.xSize = GuiConstants.GUI_WIDTH; this.rowsVisible = calculateRowsCount(); this.ySize = MAGIC_HEIGHT_NUMBER + this.rowsVisible * ROW_HEIGHT; this.setScrollBar(new GuiScrollbar()); - this.renderContext = new RenderContext(); this.dataManager = new TerminalDataManager(); - this.clickHandler = new TerminalClickHandler(); - this.storageBusClickHandler = new StorageBusClickHandler(); this.filterPanelManager = new FilterPanelManager(); // Load persisted settings CellTerminalClientConfig config = CellTerminalClientConfig.getInstance(); - this.currentTab = config.getSelectedTab(); - if (this.currentTab < 0 || this.currentTab >= TabControllerRegistry.getTabCount()) this.currentTab = GuiConstants.TAB_TERMINAL; - - // If the persisted tab is disabled, redirect to the first enabled tab - if (CellTerminalServerConfig.isInitialized() && !CellTerminalServerConfig.getInstance().isTabEnabled(this.currentTab)) { - this.currentTab = findFirstEnabledTab(); - } + this.tabManager = new TabManager(config.getSelectedTab(), this); this.currentSearchMode = config.getSearchMode(); this.currentSubnetVisibility = config.getSubnetVisibility(); @@ -291,22 +173,6 @@ protected int calculateRowsCount() { return Math.max(MIN_ROWS, extraSpace / ROW_HEIGHT); } - /** - * Find the first enabled tab, or fall back to TAB_TERMINAL if all are disabled. - */ - protected int findFirstEnabledTab() { - if (!CellTerminalServerConfig.isInitialized()) return GuiConstants.TAB_TERMINAL; - - CellTerminalServerConfig config = CellTerminalServerConfig.getInstance(); - - for (int i = 0; i < TabControllerRegistry.getTabCount(); i++) { - if (config.isTabEnabled(i)) return i; - } - - // Fallback to terminal tab even if disabled (should not happen in practice) - return GuiConstants.TAB_TERMINAL; - } - protected abstract String getGuiTitle(); @Override @@ -343,13 +209,11 @@ public void initGui() { this.getScrollBar().setTop(18).setLeft(189).setHeight(this.rowsVisible * ROW_HEIGHT - 2); this.repositionSlots(); - initTabIcons(); - initRenderers(); + initTabWidgets(); initTerminalStyleButton(); initSubnetBackButton(); initFilterButtons(); initSearchField(); - initPriorityFieldManager(); // Apply persisted filter states to data manager applyFiltersToDataManager(); @@ -358,7 +222,7 @@ public void initGui() { updateScrollbarForCurrentTab(); // Restore saved scroll position for the current tab (in-memory TabStateManager) - TabStateManager.TabType initialTabType = TabStateManager.TabType.fromIndex(this.currentTab); + TabStateManager.TabType initialTabType = TabStateManager.TabType.fromIndex(tabManager.getCurrentTab()); int initialSavedScroll = TabStateManager.getInstance().getScrollPosition(initialTabType); scrollToLine(initialSavedScroll); @@ -368,13 +232,14 @@ public void initGui() { // Notify server of the current tab so it can start sending appropriate data // This is especially important for storage bus tabs which require server polling - CellTerminalNetwork.INSTANCE.sendToServer(new PacketTabChange(currentTab)); + CellTerminalNetwork.INSTANCE.sendToServer(new PacketTabChange(tabManager.getCurrentTab())); // Send current slot limit preferences to server CellTerminalClientConfig config = CellTerminalClientConfig.getInstance(); CellTerminalNetwork.INSTANCE.sendToServer(new PacketSlotLimitChange( config.getCellSlotLimit().getLimit(), - config.getBusSlotLimit().getLimit() + config.getBusSlotLimit().getLimit(), + config.getSubnetSlotLimit().getLimit() )); // If a subnet was previously being viewed, tell the server to switch to it @@ -386,20 +251,26 @@ public void initGui() { } } - protected void initPriorityFieldManager() { - this.priorityFieldManager = new PriorityFieldManager(this.fontRenderer); - } + protected void initTabWidgets() { + // Provide scroll access so TabManager can save/restore scroll positions on tab switch + tabManager.setScrollAccessor(new TabManager.ScrollAccessor() { + @Override + public int getCurrentScroll() { + return getScrollBar().getCurrentScroll(); + } + + @Override + public void scrollToLine(int lineIndex) { + GuiCellTerminalBase.this.scrollToLine(lineIndex); + } + }); + + // Delegate widget creation and initialization to the TabManager + tabManager.initWidgets(this.fontRenderer, this.itemRender, + this.guiLeft, this.guiTop, this.rowsVisible, this); - protected void initRenderers() { - this.terminalRenderer = new TerminalTabRenderer(this.fontRenderer, this.itemRender); - this.inventoryRenderer = new InventoryTabRenderer(this.fontRenderer, this.itemRender); - this.partitionRenderer = new PartitionTabRenderer(this.fontRenderer, this.itemRender); - this.tempAreaRenderer = new TempAreaTabRenderer(this.fontRenderer, this.itemRender); - this.storageBusInventoryRenderer = new StorageBusInventoryTabRenderer(this.fontRenderer, this.itemRender); - this.storageBusPartitionRenderer = new StorageBusPartitionTabRenderer(this.fontRenderer, this.itemRender); - this.networkToolsRenderer = new NetworkToolsTabRenderer(this.fontRenderer, this.itemRender); - this.subnetOverviewRenderer = new SubnetOverviewRenderer(this.fontRenderer, this.itemRender); - this.inlineRenameEditor = new InlineRenameEditor(); + // Wire subnet context after widget init + tabManager.getSubnetTab().setSubnetContext(this); } protected void initSubnetBackButton() { @@ -409,14 +280,14 @@ protected void initSubnetBackButton() { int buttonX = this.guiLeft + 4; int buttonY = this.guiTop + 4; this.subnetBackButton = new GuiBackButton(3, buttonX, buttonY); - this.subnetBackButton.setInOverviewMode(this.isInSubnetOverviewMode); + this.subnetBackButton.setInOverviewMode(isInSubnetOverviewMode()); this.buttonList.add(this.subnetBackButton); } protected void initTerminalStyleButton() { if (this.terminalStyleButton != null) this.buttonList.remove(this.terminalStyleButton); - // Calculate button Y as if in SMALL mode (for consistent positioning across style changes) + // Calculate button Y like in SMALL mode (for consistent positioning across style changes) int smallModeYSize = MAGIC_HEIGHT_NUMBER + DEFAULT_ROWS * ROW_HEIGHT; int buttonY = Math.max(8, (this.height - smallModeYSize) / 2 + 8); this.terminalStyleButton = new GuiTerminalStyleButton(0, this.guiLeft - 18, buttonY, CellTerminalClientConfig.getInstance().getTerminalStyle()); @@ -424,7 +295,7 @@ protected void initTerminalStyleButton() { } protected void initFilterButtons() { - nextButtonId = filterPanelManager.initButtons(this.buttonList, nextButtonId, currentTab); + nextButtonId = filterPanelManager.initButtons(this.buttonList, nextButtonId, tabManager.getCurrentTab()); updateFilterButtonPositions(); } @@ -450,7 +321,7 @@ protected void initSearchField() { this.buttonList.add(this.searchHelpButton); // Search field: positioned after help button - int searchX = helpButtonX + GuiSearchHelpButton.BUTTON_SIZE + 2; + int searchX = helpButtonX + GuiSearchHelpButton.SIZE + 2; int searchY = 4; int availableWidth = 189 - searchX; @@ -469,7 +340,7 @@ public void onTextChange(String oldText) { if (this.searchModeButton != null) this.buttonList.remove(this.searchModeButton); // Search mode button: positioned above the scrollbar (top-right corner) - this.searchModeButton = new GuiSearchModeButton(1, this.guiLeft + 189, this.guiTop + 4, currentSearchMode); + this.searchModeButton = new GuiSearchModeButton(1, this.guiLeft + 189, this.guiTop + 5, currentSearchMode); this.buttonList.add(this.searchModeButton); updateSearchModeButtonVisibility(); @@ -479,8 +350,9 @@ public void onTextChange(String oldText) { // this.subnetVisibilityButton = new GuiSubnetVisibilityButton(4, this.guiLeft + 189 - 14, this.guiTop + 4, currentSubnetVisibility); // this.buttonList.add(this.subnetVisibilityButton); - // Initialize modal search bar + // Initialize modal search bar and search field handler this.modalSearchBar = new GuiModalSearchBar(this.fontRenderer, this.searchField, this::onSearchTextChanged); + this.searchFieldHandler = new SearchFieldHandler(this.searchField, this.modalSearchBar); if (!existingSearch.isEmpty()) dataManager.setSearchFilter(existingSearch, getEffectiveSearchMode()); } @@ -488,9 +360,7 @@ public void onTextChange(String oldText) { protected void updateSearchModeButtonVisibility() { if (this.searchModeButton == null) return; - // Delegate to tab controller to determine visibility - ITabController controller = TabControllerRegistry.getController(currentTab); - this.searchModeButton.visible = (controller != null && controller.showSearchModeButton()); + this.searchModeButton.visible = tabManager.isSearchModeButtonVisible(); } protected void onSearchTextChanged() { @@ -504,41 +374,10 @@ protected void onSearchTextChanged() { /** * Get the effective search mode based on current tab. - * Delegates to the active tab controller. + * Delegates to the TabManager. */ protected SearchFilterMode getEffectiveSearchMode() { - ITabController controller = TabControllerRegistry.getController(currentTab); - if (controller != null) { - return controller.getEffectiveSearchMode(currentSearchMode); - } - - return currentSearchMode; - } - - protected void initTabIcons() { - // Terminal icon - use the interface terminal part - tabIconTerminal = AEApi.instance().definitions().parts().interfaceTerminal() - .maybeStack(1).orElse(ItemStack.EMPTY); - - // Inventory icon - use ME Chest - tabIconInventory = AEApi.instance().definitions().blocks().chest() - .maybeStack(1).orElse(ItemStack.EMPTY); - - // Partition icon - use Cell Workbench - tabIconPartition = AEApi.instance().definitions().blocks().cellWorkbench() - .maybeStack(1).orElse(ItemStack.EMPTY); - - // Temp Area icon - use 64k storage cell - tabIconTempArea = AEApi.instance().definitions().items().cell64k() - .maybeStack(1).orElse(ItemStack.EMPTY); - - // Storage bus icon (used for composite rendering) - tabIconStorageBus = AEApi.instance().definitions().parts().storageBus() - .maybeStack(1).orElse(ItemStack.EMPTY); - - // Network Tools icon - tabIconNetworkTool = AEApi.instance().definitions().items().networkTool() - .maybeStack(1).orElse(ItemStack.EMPTY); + return tabManager.getEffectiveSearchMode(currentSearchMode); } protected void repositionSlots() { @@ -566,41 +405,22 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { partitionPopup.drawTooltip(mouseX, mouseY); } - // Draw hover preview - if (currentTab == GuiConstants.TAB_TERMINAL && hoveredCell != null && inventoryPopup == null && partitionPopup == null) { - int previewX = mouseX + 10, previewY = mouseY + 10; - if (hoverType == 1) new PopupCellInventory(this, hoveredCell, previewX, previewY).draw(mouseX, mouseY); - else if (hoverType == 2) new PopupCellPartition(this, hoveredCell, previewX, previewY).draw(mouseX, mouseY); - else if (hoverType == 3) this.drawHoveringText(Collections.singletonList(I18n.format("gui.cellterminal.eject_cell")), mouseX, mouseY); - } - - drawTooltips(mouseX, mouseY); - - // Draw subnet overview tooltips - if (this.isInSubnetOverviewMode && this.hoveredSubnetEntryIndex >= 0 - && this.hoveredSubnetEntryIndex < this.subnetLines.size()) { - Object line = this.subnetLines.get(this.hoveredSubnetEntryIndex); - - // Handle filter item tooltips separately (for SubnetConnectionRow) - if (line instanceof SubnetConnectionRow) { - SubnetOverviewRenderer.HoverZone zone = this.subnetOverviewRenderer.getHoveredZone(); - if (zone == SubnetOverviewRenderer.HoverZone.FILTER_SLOT) { - ItemStack filterStack = this.subnetOverviewRenderer.getHoveredFilterStack(); - if (!filterStack.isEmpty()) { - this.renderToolTip(filterStack, mouseX, mouseY); - - // Skip regular tooltip - line = null; - } - } - } + // Draw hover preview from terminal tab widget + // TODO: we can assume that the popups are only open in the terminal tab, as outside click or Esc closes them + if (tabManager.getCurrentTab() == GuiConstants.TAB_TERMINAL && inventoryPopup == null && partitionPopup == null) { + CellInfo previewCell = tabManager.getTerminalWidget().getPreviewCell(); + int previewType = tabManager.getTerminalWidget().getPreviewType(); - if (line != null) { - List tooltip = this.subnetOverviewRenderer.getTooltip(line); - if (!tooltip.isEmpty()) this.drawHoveringText(tooltip, mouseX, mouseY); + if (previewCell != null) { + int previewX = mouseX + 10, previewY = mouseY + 10; + if (previewType == 1) new PopupCellInventory(this, previewCell, previewX, previewY).draw(mouseX, mouseY); + else if (previewType == 2) new PopupCellPartition(this, previewCell, previewX, previewY).draw(mouseX, mouseY); + else if (previewType == 3) this.drawHoveringText(Collections.singletonList(I18n.format("gui.cellterminal.eject_cell")), mouseX, mouseY); } } + drawTooltips(mouseX, mouseY); + // Draw back button tooltip if (this.subnetBackButton != null && this.subnetBackButton.isMouseOver()) { List tooltip = this.subnetBackButton.getTooltip(); @@ -618,26 +438,46 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { } protected void drawTooltips(int mouseX, int mouseY) { + int relMouseX = mouseX - guiLeft; + int relMouseY = mouseY - guiTop; + + // Widget-based tooltips for tabs 0-5 + AbstractTabWidget activeTab = tabManager.getActiveTab(); + if (activeTab != null) { + // Get tooltip from hovered widget row + List widgetTooltip = activeTab.getTooltip(relMouseX, relMouseY); + if (!widgetTooltip.isEmpty()) { + this.drawHoveringText(widgetTooltip, mouseX, mouseY); + + return; + } + + // Get hovered item stack for vanilla item tooltip + ItemStack hoveredStack = activeTab.getHoveredItemStack(relMouseX, relMouseY); + if (!hoveredStack.isEmpty()) { + this.renderToolTip(hoveredStack, mouseX, mouseY); + + return; + } + } + + // Tab button tooltips (via TabManager) + int hoveredTab = tabManager.getHoveredTab(); + if (hoveredTab >= 0 && inventoryPopup == null && partitionPopup == null) { + String tabTooltip = tabManager.getTabTooltip(hoveredTab); + if (!tabTooltip.isEmpty()) { + this.drawHoveringText(Collections.singletonList(tabTooltip), mouseX, mouseY); + + return; + } + } + + // Legacy tooltip context for non-widget elements (buttons, search field) TooltipHandler.TooltipContext ctx = new TooltipHandler.TooltipContext(); - ctx.currentTab = currentTab; - ctx.hoveredTab = hoveredTab; - ctx.hoveredCell = hoveredCell; - ctx.hoverType = hoverType; - ctx.hoveredContentStack = hoveredContentStack; - ctx.hoveredContentX = hoveredContentX; - ctx.hoveredContentY = hoveredContentY; - ctx.hoveredClearButtonStorageBus = hoveredClearButtonStorageBus; - ctx.hoveredIOModeButtonStorageBus = hoveredIOModeButtonStorageBus; - ctx.hoveredPartitionAllButtonStorageBus = hoveredPartitionAllButtonStorageBus; - ctx.hoveredPartitionAllButtonCell = hoveredPartitionAllButtonCell; - ctx.hoveredClearPartitionButtonCell = hoveredClearPartitionButtonCell; - ctx.inventoryPopup = inventoryPopup; - ctx.partitionPopup = partitionPopup; ctx.terminalStyleButton = terminalStyleButton; ctx.searchModeButton = searchModeButton; ctx.searchHelpButton = searchHelpButton; // ctx.subnetVisibilityButton = subnetVisibilityButton; - ctx.priorityFieldManager = priorityFieldManager; ctx.filterPanelManager = filterPanelManager; // Search error state @@ -650,19 +490,6 @@ protected void drawTooltips(int mouseX, int mouseY) { ctx.searchFieldHeight = searchField.height + 4; } - // Upgrade icon hover state - ctx.hoveredUpgradeIcon = hoveredUpgradeIcon; - - // Network tools hover state - RenderContext renderCtx = getRenderContext(); - ctx.hoveredNetworkTool = renderCtx.hoveredNetworkTool; - ctx.hoveredNetworkToolHelpButton = renderCtx.hoveredNetworkToolHelpButton; - ctx.hoveredNetworkToolPreview = renderCtx.hoveredNetworkToolPreview; - - // Temp area hover state (Tab 3) - ctx.hoveredTempCellSlot = renderCtx.hoveredTempCellSlot; - ctx.hoveredTempCellSendButton = renderCtx.hoveredTempCellSendButton; - TooltipHandler.drawTooltips(ctx, new TooltipHandler.TooltipRenderer() { @Override public void drawHoveringText(List lines, int x, int y) { @@ -699,100 +526,36 @@ public void drawFG(int offsetX, int offsetY, int mouseX, int mouseY) { GlStateManager.popMatrix(); } - // Reset render context hover states - renderContext.resetHoverState(); - renderContext.storageMap = dataManager.getStorageMap(); - renderContext.rowsVisible = this.rowsVisible; - renderContext.guiLeft = this.guiLeft; - renderContext.guiTop = this.guiTop; - renderContext.selectedStorageBusIds = this.selectedStorageBusIds; - renderContext.selectedTempCellSlots = this.selectedTempCellSlots; - int relMouseX = mouseX - offsetX; int relMouseY = mouseY - offsetY; final int currentScroll = this.getScrollBar().getCurrentScroll(); - // Reset priority field visibility before rendering - if (priorityFieldManager != null) priorityFieldManager.resetVisibility(); - - // Subnet overview mode takes over the content area - if (this.isInSubnetOverviewMode) { - drawSubnetOverviewContent(relMouseX, relMouseY, currentScroll); - syncHoverStateFromContext(relMouseX, relMouseY); - drawControlsHelpForCurrentTab(); + // Reset priority field visibility before rendering (fields are re-registered by headers during draw) + PriorityFieldManager.getInstance().resetVisibility(); - return; - } - - // Draw based on current tab using renderers - switch (currentTab) { - case GuiConstants.TAB_TERMINAL: - terminalRenderer.draw(dataManager.getLines(), currentScroll, rowsVisible, relMouseX, relMouseY, renderContext); - break; - case GuiConstants.TAB_INVENTORY: - inventoryRenderer.draw(dataManager.getInventoryLines(), currentScroll, rowsVisible, - relMouseX, relMouseY, mouseX, mouseY, dataManager.getStorageMap(), renderContext); - break; - case GuiConstants.TAB_PARTITION: - partitionRenderer.draw(dataManager.getPartitionLines(), currentScroll, rowsVisible, - relMouseX, relMouseY, mouseX, mouseY, dataManager.getStorageMap(), - this.guiLeft, this.guiTop, renderContext); - break; - case GuiConstants.TAB_TEMP_AREA: - tempAreaRenderer.draw(dataManager.getTempAreaLines(), currentScroll, rowsVisible, - relMouseX, relMouseY, mouseX, mouseY, - this.guiLeft, this.guiTop, renderContext); - break; - case GuiConstants.TAB_STORAGE_BUS_INVENTORY: - storageBusInventoryRenderer.draw(dataManager.getStorageBusInventoryLines(), currentScroll, rowsVisible, - relMouseX, relMouseY, mouseX, mouseY, renderContext); - break; - case GuiConstants.TAB_STORAGE_BUS_PARTITION: - storageBusPartitionRenderer.draw(dataManager.getStorageBusPartitionLines(), currentScroll, rowsVisible, - relMouseX, relMouseY, mouseX, mouseY, - this.guiLeft, this.guiTop, renderContext); - break; - case GuiConstants.TAB_NETWORK_TOOLS: - networkToolsRenderer.draw(currentScroll, rowsVisible, - relMouseX, relMouseY, createNetworkToolContext(), renderContext); - break; - } - - // Draw inline rename field overlay if editing - if (inlineRenameEditor != null && inlineRenameEditor.isEditing()) { - inlineRenameEditor.drawRenameField(this.fontRenderer); + // Draw based on current tab using widgets + AbstractTabWidget activeTab = tabManager.getActiveTab(); + boolean isSubnetTab = isInSubnetOverviewMode(); + if (activeTab != null) { + activeTab.buildVisibleRows(tabManager.getActiveLines(dataManager), currentScroll); + activeTab.draw(relMouseX, relMouseY); } - // Update priority field positions based on visible storages - if (priorityFieldManager != null) { - for (RenderContext.VisibleStorageEntry entry : renderContext.visibleStorages) { - // Only show priority field if the storage type supports it - if (entry.storage.supportsPriority()) { - PriorityFieldManager.PriorityField field = priorityFieldManager.getOrCreateField( - entry.storage, guiLeft, guiTop); - priorityFieldManager.updateFieldPosition(field, entry.y, guiLeft, guiTop); - } - } - - // Update storage bus priority field positions for tabs 4 and 5 - for (RenderContext.VisibleStorageBusEntry entry : renderContext.visibleStorageBuses) { - if (entry.storageBus.supportsPriority()) { - PriorityFieldManager.StorageBusPriorityField field = priorityFieldManager.getOrCreateStorageBusField( - entry.storageBus, guiLeft, guiTop); - priorityFieldManager.updateStorageBusFieldPosition(field, entry.y, guiLeft, guiTop); - } - } + // Priority fields and inline rename are only relevant for real tabs, not subnet overview + if (!isSubnetTab) { + // Draw priority fields (positioned by headers during their draw pass, rendered in absolute coords) + PriorityFieldManager pfm = PriorityFieldManager.getInstance(); + pfm.drawFieldsRelative(guiLeft, guiTop); - // Draw priority fields (in GUI-relative context) - priorityFieldManager.drawFieldsRelative(guiLeft, guiTop); + // Cleanup fields for storages/buses that no longer exist in the data + Set activeIds = new HashSet<>(dataManager.getStorageMap().keySet()); + activeIds.addAll(dataManager.getStorageBusMap().keySet()); + pfm.cleanupStaleFields(activeIds); - // Cleanup stale fields - priorityFieldManager.cleanupStaleFields(dataManager.getStorageMap()); - priorityFieldManager.cleanupStaleStorageBusFields(dataManager.getStorageBusMap()); + // Draw inline rename field overlay on top of everything else + InlineRenameManager.getInstance().drawRenameField(this.fontRenderer); } - syncHoverStateFromContext(relMouseX, relMouseY); - // Draw controls help - delegate to tab controller drawControlsHelpForCurrentTab(); } @@ -800,141 +563,42 @@ public void drawFG(int offsetX, int offsetY, int mouseX, int mouseY) { // Constants for controls help widget positioning // JEI buttons are at guiLeft - 18, with ~4px margin from screen edge // We position the panel to leave similar margins on both sides - protected static final int CONTROLS_HELP_RIGHT_MARGIN = 4; // Gap between panel and GUI - protected static final int CONTROLS_HELP_LEFT_MARGIN = 4; // Gap between screen edge and panel - protected static final int CONTROLS_HELP_PADDING = 4; // Inner padding - protected static final int CONTROLS_HELP_LINE_HEIGHT = 10; // Height per line - - // Cached wrapped lines for controls help (used for both rendering and exclusion area) - protected List cachedControlsHelpLines = new ArrayList<>(); - protected int cachedControlsHelpTab = -1; + protected static final int CONTROLS_HELP_RIGHT_MARGIN = GuiConstants.CONTROLS_HELP_RIGHT_MARGIN; + protected static final int CONTROLS_HELP_LEFT_MARGIN = GuiConstants.CONTROLS_HELP_LEFT_MARGIN; + protected static final int CONTROLS_HELP_PADDING = GuiConstants.CONTROLS_HELP_PADDING; + protected static final int CONTROLS_HELP_LINE_HEIGHT = GuiConstants.CONTROLS_HELP_LINE_HEIGHT; /** * Draw controls help for the current tab using the handler. + * Works for both real tabs and subnet overlay (the active tab provides getHelpLines()). */ protected void drawControlsHelpForCurrentTab() { - TerminalStyle style = CellTerminalClientConfig.getInstance().getTerminalStyle(); - - // In subnet overview mode, show subnet-specific controls - if (this.isInSubnetOverviewMode) { - drawSubnetControlsHelp(style); - updateFilterButtonPositions(); - return; - } + int tabIndex = tabManager.getCurrentTab(); TabRenderingHandler.ControlsHelpContext ctx = new TabRenderingHandler.ControlsHelpContext( - this.guiLeft, this.guiTop, this.ySize, this.height, currentTab, this.fontRenderer, style); + this.guiLeft, this.guiTop, this.ySize, this.height, tabIndex, this.fontRenderer); - TabRenderingHandler.ControlsHelpResult result = TabRenderingHandler.drawControlsHelpWidget(ctx); - this.cachedControlsHelpLines = result.wrappedLines; - this.cachedControlsHelpTab = result.cachedTab; + // Get help lines from the active tab widget (works for subnet tab too) + AbstractTabWidget activeTab = tabManager.getActiveTab(); + List helpLines = activeTab != null ? activeTab.getHelpLines() : Collections.emptyList(); + + TabRenderingHandler.ControlsHelpResult result = TabRenderingHandler.drawControlsHelpWidget(ctx, helpLines); + tabManager.setCachedControlsHelp(result.wrappedLines, result.cachedTab); // Update filter button positions after controls help bounds are known updateFilterButtonPositions(); } - /** - * Draw controls help widget for subnet overview mode. - */ - protected void drawSubnetControlsHelp(TerminalStyle style) { - List lines = new ArrayList<>(); - lines.add(I18n.format("cellterminal.subnet.controls.title")); - lines.add(""); - lines.add(I18n.format("cellterminal.subnet.controls.click")); - lines.add(I18n.format("cellterminal.subnet.controls.dblclick")); - lines.add(I18n.format("cellterminal.subnet.controls.star")); - lines.add(I18n.format("cellterminal.subnet.controls.rename")); - lines.add(I18n.format("cellterminal.subnet.controls.esc")); - - // Calculate panel width - int panelWidth = this.guiLeft - CONTROLS_HELP_RIGHT_MARGIN - CONTROLS_HELP_LEFT_MARGIN; - if (panelWidth < 60) panelWidth = 60; - int textWidth = panelWidth - (CONTROLS_HELP_PADDING * 2); - - // Wrap all lines - List wrappedLines = new ArrayList<>(); - for (String line : lines) { - if (line.isEmpty()) { - wrappedLines.add(""); - } else { - wrappedLines.addAll(this.fontRenderer.listFormattedStringToWidth(line, textWidth)); - } - } - - // Calculate positions - int panelRight = -CONTROLS_HELP_RIGHT_MARGIN; - int panelLeft = -this.guiLeft + CONTROLS_HELP_LEFT_MARGIN; - int contentHeight = wrappedLines.size() * CONTROLS_HELP_LINE_HEIGHT; - int panelHeight = contentHeight + (CONTROLS_HELP_PADDING * 2); - - // Position relative to screen bottom - int bottomOffset = 28; - int panelBottom = this.height - this.guiTop - bottomOffset; - int panelTop = panelBottom - panelHeight; - - // Draw AE2-style panel background - Gui.drawRect(panelLeft, panelTop, panelRight, panelBottom, 0xC0000000); - - // Border - Gui.drawRect(panelLeft, panelTop, panelRight, panelTop + 1, 0xFF606060); - Gui.drawRect(panelLeft, panelTop, panelLeft + 1, panelBottom, 0xFF606060); - Gui.drawRect(panelLeft, panelBottom - 1, panelRight, panelBottom, 0xFF303030); - Gui.drawRect(panelRight - 1, panelTop, panelRight, panelBottom, 0xFF303030); - - // Draw text - int textX = panelLeft + CONTROLS_HELP_PADDING; - int textY = panelTop + CONTROLS_HELP_PADDING; - for (int i = 0; i < wrappedLines.size(); i++) { - String line = wrappedLines.get(i); - if (!line.isEmpty()) { - this.fontRenderer.drawString(line, textX, textY + (i * CONTROLS_HELP_LINE_HEIGHT), 0xCCCCCC); - } - } - - this.cachedControlsHelpLines = wrappedLines; - this.cachedControlsHelpTab = -1; // Mark as subnet mode - } - - /** - * Draw the subnet overview content. - * Shows a list of connected subnets with their info and connection details. - */ - protected void drawSubnetOverviewContent(int relMouseX, int relMouseY, int currentScroll) { - if (this.subnetOverviewRenderer == null) return; - - // Update scrollbar for subnet lines (headers + connection rows) - int totalLines = this.subnetLines.size(); - int maxScroll = Math.max(0, totalLines - this.rowsVisible); - this.getScrollBar().setRange(0, maxScroll, 1); - - // Draw the subnet list with connection rows - this.hoveredSubnetEntryIndex = this.subnetOverviewRenderer.draw( - this.subnetLines, - currentScroll, - this.rowsVisible, - relMouseX, - relMouseY, - this.guiLeft, - this.guiTop - ); - - // Draw rename field overlay if editing - if (this.subnetOverviewRenderer.isEditing()) this.subnetOverviewRenderer.drawRenameField(); - } - /** * Get the bounding rectangle for the controls help widget. * Uses cached wrapped lines from the last render for accurate sizing. * Used for JEI exclusion areas. */ protected Rectangle getControlsHelpBounds() { - // Use cached lines if available - // For subnet overview mode, cachedControlsHelpTab is -1 and we're not in a tab, so check isInSubnetOverviewMode - boolean isCacheValid = !cachedControlsHelpLines.isEmpty() - && (cachedControlsHelpTab == currentTab || (cachedControlsHelpTab == -1 && isInSubnetOverviewMode)); - if (!isCacheValid) return new Rectangle(0, 0, 0, 0); + List wrappedLines = tabManager.getCachedControlsHelpLines(); + if (wrappedLines.isEmpty()) return new Rectangle(0, 0, 0, 0); - int lineCount = cachedControlsHelpLines.size(); + int lineCount = wrappedLines.size(); int contentHeight = lineCount * CONTROLS_HELP_LINE_HEIGHT; int panelHeight = contentHeight + (CONTROLS_HELP_PADDING * 2); @@ -942,7 +606,6 @@ protected Rectangle getControlsHelpBounds() { int panelWidth = this.guiLeft - CONTROLS_HELP_RIGHT_MARGIN - CONTROLS_HELP_LEFT_MARGIN; if (panelWidth < 60) panelWidth = 60; - int panelRight = -CONTROLS_HELP_RIGHT_MARGIN; int panelLeft = -this.guiLeft + CONTROLS_HELP_LEFT_MARGIN; // Position relative to screen bottom @@ -974,6 +637,7 @@ public List getJEIExclusionArea() { if (filterBounds.width > 0) areas.add(filterBounds); // Add terminal style button bounds + // TODO: move terminal style to the filterPanelManager if (this.terminalStyleButton != null && this.terminalStyleButton.visible) { areas.add(new Rectangle( this.terminalStyleButton.x, @@ -992,66 +656,6 @@ public List getJEIExclusionArea() { return areas; } - protected void syncHoverStateFromContext(int relMouseX, int relMouseY) { - this.hoveredCell = renderContext.hoveredCell; - this.hoverType = renderContext.hoverType; - this.hoveredStorageLine = renderContext.hoveredStorageLine; - this.hoveredLineIndex = renderContext.hoveredLineIndex; - this.hoveredContentStack = renderContext.hoveredContentStack; - this.hoveredCellCell = renderContext.hoveredCellCell; - this.hoveredCellStorage = renderContext.hoveredCellStorage; - this.hoveredCellSlotIndex = renderContext.hoveredCellSlotIndex; - this.hoveredContentSlotIndex = renderContext.hoveredContentSlotIndex; - this.hoveredContentX = renderContext.hoveredContentX; - this.hoveredContentY = renderContext.hoveredContentY; - this.hoveredPartitionSlotIndex = renderContext.hoveredPartitionSlotIndex; - this.hoveredPartitionCell = renderContext.hoveredPartitionCell; - - // Storage bus tab hover state - this.hoveredStorageBus = renderContext.hoveredStorageBus; - this.hoveredStorageBusPartitionSlot = renderContext.hoveredStorageBusPartitionSlot; - this.hoveredStorageBusContentSlot = renderContext.hoveredStorageBusContentSlot; - this.hoveredClearButtonStorageBus = renderContext.hoveredClearButtonStorageBus; - this.hoveredIOModeButtonStorageBus = renderContext.hoveredIOModeButtonStorageBus; - this.hoveredPartitionAllButtonStorageBus = renderContext.hoveredPartitionAllButtonStorageBus; - - // Cell partition button hover state - this.hoveredPartitionAllButtonCell = renderContext.hoveredPartitionAllButtonCell; - this.hoveredClearPartitionButtonCell = renderContext.hoveredClearPartitionButtonCell; - - this.partitionSlotTargets.clear(); - for (RenderContext.PartitionSlotTarget target : renderContext.partitionSlotTargets) { - this.partitionSlotTargets.add(new PartitionSlotTarget( - target.cell, target.slotIndex, target.x, target.y, target.width, target.height)); - } - - // Storage bus partition slot targets for JEI - this.storageBusPartitionSlotTargets.clear(); - for (RenderContext.StorageBusPartitionSlotTarget target : renderContext.storageBusPartitionSlotTargets) { - this.storageBusPartitionSlotTargets.add(new StorageBusPartitionSlotTarget( - target.storageBus, target.slotIndex, target.x, target.y, target.width, target.height)); - } - - // Temp cell partition slot targets for JEI - this.tempCellPartitionSlotTargets.clear(); - for (RenderContext.TempCellPartitionSlotTarget target : renderContext.tempCellPartitionSlotTargets) { - this.tempCellPartitionSlotTargets.add(new JeiGhostHandler.TempCellPartitionSlotTarget( - target.cell, target.tempSlotIndex, target.partitionSlotIndex, target.x, target.y, target.width, target.height)); - } - - // Detect hovered upgrade icon (absolute mouse coords) - int absMouseX = relMouseX + this.guiLeft; - int absMouseY = relMouseY + this.guiTop; - this.hoveredUpgradeIcon = null; - - for (RenderContext.UpgradeIconTarget target : renderContext.upgradeIconTargets) { - if (target.isMouseOver(absMouseX, absMouseY)) { - this.hoveredUpgradeIcon = target; - break; - } - } - } - @Override public void drawBG(int offsetX, int offsetY, int mouseX, int mouseY) { GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); @@ -1077,7 +681,7 @@ public void drawBG(int offsetX, int offsetY, int mouseX, int mouseY) { int bottomY = offsetY + 18 + this.rowsVisible * ROW_HEIGHT; this.drawTexturedModalRect(offsetX, bottomY, 0, 158, this.xSize, 98); - drawTabs(offsetX, offsetY, mouseX, mouseY); + tabManager.drawTabs(this.guiLeft, offsetX, offsetY, mouseX, mouseY, this.itemRender, this.mc); this.bindTexture("guis/bus.png"); if (this.hasToolbox()) { this.drawTexturedModalRect(offsetX + this.xSize + 1, offsetY + this.ySize - 90, 178, 184 - 90, 68, 68); @@ -1088,65 +692,15 @@ public boolean hasToolbox() { return ((ContainerCellTerminalBase) this.inventorySlots).hasToolbox(); } - protected void drawTabs(int offsetX, int offsetY, int mouseX, int mouseY) { - TabRenderingHandler.TabRenderContext ctx = new TabRenderingHandler.TabRenderContext( - this.guiLeft, offsetX, offsetY, mouseX, mouseY, - TAB_WIDTH, TAB_HEIGHT, TAB_Y_OFFSET, - currentTab, this.itemRender, this.mc); - - TabRenderingHandler.TabIconProvider iconProvider = new TabRenderingHandler.TabIconProvider() { - @Override - public ItemStack getTabIcon(int tab) { - return GuiCellTerminalBase.this.getTabIcon(tab); - } - - @Override - public ItemStack getStorageBusIcon() { - return tabIconStorageBus; - } - - @Override - public ItemStack getInventoryIcon() { - return tabIconInventory; - } - - @Override - public ItemStack getPartitionIcon() { - return tabIconPartition; - } - }; - - this.hoveredTab = TabRenderingHandler.drawTabs(ctx, iconProvider).hoveredTab; - } - - protected ItemStack getTabIcon(int tab) { - switch (tab) { - case GuiConstants.TAB_TERMINAL: - return tabIconTerminal; - case GuiConstants.TAB_INVENTORY: - return tabIconInventory; - case GuiConstants.TAB_PARTITION: - return tabIconPartition; - case GuiConstants.TAB_TEMP_AREA: - return tabIconTempArea; - case GuiConstants.TAB_STORAGE_BUS_INVENTORY: - case GuiConstants.TAB_STORAGE_BUS_PARTITION: - // These use composite icons, but return storage bus as fallback - return tabIconStorageBus; - case GuiConstants.TAB_NETWORK_TOOLS: - return tabIconNetworkTool; - default: - return ItemStack.EMPTY; - } - } - @Override protected void handleMouseClick(Slot slot, int slotIdx, int mouseButton, ClickType clickType) { - // Intercept shift-clicks on upgrade items in player inventory to insert into cells or storage buses + // Intercept shift-clicks on upgrade items in player inventory to insert into + // the first visible cell/bus. Delegates to the active tab widget for tab-specific logic. if (clickType == ClickType.QUICK_MOVE && slot != null && slot.getHasStack()) { ItemStack slotStack = slot.getStack(); - if (slotStack.getItem() instanceof IUpgradeModule) { + if (slotStack.getItem() instanceof IUpgradeModule + && ((IUpgradeModule) slotStack.getItem()).getType(slotStack) != null) { // Check if upgrade insertion is enabled if (CellTerminalServerConfig.isInitialized() && !CellTerminalServerConfig.getInstance().isUpgradeInsertEnabled()) { @@ -1155,38 +709,9 @@ protected void handleMouseClick(Slot slot, int slotIdx, int mouseButton, ClickTy return; } - // On storage bus tabs, try to insert into storage buses - if (currentTab == GuiConstants.TAB_STORAGE_BUS_INVENTORY || currentTab == GuiConstants.TAB_STORAGE_BUS_PARTITION) { - StorageBusInfo targetBus = findFirstVisibleStorageBusThatCanAcceptUpgrade(slotStack); - - if (targetBus != null) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketUpgradeStorageBus( - targetBus.getId(), - true, - slot.getSlotIndex() - )); - - return; - } - } else { - // On cell tabs, try to insert into cells - CellInfo targetCell = findFirstVisibleCellThatCanAcceptUpgrade(slotStack); - - if (targetCell != null) { - StorageInfo storage = dataManager.getStorageMap().get(targetCell.getParentStorageId()); - - if (storage != null) { - // Pass the slot index so server knows where to take the upgrade from - CellTerminalNetwork.INSTANCE.sendToServer(new PacketUpgradeCell( - storage.getId(), - targetCell.getSlot(), - true, - slot.getSlotIndex() - )); - - return; - } - } + AbstractTabWidget activeTab = tabManager.getActiveTab(); + if (activeTab != null && activeTab.handleInventorySlotShiftClick(slotStack, slot.getSlotIndex())) { + return; } } } @@ -1211,101 +736,18 @@ protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOEx return; } - // Handle rename field - click outside saves and closes - if (this.subnetOverviewRenderer != null && this.subnetOverviewRenderer.isEditing()) { - SubnetOverviewRenderer.HoverZone zone = this.subnetOverviewRenderer.getHoveredZone(); - SubnetInfo hoveredSubnet = this.subnetOverviewRenderer.getHoveredSubnet(); - SubnetInfo editingSubnet = this.subnetOverviewRenderer.getEditingSubnet(); - - // If clicking on a different zone or different subnet, save and close - boolean isClickOnEditedName = zone == SubnetOverviewRenderer.HoverZone.NAME - && hoveredSubnet != null - && editingSubnet != null - && hoveredSubnet.getId() == editingSubnet.getId(); - - if (!isClickOnEditedName) { - // Save the rename - SubnetInfo subnet = this.subnetOverviewRenderer.getEditingSubnet(); - String newName = this.subnetOverviewRenderer.stopEditing(); - if (newName != null && subnet != null) sendSubnetRenamePacket(subnet, newName); - // Continue processing the click - } - } - - // Handle inline rename editor - click outside saves and closes - if (this.inlineRenameEditor != null && this.inlineRenameEditor.isEditing()) { - Renameable editingTarget = this.inlineRenameEditor.getEditingTarget(); - Renameable hoveredTarget = getHoveredRenameable(mouseX - guiLeft); - boolean isClickOnEditedTarget = hoveredTarget != null - && editingTarget != null - && hoveredTarget.getRenameTargetType() == editingTarget.getRenameTargetType() - && hoveredTarget.getRenameId() == editingTarget.getRenameId() - && hoveredTarget.getRenameSecondaryId() == editingTarget.getRenameSecondaryId(); - - if (!isClickOnEditedTarget) { - Renameable target = this.inlineRenameEditor.getEditingTarget(); - String newName = this.inlineRenameEditor.stopEditing(); - if (newName != null && target != null) sendRenamePacket(target, newName); - // Continue processing the click - } - } - - // Handle subnet overview mode clicks - if (this.isInSubnetOverviewMode && this.hoveredSubnetEntryIndex >= 0) { - if (this.hoveredSubnetEntryIndex < this.subnetLines.size()) { - Object line = this.subnetLines.get(this.hoveredSubnetEntryIndex); - SubnetOverviewRenderer.HoverZone zone = this.subnetOverviewRenderer.getHoveredZone(); - - // Get the subnet from either the header or connection row - SubnetInfo subnet = null; - if (line instanceof SubnetInfo) { - subnet = (SubnetInfo) line; - } else if (line instanceof SubnetConnectionRow) { - subnet = ((SubnetConnectionRow) line).getSubnet(); - } - - if (subnet != null) { - handleSubnetEntryClick(subnet, zone, mouseButton); - - return; - } - } - } - - // Handle search field clicks - if (this.searchField != null) { - // Right-click clears the field and focuses it - if (mouseButton == 1 && this.searchField.isMouseIn(mouseX, mouseY)) { - this.searchField.setText(""); - this.searchField.setFocused(true); - - return; - } - - // Left-click: check for double-click to open modal - if (mouseButton == 0 && this.searchField.isMouseIn(mouseX, mouseY)) { - long currentTime = System.currentTimeMillis(); - - if (currentTime - lastSearchFieldClickTime < DOUBLE_CLICK_THRESHOLD) { - // Double-click: open modal search bar - if (modalSearchBar != null) modalSearchBar.open(this.searchField.y); - lastSearchFieldClickTime = 0; - - return; - } - - lastSearchFieldClickTime = currentTime; - } - - this.searchField.mouseClicked(mouseX, mouseY, mouseButton); - } + // Handle inline rename: clicking outside the rename field saves and closes it + // (does not consume the click, let it propagate to potentially start a new rename) + InlineRenameManager.getInstance().handleClickOutside(mouseX - guiLeft, mouseY - guiTop); - if (priorityFieldManager != null && priorityFieldManager.handleClick(mouseX, mouseY, mouseButton)) return; + // Handle search field clicks (right-click clear, double-click modal, regular focus) + if (this.searchFieldHandler != null && this.searchFieldHandler.handleClick(mouseX, mouseY, mouseButton)) return; - // Handle upgrade icon clicks (extraction) - if (mouseButton == 0 && handleUpgradeIconClick()) return; + // Handle priority field clicks (only visible in certain tabs) + if (PriorityFieldManager.getInstance().handleClick(mouseX, mouseY, mouseButton)) return; - if (mouseButton == 0 && handleUpgradeClick(mouseX, mouseY)) return; + // Handle upgrade insertion (player holding upgrade + left-click on cell/bus) + if (mouseButton == 0 && handleWidgetUpgradeClick(mouseX, mouseY)) return; if (inventoryPopup != null) { if (inventoryPopup.handleClick(mouseX, mouseY, mouseButton)) return; @@ -1325,466 +767,172 @@ protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOEx return; } - // Right-click on a renameable target starts inline rename - if (mouseButton == 1 && !isInSubnetOverviewMode) { - int relMouseX = mouseX - guiLeft; - int relMouseY = mouseY - guiTop; - Renameable target = getHoveredRenameable(relMouseX); + // Handle tab header clicks via TabManager + if (tabManager.handleClick(mouseX, mouseY, guiLeft, guiTop)) return; + + super.mouseClicked(mouseX, mouseY, mouseButton); - if (target != null && target.isRenameable() - && relMouseY >= GuiConstants.CONTENT_START_Y - && relMouseY < GuiConstants.CONTENT_START_Y + rowsVisible * ROW_HEIGHT) { - int row = (relMouseY - GuiConstants.CONTENT_START_Y) / ROW_HEIGHT; - int rowY = GuiConstants.CONTENT_START_Y + row * ROW_HEIGHT; + // Delegate content area clicks to the active tab widget + int relMouseX = mouseX - guiLeft; + int relMouseY = mouseY - guiTop; + AbstractTabWidget activeTab = tabManager.getActiveTab(); + if (activeTab != null) activeTab.handleClick(relMouseX, relMouseY, mouseButton); + } - // Determine X position and right edge based on target type and current tab - int renameX = getRenameFieldX(target); - int renameRightEdge = getRenameFieldRightEdge(target); - inlineRenameEditor.startEditing(target, rowY, renameX, renameRightEdge); + // ---- TabManager.TabSwitchListener implementation ---- - return; - } - } + @Override + public void onPreSwitch(int oldTab) { + // No special action needed here for tab transitions. + } - if (clickHandler.handleTabClick(mouseX, mouseY, guiLeft, guiTop, currentTab, createClickCallback())) return; + @Override + public void onPostSwitch(int newTab) { + getScrollBar().setRange(0, 0, 1); // Reset scrollbar + updateScrollbarForCurrentTab(); // Set new scrollbar range + updateSearchModeButtonVisibility(); // Show/hide search mode button + initFilterButtons(); // Filter buttons differ per tab + applyFiltersToDataManager(); // ^ which means we need to apply them + onSearchTextChanged(); // Then apply the search filter - super.mouseClicked(mouseX, mouseY, mouseButton); + // Update back button appearance based on whether we're in subnet overview + if (this.subnetBackButton != null) { + this.subnetBackButton.setInOverviewMode(isInSubnetOverviewMode()); + } - if (currentTab == GuiConstants.TAB_TERMINAL) { - clickHandler.handleTerminalTabClick(mouseX, mouseY, mouseButton, guiLeft, guiTop, - rowsVisible, getScrollBar().getCurrentScroll(), dataManager.getLines(), - dataManager.getStorageMap(), dataManager.getTerminalDimension(), createClickCallback()); - } else if (currentTab == GuiConstants.TAB_INVENTORY || currentTab == GuiConstants.TAB_PARTITION) { - if (handleCellPartitionButtonClick(mouseButton)) return; - - int relMouseX = mouseX - guiLeft; - clickHandler.handleCellTabClick(currentTab, relMouseX, hoveredCellCell, hoveredContentSlotIndex, - hoveredPartitionCell, hoveredPartitionSlotIndex, hoveredCellStorage, hoveredCellSlotIndex, - hoveredStorageLine, hoveredLineIndex, dataManager.getStorageMap(), dataManager.getTerminalDimension(), createClickCallback()); - } else if (currentTab == GuiConstants.TAB_TEMP_AREA) { - handleTempAreaTabClick(mouseButton); - } else if (currentTab == GuiConstants.TAB_STORAGE_BUS_INVENTORY || currentTab == GuiConstants.TAB_STORAGE_BUS_PARTITION) { - handleStorageBusTabClick(mouseX, mouseY, mouseButton); - } else if (currentTab == GuiConstants.TAB_NETWORK_TOOLS) { - handleNetworkToolsTabClick(mouseButton); + // Only notify server of real tab changes, not subnet overlay transitions + if (newTab >= 0) { + CellTerminalNetwork.INSTANCE.sendToServer(new PacketTabChange(newTab)); } } /** - * Handle clicks on the Temp Area tab. + * Handle upgrade insertion when the player is holding an upgrade item and left-clicks. + * Delegates to the active tab widget for tab-specific logic. */ - protected void handleTempAreaTabClick(int mouseButton) { - if (mouseButton != 0) return; + protected boolean handleWidgetUpgradeClick(int mouseX, int mouseY) { + ItemStack heldStack = mc.player.inventory.getItemStack(); + if (heldStack.isEmpty()) return false; + if (!(heldStack.getItem() instanceof IUpgradeModule)) return false; - RenderContext ctx = getRenderContext(); + // Distinguish real upgrades from storage components that also implement IUpgradeModule + if (((IUpgradeModule) heldStack.getItem()).getType(heldStack) == null) return false; - // Handle partition-all button click (green button) - if (ctx.hoveredTempCellPartitionAllIndex >= 0 && ctx.hoveredPartitionAllButtonCell != null) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketTempCellPartitionAction( - ctx.hoveredTempCellPartitionAllIndex, - PacketTempCellPartitionAction.Action.SET_ALL_FROM_CONTENTS)); + // Check if upgrade insertion is enabled + if (CellTerminalServerConfig.isInitialized() + && !CellTerminalServerConfig.getInstance().isUpgradeInsertEnabled()) { + MessageHelper.error("cellterminal.error.upgrade_insert_disabled"); - return; + return true; // Consume click to prevent other handlers } - // Handle clear partition button click (red button) - if (ctx.hoveredTempCellClearPartitionIndex >= 0 && ctx.hoveredClearPartitionButtonCell != null) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketTempCellPartitionAction( - ctx.hoveredTempCellClearPartitionIndex, - PacketTempCellPartitionAction.Action.CLEAR_ALL)); + AbstractTabWidget activeTab = tabManager.getActiveTab(); + if (activeTab == null) return false; - return; + boolean isShiftClick = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + if (isShiftClick) return activeTab.handleShiftUpgradeClick(heldStack); + + int relMouseX = mouseX - guiLeft; + int relMouseY = mouseY - guiTop; + Object hoveredData = activeTab.getDataForHoveredRow(relMouseX, relMouseY); + + // Don't intercept upgrade clicks on content/partition rows - the partition + // slot click handler should take priority, allowing upgrades to be set as partition items. + // Upgrade insertion is only supported via headers, cell icons, and terminal lines. + if (hoveredData instanceof CellContentRow || hoveredData instanceof StorageBusContentRow) { + return false; } - // Handle clicking on a partition slot (add/remove item from partition) - if (ctx.hoveredPartitionSlotIndex >= 0 && ctx.hoveredPartitionCell != null) { - // Find the temp slot index for this cell - int tempSlotIndex = findTempSlotIndexForCell(ctx.hoveredPartitionCell); - if (tempSlotIndex >= 0) { - ItemStack heldStack = mc.player.inventory.getItemStack(); - List partitions = ctx.hoveredPartitionCell.getPartition(); - boolean slotOccupied = ctx.hoveredPartitionSlotIndex < partitions.size() - && !partitions.get(ctx.hoveredPartitionSlotIndex).isEmpty(); - - if (!heldStack.isEmpty()) { - // Add item to partition slot - CellTerminalNetwork.INSTANCE.sendToServer(new PacketTempCellPartitionAction( - tempSlotIndex, - PacketTempCellPartitionAction.Action.ADD_ITEM, - ctx.hoveredPartitionSlotIndex, - heldStack)); + return activeTab.handleUpgradeClick(hoveredData, heldStack, false); + } - return; - } + public void rebuildAndUpdateScrollbar() { + dataManager.rebuildLines(); + updateScrollbarForCurrentTab(); + } + + @Override + protected void actionPerformed(GuiButton btn) throws IOException { + if (btn == terminalStyleButton) { + // TODO: maybe add the guard to actionPerformed (or click) as a whole + // Guard against repeated clicks while mouse is still down after initGui recreates buttons + long now = System.currentTimeMillis(); + if (now - lastStyleButtonClickTime < STYLE_BUTTON_COOLDOWN) return; - if (slotOccupied) { - // Remove item from partition slot - CellTerminalNetwork.INSTANCE.sendToServer(new PacketTempCellPartitionAction( - tempSlotIndex, - PacketTempCellPartitionAction.Action.REMOVE_ITEM, - ctx.hoveredPartitionSlotIndex)); + lastStyleButtonClickTime = now; + terminalStyleButton.setStyle(CellTerminalClientConfig.getInstance().cycleTerminalStyle()); + this.buttonList.clear(); + this.initGui(); - return; - } - } + return; } - // Handle clicking on a content slot (toggle partition for that item) - if (ctx.hoveredContentSlotIndex >= 0 && ctx.hoveredCellCell != null) { - int tempSlotIndex = findTempSlotIndexForCell(ctx.hoveredCellCell); - if (tempSlotIndex >= 0) { - List contents = ctx.hoveredCellCell.getContents(); - if (ctx.hoveredContentSlotIndex < contents.size() - && !contents.get(ctx.hoveredContentSlotIndex).isEmpty()) { - // Toggle partition for this content item - CellTerminalNetwork.INSTANCE.sendToServer(new PacketTempCellPartitionAction( - tempSlotIndex, - PacketTempCellPartitionAction.Action.TOGGLE_ITEM, - contents.get(ctx.hoveredContentSlotIndex))); + if (btn == searchModeButton) { + // Cycle search mode, persist it, and reapply filter + currentSearchMode = searchModeButton.cycleMode(); + CellTerminalClientConfig.getInstance().setSearchMode(currentSearchMode); + onSearchTextChanged(); - return; - } - } + return; } - // Handle Send button click - if (ctx.hoveredTempCellSendButton != null) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketTempCellAction( - PacketTempCellAction.Action.SEND, ctx.hoveredTempCellSendButton.getTempSlotIndex())); + if (btn == subnetBackButton) { + handleSubnetBackButtonClick(); return; } - // Handle clicking on a temp cell slot (for insert/extract) - // Only consumes the click if an actual action was performed - if (ctx.hoveredTempCellSlot != null) { - ItemStack heldStack = mc.player.inventory.getItemStack(); + /* + if (btn == subnetVisibilityButton) { + // Cycle subnet visibility mode and persist it + currentSubnetVisibility = subnetVisibilityButton.cycleMode(); + CellTerminalClientConfig.getInstance().setSubnetVisibility(currentSubnetVisibility); + + return; + } + */ - if (ctx.hoveredTempCellSlot.isEmpty() && !heldStack.isEmpty()) { - // Empty slot clicked with item in hand: insert from cursor - // Don't send the itemStack - let server use player.inventory.getItemStack() - CellTerminalNetwork.INSTANCE.sendToServer(new PacketTempCellAction( - PacketTempCellAction.Action.INSERT, ctx.hoveredTempCellSlot.getTempSlotIndex())); + // Handle filter button clicks + if (btn instanceof GuiFilterButton) { + GuiFilterButton filterBtn = (GuiFilterButton) btn; + if (filterPanelManager.handleClick(filterBtn)) { + applyFiltersToDataManager(); + rebuildAndUpdateScrollbar(); return; } + } - if (!ctx.hoveredTempCellSlot.isEmpty() && heldStack.isEmpty()) { - // Occupied slot clicked with empty hand: extract - // Shift-click: send directly to inventory - boolean toInventory = isShiftKeyDown(); - CellTerminalNetwork.INSTANCE.sendToServer(new PacketTempCellAction( - PacketTempCellAction.Action.EXTRACT, ctx.hoveredTempCellSlot.getTempSlotIndex(), toInventory)); + // Handle slot limit button clicks + if (btn instanceof GuiSlotLimitButton) { + GuiSlotLimitButton slotLimitBtn = (GuiSlotLimitButton) btn; + if (filterPanelManager.handleSlotLimitClick(slotLimitBtn)) { + rebuildAndUpdateScrollbar(); return; } - - // No action taken on cell slot - fall through to header selection } - // Handle clicking on a temp cell header (for selection) - // This ONLY triggers when clicking on the actual header row, not on content/partition rows - // hoveredTempCellHeader is set only for direct TempCellInfo hover - if (ctx.hoveredTempCellHeader != null && ctx.hoveredTempCellHeader.hasCell()) { - int slotIndex = ctx.hoveredTempCellHeader.getTempSlotIndex(); - - if (selectedTempCellSlots.contains(slotIndex)) { - selectedTempCellSlots.remove(slotIndex); - } else { - // Validate that new selection is same type as existing selection - if (!selectedTempCellSlots.isEmpty()) { - TempCellInfo existingTempCell = findExistingSelectedTempCell(); - - if (existingTempCell != null && existingTempCell.getCellInfo() != null) { - CellInfo existingCell = existingTempCell.getCellInfo(); - CellInfo newCell = ctx.hoveredTempCellHeader.getCellInfo(); - - boolean sameType = (newCell.isFluid() == existingCell.isFluid()) - && (newCell.isEssentia() == existingCell.isEssentia()); + super.actionPerformed(btn); + } - if (!sameType) { - MessageHelper.error("gui.cellterminal.temp_area.mixed_cell_selection"); + @Override + protected void keyTyped(char typedChar, int keyCode) throws IOException { + // Handle inline rename keys (Esc cancels, Enter confirms, typing updates field) + if (InlineRenameManager.getInstance().handleKey(typedChar, keyCode)) return; - return; - } - } - } + // Handle network tool confirmation modal (blocks all other input) + if (networkToolModal != null) { + if (networkToolModal.handleKeyTyped(keyCode)) return; + } - selectedTempCellSlots.add(slotIndex); - } - } - } - - /** - * Find an existing selected temp cell for type validation. - */ - protected TempCellInfo findExistingSelectedTempCell() { - for (Integer slotIndex : selectedTempCellSlots) { - for (Object line : dataManager.getTempAreaLines()) { - if (line instanceof TempCellInfo) { - TempCellInfo tempCell = (TempCellInfo) line; - if (tempCell.getTempSlotIndex() == slotIndex && tempCell.hasCell()) return tempCell; - } - } - } - - return null; - } - - /** - * Find the temp slot index for a given cell by searching through temp area lines. - */ - protected int findTempSlotIndexForCell(CellInfo cell) { - for (Object line : dataManager.getTempAreaLines()) { - if (line instanceof TempCellInfo) { - TempCellInfo tempCell = (TempCellInfo) line; - if (tempCell.getCellInfo() == cell) return tempCell.getTempSlotIndex(); - } - } - - return -1; - } - - /** - * Handle clicks on the Network Tools tab. - */ - protected void handleNetworkToolsTabClick(int mouseButton) { - if (mouseButton != 0) return; - - // Handle launch button click - RenderContext ctx = getRenderContext(); - if (ctx.hoveredNetworkToolLaunchButton != null) { - showNetworkToolConfirmation(ctx.hoveredNetworkToolLaunchButton); - } - } - - /** - * Handle partition-all and clear-partition button clicks in Tabs 2 and 3. - */ - protected boolean handleCellPartitionButtonClick(int mouseButton) { - if (mouseButton != 0) return false; - - if (currentTab == GuiConstants.TAB_INVENTORY && hoveredPartitionAllButtonCell != null) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketPartitionAction( - hoveredPartitionAllButtonCell.getParentStorageId(), hoveredPartitionAllButtonCell.getSlot(), - PacketPartitionAction.Action.SET_ALL_FROM_CONTENTS)); - return true; - } - - if (currentTab == GuiConstants.TAB_PARTITION && hoveredClearPartitionButtonCell != null) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketPartitionAction( - hoveredClearPartitionButtonCell.getParentStorageId(), hoveredClearPartitionButtonCell.getSlot(), - PacketPartitionAction.Action.CLEAR_ALL)); - return true; - } - - return false; - } - - /** - * Handle click events on storage bus tabs. - */ - protected void handleStorageBusTabClick(int mouseX, int mouseY, int mouseButton) { - if (mouseButton == 0 && handleStorageBusUpgradeClick()) return; - - int relMouseX = mouseX - guiLeft; - StorageBusClickHandler.ClickContext ctx = StorageBusClickHandler.ClickContext.from( - currentTab, relMouseX, hoveredStorageBus, hoveredStorageBusPartitionSlot, hoveredStorageBusContentSlot, - hoveredClearButtonStorageBus, hoveredIOModeButtonStorageBus, hoveredPartitionAllButtonStorageBus, - hoveredContentStack, selectedStorageBusIds, dataManager.getStorageBusMap()); - - if (storageBusClickHandler.handleClick(ctx, mouseButton)) { - rebuildAndUpdateScrollbar(); - } - } - - protected TerminalClickHandler.Callback createClickCallback() { - return new TerminalClickHandler.Callback() { - @Override - public void onTabChanged(int tab) { - // Exit subnet overview mode when switching tabs - if (isInSubnetOverviewMode) exitSubnetOverviewMode(); - - // Cancel any active inline rename when switching tabs - if (inlineRenameEditor != null && inlineRenameEditor.isEditing()) { - inlineRenameEditor.cancelEditing(); - } - - // Save scroll position for current tab before switching - TabStateManager.TabType oldTabType = TabStateManager.TabType.fromIndex(currentTab); - TabStateManager.getInstance().setScrollPosition(oldTabType, getScrollBar().getCurrentScroll()); - - currentTab = tab; - getScrollBar().setRange(0, 0, 1); - updateScrollbarForCurrentTab(); - updateSearchModeButtonVisibility(); - initFilterButtons(); // Reinitialize filter buttons for new tab - applyFiltersToDataManager(); - onSearchTextChanged(); // Reapply filter with tab-specific mode - - // Restore scroll position for new tab - TabStateManager.TabType newTabType = TabStateManager.TabType.fromIndex(tab); - int savedScroll = TabStateManager.getInstance().getScrollPosition(newTabType); - scrollToLine(savedScroll); - - // Notify server of tab change for polling optimization - CellTerminalNetwork.INSTANCE.sendToServer(new PacketTabChange(tab)); - } - - @Override - public void onStorageToggle(StorageInfo storage) { - rebuildAndUpdateScrollbar(); - } - - @Override - public void openInventoryPopup(CellInfo cell, int mouseX, int mouseY) { - inventoryPopup = new PopupCellInventory(GuiCellTerminalBase.this, cell, mouseX, mouseY); - } - - @Override - public void openPartitionPopup(CellInfo cell, int mouseX, int mouseY) { - partitionPopup = new PopupCellPartition(GuiCellTerminalBase.this, cell, mouseX, mouseY); - } - - @Override - public void onTogglePartitionItem(CellInfo cell, ItemStack stack) { - GuiCellTerminalBase.this.onTogglePartitionItem(cell, stack); - } - - @Override - public void onAddPartitionItem(CellInfo cell, int slotIndex, ItemStack stack) { - GuiCellTerminalBase.this.onAddPartitionItem(cell, slotIndex, stack); - } - - @Override - public void onRemovePartitionItem(CellInfo cell, int slotIndex) { - GuiCellTerminalBase.this.onRemovePartitionItem(cell, slotIndex); - } - }; - } - - protected void rebuildAndUpdateScrollbar() { - dataManager.rebuildLines(); - updateScrollbarForCurrentTab(); - } - - @Override - protected void actionPerformed(GuiButton btn) throws IOException { - if (btn == terminalStyleButton) { - // Guard against repeated clicks while mouse is still down after initGui recreates buttons - long now = System.currentTimeMillis(); - if (now - lastStyleButtonClickTime < STYLE_BUTTON_COOLDOWN) return; - - lastStyleButtonClickTime = now; - terminalStyleButton.setStyle(CellTerminalClientConfig.getInstance().cycleTerminalStyle()); - this.buttonList.clear(); - this.initGui(); - - return; - } - - if (btn == searchModeButton) { - // Cycle search mode, persist it, and reapply filter - currentSearchMode = searchModeButton.cycleMode(); - CellTerminalClientConfig.getInstance().setSearchMode(currentSearchMode); - onSearchTextChanged(); - - return; - } - - if (btn == subnetBackButton) { - handleSubnetBackButtonClick(); - - return; - } - - /* - if (btn == subnetVisibilityButton) { - // Cycle subnet visibility mode and persist it - currentSubnetVisibility = subnetVisibilityButton.cycleMode(); - CellTerminalClientConfig.getInstance().setSubnetVisibility(currentSubnetVisibility); - - return; - } - */ - - // Handle filter button clicks - if (btn instanceof GuiFilterButton) { - GuiFilterButton filterBtn = (GuiFilterButton) btn; - if (filterPanelManager.handleClick(filterBtn)) { - applyFiltersToDataManager(); - rebuildAndUpdateScrollbar(); - - return; - } - } - - // Handle slot limit button clicks - if (btn instanceof GuiSlotLimitButton) { - GuiSlotLimitButton slotLimitBtn = (GuiSlotLimitButton) btn; - if (filterPanelManager.handleSlotLimitClick(slotLimitBtn)) { - rebuildAndUpdateScrollbar(); - - return; - } - } - - super.actionPerformed(btn); - } - - @Override - protected void keyTyped(char typedChar, int keyCode) throws IOException { - // Handle subnet rename field first (blocks other input when editing) - if (this.subnetOverviewRenderer != null && this.subnetOverviewRenderer.isEditing()) { - if (keyCode == Keyboard.KEY_ESCAPE) { - this.subnetOverviewRenderer.cancelEditing(); - return; - } - - if (keyCode == Keyboard.KEY_RETURN || keyCode == Keyboard.KEY_NUMPADENTER) { - // Confirm rename - get subnet before stopping (stopEditing clears it) - SubnetInfo subnet = this.subnetOverviewRenderer.getEditingSubnet(); - String newName = this.subnetOverviewRenderer.stopEditing(); - if (newName != null && subnet != null) sendSubnetRenamePacket(subnet, newName); - - return; - } - - if (this.subnetOverviewRenderer.handleKeyTyped(typedChar, keyCode)) return; - } - - // Handle inline rename editor (storage, cell, storage bus) - blocks all other input when editing - if (this.inlineRenameEditor != null && this.inlineRenameEditor.isEditing()) { - if (keyCode == Keyboard.KEY_ESCAPE) { - this.inlineRenameEditor.cancelEditing(); - return; - } - - if (keyCode == Keyboard.KEY_RETURN || keyCode == Keyboard.KEY_NUMPADENTER) { - Renameable target = this.inlineRenameEditor.getEditingTarget(); - String newName = this.inlineRenameEditor.stopEditing(); - if (newName != null && target != null) sendRenamePacket(target, newName); - - return; - } - - if (this.inlineRenameEditor.handleKeyTyped(typedChar, keyCode)) return; - } - - // Handle network tool confirmation modal (blocks all other input) - if (networkToolModal != null) { - if (networkToolModal.handleKeyTyped(keyCode)) return; - } - - // Handle modal search bar keyboard - if (modalSearchBar != null && modalSearchBar.isVisible()) { - if (modalSearchBar.handleKeyTyped(typedChar, keyCode)) return; + // Handle modal search bar keyboard + if (modalSearchBar != null && modalSearchBar.isVisible()) { + if (modalSearchBar.handleKeyTyped(typedChar, keyCode)) return; } // Handle priority field keyboard - if (priorityFieldManager != null) { - if (priorityFieldManager.handleKeyTyped(typedChar, keyCode)) return; - } + if (PriorityFieldManager.getInstance().handleKeyTyped(typedChar, keyCode)) return; // Esc key should close modals if (keyCode == Keyboard.KEY_ESCAPE) { @@ -1807,11 +955,10 @@ protected void keyTyped(char typedChar, int keyCode) throws IOException { // Handle search field keyboard input if (this.searchField != null && this.searchField.textboxKeyTyped(typedChar, keyCode)) return; - // Delegate keybind handling to the active tab controller - if (handleTabKeyTyped(keyCode)) return; + // Delegate key handling to the active tab widget via TabManager + if (tabManager.handleKey(keyCode)) return; - // Toggle subnet overview in last, as it's available everywhere - // And should not take priority over other handlers + // Toggle subnet overview: available everywhere and should not take priority over other handlers if (KeyBindings.SUBNET_OVERVIEW_TOGGLE.isActiveAndMatches(keyCode)) { handleSubnetBackButtonClick(); return; @@ -1820,174 +967,10 @@ protected void keyTyped(char typedChar, int keyCode) throws IOException { super.keyTyped(typedChar, keyCode); } - /** - * Delegate key press to the active tab controller. - * @return true if the key was handled - */ - protected boolean handleTabKeyTyped(int keyCode) { - ITabController controller = TabControllerRegistry.getController(currentTab); - if (controller == null) return false; - - TabContext tabContext = createTabContext(); - - // Special handling for Partition tab (needs scrollToLine callback) - if (currentTab == GuiConstants.TAB_PARTITION) { - return ((PartitionTabController) controller).handleKeyTyped(keyCode, tabContext); - } - - // Special handling for Temp Area tab (uses ADD_TO_STORAGE_BUS keybind with selection like storage buses) - if (currentTab == GuiConstants.TAB_TEMP_AREA) { - if (!KeyBindings.ADD_TO_STORAGE_BUS.isActiveAndMatches(keyCode)) return false; - - return TempAreaTabController.handleAddToTempCellKeybind( - selectedTempCellSlots, getSlotUnderMouse(), dataManager.getTempAreaLines()); - } - - // Special handling for Storage Bus Partition tab (needs access to selected bus IDs) - if (currentTab == GuiConstants.TAB_STORAGE_BUS_PARTITION) { - if (!KeyBindings.ADD_TO_STORAGE_BUS.isActiveAndMatches(keyCode)) return false; - - return StorageBusPartitionTabController.handleAddToStorageBusKeybind( - selectedStorageBusIds, getSlotUnderMouse(), dataManager.getStorageBusMap()); - } - - return controller.handleKeyTyped(keyCode, tabContext); - } - - /** - * Create a TabContext for the current GUI state. - */ - protected TabContext createTabContext() { - return new TabContext(dataManager, new TabContext.TabContextCallback() { - @Override public void onStorageToggle(StorageInfo storage) { - rebuildAndUpdateScrollbar(); - } - - @Override public void openInventoryPopup(CellInfo cell, int mouseX, int mouseY) { - inventoryPopup = new PopupCellInventory(GuiCellTerminalBase.this, cell, mouseX, mouseY); - } - - @Override public void openPartitionPopup(CellInfo cell, int mouseX, int mouseY) { - partitionPopup = new PopupCellPartition(GuiCellTerminalBase.this, cell, mouseX, mouseY); - } - - @Override public void onTogglePartitionItem(CellInfo cell, ItemStack stack) { - GuiCellTerminalBase.this.onTogglePartitionItem(cell, stack); - } - - @Override public void onAddPartitionItem(CellInfo cell, int slotIndex, ItemStack stack) { - GuiCellTerminalBase.this.onAddPartitionItem(cell, slotIndex, stack); - } - - @Override public void onRemovePartitionItem(CellInfo cell, int slotIndex) { - GuiCellTerminalBase.this.onRemovePartitionItem(cell, slotIndex); - } - - @Override public void scrollToLine(int lineIndex) { - GuiCellTerminalBase.this.scrollToLine(lineIndex); - } - }, dataManager.getTerminalDimension()); - } - - /** - * Handle upgrade icon click to extract upgrades from cells or storage buses. - * @return true if an upgrade icon click was handled - */ - protected boolean handleUpgradeIconClick() { - if (hoveredUpgradeIcon == null) return false; - - // Check if upgrade extraction is enabled - if (CellTerminalServerConfig.isInitialized() - && !CellTerminalServerConfig.getInstance().isUpgradeExtractEnabled()) { - MessageHelper.error("cellterminal.error.upgrade_extract_disabled"); - - return true; // Consume click to prevent other handlers - } - - boolean toInventory = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); - - if (hoveredUpgradeIcon.cell != null) { - CellTerminalNetwork.INSTANCE.sendToServer(PacketExtractUpgrade.forCell( - hoveredUpgradeIcon.cell.getParentStorageId(), - hoveredUpgradeIcon.cell.getSlot(), - hoveredUpgradeIcon.upgradeIndex, - toInventory - )); - } else if (hoveredUpgradeIcon.storageBus != null) { - CellTerminalNetwork.INSTANCE.sendToServer(PacketExtractUpgrade.forStorageBus( - hoveredUpgradeIcon.storageBus.getId(), - hoveredUpgradeIcon.upgradeIndex, - toInventory - )); - } else { - return false; - } - - return true; - } - - /** - * Handle upgrade click when player is holding an upgrade item. - */ - protected boolean handleUpgradeClick(int mouseX, int mouseY) { - UpgradeClickHandler.UpgradeClickContext ctx = new UpgradeClickHandler.UpgradeClickContext( - currentTab, hoveredCellCell, hoveredCellStorage, hoveredCellSlotIndex, - hoveredStorageLine, hoveredStorageBus, dataManager); - - return UpgradeClickHandler.handleUpgradeClick(ctx); - } - - /** - * Handle upgrade click on storage bus headers when player is holding an upgrade item. - * @return true if an upgrade click was handled - */ - protected boolean handleStorageBusUpgradeClick() { - return UpgradeClickHandler.handleStorageBusUpgradeClick(hoveredStorageBus); - } - - /** - * Find the first visible cell that can accept the given upgrade. - * Respects current tab's filtered and sorted line list. - * Checks both compatibility (upgrade type supported by cell) and available space. - * @param upgradeStack The upgrade item to check compatibility with - * @return The first CellInfo that can accept the upgrade, or null if none found - */ - protected CellInfo findFirstVisibleCellThatCanAcceptUpgrade(ItemStack upgradeStack) { - UpgradeClickHandler.UpgradeClickContext ctx = new UpgradeClickHandler.UpgradeClickContext( - currentTab, hoveredCellCell, hoveredCellStorage, hoveredCellSlotIndex, - hoveredStorageLine, hoveredStorageBus, dataManager); - - return UpgradeClickHandler.findFirstVisibleCellThatCanAcceptUpgrade(ctx, upgradeStack); - } - - /** - * Find the first cell in a specific storage that can accept the given upgrade. - * @param storage The storage to search in - * @param upgradeStack The upgrade item to check compatibility with - * @return The first CellInfo that can accept the upgrade, or null if none found - */ - protected CellInfo findFirstCellInStorageThatCanAcceptUpgrade(StorageInfo storage, ItemStack upgradeStack) { - return UpgradeClickHandler.findFirstCellInStorageThatCanAcceptUpgrade(storage, upgradeStack); - } - - /** - * Find the first visible storage bus that can accept the given upgrade. - * Only used on storage bus tabs. - * @param upgradeStack The upgrade item to check compatibility with - * @return The first StorageBusInfo that can accept the upgrade, or null if none found - */ - protected StorageBusInfo findFirstVisibleStorageBusThatCanAcceptUpgrade(ItemStack upgradeStack) { - UpgradeClickHandler.UpgradeClickContext ctx = new UpgradeClickHandler.UpgradeClickContext( - currentTab, hoveredCellCell, hoveredCellStorage, hoveredCellSlotIndex, - hoveredStorageLine, hoveredStorageBus, dataManager); - - return UpgradeClickHandler.findFirstVisibleStorageBusThatCanAcceptUpgrade(ctx, upgradeStack); - } - /** * Scroll to a specific line index. */ - protected void scrollToLine(int lineIndex) { + public void scrollToLine(int lineIndex) { int currentScroll = this.getScrollBar().getCurrentScroll(); // wheel() clamps delta to -1/+1, so we need to call it multiple times @@ -2021,141 +1004,56 @@ public void postUpdate(NBTTagCompound data) { // Restore saved scroll after the first update that provides line counts. if (!this.initialScrollRestored) { - TabStateManager.TabType tabType = TabStateManager.TabType.fromIndex(this.currentTab); + TabStateManager.TabType tabType = TabStateManager.TabType.fromIndex(tabManager.getCurrentTab()); int saved = TabStateManager.getInstance().getScrollPosition(tabType); scrollToLine(saved); this.initialScrollRestored = true; } } + // --- Subnet overview delegation --- + // All subnet interaction logic now lives in SubnetOverviewTabWidget. + // These methods implement SubnetOverviewContext and provide thin wrappers for the GUI. + /** * Handle subnet list update from server. - * Called when the server sends updated subnet connection data. + * Delegates to the subnet tab widget for parsing and display. */ public void handleSubnetListUpdate(NBTTagCompound data) { - this.subnetList.clear(); - - // Always add the main network as first entry - this.subnetList.add(SubnetInfo.createMainNetwork()); - - if (data.hasKey("subnets")) { - net.minecraft.nbt.NBTTagList subnetNbtList = data.getTagList("subnets", net.minecraftforge.common.util.Constants.NBT.TAG_COMPOUND); - for (int i = 0; i < subnetNbtList.tagCount(); i++) { - NBTTagCompound subnetNbt = subnetNbtList.getCompoundTagAt(i); - this.subnetList.add(new SubnetInfo(subnetNbt)); - } - } - - // Sort subnets: main network first, then favorites, then by position - this.subnetList.sort((a, b) -> { - // Main network always first - if (a.isMainNetwork()) return -1; - if (b.isMainNetwork()) return 1; - - // Favorites next - if (a.isFavorite() != b.isFavorite()) return a.isFavorite() ? -1 : 1; - - // Then by dimension - if (a.getDimension() != b.getDimension()) { - return Integer.compare(a.getDimension(), b.getDimension()); - } - - // Then by distance from origin - double distA = a.getPrimaryPos().distanceSq(0, 0, 0); - double distB = b.getPrimaryPos().distanceSq(0, 0, 0); - - return Double.compare(distA, distB); - }); - - // Build flattened line list for display (headers + connection rows) - buildSubnetLines(); - } - - /** - * Build the flattened subnet lines list from the sorted subnet list. - * Each subnet becomes a header row, followed by connection rows showing filters. - */ - private void buildSubnetLines() { - this.subnetLines.clear(); - - for (SubnetInfo subnet : this.subnetList) { - // Add header row - this.subnetLines.add(subnet); - - // Skip connection rows for main network - if (subnet.isMainNetwork()) continue; - - // Add connection rows with filters - List connectionRows = subnet.buildConnectionRows(9); - this.subnetLines.addAll(connectionRows); - } - } - - /** - * Get the list of subnets connected to the current network. - */ - public List getSubnetList() { - return subnetList; + tabManager.getSubnetTab().handleSubnetListUpdate(data); + updateScrollbarForCurrentTab(); } /** * Check if currently in subnet overview mode. */ public boolean isInSubnetOverviewMode() { - return isInSubnetOverviewMode; - } - - /** - * Enter subnet overview mode. - */ - public void enterSubnetOverviewMode() { - this.isInSubnetOverviewMode = true; - - // Update back button to show left arrow (back) - if (this.subnetBackButton != null) this.subnetBackButton.setInOverviewMode(true); - - // If we already have subnet data from a previous visit, rebuild lines and update scrollbar - // This prevents the flicker that occurs while waiting for server response - if (!this.subnetList.isEmpty()) { - buildSubnetLines(); - int totalLines = this.subnetLines.size(); - int maxScroll = Math.max(0, totalLines - this.rowsVisible); - this.getScrollBar().setRange(0, maxScroll, 1); - } - - // Request subnet list from server (will update with fresh data) - CellTerminalNetwork.INSTANCE.sendToServer(new com.cellterminal.network.PacketSubnetListRequest()); - } - - /** - * Exit subnet overview mode and return to normal terminal view. - */ - public void exitSubnetOverviewMode() { - this.isInSubnetOverviewMode = false; - - // Update back button to show right arrow (to overview) - if (this.subnetBackButton != null) this.subnetBackButton.setInOverviewMode(false); + return TabStateManager.isSubnetTab(tabManager.getCurrentTab()); } /** - * Handle back button click - toggle overview. + * Handle back button click - toggle subnet overview mode. */ protected void handleSubnetBackButtonClick() { - if (this.isInSubnetOverviewMode) { - // In overview mode - go back to the last viewed network (main or subnet) + if (isInSubnetOverviewMode()) { + // Exiting subnet overview: return to previous tab and refresh network data + tabManager.switchToTab(tabManager.getPreviousRealTab()); switchToNetwork(currentNetworkId); } else { - enterSubnetOverviewMode(); + // Entering subnet overview + tabManager.switchToTab(TabStateManager.TabType.SUBNET_OVERVIEW.getIndex()); } } - /** - * Switch to viewing a different network (main or subnet). - * @param networkId 0 for main network, subnet ID for subnets - */ + // --- SubnetOverviewContext implementation --- + + @Override public void switchToNetwork(long networkId) { this.currentNetworkId = networkId; - this.isInSubnetOverviewMode = false; + + // Exit subnet overview if it was active (e.g. clicking a Load button in overview) + if (isInSubnetOverviewMode()) tabManager.switchToTab(tabManager.getPreviousRealTab()); + // Reset data manager so the next update does a full rebuild with proper filters // instead of using snapshots from the old network context @@ -2168,288 +1066,39 @@ public void switchToNetwork(long networkId) { CellTerminalNetwork.INSTANCE.sendToServer(new PacketSwitchNetwork(networkId)); } - /** - * Handle click on a subnet entry in the overview. - */ - protected void handleSubnetEntryClick(SubnetInfo subnet, SubnetOverviewRenderer.HoverZone zone, int mouseButton) { - if (subnet == null) return; - - switch (zone) { - case STAR: - // Toggle favorite (including main network) - if (mouseButton == 0) { - boolean newFavorite = !subnet.isFavorite(); - subnet.setFavorite(newFavorite); - CellTerminalNetwork.INSTANCE.sendToServer(PacketSubnetAction.toggleFavorite(subnet.getId(), newFavorite)); - // Re-sort subnet list to reflect favorite change and rebuild display - this.subnetList.sort((a, b) -> { - if (a.isMainNetwork()) return -1; - if (b.isMainNetwork()) return 1; - if (a.isFavorite() != b.isFavorite()) return a.isFavorite() ? -1 : 1; - if (a.getDimension() != b.getDimension()) { - return Integer.compare(a.getDimension(), b.getDimension()); - } - double distA = a.getPrimaryPos().distanceSq(0, 0, 0); - double distB = b.getPrimaryPos().distanceSq(0, 0, 0); - - return Double.compare(distA, distB); - }); - buildSubnetLines(); - updateScrollbarForCurrentTab(); - } - break; - - case NAME: - if (mouseButton == 1 && !subnet.isMainNetwork()) { - // Right-click - start renaming - int rowY = this.subnetOverviewRenderer.getRowYForSubnet(subnet, this.subnetLines, getScrollBar().getCurrentScroll()); - if (rowY >= 0) this.subnetOverviewRenderer.startEditing(subnet, rowY); - } else if (mouseButton == 0 && !subnet.isMainNetwork()) { - // Left-click - double-click highlights in world - long currentTime = System.currentTimeMillis(); - if (subnet.getId() == lastSubnetClickId - && currentTime - lastSubnetClickTime < DOUBLE_CLICK_THRESHOLD) { - highlightSubnetInWorld(subnet); - lastSubnetClickTime = 0; - lastSubnetClickId = -1; - } else { - lastSubnetClickTime = currentTime; - lastSubnetClickId = subnet.getId(); - } - } - break; - - case LOAD_BUTTON: - // Load button clicked - switch to this subnet (including main network) - if (mouseButton == 0 && subnet.isAccessible() && subnet.hasPower()) { - switchToNetwork(subnet.getId()); - } - break; - - case ENTRY: - default: - // Double-click on entry highlights in world (not for main network) - if (mouseButton == 0 && !subnet.isMainNetwork()) { - long currentTime = System.currentTimeMillis(); - if (subnet.getId() == lastSubnetClickId - && currentTime - lastSubnetClickTime < DOUBLE_CLICK_THRESHOLD) { - // Double-click - highlight in world - highlightSubnetInWorld(subnet); - lastSubnetClickTime = 0; - lastSubnetClickId = -1; - } else { - // First click - track for potential double-click - lastSubnetClickTime = currentTime; - lastSubnetClickId = subnet.getId(); - } - } - break; - } - } - - /** - * Highlight a subnet's primary position in the world. - */ - protected void highlightSubnetInWorld(SubnetInfo subnet) { - if (subnet == null || subnet.isMainNetwork()) return; - - // Check if in different dimension - if (subnet.getDimension() != Minecraft.getMinecraft().player.dimension) { - MessageHelper.error("cellterminal.error.different_dimension"); - - return; - } - - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketHighlightBlock(subnet.getPrimaryPos(), subnet.getDimension()) - ); - - // Send green chat message with block name and coordinates - MessageHelper.success("gui.cellterminal.highlighted", - subnet.getPrimaryPos().getX(), - subnet.getPrimaryPos().getY(), - subnet.getPrimaryPos().getZ(), - subnet.getDisplayName()); - } - - /** - * Send a packet to rename a subnet. - */ - protected void sendSubnetRenamePacket(SubnetInfo subnet, String newName) { - if (subnet == null || subnet.isMainNetwork()) return; - - CellTerminalNetwork.INSTANCE.sendToServer(PacketSubnetAction.rename(subnet.getId(), newName)); - - // Update local name immediately for responsiveness - subnet.setCustomName(newName.isEmpty() ? null : newName); - } - - /** - * Send a rename packet for any Renameable target (storage, cell, storage bus). - */ - protected void sendRenamePacket(Renameable target, String newName) { - if (target == null) return; - - switch (target.getRenameTargetType()) { - case STORAGE: - CellTerminalNetwork.INSTANCE.sendToServer( - PacketRenameAction.renameStorage(target.getRenameId(), newName)); - break; - case CELL: - CellTerminalNetwork.INSTANCE.sendToServer( - PacketRenameAction.renameCell(target.getRenameId(), target.getRenameSecondaryId(), newName)); - break; - case STORAGE_BUS: - CellTerminalNetwork.INSTANCE.sendToServer( - PacketRenameAction.renameStorageBus(target.getRenameId(), newName)); - break; - default: - break; - } - - // Update local name immediately for responsiveness - target.setCustomName(newName.isEmpty() ? null : newName); - } - - /** - * Get the currently hovered renameable target based on the current tab and hover state. - * Returns null if nothing renameable is hovered, or if the click position overlaps buttons. - * - * @param relMouseX Mouse X relative to GUI left edge - * @return The hovered Renameable, or null - */ - protected Renameable getHoveredRenameable(int relMouseX) { - // Storage header is renameable on tabs 0-2 (exclude expand button area at x >= 165) - if (hoveredStorageLine != null && relMouseX < GuiConstants.BUTTON_PARTITION_X) return hoveredStorageLine; - - // On terminal tab, cells show name text — rename if not in button area (E/I/P at x >= 135) - if (currentTab == GuiConstants.TAB_TERMINAL && hoveredCellCell != null && relMouseX < GuiConstants.BUTTON_EJECT_X) return hoveredCellCell; - - // On inventory/partition tabs, cells are icon+content rows — rename if not on content/partition slots - if ((currentTab == GuiConstants.TAB_INVENTORY || currentTab == GuiConstants.TAB_PARTITION) - && hoveredCellCell != null - && hoveredContentSlotIndex < 0 - && hoveredPartitionSlotIndex < 0) { - return hoveredCellCell; - } - - // Storage bus is renameable on tabs 3-4 (exclude IO mode button at x >= 115) - if (hoveredStorageBus != null && relMouseX < GuiConstants.BUTTON_IO_MODE_X) return hoveredStorageBus; - - return null; - } - - /** - * Get the X position for the inline rename field based on the target type. - * The field has 2px internal padding, so we offset by -2 to align the text - * with the original name position. - * Cells are further indented (on a tree branch), while storages and buses start at the icon + 20. - */ - protected int getRenameFieldX(Renameable target) { - // Subtract 2 so the text inside the field (at x + 2) aligns with where the name was drawn - if (target instanceof CellInfo) return GuiConstants.CELL_INDENT + 18 - 2; - - // StorageInfo and StorageBusInfo: name drawn at GUI_INDENT + 20 - return GuiConstants.GUI_INDENT + 20 - 2; - } - - /** - * Get the right edge for the inline rename field based on the target type and current tab. - * Each tab has different buttons at the right side, so the field must stop before them. - */ - protected int getRenameFieldRightEdge(Renameable target) { - // Storage buses (tabs 3-4) have IO mode button at BUTTON_IO_MODE_X (115) - if (target instanceof StorageBusInfo) return GuiConstants.BUTTON_IO_MODE_X - 4; - - // Cells on terminal tab (tab 0) have E/I/P buttons starting at BUTTON_EJECT_X (135) - if (target instanceof CellInfo && currentTab == GuiConstants.TAB_TERMINAL) return GuiConstants.BUTTON_EJECT_X - 4; - - // Storage headers and cells on tabs 1-2 have priority field at CONTENT_RIGHT_EDGE - FIELD_WIDTH - RIGHT_MARGIN - return GuiConstants.CONTENT_RIGHT_EDGE - PriorityFieldManager.FIELD_WIDTH - PriorityFieldManager.RIGHT_MARGIN - 4; + @Override + public void requestSubnetList() { + CellTerminalNetwork.INSTANCE.sendToServer(new com.cellterminal.network.PacketSubnetListRequest()); } @Override public void onGuiClosed() { // Persist the current scroll position for the active tab so it is restored when the GUI is reopened. - TabStateManager.TabType tabType = TabStateManager.TabType.fromIndex(this.currentTab); - TabStateManager.getInstance().setScrollPosition(tabType, this.getScrollBar().getCurrentScroll()); + tabManager.saveCurrentScrollPosition(); super.onGuiClosed(); } protected void updateScrollbarForCurrentTab() { - ITabController controller = TabControllerRegistry.getController(currentTab); - int lineCount = (controller != null) - ? controller.getLineCount(createTabContext()) - : dataManager.getLineCount(currentTab); - this.getScrollBar().setRange(0, Math.max(0, lineCount - this.rowsVisible), 1); - } - - // Callbacks from popups - - public void onPartitionAllClicked(CellInfo cell) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketPartitionAction( - cell.getParentStorageId(), - cell.getSlot(), - PacketPartitionAction.Action.SET_ALL_FROM_CONTENTS - )); - } - - public void onTogglePartitionItem(CellInfo cell, ItemStack stack) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketPartitionAction( - cell.getParentStorageId(), - cell.getSlot(), - PacketPartitionAction.Action.TOGGLE_ITEM, - stack - )); - } - - public void onRemovePartitionItem(CellInfo cell, int partitionSlot) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketPartitionAction( - cell.getParentStorageId(), - cell.getSlot(), - PacketPartitionAction.Action.REMOVE_ITEM, - partitionSlot - )); - } - - public void onAddPartitionItem(CellInfo cell, int partitionSlot, ItemStack stack) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketPartitionAction( - cell.getParentStorageId(), - cell.getSlot(), - PacketPartitionAction.Action.ADD_ITEM, - partitionSlot, - stack - )); - } + List lines = tabManager.getActiveLines(dataManager); + int lineCount = lines.size(); - public void onAddStorageBusPartitionItem(StorageBusInfo storageBus, int partitionSlot, ItemStack stack) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketStorageBusPartitionAction( - storageBus.getId(), - PacketStorageBusPartitionAction.Action.ADD_ITEM, - partitionSlot, - stack - )); - } + // Use the tab widget's visible item count (accounts for non-standard row heights) + int visibleItems = tabManager.getActiveVisibleItemCount(); - public void onAddTempCellPartitionItem(int tempSlotIndex, int partitionSlot, ItemStack stack) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketTempCellPartitionAction( - tempSlotIndex, - PacketTempCellPartitionAction.Action.ADD_ITEM, - partitionSlot, - stack - )); + this.getScrollBar().setRange(0, Math.max(0, lineCount - visibleItems), 1); } // JEI Ghost Ingredient support @Override public List> getPhantomTargets(Object ingredient) { - return JeiGhostHandler.getPhantomTargets(currentTab, partitionPopup, partitionSlotTargets, - storageBusPartitionSlotTargets, tempCellPartitionSlotTargets, - (cell, slotIndex, stack) -> onAddPartitionItem(cell, slotIndex, stack), - (storageBus, slotIndex, stack) -> onAddStorageBusPartitionItem(storageBus, slotIndex, stack), - (tempSlotIndex, slotIndex, stack) -> onAddTempCellPartitionItem(tempSlotIndex, slotIndex, stack)); + if (partitionPopup != null) return partitionPopup.getGhostTargets(); + + AbstractTabWidget activeTab = tabManager.getActiveTab(); + if (activeTab == null) return Collections.emptyList(); + + return activeTab.getPhantomTargets(ingredient); } // Accessors for popups and renderers @@ -2458,25 +1107,16 @@ public Map getStorageMap() { return dataManager.getStorageMap(); } - public RenderContext getRenderContext() { - return renderContext; - } - /** * Create a ToolContext for the Network Tools tab. */ - protected INetworkTool.ToolContext createNetworkToolContext() { + public INetworkTool.ToolContext createNetworkToolContext() { return new INetworkTool.ToolContext( dataManager.getStorageMap(), dataManager.getStorageBusMap(), filterPanelManager.getAllFilterStates(), getEffectiveSearchMode(), new INetworkTool.NetworkToolCallback() { - @Override - public void sendToolPacket(String toolId, byte[] data) { - // Handled by tools directly - } - @Override public void showError(String message) { MessageHelper.error(message); @@ -2515,4 +1155,92 @@ public void showNetworkToolConfirmation(INetworkTool tool) { }, () -> networkToolModal = null); } + + // ---- GuiContext interface implementation ---- + + @Override + public TerminalDataManager getDataManager() { + return dataManager; + } + + @Override + public ItemStack getHeldStack() { + return mc.player.inventory.getItemStack(); + } + + @Override + public boolean isShiftDown() { + return isShiftKeyDown(); + } + + @Override + public void sendPacket(Object packet) { + CellTerminalNetwork.INSTANCE.sendToServer((IMessage) packet); + } + + @Override + public void openInventoryPopup(CellInfo cell) { + inventoryPopup = new PopupCellInventory(this, cell, 0, 0); + } + + @Override + public void openPartitionPopup(CellInfo cell) { + partitionPopup = new PopupCellPartition(this, cell, 0, 0); + } + + @Override + public void showError(String translationKey, Object... args) { + MessageHelper.error(translationKey, args); + } + + @Override + public void showSuccess(String translationKey, Object... args) { + MessageHelper.success(translationKey, args); + } + + @Override + public void showWarning(String translationKey, Object... args) { + MessageHelper.warning(translationKey, args); + } + + @Override + public void highlightInWorld(BlockPos pos, int dimension, String displayName) { + if (pos == null || pos.equals(BlockPos.ORIGIN)) return; + + // Check if in different dimension + if (dimension != Minecraft.getMinecraft().player.dimension) { + MessageHelper.error("cellterminal.error.different_dimension"); + return; + } + + CellTerminalNetwork.INSTANCE.sendToServer( + new PacketHighlightBlock(pos, dimension) + ); + + // Send green chat message with block name and coordinates + MessageHelper.success("gui.cellterminal.highlighted", + pos.getX(), pos.getY(), pos.getZ(), displayName); + } + + @Override + public void highlightCellInWorld(CellInfo cell) { + if (cell == null) return; + + // Find the parent storage to get its position + StorageInfo storage = dataManager.findStorageForCell(cell); + if (storage == null) return; + + // Use storage name (block name) since we're highlighting the block, not the cell + highlightInWorld(storage.getPos(), storage.getDimension(), storage.getName()); + } + + @Override + public Set getSelectedStorageBusIds() { + return selectedStorageBusIds; + } + + @Override + public Set getSelectedTempCellSlots() { + return selectedTempCellSlots; + } } diff --git a/src/main/java/com/cellterminal/gui/GuiConstants.java b/src/main/java/com/cellterminal/gui/GuiConstants.java index e030998..207e542 100644 --- a/src/main/java/com/cellterminal/gui/GuiConstants.java +++ b/src/main/java/com/cellterminal/gui/GuiConstants.java @@ -36,12 +36,6 @@ private GuiConstants() {} /** Right edge of content area */ public static final int CONTENT_RIGHT_EDGE = 180; - /** Right edge of hoverable area */ - public static final int HOVER_RIGHT_EDGE = 185; - - /** Left edge of hoverable area */ - public static final int HOVER_LEFT_EDGE = 4; - /** Inner padding for panels and popups */ public static final int PADDING = 8; @@ -69,6 +63,7 @@ private GuiConstants() {} // ======================================== /** Tab indices */ + // TODO: ideally, we shouldn't need them at all public static final int TAB_TERMINAL = 0; public static final int TAB_INVENTORY = 1; public static final int TAB_PARTITION = 2; @@ -77,6 +72,8 @@ private GuiConstants() {} public static final int TAB_STORAGE_BUS_PARTITION = 5; public static final int TAB_NETWORK_TOOLS = 6; + public static final int LAST_TAB = TAB_NETWORK_TOOLS; + /** Width of each tab */ public static final int TAB_WIDTH = 22; @@ -87,15 +84,129 @@ private GuiConstants() {} public static final int TAB_Y_OFFSET = -22; // ======================================== - // SLOT CONFIGURATION + // ATLAS POSITIONS // ======================================== + /** Texture atlas width (for buttons, slots, etc.) */ + public static final int ATLAS_WIDTH = 64; + + /** Texture atlas height (for buttons, slots, etc.) */ + public static final int ATLAS_HEIGHT = 128; + + /** Small button size (e.g., partition-all, clear) */ + public static final int SMALL_BUTTON_SIZE = 8; + + /** Tab 1 button size */ + public static final int TAB1_BUTTON_SIZE = 12; + + /** Subnet button size */ + public static final int SUBNET_BUTTON_SIZE = 12; + + /** Search Mode button size */ + public static final int SEARCH_MODE_BUTTON_SIZE = 10; + /** Size of a standard slot (16x16 item + 2px border) */ public static final int SLOT_SIZE = 18; /** Size of a mini slot for cell contents (16x16) */ public static final int MINI_SLOT_SIZE = 16; + /** Size of the run button for Network Tools */ + public static final int NETWORK_TOOL_RUN_BUTTON_SIZE = 16; + + /** Size of the side buttons for the terminal (settings) */ + public static final int TERMINAL_SIDE_BUTTON_SIZE = 16; + + /** Size of the ? tooltip pseudo-button for Search Bar and Network Tools */ + public static final int TOOLTIP_BUTTON_SIZE = 10; + + // 5x2 * 8x8 for small buttons + + /** Small Buttons: Texture X positions in the atlas */ + public static final int SMALL_BUTTON_X = 0; + + /** Small Buttons: Texture Y positions in the atlas */ + public static final int SMALL_BUTTON_Y = 0; + + // 3x2 * 12x12 for terminal tab buttons + // + 2x2 * 12x12 for subnet buttons + + /** Tab 1 Buttons: Texture X positions in the atlas */ + public static final int TAB1_BUTTON_X = 0; + + /** Tab 1 Buttons: Texture Y positions in the atlas */ + public static final int TAB1_BUTTON_Y = SMALL_BUTTON_Y + 2 * SMALL_BUTTON_SIZE; + + /** Subnet Buttons: Texture X positions in the atlas */ + public static final int SUBNET_BUTTON_X = 3 * TAB1_BUTTON_SIZE; + + /** Subnet Buttons: Texture Y positions in the atlas */ + public static final int SUBNET_BUTTON_Y = TAB1_BUTTON_Y; + + // 3x2 * 10x10 for search mode buttons + + /** Search Mode Buttons: Texture X positions in the atlas */ + public static final int SEARCH_MODE_BUTTON_X = 0; + + /** Search Mode Buttons: Texture Y positions in the atlas */ + public static final int SEARCH_MODE_BUTTON_Y = TAB1_BUTTON_Y + 2 * TAB1_BUTTON_SIZE; + + // 1x3 * 16x16 for network tool buttons + // + 1x2 * 16x16 for terminal style buttons + // + 1x2 * 16x16 for red terminal buttons + // + 1x2 * 16x16 for green terminal buttons + + /** Network Tool Run Button: Texture X position */ + public static final int NETWORK_TOOL_RUN_BUTTON_X = 0; + + /** Network Tool Run Button: Texture Y position */ + public static final int NETWORK_TOOL_RUN_BUTTON_Y = SEARCH_MODE_BUTTON_Y + 2 * SEARCH_MODE_BUTTON_SIZE; + + /** Terminal Style Button: Texture X position */ + public static final int TERMINAL_STYLE_BUTTON_X = NETWORK_TOOL_RUN_BUTTON_X + NETWORK_TOOL_RUN_BUTTON_SIZE; + + /** Terminal Style Button: Texture Y position */ + public static final int TERMINAL_STYLE_BUTTON_Y = NETWORK_TOOL_RUN_BUTTON_Y; + + /** Terminal Red Button: Texture X position */ + public static final int TERMINAL_RED_BUTTON_X = TERMINAL_STYLE_BUTTON_X + TERMINAL_SIDE_BUTTON_SIZE; + + /** Terminal Red Button: Texture Y position */ + public static final int TERMINAL_RED_BUTTON_Y = TERMINAL_STYLE_BUTTON_Y; + + /** Terminal Green Button: Texture X position */ + public static final int TERMINAL_GREEN_BUTTON_X = TERMINAL_RED_BUTTON_X + TERMINAL_SIDE_BUTTON_SIZE; + + /** Terminal Green Button: Texture Y position */ + public static final int TERMINAL_GREEN_BUTTON_Y = TERMINAL_STYLE_BUTTON_Y; + + // 2x1 * 16x16 for mini slots + + /** Mini slot background: Texture X position */ + public static final int MINI_SLOT_X = NETWORK_TOOL_RUN_BUTTON_X + NETWORK_TOOL_RUN_BUTTON_SIZE; + + /** Mini slot background: Texture Y position */ + public static final int MINI_SLOT_Y = NETWORK_TOOL_RUN_BUTTON_Y + 2 * NETWORK_TOOL_RUN_BUTTON_SIZE; + + // 1x2 * 10x10 for tooltip buttons + // + 2x1 * 18x18 for slot backgrounds + + /** Tooltip Button: Texture X position */ + public static final int TOOLTIP_BUTTON_X = 0; + + /** Tooltip Button: Texture Y position */ + public static final int TOOLTIP_BUTTON_Y = NETWORK_TOOL_RUN_BUTTON_Y + 3 * NETWORK_TOOL_RUN_BUTTON_SIZE; + + /** Slot background: Texture X position */ + public static final int SLOT_BACKGROUND_X = TOOLTIP_BUTTON_X + TOOLTIP_BUTTON_SIZE; + + /** Slot background: Texture Y position */ + public static final int SLOT_BACKGROUND_Y = TOOLTIP_BUTTON_Y; + + // ======================================== + // SLOT CONFIGURATION + // ======================================== + /** Number of content slots per row for cells */ public static final int CELL_SLOTS_PER_ROW = 8; @@ -112,24 +223,53 @@ private GuiConstants() {} // BUTTON CONFIGURATION // ======================================== - /** Standard button size */ - public static final int BUTTON_SIZE = 14; - - /** Small button size (e.g., partition-all, clear) */ - public static final int SMALL_BUTTON_SIZE = 8; - /** X position of eject button in terminal tab */ - public static final int BUTTON_EJECT_X = 135; + public static final int BUTTON_EJECT_X = 141; /** X position of inventory button in terminal tab */ - public static final int BUTTON_INVENTORY_X = 150; + public static final int BUTTON_INVENTORY_X = 154; /** X position of partition button in terminal tab */ - public static final int BUTTON_PARTITION_X = 165; + public static final int BUTTON_PARTITION_X = 167; /** X position of IO mode button in storage bus headers (before priority field) */ public static final int BUTTON_IO_MODE_X = 120; + /** X position for upgrade card icons (left margin area) */ + public static final int CARDS_X = 3; + + // ======================================== + // TERMINAL TAB CELL LAYOUT + // ======================================== + + /** X offset from CELL_INDENT to the name/bar/buttons area */ + public static final int CELL_NAME_X_OFFSET = 18; + + /** Usage bar width in pixels */ + public static final int USAGE_BAR_WIDTH = BUTTON_EJECT_X - CELL_INDENT - CELL_NAME_X_OFFSET - 4; + + /** Usage bar height in pixels */ + public static final int USAGE_BAR_HEIGHT = 4; + + // ======================================== + // HEADER LAYOUT + // ======================================== + + /** X position of header name/location text (icon + gap) */ + public static final int HEADER_NAME_X = GUI_INDENT + 20; + + /** Maximum pixel width for header name text (before IO mode / priority area) */ + public static final int HEADER_NAME_MAX_WIDTH = BUTTON_IO_MODE_X - HEADER_NAME_X - 4; + + /** Maximum pixel width for location text (extends to right edge unlike name) */ + public static final int HEADER_LOCATION_MAX_WIDTH = CONTENT_RIGHT_EDGE - HEADER_NAME_X; + + /** X position of expand/collapse indicator text "[+]"/"[-]" */ + public static final int EXPAND_ICON_X = 167; + + /** Y offset for the tree connector at the bottom of a header */ + public static final int HEADER_CONNECTOR_Y_OFFSET = ROW_HEIGHT - 3; + // ======================================== // CONTROLS HELP WIDGET // ======================================== @@ -169,21 +309,9 @@ private GuiConstants() {} // COLORS // ======================================== - /** Background color for slots */ - public static final int COLOR_SLOT_BACKGROUND = 0xFF8B8B8B; - - /** Dark border for slots (top-left) */ - public static final int COLOR_SLOT_BORDER_DARK = 0xFF373737; - - /** Light border for slots (bottom-right) */ - public static final int COLOR_SLOT_BORDER_LIGHT = 0xFFFFFFFF; - /** Hover highlight overlay */ public static final int COLOR_HOVER_HIGHLIGHT = 0x80FFFFFF; - /** Row hover highlight */ - public static final int COLOR_ROW_HOVER = 0x50CCCCCC; - /** Storage header hover */ public static final int COLOR_STORAGE_HEADER_HOVER = 0x30FFFFFF; @@ -193,18 +321,15 @@ private GuiConstants() {} /** Tree line color */ public static final int COLOR_TREE_LINE = 0xFF808080; - /** Button color (normal) */ - public static final int COLOR_BUTTON_NORMAL = 0xFF8B8B8B; - - /** Button color (hovered) */ - public static final int COLOR_BUTTON_HOVER = 0xFF707070; - /** Button highlight (top-left) */ public static final int COLOR_BUTTON_HIGHLIGHT = 0xFFFFFFFF; /** Button shadow (bottom-right) */ public static final int COLOR_BUTTON_SHADOW = 0xFF555555; + /** Tab background (disabled) */ + public static final int COLOR_TAB_DISABLED = 0xFF505050; + /** Tab background (selected) */ public static final int COLOR_TAB_SELECTED = 0xFFC6C6C6; @@ -217,20 +342,20 @@ private GuiConstants() {} /** Partition indicator color (green) */ public static final int COLOR_PARTITION_INDICATOR = 0xFF55FF55; - /** Partition slot tint color (amber) */ - public static final int COLOR_PARTITION_SLOT_TINT = 0x30FFAA00; - /** Selection highlight color (for multi-select) */ public static final int COLOR_SELECTION_HIGHLIGHT = 0x5055FF55; /** Selection background color (light blue) */ public static final int COLOR_SELECTION = 0x405599DD; - /** Green button (partition all) */ - public static final int COLOR_BUTTON_GREEN = 0xFF33CC33; + /** Selected header name color (dark blue) */ + public static final int COLOR_NAME_SELECTED = 0x204080; - /** Red button (clear) */ - public static final int COLOR_BUTTON_RED = 0xFFCC3333; + /** Custom display name color (green, used for renamed cells/storages) */ + public static final int COLOR_CUSTOM_NAME = 0xFF2E7D32; + + /** Usage bar background color */ + public static final int COLOR_USAGE_BAR_BACKGROUND = 0xFF555555; /** Usage bar colors */ public static final int COLOR_USAGE_LOW = 0xFF33FF33; @@ -244,9 +369,6 @@ private GuiConstants() {} /** Double-click detection time in milliseconds */ public static final long DOUBLE_CLICK_TIME_MS = 400; - /** Storage bus polling interval in ticks */ - public static final int STORAGE_BUS_POLL_INTERVAL_TICKS = 20; - // ======================================== // TEXT COLORS // ======================================== diff --git a/src/main/java/com/cellterminal/gui/GuiSearchModeButton.java b/src/main/java/com/cellterminal/gui/GuiSearchModeButton.java deleted file mode 100644 index 0422ff8..0000000 --- a/src/main/java/com/cellterminal/gui/GuiSearchModeButton.java +++ /dev/null @@ -1,171 +0,0 @@ -package com.cellterminal.gui; - -import java.util.ArrayList; -import java.util.List; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.resources.I18n; - -import com.cellterminal.client.SearchFilterMode; - - -/** - * Button that cycles through search filter modes and displays a visual indicator. - * - Inventory mode: Grey dot (like slots in Tab 2) - * - Partition mode: Orange dot (like slots in Tab 3) - * - Mixed mode: Diagonally split dot with both colors - */ -public class GuiSearchModeButton extends GuiButton { - - private static final int BUTTON_SIZE = 12; - - // Colors matching the tab 2 (inventory) and tab 3 (partition) slot colors - private static final int COLOR_INVENTORY = 0xFF8B8B8B; // Grey - private static final int COLOR_PARTITION = 0xFFFF9933; // Orange - - private SearchFilterMode currentMode; - - public GuiSearchModeButton(int buttonId, int x, int y, SearchFilterMode initialMode) { - super(buttonId, x, y, BUTTON_SIZE, BUTTON_SIZE, ""); - this.currentMode = initialMode; - } - - public void setMode(SearchFilterMode mode) { - this.currentMode = mode; - } - - public SearchFilterMode getMode() { - return currentMode; - } - - public SearchFilterMode cycleMode() { - this.currentMode = this.currentMode.next(); - - return this.currentMode; - } - - @Override - public void drawButton(Minecraft mc, int mouseX, int mouseY, float partialTicks) { - if (!this.visible) return; - - this.hovered = mouseX >= this.x && mouseY >= this.y - && mouseX < this.x + this.width && mouseY < this.y + this.height; - - // Draw button background - int bgColor = this.hovered ? 0xFF707070 : 0xFF8B8B8B; - drawRect(this.x, this.y, this.x + this.width, this.y + this.height, bgColor); - - // Draw outline - drawRect(this.x, this.y, this.x + this.width, this.y + 1, 0xFFFFFFFF); - drawRect(this.x, this.y, this.x + 1, this.y + this.height, 0xFFFFFFFF); - drawRect(this.x, this.y + this.height - 1, this.x + this.width, this.y + this.height, 0xFF555555); - drawRect(this.x + this.width - 1, this.y, this.x + this.width, this.y + this.height, 0xFF555555); - - // Draw the mode indicator dot - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - drawModeIndicator(); - } - - // TODO: Optimize by using textures instead of per-pixel drawing - private void drawModeIndicator() { - int dotX = this.x + 3; - int dotY = this.y + 3; - int dotSize = 6; - - // Draw black outline first (1px border around the dot) - drawRect(dotX - 1, dotY - 1, dotX + dotSize + 1, dotY, 0xFF000000); // top - drawRect(dotX - 1, dotY + dotSize, dotX + dotSize + 1, dotY + dotSize + 1, 0xFF000000); // bottom - drawRect(dotX - 1, dotY, dotX, dotY + dotSize, 0xFF000000); // left - drawRect(dotX + dotSize, dotY, dotX + dotSize + 1, dotY + dotSize, 0xFF000000); // right - - switch (currentMode) { - case INVENTORY: - // Solid grey dot - drawRect(dotX, dotY, dotX + dotSize, dotY + dotSize, COLOR_INVENTORY); - break; - - case PARTITION: - // Solid orange dot - drawRect(dotX, dotY, dotX + dotSize, dotY + dotSize, COLOR_PARTITION); - break; - - case MIXED: - // Solid colors with gradient only on the diagonal - // Orange (partition) on bottom-left, grey (inventory) on top-right - // Diagonal pixels get interpolated color - for (int row = 0; row < dotSize; row++) { - for (int col = 0; col < dotSize; col++) { - int color; - if (col > row) { - // Above diagonal (top-right): solid grey - color = COLOR_INVENTORY; - } else if (col < row) { - // Below diagonal (bottom-left): solid orange - color = COLOR_PARTITION; - } else { - // On the diagonal: interpolate from orange to grey - // t goes from 0 (top-left of diagonal) to 1 (bottom-right of diagonal) - float t = (float) row / (float) (dotSize - 1); - color = interpolateColor(COLOR_PARTITION, COLOR_INVENTORY, t); - } - - drawRect(dotX + col, dotY + row, dotX + col + 1, dotY + row + 1, color); - } - } - break; - } - } - - /** - * Interpolate between two ARGB colors. - * @param color1 Starting color (at t=0) - * @param color2 Ending color (at t=1) - * @param t Interpolation factor (0 to 1) - * @return Interpolated color - */ - private int interpolateColor(int color1, int color2, float t) { - int a1 = (color1 >> 24) & 0xFF; - int r1 = (color1 >> 16) & 0xFF; - int g1 = (color1 >> 8) & 0xFF; - int b1 = color1 & 0xFF; - - int a2 = (color2 >> 24) & 0xFF; - int r2 = (color2 >> 16) & 0xFF; - int g2 = (color2 >> 8) & 0xFF; - int b2 = color2 & 0xFF; - - int a = (int) (a1 + (a2 - a1) * t); - int r = (int) (r1 + (r2 - r1) * t); - int g = (int) (g1 + (g2 - g1) * t); - int b = (int) (b1 + (b2 - b1) * t); - - return (a << 24) | (r << 16) | (g << 8) | b; - } - - /** - * Get the tooltip lines for this button. - */ - public List getTooltip() { - List tooltip = new ArrayList<>(); - tooltip.add(I18n.format("gui.cellterminal.search_mode")); - - String modeKey; - switch (currentMode) { - case INVENTORY: - modeKey = "gui.cellterminal.search_mode.inventory"; - break; - case PARTITION: - modeKey = "gui.cellterminal.search_mode.partition"; - break; - case MIXED: - default: - modeKey = "gui.cellterminal.search_mode.mixed"; - break; - } - tooltip.add("§7" + I18n.format(modeKey)); - - return tooltip; - } -} diff --git a/src/main/java/com/cellterminal/gui/GuiTerminalStyleButton.java b/src/main/java/com/cellterminal/gui/GuiTerminalStyleButton.java deleted file mode 100644 index 5ae3c41..0000000 --- a/src/main/java/com/cellterminal/gui/GuiTerminalStyleButton.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.cellterminal.gui; - -import java.util.ArrayList; -import java.util.List; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.resources.I18n; - -import com.cellterminal.config.CellTerminalClientConfig.TerminalStyle; - - -/** - * Button to toggle between terminal styles (small/tall). - * Uses a custom texture or draws a simple icon. - */ -public class GuiTerminalStyleButton extends GuiButton { - - private static final int BUTTON_SIZE = 16; - - private TerminalStyle currentStyle; - - public GuiTerminalStyleButton(int buttonId, int x, int y, TerminalStyle initialStyle) { - super(buttonId, x, y, BUTTON_SIZE, BUTTON_SIZE, ""); - this.currentStyle = initialStyle; - } - - public void setStyle(TerminalStyle style) { - this.currentStyle = style; - } - - public TerminalStyle getStyle() { - return currentStyle; - } - - @Override - public void drawButton(Minecraft mc, int mouseX, int mouseY, float partialTicks) { - if (!this.visible) return; - - this.hovered = mouseX >= this.x && mouseY >= this.y - && mouseX < this.x + this.width && mouseY < this.y + this.height; - - // Draw button background - int bgColor = this.hovered ? 0xFF707070 : 0xFF8B8B8B; - drawRect(this.x, this.y, this.x + this.width, this.y + this.height, bgColor); - - // Draw 3D border - drawRect(this.x, this.y, this.x + this.width, this.y + 1, 0xFFFFFFFF); - drawRect(this.x, this.y, this.x + 1, this.y + this.height, 0xFFFFFFFF); - drawRect(this.x, this.y + this.height - 1, this.x + this.width, this.y + this.height, 0xFF555555); - drawRect(this.x + this.width - 1, this.y, this.x + this.width, this.y + this.height, 0xFF555555); - - // Draw icon based on current style - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - drawStyleIcon(mc); - } - - private void drawStyleIcon(Minecraft mc) { - int iconX = this.x + 3; - int iconY = this.y + 3; - - // Draw a simple representation of small vs tall - if (currentStyle == TerminalStyle.SMALL) { - // Small: draw a compact rectangle - drawRect(iconX, iconY + 2, iconX + 10, iconY + 8, 0xFF404040); - drawRect(iconX + 1, iconY + 3, iconX + 9, iconY + 7, 0xFFC6C6C6); - } else { - // Tall: draw an extended rectangle - drawRect(iconX + 2, iconY, iconX + 8, iconY + 10, 0xFF404040); - drawRect(iconX + 3, iconY + 1, iconX + 7, iconY + 9, 0xFFC6C6C6); - } - } - - /** - * Get the tooltip lines for this button. - */ - public List getTooltip() { - List tooltip = new ArrayList<>(); - tooltip.add(I18n.format("gui.cellterminal.terminal_style")); - - String styleKey = currentStyle == TerminalStyle.SMALL - ? "gui.cellterminal.terminal_style.small" - : "gui.cellterminal.terminal_style.tall"; - tooltip.add("§7" + I18n.format(styleKey)); - - return tooltip; - } -} diff --git a/src/main/java/com/cellterminal/gui/PopupCellInventory.java b/src/main/java/com/cellterminal/gui/PopupCellInventory.java index 7441dc5..10fd0f6 100644 --- a/src/main/java/com/cellterminal/gui/PopupCellInventory.java +++ b/src/main/java/com/cellterminal/gui/PopupCellInventory.java @@ -8,13 +8,16 @@ import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.RenderHelper; import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; import appeng.util.ReadableNumberConverter; import com.cellterminal.client.CellInfo; import com.cellterminal.client.StorageInfo; +import com.cellterminal.gui.widget.AbstractWidget; +import com.cellterminal.network.CellTerminalNetwork; +import com.cellterminal.network.PacketPartitionAction; /** @@ -23,13 +26,16 @@ */ public class PopupCellInventory extends Gui { - private static final int SLOTS_PER_ROW = 9; - private static final int MAX_ROWS = 7; - private static final int SLOT_SIZE = 18; - private static final int PADDING = 8; - private static final int HEADER_HEIGHT = 20; - private static final int BUTTON_HEIGHT = 14; - private static final int FOOTER_HEIGHT = 12; + private static final int SLOTS_PER_ROW = GuiConstants.POPUP_SLOTS_PER_ROW; + private static final int MAX_ROWS = GuiConstants.POPUP_MAX_ROWS; + private static final int SLOT_SIZE = GuiConstants.SLOT_SIZE; + private static final int PADDING = GuiConstants.PADDING; + private static final int HEADER_HEIGHT = GuiConstants.POPUP_HEADER_HEIGHT; + private static final int BUTTON_HEIGHT = GuiConstants.POPUP_BUTTON_HEIGHT; + private static final int FOOTER_HEIGHT = GuiConstants.POPUP_FOOTER_HEIGHT; + + private static final ResourceLocation TEXTURE = + new ResourceLocation("cellterminal", "textures/guis/atlas.png"); private final GuiScreen parent; private final CellInfo cell; @@ -42,9 +48,9 @@ public class PopupCellInventory extends Gui { private final int slotOffsetX; // Button for set/unset all partition - private int partitionButtonX; - private int partitionButtonY; - private int partitionButtonWidth; + private final int partitionButtonX; + private final int partitionButtonY; + private final int partitionButtonWidth; private boolean partitionAllHovered = false; // Hovered item for tooltip @@ -141,45 +147,23 @@ public void draw(int mouseX, int mouseY) { // Check if item is in partition boolean inPartition = isInPartition(stack, partition); - // Draw slot background - int slotBgColor = 0xFF8B8B8B; - boolean slotHovered = mouseX >= slotX && mouseX < slotX + SLOT_SIZE - 1 - && mouseY >= slotY && mouseY < slotY + SLOT_SIZE - 1; - - drawRect(slotX, slotY, slotX + SLOT_SIZE - 1, slotY + SLOT_SIZE - 1, slotBgColor); + // Draw textured slot background + drawSlotBackground(slotX, slotY); - // Draw slot border (3D effect) - drawRect(slotX, slotY, slotX + SLOT_SIZE - 1, slotY + 1, 0xFF373737); - drawRect(slotX, slotY, slotX + 1, slotY + SLOT_SIZE - 1, 0xFF373737); - drawRect(slotX, slotY + SLOT_SIZE - 2, slotX + SLOT_SIZE - 1, slotY + SLOT_SIZE - 1, 0xFFFFFFFF); - drawRect(slotX + SLOT_SIZE - 2, slotY, slotX + SLOT_SIZE - 1, slotY + SLOT_SIZE - 1, 0xFFFFFFFF); + // Check hover + boolean slotHovered = mouseX >= slotX && mouseX < slotX + SLOT_SIZE + && mouseY >= slotY && mouseY < slotY + SLOT_SIZE; // Draw item if (!stack.isEmpty()) { - GlStateManager.enableDepth(); - RenderHelper.enableGUIStandardItemLighting(); - mc.getRenderItem().renderItemAndEffectIntoGUI(stack, slotX, slotY); - RenderHelper.disableStandardItemLighting(); + AbstractWidget.renderItemStack(mc.getRenderItem(), stack, slotX + 1, slotY + 1); // Draw count like AE2 terminal (use actual AE2 count, not ItemStack count) - String countStr = formatItemCount(cell.getContentCount(i)); - GlStateManager.disableLighting(); - GlStateManager.disableDepth(); - int countWidth = fr.getStringWidth(countStr); - - GlStateManager.pushMatrix(); - GlStateManager.scale(0.5f, 0.5f, 0.5f); - // Right-align: for 18x18 slot, text right edge at slotX+17, bottom at slotY+15 - fr.drawStringWithShadow(countStr, (slotX + 16) * 2 - countWidth, (slotY + 12) * 2, 0xFFFFFF); - GlStateManager.popMatrix(); + drawItemCount(cell.getContentCount(i), slotX, slotY, fr); // Draw partition indicator in top-left corner if in partition if (inPartition) { - String partitionIndicator = net.minecraft.client.resources.I18n.format("gui.cellterminal.partition_indicator"); - GlStateManager.pushMatrix(); - GlStateManager.scale(0.5f, 0.5f, 0.5f); - fr.drawStringWithShadow(partitionIndicator, (slotX + 1) * 2, (slotY + 1) * 2, 0xFF55FF55); - GlStateManager.popMatrix(); + drawPartitionIndicator(slotX, slotY, fr); } // Track hovered item for tooltip @@ -189,6 +173,9 @@ public void draw(int mouseX, int mouseY) { hoveredY = mouseY; } } + + // Draw hover highlight + if (slotHovered) drawSlotHoverHighlight(slotX, slotY); } // Draw empty message if no contents @@ -208,12 +195,8 @@ public void draw(int mouseX, int mouseY) { * Draw tooltip for hovered item. Must be called after draw() in a separate pass. */ public void drawTooltip(int mouseX, int mouseY) { - if (!hoveredStack.isEmpty() && parent instanceof GuiScreen) { - ((GuiScreen) parent).drawHoveringText( - parent.getItemToolTip(hoveredStack), - hoveredX, - hoveredY - ); + if (!hoveredStack.isEmpty() && parent != null) { + parent.drawHoveringText(parent.getItemToolTip(hoveredStack), hoveredX, hoveredY); } } @@ -231,9 +214,12 @@ public boolean handleClick(int mouseX, int mouseY, int mouseButton) { if (!isInsidePopup(mouseX, mouseY)) return false; // Check partition all button click - if (mouseX >= partitionButtonX && mouseX < partitionButtonX + partitionButtonWidth - && mouseY >= partitionButtonY && mouseY < partitionButtonY + BUTTON_HEIGHT) { - if (parent instanceof GuiCellTerminalBase) ((GuiCellTerminalBase) parent).onPartitionAllClicked(cell); + if (partitionAllHovered) { + CellTerminalNetwork.INSTANCE.sendToServer(new PacketPartitionAction( + cell.getParentStorageId(), + cell.getSlot(), + PacketPartitionAction.Action.SET_ALL_FROM_CONTENTS + )); return true; } @@ -251,8 +237,13 @@ public boolean handleClick(int mouseX, int mouseY, int mouseButton) { if (slotIndex < cell.getContents().size()) { ItemStack clickedStack = cell.getContents().get(slotIndex); - if (!clickedStack.isEmpty() && parent instanceof GuiCellTerminalBase) { - ((GuiCellTerminalBase) parent).onTogglePartitionItem(cell, clickedStack); + if (!clickedStack.isEmpty()) { + CellTerminalNetwork.INSTANCE.sendToServer(new PacketPartitionAction( + cell.getParentStorageId(), + cell.getSlot(), + PacketPartitionAction.Action.TOGGLE_ITEM, + clickedStack + )); } return true; @@ -300,4 +291,48 @@ public int getX() { public int getY() { return y; } + + // ---- Drawing helpers (consistent with SlotsLine) ---- + + private void drawSlotBackground(int slotX, int slotY) { + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + + int texX = GuiConstants.SLOT_BACKGROUND_X; + int texY = GuiConstants.SLOT_BACKGROUND_Y; + Gui.drawScaledCustomSizeModalRect( + slotX, slotY, texX, texY, SLOT_SIZE, SLOT_SIZE, SLOT_SIZE, SLOT_SIZE, + GuiConstants.ATLAS_WIDTH, GuiConstants.ATLAS_HEIGHT); + } + + private void drawSlotHoverHighlight(int slotX, int slotY) { + Gui.drawRect(slotX + 1, slotY + 1, slotX + SLOT_SIZE - 1, slotY + SLOT_SIZE - 1, + GuiConstants.COLOR_HOVER_HIGHLIGHT); + } + + private void drawItemCount(long count, int slotX, int slotY, FontRenderer fr) { + String countStr = formatItemCount(count); + if (countStr.isEmpty()) return; + + int countWidth = fr.getStringWidth(countStr); + + GlStateManager.disableDepth(); + GlStateManager.pushMatrix(); + GlStateManager.scale(0.5f, 0.5f, 0.5f); + // Right-align: for 18x18 slot, text right edge at slotX+17, bottom at slotY+13 + fr.drawStringWithShadow(countStr, (slotX + SLOT_SIZE - 1) * 2 - countWidth, (slotY + SLOT_SIZE - 5) * 2, 0xFFFFFF); + GlStateManager.popMatrix(); + GlStateManager.enableDepth(); + } + + private void drawPartitionIndicator(int slotX, int slotY, FontRenderer fr) { + GlStateManager.disableLighting(); + GlStateManager.disableDepth(); + GlStateManager.pushMatrix(); + GlStateManager.scale(0.5f, 0.5f, 0.5f); + fr.drawStringWithShadow("P", (slotX + 2) * 2, (slotY + 2) * 2, GuiConstants.COLOR_PARTITION_INDICATOR); + GlStateManager.popMatrix(); + GlStateManager.enableDepth(); + } } diff --git a/src/main/java/com/cellterminal/gui/PopupCellPartition.java b/src/main/java/com/cellterminal/gui/PopupCellPartition.java index 3f6ca6c..4855df8 100644 --- a/src/main/java/com/cellterminal/gui/PopupCellPartition.java +++ b/src/main/java/com/cellterminal/gui/PopupCellPartition.java @@ -1,6 +1,6 @@ package com.cellterminal.gui; -import java.awt.*; +import java.awt.Rectangle; import java.util.ArrayList; import java.util.List; @@ -10,11 +10,11 @@ import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.RenderHelper; import net.minecraft.client.resources.I18n; import net.minecraft.enchantment.EnchantmentData; import net.minecraft.item.ItemEnchantedBook; import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; import net.minecraftforge.fluids.FluidStack; import net.minecraftforge.fluids.FluidUtil; @@ -27,7 +27,10 @@ import com.cellterminal.client.CellInfo; import com.cellterminal.gui.overlay.MessageHelper; +import com.cellterminal.gui.widget.AbstractWidget; import com.cellterminal.integration.ThaumicEnergisticsIntegration; +import com.cellterminal.network.CellTerminalNetwork; +import com.cellterminal.network.PacketPartitionAction; /** @@ -36,13 +39,16 @@ */ public class PopupCellPartition extends Gui { - private static final int SLOTS_PER_ROW = 9; - private static final int MAX_ROWS = 7; - private static final int SLOT_SIZE = 18; - private static final int PADDING = 8; - private static final int HEADER_HEIGHT = 20; - private static final int FOOTER_HEIGHT = 16; - private static final int MAX_PARTITION_SLOTS = 63; + private static final int SLOTS_PER_ROW = GuiConstants.POPUP_SLOTS_PER_ROW; + private static final int MAX_ROWS = GuiConstants.POPUP_MAX_ROWS; + private static final int SLOT_SIZE = GuiConstants.SLOT_SIZE; + private static final int PADDING = GuiConstants.PADDING; + private static final int HEADER_HEIGHT = GuiConstants.POPUP_HEADER_HEIGHT; + private static final int FOOTER_HEIGHT = GuiConstants.POPUP_FOOTER_HEIGHT; + private static final int MAX_PARTITION_SLOTS = SLOTS_PER_ROW * MAX_ROWS; + + private static final ResourceLocation TEXTURE = + new ResourceLocation("cellterminal", "textures/guis/atlas.png"); private final GuiScreen parent; private final CellInfo cell; @@ -125,28 +131,16 @@ public void draw(int mouseX, int mouseY) { ItemStack stack = i < editablePartition.size() ? editablePartition.get(i) : ItemStack.EMPTY; - // Draw slot background - int slotBgColor = 0xFF8B8B8B; - boolean hovered = mouseX >= slotX && mouseX < slotX + SLOT_SIZE - 1 - && mouseY >= slotY && mouseY < slotY + SLOT_SIZE - 1; - if (hovered && !stack.isEmpty()) slotBgColor = 0xFF996666; - - drawRect(slotX, slotY, slotX + SLOT_SIZE - 1, slotY + SLOT_SIZE - 1, slotBgColor); + // Draw textured slot background (partition variant with amber tint) + drawPartitionSlotBackground(slotX, slotY); - // Draw slot border (3D effect) - drawRect(slotX, slotY, slotX + SLOT_SIZE - 1, slotY + 1, 0xFF373737); - drawRect(slotX, slotY, slotX + 1, slotY + SLOT_SIZE - 1, 0xFF373737); - drawRect(slotX, slotY + SLOT_SIZE - 2, slotX + SLOT_SIZE - 1, slotY + SLOT_SIZE - 1, 0xFFFFFFFF); - drawRect(slotX + SLOT_SIZE - 2, slotY, slotX + SLOT_SIZE - 1, slotY + SLOT_SIZE - 1, 0xFFFFFFFF); + // Check hover + boolean hovered = mouseX >= slotX && mouseX < slotX + SLOT_SIZE + && mouseY >= slotY && mouseY < slotY + SLOT_SIZE; // Draw item if (!stack.isEmpty()) { - GlStateManager.enableDepth(); - RenderHelper.enableGUIStandardItemLighting(); - mc.getRenderItem().renderItemAndEffectIntoGUI(stack, slotX, slotY); - RenderHelper.disableStandardItemLighting(); - GlStateManager.disableDepth(); - GlStateManager.disableLighting(); + AbstractWidget.renderItemStack(mc.getRenderItem(), stack, slotX + 1, slotY + 1); // Track hovered item for tooltip if (hovered) { @@ -155,6 +149,9 @@ public void draw(int mouseX, int mouseY) { hoveredY = mouseY; } } + + // Draw hover highlight + if (hovered) drawSlotHoverHighlight(slotX, slotY); } // Draw hint at bottom @@ -172,12 +169,8 @@ public void draw(int mouseX, int mouseY) { * Draw tooltip for hovered item. Must be called after draw() in a separate pass. */ public void drawTooltip(int mouseX, int mouseY) { - if (!hoveredStack.isEmpty() && parent instanceof GuiScreen) { - ((GuiScreen) parent).drawHoveringText( - parent.getItemToolTip(hoveredStack), - hoveredX, - hoveredY - ); + if (!hoveredStack.isEmpty() && parent != null) { + parent.drawHoveringText(parent.getItemToolTip(hoveredStack), hoveredX, hoveredY); } } @@ -199,9 +192,12 @@ public boolean handleClick(int mouseX, int mouseY, int mouseButton) { if (!removed.isEmpty()) { editablePartition.set(slotIndex, ItemStack.EMPTY); - if (parent instanceof GuiCellTerminalBase) { - ((GuiCellTerminalBase) parent).onRemovePartitionItem(cell, slotIndex); - } + CellTerminalNetwork.INSTANCE.sendToServer(new PacketPartitionAction( + cell.getParentStorageId(), + cell.getSlot(), + PacketPartitionAction.Action.REMOVE_ITEM, + slotIndex + )); } return true; @@ -320,9 +316,13 @@ public boolean handleGhostDrop(int slotIndex, Object ingredient) { editablePartition.set(targetSlot, stack.copy()); - if (parent instanceof GuiCellTerminalBase) { - ((GuiCellTerminalBase) parent).onAddPartitionItem(cell, targetSlot, stack); - } + CellTerminalNetwork.INSTANCE.sendToServer(new PacketPartitionAction( + cell.getParentStorageId(), + cell.getSlot(), + PacketPartitionAction.Action.ADD_ITEM, + targetSlot, + stack + )); return true; } @@ -381,9 +381,13 @@ public boolean handleGhostDrop(int slotIndex, Object ingredient) { editablePartition.set(targetSlot, stack.copy()); - if (parent instanceof GuiCellTerminalBase) { - ((GuiCellTerminalBase) parent).onAddPartitionItem(cell, targetSlot, stack); - } + CellTerminalNetwork.INSTANCE.sendToServer(new PacketPartitionAction( + cell.getParentStorageId(), + cell.getSlot(), + PacketPartitionAction.Action.ADD_ITEM, + targetSlot, + stack + )); return true; } @@ -471,4 +475,24 @@ public int getX() { public int getY() { return y; } + + // ---- Drawing helpers (consistent with SlotsLine) ---- + + private void drawPartitionSlotBackground(int slotX, int slotY) { + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + + // Use partition variant (right half of slot texture with amber tint) + int texX = GuiConstants.SLOT_BACKGROUND_X + SLOT_SIZE; + int texY = GuiConstants.SLOT_BACKGROUND_Y; + Gui.drawScaledCustomSizeModalRect( + slotX, slotY, texX, texY, SLOT_SIZE, SLOT_SIZE, SLOT_SIZE, SLOT_SIZE, + GuiConstants.ATLAS_WIDTH, GuiConstants.ATLAS_HEIGHT); + } + + private void drawSlotHoverHighlight(int slotX, int slotY) { + Gui.drawRect(slotX + 1, slotY + 1, slotX + SLOT_SIZE - 1, slotY + SLOT_SIZE - 1, + GuiConstants.COLOR_HOVER_HIGHLIGHT); + } } diff --git a/src/main/java/com/cellterminal/gui/PriorityFieldManager.java b/src/main/java/com/cellterminal/gui/PriorityFieldManager.java index f4f985f..b1c0104 100644 --- a/src/main/java/com/cellterminal/gui/PriorityFieldManager.java +++ b/src/main/java/com/cellterminal/gui/PriorityFieldManager.java @@ -2,6 +2,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; import org.lwjgl.input.Keyboard; @@ -10,18 +11,32 @@ import net.minecraft.client.gui.GuiTextField; import net.minecraft.client.renderer.GlStateManager; -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.client.StorageInfo; +import com.cellterminal.client.Prioritizable; import com.cellterminal.network.CellTerminalNetwork; import com.cellterminal.network.PacketSetPriority; /** - * Manages priority text fields for visible storage entries (drives/chests) and storage buses. - * Each storage/bus gets its own text field positioned at the far right of its header line. + * Singleton manager for inline priority text fields in the Cell Terminal GUI. + *

+ * Each visible storage header (drive/chest or storage bus) that supports priority + * editing gets an {@link InlinePriorityField} registered here. Fields persist across + * frame rebuilds (keyed by {@link Prioritizable#getId()}) so that focus state, + * cursor position, and in-progress edits survive widget recreation. + *

+ * Headers call {@link #registerField} during their draw pass to create/position + * their own field. The GUI then calls {@link #drawFieldsRelative} to render all + * visible fields, and delegates click/key events via {@link #handleClick} and + * {@link #handleKeyTyped}. */ public class PriorityFieldManager { + private static final PriorityFieldManager INSTANCE = new PriorityFieldManager(); + + public static PriorityFieldManager getInstance() { + return INSTANCE; + } + // Field dimensions - sized for 7 digits with caret (-999999 to 9999999) public static final int FIELD_WIDTH = 35; private static final int FIELD_HEIGHT = 6; @@ -29,104 +44,47 @@ public class PriorityFieldManager { // Position offset from right edge of content area (leave room for [+]/[-] button) public static final int RIGHT_MARGIN = 15; - private final FontRenderer fontRenderer; - private final Map fields = new HashMap<>(); - private final Map storageBusFields = new HashMap<>(); - private PriorityField focusedField = null; - private StorageBusPriorityField focusedStorageBusField = null; + // Field registry (persists across frame rebuilds) + private final Map fields = new HashMap<>(); + private InlinePriorityField focusedField = null; - public PriorityFieldManager(FontRenderer fontRenderer) { - this.fontRenderer = fontRenderer; - } - - /** - * Get or create a priority field for the given storage. - * Also updates the storage reference if the field already exists. - */ - public PriorityField getOrCreateField(StorageInfo storage, int guiLeft, int guiTop) { - PriorityField field = fields.get(storage.getId()); - - if (field == null) { - field = new PriorityField(storage, fontRenderer); - fields.put(storage.getId(), field); - } else { - // Update storage reference in case it was refreshed from server - field.updateStorage(storage); - } - - return field; - } + private PriorityFieldManager() {} /** - * Get or create a priority field for the given storage bus. - * Also updates the storage bus reference if the field already exists. + * Register (or update) a priority field for the given target. + * Called by header widgets during their draw pass to create the field if it + * doesn't exist yet, update the data reference, and position it for this frame. + * + * @param target The prioritizable data object (storage or storage bus) + * @param y The GUI-relative Y position of the header row + * @param guiLeft The GUI's absolute left position + * @param guiTop The GUI's absolute top position + * @param fontRenderer Font renderer for creating new fields */ - public StorageBusPriorityField getOrCreateStorageBusField(StorageBusInfo storageBus, int guiLeft, int guiTop) { - StorageBusPriorityField field = storageBusFields.get(storageBus.getId()); + public void registerField(Prioritizable target, int y, int guiLeft, int guiTop, FontRenderer fontRenderer) { + InlinePriorityField field = fields.get(target.getId()); if (field == null) { - field = new StorageBusPriorityField(storageBus, fontRenderer); - storageBusFields.put(storageBus.getId(), field); + field = new InlinePriorityField(target, fontRenderer); + fields.put(target.getId(), field); } else { - // Update storage bus reference in case it was refreshed from server - field.updateStorageBus(storageBus); + field.updateTarget(target); } - return field; - } - - /** - * Update the position of a field for the current render frame. - * @param field The field to update - * @param y The GUI-relative Y position of the storage line - * @param guiLeft The GUI's absolute left position - * @param guiTop The GUI's absolute top position - */ - public void updateFieldPosition(PriorityField field, int y, int guiLeft, int guiTop) { - // Position at far right of the storage line - // Store absolute position for click handling - int fieldX = guiLeft + GuiConstants.CONTENT_RIGHT_EDGE - FIELD_WIDTH - RIGHT_MARGIN; - int fieldY = guiTop + y + 1; - field.updatePosition(fieldX, fieldY); - } - - /** - * Update the position of a storage bus priority field for the current render frame. - */ - public void updateStorageBusFieldPosition(StorageBusPriorityField field, int y, int guiLeft, int guiTop) { int fieldX = guiLeft + GuiConstants.CONTENT_RIGHT_EDGE - FIELD_WIDTH - RIGHT_MARGIN; int fieldY = guiTop + y + 1; field.updatePosition(fieldX, fieldY); } - /** - * Draw all visible priority fields. - * Must be called from absolute coordinate context (like drawScreen). - */ - public void drawFields(int mouseX, int mouseY) { - for (PriorityField field : fields.values()) { - if (field.isVisible()) field.draw(); - } - - for (StorageBusPriorityField field : storageBusFields.values()) { - if (field.isVisible()) field.draw(); - } - } - /** * Draw fields when in GUI-relative context (after glTranslate by guiLeft/guiTop). - * We need to undo the translation to draw at absolute positions. + * Undoes the translation to draw at the absolute positions stored in each field. */ public void drawFieldsRelative(int guiLeft, int guiTop) { - // Pop out of GUI-relative coordinates to draw at absolute positions GlStateManager.pushMatrix(); GlStateManager.translate(-guiLeft, -guiTop, 0); - for (PriorityField field : fields.values()) { - if (field.isVisible()) field.draw(); - } - - for (StorageBusPriorityField field : storageBusFields.values()) { + for (InlinePriorityField field : fields.values()) { if (field.isVisible()) field.draw(); } @@ -135,24 +93,19 @@ public void drawFieldsRelative(int guiLeft, int guiTop) { /** * Mark all fields as not visible at the start of a render cycle. + * Fields are marked visible again when their header calls {@link #registerField}. */ public void resetVisibility() { - for (PriorityField field : fields.values()) field.setVisible(false); - for (StorageBusPriorityField field : storageBusFields.values()) field.setVisible(false); - } - - /** - * Clean up fields for storages that no longer exist. - */ - public void cleanupStaleFields(Map currentStorages) { - fields.keySet().removeIf(id -> !currentStorages.containsKey(id)); + for (InlinePriorityField field : fields.values()) field.setVisible(false); } /** - * Clean up fields for storage buses that no longer exist. + * Remove fields whose IDs are no longer present in the active data set. + * + * @param activeIds The set of all currently valid storage/bus IDs */ - public void cleanupStaleStorageBusFields(Map currentStorageBuses) { - storageBusFields.keySet().removeIf(id -> !currentStorageBuses.containsKey(id)); + public void cleanupStaleFields(Set activeIds) { + fields.keySet().removeIf(id -> !activeIds.contains(id)); } /** @@ -160,39 +113,10 @@ public void cleanupStaleStorageBusFields(Map currentStorag * @return true if a field was clicked and handled the event */ public boolean handleClick(int mouseX, int mouseY, int mouseButton) { - // Check storage bus fields - for (StorageBusPriorityField field : storageBusFields.values()) { - if (!field.isVisible()) continue; - - if (field.isMouseOver(mouseX, mouseY)) { - // Unfocus any other focused field - if (focusedField != null) { - focusedField.onFocusLost(); - focusedField = null; - } - - if (focusedStorageBusField != null && focusedStorageBusField != field) { - focusedStorageBusField.onFocusLost(); - } - - focusedStorageBusField = field; - field.mouseClicked(mouseX, mouseY, mouseButton); - - return true; - } - } - - // Check storage fields - for (PriorityField field : fields.values()) { + for (InlinePriorityField field : fields.values()) { if (!field.isVisible()) continue; if (field.isMouseOver(mouseX, mouseY)) { - // Unfocus any other focused field - if (focusedStorageBusField != null) { - focusedStorageBusField.onFocusLost(); - focusedStorageBusField = null; - } - if (focusedField != null && focusedField != field) focusedField.onFocusLost(); focusedField = field; @@ -202,17 +126,12 @@ public boolean handleClick(int mouseX, int mouseY, int mouseButton) { } } - // Clicking outside any field - unfocus current + // Clicking outside any field = unfocus current if (focusedField != null) { focusedField.onFocusLost(); focusedField = null; } - if (focusedStorageBusField != null) { - focusedStorageBusField.onFocusLost(); - focusedStorageBusField = null; - } - return false; } @@ -221,61 +140,32 @@ public boolean handleClick(int mouseX, int mouseY, int mouseButton) { * @return true if the event was consumed */ public boolean handleKeyTyped(char typedChar, int keyCode) { - if (focusedField != null) { - boolean consumed = focusedField.keyTyped(typedChar, keyCode); + if (focusedField == null) return false; - // If the field was unfocused (e.g., by Escape), clear the reference - if (!focusedField.isFocused()) focusedField = null; + boolean consumed = focusedField.keyTyped(typedChar, keyCode); - return consumed; - } + // If the field was unfocused (e.g., by Escape or Enter), clear the reference + if (!focusedField.isFocused()) focusedField = null; - if (focusedStorageBusField != null) { - boolean consumed = focusedStorageBusField.keyTyped(typedChar, keyCode); - - // If the field was unfocused (e.g., by Escape), clear the reference - if (!focusedStorageBusField.isFocused()) focusedStorageBusField = null; - - return consumed; - } - - return false; - } - - /** - * Check if any field is focused. - */ - public boolean hasFocusedField() { - return (focusedField != null && focusedField.isFocused()) - || (focusedStorageBusField != null && focusedStorageBusField.isFocused()); + return consumed; } /** - * Unfocus all fields. + * Unfocus all fields (submits pending edits). */ public void unfocusAll() { if (focusedField != null) { focusedField.onFocusLost(); focusedField = null; } - - if (focusedStorageBusField != null) { - focusedStorageBusField.onFocusLost(); - focusedStorageBusField = null; - } } /** - * Check if mouse is over any visible priority field (storage or storage bus). + * Check if mouse is over any visible priority field. * @return true if mouse is over a priority field */ public boolean isMouseOverField(int mouseX, int mouseY) { - for (PriorityField field : fields.values()) { - if (!field.isVisible()) continue; - if (field.isMouseOver(mouseX, mouseY)) return true; - } - - for (StorageBusPriorityField field : storageBusFields.values()) { + for (InlinePriorityField field : fields.values()) { if (!field.isVisible()) continue; if (field.isMouseOver(mouseX, mouseY)) return true; } @@ -284,33 +174,35 @@ public boolean isMouseOverField(int mouseX, int mouseY) { } /** - * A single priority field for a storage. + * A single inline priority text field for any {@link Prioritizable} target. + * Handles rendering, keyboard input (digit filtering), and submitting + * priority changes via {@link PacketSetPriority}. */ - public static class PriorityField { + public static class InlinePriorityField { private static final float TEXT_SCALE = 0.65f; - private StorageInfo storage; + private Prioritizable target; private final GuiTextField textField; private final FontRenderer fontRenderer; private boolean visible = false; private int lastKnownPriority; - public PriorityField(StorageInfo storage, FontRenderer fontRenderer) { - this.storage = storage; + public InlinePriorityField(Prioritizable target, FontRenderer fontRenderer) { + this.target = target; this.fontRenderer = fontRenderer; - this.lastKnownPriority = storage.getPriority(); + this.lastKnownPriority = target.getPriority(); this.textField = new GuiTextField(0, fontRenderer, 0, 0, FIELD_WIDTH, FIELD_HEIGHT); this.textField.setMaxStringLength(8); - this.textField.setEnableBackgroundDrawing(false); // We'll draw our own background - this.textField.setText(String.valueOf(storage.getPriority())); + this.textField.setEnableBackgroundDrawing(false); + this.textField.setText(String.valueOf(target.getPriority())); } /** - * Update the storage reference. Called when storage data is refreshed from server. + * Update the data reference. Called when data is refreshed from server. */ - public void updateStorage(StorageInfo newStorage) { - this.storage = newStorage; + public void updateTarget(Prioritizable newTarget) { + this.target = newTarget; } public void updatePosition(int x, int y) { @@ -318,9 +210,9 @@ public void updatePosition(int x, int y) { this.textField.y = y; this.visible = true; - // Update text if storage priority changed externally - if (storage.getPriority() != lastKnownPriority && !textField.isFocused()) { - lastKnownPriority = storage.getPriority(); + // Sync text if priority changed externally (and field is not being edited) + if (target.getPriority() != lastKnownPriority && !textField.isFocused()) { + lastKnownPriority = target.getPriority(); textField.setText(String.valueOf(lastKnownPriority)); } } @@ -330,151 +222,8 @@ public void draw() { int y = textField.y; // Draw background - net.minecraft.client.gui.Gui.drawRect(x - 1, y - 1, x + FIELD_WIDTH + 1, y + FIELD_HEIGHT + 1, 0xFF373737); - net.minecraft.client.gui.Gui.drawRect(x, y, x + FIELD_WIDTH, y + FIELD_HEIGHT, textField.isFocused() ? 0xFF000000 : 0xFF1E1E1E); - - // Draw text with scaling - String text = textField.getText(); - if (!text.isEmpty()) { - GlStateManager.pushMatrix(); - GlStateManager.translate(x + 2, y + 1, 0); - GlStateManager.scale(TEXT_SCALE, TEXT_SCALE, 1.0f); - fontRenderer.drawString(text, 0, 0, 0xE0E0E0); - GlStateManager.popMatrix(); - } - - // Draw cursor if focused - if (textField.isFocused()) { - int cursorPos = textField.getCursorPosition(); - String beforeCursor = text.substring(0, Math.min(cursorPos, text.length())); - int cursorX = (int) (fontRenderer.getStringWidth(beforeCursor) * TEXT_SCALE); - net.minecraft.client.gui.Gui.drawRect(x + 2 + cursorX, y + 1, x + 3 + cursorX, y + FIELD_HEIGHT - 1, 0xFFD0D0D0); - } - } - - public boolean isVisible() { - return visible; - } - - public void setVisible(boolean visible) { - this.visible = visible; - } - - public boolean isMouseOver(int mouseX, int mouseY) { - return mouseX >= textField.x && mouseX < textField.x + FIELD_WIDTH - && mouseY >= textField.y && mouseY < textField.y + FIELD_HEIGHT; - } - - public void mouseClicked(int mouseX, int mouseY, int mouseButton) { - textField.mouseClicked(mouseX, mouseY, mouseButton); - } - - public boolean keyTyped(char typedChar, int keyCode) { - // Enter submits - if (keyCode == Keyboard.KEY_RETURN || keyCode == Keyboard.KEY_NUMPADENTER) { - submitPriority(); - textField.setFocused(false); - - return true; - } - - // Escape cancels - if (keyCode == Keyboard.KEY_ESCAPE) { - textField.setText(String.valueOf(storage.getPriority())); - textField.setFocused(false); - - return true; - } - - // Filter to only allow numeric input and minus sign - if (Character.isDigit(typedChar) || typedChar == '-' - || keyCode == Keyboard.KEY_BACK || keyCode == Keyboard.KEY_DELETE - || keyCode == Keyboard.KEY_LEFT || keyCode == Keyboard.KEY_RIGHT - || keyCode == Keyboard.KEY_HOME || keyCode == Keyboard.KEY_END) { - return textField.textboxKeyTyped(typedChar, keyCode); - } - - return false; - } - - public boolean isFocused() { - return textField.isFocused(); - } - - public void onFocusLost() { - if (textField.isFocused()) { - submitPriority(); - textField.setFocused(false); - } - } - - private void submitPriority() { - try { - int newPriority = Integer.parseInt(textField.getText().trim()); - - if (newPriority != storage.getPriority()) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketSetPriority(storage.getId(), newPriority)); - lastKnownPriority = newPriority; - } - } catch (NumberFormatException e) { - // Invalid input - revert to current value - textField.setText(String.valueOf(storage.getPriority())); - } - } - - public StorageInfo getStorage() { - return storage; - } - } - - /** - * A single priority field for a storage bus. - */ - public static class StorageBusPriorityField { - - private static final float TEXT_SCALE = 0.65f; - - private StorageBusInfo storageBus; - private final GuiTextField textField; - private final FontRenderer fontRenderer; - private boolean visible = false; - private int lastKnownPriority; - - public StorageBusPriorityField(StorageBusInfo storageBus, FontRenderer fontRenderer) { - this.storageBus = storageBus; - this.fontRenderer = fontRenderer; - this.lastKnownPriority = storageBus.getPriority(); - this.textField = new GuiTextField(0, fontRenderer, 0, 0, FIELD_WIDTH, FIELD_HEIGHT); - this.textField.setMaxStringLength(8); - this.textField.setText(String.valueOf(storageBus.getPriority())); - } - - /** - * Update the storage bus reference. Called when storage bus data is refreshed from server. - */ - public void updateStorageBus(StorageBusInfo newStorageBus) { - this.storageBus = newStorageBus; - } - - public void updatePosition(int x, int y) { - this.textField.x = x; - this.textField.y = y; - this.visible = true; - - // Update text if storage bus priority changed externally - if (storageBus.getPriority() != lastKnownPriority && !textField.isFocused()) { - lastKnownPriority = storageBus.getPriority(); - textField.setText(String.valueOf(lastKnownPriority)); - } - } - - public void draw() { - int x = textField.x; - int y = textField.y; - - // Draw background (same style as PriorityField) - net.minecraft.client.gui.Gui.drawRect(x - 1, y - 1, x + FIELD_WIDTH + 1, y + FIELD_HEIGHT + 1, 0xFF373737); - net.minecraft.client.gui.Gui.drawRect(x, y, x + FIELD_WIDTH, y + FIELD_HEIGHT, textField.isFocused() ? 0xFF000000 : 0xFF1E1E1E); + Gui.drawRect(x - 1, y - 1, x + FIELD_WIDTH + 1, y + FIELD_HEIGHT + 1, 0xFF373737); + Gui.drawRect(x, y, x + FIELD_WIDTH, y + FIELD_HEIGHT, textField.isFocused() ? 0xFF000000 : 0xFF1E1E1E); // Draw text with scaling String text = textField.getText(); @@ -523,7 +272,7 @@ public boolean keyTyped(char typedChar, int keyCode) { // Escape cancels if (keyCode == Keyboard.KEY_ESCAPE) { - textField.setText(String.valueOf(storageBus.getPriority())); + textField.setText(String.valueOf(target.getPriority())); textField.setFocused(false); return true; @@ -555,18 +304,14 @@ private void submitPriority() { try { int newPriority = Integer.parseInt(textField.getText().trim()); - if (newPriority != storageBus.getPriority()) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketSetPriority(storageBus.getId(), newPriority)); + if (newPriority != target.getPriority()) { + CellTerminalNetwork.INSTANCE.sendToServer(new PacketSetPriority(target.getId(), newPriority)); lastKnownPriority = newPriority; } } catch (NumberFormatException e) { - // Invalid input - revert to current value - textField.setText(String.valueOf(storageBus.getPriority())); + // Invalid input, revert to current value + textField.setText(String.valueOf(target.getPriority())); } } - - public StorageBusInfo getStorageBus() { - return storageBus; - } } } diff --git a/src/main/java/com/cellterminal/gui/SearchFieldHandler.java b/src/main/java/com/cellterminal/gui/SearchFieldHandler.java new file mode 100644 index 0000000..5588bff --- /dev/null +++ b/src/main/java/com/cellterminal/gui/SearchFieldHandler.java @@ -0,0 +1,62 @@ +package com.cellterminal.gui; + +import appeng.client.gui.widgets.MEGuiTextField; + + +/** + * Handles search field click interactions (right-click clear, double-click modal). + *

+ * Encapsulates the click logic that was previously spread across GuiCellTerminalBase's + * mouseClicked and initSearchField methods. The GUI creates this handler alongside the + * search field and modal, then delegates search field clicks to it. + */ +public class SearchFieldHandler { + + private static final long DOUBLE_CLICK_THRESHOLD = GuiConstants.DOUBLE_CLICK_TIME_MS; + + private final MEGuiTextField searchField; + private final GuiModalSearchBar modalSearchBar; + private long lastClickTime = 0; + + public SearchFieldHandler(MEGuiTextField searchField, GuiModalSearchBar modalSearchBar) { + this.searchField = searchField; + this.modalSearchBar = modalSearchBar; + } + + /** + * Handle a mouse click on the search field area. + * + * @return true if the click was consumed and should not propagate + */ + public boolean handleClick(int mouseX, int mouseY, int mouseButton) { + if (!searchField.isMouseIn(mouseX, mouseY)) { + // Clicking outside search field, let the default handler manage focus + searchField.mouseClicked(mouseX, mouseY, mouseButton); + return false; + } + + // Right-click: clear and focus + if (mouseButton == 1) { + searchField.setText(""); + searchField.setFocused(true); + return true; + } + + // Left-click: check double-click to open modal + if (mouseButton == 0) { + long currentTime = System.currentTimeMillis(); + + if (currentTime - lastClickTime < DOUBLE_CLICK_THRESHOLD) { + if (modalSearchBar != null) modalSearchBar.open(searchField.y); + lastClickTime = 0; + return true; + } + + lastClickTime = currentTime; + } + + // Pass through to default handling (focus, cursor positioning) + searchField.mouseClicked(mouseX, mouseY, mouseButton); + return false; + } +} diff --git a/src/main/java/com/cellterminal/gui/FilterPanelManager.java b/src/main/java/com/cellterminal/gui/buttons/FilterPanelManager.java similarity index 94% rename from src/main/java/com/cellterminal/gui/FilterPanelManager.java rename to src/main/java/com/cellterminal/gui/buttons/FilterPanelManager.java index 2bc7c27..021c46d 100644 --- a/src/main/java/com/cellterminal/gui/FilterPanelManager.java +++ b/src/main/java/com/cellterminal/gui/buttons/FilterPanelManager.java @@ -1,4 +1,4 @@ -package com.cellterminal.gui; +package com.cellterminal.gui.buttons; import java.awt.Rectangle; import java.util.ArrayList; @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; +import com.cellterminal.gui.GuiConstants; import net.minecraft.client.gui.GuiButton; import com.cellterminal.client.CellFilter; @@ -30,7 +31,7 @@ */ public class FilterPanelManager { - private static final int BUTTON_SIZE = GuiFilterButton.BUTTON_SIZE; + private static final int BUTTON_SIZE = GuiFilterButton.SIZE; private static final int BUTTON_SPACING = 2; private static final int BUTTON_WITH_SPACING = BUTTON_SIZE + BUTTON_SPACING; @@ -44,6 +45,7 @@ public class FilterPanelManager { private GuiSlotLimitButton slotLimitButton = null; private boolean forStorageBus = false; + private int currentTab = 0; /** * Initialize filter buttons for the given tab. @@ -64,13 +66,17 @@ public int initButtons(List buttonList, int startButtonId, int curren if (slotLimitButton != null) buttonList.remove(slotLimitButton); this.forStorageBus = currentTab >= GuiConstants.TAB_TEMP_AREA; + this.currentTab = currentTab; CellTerminalClientConfig config = CellTerminalClientConfig.getInstance(); int buttonId = startButtonId; - // Create slot limit button first (only for tabs that show content) - if (currentTab == GuiConstants.TAB_INVENTORY || currentTab == GuiConstants.TAB_STORAGE_BUS_INVENTORY) { - SlotLimit limit = config.getSlotLimit(forStorageBus); + // Create slot limit button for tabs that show content (inventory, storage bus inventory, subnet overview) + // FIXME: use GuiConstants + if (currentTab == GuiConstants.TAB_INVENTORY + || currentTab == GuiConstants.TAB_STORAGE_BUS_INVENTORY + || currentTab < 0) { + SlotLimit limit = config.getSlotLimitForTab(currentTab); slotLimitButton = new GuiSlotLimitButton(buttonId++, 0, 0, limit); buttonList.add(slotLimitButton); } else { @@ -326,8 +332,8 @@ public Rectangle getBounds() { // Include slot limit button if (slotLimitButton != null && slotLimitButton.visible) { - minX = Math.min(minX, slotLimitButton.x); - minY = Math.min(minY, slotLimitButton.y); + minX = slotLimitButton.x; + minY = slotLimitButton.y; maxX = Math.max(maxX, slotLimitButton.x + BUTTON_SIZE); maxY = Math.max(maxY, slotLimitButton.y + BUTTON_SIZE); } @@ -413,7 +419,9 @@ public boolean handleSlotLimitClick(GuiSlotLimitButton button) { SlotLimit newLimit = button.cycleLimit(); CellTerminalClientConfig config = CellTerminalClientConfig.getInstance(); - if (forStorageBus) { + if (currentTab < 0) { + config.setSubnetSlotLimit(newLimit); + } else if (forStorageBus) { config.setBusSlotLimit(newLimit); } else { config.setCellSlotLimit(newLimit); @@ -422,7 +430,8 @@ public boolean handleSlotLimitClick(GuiSlotLimitButton button) { // Send updated slot limits to server CellTerminalNetwork.INSTANCE.sendToServer(new PacketSlotLimitChange( config.getCellSlotLimit().getLimit(), - config.getBusSlotLimit().getLimit() + config.getBusSlotLimit().getLimit(), + config.getSubnetSlotLimit().getLimit() )); return true; @@ -432,7 +441,7 @@ public boolean handleSlotLimitClick(GuiSlotLimitButton button) { * Get the current slot limit based on the current tab. */ public SlotLimit getCurrentSlotLimit() { - return CellTerminalClientConfig.getInstance().getSlotLimit(forStorageBus); + return CellTerminalClientConfig.getInstance().getSlotLimitForTab(currentTab); } public List getButtons() { diff --git a/src/main/java/com/cellterminal/gui/buttons/GuiAtlasButton.java b/src/main/java/com/cellterminal/gui/buttons/GuiAtlasButton.java new file mode 100644 index 0000000..e20e494 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/buttons/GuiAtlasButton.java @@ -0,0 +1,86 @@ +package com.cellterminal.gui.buttons; + +import java.util.List; + +import com.cellterminal.gui.GuiConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.util.ResourceLocation; + + +/** + * Base class for all buttons that use atlas.png for background rendering. + *

+ * Provides shared logic for: + *

    + *
  • Visibility check and early return
  • + *
  • Hover detection from mouse coordinates
  • + *
  • Binding the atlas texture and setting up blend state
  • + *
  • Drawing the background from the atlas at the UV returned by subclasses
  • + *
+ * + * Subclasses implement: + *
    + *
  • {@link #getBackgroundTexX()}: atlas U for background (may depend on state)
  • + *
  • {@link #getBackgroundTexY()}: atlas V for background (typically offset by size when hovered)
  • + *
  • {@link #drawForeground(Minecraft)}: any overlay on top of the background
  • + *
  • {@link #getTooltip()}: tooltip lines for the button
  • + *
+ */ +public abstract class GuiAtlasButton extends GuiButton { + + protected static final ResourceLocation ATLAS_TEXTURE = + new ResourceLocation("cellterminal", "textures/guis/atlas.png"); + + protected GuiAtlasButton(int buttonId, int x, int y, int size) { + super(buttonId, x, y, size, size, ""); + } + + /** + * Get the texture X coordinate in the atlas for the background. + * Called every frame, may depend on button state. + */ + protected abstract int getBackgroundTexX(); + + /** + * Get the texture Y coordinate in the atlas for the background. + * Called every frame, typically returns baseY + (hovered ? size : 0). + */ + protected abstract int getBackgroundTexY(); + + /** + * Draw any content on top of the atlas background. + * Called after the background is drawn with the atlas still bound. + * Default implementation does nothing. + */ + protected void drawForeground(Minecraft mc) { + // Override in subclasses for icons, text, state indicators, etc. + } + + /** + * Get tooltip lines for this button. + */ + public abstract List getTooltip(); + + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY, float partialTicks) { + if (!this.visible) return; + + this.hovered = mouseX >= this.x && mouseY >= this.y + && mouseX < this.x + this.width && mouseY < this.y + this.height; + + mc.getTextureManager().bindTexture(ATLAS_TEXTURE); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + + Gui.drawScaledCustomSizeModalRect( + this.x, this.y, + getBackgroundTexX(), getBackgroundTexY(), + this.width, this.height, this.width, this.height, + GuiConstants.ATLAS_WIDTH, GuiConstants.ATLAS_HEIGHT); + + drawForeground(mc); + } +} diff --git a/src/main/java/com/cellterminal/gui/buttons/GuiBackButton.java b/src/main/java/com/cellterminal/gui/buttons/GuiBackButton.java new file mode 100644 index 0000000..b755ac5 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/buttons/GuiBackButton.java @@ -0,0 +1,62 @@ +package com.cellterminal.gui.buttons; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.resources.I18n; + +import com.cellterminal.gui.GuiConstants; + + +/** + * A small button used to navigate to/from subnet overview mode. + * When in normal view (main or subnet), shows a left arrow to enter overview. + * When in overview mode, shows a right arrow to go back. + */ +public class GuiBackButton extends GuiAtlasButton { + + private static final int SIZE = GuiConstants.SUBNET_BUTTON_SIZE; + + private boolean isInOverviewMode; + + public GuiBackButton(int buttonId, int x, int y) { + super(buttonId, x, y, SIZE); + this.isInOverviewMode = false; + } + + /** + * Update the button state based on whether we're in subnet overview mode. + */ + public void setInOverviewMode(boolean inOverview) { + this.isInOverviewMode = inOverview; + } + + @Override + protected int getBackgroundTexX() { + return GuiConstants.SUBNET_BUTTON_X + (isInOverviewMode ? SIZE : 0); + } + + @Override + protected int getBackgroundTexY() { + return GuiConstants.SUBNET_BUTTON_Y + (this.hovered ? SIZE : 0); + } + + /** + * Get the tooltip for this button. + */ + public List getTooltip() { + List tooltip = new ArrayList<>(); + + if (isInOverviewMode) { + tooltip.add(I18n.format("cellterminal.subnet.back")); + tooltip.add(""); + tooltip.add(I18n.format("cellterminal.subnet.back.desc")); + } else { + tooltip.add(I18n.format("cellterminal.subnet.overview")); + tooltip.add(""); + tooltip.add(I18n.format("cellterminal.subnet.overview.desc")); + } + + return tooltip; + } +} diff --git a/src/main/java/com/cellterminal/gui/GuiFilterButton.java b/src/main/java/com/cellterminal/gui/buttons/GuiFilterButton.java similarity index 66% rename from src/main/java/com/cellterminal/gui/GuiFilterButton.java rename to src/main/java/com/cellterminal/gui/buttons/GuiFilterButton.java index 659c26d..41b8a3b 100644 --- a/src/main/java/com/cellterminal/gui/GuiFilterButton.java +++ b/src/main/java/com/cellterminal/gui/buttons/GuiFilterButton.java @@ -1,10 +1,10 @@ -package com.cellterminal.gui; +package com.cellterminal.gui.buttons; import java.util.ArrayList; import java.util.List; +import com.cellterminal.gui.GuiConstants; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiButton; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.client.renderer.RenderItem; @@ -26,17 +26,13 @@ * A toggle button for cell/storage bus filters. * Cycles through three states: SHOW_ALL (neutral), SHOW_ONLY (green), HIDE (red). */ -public class GuiFilterButton extends GuiButton { - - public static final int BUTTON_SIZE = 16; +public class GuiFilterButton extends GuiAtlasButton { private final CellFilter filter; private State currentState; // Colors for different states - private static final int COLOR_NEUTRAL = 0xFF8B8B8B; // Grey - show all - private static final int COLOR_SHOW = 0xFF4CAF50; // Green - show only - private static final int COLOR_HIDE = 0xFFE53935; // Red - hide + public static final int SIZE = GuiConstants.TERMINAL_SIDE_BUTTON_SIZE; private static final ItemStack ITEM_CELL_ICON = new ItemStack(Blocks.STONE); private static final ItemStack FLUID_CELL_ICON = new ItemStack(Items.BUCKET); @@ -46,7 +42,7 @@ public class GuiFilterButton extends GuiButton { cellWorkbench().maybeStack(1).orElse(ItemStack.EMPTY); public GuiFilterButton(int buttonId, int x, int y, CellFilter filter, State initialState) { - super(buttonId, x, y, BUTTON_SIZE, BUTTON_SIZE, ""); + super(buttonId, x, y, SIZE); this.filter = filter; this.currentState = initialState; } @@ -70,41 +66,25 @@ public State cycleState() { } @Override - public void drawButton(Minecraft mc, int mouseX, int mouseY, float partialTicks) { - if (!this.visible) return; - - this.hovered = mouseX >= this.x && mouseY >= this.y - && mouseX < this.x + this.width && mouseY < this.y + this.height; - - // Draw button background based on state - int bgColor = getBackgroundColor(); - if (this.hovered) bgColor = brightenColor(bgColor, 0.2f); - - drawRect(this.x, this.y, this.x + this.width, this.y + this.height, bgColor); - - // Draw 3D border - drawRect(this.x, this.y, this.x + this.width, this.y + 1, brightenColor(bgColor, 0.3f)); - drawRect(this.x, this.y, this.x + 1, this.y + this.height, brightenColor(bgColor, 0.3f)); - drawRect(this.x, this.y + this.height - 1, this.x + this.width, this.y + this.height, darkenColor(bgColor, 0.3f)); - drawRect(this.x + this.width - 1, this.y, this.x + this.width, this.y + this.height, darkenColor(bgColor, 0.3f)); - - // Draw filter icon - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - drawFilterIcon(mc); - } - - private int getBackgroundColor() { + protected int getBackgroundTexX() { switch (currentState) { case SHOW_ONLY: - return COLOR_SHOW; + return GuiConstants.TERMINAL_STYLE_BUTTON_X + SIZE; // Green background case HIDE: - return COLOR_HIDE; + return GuiConstants.TERMINAL_STYLE_BUTTON_X + 2 * SIZE; // Red background default: - return COLOR_NEUTRAL; + return GuiConstants.TERMINAL_STYLE_BUTTON_X; // Default background } } - private void drawFilterIcon(Minecraft mc) { + @Override + protected int getBackgroundTexY() { + return GuiConstants.TERMINAL_STYLE_BUTTON_Y + (this.hovered ? SIZE : 0); + } + + @Override + protected void drawForeground(Minecraft mc) { + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); ItemStack iconStack; switch (filter) { @@ -185,22 +165,4 @@ public List getTooltip() { return tooltip; } - - private static int brightenColor(int color, float factor) { - int a = (color >> 24) & 0xFF; - int r = Math.min(255, (int) (((color >> 16) & 0xFF) * (1 + factor))); - int g = Math.min(255, (int) (((color >> 8) & 0xFF) * (1 + factor))); - int b = Math.min(255, (int) ((color & 0xFF) * (1 + factor))); - - return (a << 24) | (r << 16) | (g << 8) | b; - } - - private static int darkenColor(int color, float factor) { - int a = (color >> 24) & 0xFF; - int r = (int) (((color >> 16) & 0xFF) * (1 - factor)); - int g = (int) (((color >> 8) & 0xFF) * (1 - factor)); - int b = (int) ((color & 0xFF) * (1 - factor)); - - return (a << 24) | (r << 16) | (g << 8) | b; - } } diff --git a/src/main/java/com/cellterminal/gui/GuiSearchHelpButton.java b/src/main/java/com/cellterminal/gui/buttons/GuiSearchHelpButton.java similarity index 71% rename from src/main/java/com/cellterminal/gui/GuiSearchHelpButton.java rename to src/main/java/com/cellterminal/gui/buttons/GuiSearchHelpButton.java index d4d7314..737f99e 100644 --- a/src/main/java/com/cellterminal/gui/GuiSearchHelpButton.java +++ b/src/main/java/com/cellterminal/gui/buttons/GuiSearchHelpButton.java @@ -1,46 +1,31 @@ -package com.cellterminal.gui; +package com.cellterminal.gui.buttons; import java.util.ArrayList; import java.util.List; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiButton; +import com.cellterminal.gui.GuiConstants; import net.minecraft.client.resources.I18n; /** * A small "?" button that shows search syntax help when hovered. */ -public class GuiSearchHelpButton extends GuiButton { +public class GuiSearchHelpButton extends GuiAtlasButton { - public static final int BUTTON_SIZE = 10; + public static final int SIZE = GuiConstants.TOOLTIP_BUTTON_SIZE; public GuiSearchHelpButton(int buttonId, int x, int y) { - super(buttonId, x, y, BUTTON_SIZE, BUTTON_SIZE, "?"); + super(buttonId, x, y, SIZE); } @Override - public void drawButton(Minecraft mc, int mouseX, int mouseY, float partialTicks) { - if (!this.visible) return; - - this.hovered = mouseX >= this.x && mouseY >= this.y - && mouseX < this.x + this.width && mouseY < this.y + this.height; - - // Draw button background - int bgColor = this.hovered ? 0xFF505050 : 0xFF606060; - drawRect(this.x, this.y, this.x + this.width, this.y + this.height, bgColor); - - // Draw border - drawRect(this.x, this.y, this.x + this.width, this.y + 1, 0xFF808080); - drawRect(this.x, this.y, this.x + 1, this.y + this.height, 0xFF808080); - drawRect(this.x, this.y + this.height - 1, this.x + this.width, this.y + this.height, 0xFF303030); - drawRect(this.x + this.width - 1, this.y, this.x + this.width, this.y + this.height, 0xFF303030); + protected int getBackgroundTexX() { + return GuiConstants.TOOLTIP_BUTTON_X; + } - // Draw "?" centered - int textX = this.x + (this.width - mc.fontRenderer.getStringWidth("?")) / 2; - int textY = this.y + (this.height - 8) / 2; - int textColor = this.hovered ? 0xFFFFFF00 : 0xFFCCCCCC; - mc.fontRenderer.drawString("?", textX, textY, textColor); + @Override + protected int getBackgroundTexY() { + return GuiConstants.TOOLTIP_BUTTON_Y + (this.hovered ? SIZE : 0); } /** diff --git a/src/main/java/com/cellterminal/gui/buttons/GuiSearchModeButton.java b/src/main/java/com/cellterminal/gui/buttons/GuiSearchModeButton.java new file mode 100644 index 0000000..976fa98 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/buttons/GuiSearchModeButton.java @@ -0,0 +1,74 @@ +package com.cellterminal.gui.buttons; + +import java.util.ArrayList; +import java.util.List; + +import com.cellterminal.gui.GuiConstants; +import net.minecraft.client.resources.I18n; + +import com.cellterminal.client.SearchFilterMode; + + +/** + * Button that cycles through search filter modes and displays a visual indicator. +*/ +public class GuiSearchModeButton extends GuiAtlasButton { + + private static final int SIZE = GuiConstants.SEARCH_MODE_BUTTON_SIZE; + + private SearchFilterMode currentMode; + + public GuiSearchModeButton(int buttonId, int x, int y, SearchFilterMode initialMode) { + super(buttonId, x, y, SIZE); + this.currentMode = initialMode; + } + + public void setMode(SearchFilterMode mode) { + this.currentMode = mode; + } + + public SearchFilterMode getMode() { + return currentMode; + } + + public SearchFilterMode cycleMode() { + this.currentMode = this.currentMode.next(); + + return this.currentMode; + } + + @Override + protected int getBackgroundTexX() { + return GuiConstants.SEARCH_MODE_BUTTON_X + currentMode.ordinal() * SIZE; + } + + @Override + protected int getBackgroundTexY() { + return GuiConstants.SEARCH_MODE_BUTTON_Y + (this.hovered ? SIZE : 0); + } + + /** + * Get the tooltip lines for this button. + */ + public List getTooltip() { + List tooltip = new ArrayList<>(); + tooltip.add(I18n.format("gui.cellterminal.search_mode")); + + String modeKey; + switch (currentMode) { + case INVENTORY: + modeKey = "gui.cellterminal.search_mode.inventory"; + break; + case PARTITION: + modeKey = "gui.cellterminal.search_mode.partition"; + break; + case MIXED: + default: + modeKey = "gui.cellterminal.search_mode.mixed"; + break; + } + tooltip.add("§7" + I18n.format(modeKey)); + + return tooltip; + } +} diff --git a/src/main/java/com/cellterminal/gui/GuiSlotLimitButton.java b/src/main/java/com/cellterminal/gui/buttons/GuiSlotLimitButton.java similarity index 56% rename from src/main/java/com/cellterminal/gui/GuiSlotLimitButton.java rename to src/main/java/com/cellterminal/gui/buttons/GuiSlotLimitButton.java index e21d46b..1a0e5ea 100644 --- a/src/main/java/com/cellterminal/gui/GuiSlotLimitButton.java +++ b/src/main/java/com/cellterminal/gui/buttons/GuiSlotLimitButton.java @@ -1,12 +1,12 @@ -package com.cellterminal.gui; +package com.cellterminal.gui.buttons; import java.util.ArrayList; import java.util.List; +import com.cellterminal.gui.GuiConstants; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.resources.I18n; -import net.minecraft.client.gui.GuiButton; import com.cellterminal.client.SlotLimit; @@ -15,18 +15,15 @@ * Button for cycling through slot limit options (8, 32, 64, unlimited). * Controls how many content types are displayed per cell/storage bus. */ -public class GuiSlotLimitButton extends GuiButton { +public class GuiSlotLimitButton extends GuiAtlasButton { - public static final int BUTTON_SIZE = 16; - - private static final int COLOR_NORMAL = 0xFF8B8B8B; - private static final int COLOR_HOVER = 0xFF707070; + private static final int SIZE = GuiConstants.TERMINAL_SIDE_BUTTON_SIZE; private static final int COLOR_TEXT = 0xFFFFFFFF; private SlotLimit currentLimit; public GuiSlotLimitButton(int buttonId, int x, int y, SlotLimit initialLimit) { - super(buttonId, x, y, BUTTON_SIZE, BUTTON_SIZE, ""); + super(buttonId, x, y, SIZE); this.currentLimit = initialLimit; } @@ -48,22 +45,17 @@ public SlotLimit cycleLimit() { } @Override - public void drawButton(Minecraft mc, int mouseX, int mouseY, float partialTicks) { - if (!this.visible) return; - - this.hovered = mouseX >= this.x && mouseY >= this.y - && mouseX < this.x + this.width && mouseY < this.y + this.height; - - int bgColor = this.hovered ? COLOR_HOVER : COLOR_NORMAL; - drawRect(this.x, this.y, this.x + this.width, this.y + this.height, bgColor); + protected int getBackgroundTexX() { + return GuiConstants.TERMINAL_STYLE_BUTTON_X; + } - // Draw 3D border - drawRect(this.x, this.y, this.x + this.width, this.y + 1, brightenColor(bgColor, 0.3f)); - drawRect(this.x, this.y, this.x + 1, this.y + this.height, brightenColor(bgColor, 0.3f)); - drawRect(this.x, this.y + this.height - 1, this.x + this.width, this.y + this.height, darkenColor(bgColor, 0.3f)); - drawRect(this.x + this.width - 1, this.y, this.x + this.width, this.y + this.height, darkenColor(bgColor, 0.3f)); + @Override + protected int getBackgroundTexY() { + return GuiConstants.TERMINAL_STYLE_BUTTON_Y + (this.hovered ? SIZE : 0); + } - // Draw limit text centered + @Override + protected void drawForeground(Minecraft mc) { FontRenderer fr = mc.fontRenderer; String text = currentLimit.getDisplayText(); int textWidth = fr.getStringWidth(text); @@ -104,22 +96,4 @@ public List getTooltip() { tooltip.add(I18n.format("gui.cellterminal.slot_limit", limitText)); return tooltip; } - - private static int brightenColor(int color, float amount) { - int a = (color >> 24) & 0xFF; - int r = Math.min(255, (int) (((color >> 16) & 0xFF) + 255 * amount)); - int g = Math.min(255, (int) (((color >> 8) & 0xFF) + 255 * amount)); - int b = Math.min(255, (int) ((color & 0xFF) + 255 * amount)); - - return (a << 24) | (r << 16) | (g << 8) | b; - } - - private static int darkenColor(int color, float amount) { - int a = (color >> 24) & 0xFF; - int r = (int) (((color >> 16) & 0xFF) * (1 - amount)); - int g = (int) (((color >> 8) & 0xFF) * (1 - amount)); - int b = (int) ((color & 0xFF) * (1 - amount)); - - return (a << 24) | (r << 16) | (g << 8) | b; - } } diff --git a/src/main/java/com/cellterminal/gui/GuiSubnetVisibilityButton.java b/src/main/java/com/cellterminal/gui/buttons/GuiSubnetVisibilityButton.java similarity index 72% rename from src/main/java/com/cellterminal/gui/GuiSubnetVisibilityButton.java rename to src/main/java/com/cellterminal/gui/buttons/GuiSubnetVisibilityButton.java index b8d3b3d..bea4f20 100644 --- a/src/main/java/com/cellterminal/gui/GuiSubnetVisibilityButton.java +++ b/src/main/java/com/cellterminal/gui/buttons/GuiSubnetVisibilityButton.java @@ -1,10 +1,10 @@ -package com.cellterminal.gui; +package com.cellterminal.gui.buttons; import java.util.ArrayList; import java.util.List; +import com.cellterminal.gui.GuiConstants; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiButton; import net.minecraft.client.resources.I18n; import com.cellterminal.client.SubnetVisibility; @@ -16,19 +16,19 @@ * - Show Favorites: Star icon * - Show All: Globe/network icon */ -public class GuiSubnetVisibilityButton extends GuiButton { - - private static final int BUTTON_SIZE = 12; +public class GuiSubnetVisibilityButton extends GuiAtlasButton { // Colors private static final int COLOR_DISABLED = 0xFF808080; // Grey for "don't show" private static final int COLOR_FAVORITES = 0xFFFFD700; // Gold for favorites private static final int COLOR_ALL = 0xFF4CAF50; // Green for show all + private static final int SIZE = GuiConstants.TERMINAL_SIDE_BUTTON_SIZE; + private SubnetVisibility currentMode; public GuiSubnetVisibilityButton(int buttonId, int x, int y, SubnetVisibility initialMode) { - super(buttonId, x, y, BUTTON_SIZE, BUTTON_SIZE, ""); + super(buttonId, x, y, SIZE); this.currentMode = initialMode; } @@ -47,29 +47,23 @@ public SubnetVisibility cycleMode() { } @Override - public void drawButton(Minecraft mc, int mouseX, int mouseY, float partialTicks) { - if (!this.visible) return; - - this.hovered = mouseX >= this.x && mouseY >= this.y - && mouseX < this.x + this.width && mouseY < this.y + this.height; - - // Draw button background - int bgColor = this.hovered ? 0xFF707070 : 0xFF8B8B8B; - drawRect(this.x, this.y, this.x + this.width, this.y + this.height, bgColor); + protected int getBackgroundTexX() { + return GuiConstants.TERMINAL_STYLE_BUTTON_X; + } - // Draw outline - drawRect(this.x, this.y, this.x + this.width, this.y + 1, 0xFFFFFFFF); - drawRect(this.x, this.y, this.x + 1, this.y + this.height, 0xFFFFFFFF); - drawRect(this.x, this.y + this.height - 1, this.x + this.width, this.y + this.height, 0xFF555555); - drawRect(this.x + this.width - 1, this.y, this.x + this.width, this.y + this.height, 0xFF555555); + @Override + protected int getBackgroundTexY() { + return GuiConstants.TERMINAL_STYLE_BUTTON_Y + (this.hovered ? SIZE : 0); + } - // Draw the mode indicator + @Override + protected void drawForeground(Minecraft mc) { drawModeIndicator(mc); } private void drawModeIndicator(Minecraft mc) { - int centerX = this.x + BUTTON_SIZE / 2; - int centerY = this.y + BUTTON_SIZE / 2; + int centerX = this.x + SIZE / 2; + int centerY = this.y + SIZE / 2; switch (currentMode) { case DONT_SHOW: diff --git a/src/main/java/com/cellterminal/gui/buttons/GuiTerminalStyleButton.java b/src/main/java/com/cellterminal/gui/buttons/GuiTerminalStyleButton.java new file mode 100644 index 0000000..e8a8e8c --- /dev/null +++ b/src/main/java/com/cellterminal/gui/buttons/GuiTerminalStyleButton.java @@ -0,0 +1,75 @@ +package com.cellterminal.gui.buttons; + +import java.util.ArrayList; +import java.util.List; + +import com.cellterminal.gui.GuiConstants; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.resources.I18n; +import net.minecraft.util.ResourceLocation; + +import com.cellterminal.config.CellTerminalClientConfig.TerminalStyle; + + +/** + * Button to toggle between terminal styles (small/tall). + * Uses atlas.png for the background and states.png for the style icon overlay. + */ +public class GuiTerminalStyleButton extends GuiAtlasButton { + + private static final int SIZE = GuiConstants.TERMINAL_SIDE_BUTTON_SIZE; + private static final ResourceLocation AE2_STATES = + new ResourceLocation("appliedenergistics2", "textures/guis/states.png"); + + private TerminalStyle currentStyle; + + public GuiTerminalStyleButton(int buttonId, int x, int y, TerminalStyle initialStyle) { + super(buttonId, x, y, SIZE); + this.currentStyle = initialStyle; + } + + public void setStyle(TerminalStyle style) { + this.currentStyle = style; + } + + public TerminalStyle getStyle() { + return currentStyle; + } + + @Override + protected int getBackgroundTexX() { + return GuiConstants.TERMINAL_STYLE_BUTTON_X; + } + + @Override + protected int getBackgroundTexY() { + return GuiConstants.TERMINAL_STYLE_BUTTON_Y + (this.hovered ? SIZE : 0); + } + + @Override + protected void drawForeground(Minecraft mc) { + // Terminal style icon: row 13 in states.png, column 0 = tall, column 1 = compact + mc.getTextureManager().bindTexture(AE2_STATES); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + + int iconX = currentStyle == TerminalStyle.TALL ? 0 : SIZE; + int iconY = 13 * SIZE; + drawTexturedModalRect(this.x, this.y, iconX, iconY, SIZE, SIZE); + } + + /** + * Get the tooltip lines for this button. + */ + public List getTooltip() { + List tooltip = new ArrayList<>(); + tooltip.add(I18n.format("gui.cellterminal.terminal_style")); + + String styleKey = currentStyle == TerminalStyle.SMALL + ? "gui.cellterminal.terminal_style.small" + : "gui.cellterminal.terminal_style.tall"; + tooltip.add("§7" + I18n.format(styleKey)); + + return tooltip; + } +} diff --git a/src/main/java/com/cellterminal/gui/cells/CellHoverState.java b/src/main/java/com/cellterminal/gui/cells/CellHoverState.java deleted file mode 100644 index a0b2d09..0000000 --- a/src/main/java/com/cellterminal/gui/cells/CellHoverState.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.cellterminal.gui.cells; - -import java.util.List; - -import net.minecraft.item.ItemStack; - -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.StorageInfo; -import com.cellterminal.gui.render.RenderContext; - - -/** - * Tracks hover state for cell-related GUI elements. - *

- * This class encapsulates all the hover tracking for cells in the - * Inventory and Partition tabs. It is updated during rendering and - * consumed by click handlers and tooltip rendering. - */ -public class CellHoverState { - - // Hovered cell (for cell slot hover) - private CellInfo hoveredCell = null; - private StorageInfo hoveredCellStorage = null; - private int hoveredCellSlotIndex = -1; - - // Hovered content item within a cell - private ItemStack hoveredContentStack = ItemStack.EMPTY; - private int hoveredContentSlotIndex = -1; - private int hoveredContentX = 0; - private int hoveredContentY = 0; - - // Hovered partition slot - private CellInfo hoveredPartitionCell = null; - private int hoveredPartitionSlotIndex = -1; - - // Hovered action buttons - private CellInfo hoveredPartitionAllButtonCell = null; - private CellInfo hoveredClearPartitionButtonCell = null; - - // Hovered storage line (header row) - private StorageInfo hoveredStorageLine = null; - private int hoveredLineIndex = -1; - - // JEI ghost ingredient targets - private final List partitionSlotTargets; - - public CellHoverState(List partitionSlotTargets) { - this.partitionSlotTargets = partitionSlotTargets; - } - - /** - * Reset all hover state at the beginning of a render cycle. - */ - public void reset() { - hoveredCell = null; - hoveredCellStorage = null; - hoveredCellSlotIndex = -1; - hoveredContentStack = ItemStack.EMPTY; - hoveredContentSlotIndex = -1; - hoveredContentX = 0; - hoveredContentY = 0; - hoveredPartitionCell = null; - hoveredPartitionSlotIndex = -1; - hoveredPartitionAllButtonCell = null; - hoveredClearPartitionButtonCell = null; - hoveredStorageLine = null; - hoveredLineIndex = -1; - partitionSlotTargets.clear(); - } - - // ======================================== - // CELL SLOT HOVER - // ======================================== - - public void setHoveredCell(CellInfo cell, StorageInfo storage, int slotIndex) { - this.hoveredCell = cell; - this.hoveredCellStorage = storage; - this.hoveredCellSlotIndex = slotIndex; - } - - public CellInfo getHoveredCell() { - return hoveredCell; - } - - public StorageInfo getHoveredCellStorage() { - return hoveredCellStorage; - } - - public int getHoveredCellSlotIndex() { - return hoveredCellSlotIndex; - } - - // ======================================== - // CONTENT HOVER - // ======================================== - - public void setHoveredContent(ItemStack stack, int slotIndex, int absX, int absY) { - this.hoveredContentStack = stack; - this.hoveredContentSlotIndex = slotIndex; - this.hoveredContentX = absX; - this.hoveredContentY = absY; - } - - public ItemStack getHoveredContentStack() { - return hoveredContentStack; - } - - public int getHoveredContentSlotIndex() { - return hoveredContentSlotIndex; - } - - public int getHoveredContentX() { - return hoveredContentX; - } - - public int getHoveredContentY() { - return hoveredContentY; - } - - // ======================================== - // PARTITION SLOT HOVER - // ======================================== - - public void setHoveredPartitionSlot(CellInfo cell, int slotIndex) { - this.hoveredPartitionCell = cell; - this.hoveredPartitionSlotIndex = slotIndex; - } - - public CellInfo getHoveredPartitionCell() { - return hoveredPartitionCell; - } - - public int getHoveredPartitionSlotIndex() { - return hoveredPartitionSlotIndex; - } - - // ======================================== - // BUTTON HOVER - // ======================================== - - public void setHoveredPartitionAllButton(CellInfo cell) { - this.hoveredPartitionAllButtonCell = cell; - } - - public CellInfo getHoveredPartitionAllButtonCell() { - return hoveredPartitionAllButtonCell; - } - - public void setHoveredClearPartitionButton(CellInfo cell) { - this.hoveredClearPartitionButtonCell = cell; - } - - public CellInfo getHoveredClearPartitionButtonCell() { - return hoveredClearPartitionButtonCell; - } - - // ======================================== - // STORAGE LINE HOVER - // ======================================== - - public void setHoveredStorageLine(StorageInfo storage, int lineIndex) { - this.hoveredStorageLine = storage; - this.hoveredLineIndex = lineIndex; - } - - public StorageInfo getHoveredStorageLine() { - return hoveredStorageLine; - } - - public int getHoveredLineIndex() { - return hoveredLineIndex; - } - - // ======================================== - // JEI GHOST TARGETS - // ======================================== - - public void addPartitionSlotTarget(CellInfo cell, int slotIndex, int x, int y, int width, int height) { - partitionSlotTargets.add(new RenderContext.PartitionSlotTarget(cell, slotIndex, x, y, width, height)); - } - - public List getPartitionSlotTargets() { - return partitionSlotTargets; - } - - /** - * Copy hover state to a RenderContext for compatibility with existing code. - */ - public void copyToRenderContext(RenderContext ctx) { - ctx.hoveredCellCell = hoveredCell; - ctx.hoveredCellStorage = hoveredCellStorage; - ctx.hoveredCellSlotIndex = hoveredCellSlotIndex; - ctx.hoveredContentStack = hoveredContentStack; - ctx.hoveredContentSlotIndex = hoveredContentSlotIndex; - ctx.hoveredContentX = hoveredContentX; - ctx.hoveredContentY = hoveredContentY; - ctx.hoveredPartitionCell = hoveredPartitionCell; - ctx.hoveredPartitionSlotIndex = hoveredPartitionSlotIndex; - ctx.hoveredPartitionAllButtonCell = hoveredPartitionAllButtonCell; - ctx.hoveredClearPartitionButtonCell = hoveredClearPartitionButtonCell; - ctx.hoveredStorageLine = hoveredStorageLine; - ctx.hoveredLineIndex = hoveredLineIndex; - } -} diff --git a/src/main/java/com/cellterminal/gui/cells/CellRenderer.java b/src/main/java/com/cellterminal/gui/cells/CellRenderer.java deleted file mode 100644 index a9fda62..0000000 --- a/src/main/java/com/cellterminal/gui/cells/CellRenderer.java +++ /dev/null @@ -1,438 +0,0 @@ -package com.cellterminal.gui.cells; - -import java.util.List; -import java.util.Map; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.RenderItem; -import net.minecraft.item.ItemStack; - -import com.cellterminal.client.CellContentRow; -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.EmptySlotInfo; -import com.cellterminal.client.StorageInfo; -import com.cellterminal.client.TabStateManager; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.render.RenderContext; - - -/** - * Main renderer for cell-related GUI elements. - *

- * This class coordinates the rendering of cells in the Inventory and Partition tabs. - * It delegates to specialized sub-renderers for slots, tree lines, and content items. - *

- * Usage: - *

- * CellRenderer renderer = new CellRenderer(fontRenderer, itemRender);
- * // For inventory tab:
- * renderer.drawCellInventoryLine(cell, startIndex, isFirstRow, y, ...);
- * // For partition tab:
- * renderer.drawCellPartitionLine(cell, startIndex, isFirstRow, y, ...);
- * 
- */ -public class CellRenderer { - - private final FontRenderer fontRenderer; - private final CellSlotRenderer slotRenderer; - private final CellTreeRenderer treeRenderer; - - public CellRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.fontRenderer = fontRenderer; - this.slotRenderer = new CellSlotRenderer(fontRenderer, itemRender); - this.treeRenderer = new CellTreeRenderer(); - } - - /** - * Get the slot renderer for custom slot drawing. - */ - public CellSlotRenderer getSlotRenderer() { - return slotRenderer; - } - - /** - * Get the tree renderer for custom tree line drawing. - */ - public CellTreeRenderer getTreeRenderer() { - return treeRenderer; - } - - // ======================================== - // STORAGE HEADER RENDERING - // ======================================== - - /** - * Draw a storage header line (ME Drive or Chest). - * - * @param storage The storage info - * @param y Y position - * @param lines All lines in the list (for checking next line) - * @param lineIndex Current line index - * @param tabType The tab type for determining expansion state - * @param ctx Render context for tracking visible storages - */ - public void drawStorageHeader(StorageInfo storage, int y, List lines, int lineIndex, - TabStateManager.TabType tabType, RenderContext ctx) { - // Track this storage for priority field rendering - ctx.visibleStorages.add(new RenderContext.VisibleStorageEntry(storage, y)); - - // Draw expand/collapse indicator - boolean isExpanded = TabStateManager.getInstance().isExpanded(tabType, storage.getId()); - String expandIcon = isExpanded ? "[-]" : "[+]"; - fontRenderer.drawString(expandIcon, 167, y + 1, 0x606060); - - // Draw vertical tree line connecting to cells below (only if expanded) - boolean hasCellsFollowing = isExpanded - && lineIndex + 1 < lines.size() - && (lines.get(lineIndex + 1) instanceof CellContentRow - || lines.get(lineIndex + 1) instanceof EmptySlotInfo); - - if (hasCellsFollowing) { - int lineX = GuiConstants.GUI_INDENT + 7; - treeRenderer.drawStorageConnector(lineX, y); - } - - // Draw block icon - slotRenderer.renderItemStack(storage.getBlockItem(), GuiConstants.GUI_INDENT, y); - - // Draw name (truncated if needed) - String name = storage.getName(); - if (name.length() > 20) name = name.substring(0, 18) + "..."; - int nameColor = storage.hasCustomName() ? 0xFF2E7D32 : GuiConstants.COLOR_TEXT_NORMAL; - fontRenderer.drawString(name, GuiConstants.GUI_INDENT + 20, y + 1, nameColor); - - // Draw location - String location = storage.getLocationString(); - fontRenderer.drawString(location, GuiConstants.GUI_INDENT + 20, y + 9, GuiConstants.COLOR_TEXT_SECONDARY); - } - - // ======================================== - // INVENTORY LINE RENDERING - // ======================================== - - /** - * Draw a cell inventory line (cell slot + content items). - * - * @param cell The cell info - * @param startIndex Starting content index for this row - * @param isFirstRow Whether this is the first row for this cell - * @param y Y position - * @param mouseX Relative mouse X - * @param mouseY Relative mouse Y - * @param absMouseX Absolute mouse X (for tooltip positioning) - * @param absMouseY Absolute mouse Y (for tooltip positioning) - * @param isFirstInGroup Whether this is the first cell in the storage group - * @param isLastInGroup Whether this is the last cell in the storage group - * @param visibleTop Top of visible area - * @param visibleBottom Bottom of visible area - * @param isFirstVisibleRow Whether this is the first visible row - * @param isLastVisibleRow Whether this is the last visible row - * @param hasContentAbove Whether there's content above (scrolled out) - * @param hasContentBelow Whether there's content below (scrolled out) - * @param storageMap Map of storage IDs to StorageInfo - * @param ctx Render context for hover tracking - */ - public void drawCellInventoryLine(CellInfo cell, int startIndex, boolean isFirstRow, - int y, int mouseX, int mouseY, int absMouseX, int absMouseY, - boolean isFirstInGroup, boolean isLastInGroup, - int visibleTop, int visibleBottom, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, - Map storageMap, RenderContext ctx) { - - int lineX = GuiConstants.GUI_INDENT + 7; - - if (isFirstRow) { - drawFirstInventoryRow(cell, y, mouseX, mouseY, absMouseX, absMouseY, - lineX, isFirstInGroup, isLastInGroup, visibleTop, visibleBottom, - isFirstVisibleRow, isLastVisibleRow, hasContentAbove, hasContentBelow, - storageMap, ctx); - // Continuation rows: only draw tree lines if not last in group - } else if (!isLastInGroup) { - treeRenderer.drawTreeLines(lineX, y, false, isFirstInGroup, false, - visibleTop, visibleBottom, isFirstVisibleRow, isLastVisibleRow, - hasContentAbove, hasContentBelow, false); - } - - // Draw content item slots - drawContentSlots(cell, startIndex, y, mouseX, mouseY, absMouseX, absMouseY, ctx); - } - - private void drawFirstInventoryRow(CellInfo cell, int y, int mouseX, int mouseY, - int absMouseX, int absMouseY, int lineX, - boolean isFirstInGroup, boolean isLastInGroup, - int visibleTop, int visibleBottom, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, - Map storageMap, RenderContext ctx) { - - // Draw tree lines - treeRenderer.drawTreeLines(lineX, y, true, isFirstInGroup, isLastInGroup, - visibleTop, visibleBottom, isFirstVisibleRow, isLastVisibleRow, - hasContentAbove, hasContentBelow, false); - - // Draw partition-all button - drawPartitionAllButton(cell, lineX, y, mouseX, mouseY, ctx); - - // Draw upgrade icons with tracking for tooltips and extraction - slotRenderer.drawCellUpgradeIcons(cell, 3, y, ctx, ctx.guiLeft, ctx.guiTop); - - // Draw cell slot - drawCellSlot(cell, y, mouseX, mouseY, absMouseX, absMouseY, storageMap, ctx); - } - - private void drawPartitionAllButton(CellInfo cell, int lineX, int y, int mouseX, int mouseY, RenderContext ctx) { - int buttonX = lineX - 5; - int buttonY = y + 4; - int buttonSize = GuiConstants.SMALL_BUTTON_SIZE; - - boolean hovered = mouseX >= buttonX && mouseX < buttonX + buttonSize - && mouseY >= buttonY && mouseY < buttonY + buttonSize; - - // Draw background to cover tree line - Gui.drawRect(buttonX - 1, buttonY - 1, buttonX + buttonSize + 1, buttonY + buttonSize + 1, GuiConstants.COLOR_SLOT_BACKGROUND); - - // Draw button with green fill - slotRenderer.drawSmallButton(buttonX, buttonY, hovered, GuiConstants.COLOR_BUTTON_GREEN); - - if (hovered) ctx.hoveredPartitionAllButtonCell = cell; - } - - private void drawCellSlot(CellInfo cell, int y, int mouseX, int mouseY, - int absMouseX, int absMouseY, Map storageMap, - RenderContext ctx) { - - int cellX = GuiConstants.CELL_INDENT; - slotRenderer.drawSlotBackground(cellX, y); - - boolean cellHovered = mouseX >= cellX && mouseX < cellX + GuiConstants.MINI_SLOT_SIZE - && mouseY >= y && mouseY < y + GuiConstants.MINI_SLOT_SIZE; - if (cellHovered) { - slotRenderer.drawSlotHoverHighlight(cellX, y); - - StorageInfo storage = storageMap.get(cell.getParentStorageId()); - if (storage != null) { - ctx.hoveredCellStorage = storage; - ctx.hoveredCellCell = cell; - ctx.hoveredCellSlotIndex = cell.getSlot(); - ctx.hoveredContentStack = cell.getCellItem(); - ctx.hoveredContentX = absMouseX; - ctx.hoveredContentY = absMouseY; - } - } - - slotRenderer.renderItemStack(cell.getCellItem(), cellX, y); - } - - private void drawContentSlots(CellInfo cell, int startIndex, int y, - int mouseX, int mouseY, int absMouseX, int absMouseY, - RenderContext ctx) { - - List contents = cell.getContents(); - List partition = cell.getPartition(); - int slotStartX = GuiConstants.CELL_INDENT + 20; - - for (int i = 0; i < GuiConstants.CELL_SLOTS_PER_ROW; i++) { - int contentIndex = startIndex + i; - int slotX = slotStartX + (i * GuiConstants.MINI_SLOT_SIZE); - - slotRenderer.drawSlotBackground(slotX, y); - - if (contentIndex >= contents.size()) continue; - - ItemStack stack = contents.get(contentIndex); - if (stack.isEmpty()) continue; - - slotRenderer.renderItemStack(stack, slotX, y); - - // Draw partition indicator - if (slotRenderer.isInPartition(stack, partition)) slotRenderer.drawPartitionIndicator(slotX, y); - - // Draw item count - slotRenderer.drawItemCount(cell.getContentCount(contentIndex), slotX, y); - - // Check hover - if (mouseX >= slotX && mouseX < slotX + GuiConstants.MINI_SLOT_SIZE - && mouseY >= y && mouseY < y + GuiConstants.MINI_SLOT_SIZE) { - slotRenderer.drawSlotHoverHighlight(slotX, y); - ctx.hoveredContentStack = stack; - ctx.hoveredContentX = absMouseX; - ctx.hoveredContentY = absMouseY; - ctx.hoveredContentSlotIndex = contentIndex; - ctx.hoveredCellCell = cell; - } - } - } - - // ======================================== - // PARTITION LINE RENDERING - // ======================================== - - /** - * Draw a cell partition line (cell slot + partition slots). - * - * @param cell The cell info - * @param startIndex Starting partition index for this row - * @param isFirstRow Whether this is the first row for this cell - * @param y Y position - * @param mouseX Relative mouse X - * @param mouseY Relative mouse Y - * @param absMouseX Absolute mouse X - * @param absMouseY Absolute mouse Y - * @param isFirstInGroup Whether first in storage group - * @param isLastInGroup Whether last in storage group - * @param visibleTop Top of visible area - * @param visibleBottom Bottom of visible area - * @param isFirstVisibleRow Whether first visible row - * @param isLastVisibleRow Whether last visible row - * @param hasContentAbove Whether content above - * @param hasContentBelow Whether content below - * @param storageMap Storage map - * @param guiLeft GUI left offset (for JEI targets) - * @param guiTop GUI top offset (for JEI targets) - * @param ctx Render context - */ - public void drawCellPartitionLine(CellInfo cell, int startIndex, boolean isFirstRow, - int y, int mouseX, int mouseY, int absMouseX, int absMouseY, - boolean isFirstInGroup, boolean isLastInGroup, - int visibleTop, int visibleBottom, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, - Map storageMap, - int guiLeft, int guiTop, RenderContext ctx) { - - int lineX = GuiConstants.GUI_INDENT + 7; - - if (isFirstRow) { - drawFirstPartitionRow(cell, y, mouseX, mouseY, absMouseX, absMouseY, - lineX, isFirstInGroup, isLastInGroup, visibleTop, visibleBottom, - isFirstVisibleRow, isLastVisibleRow, hasContentAbove, hasContentBelow, - storageMap, ctx); - } else { - if (!isLastInGroup) { - treeRenderer.drawTreeLines(lineX, y, false, isFirstInGroup, false, - visibleTop, visibleBottom, isFirstVisibleRow, isLastVisibleRow, - hasContentAbove, hasContentBelow, false); - } - } - - // Draw partition slots - drawPartitionSlots(cell, startIndex, y, mouseX, mouseY, absMouseX, absMouseY, guiLeft, guiTop, ctx); - } - - private void drawFirstPartitionRow(CellInfo cell, int y, int mouseX, int mouseY, - int absMouseX, int absMouseY, int lineX, - boolean isFirstInGroup, boolean isLastInGroup, - int visibleTop, int visibleBottom, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, - Map storageMap, RenderContext ctx) { - - treeRenderer.drawTreeLines(lineX, y, true, isFirstInGroup, isLastInGroup, - visibleTop, visibleBottom, isFirstVisibleRow, isLastVisibleRow, - hasContentAbove, hasContentBelow, false); - - // Draw clear partition button (red) - drawClearPartitionButton(cell, lineX, y, mouseX, mouseY, ctx); - - // Draw upgrade icons with tracking for tooltips and extraction - slotRenderer.drawCellUpgradeIcons(cell, 3, y, ctx, ctx.guiLeft, ctx.guiTop); - - // Draw cell slot - drawCellSlot(cell, y, mouseX, mouseY, absMouseX, absMouseY, storageMap, ctx); - } - - private void drawClearPartitionButton(CellInfo cell, int lineX, int y, int mouseX, int mouseY, RenderContext ctx) { - int buttonX = lineX - 5; - int buttonY = y + 4; - int buttonSize = GuiConstants.SMALL_BUTTON_SIZE; - - boolean hovered = mouseX >= buttonX && mouseX < buttonX + buttonSize - && mouseY >= buttonY && mouseY < buttonY + buttonSize; - - // Draw background to cover tree line - Gui.drawRect(buttonX - 1, buttonY - 1, buttonX + buttonSize + 1, buttonY + buttonSize + 1, GuiConstants.COLOR_SLOT_BACKGROUND); - - // Draw button with red fill - slotRenderer.drawSmallButton(buttonX, buttonY, hovered, GuiConstants.COLOR_BUTTON_RED); - - if (hovered) ctx.hoveredClearPartitionButtonCell = cell; - } - - private void drawPartitionSlots(CellInfo cell, int startIndex, int y, - int mouseX, int mouseY, int absMouseX, int absMouseY, - int guiLeft, int guiTop, RenderContext ctx) { - - List partition = cell.getPartition(); - int slotStartX = GuiConstants.CELL_INDENT + 20; - - for (int i = 0; i < GuiConstants.CELL_SLOTS_PER_ROW; i++) { - int partitionIndex = startIndex + i; - if (partitionIndex >= GuiConstants.MAX_CELL_PARTITION_SLOTS) break; - - int slotX = slotStartX + (i * GuiConstants.MINI_SLOT_SIZE); - - // Draw partition slot with amber tint - slotRenderer.drawPartitionSlotBackground(slotX, y); - - // Register JEI ghost target - ctx.partitionSlotTargets.add(new RenderContext.PartitionSlotTarget( - cell, partitionIndex, guiLeft + slotX, guiTop + y, - GuiConstants.MINI_SLOT_SIZE, GuiConstants.MINI_SLOT_SIZE)); - - // Draw partition item if present - ItemStack partItem = partitionIndex < partition.size() ? partition.get(partitionIndex) : ItemStack.EMPTY; - if (!partItem.isEmpty()) slotRenderer.renderItemStack(partItem, slotX, y); - - // Check hover - if (mouseX >= slotX && mouseX < slotX + GuiConstants.MINI_SLOT_SIZE - && mouseY >= y && mouseY < y + GuiConstants.MINI_SLOT_SIZE) { - slotRenderer.drawSlotHoverHighlight(slotX, y); - ctx.hoveredPartitionSlotIndex = partitionIndex; - ctx.hoveredPartitionCell = cell; - - if (!partItem.isEmpty()) { - ctx.hoveredContentStack = partItem; - ctx.hoveredContentX = absMouseX; - ctx.hoveredContentY = absMouseY; - } - } - } - } - - // ======================================== - // EMPTY SLOT RENDERING - // ======================================== - - /** - * Draw an empty cell slot line. - */ - public void drawEmptySlotLine(EmptySlotInfo emptySlot, int y, int mouseX, int mouseY, - boolean isFirstInGroup, boolean isLastInGroup, - int visibleTop, int visibleBottom, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, - Map storageMap, RenderContext ctx) { - - int lineX = GuiConstants.GUI_INDENT + 7; - treeRenderer.drawTreeLines(lineX, y, true, isFirstInGroup, isLastInGroup, - visibleTop, visibleBottom, isFirstVisibleRow, isLastVisibleRow, - hasContentAbove, hasContentBelow, false); - - int slotX = GuiConstants.CELL_INDENT; - slotRenderer.drawSlotBackground(slotX, y); - - boolean slotHovered = mouseX >= slotX && mouseX < slotX + GuiConstants.MINI_SLOT_SIZE - && mouseY >= y && mouseY < y + GuiConstants.MINI_SLOT_SIZE; - if (slotHovered) { - slotRenderer.drawSlotHoverHighlight(slotX, y); - - StorageInfo storage = storageMap.get(emptySlot.getParentStorageId()); - if (storage != null) { - ctx.hoveredCellStorage = storage; - ctx.hoveredCellSlotIndex = emptySlot.getSlot(); - } - } - } -} diff --git a/src/main/java/com/cellterminal/gui/cells/CellSlotRenderer.java b/src/main/java/com/cellterminal/gui/cells/CellSlotRenderer.java deleted file mode 100644 index 25ba837..0000000 --- a/src/main/java/com/cellterminal/gui/cells/CellSlotRenderer.java +++ /dev/null @@ -1,256 +0,0 @@ -package com.cellterminal.gui.cells; - -import java.util.List; - -import org.lwjgl.opengl.GL11; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.client.renderer.RenderItem; -import net.minecraft.item.ItemStack; - -import appeng.util.ReadableNumberConverter; - -import com.cellterminal.client.CellInfo; -import com.cellterminal.gui.ComparisonUtils; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.render.RenderContext; - - -/** - * Handles rendering of cell slots, content items, and partition slots. - *

- * This class provides low-level drawing operations for cell-related GUI elements. - * It does not handle hover detection or click events - those are handled by - * the tab renderers and click handlers. - */ -public class CellSlotRenderer { - - private final FontRenderer fontRenderer; - private final RenderItem itemRender; - - public CellSlotRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.fontRenderer = fontRenderer; - this.itemRender = itemRender; - } - - /** - * Draw a standard slot background with 3D borders. - */ - public void drawSlotBackground(int x, int y) { - drawSlotBackground(x, y, GuiConstants.MINI_SLOT_SIZE, GuiConstants.MINI_SLOT_SIZE); - } - - /** - * Draw a slot background with custom dimensions. - */ - public void drawSlotBackground(int x, int y, int width, int height) { - Gui.drawRect(x, y, x + width, y + height, GuiConstants.COLOR_SLOT_BACKGROUND); - Gui.drawRect(x, y, x + width - 1, y + 1, GuiConstants.COLOR_SLOT_BORDER_DARK); - Gui.drawRect(x, y, x + 1, y + height - 1, GuiConstants.COLOR_SLOT_BORDER_DARK); - Gui.drawRect(x + 1, y + height - 1, x + width, y + height, GuiConstants.COLOR_SLOT_BORDER_LIGHT); - Gui.drawRect(x + width - 1, y + 1, x + width, y + height - 1, GuiConstants.COLOR_SLOT_BORDER_LIGHT); - } - - /** - * Draw a partition slot background with amber tint. - */ - public void drawPartitionSlotBackground(int x, int y) { - drawSlotBackground(x, y); - int inner = GuiConstants.MINI_SLOT_SIZE - 1; - Gui.drawRect(x + 1, y + 1, x + inner, y + inner, GuiConstants.COLOR_PARTITION_SLOT_TINT); - } - - /** - * Draw a hover highlight over a slot. - */ - public void drawSlotHoverHighlight(int x, int y) { - int inner = GuiConstants.MINI_SLOT_SIZE - 1; - Gui.drawRect(x + 1, y + 1, x + inner, y + inner, GuiConstants.COLOR_HOVER_HIGHLIGHT); - } - - /** - * Render an item stack at the given position. - */ - public void renderItemStack(ItemStack stack, int x, int y) { - if (stack.isEmpty()) return; - - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - RenderHelper.enableGUIStandardItemLighting(); - itemRender.renderItemIntoGUI(stack, x, y); - RenderHelper.disableStandardItemLighting(); - GlStateManager.disableLighting(); - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - GlStateManager.enableBlend(); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - } - - /** - * Render a small (8x8) item icon at the given position. - */ - public void renderSmallItemStack(ItemStack stack, int x, int y) { - if (stack.isEmpty()) return; - - GlStateManager.pushMatrix(); - GlStateManager.translate(x, y, 0); - GlStateManager.scale(0.5f, 0.5f, 1.0f); - - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - RenderHelper.enableGUIStandardItemLighting(); - itemRender.renderItemIntoGUI(stack, 0, 0); - RenderHelper.disableStandardItemLighting(); - GlStateManager.disableLighting(); - - GlStateManager.popMatrix(); - - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - GlStateManager.enableBlend(); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - } - - /** - * Draw item count in the slot (bottom-right, small text). - */ - public void drawItemCount(long count, int slotX, int slotY) { - String countStr = formatItemCount(count); - if (countStr.isEmpty()) return; - - int countWidth = fontRenderer.getStringWidth(countStr); - int textX = slotX + GuiConstants.MINI_SLOT_SIZE - 1; - int textY = slotY + GuiConstants.MINI_SLOT_SIZE - 5; - - GlStateManager.disableDepth(); - GlStateManager.pushMatrix(); - GlStateManager.scale(0.5f, 0.5f, 0.5f); - fontRenderer.drawStringWithShadow(countStr, textX * 2 - countWidth, textY * 2, 0xFFFFFF); - GlStateManager.popMatrix(); - GlStateManager.enableDepth(); - } - - /** - * Draw partition indicator ("P") in the top-left corner. - */ - public void drawPartitionIndicator(int slotX, int slotY) { - GlStateManager.disableLighting(); - GlStateManager.disableDepth(); - GlStateManager.pushMatrix(); - GlStateManager.scale(0.5f, 0.5f, 0.5f); - fontRenderer.drawStringWithShadow("P", (slotX + 1) * 2, (slotY + 1) * 2, GuiConstants.COLOR_PARTITION_INDICATOR); - GlStateManager.popMatrix(); - GlStateManager.enableDepth(); - } - - /** - * Draw cell upgrade icons to the left of the cell slot. - * - * @param cell The cell info - * @param x The x position to start drawing - * @param y The y position of the row - * @return The width consumed by upgrade icons - */ - public int drawCellUpgradeIcons(CellInfo cell, int x, int y) { - return drawCellUpgradeIcons(cell, x, y, null, 0, 0); - } - - /** - * Draw cell upgrade icons to the left of the cell slot, with hover tracking. - * - * @param cell The cell info - * @param x The x position to start drawing (relative to GUI) - * @param y The y position of the row (relative to GUI) - * @param ctx Optional render context for tracking upgrade icon positions - * @param guiLeft GUI left offset for absolute position calculation - * @param guiTop GUI top offset for absolute position calculation - * @return The width consumed by upgrade icons - */ - public int drawCellUpgradeIcons(CellInfo cell, int x, int y, RenderContext ctx, int guiLeft, int guiTop) { - List upgrades = cell.getUpgrades(); - if (upgrades.isEmpty()) return 0; - - // Find max slot index to determine layout width - int maxSlot = 0; - for (int i = 0; i < upgrades.size(); i++) { - int slotIndex = cell.getUpgradeSlotIndex(i); - if (slotIndex > maxSlot) maxSlot = slotIndex; - } - - // Render each upgrade at its actual slot position - for (int i = 0; i < upgrades.size(); i++) { - ItemStack upgrade = upgrades.get(i); - int actualSlotIndex = cell.getUpgradeSlotIndex(i); - int iconX = x + actualSlotIndex * 9; // 8px icon + 1px spacing per slot - - renderSmallItemStack(upgrade, iconX, y); - - // Track upgrade icon position for tooltip and click handling - if (ctx != null) { - ctx.upgradeIconTargets.add(new RenderContext.UpgradeIconTarget( - cell, upgrade, actualSlotIndex, guiLeft + iconX, guiTop + y)); - } - } - - return (maxSlot + 1) * 9; // Total width based on max slot position - } - - /** - * Draw a button with 3D effect. - */ - public void drawButton(int x, int y, int size, String label, boolean hovered) { - int btnColor = hovered ? GuiConstants.COLOR_BUTTON_HOVER : GuiConstants.COLOR_BUTTON_NORMAL; - Gui.drawRect(x, y, x + size, y + size, btnColor); - Gui.drawRect(x, y, x + size, y + 1, GuiConstants.COLOR_BUTTON_HIGHLIGHT); - Gui.drawRect(x, y, x + 1, y + size, GuiConstants.COLOR_BUTTON_HIGHLIGHT); - Gui.drawRect(x, y + size - 1, x + size, y + size, GuiConstants.COLOR_BUTTON_SHADOW); - Gui.drawRect(x + size - 1, y, x + size, y + size, GuiConstants.COLOR_BUTTON_SHADOW); - fontRenderer.drawString(label, x + 4, y + 3, GuiConstants.COLOR_TEXT_NORMAL); - } - - /** - * Draw a small action button (partition-all, clear, etc.). - */ - public void drawSmallButton(int x, int y, boolean hovered, int fillColor) { - int size = GuiConstants.SMALL_BUTTON_SIZE; - int btnColor = hovered ? GuiConstants.COLOR_BUTTON_HOVER : GuiConstants.COLOR_BUTTON_NORMAL; - - // Button background - Gui.drawRect(x, y, x + size, y + size, btnColor); - - // 3D borders - Gui.drawRect(x, y, x + size, y + 1, GuiConstants.COLOR_BUTTON_HIGHLIGHT); - Gui.drawRect(x, y, x + 1, y + size, GuiConstants.COLOR_BUTTON_HIGHLIGHT); - Gui.drawRect(x, y + size - 1, x + size, y + size, GuiConstants.COLOR_BUTTON_SHADOW); - Gui.drawRect(x + size - 1, y, x + size, y + size, GuiConstants.COLOR_BUTTON_SHADOW); - - // Colored fill - if (fillColor != 0) Gui.drawRect(x + 1, y + 1, x + size - 1, y + size - 1, fillColor); - } - - /** - * Format item count for display. - */ - public String formatItemCount(long count) { - if (count < 1000) return String.valueOf(count); - - return ReadableNumberConverter.INSTANCE.toWideReadableForm(count); - } - - /** - * Check if an item is in the partition list. - * Uses fluid-aware comparison for fluid items (compares by fluid type only). - */ - public boolean isInPartition(ItemStack stack, List partition) { - return ComparisonUtils.isInPartition(stack, partition); - } - - /** - * Get the usage bar color based on percentage. - */ - public int getUsageColor(float percent) { - if (percent > 0.9f) return GuiConstants.COLOR_USAGE_HIGH; - if (percent > 0.75f) return GuiConstants.COLOR_USAGE_MEDIUM; - - return GuiConstants.COLOR_USAGE_LOW; - } -} diff --git a/src/main/java/com/cellterminal/gui/cells/CellTreeRenderer.java b/src/main/java/com/cellterminal/gui/cells/CellTreeRenderer.java deleted file mode 100644 index 6dd9356..0000000 --- a/src/main/java/com/cellterminal/gui/cells/CellTreeRenderer.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.cellterminal.gui.cells; - -import net.minecraft.client.gui.Gui; - -import com.cellterminal.gui.GuiConstants; - - -/** - * Handles rendering of tree lines connecting cells to their storage parent. - *

- * Tree lines visually group cells under their parent storage (Drive/Chest) - * and help users understand the hierarchy. - */ -public class CellTreeRenderer { - - /** - * Draw tree lines connecting cells to their storage parent. - * - * @param lineX X position of the vertical line - * @param y Y position of this row - * @param isFirstRow Whether this is the first row for this cell - * @param isFirstInGroup Whether this is the first cell in the storage group - * @param isLastInGroup Whether this is the last cell in the storage group - * @param visibleTop Top Y of the visible area - * @param visibleBottom Bottom Y of the visible area - * @param isFirstVisibleRow Whether this is the first visible row - * @param isLastVisibleRow Whether this is the last visible row - * @param hasContentAbove Whether there's content above that's scrolled out - * @param hasContentBelow Whether there's content below that's scrolled out - * @param allBranches Whether to draw a horizontal branch for every row (not just first) - */ - public void drawTreeLines(int lineX, int y, boolean isFirstRow, boolean isFirstInGroup, - boolean isLastInGroup, int visibleTop, int visibleBottom, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, boolean allBranches) { - - int lineTop = calculateLineTop(y, isFirstRow, isFirstInGroup, isFirstVisibleRow, - hasContentAbove, visibleTop); - int lineBottom = calculateLineBottom(y, isLastInGroup, isLastVisibleRow, - hasContentBelow, visibleBottom); - - // Clamp lineTop to never go above visibleTop to prevent leak above GUI - if (lineTop < visibleTop) lineTop = visibleTop; - - // Vertical line - Gui.drawRect(lineX, lineTop, lineX + 1, lineBottom, GuiConstants.COLOR_TREE_LINE); - - // Horizontal branch (only on first row unless all branches are enabled) - if (allBranches || isFirstRow) Gui.drawRect(lineX, y + 8, lineX + 10, y + 9, GuiConstants.COLOR_TREE_LINE); - } - - /** - * Draw a vertical connector line from storage header to first cell. - */ - public void drawStorageConnector(int lineX, int storageY) { - Gui.drawRect(lineX, storageY + GuiConstants.ROW_HEIGHT - 1, - lineX + 1, storageY + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_TREE_LINE); - } - - private int calculateLineTop(int y, boolean isFirstRow, boolean isFirstInGroup, - boolean isFirstVisibleRow, boolean hasContentAbove, int visibleTop) { - if (isFirstRow) { - // First row in group (right after header) - extend up to connect with header's segment - if (isFirstInGroup) return y - 3; - - // First visible row with content above scrolled out - if (isFirstVisibleRow && hasContentAbove) return visibleTop; - - // Connect to row above but don't extend too high - return y - 4; - } - - // Continuation row - return isFirstVisibleRow && hasContentAbove ? visibleTop : y - 4; - } - - private int calculateLineBottom(int y, boolean isLastInGroup, boolean isLastVisibleRow, - boolean hasContentBelow, int visibleBottom) { - if (isLastInGroup) return y + 9; - if (isLastVisibleRow && hasContentBelow) return visibleBottom; - - return y + GuiConstants.ROW_HEIGHT; - } -} diff --git a/src/main/java/com/cellterminal/gui/cells/package-info.java b/src/main/java/com/cellterminal/gui/cells/package-info.java deleted file mode 100644 index 5e5a833..0000000 --- a/src/main/java/com/cellterminal/gui/cells/package-info.java +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Cell Terminal - Cell Rendering Module - *

- * This package contains all cell-specific GUI rendering and interaction logic. - * It handles the display and behavior of ME storage cells (items, fluids, essentia) - * in the Inventory and Partition tabs. - *

- * Key Classes: - * - {@link com.cellterminal.gui.cells.CellRenderer} - Core rendering for cell slots and contents - * - {@link com.cellterminal.gui.cells.CellHoverState} - Hover tracking for cells - * - {@link com.cellterminal.gui.cells.CellSlotRenderer} - Slot background and item rendering - * - {@link com.cellterminal.gui.cells.CellTreeRenderer} - Tree line rendering connecting cells to storage - *

- * This module should NOT contain any storage bus-related logic. - * Storage bus rendering is handled in {@link com.cellterminal.gui.storagebus}. - * - * @see com.cellterminal.gui.render.InventoryTabRenderer - * @see com.cellterminal.gui.render.PartitionTabRenderer - */ -package com.cellterminal.gui.cells; diff --git a/src/main/java/com/cellterminal/gui/handler/JeiGhostHandler.java b/src/main/java/com/cellterminal/gui/handler/JeiGhostHandler.java index f48d094..be2d511 100644 --- a/src/main/java/com/cellterminal/gui/handler/JeiGhostHandler.java +++ b/src/main/java/com/cellterminal/gui/handler/JeiGhostHandler.java @@ -1,9 +1,5 @@ package com.cellterminal.gui.handler; -import java.awt.Rectangle; -import java.util.ArrayList; -import java.util.List; - import net.minecraft.enchantment.EnchantmentData; import net.minecraft.item.ItemEnchantedBook; import net.minecraft.item.ItemStack; @@ -15,14 +11,8 @@ import appeng.api.storage.channels.IFluidStorageChannel; import appeng.api.storage.data.IAEFluidStack; -import mezz.jei.api.gui.IGhostIngredientHandler; - import com.cellterminal.CellTerminal; -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.StorageBusInfo; import com.cellterminal.gui.overlay.MessageHelper; -import com.cellterminal.gui.PopupCellPartition; -import com.cellterminal.gui.GuiConstants; import com.cellterminal.integration.ThaumicEnergisticsIntegration; @@ -31,84 +21,6 @@ */ public class JeiGhostHandler { - /** - * Target for a partition slot that can receive JEI ghost ingredients. - */ - public static class PartitionSlotTarget { - public final CellInfo cell; - public final int slotIndex; - public final int x; - public final int y; - public final int width; - public final int height; - - public PartitionSlotTarget(CellInfo cell, int slotIndex, int x, int y, int width, int height) { - this.cell = cell; - this.slotIndex = slotIndex; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - } - - public interface PartitionCallback { - void onAddPartitionItem(CellInfo cell, int slotIndex, ItemStack stack); - } - - /** - * Target for a storage bus partition slot that can receive JEI ghost ingredients. - */ - public static class StorageBusPartitionSlotTarget { - public final StorageBusInfo storageBus; - public final int slotIndex; - public final int x; - public final int y; - public final int width; - public final int height; - - public StorageBusPartitionSlotTarget(StorageBusInfo storageBus, int slotIndex, int x, int y, int width, int height) { - this.storageBus = storageBus; - this.slotIndex = slotIndex; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - } - - public interface StorageBusPartitionCallback { - void onAddStorageBusPartitionItem(StorageBusInfo storageBus, int slotIndex, ItemStack stack); - } - - /** - * Target for a temp cell partition slot that can receive JEI ghost ingredients. - * Similar to PartitionSlotTarget but uses temp slot index instead of parent storage ID. - */ - public static class TempCellPartitionSlotTarget { - public final CellInfo cell; - public final int tempSlotIndex; - public final int partitionSlotIndex; - public final int x; - public final int y; - public final int width; - public final int height; - - public TempCellPartitionSlotTarget(CellInfo cell, int tempSlotIndex, int partitionSlotIndex, int x, int y, int width, int height) { - this.cell = cell; - this.tempSlotIndex = tempSlotIndex; - this.partitionSlotIndex = partitionSlotIndex; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - } - - public interface TempCellPartitionCallback { - void onAddTempCellPartitionItem(int tempSlotIndex, int partitionSlotIndex, ItemStack stack); - } - /** * Convert any JEI ingredient to an ItemStack for use with AE2 cells. */ @@ -258,84 +170,4 @@ public static ItemStack convertJeiIngredientForStorageBus(Object ingredient, boo return ItemStack.EMPTY; } - - public static List> getPhantomTargets( - int currentTab, PopupCellPartition partitionPopup, - List partitionSlotTargets, - List storageBusPartitionSlotTargets, - List tempCellPartitionSlotTargets, - PartitionCallback callback, StorageBusPartitionCallback storageBusCallback, - TempCellPartitionCallback tempCellCallback) { - - if (partitionPopup != null) return partitionPopup.getGhostTargets(); - - List> targets = new ArrayList<>(); - - // Cell partition tab - if (currentTab == GuiConstants.TAB_PARTITION) { - for (PartitionSlotTarget slot : partitionSlotTargets) { - targets.add(new IGhostIngredientHandler.Target() { - @Override - public Rectangle getArea() { - return new Rectangle(slot.x, slot.y, slot.width, slot.height); - } - - @Override - public void accept(Object ing) { - ItemStack stack = convertJeiIngredientToItemStack(ing, slot.cell.isFluid(), slot.cell.isEssentia()); - - if (stack.isEmpty()) return; - - callback.onAddPartitionItem(slot.cell, slot.slotIndex, stack); - } - }); - } - } - - // Temp area tab - partition slots - if (currentTab == GuiConstants.TAB_TEMP_AREA) { - for (TempCellPartitionSlotTarget slot : tempCellPartitionSlotTargets) { - targets.add(new IGhostIngredientHandler.Target() { - @Override - public Rectangle getArea() { - return new Rectangle(slot.x, slot.y, slot.width, slot.height); - } - - @Override - public void accept(Object ing) { - ItemStack stack = convertJeiIngredientToItemStack(ing, slot.cell.isFluid(), slot.cell.isEssentia()); - - if (stack.isEmpty()) return; - - tempCellCallback.onAddTempCellPartitionItem(slot.tempSlotIndex, slot.partitionSlotIndex, stack); - } - }); - } - } - - // Storage bus partition tab - if (currentTab == GuiConstants.TAB_STORAGE_BUS_PARTITION) { - for (StorageBusPartitionSlotTarget slot : storageBusPartitionSlotTargets) { - targets.add(new IGhostIngredientHandler.Target() { - @Override - public Rectangle getArea() { - return new Rectangle(slot.x, slot.y, slot.width, slot.height); - } - - @Override - public void accept(Object ing) { - // Use storage bus-specific conversion with correct bus type flags - ItemStack stack = convertJeiIngredientForStorageBus( - ing, slot.storageBus.isFluid(), slot.storageBus.isEssentia()); - - if (stack.isEmpty()) return; - - storageBusCallback.onAddStorageBusPartitionItem(slot.storageBus, slot.slotIndex, stack); - } - }); - } - } - - return targets; - } } diff --git a/src/main/java/com/cellterminal/gui/handler/QuickPartitionHandler.java b/src/main/java/com/cellterminal/gui/handler/QuickPartitionHandler.java index ba6649e..1d938cc 100644 --- a/src/main/java/com/cellterminal/gui/handler/QuickPartitionHandler.java +++ b/src/main/java/com/cellterminal/gui/handler/QuickPartitionHandler.java @@ -328,7 +328,7 @@ private static class CellSearchResult { private static CellSearchResult findFirstCellWithoutPartition( PartitionType type, List partitionLines, - java.util.Map storageMap) { + Map storageMap) { for (int i = 0; i < partitionLines.size(); i++) { Object line = partitionLines.get(i); diff --git a/src/main/java/com/cellterminal/gui/handler/StorageBusClickHandler.java b/src/main/java/com/cellterminal/gui/handler/StorageBusClickHandler.java deleted file mode 100644 index 3c023f2..0000000 --- a/src/main/java/com/cellterminal/gui/handler/StorageBusClickHandler.java +++ /dev/null @@ -1,321 +0,0 @@ -package com.cellterminal.gui.handler; - -import java.util.Map; -import java.util.Set; - -import net.minecraft.client.Minecraft; -import net.minecraft.item.ItemStack; - -import net.minecraftforge.fluids.FluidStack; - -import appeng.fluids.items.FluidDummyItem; - -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.client.TabStateManager; -import com.cellterminal.gui.overlay.MessageHelper; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.integration.ThaumicEnergisticsIntegration; -import com.cellterminal.network.CellTerminalNetwork; -import com.cellterminal.network.PacketHighlightBlock; -import com.cellterminal.network.PacketStorageBusIOMode; -import com.cellterminal.network.PacketStorageBusPartitionAction; - - -/** - * Handles click logic for storage bus tabs (tabs 4 and 5). - */ -public class StorageBusClickHandler { - - // Double-click tracking - private long lastClickedStorageBusId = -1; - private long lastClickTimeStorageBus = 0; - private static final long DOUBLE_CLICK_TIME = 400; - - /** - * Context containing all the hover state needed for handling clicks. - */ - public static class ClickContext { - public int currentTab; - public int relMouseX; // Mouse X position relative to GUI left - public StorageBusInfo hoveredStorageBus; - public int hoveredStorageBusPartitionSlot = -1; - public int hoveredStorageBusContentSlot = -1; - public StorageBusInfo hoveredClearButtonStorageBus; - public StorageBusInfo hoveredIOModeButtonStorageBus; - public StorageBusInfo hoveredPartitionAllButtonStorageBus; - public ItemStack hoveredContentStack = ItemStack.EMPTY; - public Set selectedStorageBusIds; - public Map storageBusMap; - - public static ClickContext from(int currentTab, int relMouseX, StorageBusInfo hoveredStorageBus, - int hoveredPartitionSlot, int hoveredContentSlot, - StorageBusInfo clearButton, StorageBusInfo ioModeButton, StorageBusInfo partitionAllButton, - ItemStack hoveredContentStack, Set selectedStorageBusIds, Map storageBusMap) { - ClickContext ctx = new ClickContext(); - ctx.currentTab = currentTab; - ctx.relMouseX = relMouseX; - ctx.hoveredStorageBus = hoveredStorageBus; - ctx.hoveredStorageBusPartitionSlot = hoveredPartitionSlot; - ctx.hoveredStorageBusContentSlot = hoveredContentSlot; - ctx.hoveredClearButtonStorageBus = clearButton; - ctx.hoveredIOModeButtonStorageBus = ioModeButton; - ctx.hoveredPartitionAllButtonStorageBus = partitionAllButton; - ctx.hoveredContentStack = hoveredContentStack; - ctx.selectedStorageBusIds = selectedStorageBusIds; - ctx.storageBusMap = storageBusMap; - return ctx; - } - } - - /** - * Handle click events on storage bus tabs. - * @return true if the click was handled - */ - public boolean handleClick(ClickContext ctx, int mouseButton) { - long now = System.currentTimeMillis(); - boolean wasDoubleClick = checkDoubleClick(ctx, now, mouseButton); - - if (ctx.currentTab == GuiConstants.TAB_STORAGE_BUS_INVENTORY) { - return handleInventoryTabClick(ctx, mouseButton); - } - - if (ctx.currentTab == GuiConstants.TAB_STORAGE_BUS_PARTITION) { - return handlePartitionTabClick(ctx, mouseButton, wasDoubleClick); - } - - return false; - } - - private boolean checkDoubleClick(ClickContext ctx, long now, int mouseButton) { - StorageBusInfo hoveredBus = ctx.hoveredStorageBus; - if (hoveredBus == null || mouseButton != 0) return false; - - // If a slot is being hovered, don't trigger block highlight (but don't reset tracking) - // This allows the slot click to be handled by other methods - if (ctx.hoveredStorageBusPartitionSlot >= 0 || ctx.hoveredStorageBusContentSlot >= 0) { - return false; - } - - long currentBusId = hoveredBus.getId(); - - if (currentBusId == lastClickedStorageBusId && now - lastClickTimeStorageBus < DOUBLE_CLICK_TIME) { - highlightStorageBus(hoveredBus); - lastClickedStorageBusId = -1; - lastClickTimeStorageBus = now; - - return true; - } - - lastClickedStorageBusId = currentBusId; - lastClickTimeStorageBus = now; - - return false; - } - - private void highlightStorageBus(StorageBusInfo storageBus) { - if (storageBus.getDimension() != Minecraft.getMinecraft().player.dimension) { - MessageHelper.error("cellterminal.error.different_dimension"); - - return; - } - - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketHighlightBlock(storageBus.getPos(), storageBus.getDimension()) - ); - - // Send green chat message with block name and coordinates - MessageHelper.success("gui.cellterminal.highlighted", - storageBus.getPos().getX(), storageBus.getPos().getY(), storageBus.getPos().getZ(), - storageBus.getLocalizedName()); - } - - private boolean handleInventoryTabClick(ClickContext ctx, int mouseButton) { - if (mouseButton != 0) return false; - - // Expand/collapse button click on storage bus header - if (ctx.hoveredStorageBus != null && ctx.relMouseX >= 165 && ctx.relMouseX < 180 - && ctx.hoveredStorageBusContentSlot < 0) { - TabStateManager.getInstance().toggleBusExpanded( - TabStateManager.TabType.STORAGE_BUS_INVENTORY, ctx.hoveredStorageBus.getId()); - - return true; - } - - // IO Mode button - if (ctx.hoveredIOModeButtonStorageBus != null && ctx.hoveredIOModeButtonStorageBus.supportsIOMode()) { - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketStorageBusIOMode(ctx.hoveredIOModeButtonStorageBus.getId()) - ); - - return true; - } - - // Partition All button - if (ctx.hoveredPartitionAllButtonStorageBus != null) { - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketStorageBusPartitionAction( - ctx.hoveredPartitionAllButtonStorageBus.getId(), - PacketStorageBusPartitionAction.Action.SET_ALL_FROM_CONTENTS - ) - ); - - return true; - } - - // Content item click - toggle partition - if (ctx.hoveredStorageBusContentSlot >= 0 && ctx.hoveredStorageBus != null - && !ctx.hoveredContentStack.isEmpty()) { - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketStorageBusPartitionAction( - ctx.hoveredStorageBus.getId(), - PacketStorageBusPartitionAction.Action.TOGGLE_ITEM, - -1, - ctx.hoveredContentStack - ) - ); - - return true; - } - - return false; - } - - private boolean handlePartitionTabClick(ClickContext ctx, int mouseButton, boolean wasDoubleClick) { - if (mouseButton != 0) return false; - - // Expand/collapse button click on storage bus header - if (ctx.hoveredStorageBus != null && ctx.relMouseX >= 165 && ctx.relMouseX < 180 - && ctx.hoveredStorageBusPartitionSlot < 0) { - TabStateManager.getInstance().toggleBusExpanded( - TabStateManager.TabType.STORAGE_BUS_PARTITION, ctx.hoveredStorageBus.getId()); - - return true; - } - - // IO Mode button - if (ctx.hoveredIOModeButtonStorageBus != null && ctx.hoveredIOModeButtonStorageBus.supportsIOMode()) { - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketStorageBusIOMode(ctx.hoveredIOModeButtonStorageBus.getId()) - ); - - return true; - } - - // Clear button - if (ctx.hoveredClearButtonStorageBus != null) { - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketStorageBusPartitionAction( - ctx.hoveredClearButtonStorageBus.getId(), - PacketStorageBusPartitionAction.Action.CLEAR_ALL - ) - ); - - return true; - } - - // Header click - toggle selection (multi-select for keybind actions) - if (ctx.hoveredStorageBus != null && ctx.hoveredStorageBusPartitionSlot < 0 && !wasDoubleClick) { - return handleSelectionToggle(ctx); - } - - // Partition slot click - if (ctx.hoveredStorageBusPartitionSlot >= 0 && ctx.hoveredStorageBus != null) { - return handlePartitionSlotClick(ctx); - } - - return false; - } - - private boolean handleSelectionToggle(ClickContext ctx) { - long busId = ctx.hoveredStorageBus.getId(); - - if (ctx.selectedStorageBusIds.contains(busId)) { - ctx.selectedStorageBusIds.remove(busId); - - return true; - } - - // Validate that new selection is same type as existing selection - if (!ctx.selectedStorageBusIds.isEmpty()) { - StorageBusInfo existingBus = findExistingSelectedBus(ctx); - - if (existingBus != null) { - boolean sameType = (ctx.hoveredStorageBus.isFluid() == existingBus.isFluid()) - && (ctx.hoveredStorageBus.isEssentia() == existingBus.isEssentia()); - - if (!sameType) { - MessageHelper.error("cellterminal.error.mixed_bus_selection"); - - return true; - } - } - } - - ctx.selectedStorageBusIds.add(busId); - - return true; - } - - private StorageBusInfo findExistingSelectedBus(ClickContext ctx) { - for (Long existingId : ctx.selectedStorageBusIds) { - StorageBusInfo bus = ctx.storageBusMap.get(existingId); - if (bus != null) return bus; - } - - return null; - } - - private boolean handlePartitionSlotClick(ClickContext ctx) { - ItemStack heldStack = Minecraft.getMinecraft().player.inventory.getItemStack(); - - if (!heldStack.isEmpty()) { - return handleAddPartitionItem(ctx, heldStack); - } - - // Left click without item: clear slot - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketStorageBusPartitionAction( - ctx.hoveredStorageBus.getId(), - PacketStorageBusPartitionAction.Action.REMOVE_ITEM, - ctx.hoveredStorageBusPartitionSlot, - ItemStack.EMPTY - ) - ); - - return true; - } - - private boolean handleAddPartitionItem(ClickContext ctx, ItemStack heldStack) { - ItemStack stackToSend = heldStack; - - if (ctx.hoveredStorageBus.isFluid()) { - if (!(heldStack.getItem() instanceof FluidDummyItem)) { - FluidStack fluid = net.minecraftforge.fluids.FluidUtil.getFluidContained(heldStack); - if (fluid == null) { - MessageHelper.error("cellterminal.error.fluid_bus_item"); - - return true; - } - } - } else if (ctx.hoveredStorageBus.isEssentia()) { - ItemStack essentiaRep = ThaumicEnergisticsIntegration.tryConvertEssentiaContainerToAspect(heldStack); - if (essentiaRep.isEmpty()) { - MessageHelper.error("cellterminal.error.essentia_bus_item"); - - return true; - } - stackToSend = essentiaRep; - } - - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketStorageBusPartitionAction( - ctx.hoveredStorageBus.getId(), - PacketStorageBusPartitionAction.Action.ADD_ITEM, - ctx.hoveredStorageBusPartitionSlot, - stackToSend - ) - ); - - return true; - } -} diff --git a/src/main/java/com/cellterminal/gui/handler/TabManager.java b/src/main/java/com/cellterminal/gui/handler/TabManager.java new file mode 100644 index 0000000..b602a25 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/handler/TabManager.java @@ -0,0 +1,520 @@ +package com.cellterminal.gui.handler; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.I18n; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.item.ItemStack; + +import appeng.api.AEApi; + +import com.cellterminal.client.SearchFilterMode; +import com.cellterminal.client.TabStateManager; +import com.cellterminal.config.CellTerminalClientConfig; +import com.cellterminal.config.CellTerminalServerConfig; +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.PriorityFieldManager; +import com.cellterminal.gui.rename.InlineRenameManager; +import com.cellterminal.gui.widget.line.SlotsLine; +import com.cellterminal.gui.widget.tab.AbstractTabWidget; +import com.cellterminal.gui.widget.tab.CellContentTabWidget; +import com.cellterminal.gui.widget.tab.GuiContext; +import com.cellterminal.gui.widget.tab.NetworkToolsTabWidget; +import com.cellterminal.gui.widget.tab.StorageBusTabWidget; +import com.cellterminal.gui.widget.tab.SubnetOverviewTabWidget; +import com.cellterminal.gui.widget.tab.TempAreaTabWidget; +import com.cellterminal.gui.widget.tab.TerminalTabWidget; + + +/** + * Manages tab state, rendering, switching, and active widget access. + *

+ * Centralizes all tab-related logic that was previously in GuiCellTerminalBase: + * tab widget lifecycle, tab clicks, tab rendering, search mode delegation, + * and tab tooltips. + *

+ * The parent GUI communicates tab switch side effects via {@link TabSwitchListener}. + */ +public class TabManager { + + /** + * Listener for tab switch events. + * The GUI implements this to handle side effects of tab switching + * (scrollbar updates, filter buttons, search reapplication, etc.). + *

+ * Note: rename cancellation and scroll position save/restore are handled + * internally by TabManager. The listener should NOT duplicate those. + */ + public interface TabSwitchListener { + /** Called before the tab index changes. Exit modes, close popups, etc. */ + void onPreSwitch(int oldTab); + + /** Called after the tab index changes. Update scrollbar, filters, search, etc. */ + void onPostSwitch(int newTab); + } + + /** + * Provides scroll position access for TabManager. + * Decouples TabManager from the GUI's scrollbar implementation. + */ + public interface ScrollAccessor { + /** Get the current scroll position. */ + int getCurrentScroll(); + + /** Scroll to a specific line index. */ + void scrollToLine(int lineIndex); + } + + // ---- State ---- + + private int currentTab; + private int hoveredTab = -1; + private AbstractTabWidget[] tabWidgets; + private final TabSwitchListener listener; + private ScrollAccessor scrollAccessor; + + // Typed widget reference for preview cell access (only TerminalTabWidget exposes getPreviewCell) + private TerminalTabWidget terminalWidget; + + // Subnet overview pseudo-tab (separate from real tabs. No tab button, activated via tab index) + private SubnetOverviewTabWidget subnetTab; + private int previousRealTab = GuiConstants.TAB_TERMINAL; + + // Tab icons for composite rendering (lazy initialized) + private ItemStack tabIconInventory; + private ItemStack tabIconPartition; + private ItemStack tabIconStorageBus; + + // Controls help widget cache (used for both rendering and JEI exclusion area calculation). + // Stored here so the GUI can query the wrapped lines without maintaining its own tab-tracking logic. + private List cachedControlsHelpLines = new ArrayList<>(); + private int cachedControlsHelpTab = Integer.MIN_VALUE; + + /** + * Create a new TabManager with the given initial tab and switch listener. + * + * @param initialTab The initially selected tab index (validated to be in range and enabled) + * @param listener Callback for tab switch events + */ + public TabManager(int initialTab, TabSwitchListener listener) { + this.currentTab = validateTab(initialTab); + this.listener = listener; + } + + /** + * Set the scroll accessor for scroll position save/restore during tab switches. + * Must be called after the scrollbar is available (typically in initGui). + */ + public void setScrollAccessor(ScrollAccessor scrollAccessor) { + this.scrollAccessor = scrollAccessor; + } + + /** + * Validate and clamp a tab index to the valid range. + * Also checks server-side tab enablement and falls back to the first enabled tab. + */ + private int validateTab(int tab) { + if (tab < 0 || tab > GuiConstants.LAST_TAB) tab = GuiConstants.TAB_TERMINAL; + + if (CellTerminalServerConfig.isInitialized() && !CellTerminalServerConfig.getInstance().isTabEnabled(tab)) { + tab = findFirstEnabledTab(); + } + + return tab; + } + + /** + * Find the first enabled tab, or fall back to TAB_TERMINAL. + */ + private int findFirstEnabledTab() { + if (!CellTerminalServerConfig.isInitialized()) return GuiConstants.TAB_TERMINAL; + + CellTerminalServerConfig config = CellTerminalServerConfig.getInstance(); + + for (int i = 0; i <= GuiConstants.LAST_TAB; i++) { + if (config.isTabEnabled(i)) return i; + } + + // Fallback to terminal tab even if disabled (should not happen in practice) + return GuiConstants.TAB_TERMINAL; + } + + // ======================================================================== + // Widget lifecycle + // ======================================================================== + + /** + * Create and initialize all tab widgets. + * Called during {@code initGui()} when the GUI is (re)initialized. + * + * @param fontRenderer Font renderer for text drawing + * @param itemRender Item renderer for icon drawing + * @param guiLeft GUI left edge absolute X + * @param guiTop GUI top edge absolute Y + * @param rowsVisible Number of visible rows in the scroll area + * @param context GUI context for widget callbacks + */ + public void initWidgets(FontRenderer fontRenderer, RenderItem itemRender, + int guiLeft, int guiTop, int rowsVisible, + GuiContext context) { + // Create tab widgets + this.terminalWidget = new TerminalTabWidget(fontRenderer, itemRender); + CellContentTabWidget inventoryWidget = new CellContentTabWidget(SlotsLine.SlotMode.CONTENT, fontRenderer, itemRender); + CellContentTabWidget partitionWidget = new CellContentTabWidget(SlotsLine.SlotMode.PARTITION, fontRenderer, itemRender); + TempAreaTabWidget tempAreaWidget = new TempAreaTabWidget(fontRenderer, itemRender); + StorageBusTabWidget storageBusInventoryWidget = new StorageBusTabWidget(SlotsLine.SlotMode.CONTENT, fontRenderer, itemRender); + StorageBusTabWidget storageBusPartitionWidget = new StorageBusTabWidget(SlotsLine.SlotMode.PARTITION, fontRenderer, itemRender); + NetworkToolsTabWidget networkToolsWidget = new NetworkToolsTabWidget(fontRenderer, itemRender); + + // Populate indexed lookup + this.tabWidgets = new AbstractTabWidget[] { + terminalWidget, // TAB_TERMINAL (0) + inventoryWidget, // TAB_INVENTORY (1) + partitionWidget, // TAB_PARTITION (2) + tempAreaWidget, // TAB_TEMP_AREA (3) + storageBusInventoryWidget, // TAB_STORAGE_BUS_INVENTORY (4) + storageBusPartitionWidget, // TAB_STORAGE_BUS_PARTITION (5) + networkToolsWidget // TAB_NETWORK_TOOLS (6) + }; + + // Initialize all widgets with shared GUI context, offsets, and row count + for (AbstractTabWidget widget : tabWidgets) { + if (widget == null) continue; + widget.setGuiOffsets(guiLeft, guiTop); + widget.setRowsVisible(rowsVisible); + widget.init(context); + } + + // Create and initialize the subnet overview pseudo-tab + this.subnetTab = new SubnetOverviewTabWidget(fontRenderer, itemRender); + this.subnetTab.setGuiOffsets(guiLeft, guiTop); + this.subnetTab.setRowsVisible(rowsVisible); + this.subnetTab.init(context); + } + + // ======================================================================== + // Getters + // ======================================================================== + + /** Get the index of the currently selected tab. */ + public int getCurrentTab() { + return currentTab; + } + + /** Get the index of the currently hovered tab (-1 if none). */ + public int getHoveredTab() { + return hoveredTab; + } + + /** Get the active tab widget. Returns the subnet tab when currentTab is the subnet overview index. */ + public AbstractTabWidget getActiveTab() { + if (currentTab == TabStateManager.TabType.SUBNET_OVERVIEW.getIndex() && subnetTab != null) return subnetTab; + if (tabWidgets == null || currentTab < 0 || currentTab >= tabWidgets.length) return null; + + return tabWidgets[currentTab]; + } + + /** Get all tab widgets (indexed by tab constants). */ + public AbstractTabWidget[] getWidgets() { + return tabWidgets; + } + + /** Get the terminal tab widget for preview cell access. */ + public TerminalTabWidget getTerminalWidget() { + return terminalWidget; + } + + /** Get the number of tabs. */ + public int getTabCount() { + return tabWidgets != null ? tabWidgets.length : 0; + } + + /** + * Get the data lines for the active tab widget. + * + * @param dataManager The terminal data manager + * @return The active widget's line list, or empty list if no active widget + */ + public List getActiveLines(TerminalDataManager dataManager) { + AbstractTabWidget tab = getActiveTab(); + if (tab == null) return Collections.emptyList(); + + return tab.getLines(dataManager); + } + + /** + * Get the visible item count for the active widget. + * Accounts for non-standard row heights (e.g., NetworkTools at 36px per tool). + */ + public int getActiveVisibleItemCount() { + AbstractTabWidget tab = getActiveTab(); + + return tab != null ? tab.getVisibleItemCount() : GuiConstants.DEFAULT_ROWS; + } + + // ======================================================================== + // Tab switching + // ======================================================================== + + /** + * Handle a click on the tab header area. + * Checks if the click is on a tab and switches to it if valid. + * + * @return true if a tab was clicked (even if disabled), consuming the click + */ + public boolean handleClick(int mouseX, int mouseY, int guiLeft, int guiTop) { + int tabY = guiTop + GuiConstants.TAB_Y_OFFSET; + + // Click is outside tab header vertical bounds + if (mouseY < tabY || mouseY >= tabY + GuiConstants.TAB_HEIGHT) return false; + + for (int i = 0; i <= GuiConstants.LAST_TAB; i++) { + int tabX = guiLeft + 4 + (i * (GuiConstants.TAB_WIDTH + 2)); + + if (mouseX >= tabX && mouseX < tabX + GuiConstants.TAB_WIDTH) { + // Consume click but don't switch to disabled tab + if (CellTerminalServerConfig.isInitialized() + && !CellTerminalServerConfig.getInstance().isTabEnabled(i)) { + return true; + } + + if (currentTab != i) switchToTab(i); + + return true; + } + } + + return false; + } + + /** + * Switch to a different tab, firing pre/post switch callbacks. + * Also persists the selected tab to client config (skipped for subnet pseudo-tab -1). + * Use -1 to switch to the subnet overview pseudo-tab. + * + * @param newTab The tab index to switch to (-1 for subnet overview, 0+ for real tabs) + */ + public void switchToTab(int newTab) { + if (newTab == currentTab) return; + + int oldTab = currentTab; + + // Cancel editing in case the keybind switched subnets overview + InlineRenameManager.getInstance().cancelEditing(); + + // Save any priority field edits + PriorityFieldManager.getInstance().unfocusAll(); + + // Save scroll position for outgoing tab + if (scrollAccessor != null) { + TabStateManager.TabType oldTabType = TabStateManager.TabType.fromIndex(oldTab); + TabStateManager.getInstance().setScrollPosition(oldTabType, scrollAccessor.getCurrentScroll()); + } + + listener.onPreSwitch(oldTab); + + // Remember the last real tab + if (oldTab >= 0) previousRealTab = oldTab; + + currentTab = newTab; + + // Only persist real tab selections + if (newTab >= 0) CellTerminalClientConfig.getInstance().setSelectedTab(newTab); + + // Notify subnet tab it's being shown + if (newTab == TabStateManager.TabType.SUBNET_OVERVIEW.getIndex() && subnetTab != null) { + subnetTab.onEnterOverview(); + } + + listener.onPostSwitch(newTab); + + // Restore scroll position for incoming tab + if (scrollAccessor != null) { + TabStateManager.TabType newTabType = TabStateManager.TabType.fromIndex(newTab); + int savedScroll = TabStateManager.getInstance().getScrollPosition(newTabType); + scrollAccessor.scrollToLine(savedScroll); + } + } + + /** + * Save the current scroll position for the active tab. + * Call when the GUI is closing to persist scroll state. + */ + public void saveCurrentScrollPosition() { + if (scrollAccessor == null) return; + + TabStateManager.TabType tabType = TabStateManager.TabType.fromIndex(currentTab); + TabStateManager.getInstance().setScrollPosition(tabType, scrollAccessor.getCurrentScroll()); + } + + // ======================================================================== + // Subnet overview + // ======================================================================== + + /** Get the subnet overview pseudo-tab widget. */ + public SubnetOverviewTabWidget getSubnetTab() { + return subnetTab; + } + + /** + * Get the last real tab that was active before switching to subnet overview. + * Used by the GUI to return to the previous tab when exiting subnet mode. + */ + public int getPreviousRealTab() { + return previousRealTab; + } + + // ======================================================================== + // Event delegation + // ======================================================================== + + /** + * Delegate a key press to the active tab widget for tab-specific keybinds. + * + * @return true if the key was handled + */ + public boolean handleKey(int keyCode) { + AbstractTabWidget tab = getActiveTab(); + if (tab == null) return false; + + return tab.handleTabKeyTyped(keyCode); + } + + /** + * Get the effective search mode for the active tab. + * Some tabs force a specific mode regardless of user selection. + * + * @param userSelectedMode The mode selected by the user in the search button + * @return The effective mode for the current tab + */ + public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { + AbstractTabWidget tab = getActiveTab(); + if (tab != null) return tab.getEffectiveSearchMode(userSelectedMode); + + return userSelectedMode; + } + + /** + * Whether the search mode button should be visible for the active tab. + * Tabs that force a search mode return false. + */ + public boolean isSearchModeButtonVisible() { + AbstractTabWidget tab = getActiveTab(); + + return tab != null && tab.showSearchModeButton(); + } + + /** + * Get the tooltip for a tab button. + * Includes a disabled notice if the tab is disabled in server config. + * + * @param tab The tab index + * @return The localized tooltip text, or empty string if invalid + */ + public String getTabTooltip(int tab) { + if (tabWidgets == null || tab < 0 || tab >= tabWidgets.length || tabWidgets[tab] == null) return ""; + + String baseTooltip = tabWidgets[tab].getTabTooltip(); + + if (CellTerminalServerConfig.isInitialized() && !CellTerminalServerConfig.getInstance().isTabEnabled(tab)) { + return baseTooltip + " " + I18n.format("gui.cellterminal.tab.disabled"); + } + + return baseTooltip; + } + + // ======================================================================== + // Controls help cache + // ======================================================================== + + /** + * Update the cached controls help wrapped lines. + * Called by the GUI after rendering the controls help widget each frame. + */ + public void setCachedControlsHelp(List wrappedLines, int tabIndex) { + this.cachedControlsHelpLines = wrappedLines; + this.cachedControlsHelpTab = tabIndex; + } + + /** + * Get the cached controls help wrapped lines for JEI exclusion area sizing. + * Returns empty list if the cache is stale (tab changed since last render). + */ + public List getCachedControlsHelpLines() { + if (cachedControlsHelpLines.isEmpty() || cachedControlsHelpTab != currentTab) { + return Collections.emptyList(); + } + + return cachedControlsHelpLines; + } + + // ======================================================================== + // Tab rendering + // ======================================================================== + + /** + * Draw all tab buttons in the background layer. + * Updates {@link #hoveredTab} based on mouse position. + */ + public void drawTabs(int guiLeft, int offsetX, int offsetY, int mouseX, int mouseY, + RenderItem itemRender, Minecraft mc) { + TabRenderingHandler.TabRenderContext ctx = new TabRenderingHandler.TabRenderContext( + guiLeft, offsetX, offsetY, mouseX, mouseY, + GuiConstants.TAB_WIDTH, GuiConstants.TAB_HEIGHT, GuiConstants.TAB_Y_OFFSET, + currentTab, itemRender, mc); + + TabRenderingHandler.TabIconProvider iconProvider = new TabRenderingHandler.TabIconProvider() { + @Override + public ItemStack getTabIcon(int tab) { + return TabManager.this.getTabIconInternal(tab); + } + + @Override + public ItemStack getStorageBusIcon() { + if (tabIconStorageBus == null) { + tabIconStorageBus = AEApi.instance().definitions().parts().storageBus() + .maybeStack(1).orElse(ItemStack.EMPTY); + } + + return tabIconStorageBus; + } + + @Override + public ItemStack getInventoryIcon() { + if (tabIconInventory == null) { + tabIconInventory = AEApi.instance().definitions().blocks().chest() + .maybeStack(1).orElse(ItemStack.EMPTY); + } + + return tabIconInventory; + } + + @Override + public ItemStack getPartitionIcon() { + if (tabIconPartition == null) { + tabIconPartition = AEApi.instance().definitions().blocks().cellWorkbench() + .maybeStack(1).orElse(ItemStack.EMPTY); + } + + return tabIconPartition; + } + }; + + this.hoveredTab = TabRenderingHandler.drawTabs(ctx, iconProvider, getTabCount()).hoveredTab; + } + + /** + * Get the tab icon for a specific tab by delegating to the widget. + */ + private ItemStack getTabIconInternal(int tab) { + if (tabWidgets != null && tab >= 0 && tab < tabWidgets.length && tabWidgets[tab] != null) { + return tabWidgets[tab].getTabIcon(); + } + + return ItemStack.EMPTY; + } +} diff --git a/src/main/java/com/cellterminal/gui/handler/TabRenderingHandler.java b/src/main/java/com/cellterminal/gui/handler/TabRenderingHandler.java index 6e04713..951018b 100644 --- a/src/main/java/com/cellterminal/gui/handler/TabRenderingHandler.java +++ b/src/main/java/com/cellterminal/gui/handler/TabRenderingHandler.java @@ -14,11 +14,8 @@ import net.minecraft.client.renderer.RenderItem; import net.minecraft.item.ItemStack; -import com.cellterminal.config.CellTerminalClientConfig.TerminalStyle; import com.cellterminal.config.CellTerminalServerConfig; import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.tab.ITabController; -import com.cellterminal.gui.tab.TabControllerRegistry; /** @@ -88,9 +85,10 @@ public TabRenderResult(int hoveredTab) { * * @param ctx The rendering context * @param iconProvider Provider for tab icons + * @param tabCount Total number of tabs to draw * @return Result containing hovered tab index (-1 if none) */ - public static TabRenderResult drawTabs(TabRenderContext ctx, TabIconProvider iconProvider) { + public static TabRenderResult drawTabs(TabRenderContext ctx, TabIconProvider iconProvider, int tabCount) { GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); GlStateManager.disableLighting(); GlStateManager.enableBlend(); @@ -98,7 +96,7 @@ public static TabRenderResult drawTabs(TabRenderContext ctx, TabIconProvider ico int tabY = ctx.offsetY + ctx.tabYOffset; int hoveredTab = -1; - for (int i = 0; i < TabControllerRegistry.getTabCount(); i++) { + for (int i = 0; i < tabCount; i++) { int tabX = ctx.offsetX + 4 + (i * (ctx.tabWidth + 2)); boolean isSelected = (i == ctx.currentTab); boolean isHovered = ctx.mouseX >= tabX && ctx.mouseX < tabX + ctx.tabWidth @@ -111,13 +109,13 @@ public static TabRenderResult drawTabs(TabRenderContext ctx, TabIconProvider ico // Tab background - gray out disabled tabs int bgColor; if (isDisabled) { - bgColor = 0xFF505050; // Darker gray for disabled tabs + bgColor = GuiConstants.COLOR_TAB_DISABLED; } else if (isSelected) { - bgColor = 0xFFC6C6C6; + bgColor = GuiConstants.COLOR_TAB_SELECTED; } else if (isHovered) { - bgColor = 0xFFA0A0A0; + bgColor = GuiConstants.COLOR_TAB_HOVER; } else { - bgColor = 0xFF8B8B8B; + bgColor = GuiConstants.COLOR_TAB_NORMAL; } Gui.drawRect(tabX, tabY, tabX + ctx.tabWidth, tabY + ctx.tabHeight, bgColor); @@ -243,17 +241,15 @@ public static class ControlsHelpContext { public final int screenHeight; public final int currentTab; public final FontRenderer fontRenderer; - public final TerminalStyle style; public ControlsHelpContext(int guiLeft, int guiTop, int ySize, int screenHeight, int currentTab, - FontRenderer fontRenderer, TerminalStyle style) { + FontRenderer fontRenderer) { this.guiLeft = guiLeft; this.guiTop = guiTop; this.ySize = ySize; this.screenHeight = screenHeight; this.currentTab = currentTab; this.fontRenderer = fontRenderer; - this.style = style; } } @@ -271,23 +267,22 @@ public ControlsHelpResult(List wrappedLines, int cachedTab) { } // Constants for controls help layout - private static final int CONTROLS_HELP_LEFT_MARGIN = 4; - private static final int CONTROLS_HELP_RIGHT_MARGIN = 4; - private static final int CONTROLS_HELP_PADDING = 6; - private static final int CONTROLS_HELP_LINE_HEIGHT = 10; + private static final int CONTROLS_HELP_LEFT_MARGIN = GuiConstants.CONTROLS_HELP_LEFT_MARGIN; + private static final int CONTROLS_HELP_RIGHT_MARGIN = GuiConstants.CONTROLS_HELP_RIGHT_MARGIN; + private static final int CONTROLS_HELP_PADDING = GuiConstants.CONTROLS_HELP_PADDING; + private static final int CONTROLS_HELP_LINE_HEIGHT = GuiConstants.CONTROLS_HELP_LINE_HEIGHT; /** * Draw the controls help widget for the current tab. * * @param ctx The rendering context + * @param helpLines The help text lines from the active tab widget * @return Result containing wrapped lines and cached tab for exclusion area calculation */ - public static ControlsHelpResult drawControlsHelpWidget(ControlsHelpContext ctx) { - ITabController controller = TabControllerRegistry.getController(ctx.currentTab); - if (controller == null) return new ControlsHelpResult(new ArrayList<>(), ctx.currentTab); - - List lines = controller.getHelpLines(); - if (lines.isEmpty()) return new ControlsHelpResult(new ArrayList<>(), ctx.currentTab); + public static ControlsHelpResult drawControlsHelpWidget(ControlsHelpContext ctx, List helpLines) { + if (helpLines == null || helpLines.isEmpty()) { + return new ControlsHelpResult(new ArrayList<>(), ctx.currentTab); + } // Calculate panel width int panelWidth = ctx.guiLeft - CONTROLS_HELP_RIGHT_MARGIN - CONTROLS_HELP_LEFT_MARGIN; @@ -297,7 +292,7 @@ public static ControlsHelpResult drawControlsHelpWidget(ControlsHelpContext ctx) // Wrap all lines List wrappedLines = new ArrayList<>(); - for (String line : lines) { + for (String line : helpLines) { if (line.isEmpty()) { wrappedLines.add(""); } else { diff --git a/src/main/java/com/cellterminal/gui/handler/TerminalClickHandler.java b/src/main/java/com/cellterminal/gui/handler/TerminalClickHandler.java deleted file mode 100644 index c7f6664..0000000 --- a/src/main/java/com/cellterminal/gui/handler/TerminalClickHandler.java +++ /dev/null @@ -1,286 +0,0 @@ -package com.cellterminal.gui.handler; - -import java.util.List; -import java.util.Map; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiScreen; -import net.minecraft.item.ItemStack; - -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.StorageInfo; -import com.cellterminal.client.TabStateManager; -import com.cellterminal.config.CellTerminalClientConfig; -import com.cellterminal.config.CellTerminalServerConfig; -import com.cellterminal.gui.tab.TabControllerRegistry; -import com.cellterminal.gui.overlay.MessageHelper; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.network.CellTerminalNetwork; -import com.cellterminal.network.PacketEjectCell; -import com.cellterminal.network.PacketHighlightBlock; -import com.cellterminal.network.PacketInsertCell; -import com.cellterminal.network.PacketPickupCell; - - -/** - * Handles mouse click logic for Cell Terminal GUI. - */ -public class TerminalClickHandler { - - // Layout constants - private static final int TAB_WIDTH = 22; - private static final int TAB_HEIGHT = 22; - private static final int TAB_Y_OFFSET = -22; - private static final int ROW_HEIGHT = 18; - private static final int BUTTON_SIZE = 14; - private static final int BUTTON_EJECT_X = 135; - private static final int BUTTON_INVENTORY_X = 150; - private static final int BUTTON_PARTITION_X = 165; - - // Double-click tracking (Tab 1) - private long lastClickTime = 0; - private int lastClickedLineIndex = -1; - - // Double-click tracking (Tabs 2/3) - uses line index for reliability - private long lastClickTimeTab23 = 0; - private int lastClickedLineIndexTab23 = -1; - - // Callback interface for GUI interactions - public interface Callback { - void onTabChanged(int tab); - void onStorageToggle(StorageInfo storage); - void openInventoryPopup(CellInfo cell, int mouseX, int mouseY); - void openPartitionPopup(CellInfo cell, int mouseX, int mouseY); - void onTogglePartitionItem(CellInfo cell, ItemStack stack); - void onAddPartitionItem(CellInfo cell, int slotIndex, ItemStack stack); - void onRemovePartitionItem(CellInfo cell, int slotIndex); - } - - public boolean handleTabClick(int mouseX, int mouseY, int guiLeft, int guiTop, int currentTab, Callback callback) { - int tabY = guiTop + TAB_Y_OFFSET; - - for (int i = 0; i < TabControllerRegistry.getTabCount(); i++) { - int tabX = guiLeft + 4 + (i * (TAB_WIDTH + 2)); - - if (mouseX >= tabX && mouseX < tabX + TAB_WIDTH - && mouseY >= tabY && mouseY < tabY + TAB_HEIGHT) { - - // Check if the tab is enabled in server config - if (CellTerminalServerConfig.isInitialized() && !CellTerminalServerConfig.getInstance().isTabEnabled(i)) { - return true; // Consume click but don't switch to disabled tab - } - - if (currentTab != i) { - callback.onTabChanged(i); - CellTerminalClientConfig.getInstance().setSelectedTab(i); - } - - return true; - } - } - - return false; - } - - public void handleTerminalTabClick(int mouseX, int mouseY, int mouseButton, int guiLeft, int guiTop, - int rowsVisible, int currentScroll, List lines, - Map storageMap, int terminalDimension, Callback callback) { - - int relX = mouseX - guiLeft; - int relY = mouseY - guiTop; - - if (relX < 4 || relX > 190 || relY < 18 || relY >= 18 + rowsVisible * ROW_HEIGHT) return; - - int row = (relY - 18) / ROW_HEIGHT; - int lineIndex = currentScroll + row; - - if (lineIndex >= lines.size()) return; - - Object line = lines.get(lineIndex); - - // Check for held cell - insert into storage - ItemStack heldStack = Minecraft.getMinecraft().player.inventory.getItemStack(); - if (!heldStack.isEmpty()) { - long storageId = -1; - - if (line instanceof StorageInfo) { - storageId = ((StorageInfo) line).getId(); - } else if (line instanceof CellInfo) { - storageId = ((CellInfo) line).getParentStorageId(); - } - - if (storageId >= 0) { - CellTerminalNetwork.INSTANCE.sendToServer(new PacketInsertCell(storageId, -1)); - - return; - } - } - - // Check for double-click to highlight block - long now = System.currentTimeMillis(); - if (lineIndex == lastClickedLineIndex && now - lastClickTime < 400) { - handleDoubleClick(line, storageMap, terminalDimension); - lastClickedLineIndex = -1; - - return; - } - - lastClickedLineIndex = lineIndex; - lastClickTime = now; - - if (line instanceof StorageInfo) { - StorageInfo storage = (StorageInfo) line; - - if (relX >= 165 && relX < 180) { - boolean newState = TabStateManager.getInstance().toggleExpanded(TabStateManager.TabType.TERMINAL, storage.getId()); - storage.setExpanded(newState); - callback.onStorageToggle(storage); - } - - return; - } - - if (line instanceof CellInfo) { - handleCellClick((CellInfo) line, relX, relY, row, mouseX, mouseY, callback); - } - } - - public void handleCellTabClick(int currentTab, int relMouseX, CellInfo hoveredCellCell, int hoveredContentSlotIndex, - CellInfo hoveredPartitionCell, int hoveredPartitionSlotIndex, - StorageInfo hoveredCellStorage, int hoveredCellSlotIndex, - StorageInfo hoveredStorageLine, int hoveredLineIndex, - Map storageMap, int terminalDimension, Callback callback) { - - // Check for expand/collapse button click on storage header - if (hoveredStorageLine != null && relMouseX >= 165 && relMouseX < 180) { - TabStateManager.TabType tabType = currentTab == GuiConstants.TAB_INVENTORY - ? TabStateManager.TabType.INVENTORY - : TabStateManager.TabType.PARTITION; - TabStateManager.getInstance().toggleExpanded(tabType, hoveredStorageLine.getId()); - callback.onStorageToggle(hoveredStorageLine); - - return; - } - - // Check for double-click to highlight block - long now = System.currentTimeMillis(); - - if (hoveredStorageLine != null && hoveredLineIndex >= 0 - && hoveredLineIndex == lastClickedLineIndexTab23 && now - lastClickTimeTab23 < 400) { - handleDoubleClickTab23(hoveredStorageLine, terminalDimension); - lastClickedLineIndexTab23 = -1; - - return; - } - - // Only track for double-click when clicking directly on a storage header - if (hoveredStorageLine != null) { - lastClickedLineIndexTab23 = hoveredLineIndex; - lastClickTimeTab23 = now; - } else { - lastClickedLineIndexTab23 = -1; - } - - // Tab 2 (Inventory): Check if clicking on a content item to toggle partition - if (currentTab == GuiConstants.TAB_INVENTORY && hoveredCellCell != null && hoveredContentSlotIndex >= 0) { - List contents = hoveredCellCell.getContents(); - - if (hoveredContentSlotIndex < contents.size() && !contents.get(hoveredContentSlotIndex).isEmpty()) { - callback.onTogglePartitionItem(hoveredCellCell, contents.get(hoveredContentSlotIndex)); - - return; - } - } - - // Tab 3 (Partition): Check if clicking on a partition slot - if (currentTab == GuiConstants.TAB_PARTITION && hoveredPartitionCell != null && hoveredPartitionSlotIndex >= 0) { - List partitions = hoveredPartitionCell.getPartition(); - ItemStack heldStack = Minecraft.getMinecraft().player.inventory.getItemStack(); - boolean slotOccupied = hoveredPartitionSlotIndex < partitions.size() && !partitions.get(hoveredPartitionSlotIndex).isEmpty(); - - if (!heldStack.isEmpty()) { - callback.onAddPartitionItem(hoveredPartitionCell, hoveredPartitionSlotIndex, heldStack); - - return; - } - - if (slotOccupied) { - callback.onRemovePartitionItem(hoveredPartitionCell, hoveredPartitionSlotIndex); - - return; - } - } - - // Handle cell slot clicks (pickup/swap cells) - // Shift-click: eject to inventory, regular click: pick up to hand - if (hoveredCellStorage == null || hoveredCellSlotIndex < 0) return; - - boolean toInventory = GuiScreen.isShiftKeyDown(); - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketPickupCell(hoveredCellStorage.getId(), hoveredCellSlotIndex, toInventory) - ); - } - - private void handleDoubleClick(Object line, Map storageMap, int terminalDimension) { - StorageInfo storage = null; - - if (line instanceof StorageInfo) { - storage = (StorageInfo) line; - } else if (line instanceof CellInfo) { - CellInfo cell = (CellInfo) line; - storage = storageMap.get(cell.getParentStorageId()); - } - - if (storage == null) return; - - highlightStorage(storage); - } - - private void handleDoubleClickTab23(StorageInfo storage, int terminalDimension) { - if (storage == null) return; - - highlightStorage(storage); - } - - private void highlightStorage(StorageInfo storage) { - if (storage.getDimension() != Minecraft.getMinecraft().player.dimension) { - MessageHelper.error("cellterminal.error.different_dimension"); - - return; - } - - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketHighlightBlock(storage.getPos(), storage.getDimension()) - ); - - // Send green chat message with block name and coordinates - MessageHelper.success("gui.cellterminal.highlighted", - storage.getPos().getX(), storage.getPos().getY(), storage.getPos().getZ(), storage.getName()); - } - - private void handleCellClick(CellInfo cell, int relX, int relY, int row, int mouseX, int mouseY, Callback callback) { - int rowY = 18 + row * ROW_HEIGHT; - - // Eject button: always ejects to player's inventory - if (relX >= BUTTON_EJECT_X && relX < BUTTON_EJECT_X + BUTTON_SIZE - && relY >= rowY + 1 && relY < rowY + 1 + BUTTON_SIZE) { - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketEjectCell(cell.getParentStorageId(), cell.getSlot()) - ); - - return; - } - - if (relX >= BUTTON_INVENTORY_X && relX < BUTTON_INVENTORY_X + BUTTON_SIZE - && relY >= rowY + 1 && relY < rowY + 1 + BUTTON_SIZE) { - callback.openInventoryPopup(cell, mouseX, mouseY); - - return; - } - - if (relX >= BUTTON_PARTITION_X && relX < BUTTON_PARTITION_X + BUTTON_SIZE - && relY >= rowY + 1 && relY < rowY + 1 + BUTTON_SIZE) { - callback.openPartitionPopup(cell, mouseX, mouseY); - } - } -} diff --git a/src/main/java/com/cellterminal/gui/handler/TerminalDataManager.java b/src/main/java/com/cellterminal/gui/handler/TerminalDataManager.java index 6976f72..8a5e4df 100644 --- a/src/main/java/com/cellterminal/gui/handler/TerminalDataManager.java +++ b/src/main/java/com/cellterminal/gui/handler/TerminalDataManager.java @@ -30,7 +30,6 @@ import com.cellterminal.client.TabStateManager; import com.cellterminal.client.TempCellInfo; import com.cellterminal.config.CellTerminalClientConfig; -import com.cellterminal.gui.GuiConstants; /** @@ -61,7 +60,7 @@ public class TerminalDataManager { // Current filter settings private String searchFilter = ""; private SearchFilterMode searchMode = SearchFilterMode.MIXED; - private Map activeFilters = new EnumMap<>(CellFilter.class); + private final Map activeFilters = new EnumMap<>(CellFilter.class); // Advanced search matcher (cached for performance) private AdvancedSearchParser.SearchMatcher advancedMatcher = null; @@ -111,14 +110,6 @@ public List getTempAreaLines() { return tempAreaLines; } - public BlockPos getTerminalPos() { - return terminalPos; - } - - public int getTerminalDimension() { - return terminalDimension; - } - public void processUpdate(NBTTagCompound data) { if (data.hasKey("terminalPos")) { this.terminalPos = BlockPos.fromLong(data.getLong("terminalPos")); @@ -815,9 +806,14 @@ private int getHighestNonEmptyPartitionSlot(CellInfo cell) { */ private int getHighestNonEmptyPartitionSlot(CellInfo cell, int slotsPerRow) { List partition = cell.getPartition(); + + // Cap at cell's actual type limit (e.g. 8 for some custom cells) to avoid + // generating empty continuation rows beyond what the cell can hold. + int maxTypes = (int) cell.getTotalTypes(); + int effectiveMax = Math.min(MAX_PARTITION_SLOTS, maxTypes); int highest = -1; - for (int i = 0; i < partition.size(); i++) { + for (int i = 0; i < Math.min(partition.size(), effectiveMax); i++) { if (!partition.get(i).isEmpty()) { highest = i; } @@ -827,8 +823,8 @@ private int getHighestNonEmptyPartitionSlot(CellInfo cell, int slotsPerRow) { int currentRows = (highest / slotsPerRow) + 1; int lastSlotInLastRow = (currentRows * slotsPerRow) - 1; - if (highest == lastSlotInLastRow && highest < MAX_PARTITION_SLOTS - 1) { - highest = Math.min(highest + slotsPerRow, MAX_PARTITION_SLOTS - 1); + if (highest == lastSlotInLastRow && highest < effectiveMax - 1) { + highest = Math.min(highest + slotsPerRow, effectiveMax - 1); } } @@ -851,21 +847,6 @@ private Comparator createStorageComparator() { }; } - public int getLineCount(int currentTab) { - switch (currentTab) { - case GuiConstants.TAB_INVENTORY: - return inventoryLines.size(); - case GuiConstants.TAB_PARTITION: - return partitionLines.size(); - case GuiConstants.TAB_STORAGE_BUS_INVENTORY: - return storageBusInventoryLines.size(); - case GuiConstants.TAB_STORAGE_BUS_PARTITION: - return storageBusPartitionLines.size(); - default: - return lines.size(); - } - } - /** * Reset the data manager for a network switch. * This clears the hasInitialData flag so the next update does a full rebuild @@ -881,4 +862,18 @@ public void resetForNetworkSwitch() { this.visibleBusSnapshotInventory.clear(); this.visibleBusSnapshotPartition.clear(); } + + /** + * Find the StorageInfo containing a given CellInfo. + * Searches through all storages to find the one with matching parent ID. + * + * @param cell The cell to find the parent storage for + * @return The StorageInfo containing this cell, or null if not found + */ + public StorageInfo findStorageForCell(CellInfo cell) { + if (cell == null) return null; + + long parentId = cell.getParentStorageId(); + return storageMap.get(parentId); + } } diff --git a/src/main/java/com/cellterminal/gui/handler/TooltipHandler.java b/src/main/java/com/cellterminal/gui/handler/TooltipHandler.java index 5e18cae..24b1119 100644 --- a/src/main/java/com/cellterminal/gui/handler/TooltipHandler.java +++ b/src/main/java/com/cellterminal/gui/handler/TooltipHandler.java @@ -7,23 +7,8 @@ import net.minecraft.client.resources.I18n; import net.minecraft.item.ItemStack; -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.client.TempCellInfo; -import com.cellterminal.config.CellTerminalServerConfig; -import com.cellterminal.gui.FilterPanelManager; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.GuiFilterButton; -import com.cellterminal.gui.GuiSlotLimitButton; -import com.cellterminal.gui.GuiSearchHelpButton; -import com.cellterminal.gui.GuiSearchModeButton; -import com.cellterminal.gui.GuiSubnetVisibilityButton; -import com.cellterminal.gui.GuiTerminalStyleButton; -import com.cellterminal.gui.PopupCellInventory; -import com.cellterminal.gui.PopupCellPartition; +import com.cellterminal.gui.buttons.*; import com.cellterminal.gui.PriorityFieldManager; -import com.cellterminal.gui.networktools.INetworkTool; -import com.cellterminal.gui.render.RenderContext; /** @@ -35,54 +20,17 @@ public class TooltipHandler { * Context containing all the state needed for rendering tooltips. */ public static class TooltipContext { - // Tab state - public int currentTab; - public int hoveredTab = -1; - - // Hover state - public CellInfo hoveredCell; - public int hoverType = 0; - public ItemStack hoveredContentStack = ItemStack.EMPTY; - public int hoveredContentX; - public int hoveredContentY; - - // Storage bus state - public StorageBusInfo hoveredClearButtonStorageBus; - public StorageBusInfo hoveredIOModeButtonStorageBus; - public StorageBusInfo hoveredPartitionAllButtonStorageBus; - - // Cell partition button state - public CellInfo hoveredPartitionAllButtonCell; - public CellInfo hoveredClearPartitionButtonCell; - - // Popup state - public PopupCellInventory inventoryPopup; - public PopupCellPartition partitionPopup; - // Widget state public GuiTerminalStyleButton terminalStyleButton; public GuiSearchModeButton searchModeButton; public GuiSearchHelpButton searchHelpButton; public GuiSubnetVisibilityButton subnetVisibilityButton; - public PriorityFieldManager priorityFieldManager; public FilterPanelManager filterPanelManager; // Search error state public boolean hasSearchError = false; public List searchErrorMessage = null; public int searchFieldX, searchFieldY, searchFieldWidth, searchFieldHeight; - - // Upgrade icon hover state - public RenderContext.UpgradeIconTarget hoveredUpgradeIcon = null; - - // Network tools hover state - public INetworkTool hoveredNetworkTool = null; - public INetworkTool hoveredNetworkToolHelpButton = null; - public INetworkTool.ToolPreviewInfo hoveredNetworkToolPreview = null; - - // Temp area hover state (Tab 3) - public TempCellInfo hoveredTempCellSlot = null; // Hovering cell slot for insert/extract - public TempCellInfo hoveredTempCellSendButton = null; // Hovering send button } /** @@ -98,82 +46,6 @@ public interface TooltipRenderer { */ public static void drawTooltips(TooltipContext ctx, TooltipRenderer renderer, int mouseX, int mouseY) { - // Network tools help button tooltip - if (ctx.hoveredNetworkToolHelpButton != null) { - List tooltip = new ArrayList<>(); - tooltip.add("§e" + ctx.hoveredNetworkToolHelpButton.getName()); - tooltip.add(""); - for (String line : ctx.hoveredNetworkToolHelpButton.getHelpLines()) tooltip.add("§7" + line); - - renderer.drawHoveringText(tooltip, mouseX, mouseY); - - return; - } - - // Network tools preview tooltip - show tooltip lines from the preview - if (ctx.hoveredNetworkToolPreview != null) { - List tooltipLines = ctx.hoveredNetworkToolPreview.getTooltipLines(); - if (tooltipLines != null && !tooltipLines.isEmpty()) { - List lines = new ArrayList<>(); - lines.add("§e" + ctx.hoveredNetworkTool.getName()); - lines.add(""); - lines.addAll(tooltipLines); - renderer.drawHoveringText(lines, mouseX, mouseY); - - return; - } - } - - // Upgrade icon tooltips - if (ctx.hoveredUpgradeIcon != null) { - List tooltip = new ArrayList<>(); - tooltip.add("§6" + ctx.hoveredUpgradeIcon.upgrade.getDisplayName()); - tooltip.add(""); - tooltip.add("§b" + I18n.format("gui.cellterminal.upgrade.click_extract")); - tooltip.add("§b" + I18n.format("gui.cellterminal.upgrade.shift_click_inventory")); - renderer.drawHoveringText(tooltip, mouseX, mouseY); - - return; - } - - // Content item tooltips (including temp area tab) - if ((ctx.currentTab == GuiConstants.TAB_INVENTORY || ctx.currentTab == GuiConstants.TAB_PARTITION - || ctx.currentTab == GuiConstants.TAB_STORAGE_BUS_INVENTORY || ctx.currentTab == GuiConstants.TAB_STORAGE_BUS_PARTITION - || ctx.currentTab == GuiConstants.TAB_TEMP_AREA) - && !ctx.hoveredContentStack.isEmpty()) { - renderer.drawHoveringText(renderer.getItemToolTip(ctx.hoveredContentStack), ctx.hoveredContentX, ctx.hoveredContentY); - - return; - } - - // Temp area cell slot tooltip - if (ctx.hoveredTempCellSlot != null) { - if (!ctx.hoveredTempCellSlot.getCellStack().isEmpty()) { - renderer.drawHoveringText(renderer.getItemToolTip(ctx.hoveredTempCellSlot.getCellStack()), mouseX, mouseY); - } else { - renderer.drawHoveringText(Collections.singletonList(I18n.format("gui.cellterminal.temp_area.drop_cell")), mouseX, mouseY); - } - - return; - } - - // Temp area send button tooltip - if (ctx.hoveredTempCellSendButton != null) { - renderer.drawHoveringText(Collections.singletonList(I18n.format("gui.cellterminal.temp_area.send.tooltip")), mouseX, mouseY); - - return; - } - - // Tab tooltips - if (ctx.hoveredTab >= 0 && ctx.inventoryPopup == null && ctx.partitionPopup == null) { - String tooltip = getTabTooltip(ctx.hoveredTab); - if (!tooltip.isEmpty()) { - renderer.drawHoveringText(Collections.singletonList(tooltip), mouseX, mouseY); - - return; - } - } - // Widget tooltips if (ctx.terminalStyleButton != null && ctx.terminalStyleButton.isMouseOver()) { renderer.drawHoveringText(ctx.terminalStyleButton.getTooltip(), mouseX, mouseY); @@ -213,7 +85,7 @@ public static void drawTooltips(TooltipContext ctx, TooltipRenderer renderer, in } } - if (ctx.priorityFieldManager != null && ctx.priorityFieldManager.isMouseOverField(mouseX, mouseY)) { + if (PriorityFieldManager.getInstance().isMouseOverField(mouseX, mouseY)) { renderer.drawHoveringText(Collections.singletonList(I18n.format("gui.cellterminal.priority.tooltip")), mouseX, mouseY); return; @@ -235,81 +107,7 @@ public static void drawTooltips(TooltipContext ctx, TooltipRenderer renderer, in && mouseY >= slotBtn.y && mouseY < slotBtn.y + slotBtn.height) { renderer.drawHoveringText(slotBtn.getTooltip(), mouseX, mouseY); - return; } } - - // Storage bus button tooltips - if (ctx.hoveredClearButtonStorageBus != null) { - renderer.drawHoveringText(Collections.singletonList(I18n.format("gui.cellterminal.storagebus.clear")), mouseX, mouseY); - - return; - } - - if (ctx.hoveredIOModeButtonStorageBus != null) { - if (ctx.hoveredIOModeButtonStorageBus.supportsIOMode()) { - String currentMode = ctx.hoveredIOModeButtonStorageBus.getIOModeDisplayName(); - renderer.drawHoveringText(Collections.singletonList( - I18n.format("gui.cellterminal.storagebus.iomode.current", currentMode)), mouseX, mouseY); - } else { - renderer.drawHoveringText(Collections.singletonList( - I18n.format("gui.cellterminal.storagebus.iomode.unsupported")), mouseX, mouseY); - } - - return; - } - - // Partition button tooltips - if (ctx.hoveredPartitionAllButtonCell != null) { - renderer.drawHoveringText(Collections.singletonList(I18n.format("gui.cellterminal.cell.partitionall")), mouseX, mouseY); - - return; - } - - if (ctx.hoveredPartitionAllButtonStorageBus != null) { - renderer.drawHoveringText(Collections.singletonList(I18n.format("gui.cellterminal.storagebus.partitionall")), mouseX, mouseY); - - return; - } - - if (ctx.hoveredClearPartitionButtonCell != null) { - renderer.drawHoveringText(Collections.singletonList(I18n.format("gui.cellterminal.cell.clearpartition")), mouseX, mouseY); - - return; - } - } - - private static String getTabTooltip(int tab) { - String baseTooltip; - - switch (tab) { - case GuiConstants.TAB_TERMINAL: - baseTooltip = I18n.format("gui.cellterminal.tab.terminal.tooltip"); - break; - case GuiConstants.TAB_INVENTORY: - baseTooltip = I18n.format("gui.cellterminal.tab.inventory.tooltip"); - break; - case GuiConstants.TAB_PARTITION: - baseTooltip = I18n.format("gui.cellterminal.tab.partition.tooltip"); - break; - case GuiConstants.TAB_STORAGE_BUS_INVENTORY: - baseTooltip = I18n.format("gui.cellterminal.tab.storage_bus_inventory.tooltip"); - break; - case GuiConstants.TAB_STORAGE_BUS_PARTITION: - baseTooltip = I18n.format("gui.cellterminal.tab.storage_bus_partition.tooltip"); - break; - case GuiConstants.TAB_NETWORK_TOOLS: - baseTooltip = I18n.format("gui.cellterminal.tab.network_tools.tooltip"); - break; - default: - return ""; - } - - // Add disabled notice if tab is disabled in server config - if (CellTerminalServerConfig.isInitialized() && !CellTerminalServerConfig.getInstance().isTabEnabled(tab)) { - return baseTooltip + " " + I18n.format("gui.cellterminal.tab.disabled"); - } - - return baseTooltip; } } diff --git a/src/main/java/com/cellterminal/gui/handler/UpgradeClickHandler.java b/src/main/java/com/cellterminal/gui/handler/UpgradeClickHandler.java deleted file mode 100644 index f621d9e..0000000 --- a/src/main/java/com/cellterminal/gui/handler/UpgradeClickHandler.java +++ /dev/null @@ -1,257 +0,0 @@ -package com.cellterminal.gui.handler; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import org.lwjgl.input.Keyboard; - -import net.minecraft.client.Minecraft; -import net.minecraft.item.ItemStack; - -import appeng.api.implementations.items.IUpgradeModule; - -import com.cellterminal.client.CellContentRow; -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.StorageBusContentRow; -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.client.StorageInfo; -import com.cellterminal.config.CellTerminalServerConfig; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.overlay.MessageHelper; -import com.cellterminal.network.CellTerminalNetwork; -import com.cellterminal.network.PacketUpgradeCell; -import com.cellterminal.network.PacketUpgradeStorageBus; - - -/** - * Handler for upgrade click operations in the Cell Terminal GUI. - */ -public class UpgradeClickHandler { - - private UpgradeClickHandler() {} - - /** - * Context for upgrade click handling. - */ - public static class UpgradeClickContext { - public final int currentTab; - public final CellInfo hoveredCellCell; - public final StorageInfo hoveredCellStorage; - public final int hoveredCellSlotIndex; - public final StorageInfo hoveredStorageLine; - public final StorageBusInfo hoveredStorageBus; - public final TerminalDataManager dataManager; - - public UpgradeClickContext(int currentTab, CellInfo hoveredCellCell, StorageInfo hoveredCellStorage, - int hoveredCellSlotIndex, StorageInfo hoveredStorageLine, - StorageBusInfo hoveredStorageBus, TerminalDataManager dataManager) { - this.currentTab = currentTab; - this.hoveredCellCell = hoveredCellCell; - this.hoveredCellStorage = hoveredCellStorage; - this.hoveredCellSlotIndex = hoveredCellSlotIndex; - this.hoveredStorageLine = hoveredStorageLine; - this.hoveredStorageBus = hoveredStorageBus; - this.dataManager = dataManager; - } - } - - /** - * Handle upgrade click when player is holding an upgrade item. - * - * @param ctx Context with hover state - * @return true if an upgrade click was handled - */ - public static boolean handleUpgradeClick(UpgradeClickContext ctx) { - ItemStack heldStack = Minecraft.getMinecraft().player.inventory.getItemStack(); - if (heldStack.isEmpty()) return false; - if (!(heldStack.getItem() instanceof IUpgradeModule)) return false; - - // Check if upgrade insertion is enabled - if (CellTerminalServerConfig.isInitialized() - && !CellTerminalServerConfig.getInstance().isUpgradeInsertEnabled()) { - MessageHelper.error("cellterminal.error.upgrade_insert_disabled"); - - return true; // Consume click to prevent other handlers - } - - boolean isShiftClick = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); - - if (isShiftClick) { - CellInfo targetCell = findFirstVisibleCellThatCanAcceptUpgrade(ctx, heldStack); - if (targetCell == null) return false; - - StorageInfo storage = ctx.dataManager.getStorageMap().get(targetCell.getParentStorageId()); - if (storage == null) return false; - - CellTerminalNetwork.INSTANCE.sendToServer(new PacketUpgradeCell( - storage.getId(), - targetCell.getSlot(), - true - )); - - return true; - } - - // Regular click: check if hovering a cell directly - if (ctx.hoveredCellCell != null && ctx.hoveredCellStorage != null) { - if (!ctx.hoveredCellCell.canAcceptUpgrade(heldStack)) return false; - - CellTerminalNetwork.INSTANCE.sendToServer(new PacketUpgradeCell( - ctx.hoveredCellStorage.getId(), - ctx.hoveredCellSlotIndex, - false - )); - - return true; - } - - // Regular click on storage header: upgrade first cell in that storage - if (ctx.hoveredStorageLine != null) { - CellInfo targetCell = findFirstCellInStorageThatCanAcceptUpgrade(ctx.hoveredStorageLine, heldStack); - if (targetCell == null) return false; - - CellTerminalNetwork.INSTANCE.sendToServer(new PacketUpgradeCell( - ctx.hoveredStorageLine.getId(), - targetCell.getSlot(), - false - )); - - return true; - } - - return false; - } - - /** - * Handle upgrade click on storage bus headers when player is holding an upgrade item. - * - * @param hoveredStorageBus The storage bus being hovered - * @return true if an upgrade click was handled - */ - public static boolean handleStorageBusUpgradeClick(StorageBusInfo hoveredStorageBus) { - ItemStack heldStack = Minecraft.getMinecraft().player.inventory.getItemStack(); - if (heldStack.isEmpty()) return false; - if (!(heldStack.getItem() instanceof IUpgradeModule)) return false; - - // Check if upgrade insertion is enabled - if (CellTerminalServerConfig.isInitialized() - && !CellTerminalServerConfig.getInstance().isUpgradeInsertEnabled()) { - MessageHelper.error("cellterminal.error.upgrade_insert_disabled"); - - return true; // Consume click to prevent other handlers - } - - boolean isShiftClick = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); - - // For shift-click, send any storage bus ID (server will find the first accepting one) - // For regular click, require hovering a specific storage bus - if (isShiftClick) { - // Use ID 0 as placeholder; server will iterate through all buses - CellTerminalNetwork.INSTANCE.sendToServer(new PacketUpgradeStorageBus(0, true)); - - return true; - } - - if (hoveredStorageBus == null) return false; - - CellTerminalNetwork.INSTANCE.sendToServer(new PacketUpgradeStorageBus(hoveredStorageBus.getId(), false)); - - return true; - } - - /** - * Find the first visible cell that can accept the given upgrade. - */ - public static CellInfo findFirstVisibleCellThatCanAcceptUpgrade(UpgradeClickContext ctx, ItemStack upgradeStack) { - List lines; - - switch (ctx.currentTab) { - case GuiConstants.TAB_TERMINAL: - lines = ctx.dataManager.getLines(); - break; - case GuiConstants.TAB_INVENTORY: - lines = ctx.dataManager.getInventoryLines(); - break; - case GuiConstants.TAB_PARTITION: - lines = ctx.dataManager.getPartitionLines(); - break; - default: - return null; - } - - Set checkedCellIds = new HashSet<>(); - - for (Object line : lines) { - CellInfo cell = null; - - if (line instanceof CellInfo) { - cell = (CellInfo) line; - } else if (line instanceof CellContentRow) { - cell = ((CellContentRow) line).getCell(); - } - - if (cell == null) continue; - - long cellId = cell.getParentStorageId() * 100 + cell.getSlot(); - if (checkedCellIds.contains(cellId)) continue; - checkedCellIds.add(cellId); - - if (cell.canAcceptUpgrade(upgradeStack)) return cell; - } - - return null; - } - - /** - * Find the first cell in a specific storage that can accept the given upgrade. - */ - public static CellInfo findFirstCellInStorageThatCanAcceptUpgrade(StorageInfo storage, ItemStack upgradeStack) { - for (CellInfo cell : storage.getCells()) { - if (cell.canAcceptUpgrade(upgradeStack)) return cell; - } - - return null; - } - - /** - * Find the first visible storage bus that can accept the given upgrade. - * Used for shift-clicking upgrades from inventory on storage bus tabs. - */ - public static StorageBusInfo findFirstVisibleStorageBusThatCanAcceptUpgrade(UpgradeClickContext ctx, ItemStack upgradeStack) { - List lines; - - switch (ctx.currentTab) { - case GuiConstants.TAB_STORAGE_BUS_INVENTORY: - lines = ctx.dataManager.getStorageBusInventoryLines(); - break; - case GuiConstants.TAB_STORAGE_BUS_PARTITION: - lines = ctx.dataManager.getStorageBusPartitionLines(); - break; - default: - return null; - } - - Set checkedBusIds = new HashSet<>(); - - for (Object line : lines) { - StorageBusInfo bus = null; - - if (line instanceof StorageBusInfo) { - bus = (StorageBusInfo) line; - } else if (line instanceof StorageBusContentRow) { - bus = ((StorageBusContentRow) line).getStorageBus(); - } - - if (bus == null) continue; - - long busId = bus.getId(); - if (checkedBusIds.contains(busId)) continue; - checkedBusIds.add(busId); - - if (bus.canAcceptUpgrade(upgradeStack)) return bus; - } - - return null; - } -} diff --git a/src/main/java/com/cellterminal/gui/networktools/AttributeUniqueTool.java b/src/main/java/com/cellterminal/gui/networktools/AttributeUniqueTool.java index 420eae2..3da32de 100644 --- a/src/main/java/com/cellterminal/gui/networktools/AttributeUniqueTool.java +++ b/src/main/java/com/cellterminal/gui/networktools/AttributeUniqueTool.java @@ -166,6 +166,7 @@ private CellType getCellType(CellInfo cell) { } private String getItemKey(ItemStack stack, CellType type) { + // FIXME: use ItemStackKey and FluidStackKey from util if (type == CellType.FLUID) { // For fluids, use a simpler key based on the representation item return "fluid:" + stack.getItem().getRegistryName().toString() + "@" + stack.getMetadata(); diff --git a/src/main/java/com/cellterminal/gui/networktools/GuiToolConfirmationModal.java b/src/main/java/com/cellterminal/gui/networktools/GuiToolConfirmationModal.java index 9a9b709..7edf755 100644 --- a/src/main/java/com/cellterminal/gui/networktools/GuiToolConfirmationModal.java +++ b/src/main/java/com/cellterminal/gui/networktools/GuiToolConfirmationModal.java @@ -26,8 +26,6 @@ public class GuiToolConfirmationModal { private final INetworkTool tool; private final INetworkTool.ToolContext context; private final FontRenderer fontRenderer; - private final int screenWidth; - private final int screenHeight; private final Runnable onConfirm; private final Runnable onCancel; @@ -49,8 +47,6 @@ public GuiToolConfirmationModal(INetworkTool tool, this.tool = tool; this.context = context; this.fontRenderer = fontRenderer; - this.screenWidth = screenWidth; - this.screenHeight = screenHeight; this.onConfirm = onConfirm; this.onCancel = onCancel; @@ -147,31 +143,30 @@ public void draw(int mouseX, int mouseY) { private void drawButton(int x, int y, int width, int height, String text, boolean hovered, boolean isConfirm) { int bgColor; - int borderTop; - int borderLeft; - int borderRight; - int borderBottom; + int borderTopColor; + int borderLeftColor; + int borderRightColor; + int borderBottomColor; if (isConfirm) { bgColor = hovered ? 0xFF40A040 : 0xFF308030; - borderTop = hovered ? 0xFF60C060 : 0xFF50A050; - borderLeft = borderTop; - borderRight = hovered ? 0xFF206020 : 0xFF105010; - borderBottom = borderRight; + borderTopColor = hovered ? 0xFF60C060 : 0xFF50A050; + borderRightColor = hovered ? 0xFF206020 : 0xFF105010; } else { bgColor = hovered ? 0xFF606060 : 0xFF505050; - borderTop = hovered ? 0xFF808080 : 0xFF707070; - borderLeft = borderTop; - borderRight = hovered ? 0xFF303030 : 0xFF202020; - borderBottom = borderRight; + borderTopColor = hovered ? 0xFF808080 : 0xFF707070; + borderRightColor = hovered ? 0xFF303030 : 0xFF202020; } + borderLeftColor = borderTopColor; + borderBottomColor = borderRightColor; + GlStateManager.disableTexture2D(); Gui.drawRect(x, y, x + width, y + height, bgColor); - Gui.drawRect(x, y, x + width, y + 1, borderTop); - Gui.drawRect(x, y, x + 1, y + height, borderLeft); - Gui.drawRect(x + width - 1, y, x + width, y + height, borderRight); - Gui.drawRect(x, y + height - 1, x + width, y + height, borderBottom); + Gui.drawRect(x, y, x + width, y + 1, borderTopColor); + Gui.drawRect(x, y, x + 1, y + height, borderLeftColor); + Gui.drawRect(x + width - 1, y, x + width, y + height, borderRightColor); + Gui.drawRect(x, y + height - 1, x + width, y + height, borderBottomColor); GlStateManager.enableTexture2D(); GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); @@ -191,17 +186,13 @@ private boolean isMouseOver(int mouseX, int mouseY, int x, int y, int width, int public boolean handleClick(int mouseX, int mouseY, int button) { if (button != 0) return false; - int buttonsY = modalY + modalHeight - BUTTON_HEIGHT - PADDING; - int confirmX = modalX + (modalWidth / 2) - BUTTON_WIDTH - (BUTTON_SPACING / 2); - int cancelX = modalX + (modalWidth / 2) + (BUTTON_SPACING / 2); - - if (isMouseOver(mouseX, mouseY, confirmX, buttonsY, BUTTON_WIDTH, BUTTON_HEIGHT)) { + if (confirmHovered) { onConfirm.run(); return true; } - if (isMouseOver(mouseX, mouseY, cancelX, buttonsY, BUTTON_WIDTH, BUTTON_HEIGHT)) { + if (cancelHovered) { onCancel.run(); return true; diff --git a/src/main/java/com/cellterminal/gui/networktools/INetworkTool.java b/src/main/java/com/cellterminal/gui/networktools/INetworkTool.java index 76f8884..bdace32 100644 --- a/src/main/java/com/cellterminal/gui/networktools/INetworkTool.java +++ b/src/main/java/com/cellterminal/gui/networktools/INetworkTool.java @@ -264,7 +264,6 @@ public StorageInfo getStorage() { * Callback interface for network tool actions. */ interface NetworkToolCallback { - void sendToolPacket(String toolId, byte[] data); void showError(String message); void showSuccess(String message); } diff --git a/src/main/java/com/cellterminal/gui/rename/InlineRenameEditor.java b/src/main/java/com/cellterminal/gui/rename/InlineRenameEditor.java deleted file mode 100644 index 7a3f315..0000000 --- a/src/main/java/com/cellterminal/gui/rename/InlineRenameEditor.java +++ /dev/null @@ -1,205 +0,0 @@ -package com.cellterminal.gui.rename; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; - -import org.lwjgl.input.Keyboard; - -import com.cellterminal.gui.GuiConstants; - - -/** - * Shared inline text editing component for renaming entries in the Cell Terminal GUI. - * Provides a text field overlay that appears on right-click, with keyboard navigation, - * Enter to confirm, and Escape to cancel. - *

- * Used by all tabs to rename drives/ME chests, cells, storage buses, and subnets. - */ -public class InlineRenameEditor { - - // The renameable target currently being edited (null if not editing) - private Renameable editingTarget = null; - private String editingText = ""; - private int editingCursorPos = 0; - private int editingY = 0; - private int editingX = GuiConstants.GUI_INDENT + 20; // X position of the text field (varies by target) - private int editingRightEdge = GuiConstants.CONTENT_RIGHT_EDGE - 4; // Right edge of the text field (varies by target) - - // TODO: max length should probably be determined by available space rather than a fixed count - // Maximum length for the rename text - private static final int MAX_NAME_LENGTH = 50; - - // Layout constants for the text field - private static final int TEXT_FIELD_HEIGHT = 9; // Reduced height to align with text - - /** - * Check if currently editing a name. - */ - public boolean isEditing() { - return editingTarget != null; - } - - /** - * Get the target being edited. - */ - public Renameable getEditingTarget() { - return editingTarget; - } - - /** - * Start editing a target's name. - * @param target The renameable target - * @param y The Y position where the edit field should appear - * @param x The X position where the text field starts (after the icon) - * @param rightEdge The right edge of the text field (before any buttons) - */ - public void startEditing(Renameable target, int y, int x, int rightEdge) { - if (target == null || !target.isRenameable()) return; - - this.editingTarget = target; - this.editingText = target.hasCustomName() ? target.getCustomName() : ""; - this.editingCursorPos = editingText.length(); - this.editingY = y; - this.editingX = x; - this.editingRightEdge = rightEdge; - } - - /** - * Stop editing and return the edited text. - * @return The edited text, or null if not editing - */ - public String stopEditing() { - if (editingTarget == null) return null; - - String result = editingText.trim(); - editingTarget = null; - editingText = ""; - editingCursorPos = 0; - - return result; - } - - /** - * Cancel editing without saving. - */ - public void cancelEditing() { - editingTarget = null; - editingText = ""; - editingCursorPos = 0; - } - - /** - * Get the current editing text. - */ - public String getEditingText() { - return editingText; - } - - /** - * Handle keyboard input for rename editing. - * @param typedChar The character typed - * @param keyCode The key code - * @return true if the input was handled (return false for Enter so GUI handles confirmation) - */ - public boolean handleKeyTyped(char typedChar, int keyCode) { - if (editingTarget == null) return false; - - // Enter - return false to let GUI handle confirmation - if (keyCode == Keyboard.KEY_RETURN) return false; - - if (keyCode == Keyboard.KEY_ESCAPE) { - cancelEditing(); - return true; - } - - if (keyCode == Keyboard.KEY_BACK && editingCursorPos > 0) { - editingText = editingText.substring(0, editingCursorPos - 1) - + editingText.substring(editingCursorPos); - editingCursorPos--; - return true; - } - - if (keyCode == Keyboard.KEY_DELETE && editingCursorPos < editingText.length()) { - editingText = editingText.substring(0, editingCursorPos) - + editingText.substring(editingCursorPos + 1); - return true; - } - - if (keyCode == Keyboard.KEY_LEFT && editingCursorPos > 0) { - editingCursorPos--; - return true; - } - - if (keyCode == Keyboard.KEY_RIGHT && editingCursorPos < editingText.length()) { - editingCursorPos++; - return true; - } - - if (keyCode == Keyboard.KEY_HOME) { - editingCursorPos = 0; - return true; - } - - if (keyCode == Keyboard.KEY_END) { - editingCursorPos = editingText.length(); - return true; - } - - // Printable characters - if (typedChar >= 32 && typedChar < 127 && editingText.length() < MAX_NAME_LENGTH) { - editingText = editingText.substring(0, editingCursorPos) - + typedChar - + editingText.substring(editingCursorPos); - editingCursorPos++; - return true; - } - - return false; - } - - /** - * Draw the rename text field overlay if editing. - * Call this after drawing the main content. - * @param fontRenderer The font renderer - */ - public void drawRenameField(FontRenderer fontRenderer) { - if (editingTarget == null) return; - - int x = editingX; - int y = editingY + 1; // Vertically aligned with text (y + 1 is where text draws) - int width = editingRightEdge - editingX; - int height = TEXT_FIELD_HEIGHT; - - // Draw background (E0E0E0 to match terminal background, with dark border) - Gui.drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xFF373737); - Gui.drawRect(x, y, x + width, y + height, 0xFFE0E0E0); - - // Draw text with scrolling if too long - String displayText = editingText; - int textWidth = fontRenderer.getStringWidth(displayText); - int visibleWidth = width - 4; - int textX = x + 2; - - if (textWidth > visibleWidth) { - int cursorX = fontRenderer.getStringWidth(displayText.substring(0, editingCursorPos)); - int scrollOffset = Math.max(0, cursorX - visibleWidth + 10); - textX -= scrollOffset; - } - - fontRenderer.drawString(displayText, textX, y, 0xFF000000); - - // Draw blinking cursor - long time = System.currentTimeMillis(); - if ((time / 500) % 2 == 0) { - int cursorX = x + 2 + fontRenderer.getStringWidth(displayText.substring(0, editingCursorPos)); - Gui.drawRect(cursorX, y, cursorX + 1, y + height - 1, 0xFF000000); - } - } - - /** - * Get the Y position of the rename field. - */ - public int getEditingY() { - return editingY; - } -} diff --git a/src/main/java/com/cellterminal/gui/rename/InlineRenameManager.java b/src/main/java/com/cellterminal/gui/rename/InlineRenameManager.java new file mode 100644 index 0000000..b510b6b --- /dev/null +++ b/src/main/java/com/cellterminal/gui/rename/InlineRenameManager.java @@ -0,0 +1,284 @@ +package com.cellterminal.gui.rename; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; + +import org.lwjgl.input.Keyboard; + +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.network.CellTerminalNetwork; +import com.cellterminal.network.PacketRenameAction; +import com.cellterminal.network.PacketSubnetAction; + + +/** + * Singleton manager for inline rename editing in the Cell Terminal GUI. + *

+ * Handles the full lifecycle of rename sessions: start, draw, key events, click-outside, + * and sending rename packets directly via {@link CellTerminalNetwork}. + * Individual widgets (headers, lines) trigger rename by calling {@link #startEditing} directly + * when they receive a right-click on a renameable name area. The manager globally tracks + * the single active rename session and draws the text field overlay. + * + *

Usage flow

+ *
    + *
  1. A header/line widget detects right-click on a name → calls {@link #startEditing}
  2. + *
  3. GUI calls {@link #handleKey} in keyTyped (consumes all input while editing)
  4. + *
  5. GUI calls {@link #handleClickOutside} at the top of mouseClicked (saves on click-outside)
  6. + *
  7. GUI calls {@link #drawRenameField} after widget drawing (overlay on top)
  8. + *
+ */ +public class InlineRenameManager { + + private static final InlineRenameManager INSTANCE = new InlineRenameManager(); + + public static InlineRenameManager getInstance() { + return INSTANCE; + } + + // The renameable target currently being edited (null if not editing) + private Renameable editingTarget = null; + private String editingText = ""; + private int editingCursorPos = 0; + private int editingY = 0; + private int editingX = GuiConstants.GUI_INDENT + 20; + private int editingRightEdge = GuiConstants.CONTENT_RIGHT_EDGE - 4; + + // TODO: max length should probably be determined by available space rather than a fixed count + // Maximum length for the rename text + private static final int MAX_NAME_LENGTH = 50; + + // Layout constants for the text field + private static final int TEXT_FIELD_HEIGHT = 9; + + private InlineRenameManager() {} + + /** + * Check if currently editing a name. + */ + public boolean isEditing() { + return editingTarget != null; + } + + /** + * Get the target being edited. + */ + public Renameable getEditingTarget() { + return editingTarget; + } + + /** + * Start editing a target's name. + * If already editing the same target, does nothing (avoids flicker on re-click). + * If editing a different target, confirms the current rename first. + * + * @param target The renameable target + * @param y The Y position where the edit field should appear + * @param x The X position where the text field starts + * @param rightEdge The right edge of the text field + */ + public void startEditing(Renameable target, int y, int x, int rightEdge) { + if (target == null || !target.isRenameable()) return; + + // Already editing this exact target, do nothing + if (isEditing() && isSameTarget(editingTarget, target)) return; + + // Editing a different target, confirm the previous one first + if (isEditing()) confirmEditing(); + + this.editingTarget = target; + this.editingText = target.hasCustomName() ? target.getCustomName() : ""; + this.editingCursorPos = editingText.length(); + this.editingY = y; + this.editingX = x; + this.editingRightEdge = rightEdge; + } + + /** + * Confirm the current editing session: send the rename packet and update the local name. + */ + private void confirmEditing() { + if (editingTarget == null) return; + + Renameable target = editingTarget; + String newName = editingText.trim(); + clearState(); + + switch (target.getRenameTargetType()) { + case STORAGE: + CellTerminalNetwork.INSTANCE.sendToServer( + PacketRenameAction.renameStorage(target.getRenameId(), newName)); + break; + case CELL: + CellTerminalNetwork.INSTANCE.sendToServer( + PacketRenameAction.renameCell(target.getRenameId(), target.getRenameSecondaryId(), newName)); + break; + case STORAGE_BUS: + CellTerminalNetwork.INSTANCE.sendToServer( + PacketRenameAction.renameStorageBus(target.getRenameId(), newName)); + break; + case SUBNET: + CellTerminalNetwork.INSTANCE.sendToServer( + PacketSubnetAction.rename(target.getRenameId(), newName)); + break; + default: + break; + } + + // Update local name immediately for responsiveness + target.setCustomName(newName.isEmpty() ? null : newName); + } + + /** + * Cancel editing without saving. + */ + public void cancelEditing() { + clearState(); + } + + private void clearState() { + editingTarget = null; + editingText = ""; + editingCursorPos = 0; + } + + /** + * Handle keyboard input for rename editing. + * Handles all keys internally including Esc (cancel) and Enter (confirm). + * + * @param typedChar The character typed + * @param keyCode The key code + * @return true if the input was consumed (editing is active) + */ + public boolean handleKey(char typedChar, int keyCode) { + if (editingTarget == null) return false; + + if (keyCode == Keyboard.KEY_ESCAPE) { + cancelEditing(); + return true; + } + + if (keyCode == Keyboard.KEY_RETURN || keyCode == Keyboard.KEY_NUMPADENTER) { + confirmEditing(); + return true; + } + + if (keyCode == Keyboard.KEY_BACK && editingCursorPos > 0) { + editingText = editingText.substring(0, editingCursorPos - 1) + + editingText.substring(editingCursorPos); + editingCursorPos--; + return true; + } + + if (keyCode == Keyboard.KEY_DELETE && editingCursorPos < editingText.length()) { + editingText = editingText.substring(0, editingCursorPos) + + editingText.substring(editingCursorPos + 1); + return true; + } + + if (keyCode == Keyboard.KEY_LEFT && editingCursorPos > 0) { + editingCursorPos--; + return true; + } + + if (keyCode == Keyboard.KEY_RIGHT && editingCursorPos < editingText.length()) { + editingCursorPos++; + return true; + } + + if (keyCode == Keyboard.KEY_HOME) { + editingCursorPos = 0; + return true; + } + + if (keyCode == Keyboard.KEY_END) { + editingCursorPos = editingText.length(); + return true; + } + + // Printable characters + if (typedChar >= 32 && typedChar < 127 && editingText.length() < MAX_NAME_LENGTH) { + editingText = editingText.substring(0, editingCursorPos) + + typedChar + + editingText.substring(editingCursorPos); + editingCursorPos++; + return true; + } + + // Consume all input while editing (don't let keys leak to other handlers) + return true; + } + + /** + * Handle a click-outside event. Call at the top of mouseClicked, before any + * other click processing. If editing is active and the click is outside the + * rename field, confirms the current session. + *

+ * Does NOT consume the click, letting it propagate so it may start a new rename + * or interact with other GUI elements. + */ + public void handleClickOutside(int mouseX, int mouseY) { + if (!isEditing()) return; + + // Click inside the rename field: keep editing + int fieldWidth = editingRightEdge - editingX; + int fieldY = editingY + 1; + if (mouseX >= editingX && mouseX < editingX + fieldWidth + && mouseY >= fieldY && mouseY < fieldY + TEXT_FIELD_HEIGHT) { + return; + } + + // Click outside: confirm and save + confirmEditing(); + } + + /** + * Draw the rename text field overlay if editing. + * Call after drawing all widget content so the field appears on top. + */ + public void drawRenameField(FontRenderer fontRenderer) { + if (editingTarget == null) return; + + int x = editingX; + int y = editingY + 1; // Vertically aligned with text (y + 1 is where text draws) + int width = editingRightEdge - editingX; + int height = TEXT_FIELD_HEIGHT; + + // Draw background with dark border + Gui.drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xFF373737); + Gui.drawRect(x, y, x + width, y + height, 0xFFE0E0E0); + + // Draw text with scrolling if too long + String displayText = editingText; + int textWidth = fontRenderer.getStringWidth(displayText); + int visibleWidth = width - 4; + int textX = x + 2; + + if (textWidth > visibleWidth) { + int cursorX = fontRenderer.getStringWidth(displayText.substring(0, editingCursorPos)); + int scrollOffset = Math.max(0, cursorX - visibleWidth + 10); + textX -= scrollOffset; + } + + fontRenderer.drawString(displayText, textX, y, 0xFF000000); + + // Draw blinking cursor + long time = System.currentTimeMillis(); + if ((time / 500) % 2 == 0) { + int cursorX = x + 2 + fontRenderer.getStringWidth(displayText.substring(0, editingCursorPos)); + Gui.drawRect(cursorX, y, cursorX + 1, y + height - 1, 0xFF000000); + } + } + + /** + * Check if two renameable targets refer to the same entity. + */ + private static boolean isSameTarget(Renameable a, Renameable b) { + if (a == b) return true; + if (a == null || b == null) return false; + + return a.getRenameTargetType() == b.getRenameTargetType() + && a.getRenameId() == b.getRenameId() + && a.getRenameSecondaryId() == b.getRenameSecondaryId(); + } +} diff --git a/src/main/java/com/cellterminal/gui/render/CellTerminalRenderer.java b/src/main/java/com/cellterminal/gui/render/CellTerminalRenderer.java deleted file mode 100644 index 54e8505..0000000 --- a/src/main/java/com/cellterminal/gui/render/CellTerminalRenderer.java +++ /dev/null @@ -1,304 +0,0 @@ -package com.cellterminal.gui.render; - -import java.util.List; - -import org.lwjgl.opengl.GL11; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.client.renderer.RenderItem; -import net.minecraft.item.ItemStack; - -import appeng.util.ReadableNumberConverter; - -import com.cellterminal.client.CellContentRow; -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.EmptySlotInfo; -import com.cellterminal.client.StorageInfo; -import com.cellterminal.gui.ComparisonUtils; -import com.cellterminal.gui.GuiConstants; - - -/** - * Base renderer class for Cell Terminal tabs. - * Provides common drawing utilities shared across all tab renderers. - */ -public abstract class CellTerminalRenderer { - - protected static final int ROW_HEIGHT = GuiConstants.ROW_HEIGHT; - protected static final int GUI_INDENT = GuiConstants.GUI_INDENT; - protected static final int CELL_INDENT = GuiConstants.CELL_INDENT; - protected static final int SLOTS_PER_ROW = 8; - protected static final int SLOTS_PER_ROW_BUS = 9; - - protected final FontRenderer fontRenderer; - protected final RenderItem itemRender; - - public CellTerminalRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.fontRenderer = fontRenderer; - this.itemRender = itemRender; - } - - /** - * Draw a standard slot background. - */ - protected void drawSlotBackground(int x, int y) { - int size = GuiConstants.MINI_SLOT_SIZE; - Gui.drawRect(x, y, x + size, y + size, GuiConstants.COLOR_SLOT_BACKGROUND); - Gui.drawRect(x, y, x + size - 1, y + 1, GuiConstants.COLOR_SLOT_BORDER_DARK); - Gui.drawRect(x, y, x + 1, y + size - 1, GuiConstants.COLOR_SLOT_BORDER_DARK); - Gui.drawRect(x + 1, y + size - 1, x + size, y + size, GuiConstants.COLOR_SLOT_BORDER_LIGHT); - Gui.drawRect(x + size - 1, y + 1, x + size, y + size - 1, GuiConstants.COLOR_SLOT_BORDER_LIGHT); - } - - /** - * Draw a button with 3D effect. - */ - protected void drawButton(int x, int y, int size, String label, boolean hovered) { - int btnColor = hovered ? GuiConstants.COLOR_BUTTON_HOVER : GuiConstants.COLOR_BUTTON_NORMAL; - Gui.drawRect(x, y, x + size, y + size, btnColor); - Gui.drawRect(x, y, x + size, y + 1, GuiConstants.COLOR_SLOT_BORDER_LIGHT); - Gui.drawRect(x, y, x + 1, y + size, GuiConstants.COLOR_SLOT_BORDER_LIGHT); - Gui.drawRect(x, y + size - 1, x + size, y + size, GuiConstants.COLOR_BUTTON_SHADOW); - Gui.drawRect(x + size - 1, y, x + size, y + size, GuiConstants.COLOR_BUTTON_SHADOW); - fontRenderer.drawString(label, x + 4, y + 3, GuiConstants.COLOR_TEXT_NORMAL); - } - - /** - * Format item count for display. - */ - protected String formatItemCount(long count) { - if (count < 1000) return String.valueOf(count); - - return ReadableNumberConverter.INSTANCE.toWideReadableForm(count); - } - - /** - * Get the usage bar color based on percentage. - */ - protected int getUsageColor(float percent) { - if (percent > 0.9f) return 0xFFFF3333; - if (percent > 0.75f) return 0xFFFFAA00; - - return 0xFF33FF33; - } - - /** - * Get the number of formatting code characters (§X pairs) in a string. - */ - protected int getDecorationLength(String name) { - int decorLength = 0; - - for (int i = 0; i < name.length() - 1; i++) { - if (name.charAt(i) == '§') { - decorLength += 2; - i++; - } - } - - return decorLength; - } - - /** - * Render an item stack at the given position. - */ - protected void renderItemStack(ItemStack stack, int x, int y) { - if (stack.isEmpty()) return; - - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - RenderHelper.enableGUIStandardItemLighting(); - itemRender.renderItemIntoGUI(stack, x, y); - RenderHelper.disableStandardItemLighting(); - GlStateManager.disableLighting(); - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - GlStateManager.enableBlend(); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - } - - /** - * Render a small (8x8) item icon at the given position. - */ - protected void renderSmallItemStack(ItemStack stack, int x, int y) { - if (stack.isEmpty()) return; - - GlStateManager.pushMatrix(); - GlStateManager.translate(x, y, 0); - GlStateManager.scale(0.5f, 0.5f, 1.0f); - - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - RenderHelper.enableGUIStandardItemLighting(); - itemRender.renderItemIntoGUI(stack, 0, 0); - RenderHelper.disableStandardItemLighting(); - GlStateManager.disableLighting(); - - GlStateManager.popMatrix(); - - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - GlStateManager.enableBlend(); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - } - - /** - * Draw upgrade icons for a cell. - * Icons are drawn at the left side of the cell entry, using small 8x8 icons. - * @param cell The cell info - * @param x The x position to start drawing (left edge) - * @param y The y position of the row - * @return The width consumed by upgrade icons - */ - protected int drawCellUpgradeIcons(CellInfo cell, int x, int y) { - return drawCellUpgradeIcons(cell, x, y, null); - } - - /** - * Draw upgrade icons for a cell with hover tracking. - * Icons are drawn at the left side of the cell entry, using small 8x8 icons. - * @param cell The cell info - * @param x The x position to start drawing (relative to GUI) - * @param y The y position of the row (relative to GUI) - * @param ctx Optional render context for tracking upgrade icon positions - * @return The width consumed by upgrade icons - */ - protected int drawCellUpgradeIcons(CellInfo cell, int x, int y, RenderContext ctx) { - List upgrades = cell.getUpgrades(); - if (upgrades.isEmpty()) return 0; - - int iconY = y; // Align with top of cell icon - - // Find max slot index to determine layout width - int maxSlot = 0; - for (int i = 0; i < upgrades.size(); i++) { - int slotIndex = cell.getUpgradeSlotIndex(i); - if (slotIndex > maxSlot) maxSlot = slotIndex; - } - - // Render each upgrade at its actual slot position - for (int i = 0; i < upgrades.size(); i++) { - ItemStack upgrade = upgrades.get(i); - int actualSlotIndex = cell.getUpgradeSlotIndex(i); - int iconX = x + actualSlotIndex * 9; // 8px icon + 1px spacing per slot - - renderSmallItemStack(upgrade, iconX, iconY); - - // Track upgrade icon position for tooltip and click handling - if (ctx != null) { - ctx.upgradeIconTargets.add(new RenderContext.UpgradeIconTarget( - cell, upgrade, actualSlotIndex, ctx.guiLeft + iconX, ctx.guiTop + iconY)); - } - } - - return (maxSlot + 1) * 9; // Total width based on max slot position - } - - /** - * Check if the line at the given index is the first in its storage group. - */ - protected boolean isFirstInStorageGroup(List lines, int index) { - if (index <= 0) return true; - - return lines.get(index - 1) instanceof StorageInfo; - } - - /** - * Check if the line at the given index is the last in its storage group. - * For multi-row cells, this returns true for ALL rows of the last cell. - */ - protected boolean isLastInStorageGroup(List lines, int index) { - if (index >= lines.size() - 1) return true; - - // Look ahead to find if there are any more cells after all rows of current cell - for (int i = index + 1; i < lines.size(); i++) { - Object line = lines.get(i); - - // Hit the next storage, so current cell is last in group - if (line instanceof StorageInfo) return true; - - if (line instanceof CellContentRow) { - CellContentRow row = (CellContentRow) line; - // If this is a first row, it's a different cell - current is last - if (row.isFirstRow()) return false; - // Otherwise it's a continuation row of the same cell, keep looking - } else if (line instanceof EmptySlotInfo) { - // Empty slot is a different entry - current is not last - return false; - } - } - - // Reached end of list - return true; - } - - /** - * Draw tree lines connecting cells to their storage parent. - * @param lineX The x position of the vertical line - * @param y The y position of this row - * @param isFirstRow Whether this is the first row for this cell - * @param isFirstInGroup Whether this is the first cell in the storage group - * @param isLastInGroup Whether this is the last cell in the storage group - * @param visibleTop The top y of the visible area - * @param visibleBottom The bottom y of the visible area - * @param isFirstVisibleRow Whether this is the first visible row - * @param isLastVisibleRow Whether this is the last visible row - * @param hasContentAbove Whether there's content above that's scrolled out - * @param hasContentBelow Whether there's content below that's scrolled out - * @param allBranches Whether to draw a horizontal branch for every row (not just first) - */ - protected void drawTreeLines(int lineX, int y, boolean isFirstRow, boolean isFirstInGroup, - boolean isLastInGroup, int visibleTop, int visibleBottom, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, - boolean allBranches) { - - int lineTop; - if (isFirstRow) { - if (isFirstInGroup) { - // First row in group (right after header) - extend up to connect with header's segment - lineTop = y - 3; - } else if (isFirstVisibleRow && hasContentAbove) { - // First visible row with content above scrolled out - - // clamp to visibleTop to avoid leaking above GUI - lineTop = visibleTop; - } else { - // Connect to row above but don't extend too high (avoid overlapping buttons/icons) - lineTop = y - 4; - } - } else { - // Connect to row above with minimal overlap - lineTop = isFirstVisibleRow && hasContentAbove ? visibleTop : y - 4; - } - - // Clamp lineTop to never go above visibleTop to prevent leak above GUI - if (lineTop < visibleTop) lineTop = visibleTop; - - int lineBottom; - if (isLastInGroup) { - lineBottom = y + 9; - } else if (isLastVisibleRow && hasContentBelow) { - lineBottom = visibleBottom; - } else { - lineBottom = y + ROW_HEIGHT; - } - - // Vertical line - Gui.drawRect(lineX, lineTop, lineX + 1, lineBottom, 0xFF808080); - - // Horizontal branch (only on first row unless all branches are enabled) - if (allBranches) { - Gui.drawRect(lineX, y + 8, lineX + 10, y + 9, 0xFF808080); - } else if (isFirstRow) { - Gui.drawRect(lineX, y + 8, lineX + 10, y + 9, 0xFF808080); - } - } - - /** - * Check if an item is in the partition list. - * Uses fluid-aware comparison for fluid items (compares by fluid type only). - * Uses areItemStacksEqual for other items (important for essentia items - * where the aspect type is stored in NBT). - */ - protected boolean isInPartition(ItemStack stack, List partition) { - return ComparisonUtils.isInPartition(stack, partition); - } -} diff --git a/src/main/java/com/cellterminal/gui/render/InventoryTabRenderer.java b/src/main/java/com/cellterminal/gui/render/InventoryTabRenderer.java deleted file mode 100644 index 31587ec..0000000 --- a/src/main/java/com/cellterminal/gui/render/InventoryTabRenderer.java +++ /dev/null @@ -1,172 +0,0 @@ -package com.cellterminal.gui.render; - -import java.util.List; -import java.util.Map; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.RenderItem; - -import com.cellterminal.client.CellContentRow; -import com.cellterminal.client.EmptySlotInfo; -import com.cellterminal.client.StorageInfo; -import com.cellterminal.client.TabStateManager; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.cells.CellRenderer; - - -/** - * Renderer for the Inventory tab (Tab 1). - * Displays cells as expandable rows with their contents shown in a grid. - * Content items show "P" indicator if they're in the cell's partition. - *

- * This renderer delegates actual cell rendering to {@link CellRenderer}. - * It handles the overall tab layout and line iteration. - * - * @see CellRenderer - */ -public class InventoryTabRenderer { - - private final CellRenderer cellRenderer; - - public InventoryTabRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.cellRenderer = new CellRenderer(fontRenderer, itemRender); - } - - /** - * Draw the inventory tab content. - * - * @param inventoryLines List of line objects (StorageInfo, CellContentRow, EmptySlotInfo) - * @param currentScroll Current scroll position - * @param rowsVisible Number of visible rows - * @param relMouseX Mouse X relative to GUI - * @param relMouseY Mouse Y relative to GUI - * @param absMouseX Absolute mouse X (for tooltips) - * @param absMouseY Absolute mouse Y (for tooltips) - * @param storageMap Map of storage IDs to StorageInfo - * @param ctx Render context for hover tracking - */ - public void draw(List inventoryLines, int currentScroll, int rowsVisible, - int relMouseX, int relMouseY, int absMouseX, int absMouseY, - Map storageMap, RenderContext ctx) { - - int y = GuiConstants.CONTENT_START_Y; - int visibleTop = GuiConstants.CONTENT_START_Y; - int visibleBottom = GuiConstants.CONTENT_START_Y + rowsVisible * GuiConstants.ROW_HEIGHT; - int totalLines = inventoryLines.size(); - - for (int i = 0; i < rowsVisible && currentScroll + i < totalLines; i++) { - Object line = inventoryLines.get(currentScroll + i); - int lineIndex = currentScroll + i; - - boolean isHovered = relMouseX >= GuiConstants.HOVER_LEFT_EDGE && relMouseX < GuiConstants.HOVER_RIGHT_EDGE - && relMouseY >= y && relMouseY < y + GuiConstants.ROW_HEIGHT; - - // Draw hover highlight and track state - if (isHovered) handleLineHover(line, lineIndex, y, ctx); - - // Draw separator line above storage entries - if (line instanceof StorageInfo && i > 0) { - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y, GuiConstants.COLOR_SEPARATOR); - } - - // Calculate tree line parameters - LineContext lineCtx = buildLineContext(inventoryLines, lineIndex, i, rowsVisible, totalLines); - - // Render the line - if (line instanceof StorageInfo) { - cellRenderer.drawStorageHeader((StorageInfo) line, y, inventoryLines, lineIndex, - TabStateManager.TabType.INVENTORY, ctx); - } else if (line instanceof CellContentRow) { - CellContentRow row = (CellContentRow) line; - cellRenderer.drawCellInventoryLine( - row.getCell(), row.getStartIndex(), row.isFirstRow(), - y, relMouseX, relMouseY, absMouseX, absMouseY, - lineCtx.isFirstInGroup, lineCtx.isLastInGroup, visibleTop, visibleBottom, - lineCtx.isFirstVisibleRow, lineCtx.isLastVisibleRow, - lineCtx.hasContentAbove, lineCtx.hasContentBelow, - storageMap, ctx); - } else if (line instanceof EmptySlotInfo) { - cellRenderer.drawEmptySlotLine( - (EmptySlotInfo) line, y, relMouseX, relMouseY, - lineCtx.isFirstInGroup, lineCtx.isLastInGroup, visibleTop, visibleBottom, - lineCtx.isFirstVisibleRow, lineCtx.isLastVisibleRow, - lineCtx.hasContentAbove, lineCtx.hasContentBelow, - storageMap, ctx); - } - - y += GuiConstants.ROW_HEIGHT; - } - } - - private void handleLineHover(Object line, int lineIndex, int y, RenderContext ctx) { - if (line instanceof CellContentRow) { - ctx.hoveredLineIndex = lineIndex; - ctx.hoveredCellCell = ((CellContentRow) line).getCell(); - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT - 1, GuiConstants.COLOR_ROW_HOVER); - } else if (line instanceof EmptySlotInfo) { - ctx.hoveredLineIndex = lineIndex; - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT - 1, GuiConstants.COLOR_ROW_HOVER); - } else if (line instanceof StorageInfo) { - ctx.hoveredStorageLine = (StorageInfo) line; - ctx.hoveredLineIndex = lineIndex; - Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_STORAGE_HEADER_HOVER); - } - } - - private LineContext buildLineContext(List lines, int lineIndex, int visibleIndex, - int rowsVisible, int totalLines) { - LineContext ctx = new LineContext(); - ctx.isFirstInGroup = isFirstInStorageGroup(lines, lineIndex); - ctx.isLastInGroup = isLastInStorageGroup(lines, lineIndex); - ctx.isFirstVisibleRow = (visibleIndex == 0); - ctx.isLastVisibleRow = (visibleIndex == rowsVisible - 1) || (lineIndex == totalLines - 1); - ctx.hasContentAbove = (lineIndex > 0) && !ctx.isFirstInGroup; - ctx.hasContentBelow = (lineIndex < totalLines - 1) && !ctx.isLastInGroup; - - return ctx; - } - - /** - * Check if the line at the given index is the first in its storage group. - */ - private boolean isFirstInStorageGroup(List lines, int index) { - if (index <= 0) return true; - - return lines.get(index - 1) instanceof StorageInfo; - } - - /** - * Check if the line at the given index is the last in its storage group. - */ - private boolean isLastInStorageGroup(List lines, int index) { - if (index >= lines.size() - 1) return true; - - // Look ahead to find if there are any more cells after all rows of current cell - for (int i = index + 1; i < lines.size(); i++) { - Object line = lines.get(i); - if (line instanceof StorageInfo) return true; - - if (line instanceof CellContentRow) { - CellContentRow row = (CellContentRow) line; - if (row.isFirstRow()) return false; - } else if (line instanceof EmptySlotInfo) { - return false; - } - } - - return true; - } - - /** - * Context for line rendering parameters. - */ - private static class LineContext { - boolean isFirstInGroup; - boolean isLastInGroup; - boolean isFirstVisibleRow; - boolean isLastVisibleRow; - boolean hasContentAbove; - boolean hasContentBelow; - } -} diff --git a/src/main/java/com/cellterminal/gui/render/NetworkToolsTabRenderer.java b/src/main/java/com/cellterminal/gui/render/NetworkToolsTabRenderer.java deleted file mode 100644 index 942c965..0000000 --- a/src/main/java/com/cellterminal/gui/render/NetworkToolsTabRenderer.java +++ /dev/null @@ -1,243 +0,0 @@ -package com.cellterminal.gui.render; - -import java.util.List; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.client.renderer.RenderItem; - -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.networktools.INetworkTool; -import com.cellterminal.gui.networktools.INetworkTool.ToolContext; -import com.cellterminal.gui.networktools.INetworkTool.ToolPreviewInfo; -import com.cellterminal.gui.networktools.NetworkToolRegistry; - - -/** - * Renderer for the Network Tools tab (Tab 5). - * Displays a scrollable list of network tools with preview information. - */ -public class NetworkToolsTabRenderer { - - private static final int TOOL_ROW_HEIGHT = 36; - private static final int TOOL_PADDING = 4; - private static final int ICON_SIZE = 16; - private static final int RUN_BUTTON_SIZE = 16; - private static final int HELP_BUTTON_SIZE = 10; - private static final int HELP_BUTTON_Y_OFFSET = 3; - - private final FontRenderer fontRenderer; - private final RenderItem itemRender; - - // Hover tracking - private int hoveredToolIndex = -1; - private boolean launchButtonHovered = false; - private boolean helpButtonHovered = false; - - public NetworkToolsTabRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.fontRenderer = fontRenderer; - this.itemRender = itemRender; - } - - /** - * Draw the Network Tools tab content. - */ - public void draw(int currentScroll, int rowsVisible, - int relMouseX, int relMouseY, - ToolContext toolContext, - RenderContext ctx) { - - // Reset hover state - hoveredToolIndex = -1; - launchButtonHovered = false; - helpButtonHovered = false; - ctx.hoveredNetworkTool = null; - ctx.hoveredNetworkToolLaunchButton = null; - ctx.hoveredNetworkToolHelpButton = null; - ctx.hoveredNetworkToolPreview = null; - - List tools = NetworkToolRegistry.getAllTools(); - int contentY = GuiConstants.CONTENT_START_Y; - int contentHeight = rowsVisible * GuiConstants.ROW_HEIGHT; - - // Calculate visible tools - int toolsPerPage = contentHeight / TOOL_ROW_HEIGHT; - int startIndex = currentScroll; - int endIndex = Math.min(startIndex + toolsPerPage + 1, tools.size()); - - int y = contentY; - for (int i = startIndex; i < endIndex; i++) { - INetworkTool tool = tools.get(i); - int toolY = y; - int toolHeight = TOOL_ROW_HEIGHT; - - // Skip if completely outside visible area - if (toolY + toolHeight < contentY || toolY > contentY + contentHeight) { - y += TOOL_ROW_HEIGHT; - continue; - } - - drawToolRow(tool, i, toolY, relMouseX, relMouseY, toolContext, ctx); - y += TOOL_ROW_HEIGHT; - } - } - - private void drawToolRow(INetworkTool tool, int index, int y, - int relMouseX, int relMouseY, - ToolContext toolContext, RenderContext ctx) { - - int x = GuiConstants.GUI_INDENT; - int width = GuiConstants.CONTENT_RIGHT_EDGE - GuiConstants.GUI_INDENT; - - // Check if mouse is over this row - boolean rowHovered = relMouseX >= x && relMouseX < x + width && - relMouseY >= y && relMouseY < y + TOOL_ROW_HEIGHT; - - // Draw row background - int bgColor = rowHovered ? 0x30FFFFFF : 0x20FFFFFF; - Gui.drawRect(x, y, x + width, y + TOOL_ROW_HEIGHT - 1, bgColor); - - // Draw separator line - Gui.drawRect(x, y + TOOL_ROW_HEIGHT - 1, x + width, y + TOOL_ROW_HEIGHT, GuiConstants.COLOR_SEPARATOR); - - // Get preview info - ToolPreviewInfo preview = tool.getPreview(toolContext); - - // Draw help button (?) at the start of the row - int helpButtonX = x + TOOL_PADDING; - int helpButtonY = y + TOOL_PADDING + HELP_BUTTON_Y_OFFSET; - boolean helpHovered = relMouseX >= helpButtonX && relMouseX < helpButtonX + HELP_BUTTON_SIZE && - relMouseY >= helpButtonY && relMouseY < helpButtonY + HELP_BUTTON_SIZE; - - int helpBgColor = helpHovered ? 0xFF505050 : 0xFF606060; - Gui.drawRect(helpButtonX, helpButtonY, helpButtonX + HELP_BUTTON_SIZE, helpButtonY + HELP_BUTTON_SIZE, helpBgColor); - // 3D border like GuiSearchHelpButton - Gui.drawRect(helpButtonX, helpButtonY, helpButtonX + HELP_BUTTON_SIZE, helpButtonY + 1, 0xFF808080); - Gui.drawRect(helpButtonX, helpButtonY, helpButtonX + 1, helpButtonY + HELP_BUTTON_SIZE, 0xFF808080); - Gui.drawRect(helpButtonX, helpButtonY + HELP_BUTTON_SIZE - 1, helpButtonX + HELP_BUTTON_SIZE, helpButtonY + HELP_BUTTON_SIZE, 0xFF303030); - Gui.drawRect(helpButtonX + HELP_BUTTON_SIZE - 1, helpButtonY, helpButtonX + HELP_BUTTON_SIZE, helpButtonY + HELP_BUTTON_SIZE, 0xFF303030); - - String helpText = "?"; - int helpTextX = helpButtonX + (HELP_BUTTON_SIZE - fontRenderer.getStringWidth(helpText)) / 2; - int helpTextY = helpButtonY + (HELP_BUTTON_SIZE - fontRenderer.FONT_HEIGHT) / 2 + 1; - int helpTextColor = helpHovered ? 0xFFFF00 : 0xCCCCCC; - fontRenderer.drawString(helpText, helpTextX, helpTextY, helpTextColor); - - // Draw tool icon - int iconX = x + TOOL_PADDING + HELP_BUTTON_SIZE + 4; - int iconY = y + TOOL_PADDING; - if (!preview.getIcon().isEmpty()) { - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - RenderHelper.enableGUIStandardItemLighting(); - itemRender.renderItemIntoGUI(preview.getIcon(), iconX, iconY); - RenderHelper.disableStandardItemLighting(); - GlStateManager.disableLighting(); - } - - // Draw preview count after icon (TODO: just delegate the rendering to preview) - String countText = preview.getCountText(); - int countColor = preview.getCountColor(); - int countX = iconX + ICON_SIZE + 4; - int countY = iconY + (ICON_SIZE - fontRenderer.FONT_HEIGHT) / 2; - fontRenderer.drawString(countText, countX, countY, countColor); - - // Draw run button (square with arrow) at far right - int runButtonX = x + width - RUN_BUTTON_SIZE - TOOL_PADDING; - int runButtonY = y + TOOL_PADDING; - - String executionError = tool.getExecutionError(toolContext); - boolean canExecute = executionError == null; - - boolean runHovered = relMouseX >= runButtonX && relMouseX < runButtonX + RUN_BUTTON_SIZE && - relMouseY >= runButtonY && relMouseY < runButtonY + RUN_BUTTON_SIZE; - - int buttonBgColor; - if (!canExecute) { - buttonBgColor = 0xFF404040; - } else if (runHovered) { - buttonBgColor = 0xFF40A040; - } else { - buttonBgColor = 0xFF308030; - } - - Gui.drawRect(runButtonX, runButtonY, runButtonX + RUN_BUTTON_SIZE, runButtonY + RUN_BUTTON_SIZE, buttonBgColor); - // 3D button border - Gui.drawRect(runButtonX, runButtonY, runButtonX + RUN_BUTTON_SIZE, runButtonY + 1, - canExecute ? 0xFF60C060 : 0xFF606060); - Gui.drawRect(runButtonX, runButtonY, runButtonX + 1, runButtonY + RUN_BUTTON_SIZE, - canExecute ? 0xFF60C060 : 0xFF606060); - Gui.drawRect(runButtonX + RUN_BUTTON_SIZE - 1, runButtonY, runButtonX + RUN_BUTTON_SIZE, runButtonY + RUN_BUTTON_SIZE, - canExecute ? 0xFF206020 : 0xFF303030); - Gui.drawRect(runButtonX, runButtonY + RUN_BUTTON_SIZE - 1, runButtonX + RUN_BUTTON_SIZE, runButtonY + RUN_BUTTON_SIZE, - canExecute ? 0xFF206020 : 0xFF303030); - - // Draw arrow centered in the button - int arrowTextColor = canExecute ? 0xFFFFFF : 0x808080; - int arrowTextX = runButtonX + (RUN_BUTTON_SIZE - fontRenderer.getStringWidth("▶")) / 2; - int arrowTextY = runButtonY + (RUN_BUTTON_SIZE - fontRenderer.FONT_HEIGHT) / 2 - 2; - - GlStateManager.pushMatrix(); - GlStateManager.scale(2.0F, 2.0F, 1.0F); - fontRenderer.drawString("▶", arrowTextX / 2, arrowTextY / 2, arrowTextColor); - GlStateManager.popMatrix(); - - // Draw tool name with text wrapping - int nameX = x + TOOL_PADDING; - int nameY = y + TOOL_PADDING + ICON_SIZE + 2; - int maxNameWidth = width - TOOL_PADDING * 2; - String toolName = tool.getName(); - - // Wrap the text if too long - List nameLines = fontRenderer.listFormattedStringToWidth(toolName, maxNameWidth); - for (int i = 0; i < Math.min(nameLines.size(), 1); i++) { - String line = nameLines.get(i); - if (nameLines.size() > 1 && i == 0) { - // Truncate with ellipsis if there's more - while (fontRenderer.getStringWidth(line + "...") > maxNameWidth && !line.isEmpty()) { - line = line.substring(0, line.length() - 1); - } - line = line + "..."; - } - fontRenderer.drawString(line, nameX, nameY, 0x000000); - } - - // Update hover state - if (rowHovered) { - hoveredToolIndex = index; - ctx.hoveredNetworkTool = tool; - ctx.hoveredNetworkToolPreview = preview; - - if (runHovered && canExecute) { - launchButtonHovered = true; - ctx.hoveredNetworkToolLaunchButton = tool; - } - - if (helpHovered) { - helpButtonHovered = true; - ctx.hoveredNetworkToolHelpButton = tool; - } - } - } - - public int getHoveredToolIndex() { - return hoveredToolIndex; - } - - public boolean isLaunchButtonHovered() { - return launchButtonHovered; - } - - public boolean isHelpButtonHovered() { - return helpButtonHovered; - } - - /** - * Get the number of logical rows for scrollbar calculation. - * Each tool is one "row" for scrolling purposes. - */ - public int getRowCount() { - return NetworkToolRegistry.getToolCount(); - } -} diff --git a/src/main/java/com/cellterminal/gui/render/PartitionTabRenderer.java b/src/main/java/com/cellterminal/gui/render/PartitionTabRenderer.java deleted file mode 100644 index 9d024cd..0000000 --- a/src/main/java/com/cellterminal/gui/render/PartitionTabRenderer.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.cellterminal.gui.render; - -import java.util.List; -import java.util.Map; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.RenderItem; - -import com.cellterminal.client.CellContentRow; -import com.cellterminal.client.EmptySlotInfo; -import com.cellterminal.client.StorageInfo; -import com.cellterminal.client.TabStateManager; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.cells.CellRenderer; - - -/** - * Renderer for the Partition tab (Tab 2). - * Displays cells with their partition configuration in a grid. - * Partition slots have an orange/amber tint to differentiate from regular slots. - * Supports JEI ghost ingredient drag-and-drop. - *

- * IMPORTANT: This renderer should replicate the behavior of the - * Partition Popup! - *

- * This renderer delegates actual cell rendering to {@link CellRenderer}. - * It handles the overall tab layout and line iteration. - * - * @see CellRenderer - */ -public class PartitionTabRenderer { - - private final CellRenderer cellRenderer; - - public PartitionTabRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.cellRenderer = new CellRenderer(fontRenderer, itemRender); - } - - /** - * Draw the partition tab content. - * - * @param partitionLines List of line objects (StorageInfo, CellContentRow, EmptySlotInfo) - * @param currentScroll Current scroll position - * @param rowsVisible Number of visible rows - * @param relMouseX Mouse X relative to GUI - * @param relMouseY Mouse Y relative to GUI - * @param absMouseX Absolute mouse X (for tooltips) - * @param absMouseY Absolute mouse Y (for tooltips) - * @param storageMap Map of storage IDs to StorageInfo - * @param guiLeft GUI left position (for JEI ghost targets) - * @param guiTop GUI top position (for JEI ghost targets) - * @param ctx Render context for hover tracking - */ - public void draw(List partitionLines, int currentScroll, int rowsVisible, - int relMouseX, int relMouseY, int absMouseX, int absMouseY, - Map storageMap, int guiLeft, int guiTop, - RenderContext ctx) { - - int y = GuiConstants.CONTENT_START_Y; - int visibleTop = GuiConstants.CONTENT_START_Y; - int visibleBottom = GuiConstants.CONTENT_START_Y + rowsVisible * GuiConstants.ROW_HEIGHT; - int totalLines = partitionLines.size(); - - for (int i = 0; i < rowsVisible && currentScroll + i < totalLines; i++) { - Object line = partitionLines.get(currentScroll + i); - int lineIndex = currentScroll + i; - - boolean isHovered = relMouseX >= GuiConstants.HOVER_LEFT_EDGE && relMouseX < GuiConstants.HOVER_RIGHT_EDGE - && relMouseY >= y && relMouseY < y + GuiConstants.ROW_HEIGHT; - - // Draw hover highlight and track state - if (isHovered) { - handleLineHover(line, lineIndex, y, ctx); - } - - // Draw separator line above storage entries - if (line instanceof StorageInfo && i > 0) { - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y, GuiConstants.COLOR_SEPARATOR); - } - - // Calculate tree line parameters - LineContext lineCtx = buildLineContext(partitionLines, lineIndex, i, rowsVisible, totalLines); - - // Render the line - if (line instanceof StorageInfo) { - cellRenderer.drawStorageHeader((StorageInfo) line, y, partitionLines, lineIndex, - TabStateManager.TabType.PARTITION, ctx); - } else if (line instanceof CellContentRow) { - CellContentRow row = (CellContentRow) line; - cellRenderer.drawCellPartitionLine( - row.getCell(), row.getStartIndex(), row.isFirstRow(), - y, relMouseX, relMouseY, absMouseX, absMouseY, - lineCtx.isFirstInGroup, lineCtx.isLastInGroup, visibleTop, visibleBottom, - lineCtx.isFirstVisibleRow, lineCtx.isLastVisibleRow, - lineCtx.hasContentAbove, lineCtx.hasContentBelow, - storageMap, guiLeft, guiTop, ctx); - } else if (line instanceof EmptySlotInfo) { - cellRenderer.drawEmptySlotLine( - (EmptySlotInfo) line, y, relMouseX, relMouseY, - lineCtx.isFirstInGroup, lineCtx.isLastInGroup, visibleTop, visibleBottom, - lineCtx.isFirstVisibleRow, lineCtx.isLastVisibleRow, - lineCtx.hasContentAbove, lineCtx.hasContentBelow, - storageMap, ctx); - } - - y += GuiConstants.ROW_HEIGHT; - } - } - - private void handleLineHover(Object line, int lineIndex, int y, RenderContext ctx) { - if (line instanceof CellContentRow) { - ctx.hoveredLineIndex = lineIndex; - ctx.hoveredPartitionCell = ((CellContentRow) line).getCell(); - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT - 1, GuiConstants.COLOR_ROW_HOVER); - } else if (line instanceof EmptySlotInfo) { - ctx.hoveredLineIndex = lineIndex; - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT - 1, GuiConstants.COLOR_ROW_HOVER); - } else if (line instanceof StorageInfo) { - ctx.hoveredStorageLine = (StorageInfo) line; - ctx.hoveredLineIndex = lineIndex; - Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_STORAGE_HEADER_HOVER); - } - } - - private LineContext buildLineContext(List lines, int lineIndex, int visibleIndex, - int rowsVisible, int totalLines) { - LineContext ctx = new LineContext(); - ctx.isFirstInGroup = isFirstInStorageGroup(lines, lineIndex); - ctx.isLastInGroup = isLastInStorageGroup(lines, lineIndex); - ctx.isFirstVisibleRow = (visibleIndex == 0); - ctx.isLastVisibleRow = (visibleIndex == rowsVisible - 1) || (lineIndex == totalLines - 1); - ctx.hasContentAbove = (lineIndex > 0) && !ctx.isFirstInGroup; - ctx.hasContentBelow = (lineIndex < totalLines - 1) && !ctx.isLastInGroup; - - return ctx; - } - - /** - * Check if the line at the given index is the first in its storage group. - */ - private boolean isFirstInStorageGroup(List lines, int index) { - if (index <= 0) return true; - - return lines.get(index - 1) instanceof StorageInfo; - } - - /** - * Check if the line at the given index is the last in its storage group. - */ - private boolean isLastInStorageGroup(List lines, int index) { - if (index >= lines.size() - 1) return true; - - // Look ahead to find if there are any more cells after all rows of current cell - for (int i = index + 1; i < lines.size(); i++) { - Object line = lines.get(i); - if (line instanceof StorageInfo) return true; - - if (line instanceof CellContentRow) { - CellContentRow row = (CellContentRow) line; - if (row.isFirstRow()) return false; - } else if (line instanceof EmptySlotInfo) { - return false; - } - } - - return true; - } - - /** - * Context for line rendering parameters. - */ - private static class LineContext { - boolean isFirstInGroup; - boolean isLastInGroup; - boolean isFirstVisibleRow; - boolean isLastVisibleRow; - boolean hasContentAbove; - boolean hasContentBelow; - } -} diff --git a/src/main/java/com/cellterminal/gui/render/RenderContext.java b/src/main/java/com/cellterminal/gui/render/RenderContext.java deleted file mode 100644 index 2adad47..0000000 --- a/src/main/java/com/cellterminal/gui/render/RenderContext.java +++ /dev/null @@ -1,270 +0,0 @@ -package com.cellterminal.gui.render; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import net.minecraft.item.ItemStack; - -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.client.StorageInfo; -import com.cellterminal.client.TempCellInfo; -import com.cellterminal.gui.networktools.INetworkTool; - - -/** - * Holds the rendering context and hover state for Cell Terminal tab rendering. - * This object is passed to renderers and updated during drawing to track what - * the user is hovering over for tooltips and click handling. - */ -public class RenderContext { - - // Storage data - public Map storageMap; - - // Visible area bounds - public int visibleTop; - public int visibleBottom; - public int rowsVisible; - - // GUI positioning - public int guiLeft; - public int guiTop; - - // Hover state - Terminal tab - public CellInfo hoveredCell = null; - public int hoverType = 0; // 0=none, 1=inventory, 2=partition, 3=eject - public StorageInfo hoveredStorageLine = null; // Storage header line being hovered - - // Hover state - line tracking - public int hoveredLineIndex = -1; - - // Hover state - Tab 2/3 (Inventory/Partition) - public CellInfo hoveredCellCell = null; - public StorageInfo hoveredCellStorage = null; - public int hoveredCellSlotIndex = -1; - public ItemStack hoveredContentStack = ItemStack.EMPTY; - public int hoveredContentX = 0; - public int hoveredContentY = 0; - public int hoveredContentSlotIndex = -1; - - // Partition slot tracking for JEI ghost ingredients - public int hoveredPartitionSlotIndex = -1; - public CellInfo hoveredPartitionCell = null; - public final List partitionSlotTargets = new ArrayList<>(); - - // Storage Bus tab hover state (Tab 4/5) - public StorageBusInfo hoveredStorageBus = null; - public int hoveredStorageBusPartitionSlot = -1; - public int hoveredStorageBusContentSlot = -1; - public StorageBusInfo hoveredClearButtonStorageBus = null; // Tab 5: clear button hovered - public StorageBusInfo hoveredIOModeButtonStorageBus = null; // Tab 4: IO mode button hovered - public StorageBusInfo hoveredPartitionAllButtonStorageBus = null; // Tab 4: partition all button hovered - public final List storageBusPartitionSlotTargets = new ArrayList<>(); - - // Cell partition-all / clear-partition button hover (Tab 2/3) - public CellInfo hoveredPartitionAllButtonCell = null; // Tab 2: partition all button hovered - public CellInfo hoveredClearPartitionButtonCell = null; // Tab 3: clear partition button hovered - - // Selected storage bus IDs for Tab 5 (multi-select, receives items from A key/JEI) - public Set selectedStorageBusIds = new HashSet<>(); - - // Selected temp cell slot indexes for Tab 3 (multi-select, receives items from A key/JEI) - public Set selectedTempCellSlots = new HashSet<>(); - - // Visible storage positions for priority field rendering - public final List visibleStorages = new ArrayList<>(); - - // Visible storage bus positions for priority field rendering (Tab 4/5) - public final List visibleStorageBuses = new ArrayList<>(); - - // Upgrade icon hover tracking - public final List upgradeIconTargets = new ArrayList<>(); - public UpgradeIconTarget hoveredUpgradeIcon = null; - - // Network Tools tab hover state (Tab 6) - public INetworkTool hoveredNetworkTool = null; - public INetworkTool hoveredNetworkToolLaunchButton = null; - public INetworkTool hoveredNetworkToolHelpButton = null; - public INetworkTool.ToolPreviewInfo hoveredNetworkToolPreview = null; - - // Temp Area tab hover state (Tab 3) - public TempCellInfo hoveredTempCell = null; - public TempCellInfo hoveredTempCellHeader = null; // Directly hovering header (not content row) - public TempCellInfo hoveredTempCellSendButton = null; // Hovering send button - public TempCellInfo hoveredTempCellSlot = null; // Hovering cell slot for insert/extract - public final List tempCellPartitionSlotTargets = new ArrayList<>(); - public int hoveredTempCellPartitionAllIndex = -1; // Temp slot index for partition-all button hover - public int hoveredTempCellClearPartitionIndex = -1; // Temp slot index for clear partition button hover - - /** - * Tracks a visible storage entry and its Y position for priority field placement. - */ - public static class VisibleStorageEntry { - public final StorageInfo storage; - public final int y; - - public VisibleStorageEntry(StorageInfo storage, int y) { - this.storage = storage; - this.y = y; - } - } - - /** - * Tracks a visible storage bus entry and its Y position for priority field placement. - */ - public static class VisibleStorageBusEntry { - public final StorageBusInfo storageBus; - public final int y; - - public VisibleStorageBusEntry(StorageBusInfo storageBus, int y) { - this.storageBus = storageBus; - this.y = y; - } - } - - /** - * Reset all hover state at the start of a render cycle. - */ - public void resetHoverState() { - hoveredCell = null; - hoverType = 0; - hoveredStorageLine = null; - hoveredLineIndex = -1; - hoveredContentStack = ItemStack.EMPTY; - hoveredCellCell = null; - hoveredCellStorage = null; - hoveredCellSlotIndex = -1; - hoveredContentSlotIndex = -1; - hoveredPartitionSlotIndex = -1; - hoveredPartitionCell = null; - partitionSlotTargets.clear(); - hoveredStorageBus = null; - hoveredStorageBusPartitionSlot = -1; - hoveredStorageBusContentSlot = -1; - hoveredClearButtonStorageBus = null; - hoveredIOModeButtonStorageBus = null; - hoveredPartitionAllButtonStorageBus = null; - hoveredPartitionAllButtonCell = null; - hoveredClearPartitionButtonCell = null; - storageBusPartitionSlotTargets.clear(); - visibleStorages.clear(); - visibleStorageBuses.clear(); - upgradeIconTargets.clear(); - hoveredUpgradeIcon = null; - hoveredNetworkTool = null; - hoveredNetworkToolLaunchButton = null; - hoveredNetworkToolHelpButton = null; - hoveredNetworkToolPreview = null; - hoveredTempCell = null; - hoveredTempCellHeader = null; - hoveredTempCellSendButton = null; - hoveredTempCellSlot = null; - tempCellPartitionSlotTargets.clear(); - hoveredTempCellPartitionAllIndex = -1; - hoveredTempCellClearPartitionIndex = -1; - } - - /** - * Helper class to track partition slot positions for JEI ghost ingredients. - */ - public static class PartitionSlotTarget { - public final CellInfo cell; - public final int slotIndex; - public final int x; - public final int y; - public final int width; - public final int height; - - public PartitionSlotTarget(CellInfo cell, int slotIndex, int x, int y, int width, int height) { - this.cell = cell; - this.slotIndex = slotIndex; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - } - - /** - * Helper class to track storage bus partition slot positions for JEI ghost ingredients. - */ - public static class StorageBusPartitionSlotTarget { - public final StorageBusInfo storageBus; - public final int slotIndex; - public final int x; - public final int y; - public final int width; - public final int height; - - public StorageBusPartitionSlotTarget(StorageBusInfo storageBus, int slotIndex, int x, int y, int width, int height) { - this.storageBus = storageBus; - this.slotIndex = slotIndex; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - } - - /** - * Helper class to track temp cell partition slot positions for JEI ghost ingredients. - */ - public static class TempCellPartitionSlotTarget { - public final CellInfo cell; - public final int tempSlotIndex; - public final int partitionSlotIndex; - public final int x; - public final int y; - public final int width; - public final int height; - - public TempCellPartitionSlotTarget(CellInfo cell, int tempSlotIndex, int partitionSlotIndex, int x, int y, int width, int height) { - this.cell = cell; - this.tempSlotIndex = tempSlotIndex; - this.partitionSlotIndex = partitionSlotIndex; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - } - } - - /** - * Helper class to track upgrade icon positions for tooltips and click handling. - */ - public static class UpgradeIconTarget { - public final CellInfo cell; // For cell upgrades (null for storage bus) - public final StorageBusInfo storageBus; // For storage bus upgrades (null for cell) - public final ItemStack upgrade; - public final int upgradeIndex; - public final int x; - public final int y; - public static final int SIZE = 8; // Small icon size - - public UpgradeIconTarget(CellInfo cell, ItemStack upgrade, int upgradeIndex, int x, int y) { - this.cell = cell; - this.storageBus = null; - this.upgrade = upgrade; - this.upgradeIndex = upgradeIndex; - this.x = x; - this.y = y; - } - - public UpgradeIconTarget(StorageBusInfo storageBus, ItemStack upgrade, int upgradeIndex, int x, int y) { - this.cell = null; - this.storageBus = storageBus; - this.upgrade = upgrade; - this.upgradeIndex = upgradeIndex; - this.x = x; - this.y = y; - } - - public boolean isMouseOver(int mouseX, int mouseY) { - return mouseX >= x && mouseX < x + SIZE && mouseY >= y && mouseY < y + SIZE; - } - } -} diff --git a/src/main/java/com/cellterminal/gui/render/StorageBusInventoryTabRenderer.java b/src/main/java/com/cellterminal/gui/render/StorageBusInventoryTabRenderer.java deleted file mode 100644 index e6a94d9..0000000 --- a/src/main/java/com/cellterminal/gui/render/StorageBusInventoryTabRenderer.java +++ /dev/null @@ -1,159 +0,0 @@ -package com.cellterminal.gui.render; - -import java.util.List; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.RenderItem; - -import com.cellterminal.client.StorageBusContentRow; -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.client.TabStateManager; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.storagebus.StorageBusRenderer; - - -/** - * Renderer for the Storage Bus Inventory tab (Tab 4). - * Displays storage buses with a header row followed by content rows in a tree structure. - * Content items show "P" indicator if they're in the storage bus's partition. - *

- * This renderer delegates actual storage bus rendering to {@link StorageBusRenderer}. - * It handles the overall tab layout and line iteration. - * - * @see StorageBusRenderer - */ -public class StorageBusInventoryTabRenderer { - - private final StorageBusRenderer storageBusRenderer; - - public StorageBusInventoryTabRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.storageBusRenderer = new StorageBusRenderer(fontRenderer, itemRender); - } - - /** - * Draw the storage bus inventory tab content. - * - * @param inventoryLines List of line objects (StorageBusInfo, StorageBusContentRow) - * @param currentScroll Current scroll position - * @param rowsVisible Number of visible rows - * @param relMouseX Mouse X relative to GUI - * @param relMouseY Mouse Y relative to GUI - * @param absMouseX Absolute mouse X (for tooltips) - * @param absMouseY Absolute mouse Y (for tooltips) - * @param ctx Render context for hover tracking - */ - public void draw(List inventoryLines, int currentScroll, int rowsVisible, - int relMouseX, int relMouseY, int absMouseX, int absMouseY, - RenderContext ctx) { - - int y = GuiConstants.CONTENT_START_Y; - int totalLines = inventoryLines.size(); - - for (int i = 0; i < rowsVisible && currentScroll + i < totalLines; i++) { - Object line = inventoryLines.get(currentScroll + i); - int lineIndex = currentScroll + i; - - boolean isHovered = relMouseX >= GuiConstants.HOVER_LEFT_EDGE && relMouseX < GuiConstants.HOVER_RIGHT_EDGE - && relMouseY >= y && relMouseY < y + GuiConstants.ROW_HEIGHT; - - if (line instanceof StorageBusInfo) { - StorageBusInfo storageBus = (StorageBusInfo) line; - - if (isHovered) ctx.hoveredLineIndex = lineIndex; - - // Draw separator line above header (except for first) - if (lineIndex > 0) { - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y, GuiConstants.COLOR_SEPARATOR); - } - - storageBusRenderer.drawStorageBusHeader(storageBus, y, inventoryLines, lineIndex, - TabStateManager.TabType.STORAGE_BUS_INVENTORY, - relMouseX, relMouseY, absMouseX, absMouseY, ctx); - - } else if (line instanceof StorageBusContentRow) { - StorageBusContentRow row = (StorageBusContentRow) line; - - if (isHovered) { - ctx.hoveredLineIndex = lineIndex; - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT - 1, - GuiConstants.COLOR_ROW_HOVER); - } - - LineContext lineCtx = buildLineContext(inventoryLines, lineIndex, i, rowsVisible, totalLines); - - storageBusRenderer.drawStorageBusInventoryLine( - row.getStorageBus(), row.getStartIndex(), - y, relMouseX, relMouseY, absMouseX, absMouseY, - lineCtx.isFirstInGroup, lineCtx.isLastInGroup, - lineCtx.isFirstVisibleRow, lineCtx.isLastVisibleRow, - lineCtx.hasContentAbove, lineCtx.hasContentBelow, ctx); - } - - y += GuiConstants.ROW_HEIGHT; - } - } - - private LineContext buildLineContext(List lines, int lineIndex, int visibleIndex, - int rowsVisible, int totalLines) { - LineContext ctx = new LineContext(); - ctx.isFirstInGroup = isFirstContentRowOfStorageBus(lines, lineIndex); - ctx.isLastInGroup = isLastContentRowOfStorageBus(lines, lineIndex); - ctx.isFirstVisibleRow = (visibleIndex == 0); - ctx.isLastVisibleRow = (visibleIndex == rowsVisible - 1) || (lineIndex == totalLines - 1); - ctx.hasContentAbove = hasContentRowAbove(lines, lineIndex); - ctx.hasContentBelow = hasContentRowBelow(lines, lineIndex); - - return ctx; - } - - /** - * Check if the given line is the first content row of a storage bus. - */ - private boolean isFirstContentRowOfStorageBus(List lines, int lineIndex) { - if (!(lines.get(lineIndex) instanceof StorageBusContentRow)) return false; - if (lineIndex == 0) return true; - - return lines.get(lineIndex - 1) instanceof StorageBusInfo; - } - - /** - * Check if the given line is the last content row of a storage bus. - */ - private boolean isLastContentRowOfStorageBus(List lines, int lineIndex) { - if (!(lines.get(lineIndex) instanceof StorageBusContentRow)) return false; - if (lineIndex >= lines.size() - 1) return true; - - return lines.get(lineIndex + 1) instanceof StorageBusInfo; - } - - /** - * Check if there's a content row above this line within the same storage bus. - */ - private boolean hasContentRowAbove(List lines, int lineIndex) { - if (lineIndex == 0) return false; - - return lines.get(lineIndex - 1) instanceof StorageBusContentRow; - } - - /** - * Check if there's a content row below this line within the same storage bus. - */ - private boolean hasContentRowBelow(List lines, int lineIndex) { - if (lineIndex >= lines.size() - 1) return false; - - return lines.get(lineIndex + 1) instanceof StorageBusContentRow; - } - - /** - * Context for line rendering parameters. - */ - private static class LineContext { - boolean isFirstInGroup; - boolean isLastInGroup; - boolean isFirstVisibleRow; - boolean isLastVisibleRow; - boolean hasContentAbove; - boolean hasContentBelow; - } -} diff --git a/src/main/java/com/cellterminal/gui/render/StorageBusPartitionTabRenderer.java b/src/main/java/com/cellterminal/gui/render/StorageBusPartitionTabRenderer.java deleted file mode 100644 index 70cd84e..0000000 --- a/src/main/java/com/cellterminal/gui/render/StorageBusPartitionTabRenderer.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.cellterminal.gui.render; - -import java.util.List; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.RenderItem; - -import com.cellterminal.client.StorageBusContentRow; -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.client.TabStateManager; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.storagebus.StorageBusRenderer; - - -/** - * Renderer for the Storage Bus Partition tab (Tab 5). - * Displays storage buses with a header row followed by partition rows in a tree structure. - * Partition slots have an orange/amber tint and support JEI ghost ingredient drag-and-drop. - *

- * This renderer delegates actual storage bus rendering to {@link StorageBusRenderer}. - * It handles the overall tab layout and line iteration. - * - * @see StorageBusRenderer - */ -public class StorageBusPartitionTabRenderer { - - private final StorageBusRenderer storageBusRenderer; - - public StorageBusPartitionTabRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.storageBusRenderer = new StorageBusRenderer(fontRenderer, itemRender); - } - - /** - * Draw the storage bus partition tab content. - * - * @param partitionLines List of line objects (StorageBusInfo, StorageBusContentRow) - * @param currentScroll Current scroll position - * @param rowsVisible Number of visible rows - * @param relMouseX Mouse X relative to GUI - * @param relMouseY Mouse Y relative to GUI - * @param absMouseX Absolute mouse X (for tooltips) - * @param absMouseY Absolute mouse Y (for tooltips) - * @param guiLeft GUI left position (for JEI ghost targets) - * @param guiTop GUI top position (for JEI ghost targets) - * @param ctx Render context for hover tracking - */ - public void draw(List partitionLines, int currentScroll, int rowsVisible, - int relMouseX, int relMouseY, int absMouseX, int absMouseY, - int guiLeft, int guiTop, RenderContext ctx) { - - int y = GuiConstants.CONTENT_START_Y; - int visibleTop = GuiConstants.CONTENT_START_Y; - int visibleBottom = GuiConstants.CONTENT_START_Y + rowsVisible * GuiConstants.ROW_HEIGHT; - int totalLines = partitionLines.size(); - - for (int i = 0; i < rowsVisible && currentScroll + i < totalLines; i++) { - Object line = partitionLines.get(currentScroll + i); - int lineIndex = currentScroll + i; - - boolean isHovered = relMouseX >= GuiConstants.HOVER_LEFT_EDGE && relMouseX < GuiConstants.HOVER_RIGHT_EDGE - && relMouseY >= y && relMouseY < y + GuiConstants.ROW_HEIGHT; - - if (line instanceof StorageBusInfo) { - StorageBusInfo storageBus = (StorageBusInfo) line; - - if (isHovered) ctx.hoveredLineIndex = lineIndex; - - // Draw separator line above header (except for first) - if (lineIndex > 0) { - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y, GuiConstants.COLOR_SEPARATOR); - } - - storageBusRenderer.drawStorageBusPartitionHeader(storageBus, y, partitionLines, lineIndex, - TabStateManager.TabType.STORAGE_BUS_PARTITION, - relMouseX, relMouseY, absMouseX, absMouseY, guiLeft, guiTop, ctx); - - } else if (line instanceof StorageBusContentRow) { - StorageBusContentRow row = (StorageBusContentRow) line; - StorageBusInfo storageBus = row.getStorageBus(); - - // Draw selection background if this bus is selected - boolean isSelected = ctx.selectedStorageBusIds != null - && ctx.selectedStorageBusIds.contains(storageBus.getId()); - - if (isSelected) { - Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT, - GuiConstants.COLOR_SELECTION); - } - - if (isHovered) { - ctx.hoveredLineIndex = lineIndex; - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT - 1, - GuiConstants.COLOR_ROW_HOVER); - } - - LineContext lineCtx = buildLineContext(partitionLines, lineIndex, i, rowsVisible, totalLines); - - storageBusRenderer.drawStorageBusPartitionLine( - storageBus, row.getStartIndex(), - y, relMouseX, relMouseY, absMouseX, absMouseY, - lineCtx.isFirstInGroup, lineCtx.isLastInGroup, visibleTop, visibleBottom, - lineCtx.isFirstVisibleRow, lineCtx.isLastVisibleRow, - lineCtx.hasContentAbove, lineCtx.hasContentBelow, - guiLeft, guiTop, ctx); - } - - y += GuiConstants.ROW_HEIGHT; - } - } - - private LineContext buildLineContext(List lines, int lineIndex, int visibleIndex, - int rowsVisible, int totalLines) { - LineContext ctx = new LineContext(); - ctx.isFirstInGroup = isFirstContentRowOfStorageBus(lines, lineIndex); - ctx.isLastInGroup = isLastContentRowOfStorageBus(lines, lineIndex); - ctx.isFirstVisibleRow = (visibleIndex == 0); - ctx.isLastVisibleRow = (visibleIndex == rowsVisible - 1) || (lineIndex == totalLines - 1); - ctx.hasContentAbove = hasContentRowAbove(lines, lineIndex); - ctx.hasContentBelow = hasContentRowBelow(lines, lineIndex); - - return ctx; - } - - /** - * Check if the given line is the first content row of a storage bus. - */ - private boolean isFirstContentRowOfStorageBus(List lines, int lineIndex) { - if (!(lines.get(lineIndex) instanceof StorageBusContentRow)) return false; - if (lineIndex == 0) return true; - - return lines.get(lineIndex - 1) instanceof StorageBusInfo; - } - - /** - * Check if the given line is the last content row of a storage bus. - */ - private boolean isLastContentRowOfStorageBus(List lines, int lineIndex) { - if (!(lines.get(lineIndex) instanceof StorageBusContentRow)) return false; - if (lineIndex >= lines.size() - 1) return true; - - return lines.get(lineIndex + 1) instanceof StorageBusInfo; - } - - /** - * Check if there's a content row above this line within the same storage bus. - */ - private boolean hasContentRowAbove(List lines, int lineIndex) { - if (lineIndex == 0) return false; - - return lines.get(lineIndex - 1) instanceof StorageBusContentRow; - } - - /** - * Check if there's a content row below this line within the same storage bus. - */ - private boolean hasContentRowBelow(List lines, int lineIndex) { - if (lineIndex >= lines.size() - 1) return false; - - return lines.get(lineIndex + 1) instanceof StorageBusContentRow; - } - - /** - * Context for line rendering parameters. - */ - private static class LineContext { - boolean isFirstInGroup; - boolean isLastInGroup; - boolean isFirstVisibleRow; - boolean isLastVisibleRow; - boolean hasContentAbove; - boolean hasContentBelow; - } -} diff --git a/src/main/java/com/cellterminal/gui/render/TempAreaTabRenderer.java b/src/main/java/com/cellterminal/gui/render/TempAreaTabRenderer.java deleted file mode 100644 index e3292cf..0000000 --- a/src/main/java/com/cellterminal/gui/render/TempAreaTabRenderer.java +++ /dev/null @@ -1,610 +0,0 @@ -package com.cellterminal.gui.render; - -import java.util.List; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.RenderItem; -import net.minecraft.client.resources.I18n; -import net.minecraft.item.ItemStack; - -import com.cellterminal.client.CellContentRow; -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.TempCellInfo; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.cells.CellRenderer; -import com.cellterminal.gui.cells.CellSlotRenderer; -import com.cellterminal.gui.cells.CellTreeRenderer; - - -/** - * Renderer for the Temp Area tab (Tab 3). - * Displays temporary cells with combined inventory and partition views. - *

- * Each temp cell is shown as: - * - Header row with cell icon, name, and "Send" button - * - Inventory rows showing cell contents (like Inventory tab) with tree lines and partition-all button - * - Partition rows showing cell partition (like Partition tab) with tree lines and clear button - *

- * The renderer draws tree lines connecting sections and handles hover highlighting. - */ -public class TempAreaTabRenderer { - - // Maximum width for cell name before the Send button - private static final int MAX_NAME_WIDTH = 120; - - // Send button color (light blue for better visibility) - private static final int COLOR_SEND_BUTTON = 0xFF5599CC; - private static final int COLOR_SEND_BUTTON_HOVER = 0xFF77BBEE; - - private final FontRenderer fontRenderer; - private final CellRenderer cellRenderer; - private final CellSlotRenderer slotRenderer; - private final CellTreeRenderer treeRenderer; - - public TempAreaTabRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.fontRenderer = fontRenderer; - this.cellRenderer = new CellRenderer(fontRenderer, itemRender); - this.slotRenderer = cellRenderer.getSlotRenderer(); - this.treeRenderer = cellRenderer.getTreeRenderer(); - } - - /** - * Draw the temp area tab content. - * - * @param tempAreaLines List of line objects (TempCellInfo, CellContentRow for inventory/partition) - * @param currentScroll Current scroll position - * @param rowsVisible Number of visible rows - * @param relMouseX Mouse X relative to GUI - * @param relMouseY Mouse Y relative to GUI - * @param absMouseX Absolute mouse X (for tooltips) - * @param absMouseY Absolute mouse Y (for tooltips) - * @param guiLeft GUI left position - * @param guiTop GUI top position - * @param ctx Render context for hover tracking - */ - public void draw(List tempAreaLines, int currentScroll, int rowsVisible, - int relMouseX, int relMouseY, int absMouseX, int absMouseY, - int guiLeft, int guiTop, RenderContext ctx) { - - int y = GuiConstants.CONTENT_START_Y; - int visibleTop = GuiConstants.CONTENT_START_Y; - int visibleBottom = GuiConstants.CONTENT_START_Y + rowsVisible * GuiConstants.ROW_HEIGHT; - int totalLines = tempAreaLines.size(); - - // No temp cells - if (totalLines == 0) return; - - for (int i = 0; i < rowsVisible && currentScroll + i < totalLines; i++) { - Object line = tempAreaLines.get(currentScroll + i); - int lineIndex = currentScroll + i; - - boolean isHovered = relMouseX >= GuiConstants.HOVER_LEFT_EDGE && relMouseX < GuiConstants.HOVER_RIGHT_EDGE - && relMouseY >= y && relMouseY < y + GuiConstants.ROW_HEIGHT; - - // Draw hover highlight and set hover state - if (isHovered) handleLineHover(line, lineIndex, y, relMouseX, relMouseY, tempAreaLines, ctx); - - // Calculate tree line context - LineContext lineCtx = buildLineContext(tempAreaLines, lineIndex, i, rowsVisible, totalLines); - - // Draw the line based on its type - if (line instanceof TempCellInfo) { - TempCellInfo tempCell = (TempCellInfo) line; - - // Draw selection background for selected temp cells (before hover) - boolean isSelected = tempCell.hasCell() - && ctx.selectedTempCellSlots != null - && ctx.selectedTempCellSlots.contains(tempCell.getTempSlotIndex()); - if (isSelected) { - Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, - y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_SELECTION); - } - - // Draw connector line to content below if there is a content row following - boolean hasContentFollowing = tempCell.hasCell() - && lineIndex + 1 < totalLines - && tempAreaLines.get(lineIndex + 1) instanceof CellContentRow; - if (hasContentFollowing) drawStorageConnector(y); - - drawTempCellHeader(tempCell, y, relMouseX, relMouseY, ctx); - } else if (line instanceof CellContentRow) { - CellContentRow row = (CellContentRow) line; - - // Find the temp slot index for this row - int tempSlotIndex = findParentTempSlotIndex(tempAreaLines, lineIndex); - - // Draw selection background for content rows belonging to selected temp cells - boolean isSelected = tempSlotIndex >= 0 - && ctx.selectedTempCellSlots != null - && ctx.selectedTempCellSlots.contains(tempSlotIndex); - if (isSelected) { - Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, - y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_SELECTION); - } - - if (row.isPartitionRow()) { - // Partition view row - with tree lines, clear button, 9 slots - drawPartitionContentRow(row, y, relMouseX, relMouseY, absMouseX, absMouseY, - guiLeft, guiTop, lineCtx, visibleTop, visibleBottom, tempSlotIndex, ctx); - } else { - // Inventory view row - with tree lines, partition-all button, 9 slots - drawInventoryContentRow(row, y, relMouseX, relMouseY, absMouseX, absMouseY, - lineCtx, visibleTop, visibleBottom, tempSlotIndex, ctx); - } - } - - y += GuiConstants.ROW_HEIGHT; - } - } - - /** - * Draw vertical connector line from temp cell header to content rows. - */ - private void drawStorageConnector(int y) { - int lineX = GuiConstants.GUI_INDENT + 7; - Gui.drawRect(lineX, y + GuiConstants.ROW_HEIGHT - 1, - lineX + 1, y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_TREE_LINE); - } - - /** - * Draw an inventory content row with tree lines and partition-all button. - */ - private void drawInventoryContentRow(CellContentRow row, int y, int relMouseX, int relMouseY, - int absMouseX, int absMouseY, - LineContext lineCtx, int visibleTop, int visibleBottom, - int tempSlotIndex, RenderContext ctx) { - - CellInfo cell = row.getCell(); - int lineX = GuiConstants.GUI_INDENT + 7; - - // Draw tree lines - drawTreeLines(lineX, y, row.isFirstRow(), lineCtx.isFirstInGroup, lineCtx.isLastInGroup, - visibleTop, visibleBottom, lineCtx.isFirstVisibleRow, lineCtx.isLastVisibleRow, - lineCtx.hasContentAbove, lineCtx.hasContentBelow, false); - - // Draw partition-all button for first row (green) - if (row.isFirstRow()) drawPartitionAllButton(cell, lineX, y, relMouseX, relMouseY, tempSlotIndex, ctx); - - // Draw inventory content slots (9 per row, like storage bus) - drawInventoryContentSlots(cell, row.getStartIndex(), y, relMouseX, relMouseY, - absMouseX, absMouseY, ctx); - } - - /** - * Draw a partition content row with tree lines and clear button. - */ - private void drawPartitionContentRow(CellContentRow row, int y, int relMouseX, int relMouseY, - int absMouseX, int absMouseY, - int guiLeft, int guiTop, - LineContext lineCtx, int visibleTop, int visibleBottom, - int tempSlotIndex, RenderContext ctx) { - - CellInfo cell = row.getCell(); - int lineX = GuiConstants.GUI_INDENT + 7; - - // Draw tree lines - drawTreeLines(lineX, y, row.isFirstRow(), lineCtx.isFirstInGroup, lineCtx.isLastInGroup, - visibleTop, visibleBottom, lineCtx.isFirstVisibleRow, lineCtx.isLastVisibleRow, - lineCtx.hasContentAbove, lineCtx.hasContentBelow, row.isFirstRow()); - - // Draw clear partition button for first row (red) - if (row.isFirstRow()) drawClearPartitionButton(cell, lineX, y, relMouseX, relMouseY, tempSlotIndex, ctx); - - // Draw partition slots (9 per row, like storage bus) - drawPartitionSlots(cell, row.getStartIndex(), y, relMouseX, relMouseY, - absMouseX, absMouseY, guiLeft, guiTop, tempSlotIndex, ctx); - } - - /** - * Draw tree lines connecting content rows. - */ - private void drawTreeLines(int lineX, int y, boolean isFirstRow, - boolean isFirstInGroup, boolean isLastInGroup, - int visibleTop, int visibleBottom, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, - boolean suppressTopAboveButton) { - - int lineTop = calculateLineTop(y, isFirstRow, isFirstInGroup, isFirstVisibleRow, hasContentAbove, visibleTop); - - // If requested, suppress drawing the small top segment - if (suppressTopAboveButton) { - int minTop = y + 6; // start below the clear button area (buttonY = y + 4) - if (lineTop < minTop) lineTop = minTop; - } - int lineBottom = calculateLineBottom(y, isLastInGroup, isLastVisibleRow, hasContentBelow, visibleBottom); - - // Clamp lineTop to visible area - if (lineTop < visibleTop) lineTop = visibleTop; - - // Vertical line - Gui.drawRect(lineX, lineTop, lineX + 1, lineBottom, GuiConstants.COLOR_TREE_LINE); - - // Horizontal branch - Gui.drawRect(lineX, y + 8, lineX + 10, y + 9, GuiConstants.COLOR_TREE_LINE); - } - - private int calculateLineTop(int y, boolean isFirstRow, boolean isFirstInGroup, - boolean isFirstVisibleRow, boolean hasContentAbove, int visibleTop) { - if (isFirstRow) { - if (isFirstInGroup) return y - 3; - if (isFirstVisibleRow && hasContentAbove) return visibleTop; - - return y - 4; - } - - return isFirstVisibleRow && hasContentAbove ? visibleTop : y - 4; - } - - private int calculateLineBottom(int y, boolean isLastInGroup, boolean isLastVisibleRow, - boolean hasContentBelow, int visibleBottom) { - if (isLastInGroup) return y + 9; - if (isLastVisibleRow && hasContentBelow) return visibleBottom; - - return y + GuiConstants.ROW_HEIGHT; - } - - /** - * Draw the partition-all button (green) on the tree line. - */ - private void drawPartitionAllButton(CellInfo cell, int lineX, int y, int relMouseX, int relMouseY, int tempSlotIndex, RenderContext ctx) { - int buttonX = lineX - 3; - int buttonY = y + 4; - int buttonSize = GuiConstants.SMALL_BUTTON_SIZE; - - boolean hovered = relMouseX >= buttonX && relMouseX < buttonX + buttonSize - && relMouseY >= buttonY && relMouseY < buttonY + buttonSize; - - // Draw background to cover tree line - Gui.drawRect(buttonX - 1, buttonY - 1, buttonX + buttonSize + 1, buttonY + buttonSize + 1, GuiConstants.COLOR_SLOT_BACKGROUND); - - // Draw button with green fill - slotRenderer.drawSmallButton(buttonX, buttonY, hovered, GuiConstants.COLOR_BUTTON_GREEN); - - if (hovered) { - ctx.hoveredPartitionAllButtonCell = cell; - ctx.hoveredTempCellPartitionAllIndex = tempSlotIndex; - } - } - - /** - * Draw the clear partition button (red) on the tree line. - */ - private void drawClearPartitionButton(CellInfo cell, int lineX, int y, int relMouseX, int relMouseY, int tempSlotIndex, RenderContext ctx) { - int buttonX = lineX - 3; - int buttonY = y + 4; - int buttonSize = GuiConstants.SMALL_BUTTON_SIZE; - - boolean hovered = relMouseX >= buttonX && relMouseX < buttonX + buttonSize - && relMouseY >= buttonY && relMouseY < buttonY + buttonSize; - - // Draw background to cover tree line - Gui.drawRect(buttonX - 1, buttonY - 1, buttonX + buttonSize + 1, buttonY + buttonSize + 1, GuiConstants.COLOR_SLOT_BACKGROUND); - - // Draw button with red fill - slotRenderer.drawSmallButton(buttonX, buttonY, hovered, GuiConstants.COLOR_BUTTON_RED); - - if (hovered) { - ctx.hoveredClearPartitionButtonCell = cell; - ctx.hoveredTempCellClearPartitionIndex = tempSlotIndex; - } - } - - /** - * Draw inventory content slots (9 per row). - */ - private void drawInventoryContentSlots(CellInfo cell, int startIndex, int y, - int relMouseX, int relMouseY, - int absMouseX, int absMouseY, RenderContext ctx) { - - List contents = cell.getContents(); - List partition = cell.getPartition(); - int slotStartX = GuiConstants.CELL_INDENT + 4; - - for (int i = 0; i < GuiConstants.STORAGE_BUS_SLOTS_PER_ROW; i++) { - int contentIndex = startIndex + i; - int slotX = slotStartX + (i * GuiConstants.MINI_SLOT_SIZE); - - slotRenderer.drawSlotBackground(slotX, y); - - if (contentIndex >= contents.size()) continue; - - ItemStack stack = contents.get(contentIndex); - if (stack.isEmpty()) continue; - - slotRenderer.renderItemStack(stack, slotX, y); - - // Draw partition indicator - if (slotRenderer.isInPartition(stack, partition)) slotRenderer.drawPartitionIndicator(slotX, y); - - // Draw item count - slotRenderer.drawItemCount(cell.getContentCount(contentIndex), slotX, y); - - // Check hover - if (relMouseX >= slotX && relMouseX < slotX + GuiConstants.MINI_SLOT_SIZE - && relMouseY >= y && relMouseY < y + GuiConstants.MINI_SLOT_SIZE) { - slotRenderer.drawSlotHoverHighlight(slotX, y); - ctx.hoveredContentStack = stack; - ctx.hoveredContentX = absMouseX; - ctx.hoveredContentY = absMouseY; - ctx.hoveredContentSlotIndex = contentIndex; - ctx.hoveredCellCell = cell; - } - } - } - - /** - * Draw partition slots (9 per row, with amber tint). - */ - private void drawPartitionSlots(CellInfo cell, int startIndex, int y, - int relMouseX, int relMouseY, - int absMouseX, int absMouseY, - int guiLeft, int guiTop, int tempSlotIndex, RenderContext ctx) { - - List partition = cell.getPartition(); - int slotStartX = GuiConstants.CELL_INDENT + 4; - - for (int i = 0; i < GuiConstants.STORAGE_BUS_SLOTS_PER_ROW; i++) { - int partitionIndex = startIndex + i; - if (partitionIndex >= GuiConstants.MAX_CELL_PARTITION_SLOTS) break; - - int slotX = slotStartX + (i * GuiConstants.MINI_SLOT_SIZE); - - // Draw partition slot with amber tint - slotRenderer.drawPartitionSlotBackground(slotX, y); - - // Register JEI ghost target for temp cells - ctx.tempCellPartitionSlotTargets.add(new RenderContext.TempCellPartitionSlotTarget( - cell, tempSlotIndex, partitionIndex, guiLeft + slotX, guiTop + y, - GuiConstants.MINI_SLOT_SIZE, GuiConstants.MINI_SLOT_SIZE)); - - // Draw partition item if present - ItemStack partItem = partitionIndex < partition.size() ? partition.get(partitionIndex) : ItemStack.EMPTY; - if (!partItem.isEmpty()) slotRenderer.renderItemStack(partItem, slotX, y); - - // Check hover - if (relMouseX >= slotX && relMouseX < slotX + GuiConstants.MINI_SLOT_SIZE - && relMouseY >= y && relMouseY < y + GuiConstants.MINI_SLOT_SIZE) { - slotRenderer.drawSlotHoverHighlight(slotX, y); - ctx.hoveredPartitionSlotIndex = partitionIndex; - ctx.hoveredPartitionCell = cell; - - if (!partItem.isEmpty()) { - ctx.hoveredContentStack = partItem; - ctx.hoveredContentX = absMouseX; - ctx.hoveredContentY = absMouseY; - } - } - } - } - - /** - * Draw a temp cell header row. - */ - private void drawTempCellHeader(TempCellInfo tempCell, int y, int relMouseX, int relMouseY, RenderContext ctx) { - CellInfo cell = tempCell.getCellInfo(); - boolean hasCell = !tempCell.getCellStack().isEmpty(); - - // Draw cell slot background - int slotX = GuiConstants.GUI_INDENT; - int slotSize = GuiConstants.MINI_SLOT_SIZE; - slotRenderer.drawSlotBackground(slotX, y); - - // Check if mouse is over the cell slot area (for insert/extract) - boolean slotHovered = relMouseX >= slotX && relMouseX < slotX + slotSize - && relMouseY >= y && relMouseY < y + slotSize; - - // Track slot hover for insert/extract clicks - if (slotHovered) ctx.hoveredTempCellSlot = tempCell; - - // Draw hover highlight on slot if hovered - if (slotHovered) slotRenderer.drawSlotHoverHighlight(slotX, y); - - // Draw cell item - if (hasCell) slotRenderer.renderItemStack(tempCell.getCellStack(), slotX, y); - - // Draw cell name (or "Drop a cell here" for empty slots), truncated to fit before Send button - String name; - if (cell != null) { - name = cell.getDisplayName(); - } else { - name = I18n.format("gui.cellterminal.temp_area.drop_cell"); - } - - // Truncate name if it's too long - int nameX = slotX + 20; - String displayName = truncateName(name, MAX_NAME_WIDTH); - fontRenderer.drawString(displayName, nameX, y + 5, hasCell ? GuiConstants.COLOR_TEXT_NORMAL : GuiConstants.COLOR_TEXT_PLACEHOLDER); - - // Only draw "Send" button for cells that actually have content - if (!hasCell) return; - - // Draw "Send" button (right side) - light blue color for visibility - int sendBtnX = 150; - int sendBtnY = y + 2; - int sendBtnW = 28; - int sendBtnH = 12; - - boolean sendHovered = relMouseX >= sendBtnX && relMouseX < sendBtnX + sendBtnW - && relMouseY >= sendBtnY && relMouseY < sendBtnY + sendBtnH; - - int btnColor = sendHovered ? COLOR_SEND_BUTTON_HOVER : COLOR_SEND_BUTTON; - Gui.drawRect(sendBtnX, sendBtnY, sendBtnX + sendBtnW, sendBtnY + sendBtnH, btnColor); - - // Button 3D effect - Gui.drawRect(sendBtnX, sendBtnY, sendBtnX + sendBtnW, sendBtnY + 1, GuiConstants.COLOR_BUTTON_HIGHLIGHT); - Gui.drawRect(sendBtnX, sendBtnY, sendBtnX + 1, sendBtnY + sendBtnH, GuiConstants.COLOR_BUTTON_HIGHLIGHT); - Gui.drawRect(sendBtnX, sendBtnY + sendBtnH - 1, sendBtnX + sendBtnW, sendBtnY + sendBtnH, GuiConstants.COLOR_BUTTON_SHADOW); - Gui.drawRect(sendBtnX + sendBtnW - 1, sendBtnY, sendBtnX + sendBtnW, sendBtnY + sendBtnH, GuiConstants.COLOR_BUTTON_SHADOW); - - String sendText = I18n.format("gui.cellterminal.temp_area.send"); - int textX = sendBtnX + (sendBtnW - fontRenderer.getStringWidth(sendText)) / 2; - // Use black text on light blue button for better contrast - fontRenderer.drawString(sendText, textX, sendBtnY + 2, 0x000000); - - // Track send button hover - if (sendHovered && tempCell.getCellInfo() != null) ctx.hoveredTempCellSendButton = tempCell; - } - - /** - * Truncate a string to fit within the given pixel width. - */ - private String truncateName(String name, int maxWidth) { - if (fontRenderer.getStringWidth(name) <= maxWidth) return name; - - String ellipsis = "..."; - int ellipsisWidth = fontRenderer.getStringWidth(ellipsis); - - // Binary search for the maximum length that fits - int maxLen = name.length(); - while (maxLen > 0 && fontRenderer.getStringWidth(name.substring(0, maxLen)) + ellipsisWidth > maxWidth) { - maxLen--; - } - - return maxLen > 0 ? name.substring(0, maxLen) + ellipsis : ellipsis; - } - - /** - * Handle hover for a line, setting appropriate context state for selection. - * For both TempCellInfo headers and CellContentRow content, we set hoveredTempCell - * to enable selection by clicking anywhere on a temp cell's rows. - */ - private void handleLineHover(Object line, int lineIndex, int y, int relMouseX, int relMouseY, - List tempAreaLines, RenderContext ctx) { - ctx.hoveredLineIndex = lineIndex; - - if (line instanceof TempCellInfo) { - ctx.hoveredTempCell = (TempCellInfo) line; - ctx.hoveredTempCellHeader = (TempCellInfo) line; // Direct header hover - selection allowed - Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_STORAGE_HEADER_HOVER); - } else if (line instanceof CellContentRow) { - CellContentRow row = (CellContentRow) line; - ctx.hoveredCellCell = row.getCell(); - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT - 1, GuiConstants.COLOR_ROW_HOVER); - - // Also find and set the parent TempCellInfo so clicking on content rows can select the cell - int parentSlotIndex = findParentTempSlotIndex(tempAreaLines, lineIndex); - if (parentSlotIndex >= 0) { - TempCellInfo parentCell = findTempCellBySlot(tempAreaLines, parentSlotIndex); - if (parentCell != null) ctx.hoveredTempCell = parentCell; - } - } - } - - /** - * Find TempCellInfo for a given slot index in the temp area lines. - */ - private TempCellInfo findTempCellBySlot(List lines, int slotIndex) { - for (Object line : lines) { - if (line instanceof TempCellInfo) { - TempCellInfo tempCell = (TempCellInfo) line; - if (tempCell.getTempSlotIndex() == slotIndex) return tempCell; - } - } - - return null; - } - - private LineContext buildLineContext(List lines, int lineIndex, int visibleIndex, - int rowsVisible, int totalLines) { - LineContext ctx = new LineContext(); - - // Determine if this is first/last in section (inventory OR partition section, not whole group) - ctx.isFirstInGroup = isFirstInSection(lines, lineIndex); - ctx.isLastInGroup = isLastInSection(lines, lineIndex); - ctx.isFirstVisibleRow = (visibleIndex == 0); - ctx.isLastVisibleRow = (visibleIndex == rowsVisible - 1) || (lineIndex == totalLines - 1); - ctx.hasContentAbove = (lineIndex > 0) && !ctx.isFirstInGroup; - ctx.hasContentBelow = (lineIndex < totalLines - 1) && !ctx.isLastInGroup; - - return ctx; - } - - /** - * Check if the given line is the first content row in its section. - * A "section" is either all inventory rows or all partition rows under a temp cell. - * The first inventory row comes right after TempCellInfo. - * The first partition row comes right after the last inventory row. - */ - private boolean isFirstInSection(List lines, int index) { - Object current = lines.get(index); - if (!(current instanceof CellContentRow)) return false; - - CellContentRow currentRow = (CellContentRow) current; - - // Check previous line - if (index <= 0) return true; - - Object previous = lines.get(index - 1); - - // If previous is a TempCellInfo header, this is the first content row - if (previous instanceof TempCellInfo) return true; - - // If previous is a CellContentRow, check if we're transitioning from inventory to partition - if (previous instanceof CellContentRow) { - CellContentRow prevRow = (CellContentRow) previous; - - // If current is partition and previous is inventory, this is first in partition section - if (currentRow.isPartitionRow() && !prevRow.isPartitionRow()) return true; - } - - return false; - } - - /** - * Check if the given line is the last content row in its section. - * The last inventory row comes right before the first partition row or next TempCellInfo. - * The last partition row comes right before the next TempCellInfo. - */ - private boolean isLastInSection(List lines, int index) { - Object current = lines.get(index); - if (!(current instanceof CellContentRow)) return false; - - CellContentRow currentRow = (CellContentRow) current; - - // Check next line - if (index >= lines.size() - 1) return true; - - Object next = lines.get(index + 1); - - // If next is a TempCellInfo header, this is the last content row - if (next instanceof TempCellInfo) return true; - - // If next is a CellContentRow, check if we're transitioning from inventory to partition - if (next instanceof CellContentRow) { - CellContentRow nextRow = (CellContentRow) next; - - // If current is inventory and next is partition, this is last in inventory section - if (!currentRow.isPartitionRow() && nextRow.isPartitionRow()) return true; - } - - return false; - } - - /** - * Find the temp slot index for a CellContentRow by looking back to find - * the parent TempCellInfo. - * - * @param lines The list of temp area lines - * @param lineIndex The current line index - * @return The temp slot index, or -1 if not found - */ - private int findParentTempSlotIndex(List lines, int lineIndex) { - for (int i = lineIndex; i >= 0; i--) { - Object line = lines.get(i); - if (line instanceof TempCellInfo) return ((TempCellInfo) line).getTempSlotIndex(); - } - - return -1; - } - - private static class LineContext { - boolean isFirstInGroup; - boolean isLastInGroup; - boolean isFirstVisibleRow; - boolean isLastVisibleRow; - boolean hasContentAbove; - boolean hasContentBelow; - } -} diff --git a/src/main/java/com/cellterminal/gui/render/TerminalTabRenderer.java b/src/main/java/com/cellterminal/gui/render/TerminalTabRenderer.java deleted file mode 100644 index 371652e..0000000 --- a/src/main/java/com/cellterminal/gui/render/TerminalTabRenderer.java +++ /dev/null @@ -1,200 +0,0 @@ -package com.cellterminal.gui.render; - -import java.util.List; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.RenderItem; - -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.StorageInfo; -import com.cellterminal.client.TabStateManager; -import com.cellterminal.gui.GuiConstants; - - -/** - * Renderer for the Terminal tab (Tab 0). - * Displays storage devices as expandable tree nodes with cells listed below. - * Each cell shows name, usage bar, and action buttons (Eject, Inventory, Partition). - */ -public class TerminalTabRenderer extends CellTerminalRenderer { - - public TerminalTabRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - super(fontRenderer, itemRender); - } - - /** - * Draw the terminal tab content. - * @param lines The list of lines (StorageInfo or CellInfo objects) - * @param currentScroll Current scroll position - * @param rowsVisible Number of visible rows - * @param relMouseX Mouse X relative to GUI - * @param relMouseY Mouse Y relative to GUI - * @param ctx Render context for hover state tracking - */ - public void draw(List lines, int currentScroll, int rowsVisible, - int relMouseX, int relMouseY, RenderContext ctx) { - int y = GuiConstants.CONTENT_START_Y; - int visibleTop = GuiConstants.CONTENT_START_Y; - int visibleBottom = GuiConstants.CONTENT_START_Y + rowsVisible * ROW_HEIGHT; - int totalLines = lines.size(); - - for (int i = 0; i < rowsVisible && currentScroll + i < totalLines; i++) { - Object line = lines.get(currentScroll + i); - int lineIndex = currentScroll + i; - - boolean isHovered = relMouseX >= GuiConstants.HOVER_LEFT_EDGE && relMouseX < GuiConstants.HOVER_RIGHT_EDGE - && relMouseY >= y && relMouseY < y + ROW_HEIGHT; - - // Track hover state based on line type - if (isHovered) { - if (line instanceof CellInfo) { - ctx.hoveredLineIndex = lineIndex; - ctx.hoveredCellCell = (CellInfo) line; - ctx.hoveredCellStorage = ctx.storageMap.get(((CellInfo) line).getParentStorageId()); - ctx.hoveredCellSlotIndex = ((CellInfo) line).getSlot(); - // Draw hover background for cell lines - Gui.drawRect(GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y + ROW_HEIGHT - 1, GuiConstants.COLOR_ROW_HOVER); - } else if (line instanceof StorageInfo) { - ctx.hoveredStorageLine = (StorageInfo) line; - ctx.hoveredLineIndex = lineIndex; - } - } - - // Draw separator line above storage entries (except first one) - if (line instanceof StorageInfo && i > 0) { - Gui.drawRect(GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y, GuiConstants.COLOR_SEPARATOR); - } - - // Tree line parameters - boolean isFirstInGroup = isFirstInStorageGroup(lines, lineIndex); - boolean isLastInGroup = isLastInStorageGroup(lines, lineIndex); - boolean isFirstVisibleRow = (i == 0); - boolean isLastVisibleRow = (i == rowsVisible - 1) || (currentScroll + i == totalLines - 1); - boolean hasContentAbove = (lineIndex > 0) && !isFirstInGroup; - boolean hasContentBelow = (lineIndex < totalLines - 1) && !isLastInGroup; - - if (line instanceof StorageInfo) { - drawStorageLine((StorageInfo) line, y, lines, lineIndex, ctx); - } else if (line instanceof CellInfo) { - drawCellLine((CellInfo) line, y, relMouseX, relMouseY, - isFirstInGroup, isLastInGroup, visibleTop, visibleBottom, - isFirstVisibleRow, isLastVisibleRow, hasContentAbove, hasContentBelow, ctx); - } - - y += ROW_HEIGHT; - } - } - - private void drawStorageLine(StorageInfo storage, int y, List lines, int lineIndex, RenderContext ctx) { - // Track this storage for priority field rendering - ctx.visibleStorages.add(new RenderContext.VisibleStorageEntry(storage, y)); - - // Determine expansion state from TabStateManager so it's remembered per-tab - boolean isExpanded = TabStateManager.getInstance().isExpanded(TabStateManager.TabType.TERMINAL, storage.getId()); - - // Draw expand/collapse indicator - String expandIcon = isExpanded ? "[-]" : "[+]"; - fontRenderer.drawString(expandIcon, 167, y + 1, 0x606060); - - // Draw vertical tree line connecting to cells below (if expanded and has cells following) - // Terminal tab has no button covering the junction, so extend the line further up - // to properly connect with the cell's tree line (which starts at y - 3 of the cell row) - boolean hasCellsFollowing = isExpanded - && lineIndex + 1 < lines.size() - && lines.get(lineIndex + 1) instanceof CellInfo; - - if (hasCellsFollowing) { - int lineX = GUI_INDENT + 7; - Gui.drawRect(lineX, y + ROW_HEIGHT - 4, lineX + 1, y + ROW_HEIGHT, 0xFF808080); - } - - // Draw block icon - renderItemStack(storage.getBlockItem(), GUI_INDENT, y); - - // Draw name and location - String name = storage.getName(); - if (name.length() > 20) name = name.substring(0, 18) + "..."; - int nameColor = storage.hasCustomName() ? 0xFF2E7D32 : 0x404040; - fontRenderer.drawString(name, GUI_INDENT + 20, y + 1, nameColor); - - String location = storage.getLocationString(); - fontRenderer.drawString(location, GUI_INDENT + 20, y + 9, 0x808080); - } - - private void drawCellLine(CellInfo cell, int y, int mouseX, int mouseY, - boolean isFirstInGroup, boolean isLastInGroup, int visibleTop, int visibleBottom, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, RenderContext ctx) { - - int lineX = GUI_INDENT + 7; - drawTreeLines(lineX, y, true, isFirstInGroup, isLastInGroup, - visibleTop, visibleBottom, isFirstVisibleRow, isLastVisibleRow, - hasContentAbove, hasContentBelow, false); - - // Terminal tab has no buttons covering the tree line, so fill the gap between cells - // The base drawTreeLines leaves a gap for buttons - draw an additional segment to connect - if (!isFirstInGroup && !isFirstVisibleRow) { - // Fill gap between previous cell's bottom and this cell's lineTop - Gui.drawRect(lineX, y - ROW_HEIGHT + 9, lineX + 1, y - 3, 0xFF808080); - } - - // Extend horizontal branch to reach the cell icon (covering the gap left by no button) - Gui.drawRect(lineX + 10, y + 8, CELL_INDENT, y + 9, 0xFF808080); - - // Draw upgrade icons to the left of the cell icon (with tracking) - drawCellUpgradeIcons(cell, 3, y, ctx); - - // Draw cell icon - renderItemStack(cell.getCellItem(), CELL_INDENT, y); - - // Draw cell name - String name = cell.getDisplayName(); - int decorLength = getDecorationLength(name); - if (name.length() - decorLength > 16) name = name.substring(0, 14 + decorLength) + "..."; - int cellNameColor = cell.hasCustomName() ? 0xFF2E7D32 : 0x404040; - fontRenderer.drawString(name, CELL_INDENT + 18, y + 1, cellNameColor); - - // Draw usage bar - int barX = CELL_INDENT + 18; - int barY = y + 10; - int barWidth = 80; - int barHeight = 4; - - // TODO: add tooltip showing full name and usage details - Gui.drawRect(barX, barY, barX + barWidth, barY + barHeight, 0xFF555555); - int filledWidth = (int) (barWidth * cell.getByteUsagePercent()); - int fillColor = getUsageColor(cell.getByteUsagePercent()); - if (filledWidth > 0) { - Gui.drawRect(barX, barY, barX + filledWidth, barY + barHeight, fillColor); - } - - // Check button hover states - boolean ejectHovered = mouseX >= GuiConstants.BUTTON_EJECT_X - && mouseX < GuiConstants.BUTTON_EJECT_X + GuiConstants.BUTTON_SIZE - && mouseY >= y + 1 && mouseY < y + 1 + GuiConstants.BUTTON_SIZE; - boolean invHovered = mouseX >= GuiConstants.BUTTON_INVENTORY_X - && mouseX < GuiConstants.BUTTON_INVENTORY_X + GuiConstants.BUTTON_SIZE - && mouseY >= y + 1 && mouseY < y + 1 + GuiConstants.BUTTON_SIZE; - boolean partHovered = mouseX >= GuiConstants.BUTTON_PARTITION_X - && mouseX < GuiConstants.BUTTON_PARTITION_X + GuiConstants.BUTTON_SIZE - && mouseY >= y + 1 && mouseY < y + 1 + GuiConstants.BUTTON_SIZE; - - // Draw buttons - drawButton(GuiConstants.BUTTON_EJECT_X, y + 1, GuiConstants.BUTTON_SIZE, "E", ejectHovered); - drawButton(GuiConstants.BUTTON_INVENTORY_X, y + 1, GuiConstants.BUTTON_SIZE, "I", invHovered); - drawButton(GuiConstants.BUTTON_PARTITION_X, y + 1, GuiConstants.BUTTON_SIZE, "P", partHovered); - - // Track hover state - if (ejectHovered) { - ctx.hoveredCell = cell; - ctx.hoverType = 3; - } else if (invHovered) { - ctx.hoveredCell = cell; - ctx.hoverType = 1; - } else if (partHovered) { - ctx.hoveredCell = cell; - ctx.hoverType = 2; - } - } -} diff --git a/src/main/java/com/cellterminal/gui/storagebus/StorageBusHoverState.java b/src/main/java/com/cellterminal/gui/storagebus/StorageBusHoverState.java deleted file mode 100644 index 82da38b..0000000 --- a/src/main/java/com/cellterminal/gui/storagebus/StorageBusHoverState.java +++ /dev/null @@ -1,186 +0,0 @@ -package com.cellterminal.gui.storagebus; - -import java.util.List; - -import net.minecraft.item.ItemStack; - -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.gui.render.RenderContext; - - -/** - * Tracks hover state for storage bus-related GUI elements. - *

- * This class encapsulates all the hover tracking for storage buses in the - * Storage Bus Inventory and Partition tabs. It is updated during rendering - * and consumed by click handlers and tooltip rendering. - */ -public class StorageBusHoverState { - - // Hovered storage bus - private StorageBusInfo hoveredStorageBus = null; - - // Hovered content slot in a storage bus - private int hoveredStorageBusContentSlot = -1; - private ItemStack hoveredContentStack = ItemStack.EMPTY; - private int hoveredContentX = 0; - private int hoveredContentY = 0; - - // Hovered partition slot - private int hoveredStorageBusPartitionSlot = -1; - - // Hovered action buttons - private StorageBusInfo hoveredClearButtonStorageBus = null; - private StorageBusInfo hoveredIOModeButtonStorageBus = null; - private StorageBusInfo hoveredPartitionAllButtonStorageBus = null; - - // Line hover - private int hoveredLineIndex = -1; - - // JEI ghost ingredient targets - private final List partitionSlotTargets; - - public StorageBusHoverState(List partitionSlotTargets) { - this.partitionSlotTargets = partitionSlotTargets; - } - - /** - * Reset all hover state at the beginning of a render cycle. - */ - public void reset() { - hoveredStorageBus = null; - hoveredStorageBusContentSlot = -1; - hoveredContentStack = ItemStack.EMPTY; - hoveredContentX = 0; - hoveredContentY = 0; - hoveredStorageBusPartitionSlot = -1; - hoveredClearButtonStorageBus = null; - hoveredIOModeButtonStorageBus = null; - hoveredPartitionAllButtonStorageBus = null; - hoveredLineIndex = -1; - partitionSlotTargets.clear(); - } - - // ======================================== - // STORAGE BUS HOVER - // ======================================== - - public void setHoveredStorageBus(StorageBusInfo storageBus) { - this.hoveredStorageBus = storageBus; - } - - public StorageBusInfo getHoveredStorageBus() { - return hoveredStorageBus; - } - - // ======================================== - // CONTENT SLOT HOVER - // ======================================== - - public void setHoveredContentSlot(int slotIndex, ItemStack stack, int absX, int absY) { - this.hoveredStorageBusContentSlot = slotIndex; - this.hoveredContentStack = stack; - this.hoveredContentX = absX; - this.hoveredContentY = absY; - } - - public int getHoveredStorageBusContentSlot() { - return hoveredStorageBusContentSlot; - } - - public ItemStack getHoveredContentStack() { - return hoveredContentStack; - } - - public int getHoveredContentX() { - return hoveredContentX; - } - - public int getHoveredContentY() { - return hoveredContentY; - } - - // ======================================== - // PARTITION SLOT HOVER - // ======================================== - - public void setHoveredPartitionSlot(int slotIndex) { - this.hoveredStorageBusPartitionSlot = slotIndex; - } - - public int getHoveredStorageBusPartitionSlot() { - return hoveredStorageBusPartitionSlot; - } - - // ======================================== - // BUTTON HOVER - // ======================================== - - public void setHoveredClearButton(StorageBusInfo storageBus) { - this.hoveredClearButtonStorageBus = storageBus; - } - - public StorageBusInfo getHoveredClearButtonStorageBus() { - return hoveredClearButtonStorageBus; - } - - public void setHoveredIOModeButton(StorageBusInfo storageBus) { - this.hoveredIOModeButtonStorageBus = storageBus; - } - - public StorageBusInfo getHoveredIOModeButtonStorageBus() { - return hoveredIOModeButtonStorageBus; - } - - public void setHoveredPartitionAllButton(StorageBusInfo storageBus) { - this.hoveredPartitionAllButtonStorageBus = storageBus; - } - - public StorageBusInfo getHoveredPartitionAllButtonStorageBus() { - return hoveredPartitionAllButtonStorageBus; - } - - // ======================================== - // LINE HOVER - // ======================================== - - public void setHoveredLineIndex(int lineIndex) { - this.hoveredLineIndex = lineIndex; - } - - public int getHoveredLineIndex() { - return hoveredLineIndex; - } - - // ======================================== - // JEI GHOST TARGETS - // ======================================== - - public void addPartitionSlotTarget(StorageBusInfo storageBus, int slotIndex, int x, int y, int width, int height) { - partitionSlotTargets.add(new RenderContext.StorageBusPartitionSlotTarget(storageBus, slotIndex, x, y, width, height)); - } - - public List getPartitionSlotTargets() { - return partitionSlotTargets; - } - - /** - * Copy hover state to a RenderContext for compatibility with existing code. - */ - public void copyToRenderContext(RenderContext ctx) { - ctx.hoveredStorageBus = hoveredStorageBus; - ctx.hoveredStorageBusContentSlot = hoveredStorageBusContentSlot; - ctx.hoveredStorageBusPartitionSlot = hoveredStorageBusPartitionSlot; - ctx.hoveredClearButtonStorageBus = hoveredClearButtonStorageBus; - ctx.hoveredIOModeButtonStorageBus = hoveredIOModeButtonStorageBus; - ctx.hoveredPartitionAllButtonStorageBus = hoveredPartitionAllButtonStorageBus; - ctx.hoveredLineIndex = hoveredLineIndex; - - // Copy content stack to context if set - if (!hoveredContentStack.isEmpty()) { - ctx.hoveredContentStack = hoveredContentStack; - ctx.hoveredContentX = hoveredContentX; - ctx.hoveredContentY = hoveredContentY; - } - } -} diff --git a/src/main/java/com/cellterminal/gui/storagebus/StorageBusRenderer.java b/src/main/java/com/cellterminal/gui/storagebus/StorageBusRenderer.java deleted file mode 100644 index 6bdec88..0000000 --- a/src/main/java/com/cellterminal/gui/storagebus/StorageBusRenderer.java +++ /dev/null @@ -1,616 +0,0 @@ -package com.cellterminal.gui.storagebus; - -import java.util.List; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.RenderItem; -import net.minecraft.item.ItemStack; - -import com.cellterminal.client.StorageBusContentRow; -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.client.TabStateManager; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.render.RenderContext; - - -/** - * Main renderer for storage bus-related GUI elements. - *

- * This class coordinates the rendering of storage buses in the Storage Bus - * Inventory and Partition tabs. It delegates to specialized sub-renderers - * for slots and content items. - *

- * Usage: - *

- * StorageBusRenderer renderer = new StorageBusRenderer(fontRenderer, itemRender);
- * // For inventory tab:
- * renderer.drawStorageBusInventoryLine(storageBus, startIndex, y, ...);
- * // For partition tab:
- * renderer.drawStorageBusPartitionLine(storageBus, startIndex, y, ...);
- * 
- */ -public class StorageBusRenderer { - - // IO Mode dot colors - private static final int COLOR_BLUE_EXTRACT = 0xFF4466FF; - private static final int COLOR_ORANGE_INSERT = 0xFFFF9933; - private static final int COLOR_GREY_NONE = 0xFF555555; - - // Color for custom-named storage buses (green, matching subnet custom names) - private static final int COLOR_NAME_CUSTOM = 0xFF2E7D32; - - private final FontRenderer fontRenderer; - private final StorageBusSlotRenderer slotRenderer; - - public StorageBusRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.fontRenderer = fontRenderer; - this.slotRenderer = new StorageBusSlotRenderer(fontRenderer, itemRender); - } - - /** - * Get the slot renderer for custom slot drawing. - */ - public StorageBusSlotRenderer getSlotRenderer() { - return slotRenderer; - } - - // ======================================== - // HEADER RENDERING - // ======================================== - - /** - * Draw a storage bus header row. - * - * @param storageBus The storage bus info - * @param y Y position - * @param lines All lines in the list - * @param lineIndex Current line index - * @param tabType The tab type for determining expansion state - * @param mouseX Relative mouse X - * @param mouseY Relative mouse Y - * @param absMouseX Absolute mouse X - * @param absMouseY Absolute mouse Y - * @param ctx Render context - */ - public void drawStorageBusHeader(StorageBusInfo storageBus, int y, - List lines, int lineIndex, - TabStateManager.TabType tabType, - int mouseX, int mouseY, int absMouseX, int absMouseY, - RenderContext ctx) { - - // Track this storage bus for priority field rendering - ctx.visibleStorageBuses.add(new RenderContext.VisibleStorageBusEntry(storageBus, y)); - - // Draw expand/collapse indicator - boolean isExpanded = TabStateManager.getInstance().isBusExpanded(tabType, storageBus.getId()); - String expandIcon = isExpanded ? "[-]" : "[+]"; - fontRenderer.drawString(expandIcon, 167, y + 1, 0x606060); - - // Draw vertical tree line connecting to content rows below (only if expanded) - boolean hasContentFollowing = isExpanded - && lineIndex + 1 < lines.size() - && lines.get(lineIndex + 1) instanceof StorageBusContentRow; - - if (hasContentFollowing) { - int lineX = GuiConstants.GUI_INDENT + 7; - Gui.drawRect(lineX, y + GuiConstants.ROW_HEIGHT - 1, lineX + 1, y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_TREE_LINE); - } - - // Draw connected inventory icon - ItemStack connectedIcon = storageBus.getConnectedInventoryIcon(); - if (!connectedIcon.isEmpty()) slotRenderer.renderItemStack(connectedIcon, GuiConstants.GUI_INDENT, y); - - // Draw name and location (use pixel-based truncation) - int nameX = GuiConstants.GUI_INDENT + 20; - int nameMaxWidth = GuiConstants.BUTTON_IO_MODE_X - nameX - 4; - String name = storageBus.getLocalizedName(); - String displayName = trimTextToWidth(name, nameMaxWidth); - int nameColor = storageBus.hasCustomName() ? COLOR_NAME_CUSTOM : GuiConstants.COLOR_TEXT_NORMAL; - fontRenderer.drawString(displayName, nameX, y + 1, nameColor); - - int locationMaxWidth = GuiConstants.BUTTON_IO_MODE_X - nameX - 4; - String location = storageBus.getLocationString(); - String displayLocation = trimTextToWidth(location, locationMaxWidth); - fontRenderer.drawString(displayLocation, nameX, y + 9, GuiConstants.COLOR_TEXT_SECONDARY); - - // Draw IO Mode button - int buttonX = GuiConstants.BUTTON_IO_MODE_X; - int buttonY = y; - int buttonSize = GuiConstants.SMALL_BUTTON_SIZE; - - boolean ioModeHovered = mouseX >= buttonX && mouseX < buttonX + buttonSize - && mouseY >= buttonY && mouseY < buttonY + buttonSize; - - drawIOModeDotButton(buttonX, buttonY, buttonSize, storageBus.getAccessRestriction(), ioModeHovered); - - if (ioModeHovered) ctx.hoveredIOModeButtonStorageBus = storageBus; - - // Header hover detection (up to the expand button and IO mode area) - int headerRightBound = buttonX; - boolean headerHovered = mouseX >= GuiConstants.GUI_INDENT && mouseX < headerRightBound - && mouseY >= y && mouseY < y + GuiConstants.ROW_HEIGHT; - - // Also treat the expand/collapse icon area as part of the header for hover/click purposes - boolean expandIconHovered = mouseX >= 165 && mouseX < 180 && mouseY >= y && mouseY < y + GuiConstants.ROW_HEIGHT; - - if (headerHovered) { - Gui.drawRect(GuiConstants.GUI_INDENT, y, headerRightBound, y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_STORAGE_HEADER_HOVER); - ctx.hoveredStorageBus = storageBus; - ctx.hoveredContentStack = ItemStack.EMPTY; - ctx.hoveredContentX = absMouseX; - ctx.hoveredContentY = absMouseY; - } else if (expandIconHovered) { - // When hovering the expand icon we still want clicks to target this storage bus - ctx.hoveredStorageBus = storageBus; - ctx.hoveredContentStack = ItemStack.EMPTY; - ctx.hoveredContentX = absMouseX; - ctx.hoveredContentY = absMouseY; - } - - // Draw upgrade icons - drawUpgradeIcons(storageBus, 3, y - 1, ctx); - } - - /** - * Draw IO mode button with colored dot. - */ - private void drawIOModeDotButton(int x, int y, int size, int accessMode, boolean hovered) { - int btnColor = hovered ? GuiConstants.COLOR_BUTTON_HOVER : GuiConstants.COLOR_BUTTON_NORMAL; - - Gui.drawRect(x, y, x + size, y + size, btnColor); - Gui.drawRect(x, y, x + size, y + 1, GuiConstants.COLOR_BUTTON_SHADOW); - Gui.drawRect(x, y, x + 1, y + size, GuiConstants.COLOR_BUTTON_SHADOW); - Gui.drawRect(x, y + size - 1, x + size, y + size, GuiConstants.COLOR_BUTTON_SHADOW); - Gui.drawRect(x + size - 1, y, x + size, y + size, GuiConstants.COLOR_BUTTON_SHADOW); - - drawIOModeDot(x + 1, y + 1, size - 2, accessMode); - } - - /** - * Draw a storage bus header row for the Partition tab. - * Includes selection highlighting support. - * - * @param storageBus The storage bus info - * @param y Y position - * @param lines All lines in the list - * @param lineIndex Current line index - * @param tabType The tab type for determining expansion state - * @param mouseX Relative mouse X - * @param mouseY Relative mouse Y - * @param absMouseX Absolute mouse X - * @param absMouseY Absolute mouse Y - * @param guiLeft GUI left position - * @param guiTop GUI top position - * @param ctx Render context - */ - public void drawStorageBusPartitionHeader(StorageBusInfo storageBus, int y, - List lines, int lineIndex, - TabStateManager.TabType tabType, - int mouseX, int mouseY, int absMouseX, int absMouseY, - int guiLeft, int guiTop, RenderContext ctx) { - - // Track this storage bus for priority field rendering - ctx.visibleStorageBuses.add(new RenderContext.VisibleStorageBusEntry(storageBus, y)); - - // Check if this storage bus is selected - boolean isSelected = ctx.selectedStorageBusIds != null - && ctx.selectedStorageBusIds.contains(storageBus.getId()); - - // Draw selection background - if (isSelected) { - Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_SELECTION); - } - - // Draw expand/collapse indicator - boolean isExpanded = TabStateManager.getInstance().isBusExpanded(tabType, storageBus.getId()); - String expandIcon = isExpanded ? "[-]" : "[+]"; - fontRenderer.drawString(expandIcon, 167, y + 1, 0x606060); - - // Draw vertical tree line connecting to content rows below (only if expanded) - boolean hasContentFollowing = isExpanded - && lineIndex + 1 < lines.size() && lines.get(lineIndex + 1) instanceof StorageBusContentRow; - - if (hasContentFollowing) { - int lineX = GuiConstants.GUI_INDENT + 7; - Gui.drawRect(lineX, y + GuiConstants.ROW_HEIGHT - 1, lineX + 1, y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_TREE_LINE); - } - - // Draw connected inventory icon - ItemStack connectedIcon = storageBus.getConnectedInventoryIcon(); - if (!connectedIcon.isEmpty()) slotRenderer.renderItemStack(connectedIcon, GuiConstants.GUI_INDENT, y); - - // Draw name and location (use pixel-based truncation) - int nameX = GuiConstants.GUI_INDENT + 20; - int nameMaxWidth = GuiConstants.BUTTON_IO_MODE_X - nameX - 4; - String name = storageBus.getLocalizedName(); - String displayName = trimTextToWidth(name, nameMaxWidth); - // Use blue for selected, green for custom-named, normal otherwise - int nameColor; - if (isSelected) { - nameColor = 0x204080; - } else if (storageBus.hasCustomName()) { - nameColor = COLOR_NAME_CUSTOM; - } else { - nameColor = GuiConstants.COLOR_TEXT_NORMAL; - } - fontRenderer.drawString(displayName, nameX, y + 1, nameColor); - - int locationMaxWidth = GuiConstants.BUTTON_IO_MODE_X - nameX - 4; - String location = storageBus.getLocationString(); - String displayLocation = trimTextToWidth(location, locationMaxWidth); - fontRenderer.drawString(displayLocation, nameX, y + 9, GuiConstants.COLOR_TEXT_SECONDARY); - - // Draw IO Mode button - int buttonX = GuiConstants.BUTTON_IO_MODE_X; - int buttonY = y; - int buttonSize = GuiConstants.SMALL_BUTTON_SIZE; - - boolean ioModeHovered = mouseX >= buttonX && mouseX < buttonX + buttonSize - && mouseY >= buttonY && mouseY < buttonY + buttonSize; - - drawIOModeDotButton(buttonX, buttonY, buttonSize, storageBus.getAccessRestriction(), - ioModeHovered); - - if (ioModeHovered) ctx.hoveredIOModeButtonStorageBus = storageBus; - - // Header hover detection (up to the expand button, excluding IO mode button area) - int headerRightBound = buttonX; - boolean headerHovered = mouseX >= GuiConstants.GUI_INDENT && mouseX < headerRightBound - && mouseY >= y && mouseY < y + GuiConstants.ROW_HEIGHT; - - boolean expandIconHovered = mouseX >= 165 && mouseX < 180 && mouseY >= y && mouseY < y + GuiConstants.ROW_HEIGHT; - - if (headerHovered) { - Gui.drawRect(GuiConstants.GUI_INDENT, y, headerRightBound, y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_STORAGE_HEADER_HOVER); - ctx.hoveredStorageBus = storageBus; - ctx.hoveredContentStack = ItemStack.EMPTY; - ctx.hoveredContentX = absMouseX; - ctx.hoveredContentY = absMouseY; - } else if (expandIconHovered) { - ctx.hoveredStorageBus = storageBus; - ctx.hoveredContentStack = ItemStack.EMPTY; - ctx.hoveredContentX = absMouseX; - ctx.hoveredContentY = absMouseY; - } - - // Draw upgrade icons - drawUpgradeIcons(storageBus, 3, y - 1, ctx); - } - - /** - * Draw upgrade icons in 2 columns with hover tracking. - * Icons are positioned based on their actual slot index to preserve gaps. - */ - private void drawUpgradeIcons(StorageBusInfo storageBus, int x, int y, RenderContext ctx) { - List upgrades = storageBus.getUpgrades(); - if (upgrades.isEmpty()) return; - - int iconSize = 8; - int spacing = 1; - int cols = 2; - - // Render each upgrade at its actual slot position in the 2-column grid - for (int i = 0; i < Math.min(upgrades.size(), 5); i++) { - ItemStack upgrade = upgrades.get(i); - if (!upgrade.isEmpty()) { - int actualSlotIndex = storageBus.getUpgradeSlotIndex(i); - int col = actualSlotIndex % cols; - int row = actualSlotIndex / cols; - int iconX = x + col * (iconSize + spacing); - int iconY = y + 1 + row * (iconSize + spacing); - slotRenderer.renderSmallItemStack(upgrade, iconX, iconY); - - // Track upgrade icon position for tooltip and click handling - if (ctx != null) { - ctx.upgradeIconTargets.add(new RenderContext.UpgradeIconTarget( - storageBus, upgrade, actualSlotIndex, ctx.guiLeft + iconX, ctx.guiTop + iconY)); - } - } - } - } - - // ======================================== - // INVENTORY LINE RENDERING - // ======================================== - - /** - * Draw a storage bus inventory line (content items). - */ - public void drawStorageBusInventoryLine(StorageBusInfo storageBus, int startIndex, - int y, int mouseX, int mouseY, - int absMouseX, int absMouseY, - boolean isFirstInGroup, boolean isLastInGroup, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, - RenderContext ctx) { - - int lineX = GuiConstants.GUI_INDENT + 7; - int visibleTop = GuiConstants.CONTENT_START_Y; - int visibleBottom = GuiConstants.CONTENT_START_Y + ctx.rowsVisible * GuiConstants.ROW_HEIGHT; - - // Draw tree lines - drawTreeLines(lineX, y, isFirstInGroup, isLastInGroup, visibleTop, visibleBottom, - isFirstVisibleRow, isLastVisibleRow, hasContentAbove, hasContentBelow); - - // Draw partition-all button for first row - if (isFirstInGroup) drawPartitionAllButton(storageBus, lineX, y, mouseX, mouseY, ctx); - - // Draw content slots - drawInventoryContentSlots(storageBus, startIndex, y, mouseX, mouseY, absMouseX, absMouseY, ctx); - } - - private void drawTreeLines(int lineX, int y, boolean isFirstInGroup, boolean isLastInGroup, - int visibleTop, int visibleBottom, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow) { - - int lineTop = isFirstVisibleRow && hasContentAbove ? visibleTop : y - 4; - int lineBottom = isLastInGroup ? y + 9 : (isLastVisibleRow && hasContentBelow ? visibleBottom : y + GuiConstants.ROW_HEIGHT); - - if (lineTop < visibleTop) lineTop = visibleTop; - - // Vertical line - Gui.drawRect(lineX, lineTop, lineX + 1, lineBottom, GuiConstants.COLOR_TREE_LINE); - - // Horizontal branch - Gui.drawRect(lineX, y + 8, lineX + 10, y + 9, GuiConstants.COLOR_TREE_LINE); - } - - private void drawPartitionAllButton(StorageBusInfo storageBus, int lineX, int y, int mouseX, int mouseY, RenderContext ctx) { - int buttonX = lineX - 3; - int buttonY = y + 4; - int buttonSize = GuiConstants.SMALL_BUTTON_SIZE; - - boolean hovered = mouseX >= buttonX && mouseX < buttonX + buttonSize - && mouseY >= buttonY && mouseY < buttonY + buttonSize; - - // Draw background - Gui.drawRect(buttonX - 1, buttonY - 1, buttonX + buttonSize + 1, buttonY + buttonSize + 1, GuiConstants.COLOR_SLOT_BACKGROUND); - - // Draw button with green fill - slotRenderer.drawSmallButton(buttonX, buttonY, hovered, GuiConstants.COLOR_BUTTON_GREEN); - - if (hovered) ctx.hoveredPartitionAllButtonStorageBus = storageBus; - } - - private void drawInventoryContentSlots(StorageBusInfo storageBus, int startIndex, int y, - int mouseX, int mouseY, int absMouseX, int absMouseY, - RenderContext ctx) { - - List contents = storageBus.getContents(); - List partition = storageBus.getPartition(); - int slotStartX = GuiConstants.CELL_INDENT + 4; - - for (int i = 0; i < GuiConstants.STORAGE_BUS_SLOTS_PER_ROW; i++) { - int contentIndex = startIndex + i; - int slotX = slotStartX + (i * GuiConstants.MINI_SLOT_SIZE); - - slotRenderer.drawSlotBackground(slotX, y); - - if (contentIndex >= contents.size()) continue; - - ItemStack stack = contents.get(contentIndex); - if (stack.isEmpty()) continue; - - slotRenderer.renderItemStack(stack, slotX, y); - - // Draw partition indicator - if (slotRenderer.isInPartition(stack, partition)) slotRenderer.drawPartitionIndicator(slotX, y); - - // Draw item count - slotRenderer.drawItemCount(storageBus.getContentCount(contentIndex), slotX, y); - - // Check hover - if (mouseX >= slotX && mouseX < slotX + GuiConstants.MINI_SLOT_SIZE - && mouseY >= y && mouseY < y + GuiConstants.MINI_SLOT_SIZE) { - slotRenderer.drawSlotHoverHighlight(slotX, y); - ctx.hoveredContentStack = stack; - ctx.hoveredContentX = absMouseX; - ctx.hoveredContentY = absMouseY; - ctx.hoveredStorageBusContentSlot = contentIndex; - ctx.hoveredStorageBus = storageBus; - } - } - } - - // ======================================== - // PARTITION LINE RENDERING - // ======================================== - - /** - * Draw a storage bus partition line (partition slots). - * Overload without selection support (selection is handled at tab level). - */ - public void drawStorageBusPartitionLine(StorageBusInfo storageBus, int startIndex, - int y, int mouseX, int mouseY, - int absMouseX, int absMouseY, - boolean isFirstInGroup, boolean isLastInGroup, - int visibleTop, int visibleBottom, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, - int guiLeft, int guiTop, - RenderContext ctx) { - - int lineX = GuiConstants.GUI_INDENT + 7; - - // Draw tree lines - drawTreeLines(lineX, y, isFirstInGroup, isLastInGroup, visibleTop, visibleBottom, - isFirstVisibleRow, isLastVisibleRow, hasContentAbove, hasContentBelow); - - // Draw clear button for first row - if (isFirstInGroup) drawClearButton(storageBus, lineX, y, mouseX, mouseY, ctx); - - // Draw partition slots - drawPartitionSlots(storageBus, startIndex, y, mouseX, mouseY, absMouseX, absMouseY, guiLeft, guiTop, ctx); - } - - /** - * Draw a storage bus partition line (partition slots) with selection support. - */ - public void drawStorageBusPartitionLineWithSelection(StorageBusInfo storageBus, int startIndex, - int y, int mouseX, int mouseY, - int absMouseX, int absMouseY, - boolean isFirstInGroup, boolean isLastInGroup, - boolean isFirstVisibleRow, boolean isLastVisibleRow, - boolean hasContentAbove, boolean hasContentBelow, - boolean isSelected, int guiLeft, int guiTop, - RenderContext ctx) { - - int lineX = GuiConstants.GUI_INDENT + 7; - int visibleTop = GuiConstants.CONTENT_START_Y; - int visibleBottom = GuiConstants.CONTENT_START_Y + ctx.rowsVisible * GuiConstants.ROW_HEIGHT; - - // Draw tree lines - drawTreeLines(lineX, y, isFirstInGroup, isLastInGroup, visibleTop, visibleBottom, - isFirstVisibleRow, isLastVisibleRow, hasContentAbove, hasContentBelow); - - // Draw clear button for first row - if (isFirstInGroup) drawClearButton(storageBus, lineX, y, mouseX, mouseY, ctx); - - // Draw selection highlight - if (isSelected) { - Gui.drawRect(GuiConstants.GUI_INDENT - 2, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y + GuiConstants.ROW_HEIGHT - 1, GuiConstants.COLOR_SELECTION_HIGHLIGHT); - } - - // Draw partition slots - drawPartitionSlots(storageBus, startIndex, y, mouseX, mouseY, absMouseX, absMouseY, guiLeft, guiTop, ctx); - } - - private void drawClearButton(StorageBusInfo storageBus, int lineX, int y, int mouseX, int mouseY, RenderContext ctx) { - int buttonX = lineX - 3; - int buttonY = y + 4; - int buttonSize = GuiConstants.SMALL_BUTTON_SIZE; - - boolean hovered = mouseX >= buttonX && mouseX < buttonX + buttonSize - && mouseY >= buttonY && mouseY < buttonY + buttonSize; - - // Draw background - Gui.drawRect(buttonX - 1, buttonY - 1, buttonX + buttonSize + 1, buttonY + buttonSize + 1, GuiConstants.COLOR_SLOT_BACKGROUND); - - // Draw button with red fill - slotRenderer.drawSmallButton(buttonX, buttonY, hovered, GuiConstants.COLOR_BUTTON_RED); - - if (hovered) ctx.hoveredClearButtonStorageBus = storageBus; - } - - private void drawPartitionSlots(StorageBusInfo storageBus, int startIndex, int y, - int mouseX, int mouseY, int absMouseX, int absMouseY, - int guiLeft, int guiTop, RenderContext ctx) { - - List partition = storageBus.getPartition(); - int maxSlots = storageBus.getAvailableConfigSlots(); - int slotStartX = GuiConstants.CELL_INDENT + 4; - - for (int i = 0; i < GuiConstants.STORAGE_BUS_SLOTS_PER_ROW; i++) { - int partitionIndex = startIndex + i; - if (partitionIndex >= maxSlots) break; - - int slotX = slotStartX + (i * GuiConstants.MINI_SLOT_SIZE); - - // Draw partition slot with amber tint - slotRenderer.drawPartitionSlotBackground(slotX, y); - - // Register JEI ghost target - ctx.storageBusPartitionSlotTargets.add(new RenderContext.StorageBusPartitionSlotTarget( - storageBus, partitionIndex, guiLeft + slotX, guiTop + y, - GuiConstants.MINI_SLOT_SIZE, GuiConstants.MINI_SLOT_SIZE)); - - // Draw partition item if present - ItemStack partItem = partitionIndex < partition.size() ? partition.get(partitionIndex) : ItemStack.EMPTY; - if (!partItem.isEmpty()) slotRenderer.renderItemStack(partItem, slotX, y); - - // Check hover - if (mouseX >= slotX && mouseX < slotX + GuiConstants.MINI_SLOT_SIZE - && mouseY >= y && mouseY < y + GuiConstants.MINI_SLOT_SIZE) { - slotRenderer.drawSlotHoverHighlight(slotX, y); - ctx.hoveredStorageBusPartitionSlot = partitionIndex; - ctx.hoveredStorageBus = storageBus; - - if (!partItem.isEmpty()) { - ctx.hoveredContentStack = partItem; - ctx.hoveredContentX = absMouseX; - ctx.hoveredContentY = absMouseY; - } - } - } - } - - // ======================================== - // IO MODE RENDERING - // ======================================== - - /** - * Draw a colored dot indicating IO mode. - * - * @param x Left edge of dot area - * @param y Top edge of dot area - * @param size Size of dot area - * @param accessMode 0=NO_ACCESS, 1=READ, 2=WRITE, 3=READ_WRITE - */ - private void drawIOModeDot(int x, int y, int size, int accessMode) { - int color1, color2; - - switch (accessMode) { - case 1: // READ - extract only - color1 = COLOR_BLUE_EXTRACT; - color2 = COLOR_BLUE_EXTRACT; - break; - case 2: // WRITE - insert only - color1 = COLOR_ORANGE_INSERT; - color2 = COLOR_ORANGE_INSERT; - break; - case 3: // READ_WRITE - both - color1 = COLOR_BLUE_EXTRACT; - color2 = COLOR_ORANGE_INSERT; - break; - default: // NO_ACCESS - color1 = COLOR_GREY_NONE; - color2 = COLOR_GREY_NONE; - break; - } - - // For mixed mode, draw diagonal split - if (accessMode == 3) { - drawDiagonalSplit(x, y, size, color1, color2); - } else { - Gui.drawRect(x, y, x + size, y + size, color1); - } - } - - private void drawDiagonalSplit(int x, int y, int size, int color1, int color2) { - for (int py = 0; py < size; py++) { - for (int px = 0; px < size; px++) { - int color = (px + py < size) ? color1 : color2; - Gui.drawRect(x + px, y + py, x + px + 1, y + py + 1, color); - } - } - } - - /** - * Trim text to fit within maxWidth pixels, adding ellipsis if needed. - */ - private String trimTextToWidth(String text, int maxWidth) { - if (fontRenderer.getStringWidth(text) <= maxWidth) return text; - - String ellipsis = "..."; - int ellipsisWidth = fontRenderer.getStringWidth(ellipsis); - int availableWidth = maxWidth - ellipsisWidth; - - if (availableWidth <= 0) return ellipsis; - - // Binary search for the right truncation point - int low = 0; - int high = text.length(); - while (low < high) { - int mid = (low + high + 1) / 2; - if (fontRenderer.getStringWidth(text.substring(0, mid)) <= availableWidth) { - low = mid; - } else { - high = mid - 1; - } - } - - return text.substring(0, low) + ellipsis; - } -} diff --git a/src/main/java/com/cellterminal/gui/storagebus/StorageBusSlotRenderer.java b/src/main/java/com/cellterminal/gui/storagebus/StorageBusSlotRenderer.java deleted file mode 100644 index b4305d9..0000000 --- a/src/main/java/com/cellterminal/gui/storagebus/StorageBusSlotRenderer.java +++ /dev/null @@ -1,245 +0,0 @@ -package com.cellterminal.gui.storagebus; - -import java.util.List; - -import org.lwjgl.opengl.GL11; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.client.renderer.RenderItem; -import net.minecraft.item.ItemStack; - -import appeng.api.config.AccessRestriction; -import appeng.util.ReadableNumberConverter; - -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.gui.ComparisonUtils; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.render.RenderContext; - - -/** - * Handles rendering of storage bus slots, content items, and partition slots. - *

- * This class provides low-level drawing operations for storage bus-related GUI elements. - * It extends cell slot rendering with storage bus-specific features like IO mode indicators. - */ -public class StorageBusSlotRenderer { - - private final FontRenderer fontRenderer; - private final RenderItem itemRender; - - public StorageBusSlotRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.fontRenderer = fontRenderer; - this.itemRender = itemRender; - } - - /** - * Draw a standard slot background with 3D borders. - */ - public void drawSlotBackground(int x, int y) { - int size = GuiConstants.MINI_SLOT_SIZE; - Gui.drawRect(x, y, x + size, y + size, GuiConstants.COLOR_SLOT_BACKGROUND); - Gui.drawRect(x, y, x + size - 1, y + 1, GuiConstants.COLOR_SLOT_BORDER_DARK); - Gui.drawRect(x, y, x + 1, y + size - 1, GuiConstants.COLOR_SLOT_BORDER_DARK); - Gui.drawRect(x + 1, y + size - 1, x + size, y + size, GuiConstants.COLOR_SLOT_BORDER_LIGHT); - Gui.drawRect(x + size - 1, y + 1, x + size, y + size - 1, GuiConstants.COLOR_SLOT_BORDER_LIGHT); - } - - /** - * Draw a partition slot background with amber tint. - */ - public void drawPartitionSlotBackground(int x, int y) { - drawSlotBackground(x, y); - int inner = GuiConstants.MINI_SLOT_SIZE - 1; - Gui.drawRect(x + 1, y + 1, x + inner, y + inner, GuiConstants.COLOR_PARTITION_SLOT_TINT); - } - - /** - * Draw a hover highlight over a slot. - */ - public void drawSlotHoverHighlight(int x, int y) { - int inner = GuiConstants.MINI_SLOT_SIZE - 1; - Gui.drawRect(x + 1, y + 1, x + inner, y + inner, GuiConstants.COLOR_HOVER_HIGHLIGHT); - } - - /** - * Render an item stack at the given position. - */ - public void renderItemStack(ItemStack stack, int x, int y) { - if (stack.isEmpty()) return; - - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - RenderHelper.enableGUIStandardItemLighting(); - itemRender.renderItemIntoGUI(stack, x, y); - RenderHelper.disableStandardItemLighting(); - GlStateManager.disableLighting(); - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - GlStateManager.enableBlend(); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - } - - /** - * Render a small (8x8) item icon at the given position. - */ - public void renderSmallItemStack(ItemStack stack, int x, int y) { - if (stack.isEmpty()) return; - - GlStateManager.pushMatrix(); - GlStateManager.translate(x, y, 0); - GlStateManager.scale(0.5f, 0.5f, 1.0f); - - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - RenderHelper.enableGUIStandardItemLighting(); - itemRender.renderItemIntoGUI(stack, 0, 0); - RenderHelper.disableStandardItemLighting(); - GlStateManager.disableLighting(); - - GlStateManager.popMatrix(); - - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - GlStateManager.enableBlend(); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - } - - /** - * Draw item count in the slot (bottom-right, small text). - */ - public void drawItemCount(long count, int slotX, int slotY) { - String countStr = formatItemCount(count); - if (countStr.isEmpty()) return; - - int countWidth = fontRenderer.getStringWidth(countStr); - int textX = slotX + GuiConstants.MINI_SLOT_SIZE - 1; - int textY = slotY + GuiConstants.MINI_SLOT_SIZE - 5; - - GlStateManager.disableDepth(); - GlStateManager.pushMatrix(); - GlStateManager.scale(0.5f, 0.5f, 0.5f); - fontRenderer.drawStringWithShadow(countStr, textX * 2 - countWidth, textY * 2, 0xFFFFFF); - GlStateManager.popMatrix(); - GlStateManager.enableDepth(); - } - - /** - * Draw partition indicator ("P") in the top-left corner. - */ - public void drawPartitionIndicator(int slotX, int slotY) { - GlStateManager.disableLighting(); - GlStateManager.disableDepth(); - GlStateManager.pushMatrix(); - GlStateManager.scale(0.5f, 0.5f, 0.5f); - fontRenderer.drawStringWithShadow("P", (slotX + 1) * 2, (slotY + 1) * 2, GuiConstants.COLOR_PARTITION_INDICATOR); - GlStateManager.popMatrix(); - GlStateManager.enableDepth(); - } - - /** - * Draw storage bus upgrade icons on the header row. - * - * @param storageBus The storage bus info - * @param x X position to start - * @param y Y position of the row - * @return Width consumed by upgrade icons - */ - public int drawUpgradeIcons(StorageBusInfo storageBus, int x, int y) { - return drawUpgradeIcons(storageBus, x, y, null, 0, 0); - } - - /** - * Draw storage bus upgrade icons on the header row, with hover tracking. - * - * @param storageBus The storage bus info - * @param x X position to start (relative to GUI) - * @param y Y position of the row (relative to GUI) - * @param ctx Optional render context for tracking upgrade icon positions - * @param guiLeft GUI left offset for absolute position calculation - * @param guiTop GUI top offset for absolute position calculation - * @return Width consumed by upgrade icons - */ - public int drawUpgradeIcons(StorageBusInfo storageBus, int x, int y, RenderContext ctx, int guiLeft, int guiTop) { - List upgrades = storageBus.getUpgrades(); - if (upgrades.isEmpty()) return 0; - - int iconX = x; - - for (int i = 0; i < upgrades.size(); i++) { - ItemStack upgrade = upgrades.get(i); - renderSmallItemStack(upgrade, iconX, y); - - // Track upgrade icon position for tooltip and click handling - // Use actual slot index from the upgrade inventory, not the iteration index - if (ctx != null) { - int actualSlotIndex = storageBus.getUpgradeSlotIndex(i); - ctx.upgradeIconTargets.add(new RenderContext.UpgradeIconTarget( - storageBus, upgrade, actualSlotIndex, guiLeft + iconX, guiTop + y)); - } - - iconX += 9; // 8px icon + 1px spacing - } - - return iconX - x; - } - - /** - * Draw a small action button (IO mode, clear, partition-all). - */ - public void drawSmallButton(int x, int y, boolean hovered, int fillColor) { - int size = GuiConstants.SMALL_BUTTON_SIZE; - int btnColor = hovered ? GuiConstants.COLOR_BUTTON_HOVER : GuiConstants.COLOR_BUTTON_NORMAL; - - Gui.drawRect(x, y, x + size, y + size, btnColor); - Gui.drawRect(x, y, x + size, y + 1, GuiConstants.COLOR_BUTTON_HIGHLIGHT); - Gui.drawRect(x, y, x + 1, y + size, GuiConstants.COLOR_BUTTON_HIGHLIGHT); - Gui.drawRect(x, y + size - 1, x + size, y + size, GuiConstants.COLOR_BUTTON_SHADOW); - Gui.drawRect(x + size - 1, y, x + size, y + size, GuiConstants.COLOR_BUTTON_SHADOW); - - if (fillColor != 0) Gui.drawRect(x + 1, y + 1, x + size - 1, y + size - 1, fillColor); - } - - /** - * Draw IO mode colored dot inside a button area. - * - * @param x Left edge of dot area - * @param y Top edge of dot area - * @param size Size of dot area - * @param accessRestriction Current access mode - */ - public void drawIOModeDot(int x, int y, int size, AccessRestriction accessRestriction) { - int color; - - switch (accessRestriction) { - case READ: - color = 0xFF55FF55; // Green for read-only - break; - case WRITE: - color = 0xFFFF5555; // Red for write-only - break; - case READ_WRITE: - default: - color = 0xFF5555FF; // Blue for read-write - break; - } - - Gui.drawRect(x, y, x + size, y + size, color); - } - - /** - * Format item count for display. - */ - public String formatItemCount(long count) { - if (count < 1000) return String.valueOf(count); - - return ReadableNumberConverter.INSTANCE.toWideReadableForm(count); - } - - /** - * Check if an item is in the partition list. - * Uses fluid-aware comparison for fluid items (compares by fluid type only). - */ - public boolean isInPartition(ItemStack stack, List partition) { - return ComparisonUtils.isInPartition(stack, partition); - } -} diff --git a/src/main/java/com/cellterminal/gui/storagebus/package-info.java b/src/main/java/com/cellterminal/gui/storagebus/package-info.java deleted file mode 100644 index 690adde..0000000 --- a/src/main/java/com/cellterminal/gui/storagebus/package-info.java +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Cell Terminal - Storage Bus Rendering Module - *

- * This package contains all storage bus-specific GUI rendering and interaction logic. - * It handles the display and behavior of storage buses (items, fluids, essentia) - * in the Storage Bus Inventory and Storage Bus Partition tabs. - *

- * Key Classes: - * - {@link com.cellterminal.gui.storagebus.StorageBusRenderer} - Core rendering for storage bus rows - * - {@link com.cellterminal.gui.storagebus.StorageBusHoverState} - Hover tracking for storage buses - * - {@link com.cellterminal.gui.storagebus.StorageBusSlotRenderer} - Slot rendering utilities - *

- * This module should NOT contain any cell-related logic. - * Cell rendering is handled in {@link com.cellterminal.gui.cells}. - * - * @see com.cellterminal.gui.render.StorageBusInventoryTabRenderer - * @see com.cellterminal.gui.render.StorageBusPartitionTabRenderer - */ -package com.cellterminal.gui.storagebus; diff --git a/src/main/java/com/cellterminal/gui/subnet/GuiBackButton.java b/src/main/java/com/cellterminal/gui/subnet/GuiBackButton.java deleted file mode 100644 index 598a014..0000000 --- a/src/main/java/com/cellterminal/gui/subnet/GuiBackButton.java +++ /dev/null @@ -1,106 +0,0 @@ -package com.cellterminal.gui.subnet; - -import java.util.ArrayList; -import java.util.List; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.resources.I18n; - - -/** - * A small button used to navigate to/from subnet overview mode. - * When in normal view (main or subnet), shows a left arrow to enter overview. - * When in overview mode, shows a right arrow to go back. - */ -public class GuiBackButton extends GuiButton { - - public static final int BUTTON_SIZE = 12; - - private boolean isInOverviewMode; - - public GuiBackButton(int buttonId, int x, int y) { - super(buttonId, x, y, BUTTON_SIZE, BUTTON_SIZE, ""); - this.isInOverviewMode = false; - } - - /** - * Update the button state based on whether we're in subnet overview mode. - */ - public void setInOverviewMode(boolean inOverview) { - this.isInOverviewMode = inOverview; - } - - - public boolean isInOverviewMode() { - return isInOverviewMode; - } - - @Override - public void drawButton(Minecraft mc, int mouseX, int mouseY, float partialTicks) { - if (!this.visible) return; - - this.hovered = mouseX >= this.x && mouseY >= this.y - && mouseX < this.x + this.width && mouseY < this.y + this.height; - - // Draw button background (matching GuiSearchHelpButton) - int bgColor = this.hovered ? 0xFF505050 : 0xFF606060; - drawRect(this.x, this.y, this.x + this.width, this.y + this.height, bgColor); - - // Draw border (matching GuiSearchHelpButton) - drawRect(this.x, this.y, this.x + this.width, this.y + 1, 0xFF808080); - drawRect(this.x, this.y, this.x + 1, this.y + this.height, 0xFF808080); - drawRect(this.x, this.y + this.height - 1, this.x + this.width, this.y + this.height, 0xFF303030); - drawRect(this.x + this.width - 1, this.y, this.x + this.width, this.y + this.height, 0xFF303030); - - // Draw arrow icon - int iconColor = this.hovered ? 0xFFFFFF00 : 0xFFCCCCCC; - if (isInOverviewMode) { - // Right arrow when in overview (return to main/subnet view) - drawRightArrow(mc, iconColor); - } else { - // Left arrow when in normal view (enter subnet overview) - drawLeftArrow(mc, iconColor); - } - } - - private void drawLeftArrow(Minecraft mc, int color) { - // TODO: we need the arrow just a pixel lower, but scaling messes with that. - // Should add custom textures for the buttons. - // Draw a simple left arrow: < - int cx = this.x + this.width / 2 - mc.fontRenderer.getStringWidth("◀") / 2; // Center the arrow - int cy = this.y + this.height / 2 - mc.fontRenderer.FONT_HEIGHT / 2 - 4; - - GlStateManager.pushMatrix(); - GlStateManager.scale(2.0f, 2.0f, 1.0f); // Scale up for better visibility - mc.fontRenderer.drawString("◀", cx / 2, cy / 2, color); - GlStateManager.popMatrix(); - } - - private void drawRightArrow(Minecraft mc, int color) { - // Draw a simple right arrow: > - int cx = this.x + this.width / 2 - mc.fontRenderer.getStringWidth("▶") / 2; // Center the arrow - int cy = this.y + this.height / 2 - mc.fontRenderer.FONT_HEIGHT / 2 - 4; - - GlStateManager.pushMatrix(); - GlStateManager.scale(2.0f, 2.0f, 1.0f); // Scale up for better visibility - mc.fontRenderer.drawString("▶", cx / 2, cy / 2, color); - GlStateManager.popMatrix(); - } - - /** - * Get the tooltip for this button. - */ - public List getTooltip() { - List tooltip = new ArrayList<>(); - - if (isInOverviewMode) { - tooltip.add(I18n.format("cellterminal.subnet.back")); - } else { - tooltip.add(I18n.format("cellterminal.subnet.overview")); - } - - return tooltip; - } -} diff --git a/src/main/java/com/cellterminal/gui/subnet/SubnetOverviewRenderer.java b/src/main/java/com/cellterminal/gui/subnet/SubnetOverviewRenderer.java deleted file mode 100644 index f98f793..0000000 --- a/src/main/java/com/cellterminal/gui/subnet/SubnetOverviewRenderer.java +++ /dev/null @@ -1,876 +0,0 @@ -package com.cellterminal.gui.subnet; - -import java.util.ArrayList; -import java.util.List; - -import org.lwjgl.input.Keyboard; -import org.lwjgl.opengl.GL11; - -import net.minecraft.client.gui.FontRenderer; -import net.minecraft.client.gui.Gui; -import net.minecraft.client.renderer.GlStateManager; -import net.minecraft.client.renderer.RenderHelper; -import net.minecraft.client.renderer.RenderItem; -import net.minecraft.client.resources.I18n; -import net.minecraft.item.ItemStack; - -import com.cellterminal.client.SubnetConnectionRow; -import com.cellterminal.client.SubnetInfo; -import com.cellterminal.gui.GuiConstants; - - -/** - * Renderer for the subnet overview mode. - * Displays a list of connected subnets in the same style as Storage Bus Partition tab (Tab 5): - * - Header row: Icon, name, location, [Load] button - * - Connection rows: Filter items displayed under each connection with tree lines - *

- * The renderer takes a flattened list where SubnetInfo objects are headers - * and SubnetConnectionRow objects are connection/filter detail rows. - */ -public class SubnetOverviewRenderer { - - // Layout constants - matching StorageBusRenderer style - private static final int ROW_HEIGHT = 18; - private static final int ICON_X = GuiConstants.GUI_INDENT; - private static final int STAR_X = 6; // Left sidebar, same as upgrade icons - private static final int STAR_WIDTH = 18; - private static final int NAME_X = GuiConstants.GUI_INDENT + 18; // After icon - private static final int LOAD_BUTTON_WIDTH = 28; - private static final int LOAD_BUTTON_MARGIN = 4; - private static final int LOAD_BUTTON_Y_OFFSET = 0; - - // Connection row layout (filter items under header) - private static final int TREE_LINE_X = GuiConstants.GUI_INDENT + 7; - private static final int FILTER_SLOTS_X = GuiConstants.CELL_INDENT + 4; - private static final int FILTER_SLOTS_PER_ROW = 9; - private static final int MINI_SLOT_SIZE = 16; - - // Colors - private static final int COLOR_NAME_NORMAL = GuiConstants.COLOR_TEXT_NORMAL; - private static final int COLOR_NAME_CUSTOM = 0xFF2E7D32; // Dark green for custom names - private static final int COLOR_NAME_INACCESSIBLE = 0xFF909090; // Medium gray - private static final int COLOR_OUTBOUND = 0xFF2E7D32; // Dark green arrow (out) - private static final int COLOR_INBOUND = 0xFF1565C0; // Dark blue arrow (in) - private static final int COLOR_TREE_LINE = GuiConstants.COLOR_TREE_LINE; - private static final int COLOR_MAIN_NETWORK = 0xFF00838F; // Dark cyan for main network - private static final int COLOR_FAVORITE_ON = 0xFFCC9900; // Darker amber for better contrast with C6C6C6 - private static final int COLOR_FAVORITE_OFF = 0xFF505050; // Dark gray for better contrast - - private final FontRenderer fontRenderer; - private final RenderItem itemRender; - - // Hover tracking - private int hoveredLineIndex = -1; - private HoverZone hoveredZone = HoverZone.NONE; - private SubnetInfo hoveredSubnet = null; - private SubnetConnectionRow hoveredConnectionRow = null; - private int hoveredFilterSlot = -1; - private ItemStack hoveredFilterStack = ItemStack.EMPTY; - - // Rename editing state - private SubnetInfo editingSubnet = null; // The subnet currently being renamed - private String editingText = ""; // Current text in the rename field - private int editingCursorPos = 0; // Cursor position for text editing - private int editingY = 0; // Y position of the editing field - - public enum HoverZone { - NONE, - STAR, - NAME, - LOAD_BUTTON, - FILTER_SLOT, - ENTRY // General entry hover (for double-click highlight) - } - - public SubnetOverviewRenderer(FontRenderer fontRenderer, RenderItem itemRender) { - this.fontRenderer = fontRenderer; - this.itemRender = itemRender; - } - - /** - * Draw the subnet overview content using a flattened line list. - * - * @param lines Mixed list of SubnetInfo (headers) and SubnetConnectionRow (filter rows) - * @param currentScroll Current scroll position - * @param rowsVisible Number of visible rows - * @param relMouseX Mouse X relative to GUI - * @param relMouseY Mouse Y relative to GUI - * @param guiLeft Left edge of GUI - * @param guiTop Top edge of GUI - * @return The hovered line index, or -1 if none - */ - public int draw(List lines, int currentScroll, int rowsVisible, - int relMouseX, int relMouseY, int guiLeft, int guiTop) { - // Reset hover state - this.hoveredLineIndex = -1; - this.hoveredZone = HoverZone.NONE; - this.hoveredSubnet = null; - this.hoveredConnectionRow = null; - this.hoveredFilterSlot = -1; - this.hoveredFilterStack = ItemStack.EMPTY; - - int y = GuiConstants.CONTENT_START_Y; - int totalLines = lines.size(); - int visibleTop = GuiConstants.CONTENT_START_Y; - int visibleBottom = GuiConstants.CONTENT_START_Y + rowsVisible * ROW_HEIGHT; - - // Draw each visible line - for (int i = 0; i < rowsVisible && currentScroll + i < totalLines; i++) { - Object line = lines.get(currentScroll + i); - int lineIndex = currentScroll + i; - - boolean isHovered = relMouseX >= GuiConstants.HOVER_LEFT_EDGE - && relMouseX < GuiConstants.HOVER_RIGHT_EDGE - && relMouseY >= y && relMouseY < y + ROW_HEIGHT; - - if (line instanceof SubnetInfo) { - SubnetInfo subnet = (SubnetInfo) line; - - if (isHovered) { - this.hoveredLineIndex = lineIndex; - this.hoveredSubnet = subnet; - this.hoveredZone = determineHeaderHoverZone(relMouseX, subnet); - Gui.drawRect(GuiConstants.HOVER_LEFT_EDGE, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y + ROW_HEIGHT - 1, GuiConstants.COLOR_ROW_HOVER); - } - - // Draw separator line above header (except for first) - if (lineIndex > 0) { - Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y, GuiConstants.COLOR_SEPARATOR); - } - - // Draw tree line down to connection rows if this subnet has connections - boolean hasConnectionRowsBelow = hasConnectionRowBelow(lines, lineIndex); - if (hasConnectionRowsBelow) { - Gui.drawRect(TREE_LINE_X, y + ROW_HEIGHT - 1, TREE_LINE_X + 1, y + ROW_HEIGHT, COLOR_TREE_LINE); - } - - drawSubnetHeader(subnet, y, relMouseX, relMouseY, isHovered, guiLeft, guiTop); - - } else if (line instanceof SubnetConnectionRow) { - SubnetConnectionRow row = (SubnetConnectionRow) line; - - if (isHovered) { - this.hoveredLineIndex = lineIndex; - this.hoveredConnectionRow = row; - this.hoveredSubnet = row.getSubnet(); - // Check if hovering a specific filter slot - int slot = getHoveredFilterSlot(relMouseX, relMouseY, y); - if (slot >= 0 && slot < row.getFilterCountInRow()) { - this.hoveredZone = HoverZone.FILTER_SLOT; - this.hoveredFilterSlot = slot; - this.hoveredFilterStack = row.getFilterAt(slot); - } else { - this.hoveredZone = HoverZone.ENTRY; - } - Gui.drawRect(GuiConstants.HOVER_LEFT_EDGE, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y + ROW_HEIGHT - 1, GuiConstants.COLOR_ROW_HOVER); - } - - // Build line context for tree drawing - LineContext ctx = buildLineContext(lines, lineIndex, i, rowsVisible, totalLines); - drawConnectionRow(row, y, relMouseX, relMouseY, isHovered, ctx, visibleTop, visibleBottom, guiLeft, guiTop); - } - - y += ROW_HEIGHT; - } - - // Draw "no subnets" message if empty - if (totalLines == 0) { - String noSubnets = I18n.format("cellterminal.subnet.none"); - int textWidth = fontRenderer.getStringWidth(noSubnets); - fontRenderer.drawString(noSubnets, (GuiConstants.CONTENT_RIGHT_EDGE - GuiConstants.HOVER_LEFT_EDGE - textWidth) / 2 + GuiConstants.HOVER_LEFT_EDGE, GuiConstants.CONTENT_START_Y + 20, 0xFF606060); - } - - return hoveredLineIndex; - } - - /** - * Trim text to fit within a maximum width, adding ellipsis if truncated. - */ - private String trimTextToWidth(String text, int maxWidth) { - if (fontRenderer.getStringWidth(text) <= maxWidth) return text; - - while (fontRenderer.getStringWidth(text + "...") > maxWidth && text.length() > 1) { - text = text.substring(0, text.length() - 1); - } - - return text + "..."; - } - - // ======================================== - // HEADER RENDERING (matches StorageBusRenderer.drawStorageBusPartitionHeader style) - // ======================================== - - private void drawSubnetHeader(SubnetInfo subnet, int y, int relMouseX, int relMouseY, - boolean isHovered, int guiLeft, int guiTop) { - boolean isMainNetwork = subnet.isMainNetwork(); - boolean canLoad = subnet.isAccessible() && subnet.hasPower(); - - // Calculate button position from right edge - String loadText = I18n.format("cellterminal.subnet.load"); - int loadTextWidth = fontRenderer.getStringWidth(loadText); - int loadButtonX = GuiConstants.CONTENT_RIGHT_EDGE - LOAD_BUTTON_MARGIN - loadTextWidth; - int nameMaxWidth = loadButtonX - NAME_X - 4; - - // Draw favorite star on left sidebar (same position as upgrade icons) - boolean starHovered = isHovered && hoveredZone == HoverZone.STAR; - int starColor = subnet.isFavorite() ? COLOR_FAVORITE_ON : COLOR_FAVORITE_OFF; - if (starHovered) starColor = subnet.isFavorite() ? 0xFFDDB000 : 0xFF707070; - GlStateManager.pushMatrix(); - GlStateManager.scale(2.0F, 2.0F, 1.0F); // Scale up for better visibility - fontRenderer.drawString("★", STAR_X / 2, y / 2, starColor); - GlStateManager.popMatrix(); - - // Draw subnet icon (connection icon for subnets, house for main network) - if (isMainNetwork) { - GlStateManager.pushMatrix(); - GlStateManager.scale(2.0F, 2.0F, 1.0F); - fontRenderer.drawString("⌂", (ICON_X + 5) / 2, (y - 1) / 2, COLOR_MAIN_NETWORK); - GlStateManager.popMatrix(); - } else { - ItemStack icon = subnet.getConnections().isEmpty() ? ItemStack.EMPTY - : subnet.getConnections().get(0).getLocalIcon(); - if (!icon.isEmpty()) renderItemStack(icon, ICON_X, y); - } - - // Draw name (first line) - int nameColor = COLOR_NAME_NORMAL; - if (isMainNetwork) { - nameColor = COLOR_MAIN_NETWORK; - } else if (!canLoad) { - nameColor = COLOR_NAME_INACCESSIBLE; - } else if (subnet.hasCustomName()) { - nameColor = COLOR_NAME_CUSTOM; - } - - String name = subnet.getDisplayName(); - if (fontRenderer.getStringWidth(name) > nameMaxWidth) { - while (fontRenderer.getStringWidth(name + "...") > nameMaxWidth && name.length() > 3) { - name = name.substring(0, name.length() - 1); - } - name = name + "..."; - } - - // Main network has no location line, so center the name vertically - // Row height is 18, font height is ~9, so centered position is y + (18-9)/2 = y + 4.5 ~ y + 5 - int nameY = isMainNetwork ? (y + 5) : (y + 1); - fontRenderer.drawString(name, NAME_X, nameY, nameColor); - - // Draw location (second line) - skip for main network - // Position text can extend to the full content width since it's on a separate line from the Load button - if (!isMainNetwork) { - int locationMaxWidth = GuiConstants.CONTENT_RIGHT_EDGE - NAME_X - 4; - String location = I18n.format("cellterminal.subnet.pos", - subnet.getPrimaryPos().getX(), - subnet.getPrimaryPos().getY(), - subnet.getPrimaryPos().getZ()); - if (fontRenderer.getStringWidth(location) > locationMaxWidth) { - location = trimTextToWidth(location, locationMaxWidth); - } - fontRenderer.drawString(location, NAME_X, y + 9, GuiConstants.COLOR_TEXT_SECONDARY); - } - - // Draw Load button - boolean loadButtonHovered = isHovered && hoveredZone == HoverZone.LOAD_BUTTON; - drawLoadButton(loadButtonX, y + LOAD_BUTTON_Y_OFFSET, loadButtonHovered, canLoad); - } - - /** - * Draw a small "Load" button styled like AE2 buttons. - */ - private void drawLoadButton(int x, int y, boolean isHovered, boolean isEnabled) { - int buttonHeight = 10; - - String text = I18n.format("cellterminal.subnet.load"); - int textWidth = fontRenderer.getStringWidth(text); - int buttonWidth = textWidth + LOAD_BUTTON_MARGIN; - - // Background - int bgColor; - if (!isEnabled) { - bgColor = 0xFF808080; // Gray for disabled - } else if (isHovered) { - bgColor = 0xFF4A90D9; // Light blue hover - } else { - bgColor = 0xFF3B7DC9; // Blue - } - Gui.drawRect(x, y, x + buttonWidth, y + buttonHeight, bgColor); - - // Border (3D effect) - int highlightColor = isEnabled ? 0xFF6BA5E7 : 0xFFA0A0A0; - int shadowColor = isEnabled ? 0xFF2A5B8A : 0xFF606060; - Gui.drawRect(x, y, x + buttonWidth, y + 1, highlightColor); // top - Gui.drawRect(x, y, x + 1, y + buttonHeight, highlightColor); // left - Gui.drawRect(x, y + buttonHeight - 1, x + buttonWidth, y + buttonHeight, shadowColor); // bottom - Gui.drawRect(x + buttonWidth - 1, y, x + buttonWidth, y + buttonHeight, shadowColor); // right - - // Text - int textX = x + LOAD_BUTTON_MARGIN / 2; - int textY = y + 1; - int textColor = isEnabled ? 0xFFFFFFFF : 0xFFC0C0C0; - fontRenderer.drawString(text, textX, textY, textColor); - } - - // ======================================== - // CONNECTION ROW RENDERING - // ======================================== - - private void drawConnectionRow(SubnetConnectionRow row, int y, int relMouseX, int relMouseY, - boolean isHovered, LineContext ctx, int visibleTop, int visibleBottom, - int guiLeft, int guiTop) { - SubnetInfo.ConnectionPoint conn = row.getConnection(); - - // Draw tree lines - drawTreeLines(y, ctx, visibleTop, visibleBottom); - - // Draw connection direction indicator on first row - if (row.isFirstRowForConnection()) drawConnectionIndicator(conn, y); - - // Draw filter slots - drawFilterSlots(row, y, relMouseX, relMouseY, isHovered, guiLeft, guiTop); - } - - private void drawTreeLines(int y, LineContext ctx, int visibleTop, int visibleBottom) { - int lineX = TREE_LINE_X; - - // Vertical line - extends based on position in group - int lineTop = ctx.isFirstVisibleRow && ctx.hasContentAbove ? visibleTop : y - 4; - int lineBottom = ctx.isLastInGroup ? y + 9 : (ctx.isLastVisibleRow && ctx.hasContentBelow ? visibleBottom : y + ROW_HEIGHT); - - if (lineTop < visibleTop) lineTop = visibleTop; - - // Vertical line - Gui.drawRect(lineX, lineTop, lineX + 1, lineBottom, COLOR_TREE_LINE); - - // Horizontal branch - Gui.drawRect(lineX, y + 8, lineX + 12, y + 9, COLOR_TREE_LINE); - } - - private void drawConnectionIndicator(SubnetInfo.ConnectionPoint conn, int y) { - // Draw direction arrow only - int arrowX = TREE_LINE_X + 2; - String arrow = conn.isOutbound() ? "→" : "←"; - int arrowColor = conn.isOutbound() ? COLOR_OUTBOUND : COLOR_INBOUND; - fontRenderer.drawString(arrow, arrowX, y + 4, arrowColor); - } - - private void drawFilterSlots(SubnetConnectionRow row, int y, int relMouseX, int relMouseY, - boolean isHovered, int guiLeft, int guiTop) { - int filterCount = row.getFilterCountInRow(); - - // If no filters, show "No filter" text with background to avoid tree line overlap - if (row.getTotalFilterCount() == 0) { - String noFilterText = I18n.format("cellterminal.subnet.no_filter"); - int textWidth = fontRenderer.getStringWidth(noFilterText); - // Draw background to separate from tree line - Gui.drawRect(FILTER_SLOTS_X - 2, y + 2, FILTER_SLOTS_X + textWidth + 2, y + 14, GuiConstants.COLOR_SLOT_BACKGROUND); - fontRenderer.drawString(noFilterText, FILTER_SLOTS_X, y + 4, 0xFFE0E0E0); - return; - } - - // Draw filter item slots (same style as storage bus partition slots) - for (int i = 0; i < FILTER_SLOTS_PER_ROW; i++) { - int slotX = FILTER_SLOTS_X + (i * MINI_SLOT_SIZE); - - // Only draw slots for available filter items - if (i >= filterCount) break; - - // Draw slot background - drawSlotBackground(slotX, y); - - ItemStack filter = row.getFilterAt(i); - if (!filter.isEmpty()) { - GlStateManager.pushMatrix(); - GlStateManager.enableDepth(); - renderItemStack(filter, slotX, y); - GlStateManager.popMatrix(); - } - - // Hover highlight - if (isHovered && hoveredZone == HoverZone.FILTER_SLOT && hoveredFilterSlot == i) { - drawSlotHoverHighlight(slotX, y); - } - } - } - - private void drawSlotBackground(int x, int y) { - int size = MINI_SLOT_SIZE; - Gui.drawRect(x, y, x + size, y + size, GuiConstants.COLOR_SLOT_BACKGROUND); - Gui.drawRect(x, y, x + size - 1, y + 1, GuiConstants.COLOR_SLOT_BORDER_DARK); - Gui.drawRect(x, y, x + 1, y + size - 1, GuiConstants.COLOR_SLOT_BORDER_DARK); - Gui.drawRect(x + 1, y + size - 1, x + size, y + size, GuiConstants.COLOR_SLOT_BORDER_LIGHT); - Gui.drawRect(x + size - 1, y + 1, x + size, y + size - 1, GuiConstants.COLOR_SLOT_BORDER_LIGHT); - } - - private void drawSlotHoverHighlight(int x, int y) { - int inner = MINI_SLOT_SIZE - 1; - Gui.drawRect(x + 1, y + 1, x + inner, y + inner, GuiConstants.COLOR_HOVER_HIGHLIGHT); - } - - private void renderItemStack(ItemStack stack, int x, int y) { - if (stack.isEmpty()) return; - - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - RenderHelper.enableGUIStandardItemLighting(); - itemRender.renderItemIntoGUI(stack, x, y); - RenderHelper.disableStandardItemLighting(); - GlStateManager.disableLighting(); - GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); - GlStateManager.enableBlend(); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - } - - // ======================================== - // HOVER ZONE DETECTION - // ======================================== - - private HoverZone determineHeaderHoverZone(int relMouseX, SubnetInfo subnet) { - int loadButtonX = GuiConstants.CONTENT_RIGHT_EDGE - LOAD_BUTTON_MARGIN - LOAD_BUTTON_WIDTH; - - if (relMouseX >= loadButtonX && relMouseX < loadButtonX + LOAD_BUTTON_WIDTH) return HoverZone.LOAD_BUTTON; - if (relMouseX >= STAR_X && relMouseX < STAR_X + STAR_WIDTH) return HoverZone.STAR; - if (relMouseX >= NAME_X && relMouseX < loadButtonX) return HoverZone.NAME; - - return HoverZone.ENTRY; - } - - /** - * Get the currently hovered entry index. - * @return Index of hovered subnet, or -1 if none - */ - private int getHoveredFilterSlot(int relMouseX, int relMouseY, int rowY) { - if (relMouseX < FILTER_SLOTS_X) return -1; - - int slotIndex = (relMouseX - FILTER_SLOTS_X) / MINI_SLOT_SIZE; - if (slotIndex < 0 || slotIndex >= FILTER_SLOTS_PER_ROW) return -1; - - // Check Y is within slot bounds - if (relMouseY < rowY || relMouseY >= rowY + MINI_SLOT_SIZE) return -1; - - return slotIndex; - } - - // ======================================== - // LINE CONTEXT HELPERS - // ======================================== - - private LineContext buildLineContext(List lines, int lineIndex, int visibleIndex, - int rowsVisible, int totalLines) { - LineContext ctx = new LineContext(); - ctx.isFirstInGroup = isFirstConnectionRowOfSubnet(lines, lineIndex); - ctx.isLastInGroup = isLastConnectionRowOfSubnet(lines, lineIndex); - ctx.isFirstVisibleRow = (visibleIndex == 0); - ctx.isLastVisibleRow = (visibleIndex == rowsVisible - 1) || (lineIndex == totalLines - 1); - ctx.hasContentAbove = hasConnectionRowAbove(lines, lineIndex); - ctx.hasContentBelow = hasConnectionRowBelow(lines, lineIndex); - - return ctx; - } - - private boolean isFirstConnectionRowOfSubnet(List lines, int lineIndex) { - if (!(lines.get(lineIndex) instanceof SubnetConnectionRow)) return false; - if (lineIndex == 0) return true; - - return lines.get(lineIndex - 1) instanceof SubnetInfo; - } - - private boolean isLastConnectionRowOfSubnet(List lines, int lineIndex) { - if (!(lines.get(lineIndex) instanceof SubnetConnectionRow)) return false; - if (lineIndex >= lines.size() - 1) return true; - - return lines.get(lineIndex + 1) instanceof SubnetInfo; - } - - private boolean hasConnectionRowAbove(List lines, int lineIndex) { - if (lineIndex == 0) return false; - - return lines.get(lineIndex - 1) instanceof SubnetConnectionRow; - } - - private boolean hasConnectionRowBelow(List lines, int lineIndex) { - if (lineIndex >= lines.size() - 1) return false; - - return lines.get(lineIndex + 1) instanceof SubnetConnectionRow; - } - - private static class LineContext { - boolean isFirstInGroup; - boolean isLastInGroup; - boolean isFirstVisibleRow; - boolean isLastVisibleRow; - boolean hasContentAbove; - boolean hasContentBelow; - } - - // ======================================== - // PUBLIC ACCESSORS - // ======================================== - - /** - * Get the currently hovered line index. - */ - public int getHoveredLineIndex() { - return hoveredLineIndex; - } - - /** - * Get the currently hovered entry index (for backward compatibility). - * @deprecated Use getHoveredLineIndex() instead - */ - @Deprecated - public int getHoveredEntryIndex() { - return hoveredLineIndex; - } - - /** - * Get the currently hovered zone. - */ - public HoverZone getHoveredZone() { - return hoveredZone; - } - - /** - * Get the currently hovered subnet (from header or connection row). - */ - public SubnetInfo getHoveredSubnet() { - return hoveredSubnet; - } - - /** - * Get the currently hovered connection row (if hovering a filter row). - */ - public SubnetConnectionRow getHoveredConnectionRow() { - return hoveredConnectionRow; - } - - /** - * Get the hovered filter slot index (-1 if not hovering a slot). - */ - public int getHoveredFilterSlot() { - return hoveredFilterSlot; - } - - /** - * Get the hovered filter item stack. - */ - public ItemStack getHoveredFilterStack() { - return hoveredFilterStack; - } - - /** - * Get the Y position for a subnet row. - * @param subnet The subnet to find - * @param lines The list of subnet lines - * @param currentScroll The current scroll position - * @return The Y position, or -1 if not visible - */ - public int getRowYForSubnet(SubnetInfo subnet, List lines, int currentScroll) { - if (subnet == null) return -1; - - for (int i = 0; i < lines.size(); i++) { - Object line = lines.get(i); - if (line instanceof SubnetInfo && ((SubnetInfo) line).getId() == subnet.getId()) { - int visualIndex = i - currentScroll; - if (visualIndex < 0) return -1; // Not visible (above scroll) - - return GuiConstants.CONTENT_START_Y + visualIndex * ROW_HEIGHT; - } - } - - return -1; - } - - // ======================================== - // RENAME EDITING - // ======================================== - - /** - * Check if currently editing a subnet name. - */ - public boolean isEditing() { - return editingSubnet != null; - } - - /** - * Get the subnet being edited. - */ - public SubnetInfo getEditingSubnet() { - return editingSubnet; - } - - /** - * Start editing a subnet's name. - * @param subnet The subnet to rename - * @param y The Y position where the edit field should appear - */ - public void startEditing(SubnetInfo subnet, int y) { - if (subnet == null || subnet.isMainNetwork()) return; - - this.editingSubnet = subnet; - this.editingText = subnet.hasCustomName() ? subnet.getCustomName() : ""; - this.editingCursorPos = editingText.length(); - this.editingY = y; - } - - /** - * Stop editing and return the edited text. - * @return The edited text, or null if not editing - */ - public String stopEditing() { - if (editingSubnet == null) return null; - - String result = editingText.trim(); - editingSubnet = null; - editingText = ""; - editingCursorPos = 0; - - return result; - } - - /** - * Cancel editing without saving. - */ - public void cancelEditing() { - editingSubnet = null; - editingText = ""; - editingCursorPos = 0; - } - - /** - * Get the current editing text. - */ - public String getEditingText() { - return editingText; - } - - /** - * Handle keyboard input for rename editing. - * @param typedChar The character typed - * @param keyCode The key code - * @return true if the input was handled - */ - public boolean handleKeyTyped(char typedChar, int keyCode) { - if (editingSubnet == null) return false; - - // Enter - confirm - if (keyCode == Keyboard.KEY_RETURN) return false; // Return false to let GUI handle confirmation - - if (keyCode == Keyboard.KEY_ESCAPE) { - cancelEditing(); - return true; - } - - if (keyCode == Keyboard.KEY_BACK && editingCursorPos > 0) { - editingText = editingText.substring(0, editingCursorPos - 1) - + editingText.substring(editingCursorPos); - editingCursorPos--; - return true; - } - - if (keyCode == Keyboard.KEY_DELETE && editingCursorPos < editingText.length()) { - editingText = editingText.substring(0, editingCursorPos) - + editingText.substring(editingCursorPos + 1); - return true; - } - - if (keyCode == Keyboard.KEY_LEFT && editingCursorPos > 0) { - editingCursorPos--; - return true; - } - - if (keyCode == Keyboard.KEY_RIGHT && editingCursorPos < editingText.length()) { - editingCursorPos++; - return true; - } - - if (keyCode == Keyboard.KEY_HOME) { - editingCursorPos = 0; - return true; - } - - if (keyCode == Keyboard.KEY_END) { - editingCursorPos = editingText.length(); - return true; - } - - // Printable characters - if (typedChar >= 32 && typedChar < 127 && editingText.length() < 50) { - editingText = editingText.substring(0, editingCursorPos) - + typedChar - + editingText.substring(editingCursorPos); - editingCursorPos++; - return true; - } - - return false; - } - - /** - * Draw the rename text field if editing. - * Call this after drawing the subnet list. - */ - public void drawRenameField() { - if (editingSubnet == null) return; - - int loadButtonX = GuiConstants.CONTENT_RIGHT_EDGE - LOAD_BUTTON_MARGIN - LOAD_BUTTON_WIDTH; - // Offset by -2 so the text inside the field (at x + 2) aligns with the original name position - int x = NAME_X - 2; - int y = editingY + 1; // Vertically aligned with text (y + 1 is where text draws) - int width = loadButtonX - x - 4; - int height = 9; // Reduced height to align with text - - // Draw background (E0E0E0 to match terminal background, with dark border) - Gui.drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xFF373737); - Gui.drawRect(x, y, x + width, y + height, 0xFFE0E0E0); - - // Draw text - String displayText = editingText; - int textWidth = fontRenderer.getStringWidth(displayText); - - // Scroll text if too long - int visibleWidth = width - 4; - int textX = x + 2; - if (textWidth > visibleWidth) { - int cursorX = fontRenderer.getStringWidth(displayText.substring(0, editingCursorPos)); - int scrollOffset = Math.max(0, cursorX - visibleWidth + 10); - textX -= scrollOffset; - } - - fontRenderer.drawString(displayText, textX, y, 0xFF000000); - - // Draw cursor (blinking) - long time = System.currentTimeMillis(); - if ((time / 500) % 2 == 0) { - int cursorX = x + 2 + fontRenderer.getStringWidth(displayText.substring(0, editingCursorPos)); - Gui.drawRect(cursorX, y, cursorX + 1, y + height - 1, 0xFF000000); - } - } - - /** - * Get tooltip lines for the currently hovered element. - * @param line The line object being hovered (SubnetInfo or SubnetConnectionRow) - * @return List of tooltip lines - */ - public List getTooltip(Object line) { - List tooltipLines = new ArrayList<>(); - - if (line == null) return tooltipLines; - - // Delegate based on line type - if (line instanceof SubnetInfo) { - return getSubnetTooltip((SubnetInfo) line); - } else if (line instanceof SubnetConnectionRow) { - return getConnectionRowTooltip((SubnetConnectionRow) line); - } - - return tooltipLines; - } - - private List getSubnetTooltip(SubnetInfo subnet) { - List lines = new ArrayList<>(); - - // Main network tooltip - if (subnet.isMainNetwork()) { - lines.add(I18n.format("cellterminal.subnet.main_network")); - lines.add("§e" + I18n.format("cellterminal.subnet.click_load_main")); - - return lines; - } - - switch (hoveredZone) { - case STAR: - lines.add(I18n.format("cellterminal.subnet.controls.star")); - break; - - case NAME: - // Show general entry info on name (more useful than just rename hint) - lines.add(subnet.getDisplayName()); - lines.add("§7" + I18n.format("cellterminal.subnet.pos", - subnet.getPrimaryPos().getX(), - subnet.getPrimaryPos().getY(), - subnet.getPrimaryPos().getZ())); - lines.add("§7" + I18n.format("cellterminal.subnet.dim", subnet.getDimension())); - if (subnet.getOutboundCount() > 0) { - lines.add("§a" + I18n.format("cellterminal.subnet.outbound", subnet.getOutboundCount())); - } - if (subnet.getInboundCount() > 0) { - lines.add("§9" + I18n.format("cellterminal.subnet.inbound", subnet.getInboundCount())); - } - lines.add(""); - lines.add("§e" + I18n.format("cellterminal.subnet.right_click_rename")); - break; - - case LOAD_BUTTON: - if (!subnet.hasPower()) { - lines.add("§c" + I18n.format("cellterminal.subnet.no_power")); - } else if (!subnet.isAccessible()) { - lines.add("§6" + I18n.format("cellterminal.subnet.no_access")); - } else { - lines.add(I18n.format("cellterminal.subnet.load.tooltip")); - } - break; - - case ENTRY: - lines.add(subnet.getDisplayName()); - lines.add("§7" + I18n.format("cellterminal.subnet.pos", - subnet.getPrimaryPos().getX(), - subnet.getPrimaryPos().getY(), - subnet.getPrimaryPos().getZ())); - lines.add("§7" + I18n.format("cellterminal.subnet.dim", subnet.getDimension())); - - // Show connection counts - if (subnet.getOutboundCount() > 0) { - lines.add("§a" + I18n.format("cellterminal.subnet.outbound", subnet.getOutboundCount())); - } - if (subnet.getInboundCount() > 0) { - lines.add("§9" + I18n.format("cellterminal.subnet.inbound", subnet.getInboundCount())); - } - - lines.add(""); - lines.add("§e" + I18n.format("cellterminal.subnet.double_click_highlight")); - if (!subnet.hasPower()) { - lines.add("§c" + I18n.format("cellterminal.subnet.no_power")); - } - if (subnet.hasSecurity() && !subnet.isAccessible()) { - lines.add("§6" + I18n.format("cellterminal.subnet.no_access")); - } - break; - - default: - break; - } - - return lines; - } - - private List getConnectionRowTooltip(SubnetConnectionRow row) { - List lines = new ArrayList<>(); - - if (hoveredZone == HoverZone.FILTER_SLOT && !hoveredFilterStack.isEmpty()) return lines; - - // General connection row tooltip - SubnetInfo.ConnectionPoint conn = row.getConnection(); - String direction = conn.isOutbound() - ? I18n.format("cellterminal.subnet.direction.outbound") - : I18n.format("cellterminal.subnet.direction.inbound"); - lines.add(direction); - - // Show connection position - lines.add("§7" + I18n.format("cellterminal.subnet.connection.pos", - conn.getPos().getX(), conn.getPos().getY(), conn.getPos().getZ())); - - // Show filter count - int nonEmptyCount = 0; - for (ItemStack stack : conn.getFilter()) { - if (!stack.isEmpty()) nonEmptyCount++; - } - - if (nonEmptyCount > 0) { - lines.add("§7" + I18n.format("cellterminal.subnet.filter_count", nonEmptyCount)); - } else { - lines.add("§7" + I18n.format("cellterminal.subnet.no_filter")); - } - - return lines; - } -} diff --git a/src/main/java/com/cellterminal/gui/tab/ITabController.java b/src/main/java/com/cellterminal/gui/tab/ITabController.java deleted file mode 100644 index 9e95cd3..0000000 --- a/src/main/java/com/cellterminal/gui/tab/ITabController.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.cellterminal.gui.tab; - -import java.util.List; - -import com.cellterminal.client.SearchFilterMode; - - -/** - * Interface for tab controllers that manage individual tabs in the Cell Terminal GUI. - * Each tab can define its own behavior for: - * - Help text content - * - Search filter mode - * - Tooltip handling - * - Click handling - * - Keyboard handling - */ -public interface ITabController { - - /** - * Get the tab index this controller manages. - * @return The tab index (0-4) - */ - int getTabIndex(); - - /** - * Get the search filter mode that should be used when this tab is active. - * @param userSelectedMode The mode the user has selected (for tabs that respect user choice) - * @return The effective search filter mode for this tab - */ - SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode); - - /** - * Check if the search mode button should be visible when this tab is active. - * @return true if the search mode button should be shown - */ - boolean showSearchModeButton(); - - /** - * Get the help lines to display for this tab. - * These are displayed in the controls help widget. - * @return List of localized help text lines (can include empty strings for spacing) - */ - List getHelpLines(); - - /** - * Handle a click on this tab's content area. - * @param context The click context containing all relevant state - * @return true if the click was handled and should not propagate - */ - boolean handleClick(TabClickContext context); - - /** - * Handle a key press when this tab is active. - * @param keyCode The key code that was pressed - * @param context The tab context containing relevant state - * @return true if the key was handled and should not propagate - */ - boolean handleKeyTyped(int keyCode, TabContext context); - - /** - * Called when this tab becomes active. - * @param context The tab context - */ - default void onTabActivated(TabContext context) {} - - /** - * Called when this tab becomes inactive (another tab is selected). - * @param context The tab context - */ - default void onTabDeactivated(TabContext context) {} - - /** - * Get whether this tab requires server-side polling for data updates. - * @return true if the server should poll for data while this tab is active - */ - default boolean requiresServerPolling() { - return false; - } - - /** - * Get the lines to display for this tab from the data manager. - * @param context The tab context - * @return The list of line objects for rendering - */ - List getLines(TabContext context); - - /** - * Get the total line count for scrollbar calculation. - * @param context The tab context - * @return The number of lines - */ - int getLineCount(TabContext context); -} diff --git a/src/main/java/com/cellterminal/gui/tab/InventoryTabController.java b/src/main/java/com/cellterminal/gui/tab/InventoryTabController.java deleted file mode 100644 index 978c8b5..0000000 --- a/src/main/java/com/cellterminal/gui/tab/InventoryTabController.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.cellterminal.gui.tab; - -import java.util.ArrayList; -import java.util.List; - -import net.minecraft.client.resources.I18n; - -import com.cellterminal.client.SearchFilterMode; -import com.cellterminal.gui.GuiConstants; - - -/** - * Tab controller for the Inventory tab (Tab 1). - * This tab displays cells with their contents in a grid format. - * Content items show "P" indicator if they're in the cell's partition. - */ -public class InventoryTabController implements ITabController { - - @Override - public int getTabIndex() { - return GuiConstants.TAB_INVENTORY; - } - - @Override - public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { - // Inventory tab forces INVENTORY mode for searching by contents - return SearchFilterMode.INVENTORY; - } - - @Override - public boolean showSearchModeButton() { - // Inventory tab hides the search mode button (mode is forced) - return false; - } - - @Override - public List getHelpLines() { - List lines = new ArrayList<>(); - - lines.add(I18n.format("gui.cellterminal.controls.partition_indicator")); - lines.add(I18n.format("gui.cellterminal.controls.click_partition_toggle")); - lines.add(I18n.format("gui.cellterminal.controls.double_click_storage")); - lines.add(I18n.format("gui.cellterminal.right_click_rename")); - - return lines; - } - - @Override - public boolean handleClick(TabClickContext context) { - // Handle partition-all button click - if (context.hoveredPartitionAllButtonCell != null && context.isLeftClick()) { - // Partition all action is handled in GuiCellTerminalBase for now - // as it requires sending a network packet - return false; // Let the GUI handle this - } - - // Cell tab click handling is done by TerminalClickHandler - return false; - } - - @Override - public boolean handleKeyTyped(int keyCode, TabContext context) { - // Inventory tab has no special keybinds - return false; - } - - @Override - public List getLines(TabContext context) { - return context.getInventoryLines(); - } - - @Override - public int getLineCount(TabContext context) { - return context.getInventoryLines().size(); - } -} diff --git a/src/main/java/com/cellterminal/gui/tab/NetworkToolsTabController.java b/src/main/java/com/cellterminal/gui/tab/NetworkToolsTabController.java deleted file mode 100644 index 16126fd..0000000 --- a/src/main/java/com/cellterminal/gui/tab/NetworkToolsTabController.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.cellterminal.gui.tab; - -import java.util.ArrayList; -import java.util.List; - -import net.minecraft.client.resources.I18n; - -import com.cellterminal.client.SearchFilterMode; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.networktools.INetworkTool; -import com.cellterminal.gui.networktools.NetworkToolRegistry; - - -/** - * Tab controller for the Network Tools tab (Tab 6). - * This tab displays a list of batch operation tools that can affect the entire network. - */ -public class NetworkToolsTabController implements ITabController { - - @Override - public int getTabIndex() { - return GuiConstants.TAB_NETWORK_TOOLS; - } - - @Override - public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { - // Network tools tab respects the user's selected search mode - return userSelectedMode; - } - - @Override - public boolean showSearchModeButton() { - return true; - } - - @Override - public List getHelpLines() { - List lines = new ArrayList<>(); - lines.add("§c" + I18n.format("gui.cellterminal.networktools.warning.caution")); - lines.add(I18n.format("gui.cellterminal.networktools.warning.irreversible")); - lines.add(""); - lines.add(I18n.format("gui.cellterminal.networktools.help.read_tooltip")); - - return lines; - } - - @Override - public boolean handleClick(TabClickContext context) { - // Click handling is done by the GUI/renderer for this tab - return false; - } - - @Override - public boolean handleKeyTyped(int keyCode, TabContext context) { - // No special keybinds for Network Tools tab - return false; - } - - @Override - public boolean requiresServerPolling() { - // Need server data for accurate tool previews - return false; - } - - @Override - public List getLines(TabContext context) { - // Return tools as lines for rendering - List lines = new ArrayList<>(); - lines.addAll(NetworkToolRegistry.getAllTools()); - - return lines; - } - - @Override - public int getLineCount(TabContext context) { - return NetworkToolRegistry.getToolCount(); - } -} diff --git a/src/main/java/com/cellterminal/gui/tab/PartitionTabController.java b/src/main/java/com/cellterminal/gui/tab/PartitionTabController.java deleted file mode 100644 index 1673c29..0000000 --- a/src/main/java/com/cellterminal/gui/tab/PartitionTabController.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.cellterminal.gui.tab; - -import java.util.ArrayList; -import java.util.List; - -import net.minecraft.client.resources.I18n; - -import com.cellterminal.client.KeyBindings; -import com.cellterminal.client.SearchFilterMode; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.handler.QuickPartitionHandler; -import com.cellterminal.gui.overlay.MessageHelper; -import com.cellterminal.integration.ThaumicEnergisticsIntegration; - - -/** - * Tab controller for the Partition tab (Tab 2). - * This tab displays cells with their partition configuration in a grid. - * Supports JEI ghost ingredient drag-and-drop and quick partition keybinds. - */ -public class PartitionTabController implements ITabController { - - @Override - public int getTabIndex() { - return GuiConstants.TAB_PARTITION; - } - - @Override - public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { - // Partition tab forces PARTITION mode for searching by partition contents - return SearchFilterMode.PARTITION; - } - - @Override - public boolean showSearchModeButton() { - // Partition tab hides the search mode button (mode is forced) - return false; - } - - @Override - public List getHelpLines() { - List lines = new ArrayList<>(); - - // Note about what keybinds target - lines.add(I18n.format("gui.cellterminal.controls.keybind_targets")); - - lines.add(""); // spacing line - - // Keybind instructions - String notSet = I18n.format("gui.cellterminal.controls.key_not_set"); - - String autoKey = KeyBindings.QUICK_PARTITION_AUTO.isBound() - ? KeyBindings.QUICK_PARTITION_AUTO.getDisplayName() : notSet; - lines.add(I18n.format("gui.cellterminal.controls.key_auto", autoKey)); - - // Auto type warning if set - if (!autoKey.equals(notSet)) { - lines.add(I18n.format("gui.cellterminal.controls.auto_warning")); - } - - lines.add(""); - - String itemKey = KeyBindings.QUICK_PARTITION_ITEM.isBound() - ? KeyBindings.QUICK_PARTITION_ITEM.getDisplayName() : notSet; - lines.add(I18n.format("gui.cellterminal.controls.key_item", itemKey)); - - String fluidKey = KeyBindings.QUICK_PARTITION_FLUID.isBound() - ? KeyBindings.QUICK_PARTITION_FLUID.getDisplayName() : notSet; - lines.add(I18n.format("gui.cellterminal.controls.key_fluid", fluidKey)); - - String essentiaKey = KeyBindings.QUICK_PARTITION_ESSENTIA.isBound() - ? KeyBindings.QUICK_PARTITION_ESSENTIA.getDisplayName() : notSet; - lines.add(I18n.format("gui.cellterminal.controls.key_essentia", essentiaKey)); - - // Essentia cells warning if set and Thaumic Energistics not loaded - if (!essentiaKey.equals(notSet) && !ThaumicEnergisticsIntegration.isModLoaded()) { - lines.add(I18n.format("gui.cellterminal.controls.essentia_warning")); - } - - lines.add(""); - - lines.add(I18n.format("gui.cellterminal.controls.jei_drag")); - lines.add(I18n.format("gui.cellterminal.controls.click_to_remove")); - lines.add(I18n.format("gui.cellterminal.controls.double_click_storage")); - lines.add(I18n.format("gui.cellterminal.right_click_rename")); - - return lines; - } - - @Override - public boolean handleClick(TabClickContext context) { - // Handle clear-partition button click - if (context.hoveredClearPartitionButtonCell != null && context.isLeftClick()) { - // Clear all action is handled in GuiCellTerminalBase for now - // as it requires sending a network packet - return false; // Let the GUI handle this - } - - return false; - } - - @Override - public boolean handleKeyTyped(int keyCode, TabContext context) { - KeyBindings matchedKey = null; - QuickPartitionHandler.PartitionType type = null; - - if (KeyBindings.QUICK_PARTITION_AUTO.isActiveAndMatches(keyCode)) { - matchedKey = KeyBindings.QUICK_PARTITION_AUTO; - type = QuickPartitionHandler.PartitionType.AUTO; - } else if (KeyBindings.QUICK_PARTITION_ITEM.isActiveAndMatches(keyCode)) { - matchedKey = KeyBindings.QUICK_PARTITION_ITEM; - type = QuickPartitionHandler.PartitionType.ITEM; - } else if (KeyBindings.QUICK_PARTITION_FLUID.isActiveAndMatches(keyCode)) { - matchedKey = KeyBindings.QUICK_PARTITION_FLUID; - type = QuickPartitionHandler.PartitionType.FLUID; - } else if (KeyBindings.QUICK_PARTITION_ESSENTIA.isActiveAndMatches(keyCode)) { - matchedKey = KeyBindings.QUICK_PARTITION_ESSENTIA; - type = QuickPartitionHandler.PartitionType.ESSENTIA; - } - - if (matchedKey == null || type == null) return false; - - QuickPartitionHandler.QuickPartitionResult result = QuickPartitionHandler.attemptQuickPartition( - type, context.getPartitionLines(), context.getStorageMap()); - - // Display result message with appropriate color - if (result.success) { - MessageHelper.successRaw(result.message); - } else { - MessageHelper.errorRaw(result.message); - } - - // Scroll to the cell if successful - if (result.success && result.scrollToLine >= 0) context.scrollToLine(result.scrollToLine); - - return true; - } - - @Override - public List getLines(TabContext context) { - return context.getPartitionLines(); - } - - @Override - public int getLineCount(TabContext context) { - return context.getPartitionLines().size(); - } -} diff --git a/src/main/java/com/cellterminal/gui/tab/StorageBusInventoryTabController.java b/src/main/java/com/cellterminal/gui/tab/StorageBusInventoryTabController.java deleted file mode 100644 index ed00fb3..0000000 --- a/src/main/java/com/cellterminal/gui/tab/StorageBusInventoryTabController.java +++ /dev/null @@ -1,74 +0,0 @@ -package com.cellterminal.gui.tab; - -import java.util.ArrayList; -import java.util.List; - -import net.minecraft.client.resources.I18n; - -import com.cellterminal.client.SearchFilterMode; -import com.cellterminal.gui.GuiConstants; - - -/** - * Tab controller for the Storage Bus Inventory tab (Tab 4). - * This tab displays storage buses with their connected inventory contents. - */ -public class StorageBusInventoryTabController implements ITabController { - - @Override - public int getTabIndex() { - return GuiConstants.TAB_STORAGE_BUS_INVENTORY; - } - - @Override - public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { - // Storage Bus Inventory tab forces INVENTORY mode - return SearchFilterMode.INVENTORY; - } - - @Override - public boolean showSearchModeButton() { - // Storage bus tabs hide the search mode button - return false; - } - - @Override - public List getHelpLines() { - List lines = new ArrayList<>(); - - lines.add(I18n.format("gui.cellterminal.controls.filter_indicator")); - lines.add(I18n.format("gui.cellterminal.controls.click_to_remove")); - lines.add(I18n.format("gui.cellterminal.controls.double_click_storage")); - lines.add(I18n.format("gui.cellterminal.right_click_rename")); - - return lines; - } - - @Override - public boolean handleClick(TabClickContext context) { - // Storage bus click handling is done in GuiCellTerminalBase.handleStorageBusTabClick - return false; - } - - @Override - public boolean handleKeyTyped(int keyCode, TabContext context) { - // Storage Bus Inventory tab has no special keybinds - return false; - } - - @Override - public boolean requiresServerPolling() { - // Storage bus tabs require server polling - return true; - } - - @Override - public List getLines(TabContext context) { - return context.getStorageBusInventoryLines(); - } - - @Override - public int getLineCount(TabContext context) { - return context.getStorageBusInventoryLines().size(); - } -} diff --git a/src/main/java/com/cellterminal/gui/tab/StorageBusPartitionTabController.java b/src/main/java/com/cellterminal/gui/tab/StorageBusPartitionTabController.java deleted file mode 100644 index e135242..0000000 --- a/src/main/java/com/cellterminal/gui/tab/StorageBusPartitionTabController.java +++ /dev/null @@ -1,224 +0,0 @@ -package com.cellterminal.gui.tab; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.resources.I18n; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; - -import net.minecraftforge.fluids.FluidStack; - -import appeng.fluids.items.FluidDummyItem; - -import com.cellterminal.client.KeyBindings; -import com.cellterminal.client.SearchFilterMode; -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.handler.QuickPartitionHandler; -import com.cellterminal.gui.overlay.MessageHelper; -import com.cellterminal.integration.ThaumicEnergisticsIntegration; -import com.cellterminal.network.CellTerminalNetwork; -import com.cellterminal.network.PacketStorageBusPartitionAction; - - -/** - * Tab controller for the Storage Bus Partition tab (Tab 5). - * This tab displays storage buses with their partition configuration. - * Supports multi-selection of buses and keybind to add items to selected buses. - */ -public class StorageBusPartitionTabController implements ITabController { - - @Override - public int getTabIndex() { - return GuiConstants.TAB_STORAGE_BUS_PARTITION; - } - - @Override - public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { - // Storage Bus Partition tab forces PARTITION mode - return SearchFilterMode.PARTITION; - } - - @Override - public boolean showSearchModeButton() { - // Storage bus tabs hide the search mode button - return false; - } - - @Override - public List getHelpLines() { - List lines = new ArrayList<>(); - - lines.add(I18n.format("gui.cellterminal.controls.storage_bus_add_key", - KeyBindings.ADD_TO_STORAGE_BUS.getDisplayName())); - - lines.add(I18n.format("gui.cellterminal.controls.storage_bus_capacity")); - - lines.add(""); // spacing line - - lines.add(I18n.format("gui.cellterminal.controls.jei_drag")); - lines.add(I18n.format("gui.cellterminal.controls.click_to_remove")); - lines.add(I18n.format("gui.cellterminal.controls.double_click_storage")); - lines.add(I18n.format("gui.cellterminal.right_click_rename")); - - return lines; - } - - @Override - public boolean handleClick(TabClickContext context) { - // Storage bus click handling is done in GuiCellTerminalBase.handleStorageBusTabClick - return false; - } - - @Override - public boolean handleKeyTyped(int keyCode, TabContext context) { - if (!KeyBindings.ADD_TO_STORAGE_BUS.isActiveAndMatches(keyCode)) return false; - - // This method is called from the GUI which has access to the selected bus IDs - // We need to get them from the context - // For now, this is handled in GuiCellTerminalBase since we need access to - // selectedStorageBusIds which is GUI state - - return false; // Let GUI handle this for now since it needs GUI-specific state - } - - /** - * Handle the add-to-storage-bus keybind. - * Called from GuiCellTerminalBase with the actual selected bus IDs. - * @param selectedBusIds The set of selected storage bus IDs - * @param hoveredSlot The slot the mouse is over (or null) - * @param storageBusMap Map of storage bus IDs to info - * @return true if the keybind was handled - */ - public static boolean handleAddToStorageBusKeybind(Set selectedBusIds, - Slot hoveredSlot, - Map storageBusMap) { - if (selectedBusIds.isEmpty()) { - if (Minecraft.getMinecraft().player != null) { - MessageHelper.warning("cellterminal.storage_bus.no_selection"); - } - - return true; - } - - // Try to get item from inventory slot first - ItemStack stack = ItemStack.EMPTY; - - if (hoveredSlot != null && hoveredSlot.getHasStack()) { - stack = hoveredSlot.getStack(); - } - - // If no inventory item, try JEI/bookmark - if (stack.isEmpty()) { - QuickPartitionHandler.HoveredIngredient jeiItem = QuickPartitionHandler.getHoveredIngredient(); - if (jeiItem != null && !jeiItem.stack.isEmpty()) stack = jeiItem.stack; - } - - if (stack.isEmpty()) { - if (Minecraft.getMinecraft().player != null) { - MessageHelper.warning("cellterminal.storage_bus.no_item"); - } - - return true; - } - - // Add to all selected storage buses - int successCount = 0; - int invalidItemCount = 0; - int noSlotCount = 0; - - for (Long busId : selectedBusIds) { - StorageBusInfo storageBus = storageBusMap.get(busId); - if (storageBus == null) continue; - - // Convert the item for non-item bus types first to check validity - ItemStack stackToSend = stack; - boolean validForBusType = true; - - if (storageBus.isFluid()) { - // For fluid buses, need FluidDummyItem or fluid container - if (!(stack.getItem() instanceof FluidDummyItem)) { - FluidStack fluid = net.minecraftforge.fluids.FluidUtil.getFluidContained(stack); - // Can't use this item on fluid bus - if (fluid == null) { - invalidItemCount++; - validForBusType = false; - } - } - } else if (storageBus.isEssentia()) { - // For essentia buses, need ItemDummyAspect or essentia container - ItemStack essentiaRep = ThaumicEnergisticsIntegration.tryConvertEssentiaContainerToAspect(stack); - // Can't use this item on essentia bus - if (essentiaRep.isEmpty()) { - invalidItemCount++; - validForBusType = false; - } else { - stackToSend = essentiaRep; - } - } - - if (!validForBusType) continue; - - // Find first empty slot in this storage bus - List partition = storageBus.getPartition(); - int availableSlots = storageBus.getAvailableConfigSlots(); - int targetSlot = -1; - - for (int i = 0; i < availableSlots; i++) { - if (i >= partition.size() || partition.get(i).isEmpty()) { - targetSlot = i; - break; - } - } - - if (targetSlot < 0) { - noSlotCount++; - continue; - } - - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketStorageBusPartitionAction( - busId, - PacketStorageBusPartitionAction.Action.ADD_ITEM, - targetSlot, - stackToSend - ) - ); - successCount++; - } - - if (successCount == 0 && Minecraft.getMinecraft().player != null) { - // Show appropriate error message based on what failed - if (invalidItemCount > 0 && noSlotCount == 0) { - MessageHelper.error("cellterminal.storage_bus.invalid_item"); - } else if (noSlotCount > 0 && invalidItemCount == 0) { - MessageHelper.error("cellterminal.storage_bus.partition_full"); - } else { - // Mixed: some were invalid, some were full - MessageHelper.error("cellterminal.storage_bus.partition_full"); - } - } - - return true; - } - - @Override - public boolean requiresServerPolling() { - // Storage bus tabs require server polling - return true; - } - - @Override - public List getLines(TabContext context) { - return context.getStorageBusPartitionLines(); - } - - @Override - public int getLineCount(TabContext context) { - return context.getStorageBusPartitionLines().size(); - } -} diff --git a/src/main/java/com/cellterminal/gui/tab/TabClickContext.java b/src/main/java/com/cellterminal/gui/tab/TabClickContext.java deleted file mode 100644 index eb10dc0..0000000 --- a/src/main/java/com/cellterminal/gui/tab/TabClickContext.java +++ /dev/null @@ -1,137 +0,0 @@ -package com.cellterminal.gui.tab; - -import java.util.Set; - -import org.lwjgl.input.Keyboard; - -import net.minecraft.item.ItemStack; - -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.client.StorageInfo; - - -/** - * Context object containing all information about a click event on a tab. - * This is passed to tab controllers to handle click logic. - */ -public class TabClickContext { - - // Mouse information - public final int mouseX; - public final int mouseY; - public final int mouseButton; - public final int relMouseX; // Relative to GUI - public final int relMouseY; // Relative to GUI - - // GUI positioning - public final int guiLeft; - public final int guiTop; - public final int rowsVisible; - public final int currentScroll; - - // General tab context - public final TabContext tabContext; - - // Hovered elements (set by renderer during draw) - public CellInfo hoveredCell; - public int hoveredCellHoverType; // 0=none, 1=inventory, 2=partition, 3=eject - public StorageInfo hoveredStorageLine; - public int hoveredLineIndex; - public CellInfo hoveredCellCell; - public StorageInfo hoveredCellStorage; - public int hoveredCellSlotIndex; - public ItemStack hoveredContentStack; - public int hoveredContentSlotIndex; - public int hoveredPartitionSlotIndex; - public CellInfo hoveredPartitionCell; - - // Storage bus hover state - public StorageBusInfo hoveredStorageBus; - public int hoveredStorageBusPartitionSlot; - public int hoveredStorageBusContentSlot; - public StorageBusInfo hoveredClearButtonStorageBus; - public StorageBusInfo hoveredIOModeButtonStorageBus; - public StorageBusInfo hoveredPartitionAllButtonStorageBus; - public Set selectedStorageBusIds; - - // Cell button hover state - public CellInfo hoveredPartitionAllButtonCell; - public CellInfo hoveredClearPartitionButtonCell; - - public TabClickContext(int mouseX, int mouseY, int mouseButton, - int guiLeft, int guiTop, int rowsVisible, int currentScroll, - TabContext tabContext) { - this.mouseX = mouseX; - this.mouseY = mouseY; - this.mouseButton = mouseButton; - this.relMouseX = mouseX - guiLeft; - this.relMouseY = mouseY - guiTop; - this.guiLeft = guiLeft; - this.guiTop = guiTop; - this.rowsVisible = rowsVisible; - this.currentScroll = currentScroll; - this.tabContext = tabContext; - } - - /** - * Copy hover state from the GUI after rendering. - */ - public void copyHoverState(CellInfo hoveredCell, int hoverType, - StorageInfo hoveredStorageLine, int hoveredLineIndex, - CellInfo hoveredCellCell, StorageInfo hoveredCellStorage, - int hoveredCellSlotIndex, ItemStack hoveredContentStack, - int hoveredContentSlotIndex, int hoveredPartitionSlotIndex, - CellInfo hoveredPartitionCell, - StorageBusInfo hoveredStorageBus, - int hoveredStorageBusPartitionSlot, - int hoveredStorageBusContentSlot, - StorageBusInfo hoveredClearButtonStorageBus, - StorageBusInfo hoveredIOModeButtonStorageBus, - StorageBusInfo hoveredPartitionAllButtonStorageBus, - Set selectedStorageBusIds, - CellInfo hoveredPartitionAllButtonCell, - CellInfo hoveredClearPartitionButtonCell) { - this.hoveredCell = hoveredCell; - this.hoveredCellHoverType = hoverType; - this.hoveredStorageLine = hoveredStorageLine; - this.hoveredLineIndex = hoveredLineIndex; - this.hoveredCellCell = hoveredCellCell; - this.hoveredCellStorage = hoveredCellStorage; - this.hoveredCellSlotIndex = hoveredCellSlotIndex; - this.hoveredContentStack = hoveredContentStack; - this.hoveredContentSlotIndex = hoveredContentSlotIndex; - this.hoveredPartitionSlotIndex = hoveredPartitionSlotIndex; - this.hoveredPartitionCell = hoveredPartitionCell; - this.hoveredStorageBus = hoveredStorageBus; - this.hoveredStorageBusPartitionSlot = hoveredStorageBusPartitionSlot; - this.hoveredStorageBusContentSlot = hoveredStorageBusContentSlot; - this.hoveredClearButtonStorageBus = hoveredClearButtonStorageBus; - this.hoveredIOModeButtonStorageBus = hoveredIOModeButtonStorageBus; - this.hoveredPartitionAllButtonStorageBus = hoveredPartitionAllButtonStorageBus; - this.selectedStorageBusIds = selectedStorageBusIds; - this.hoveredPartitionAllButtonCell = hoveredPartitionAllButtonCell; - this.hoveredClearPartitionButtonCell = hoveredClearPartitionButtonCell; - } - - /** - * Check if this is a left click. - */ - public boolean isLeftClick() { - return mouseButton == 0; - } - - /** - * Check if this is a right click. - */ - public boolean isRightClick() { - return mouseButton == 1; - } - - /** - * Check if shift is held. - */ - public boolean isShiftHeld() { - return Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); - } -} diff --git a/src/main/java/com/cellterminal/gui/tab/TabContext.java b/src/main/java/com/cellterminal/gui/tab/TabContext.java deleted file mode 100644 index c53d711..0000000 --- a/src/main/java/com/cellterminal/gui/tab/TabContext.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.cellterminal.gui.tab; - -import java.util.List; -import java.util.Map; - -import net.minecraft.item.ItemStack; - -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.StorageBusInfo; -import com.cellterminal.client.StorageInfo; -import com.cellterminal.gui.handler.TerminalDataManager; - - -/** - * Context object providing access to tab-related state and services. - * Passed to tab controllers to give them access to the data they need. - */ -public class TabContext { - - private final TerminalDataManager dataManager; - private final TabContextCallback callback; - private final int terminalDimension; - - public TabContext(TerminalDataManager dataManager, TabContextCallback callback, int terminalDimension) { - this.dataManager = dataManager; - this.callback = callback; - this.terminalDimension = terminalDimension; - } - - public TerminalDataManager getDataManager() { - return dataManager; - } - - public Map getStorageMap() { - return dataManager.getStorageMap(); - } - - public Map getStorageBusMap() { - return dataManager.getStorageBusMap(); - } - - public int getTerminalDimension() { - return terminalDimension; - } - - public List getTerminalLines() { - return dataManager.getLines(); - } - - public List getInventoryLines() { - return dataManager.getInventoryLines(); - } - - public List getPartitionLines() { - return dataManager.getPartitionLines(); - } - - public List getStorageBusInventoryLines() { - return dataManager.getStorageBusInventoryLines(); - } - - public List getStorageBusPartitionLines() { - return dataManager.getStorageBusPartitionLines(); - } - - public List getTempAreaLines() { - return dataManager.getTempAreaLines(); - } - - // Callback methods for GUI interactions - - public void toggleStorageExpansion(StorageInfo storage) { - callback.onStorageToggle(storage); - } - - public void openInventoryPopup(CellInfo cell, int mouseX, int mouseY) { - callback.openInventoryPopup(cell, mouseX, mouseY); - } - - public void openPartitionPopup(CellInfo cell, int mouseX, int mouseY) { - callback.openPartitionPopup(cell, mouseX, mouseY); - } - - public void togglePartitionItem(CellInfo cell, ItemStack stack) { - callback.onTogglePartitionItem(cell, stack); - } - - public void addPartitionItem(CellInfo cell, int slotIndex, ItemStack stack) { - callback.onAddPartitionItem(cell, slotIndex, stack); - } - - public void removePartitionItem(CellInfo cell, int slotIndex) { - callback.onRemovePartitionItem(cell, slotIndex); - } - - public void scrollToLine(int lineIndex) { - callback.scrollToLine(lineIndex); - } - - /** - * Callback interface for GUI interactions triggered by tab controllers. - */ - public interface TabContextCallback { - - void onStorageToggle(StorageInfo storage); - - void openInventoryPopup(CellInfo cell, int mouseX, int mouseY); - - void openPartitionPopup(CellInfo cell, int mouseX, int mouseY); - - void onTogglePartitionItem(CellInfo cell, ItemStack stack); - - void onAddPartitionItem(CellInfo cell, int slotIndex, ItemStack stack); - - void onRemovePartitionItem(CellInfo cell, int slotIndex); - - void scrollToLine(int lineIndex); - } -} diff --git a/src/main/java/com/cellterminal/gui/tab/TabControllerRegistry.java b/src/main/java/com/cellterminal/gui/tab/TabControllerRegistry.java deleted file mode 100644 index c6bf536..0000000 --- a/src/main/java/com/cellterminal/gui/tab/TabControllerRegistry.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.cellterminal.gui.tab; - -import java.util.HashMap; -import java.util.Map; - - -/** - * Registry for tab controllers. - * Provides access to tab controllers by index. - */ -public class TabControllerRegistry { - - private static final Map controllers = new HashMap<>(); - - static { - registerController(new TerminalTabController()); - registerController(new InventoryTabController()); - registerController(new PartitionTabController()); - registerController(new TempAreaTabController()); - registerController(new StorageBusInventoryTabController()); - registerController(new StorageBusPartitionTabController()); - registerController(new NetworkToolsTabController()); - } - - private static void registerController(ITabController controller) { - controllers.put(controller.getTabIndex(), controller); - } - - /** - * Get the controller for a specific tab index. - * @param tabIndex The tab index - * @return The controller, or null if not found - */ - public static ITabController getController(int tabIndex) { - return controllers.get(tabIndex); - } - - /** - * Get the total number of tabs. - * @return The number of registered tabs - */ - public static int getTabCount() { - return controllers.size(); - } - - /** - * Check if a tab requires server polling. - * @param tabIndex The tab index - * @return true if the tab requires polling - */ - public static boolean requiresPolling(int tabIndex) { - ITabController controller = controllers.get(tabIndex); - - return controller != null && controller.requiresServerPolling(); - } -} diff --git a/src/main/java/com/cellterminal/gui/tab/TempAreaTabController.java b/src/main/java/com/cellterminal/gui/tab/TempAreaTabController.java deleted file mode 100644 index 71dd962..0000000 --- a/src/main/java/com/cellterminal/gui/tab/TempAreaTabController.java +++ /dev/null @@ -1,246 +0,0 @@ -package com.cellterminal.gui.tab; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -import net.minecraft.client.Minecraft; -import net.minecraft.client.resources.I18n; -import net.minecraft.inventory.Slot; -import net.minecraft.item.ItemStack; - -import net.minecraftforge.fluids.FluidStack; - -import appeng.fluids.items.FluidDummyItem; - -import com.cellterminal.client.CellInfo; -import com.cellterminal.client.KeyBindings; -import com.cellterminal.client.SearchFilterMode; -import com.cellterminal.client.TempCellInfo; -import com.cellterminal.gui.GuiConstants; -import com.cellterminal.gui.handler.QuickPartitionHandler; -import com.cellterminal.gui.overlay.MessageHelper; -import com.cellterminal.integration.ThaumicEnergisticsIntegration; -import com.cellterminal.network.CellTerminalNetwork; -import com.cellterminal.network.PacketTempCellPartitionAction; - - -/** - * Tab controller for the Temp Area tab (Tab 3). - * This tab provides a temporary staging area for cells where users can: - * - Place cells from their inventory - * - View cell contents (like Inventory tab) - * - Edit cell partitions (like Partition tab) - * - Send cells to the first available slot in the network - *

- * Uses selection-based partitioning like StorageBusPartitionTabController: - * select cell(s) by clicking header, then press keybind to add hovered item. - */ -public class TempAreaTabController implements ITabController { - - @Override - public int getTabIndex() { - return GuiConstants.TAB_TEMP_AREA; - } - - @Override - public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { - // Temp area respects the user's selected search mode - return userSelectedMode; - } - - @Override - public boolean showSearchModeButton() { - // Show search mode button since we support both inventory and partition views - return true; - } - - @Override - public List getHelpLines() { - List lines = new ArrayList<>(); - - lines.add(I18n.format("gui.cellterminal.controls.temp_area.drag_cell")); - lines.add(I18n.format("gui.cellterminal.controls.temp_area.send_cell")); - - lines.add(""); - - // Use ADD_TO_STORAGE_BUS keybind like storage bus partition tab - lines.add(I18n.format("gui.cellterminal.controls.temp_area.add_key", - KeyBindings.ADD_TO_STORAGE_BUS.getDisplayName())); - - lines.add(""); - - lines.add(I18n.format("gui.cellterminal.controls.jei_drag")); - lines.add(I18n.format("gui.cellterminal.controls.click_to_remove")); - - return lines; - } - - @Override - public boolean handleClick(TabClickContext context) { - // Click handling for temp area is done in GuiCellTerminalBase - // - Click on cell slot to insert/extract cell - // - Click on Send button to send cell to network - // - Click on partition slots to edit partition - return false; - } - - @Override - public boolean handleKeyTyped(int keyCode, TabContext context) { - return false; - } - - @Override - public boolean requiresServerPolling() { - // Temp area doesn't need server polling - data is managed locally - return false; - } - - @Override - public List getLines(TabContext context) { - return context.getTempAreaLines(); - } - - @Override - public int getLineCount(TabContext context) { - return context.getTempAreaLines().size(); - } - - /** - * Handle the add-to-temp-cell keybind (same key as ADD_TO_STORAGE_BUS). - * Adds the hovered item to all selected temp cells' partitions. - * Matches storage bus behavior: converts items for fluid/essentia cells, finds empty slots. - * - * @param selectedTempCellSlots Set of selected temp cell slot indexes - * @param hoveredSlot The slot the mouse is over (or null) - * @param tempAreaLines List of temp area line objects - * @return true if the keybind was handled - */ - public static boolean handleAddToTempCellKeybind(Set selectedTempCellSlots, - Slot hoveredSlot, - List tempAreaLines) { - if (selectedTempCellSlots.isEmpty()) { - if (Minecraft.getMinecraft().player != null) { - MessageHelper.warning("gui.cellterminal.temp_area.no_selection"); - } - - return true; - } - - // Get the item to add - ItemStack stack = ItemStack.EMPTY; - if (hoveredSlot != null && hoveredSlot.getHasStack()) stack = hoveredSlot.getStack(); - - // Try JEI/bookmark if no inventory item - if (stack.isEmpty()) { - QuickPartitionHandler.HoveredIngredient jeiItem = QuickPartitionHandler.getHoveredIngredient(); - if (jeiItem != null && !jeiItem.stack.isEmpty()) stack = jeiItem.stack; - } - - if (stack.isEmpty()) { - if (Minecraft.getMinecraft().player != null) { - MessageHelper.warning("gui.cellterminal.temp_area.no_item"); - } - - return true; - } - - // Add to all selected temp cells - int successCount = 0; - int invalidItemCount = 0; - int noSlotCount = 0; - - for (Integer tempSlotIndex : selectedTempCellSlots) { - // Find the TempCellInfo for this slot - TempCellInfo tempCell = findTempCellBySlot(tempAreaLines, tempSlotIndex); - if (tempCell == null || tempCell.getCellInfo() == null) continue; - - CellInfo cellInfo = tempCell.getCellInfo(); - - // Convert the item for non-item cell types first to check validity - ItemStack stackToSend = stack; - boolean validForCellType = true; - - if (cellInfo.isFluid()) { - // For fluid cells, need FluidDummyItem or fluid container - if (!(stack.getItem() instanceof FluidDummyItem)) { - FluidStack fluid = net.minecraftforge.fluids.FluidUtil.getFluidContained(stack); - // Can't use this item on fluid cell - if (fluid == null) { - invalidItemCount++; - validForCellType = false; - } - } - } else if (cellInfo.isEssentia()) { - // For essentia cells, need ItemDummyAspect or essentia container - ItemStack essentiaRep = ThaumicEnergisticsIntegration.tryConvertEssentiaContainerToAspect(stack); - // Can't use this item on essentia cell - if (essentiaRep.isEmpty()) { - invalidItemCount++; - validForCellType = false; - } else { - stackToSend = essentiaRep; - } - } - - if (!validForCellType) continue; - - // Find first empty slot in this cell's partition - // For cells, use totalTypes as the maximum config slots (always 63 for standard cells) - List partition = cellInfo.getPartition(); - int availableSlots = (int) cellInfo.getTotalTypes(); - int targetSlot = -1; - - for (int i = 0; i < availableSlots; i++) { - if (i >= partition.size() || partition.get(i).isEmpty()) { - targetSlot = i; - break; - } - } - - if (targetSlot < 0) { - noSlotCount++; - continue; - } - - // Send packet to add item to this temp cell's partition at specific slot - CellTerminalNetwork.INSTANCE.sendToServer( - new PacketTempCellPartitionAction( - tempSlotIndex, - PacketTempCellPartitionAction.Action.ADD_ITEM, - targetSlot, - stackToSend - ) - ); - successCount++; - } - - if (successCount == 0 && Minecraft.getMinecraft().player != null) { - // Show appropriate error message based on what failed - if (invalidItemCount > 0 && noSlotCount == 0) { - MessageHelper.error("gui.cellterminal.temp_area.invalid_item"); - } else if (noSlotCount > 0 && invalidItemCount == 0) { - MessageHelper.error("gui.cellterminal.temp_area.partition_full"); - } else { - // Mixed or other failure - MessageHelper.error("gui.cellterminal.temp_area.add_failed"); - } - } - - return true; - } - - /** - * Find TempCellInfo for a given slot index in the temp area lines. - */ - private static TempCellInfo findTempCellBySlot(List lines, int slotIndex) { - for (Object line : lines) { - if (line instanceof TempCellInfo) { - TempCellInfo tempCell = (TempCellInfo) line; - if (tempCell.getTempSlotIndex() == slotIndex) return tempCell; - } - } - - return null; - } -} diff --git a/src/main/java/com/cellterminal/gui/tab/TerminalTabController.java b/src/main/java/com/cellterminal/gui/tab/TerminalTabController.java deleted file mode 100644 index 3940ef5..0000000 --- a/src/main/java/com/cellterminal/gui/tab/TerminalTabController.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.cellterminal.gui.tab; - -import java.util.ArrayList; -import java.util.List; - -import net.minecraft.client.resources.I18n; - -import com.cellterminal.client.SearchFilterMode; -import com.cellterminal.gui.GuiConstants; - - -/** - * Tab controller for the Terminal tab (Tab 0). - * This tab displays a tree-style list of storages with expandable cells. - */ -public class TerminalTabController implements ITabController { - - @Override - public int getTabIndex() { - return GuiConstants.TAB_TERMINAL; - } - - @Override - public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { - // Terminal tab respects the user's selected search mode - return userSelectedMode; - } - - @Override - public boolean showSearchModeButton() { - // Only Terminal tab shows the search mode button - return true; - } - - @Override - public List getHelpLines() { - List lines = new ArrayList<>(); - lines.add(I18n.format("gui.cellterminal.controls.double_click_storage_cell")); - lines.add(I18n.format("gui.cellterminal.right_click_rename")); - - return lines; - } - - @Override - public boolean handleClick(TabClickContext context) { - // Terminal tab click handling is done by TerminalClickHandler - // This controller just provides the structure - return false; - } - - @Override - public boolean handleKeyTyped(int keyCode, TabContext context) { - // Terminal tab has no special keybinds - return false; - } - - @Override - public List getLines(TabContext context) { - return context.getTerminalLines(); - } - - @Override - public int getLineCount(TabContext context) { - return context.getTerminalLines().size(); - } -} diff --git a/src/main/java/com/cellterminal/gui/widget/AbstractWidget.java b/src/main/java/com/cellterminal/gui/widget/AbstractWidget.java new file mode 100644 index 0000000..f03e89d --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/AbstractWidget.java @@ -0,0 +1,127 @@ +package com.cellterminal.gui.widget; + +import org.lwjgl.opengl.GL11; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.item.ItemStack; + + +/** + * Base implementation of {@link IWidget} providing common fields and behavior. + *

+ * Subclasses must implement {@link #draw(int, int)} and {@link #handleClick(int, int, int)}. + * Hover detection is provided by default based on the widget's bounding rectangle. + *

+ * Also provides shared utility methods for common rendering tasks used + * across the widget hierarchy (line, header, tab, etc.). + */ +public abstract class AbstractWidget implements IWidget { + + protected int x; + protected int y; + protected int width; + protected int height; + protected boolean visible = true; + + protected AbstractWidget(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + @Override + public boolean isHovered(int mouseX, int mouseY) { + if (!visible) return false; + + return mouseX >= x && mouseX < x + width + && mouseY >= y && mouseY < y + height; + } + + @Override + public int getX() { + return x; + } + + @Override + public int getY() { + return y; + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public void setPosition(int x, int y) { + this.x = x; + this.y = y; + } + + public void setSize(int width, int height) { + this.width = width; + this.height = height; + } + + // ---- Shared utilities ---- + + /** + * Truncate a string to fit within a pixel width, appending "..." if needed. + * Correctly handles §X formatting codes via fontRenderer.getStringWidth. + *

+ * Shared across both line and header widget hierarchies + * to avoid duplicating the truncation logic. + */ + public static String trimTextToWidth(FontRenderer fontRenderer, String text, int maxWidth) { + if (fontRenderer.getStringWidth(text) <= maxWidth) return text; + + String ellipsis = "..."; + int ellipWidth = fontRenderer.getStringWidth(ellipsis); + + for (int i = text.length() - 1; i > 0; i--) { + String trimmed = text.substring(0, i); + if (fontRenderer.getStringWidth(trimmed) + ellipWidth <= maxWidth) { + return trimmed + ellipsis; + } + } + + return ellipsis; + } + + /** + * Render an item stack at the given position with standard GUI lighting. + * Restores GL state (lighting, blend) after rendering. + *

+ * Shared across both line and header widget hierarchies + * to avoid duplicating the item rendering boilerplate. + */ + public static void renderItemStack(RenderItem itemRender, ItemStack stack, int renderX, int renderY) { + if (stack.isEmpty()) return; + + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + RenderHelper.enableGUIStandardItemLighting(); + itemRender.renderItemIntoGUI(stack, renderX, renderY); + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableLighting(); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/CardsDisplay.java b/src/main/java/com/cellterminal/gui/widget/CardsDisplay.java new file mode 100644 index 0000000..631d914 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/CardsDisplay.java @@ -0,0 +1,192 @@ +package com.cellterminal.gui.widget; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import org.lwjgl.opengl.GL11; + +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.client.resources.I18n; +import net.minecraft.item.ItemStack; + + +/** + * Widget that displays cell upgrade cards (small 8x8 icons). + *

+ * Cards are drawn as half-size item icons in a 2-column grid layout. + * Columns are placed left-to-right, rows top-to-bottom. + * Supports hover tracking for tooltip display and click-to-extract behavior. + *

+ * Usage: created by line or header widgets, positioned relative to the cell icon. + * The cards data comes from a supplier so the widget always reflects current state. + */ +public class CardsDisplay extends AbstractWidget { + + /** Width of each card icon (8px half-size item) */ + private static final int CARD_ICON_SIZE = 8; + + /** Spacing between card icons (icon + 1px gap) */ + private static final int CARD_STRIDE = 9; + + /** Number of columns in the card grid */ + private static final int COLUMNS = 2; + + private final Supplier> cardsSupplier; + private final RenderItem itemRender; + + // Tracked hover state + private int hoveredCardIndex = -1; + private ItemStack hoveredCardStack = ItemStack.EMPTY; + + /** + * A single card entry with its item and slot position. + */ + public static class CardEntry { + public final ItemStack stack; + public final int slotIndex; + + public CardEntry(ItemStack stack, int slotIndex) { + this.stack = stack; + this.slotIndex = slotIndex; + } + } + + /** + * Callback for card click events (upgrade extraction). + */ + @FunctionalInterface + public interface CardClickCallback { + /** + * @param slotIndex The upgrade slot index that was clicked + */ + void onCardClicked(int slotIndex); + } + + private CardClickCallback clickCallback; + + /** + * @param x X position (relative to GUI) + * @param y Y position (relative to GUI, aligned with top of cell icon) + * @param cardsSupplier Supplier for the card entries to display + * @param itemRender Item renderer for icons + */ + public CardsDisplay(int x, int y, Supplier> cardsSupplier, RenderItem itemRender) { + super(x, y, 0, CARD_ICON_SIZE); + this.cardsSupplier = cardsSupplier; + this.itemRender = itemRender; + } + + public void setClickCallback(CardClickCallback callback) { + this.clickCallback = callback; + } + + @Override + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + List cards = cardsSupplier.get(); + if (cards.isEmpty()) return; + + hoveredCardIndex = -1; + hoveredCardStack = ItemStack.EMPTY; + + // Calculate grid dimensions + int rows = (cards.size() + COLUMNS - 1) / COLUMNS; + this.width = COLUMNS * CARD_STRIDE; + this.height = rows * CARD_STRIDE; + + // Render each card in a 2-column grid (column-first, then row) + for (int i = 0; i < cards.size(); i++) { + CardEntry entry = cards.get(i); + int col = i % COLUMNS; + int row = i / COLUMNS; + int iconX = x + col * CARD_STRIDE; + int iconY = y + row * CARD_STRIDE; + + if (!entry.stack.isEmpty()) { + renderSmallItemStack(entry.stack, iconX, iconY); + + // Check hover (using 8x8 icon bounds) - only for non-empty cards + if (mouseX >= iconX && mouseX < iconX + CARD_ICON_SIZE + && mouseY >= iconY && mouseY < iconY + CARD_ICON_SIZE) { + hoveredCardIndex = entry.slotIndex; + hoveredCardStack = entry.stack; + } + } + } + } + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible || button != 0 || hoveredCardIndex < 0) return false; + if (clickCallback == null) return false; + + clickCallback.onCardClicked(hoveredCardIndex); + + return true; + } + + @Override + public boolean isHovered(int mouseX, int mouseY) { + if (!visible) return false; + + List cards = cardsSupplier.get(); + if (cards.isEmpty()) return false; + + // Check each individual card's bounds in the 2-column grid + // Only non-empty cards are hoverable (empty slots are just visual placeholders) + for (int i = 0; i < cards.size(); i++) { + CardEntry entry = cards.get(i); + if (entry.stack.isEmpty()) continue; + + int col = i % COLUMNS; + int row = i / COLUMNS; + int iconX = x + col * CARD_STRIDE; + int iconY = y + row * CARD_STRIDE; + if (mouseX >= iconX && mouseX < iconX + CARD_ICON_SIZE + && mouseY >= iconY && mouseY < iconY + CARD_ICON_SIZE) { + return true; + } + } + + return false; + } + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (hoveredCardIndex < 0 || hoveredCardStack.isEmpty()) return Collections.emptyList(); + + // Return the item's display name with extraction hint + List lines = new ArrayList<>(); + lines.add("§6" + hoveredCardStack.getDisplayName()); + lines.add(""); + lines.add("§b" + I18n.format("gui.cellterminal.upgrade.click_extract")); + lines.add("§b" + I18n.format("gui.cellterminal.upgrade.shift_click_inventory")); + + return lines; + } + + private void renderSmallItemStack(ItemStack stack, int renderX, int renderY) { + if (stack.isEmpty()) return; + + GlStateManager.pushMatrix(); + GlStateManager.translate(renderX, renderY, 0); + GlStateManager.scale(0.5f, 0.5f, 1.0f); + + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + RenderHelper.enableGUIStandardItemLighting(); + itemRender.renderItemIntoGUI(stack, 0, 0); + RenderHelper.disableStandardItemLighting(); + GlStateManager.disableLighting(); + + GlStateManager.popMatrix(); + + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/DoubleClickTracker.java b/src/main/java/com/cellterminal/gui/widget/DoubleClickTracker.java new file mode 100644 index 0000000..c890052 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/DoubleClickTracker.java @@ -0,0 +1,102 @@ +package com.cellterminal.gui.widget; + +import com.cellterminal.gui.GuiConstants; + + +/** + * Centralized double-click tracking for widgets that are recreated each frame. + *

+ * Since widgets (headers, lines) are recreated in {@code buildVisibleRows()} every frame, + * storing {@code lastClickTime} in the widget instance doesn't work - the state is lost + * when a new widget object is created. + *

+ * This static tracker stores the last click time and target ID, allowing widgets to + * detect double-clicks across instance recreations. + *

+ * Usage: + *

+ * // In widget's handleClick:
+ * if (DoubleClickTracker.isDoubleClick(targetId)) {
+ *     onDoubleClick();
+ *     return true;
+ * }
+ * 
+ */ +public final class DoubleClickTracker { + + private DoubleClickTracker() {} + + /** The ID of the last clicked target (combines type + id) */ + private static long lastClickTargetId = -1; + + /** Time of the last click */ + private static long lastClickTime = 0; + + /** + * Check if the current click is a double-click on the given target. + *

+ * If this is the first click on the target, records the time and returns false. + * If this is a second click within the threshold, returns true and resets the state. + * + * @param targetId A unique identifier for the click target (e.g., storage ID, cell slot, etc.) + * @return true if this is a double-click, false otherwise + */ + public static boolean isDoubleClick(long targetId) { + long currentTime = System.currentTimeMillis(); + + if (lastClickTargetId == targetId + && currentTime - lastClickTime < GuiConstants.DOUBLE_CLICK_TIME_MS) { + // Double-click detected - reset state to prevent triple-click triggering + lastClickTargetId = -1; + lastClickTime = 0; + return true; + } + + // First click or different target - record for potential double-click + lastClickTargetId = targetId; + lastClickTime = currentTime; + return false; + } + + /** + * Generate a unique target ID for a storage (drive/chest). + * Uses positive IDs. + */ + public static long storageTargetId(long storageId) { + return storageId; + } + + /** + * Generate a unique target ID for a cell within a storage. + * Encodes both storage ID and slot to create a unique identifier. + */ + public static long cellTargetId(long storageId, int slot) { + // Combine storage ID and slot into unique ID (high bits = storage, low 8 bits = slot) + return (storageId << 8) | (slot & 0xFF); + } + + /** + * Generate a unique target ID for a storage bus. + * Uses negative IDs to distinguish from storages. + */ + public static long storageBusTargetId(long busId) { + return -busId - 1; + } + + /** + * Generate a unique target ID for a subnet. + * Uses a distinct negative range (offset by Long.MIN_VALUE/2) to avoid + * collision with storageBusTargetId which uses -id - 1. + */ + public static long subnetTargetId(long subnetId) { + return Long.MIN_VALUE / 2 - subnetId; + } + + /** + * Reset the tracker state (e.g., when closing GUI). + */ + public static void reset() { + lastClickTargetId = -1; + lastClickTime = 0; + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/IWidget.java b/src/main/java/com/cellterminal/gui/widget/IWidget.java new file mode 100644 index 0000000..41c883b --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/IWidget.java @@ -0,0 +1,115 @@ +package com.cellterminal.gui.widget; + +import java.util.Collections; +import java.util.List; + +import net.minecraft.item.ItemStack; + + +/** + * Base interface for all widgets in the Cell Terminal GUI. + *

+ * A widget is a self-contained visual component that handles its own: + * - Rendering (draw) + * - Click handling (handleClick) + * - Keyboard handling (handleKey) + * - Hover detection (isHovered) + * - Tooltip provision (getTooltip) + *

+ * Widgets do not need to know about sibling widgets. They only communicate + * upward through return values (e.g., "I handled this click") or callbacks + * provided at construction time. + */ +public interface IWidget { + + /** + * Draw this widget. + * + * @param mouseX Mouse X relative to the GUI + * @param mouseY Mouse Y relative to the GUI + */ + void draw(int mouseX, int mouseY); + + /** + * Handle a mouse click. + * + * @param mouseX Mouse X relative to the GUI + * @param mouseY Mouse Y relative to the GUI + * @param button Mouse button (0=left, 1=right, 2=middle) + * @return true if the click was handled and should not propagate + */ + boolean handleClick(int mouseX, int mouseY, int button); + + /** + * Handle a key press. + * + * @param typedChar The typed character + * @param keyCode The key code + * @return true if the key was handled and should not propagate + */ + default boolean handleKey(char typedChar, int keyCode) { + return false; + } + + /** + * Check if the mouse is over this widget. + * + * @param mouseX Mouse X relative to the GUI + * @param mouseY Mouse Y relative to the GUI + * @return true if the mouse is over this widget + */ + boolean isHovered(int mouseX, int mouseY); + + /** + * Get tooltip lines to display when this widget is hovered. + * Returns empty list if no tooltip should be shown. + * + * @param mouseX Mouse X relative to the GUI + * @param mouseY Mouse Y relative to the GUI + * @return List of tooltip lines (can be empty, never null) + */ + default List getTooltip(int mouseX, int mouseY) { + return Collections.emptyList(); + } + + /** + * Get the ItemStack under the mouse cursor, if any. + * Used by the parent GUI to render item tooltips (which require full GUI context + * with drawHoveringText). This is separate from {@link #getTooltip} which provides + * custom text tooltips (button hints, etc.). + * + * @param mouseX Mouse X relative to the GUI + * @param mouseY Mouse Y relative to the GUI + * @return The hovered ItemStack, or ItemStack.EMPTY if none + */ + default ItemStack getHoveredItemStack(int mouseX, int mouseY) { + return ItemStack.EMPTY; + } + + /** + * Get the X position of this widget relative to GUI. + */ + int getX(); + + /** + * Get the Y position of this widget relative to GUI. + */ + int getY(); + + /** + * Get the width of this widget. + */ + int getWidth(); + + /** + * Get the height of this widget. + */ + int getHeight(); + + /** + * Whether this widget is visible and should be drawn. + */ + default boolean isVisible() { + return true; + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/NetworkToolRowWidget.java b/src/main/java/com/cellterminal/gui/widget/NetworkToolRowWidget.java new file mode 100644 index 0000000..e3ae432 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/NetworkToolRowWidget.java @@ -0,0 +1,243 @@ +package com.cellterminal.gui.widget; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.util.ResourceLocation; + +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.networktools.INetworkTool; +import com.cellterminal.gui.networktools.INetworkTool.ToolContext; +import com.cellterminal.gui.networktools.INetworkTool.ToolPreviewInfo; + + +/** + * Self-contained widget representing a single network tool row. + *

+ * Layout (two-line row with separator at bottom): + *

+ *   [?] [icon] countText                    [▶]   <- Line 1
+ *     Tool Name                                    <- Line 2
+ *   ____________________________________________   <- Separator
+ * 
+ * + * Each row handles its own: + *
    + *
  • Help pseudo-button (?) with tooltip
  • + *
  • Tool icon (ItemStack) with count text
  • + *
  • Run button (atlas texture, 3 states: normal/hovered/disabled)
  • + *
  • Tool name on the second line
  • + *
  • Hover detection for sub-areas (help, run, row)
  • + *
  • Click handling for the run button
  • + *
+ */ +public class NetworkToolRowWidget extends AbstractWidget { + + public static final int ROW_HEIGHT = 36; + + private static final int PADDING = 4; + private static final int ICON_SIZE = 16; + private static final int HELP_BUTTON_SIZE = GuiConstants.TOOLTIP_BUTTON_SIZE; + private static final int HELP_BUTTON_Y_OFFSET = 3; + private static final int RUN_SIZE = GuiConstants.NETWORK_TOOL_RUN_BUTTON_SIZE; + private static final ResourceLocation ATLAS_TEXTURE = + new ResourceLocation("cellterminal", "textures/guis/atlas.png"); + + private final INetworkTool tool; + private final FontRenderer fontRenderer; + private final RenderItem itemRender; + + /** Callback for when the run button is clicked */ + private Runnable onRunClicked; + + /** Supplier for the tool context (re-evaluated each frame for live preview) */ + private Supplier contextSupplier; + + // Sub-area positions (computed during draw, used for hover/click) + private int helpBtnX, helpBtnY; + private int runBtnX, runBtnY; + private boolean runHovered, helpHovered; + private boolean canExecute; + + public NetworkToolRowWidget(INetworkTool tool, int y, + FontRenderer fontRenderer, RenderItem itemRender) { + super(GuiConstants.GUI_INDENT, y, + GuiConstants.CONTENT_RIGHT_EDGE - GuiConstants.GUI_INDENT, ROW_HEIGHT); + this.tool = tool; + this.fontRenderer = fontRenderer; + this.itemRender = itemRender; + } + + public void setOnRunClicked(Runnable onRunClicked) { + this.onRunClicked = onRunClicked; + } + + public void setContextSupplier(java.util.function.Supplier contextSupplier) { + this.contextSupplier = contextSupplier; + } + + public INetworkTool getTool() { + return tool; + } + + // ---- Drawing ---- + + @Override + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + boolean rowHovered = isHovered(mouseX, mouseY); + + // Row background + int bgColor = rowHovered ? 0x30FFFFFF : 0x20FFFFFF; + Gui.drawRect(x, y, x + width, y + height - 1, bgColor); + + // Bottom separator line + Gui.drawRect(x, y + height - 1, x + width, y + height, GuiConstants.COLOR_SEPARATOR); + + // Get preview from current tool context + ToolContext ctx = contextSupplier != null ? contextSupplier.get() : null; + ToolPreviewInfo preview = ctx != null ? tool.getPreview(ctx) : null; + String executionError = ctx != null ? tool.getExecutionError(ctx) : "No context"; + canExecute = executionError == null; + + // Compute sub-area positions + helpBtnX = x + PADDING; + helpBtnY = y + PADDING + HELP_BUTTON_Y_OFFSET; + runBtnX = x + width - RUN_SIZE - PADDING; + runBtnY = y + PADDING; + + // Check sub-area hover + helpHovered = mouseX >= helpBtnX && mouseX < helpBtnX + HELP_BUTTON_SIZE + && mouseY >= helpBtnY && mouseY < helpBtnY + HELP_BUTTON_SIZE; + runHovered = mouseX >= runBtnX && mouseX < runBtnX + RUN_SIZE + && mouseY >= runBtnY && mouseY < runBtnY + RUN_SIZE; + + drawHelpButton(); + drawToolIcon(preview); + drawRunButton(); + drawToolName(); + } + + private void drawHelpButton() { + // Background from atlas (same texture as GuiSearchHelpButton) + Minecraft.getMinecraft().getTextureManager().bindTexture(ATLAS_TEXTURE); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + + int texX = GuiConstants.TOOLTIP_BUTTON_X; + int texY = GuiConstants.TOOLTIP_BUTTON_Y + (helpHovered ? HELP_BUTTON_SIZE : 0); + Gui.drawScaledCustomSizeModalRect( + helpBtnX, helpBtnY, texX, texY, + HELP_BUTTON_SIZE, HELP_BUTTON_SIZE, HELP_BUTTON_SIZE, HELP_BUTTON_SIZE, + GuiConstants.ATLAS_WIDTH, GuiConstants.ATLAS_HEIGHT); + } + + private void drawToolIcon(ToolPreviewInfo preview) { + int iconX = x + PADDING + HELP_BUTTON_SIZE + 4; + int iconY = y + PADDING; + + // Draw the tool icon ItemStack + if (preview != null && !preview.getIcon().isEmpty()) { + AbstractWidget.renderItemStack(itemRender, preview.getIcon(), iconX, iconY); + } + + // Draw preview count text after the icon + if (preview != null) { + String countText = preview.getCountText(); + int countColor = preview.getCountColor(); + int countX = iconX + ICON_SIZE + 4; + int countY = iconY + (ICON_SIZE - fontRenderer.FONT_HEIGHT) / 2; + fontRenderer.drawString(countText, countX, countY, countColor); + } + } + + private void drawRunButton() { + Minecraft.getMinecraft().getTextureManager().bindTexture(ATLAS_TEXTURE); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + + int texX = GuiConstants.NETWORK_TOOL_RUN_BUTTON_X; + int texY; + if (!canExecute) { + texY = GuiConstants.NETWORK_TOOL_RUN_BUTTON_Y + 2 * RUN_SIZE; // Disabled state + } else if (runHovered) { + texY = GuiConstants.NETWORK_TOOL_RUN_BUTTON_Y + RUN_SIZE; // Hovered state + } else { + texY = GuiConstants.NETWORK_TOOL_RUN_BUTTON_Y; // Normal state + } + + Gui.drawScaledCustomSizeModalRect( + runBtnX, runBtnY, texX, texY, + RUN_SIZE, RUN_SIZE, RUN_SIZE, RUN_SIZE, + GuiConstants.ATLAS_WIDTH, GuiConstants.ATLAS_HEIGHT); + } + + private void drawToolName() { + int nameX = x + PADDING; + int nameY = y + PADDING + ICON_SIZE + 2; + int maxNameWidth = width - PADDING * 2; + String toolName = tool.getName(); + + String displayName = AbstractWidget.trimTextToWidth(fontRenderer, toolName, maxNameWidth); + fontRenderer.drawString(displayName, nameX, nameY, 0x000000); + } + + // ---- Click handling ---- + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible || button != 0) return false; + if (!isHovered(mouseX, mouseY)) return false; + + // Only the run button is clickable + if (runHovered && canExecute && onRunClicked != null) { + onRunClicked.run(); + + return true; + } + + return false; + } + + // ---- Tooltips ---- + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return Collections.emptyList(); + + // Help button tooltip: tool name + description + if (helpHovered) { + List tooltip = new ArrayList<>(); + tooltip.add("§e" + tool.getName()); + tooltip.add(""); + for (String line : tool.getHelpLines()) tooltip.add("§7" + line); + + return tooltip; + } + + // Row hover tooltip: tool name + preview details + ToolContext ctx = contextSupplier != null ? contextSupplier.get() : null; + ToolPreviewInfo preview = ctx != null ? tool.getPreview(ctx) : null; + if (preview != null) { + List tooltipLines = preview.getTooltipLines(); + if (tooltipLines != null && !tooltipLines.isEmpty()) { + List lines = new ArrayList<>(); + lines.add("§e" + tool.getName()); + lines.add(""); + lines.addAll(tooltipLines); + + return lines; + } + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/WidgetContainer.java b/src/main/java/com/cellterminal/gui/widget/WidgetContainer.java new file mode 100644 index 0000000..fad889c --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/WidgetContainer.java @@ -0,0 +1,130 @@ +package com.cellterminal.gui.widget; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import net.minecraft.item.ItemStack; + + +/** + * A container that manages a list of child widgets. + *

+ * Provides ordered iteration for drawing (first added = drawn first = background) + * and reverse iteration for click handling (last added = drawn on top = gets first click). + *

+ * Events propagate to children in the appropriate order and stop at the first + * handler that returns true (indicating the event was consumed). + */ +public class WidgetContainer extends AbstractWidget { + + protected final List children = new ArrayList<>(); + + public WidgetContainer(int x, int y, int width, int height) { + super(x, y, width, height); + } + + /** + * Add a child widget at the end of the list (drawn on top, gets clicks first). + */ + public void addChild(IWidget child) { + children.add(child); + } + + /** + * Remove a child widget. + */ + public void removeChild(IWidget child) { + children.remove(child); + } + + /** + * Remove all child widgets. + */ + public void clearChildren() { + children.clear(); + } + + /** + * Get an unmodifiable view of the children. + */ + public List getChildren() { + return Collections.unmodifiableList(children); + } + + @Override + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + // Draw children in order (first = background, last = foreground) + for (IWidget child : children) { + if (child.isVisible()) child.draw(mouseX, mouseY); + } + } + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible) return false; + + // Process clicks in reverse order (foreground widgets get priority) + for (int i = children.size() - 1; i >= 0; i--) { + IWidget child = children.get(i); + + if (child.isVisible() && child.isHovered(mouseX, mouseY)) { + if (child.handleClick(mouseX, mouseY, button)) return true; + } + } + + return false; + } + + @Override + public boolean handleKey(char typedChar, int keyCode) { + if (!visible) return false; + + // Propagate to children in reverse order (foreground widgets get priority) + for (int i = children.size() - 1; i >= 0; i--) { + IWidget child = children.get(i); + + if (child.isVisible() && child.handleKey(typedChar, keyCode)) return true; + } + + return false; + } + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (!visible) return Collections.emptyList(); + + // Check children in reverse order (foreground first) + for (int i = children.size() - 1; i >= 0; i--) { + IWidget child = children.get(i); + + if (!child.isVisible() || !child.isHovered(mouseX, mouseY)) continue; + + List tooltip = child.getTooltip(mouseX, mouseY); + // Item tooltips (cell slots, content slots) are handled separately via + // getHoveredItemStack(), so text tooltips don't conflict with item tooltips. + if (!tooltip.isEmpty()) return tooltip; + } + + return Collections.emptyList(); + } + + @Override + public ItemStack getHoveredItemStack(int mouseX, int mouseY) { + if (!visible) return ItemStack.EMPTY; + + // Check children in reverse order (foreground first) + for (int i = children.size() - 1; i >= 0; i--) { + IWidget child = children.get(i); + + if (!child.isVisible() || !child.isHovered(mouseX, mouseY)) continue; + + ItemStack stack = child.getHoveredItemStack(mouseX, mouseY); + if (!stack.isEmpty()) return stack; + } + + return ItemStack.EMPTY; + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/button/ButtonType.java b/src/main/java/com/cellterminal/gui/widget/button/ButtonType.java new file mode 100644 index 0000000..a9b7598 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/button/ButtonType.java @@ -0,0 +1,62 @@ +package com.cellterminal.gui.widget.button; + +import net.minecraft.client.resources.I18n; + + +/** + * Defines the types of small buttons (8x8) used in the Cell Terminal GUI. + *

+ * Each type maps to a column in the {@code textures/guis/atlas.png} sprite sheet + * (5x2 grid of 8x8 icons). The hover state is the row (0=normal, 1=hovered). + * + * @see SmallButton + */ +public enum ButtonType { + + /** Green button: partition cell contents from current inventory */ + DO_PARTITION(0, "gui.cellterminal.button.do_partition"), + + /** Red button: clear all partition entries */ + CLEAR_PARTITION(1, "gui.cellterminal.button.clear_partition"), + + /** IO mode: read-only */ + READ_ONLY(2, "gui.cellterminal.button.read_only"), + + /** IO mode: write-only */ + WRITE_ONLY(3, "gui.cellterminal.button.write_only"), + + /** IO mode: read-write (bidirectional) */ + READ_WRITE(4, "gui.cellterminal.button.read_write"); + + /** X offset in the texture atlas (column index * 8) */ + private final int textureX; + + /** Localization key for the tooltip */ + private final String tooltipKey; + + ButtonType(int column, String tooltipKey) { + this.textureX = column * 8; + this.tooltipKey = tooltipKey; + } + + /** + * Get the X offset in the sprite sheet for this button type. + */ + public int getTextureX() { + return textureX; + } + + /** + * Get the localized tooltip text for this button type. + */ + public String getTooltip() { + return I18n.format(tooltipKey); + } + + /** + * Get the tooltip localization key. + */ + public String getTooltipKey() { + return tooltipKey; + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/button/SmallButton.java b/src/main/java/com/cellterminal/gui/widget/button/SmallButton.java new file mode 100644 index 0000000..0026da4 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/button/SmallButton.java @@ -0,0 +1,93 @@ +package com.cellterminal.gui.widget.button; + +import java.util.Collections; +import java.util.List; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.util.ResourceLocation; + +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.widget.AbstractWidget; + + +/** + * A small 8x8 button widget rendered from a texture atlas. + *

+ * The texture is {@code textures/guis/atlas.png}, arranged as an 5x2 grid + * of 8x8 icons. The column is determined by the {@link ButtonType} and the + * row is determined by the hover state (0=normal, 1=hovered). + * + * @see ButtonType + * @see SmallSwitchingButton + */ +public class SmallButton extends AbstractWidget { + + private static final int SIZE = GuiConstants.SMALL_BUTTON_SIZE; + private static final ResourceLocation TEXTURE = + new ResourceLocation("cellterminal", "textures/guis/atlas.png"); + + protected ButtonType type; + private final Runnable onClick; + + /** + * @param x X position relative to GUI + * @param y Y position relative to GUI + * @param type The button type (determines texture column and tooltip) + * @param onClick Callback invoked when the button is clicked + */ + public SmallButton(int x, int y, ButtonType type, Runnable onClick) { + super(x, y, SIZE, SIZE); + this.type = type; + this.onClick = onClick; + } + + @Override + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + boolean hovered = isHovered(mouseX, mouseY); + + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + + int texX = GuiConstants.SMALL_BUTTON_X + type.getTextureX(); + int texY = GuiConstants.SMALL_BUTTON_Y + (hovered ? SIZE : 0); + Gui.drawScaledCustomSizeModalRect( + this.x, this.y, texX, texY, SIZE, SIZE, SIZE, SIZE, + GuiConstants.ATLAS_WIDTH, GuiConstants.ATLAS_HEIGHT); + } + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible || button != 0) return false; + if (!isHovered(mouseX, mouseY)) return false; + + onClick.run(); + + return true; + } + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return Collections.emptyList(); + + return Collections.singletonList(type.getTooltip()); + } + + /** + * Get the current button type. + */ + public ButtonType getType() { + return type; + } + + /** + * Set the button type (changes appearance and tooltip). + */ + public void setType(ButtonType type) { + this.type = type; + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/button/SmallSwitchingButton.java b/src/main/java/com/cellterminal/gui/widget/button/SmallSwitchingButton.java new file mode 100644 index 0000000..c3a5450 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/button/SmallSwitchingButton.java @@ -0,0 +1,59 @@ +package com.cellterminal.gui.widget.button; + + +/** + * A small button that cycles through multiple {@link ButtonType}s on each click. + *

+ * The type is advanced to the next value in the provided array before the + * onClick callback is invoked. This allows the callback to read the new type + * and act accordingly (e.g., switch IO mode). + * + * @see SmallButton + * @see ButtonType + */ +public class SmallSwitchingButton extends SmallButton { + + private final ButtonType[] types; + private int currentIndex; + + /** + * @param x X position relative to GUI + * @param y Y position relative to GUI + * @param types The button types to cycle through (at least 2) + * @param initialIndex The starting index in the types array + * @param onClick Callback invoked after the type switches (new type is already set) + */ + public SmallSwitchingButton(int x, int y, ButtonType[] types, int initialIndex, Runnable onClick) { + super(x, y, types[initialIndex], onClick); + this.types = types; + this.currentIndex = initialIndex; + } + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible || button != 0) return false; + if (!isHovered(mouseX, mouseY)) return false; + + // Advance to next type + currentIndex = (currentIndex + 1) % types.length; + this.type = types[currentIndex]; + + // Delegate to parent which invokes the callback + return super.handleClick(mouseX, mouseY, button); + } + + /** + * Get the current type index (0-based position in the types array). + */ + public int getCurrentIndex() { + return currentIndex; + } + + /** + * Set the current type by index. + */ + public void setCurrentIndex(int index) { + this.currentIndex = index % types.length; + this.type = types[currentIndex]; + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/header/AbstractHeader.java b/src/main/java/com/cellterminal/gui/widget/header/AbstractHeader.java new file mode 100644 index 0000000..8749a5f --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/header/AbstractHeader.java @@ -0,0 +1,386 @@ +package com.cellterminal.gui.widget.header; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.item.ItemStack; + +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.rename.InlineRenameManager; +import com.cellterminal.gui.rename.Renameable; +import com.cellterminal.gui.widget.AbstractWidget; +import com.cellterminal.gui.widget.CardsDisplay; +import com.cellterminal.gui.widget.DoubleClickTracker; + + +/** + * Base class for all header widgets in the Cell Terminal GUI. + *

+ * A header represents the top row of a storage group (drive, chest, storage bus, + * or temp area cell slot). It shows: + *

    + *
  • Icon (block/item) at {@link GuiConstants#GUI_INDENT}
  • + *
  • Name (clickable for rename) at {@link GuiConstants#HEADER_NAME_X}
  • + *
  • Header hover highlight when the mouse is over the header area
  • + *
  • Tree connector at the bottom for linking to content rows below
  • + *
+ * + * Subclasses add location text, expand/collapse, priority field positioning, + * IO mode buttons, cell slots, etc. + * + *

Tree connector

+ * The header draws a 1px vertical connector at the bottom of the row when + * content rows follow below. The first content line uses + * {@link #getConnectorY()} as its {@code lineAboveCutY}. + * + *

Priority field

+ * Subclasses that support priority (e.g., {@link StorageHeader}) register their + * field with the {@link com.cellterminal.gui.PriorityFieldManager} singleton + * during draw. The base header does not handle priority fields directly. + * + * @see StorageHeader + * @see StorageBusHeader + * @see TempAreaHeader + */ +public abstract class AbstractHeader extends AbstractWidget { + + /** X position of the tree connector line (same as AbstractLine.TREE_LINE_X) */ + protected static final int TREE_LINE_X = GuiConstants.GUI_INDENT + 7; + + protected final FontRenderer fontRenderer; + protected final RenderItem itemRender; + + /** Supplier for the icon ItemStack (block icon for storages, cell for temp area) */ + protected Supplier iconSupplier; + + /** Supplier for the display name */ + protected Supplier nameSupplier; + + /** Supplier for whether the name is a custom (user-set) name */ + protected Supplier hasCustomNameSupplier; + + /** Whether to draw the tree connector at the bottom (content follows below) */ + protected boolean drawConnector = false; + + /** Maximum pixel width for name text before truncation */ + protected int nameMaxWidth = GuiConstants.HEADER_NAME_MAX_WIDTH; + + /** Renameable target for right-click rename (null if this header is not renameable) */ + protected Renameable renameable; + + /** Rename field X position (text field start) */ + protected int renameFieldX; + + /** Rename field Y offset relative to the row Y position */ + protected int renameFieldYOffset; + + /** Rename field right edge (stops before buttons) */ + protected int renameFieldRightEdge; + + /** Callback when the name area is double-clicked (for highlight in world) */ + protected Runnable onNameDoubleClick; + + /** Target ID for double-click tracking (stored in DoubleClickTracker for persistence across rebuilds) */ + protected long doubleClickTargetId = -1; + + /** Callback when the header row is clicked (for area selection / quick add) */ + protected Runnable onHeaderClick; + + /** Supplier for the selection state (selected headers get a highlight overlay) */ + protected Supplier selectedSupplier; + + /** Cards display widget for upgrade icons (optional, used by StorageBus and TempArea headers) */ + protected CardsDisplay cardsDisplay; + + // Hover state (computed during draw) + protected boolean nameHovered = false; + protected boolean headerHovered = false; + + protected AbstractHeader(int y, FontRenderer fontRenderer, RenderItem itemRender) { + super(0, y, GuiConstants.CONTENT_RIGHT_EDGE, GuiConstants.ROW_HEIGHT); + this.fontRenderer = fontRenderer; + this.itemRender = itemRender; + } + + // ---- Configuration ---- + + public void setIconSupplier(Supplier supplier) { + this.iconSupplier = supplier; + } + + public void setNameSupplier(Supplier supplier) { + this.nameSupplier = supplier; + } + + public void setHasCustomNameSupplier(Supplier supplier) { + this.hasCustomNameSupplier = supplier; + } + + public void setDrawConnector(boolean drawConnector) { + this.drawConnector = drawConnector; + } + + /** + * Set the rename info for this header. When the name area is right-clicked, + * the header triggers InlineRenameManager directly. + * + * @param target The renameable data object + * @param fieldX The X position for the rename text field + * @param yOffset The Y offset relative to the row's Y position (0 for most tabs, 4 for TempArea) + * @param fieldRightEdge The right edge for the rename text field + */ + public void setRenameInfo(Renameable target, int fieldX, int yOffset, int fieldRightEdge) { + this.renameable = target; + this.renameFieldX = fieldX; + this.renameFieldYOffset = yOffset; + this.renameFieldRightEdge = fieldRightEdge; + } + + /** + * Set the callback and target ID for double-clicking the name area (for highlight in world). + *

+ * The target ID is used by {@link DoubleClickTracker} to track clicks across widget + * rebuilds. Since widgets are recreated every frame, storing the last click time on + * the widget instance doesn't work - we need centralized tracking keyed by target ID. + * + * @param callback The action to perform on double-click + * @param targetId Unique identifier for this target (use DoubleClickTracker.storageTargetId() etc.) + */ + public void setOnNameDoubleClick(Runnable callback, long targetId) { + this.onNameDoubleClick = callback; + this.doubleClickTargetId = targetId; + } + + /** + * Set the callback for clicking the header row area (for quick-add selection toggle). + * This fires when the header row is clicked but no specific interactive element was hit. + */ + public void setOnHeaderClick(Runnable callback) { + this.onHeaderClick = callback; + } + + /** + * Set the selection state supplier. When selected, the header row gets a + * selection highlight overlay (for batch keybind operations like quick-add). + */ + public void setSelectedSupplier(Supplier supplier) { + this.selectedSupplier = supplier; + } + + /** + * Set the cards display widget for upgrade icons. + * Used by StorageBus and TempArea headers. + */ + public void setCardsDisplay(CardsDisplay cards) { + this.cardsDisplay = cards; + } + + // ---- Hover detection ---- + + /** + * Extend hover detection to include the CardsDisplay overflow area. + *

+ * When a header has more upgrade cards than fit in a single row height + * (e.g. 5 cards = 3 rows × 9px = 27px, but ROW_HEIGHT is only 18px), + * the overflow cards extend below the header's standard bounds. + * Without this override, {@link com.cellterminal.gui.widget.tab.AbstractTabWidget#handleClick} + * (which iterates in reverse order and checks isHovered first) would skip + * the header for clicks in the overflow area, making those cards unclickable. + */ + @Override + public boolean isHovered(int mouseX, int mouseY) { + if (super.isHovered(mouseX, mouseY)) return true; + + // Check if the mouse is over an overflow card below the header's standard bounds + if (cardsDisplay != null && cardsDisplay.isHovered(mouseX, mouseY)) return true; + + return false; + } + + // ---- Accessors ---- + + /** + * Get the Y position of the tree connector at the bottom of this header. + * The first content line below should use this as its {@code lineAboveCutY}. + */ + public int getConnectorY() { + return y + GuiConstants.HEADER_CONNECTOR_Y_OFFSET; + } + + // ---- Rendering ---- + + @Override + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + nameHovered = false; + headerHovered = false; + + // Draw horizontal separator line at the top of the header + Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y, + GuiConstants.COLOR_SEPARATOR); + + // Draw selection background (below everything else) + boolean isSelected = selectedSupplier != null && selectedSupplier.get(); + if (isSelected) { + Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, + y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_SELECTION); + } + + // Subclass-specific drawing (may set right bound for hover area) + int hoverRightBound = drawHeaderContent(mouseX, mouseY); + + // Header hover highlight + headerHovered = mouseX >= GuiConstants.GUI_INDENT && mouseX < hoverRightBound + && mouseY >= y && mouseY < y + GuiConstants.ROW_HEIGHT; + if (headerHovered) { + Gui.drawRect(GuiConstants.GUI_INDENT, y, hoverRightBound, y + GuiConstants.ROW_HEIGHT, + GuiConstants.COLOR_STORAGE_HEADER_HOVER); + } + + // Draw icon + drawIcon(); + + // Draw name + drawName(mouseX, mouseY); + + // Draw tree connector if content follows + if (drawConnector) { + Gui.drawRect(TREE_LINE_X, y + GuiConstants.HEADER_CONNECTOR_Y_OFFSET, + TREE_LINE_X + 1, y + GuiConstants.ROW_HEIGHT, + GuiConstants.COLOR_TREE_LINE); + } + } + + /** + * Draw subclass-specific header content (location, expand/collapse, IO mode, etc.). + * Called before the base icon/name/connector drawing, so subclass elements + * are drawn first (as background) and the base draws on top. + * + * @return The right X bound of the hoverable header area + */ + protected abstract int drawHeaderContent(int mouseX, int mouseY); + + /** + * Draw the icon at the left side of the header. + * Can be overridden by subclasses (e.g., TempAreaHeader draws a cell slot instead). + */ + protected void drawIcon() { + ItemStack icon = iconSupplier != null ? iconSupplier.get() : ItemStack.EMPTY; + if (!icon.isEmpty()) renderItemStack(icon, GuiConstants.GUI_INDENT, y); + } + + /** + * Draw the name text, handling truncation and hover detection. + * When selected, the name is drawn in a different color (blue) to indicate selection state. + */ + protected void drawName(int mouseX, int mouseY) { + String name = nameSupplier != null ? nameSupplier.get() : ""; + if (name.isEmpty()) return; + + String displayName = trimTextToWidth(fontRenderer, name, nameMaxWidth); + + boolean isSelected = selectedSupplier != null && selectedSupplier.get(); + int nameColor; + if (isSelected) { + nameColor = GuiConstants.COLOR_NAME_SELECTED; + } else if (hasCustomNameSupplier != null && hasCustomNameSupplier.get()) { + nameColor = GuiConstants.COLOR_CUSTOM_NAME; + } else { + nameColor = GuiConstants.COLOR_TEXT_NORMAL; + } + + fontRenderer.drawString(displayName, GuiConstants.HEADER_NAME_X, y + 1, nameColor); + + // Check name hover for rename interaction - use full name area for easier clicking + if (mouseX >= GuiConstants.HEADER_NAME_X && mouseX < GuiConstants.HEADER_NAME_X + nameMaxWidth + && mouseY >= y + 1 && mouseY < y + 10) { + nameHovered = true; + } + } + + // ---- Click handling ---- + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible) return false; + + // Name right-click for rename + if (button == 1 && nameHovered && renameable != null && renameable.isRenameable()) { + InlineRenameManager.getInstance().startEditing( + renameable, y + renameFieldYOffset, renameFieldX, renameFieldRightEdge); + return true; + } + + // Only left-click for remaining actions + if (button != 0) return false; + + // Cards click takes priority over header double-click + if (cardsDisplay != null && cardsDisplay.isHovered(mouseX, mouseY)) { + return cardsDisplay.handleClick(mouseX, mouseY, button); + } + + // Header double-click for highlight in world (full header area, not just name) + // Uses centralized DoubleClickTracker since widgets are recreated every frame + if (headerHovered && onNameDoubleClick != null && doubleClickTargetId != -1) { + if (DoubleClickTracker.isDoubleClick(doubleClickTargetId)) { + onNameDoubleClick.run(); + return true; + } + // Don't return - let it fall through in case something else needs to handle it + } + + // Header row click for area selection / quick add (fallthrough: nothing specific was hit) + if (headerHovered && onHeaderClick != null) { + onHeaderClick.run(); + return true; + } + + return false; + } + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return Collections.emptyList(); + + // Cards tooltip + if (cardsDisplay != null && cardsDisplay.isHovered(mouseX, mouseY)) { + return cardsDisplay.getTooltip(mouseX, mouseY); + } + + return Collections.emptyList(); + } + + @Override + public ItemStack getHoveredItemStack(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return ItemStack.EMPTY; + + // Check if hovering the icon + int iconX = GuiConstants.GUI_INDENT; + if (mouseX >= iconX && mouseX < iconX + GuiConstants.MINI_SLOT_SIZE + && mouseY >= y && mouseY < y + GuiConstants.MINI_SLOT_SIZE) { + ItemStack icon = iconSupplier != null ? iconSupplier.get() : ItemStack.EMPTY; + if (!icon.isEmpty()) return icon; + } + + return ItemStack.EMPTY; + } + + // ---- Utilities ---- + + /** + * Delegate to the shared static utility in AbstractWidget. + * Kept as an instance method for convenience in subclasses. + */ + protected String trimTextToWidth(String text, int maxWidth) { + return AbstractWidget.trimTextToWidth(fontRenderer, text, maxWidth); + } + + protected void renderItemStack(ItemStack stack, int renderX, int renderY) { + AbstractWidget.renderItemStack(itemRender, stack, renderX, renderY); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/header/StorageBusHeader.java b/src/main/java/com/cellterminal/gui/widget/header/StorageBusHeader.java new file mode 100644 index 0000000..75fb576 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/header/StorageBusHeader.java @@ -0,0 +1,171 @@ +package com.cellterminal.gui.widget.header; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderItem; + +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.PriorityFieldManager; +import com.cellterminal.gui.widget.button.ButtonType; +import com.cellterminal.gui.widget.button.SmallButton; + + +/** + * Storage bus header widget for the Cell Terminal (tabs 4-5). + *

+ * Extends the storage header with: + *

    + *
  • IO mode button (texture-based, cycles through Read/Write/ReadWrite)
  • + *
  • Upgrade card icons (rendered at the left edge, inherited from AbstractHeader)
  • + *
  • Selection support for batch operations (quick-add via keybind)
  • + *
+ * + * The IO mode button uses textured icons to indicate the current access mode: + *
    + *
  • {@link ButtonType#READ_ONLY} = Read-only (extract)
  • + *
  • {@link ButtonType#WRITE_ONLY} = Write-only (insert)
  • + *
  • {@link ButtonType#READ_WRITE} = Read+Write (bidirectional split)
  • + *
+ * + * @see StorageHeader + * @see AbstractHeader + */ +public class StorageBusHeader extends StorageHeader { + + /** Supplier for the access restriction mode (0=NONE, 1=READ, 2=WRITE, 3=READ_WRITE) */ + private Supplier accessModeSupplier; + + /** Whether IO mode switching is supported */ + private Supplier supportsIOModeSupplier; + + /** Callback when the IO mode button is clicked */ + private Runnable onIOModeClick; + + /** Textured IO mode button (READ_ONLY, WRITE_ONLY, or READ_WRITE) */ + private final SmallButton ioModeButton; + + // IO mode button hover state + private boolean ioModeHovered = false; + + public StorageBusHeader(int y, FontRenderer fontRenderer, RenderItem itemRender) { + super(y, fontRenderer, itemRender); + // IO mode button: type is updated each frame from accessModeSupplier. + // Default to READ_WRITE since it will be overwritten before drawing. + this.ioModeButton = new SmallButton( + GuiConstants.BUTTON_IO_MODE_X, y, ButtonType.READ_WRITE, + () -> { if (onIOModeClick != null) onIOModeClick.run(); }); + } + + // ---- Configuration ---- + + public void setAccessModeSupplier(Supplier supplier) { + this.accessModeSupplier = supplier; + } + + public void setSupportsIOModeSupplier(Supplier supplier) { + this.supportsIOModeSupplier = supplier; + } + + public void setOnIOModeClick(Runnable callback) { + this.onIOModeClick = callback; + } + + // ---- Rendering ---- + + @Override + protected int drawHeaderContent(int mouseX, int mouseY) { + ioModeHovered = false; + + // Draw location text + drawLocation(); + + // Draw expand/collapse indicator + drawExpandIcon(mouseX, mouseY); + + // Draw IO mode button + drawIOModeButton(mouseX, mouseY); + + // Draw upgrade cards (from AbstractHeader) + if (cardsDisplay != null) cardsDisplay.draw(mouseX, mouseY); + + // Register priority field with the singleton (positions it for this frame) + if (prioritizable != null && prioritizable.supportsPriority()) { + PriorityFieldManager.getInstance().registerField( + prioritizable, y, guiLeft, guiTop, fontRenderer); + } + + // Return the hover right bound (up to IO mode button area) + return GuiConstants.BUTTON_IO_MODE_X; + } + + /** + * Draw the IO mode button using textured SmallButton. + * Updates the button type each frame based on the current access restriction. + */ + private void drawIOModeButton(int mouseX, int mouseY) { + boolean supportsIOMode = supportsIOModeSupplier != null && supportsIOModeSupplier.get(); + if (!supportsIOMode) return; + + int accessMode = accessModeSupplier != null ? accessModeSupplier.get() : 3; + + // Map access restriction to button type + switch (accessMode) { + case 1: + ioModeButton.setType(ButtonType.READ_ONLY); + break; + case 2: + ioModeButton.setType(ButtonType.WRITE_ONLY); + break; + default: + ioModeButton.setType(ButtonType.READ_WRITE); + break; + } + + // Position at current header Y (since header Y can change per frame) + ioModeButton.setPosition(GuiConstants.BUTTON_IO_MODE_X, y); + ioModeButton.draw(mouseX, mouseY); + ioModeHovered = ioModeButton.isHovered(mouseX, mouseY); + } + + // ---- Click handling ---- + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible) return false; + + // Left-click only for IO mode and expand/collapse + if (button == 0) { + // IO mode button click (delegated to SmallButton) + boolean supportsIOMode = supportsIOModeSupplier != null && supportsIOModeSupplier.get(); + if (supportsIOMode && ioModeButton.handleClick(mouseX, mouseY, button)) { + return true; + } + + // Expand/collapse click + if (expandHovered && onExpandToggle != null) { + onExpandToggle.run(); + return true; + } + } + + // Name click (right-click), cards click, and header selection for quick-add (from base) + return super.handleClick(mouseX, mouseY, button); + } + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return Collections.emptyList(); + + // IO mode button tooltip + boolean supportsIOMode = supportsIOModeSupplier != null && supportsIOModeSupplier.get(); + if (supportsIOMode && ioModeButton.isHovered(mouseX, mouseY)) { + return ioModeButton.getTooltip(mouseX, mouseY); + } + + // Cards tooltip (from base) + return super.getTooltip(mouseX, mouseY); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/header/StorageHeader.java b/src/main/java/com/cellterminal/gui/widget/header/StorageHeader.java new file mode 100644 index 0000000..1b512ca --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/header/StorageHeader.java @@ -0,0 +1,157 @@ +package com.cellterminal.gui.widget.header; + +import java.util.function.Supplier; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderItem; + +import com.cellterminal.client.Prioritizable; +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.PriorityFieldManager; + + +/** + * Storage header widget for the Cell Terminal (tabs 0-2). + *

+ * Extends the base header with: + *

    + *
  • Location string (dimension + coordinates, rendered below the name)
  • + *
  • Expand/collapse button ("[+]"/"[-]" at the right side)
  • + *
  • Priority field (inline, managed via {@link PriorityFieldManager} singleton)
  • + *
+ * + * When a {@link Prioritizable} target is set via {@link #setPrioritizable}, the header + * registers its priority field with the singleton during each draw pass. The field itself + * persists in the manager's registry across frame rebuilds (keyed by target ID). + * + * @see AbstractHeader + * @see StorageBusHeader + */ +public class StorageHeader extends AbstractHeader { + + /** Supplier for the location string (e.g., "(x, y, z, dim)") */ + protected Supplier locationSupplier; + + /** Supplier for the expand/collapse state */ + protected Supplier expandedSupplier; + + /** Callback when the expand/collapse button is clicked */ + protected Runnable onExpandToggle; + + /** The prioritizable target for inline priority editing (null if not editable) */ + protected Prioritizable prioritizable; + + /** GUI absolute offsets needed for priority field positioning */ + protected int guiLeft; + protected int guiTop; + + // Hover state + protected boolean expandHovered = false; + + public StorageHeader(int y, FontRenderer fontRenderer, RenderItem itemRender) { + super(y, fontRenderer, itemRender); + } + + // ---- Configuration ---- + + public void setLocationSupplier(Supplier supplier) { + this.locationSupplier = supplier; + } + + public void setExpandedSupplier(Supplier supplier) { + this.expandedSupplier = supplier; + } + + public void setOnExpandToggle(Runnable callback) { + this.onExpandToggle = callback; + } + + /** + * Set the prioritizable target for this header. + * When set, the header registers its priority field with the singleton + * {@link PriorityFieldManager} during each draw pass. + */ + public void setPrioritizable(Prioritizable target) { + this.prioritizable = target; + } + + /** + * Set GUI absolute offsets needed for priority field positioning. + * Must be called before draw if a priority field is set. + */ + public void setGuiOffsets(int guiLeft, int guiTop) { + this.guiLeft = guiLeft; + this.guiTop = guiTop; + } + + // ---- Rendering ---- + + @Override + protected int drawHeaderContent(int mouseX, int mouseY) { + expandHovered = false; + + // Draw location text + drawLocation(); + + // Draw expand/collapse indicator + drawExpandIcon(mouseX, mouseY); + + // Register priority field with the singleton (positions it for this frame) + if (prioritizable != null && prioritizable.supportsPriority()) { + PriorityFieldManager.getInstance().registerField( + prioritizable, y, guiLeft, guiTop, fontRenderer); + } + + // Return the hover right bound (up to expand area, excluding priority field) + return GuiConstants.EXPAND_ICON_X; + } + + /** + * Draw the location string below the name. + * Location strings extend to the right edge (wider than the name which stops at IO mode area). + */ + protected void drawLocation() { + String location = locationSupplier != null ? locationSupplier.get() : ""; + if (location.isEmpty()) return; + + String displayLocation = trimTextToWidth(location, GuiConstants.HEADER_LOCATION_MAX_WIDTH); + fontRenderer.drawString(displayLocation, GuiConstants.HEADER_NAME_X, y + 9, + GuiConstants.COLOR_TEXT_SECONDARY); + } + + /** + * Draw the expand/collapse indicator ("[+]" or "[-]"). + */ + protected void drawExpandIcon(int mouseX, int mouseY) { + boolean expanded = expandedSupplier != null && expandedSupplier.get(); + String expandIcon = expanded ? "[-]" : "[+]"; + fontRenderer.drawString(expandIcon, GuiConstants.EXPAND_ICON_X, y + 1, + GuiConstants.COLOR_TEXT_PLACEHOLDER); + + // Check expand icon hover (wider area for easy clicking) + expandHovered = mouseX >= GuiConstants.EXPAND_ICON_X - 2 + && mouseX < GuiConstants.CONTENT_RIGHT_EDGE + && mouseY >= y && mouseY < y + GuiConstants.ROW_HEIGHT; + } + + // ---- Click handling ---- + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible) return false; + + // Expand/collapse click - left click only, check at click time not cached hover + if (button == 0) { + boolean isExpandArea = mouseX >= GuiConstants.EXPAND_ICON_X - 2 + && mouseX < GuiConstants.CONTENT_RIGHT_EDGE + && mouseY >= y && mouseY < y + GuiConstants.ROW_HEIGHT; + if (isExpandArea && onExpandToggle != null) { + onExpandToggle.run(); + return true; + } + } + + // Name click (right-click), cards click, and header selection (from base) + return super.handleClick(mouseX, mouseY, button); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/header/SubnetHeader.java b/src/main/java/com/cellterminal/gui/widget/header/SubnetHeader.java new file mode 100644 index 0000000..2ee5019 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/header/SubnetHeader.java @@ -0,0 +1,355 @@ +package com.cellterminal.gui.widget.header; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.client.resources.I18n; + +import com.cellterminal.gui.GuiConstants; + + +/** + * Header widget for a subnet entry in the subnet overview tab. + *

+ * Displays: + *

    + *
  • Favorite star (★ at 2x scale on left sidebar)
  • + *
  • Icon (connection icon for subnets, ⌂ for main network)
  • + *
  • Name (custom name in green, main network in cyan, inaccessible in gray)
  • + *
  • Location text (coordinates, below name. Skipped for main network)
  • + *
  • Load button (blue button at right edge)
  • + *
+ * + * Rename is handled via {@link com.cellterminal.gui.rename.InlineRenameManager} + * through the base class's {@link #setRenameInfo} mechanism. + * Double-click highlight uses {@link com.cellterminal.gui.widget.DoubleClickTracker}. + * + * @see AbstractHeader + */ +public class SubnetHeader extends AbstractHeader { + + private static final int STAR_X = 6; + private static final int STAR_WIDTH = 18; + private static final int LOAD_BUTTON_MARGIN = 4; + + // Colors + private static final int COLOR_MAIN_NETWORK = 0xFF00838F; + private static final int COLOR_NAME_INACCESSIBLE = 0xFF909090; + private static final int COLOR_FAVORITE_ON = 0xFFCC9900; + private static final int COLOR_FAVORITE_OFF = 0xFF505050; + private static final int COLOR_OUTBOUND = 0xFF4CAF50; + private static final int COLOR_INBOUND = 0xFF42A5F5; + + // Direction arrow config (null = no arrow, true = outbound →, false = inbound ←) + private Supplier directionSupplier; + private static final int ARROW_X = GuiConstants.GUI_INDENT + 18; + private static final int ARROW_WIDTH = 10; + + private boolean isMain = false; + + // Subnet state suppliers + private Supplier isFavoriteSupplier; + private Supplier canLoadSupplier; + private Supplier locationSupplier; + + // Callbacks + private Runnable onStarClick; + private Runnable onLoadClick; + + // Hover state + private boolean starHovered = false; + private boolean loadButtonHovered = false; + + // Cached Load button layout (computed during draw) + private int loadButtonX; + private int loadButtonWidth; + + public SubnetHeader(int y, FontRenderer fontRenderer, RenderItem itemRender) { + super(y, fontRenderer, itemRender); + } + + public SubnetHeader(int y, FontRenderer fontRenderer, RenderItem itemRender, boolean isMainNetwork) { + super(y, fontRenderer, itemRender); + + isMain = isMainNetwork; + } + + // ---- Configuration ---- + + /** + * Set the direction supplier for the connection arrow. + * When non-null, a colored direction arrow is drawn between the icon and the name. + * True = outbound (→ green), false = inbound (← blue). + */ + public void setDirectionSupplier(Supplier supplier) { + this.directionSupplier = supplier; + } + + public void setIsFavoriteSupplier(Supplier supplier) { + this.isFavoriteSupplier = supplier; + } + + public void setCanLoadSupplier(Supplier supplier) { + this.canLoadSupplier = supplier; + } + + public void setLocationSupplier(Supplier supplier) { + this.locationSupplier = supplier; + } + + public void setOnStarClick(Runnable callback) { + this.onStarClick = callback; + } + + public void setOnLoadClick(Runnable callback) { + this.onLoadClick = callback; + } + + // ---- Rendering ---- + + @Override + protected int drawHeaderContent(int mouseX, int mouseY) { + starHovered = false; + loadButtonHovered = false; + + boolean canLoad = canLoadSupplier != null && canLoadSupplier.get(); + + // Calculate Load button position from right edge + String loadText = I18n.format("cellterminal.subnet.load"); + int loadTextWidth = fontRenderer.getStringWidth(loadText); + loadButtonWidth = loadTextWidth + LOAD_BUTTON_MARGIN; + loadButtonX = GuiConstants.CONTENT_RIGHT_EDGE - LOAD_BUTTON_MARGIN - loadTextWidth; + + // Draw direction arrow between icon and name (when connection-level header) + boolean hasArrow = directionSupplier != null; + int nameStartX = hasArrow ? (ARROW_X + ARROW_WIDTH) : GuiConstants.HEADER_NAME_X; + if (hasArrow) drawDirectionArrow(); + + // Adjust name max width to stop before Load button + this.nameMaxWidth = loadButtonX - nameStartX - 4; + + // Draw favorite star on left sidebar (2x scale for visibility) + drawStar(mouseX, mouseY); + + // Draw location text below name (skip for main network) + if (!isMain) drawLocation(); + + // Draw Load button + boolean isLoadHover = mouseX >= loadButtonX && mouseX < loadButtonX + loadButtonWidth + && mouseY >= y && mouseY < y + 10; + loadButtonHovered = isLoadHover; + drawLoadButton(loadButtonX, y, loadText, isLoadHover, canLoad); + + // Return hover right bound (up to Load button area) + return loadButtonX; + } + + /** + * Draw the favorite star on the left sidebar at 2x scale. + */ + private void drawStar(int mouseX, int mouseY) { + // TODO: replace with a proper icon + boolean isFav = isFavoriteSupplier != null && isFavoriteSupplier.get(); + + // Check star hover + starHovered = mouseX >= STAR_X && mouseX < STAR_X + STAR_WIDTH + && mouseY >= y && mouseY < y + GuiConstants.ROW_HEIGHT; + + int starColor = isFav ? COLOR_FAVORITE_ON : COLOR_FAVORITE_OFF; + if (starHovered) starColor = isFav ? 0xFFDDB000 : 0xFF707070; + + GlStateManager.pushMatrix(); + GlStateManager.scale(2.0F, 2.0F, 1.0F); + fontRenderer.drawString("★", STAR_X / 2, y / 2, starColor); + GlStateManager.popMatrix(); + } + + /** + * Draw the colored direction arrow (→ or ←) between icon and name. + */ + private void drawDirectionArrow() { + // TODO: replace with a proper icon + boolean outbound = directionSupplier.get(); + String arrow = outbound ? "→" : "←"; + int color = outbound ? COLOR_OUTBOUND : COLOR_INBOUND; + + fontRenderer.drawString(arrow, ARROW_X, y + 5, color); + } + + /** + * Draw the location string below the name. + */ + private void drawLocation() { + String location = locationSupplier != null ? locationSupplier.get() : ""; + if (location.isEmpty()) return; + + // Location starts at the same X as the name (adjusted for arrow presence) + int locationX = directionSupplier != null ? (ARROW_X + ARROW_WIDTH) : GuiConstants.HEADER_NAME_X; + int locationMaxWidth = GuiConstants.CONTENT_RIGHT_EDGE - locationX - 4; + String displayLocation = trimTextToWidth(location, locationMaxWidth); + fontRenderer.drawString(displayLocation, locationX, y + 9, GuiConstants.COLOR_TEXT_SECONDARY); + } + + /** + * Draw the Load button at the right edge of the header. + */ + private void drawLoadButton(int x, int btnY, String text, boolean isHovered, boolean isEnabled) { + int buttonHeight = 10; + + // Background + int bgColor; + if (!isEnabled) { + bgColor = 0xFF808080; + } else if (isHovered) { + bgColor = 0xFF4A90D9; + } else { + bgColor = 0xFF3B7DC9; + } + Gui.drawRect(x, btnY, x + loadButtonWidth, btnY + buttonHeight, bgColor); + + // Border (3D effect) + int highlightColor = isEnabled ? 0xFF6BA5E7 : 0xFFA0A0A0; + int shadowColor = isEnabled ? 0xFF2A5B8A : 0xFF606060; + Gui.drawRect(x, btnY, x + loadButtonWidth, btnY + 1, highlightColor); + Gui.drawRect(x, btnY, x + 1, btnY + buttonHeight, highlightColor); + Gui.drawRect(x, btnY + buttonHeight - 1, x + loadButtonWidth, btnY + buttonHeight, shadowColor); + Gui.drawRect(x + loadButtonWidth - 1, btnY, x + loadButtonWidth, btnY + buttonHeight, shadowColor); + + // Text + int textX = x + LOAD_BUTTON_MARGIN / 2; + int textY = btnY + 1; + int textColor = isEnabled ? 0xFFFFFFFF : 0xFFC0C0C0; + fontRenderer.drawString(text, textX, textY, textColor); + } + + /** + * Override icon drawing: main network gets a ⌂ symbol, subnets get normal item icon. + */ + @Override + protected void drawIcon() { + if (isMain) { + // TODO: replace with a proper icon + GlStateManager.pushMatrix(); + GlStateManager.scale(2.0F, 2.0F, 1.0F); + fontRenderer.drawString("⌂", (GuiConstants.GUI_INDENT + 5) / 2, (y - 1) / 2, COLOR_MAIN_NETWORK); + GlStateManager.popMatrix(); + } else { + super.drawIcon(); + } + } + + /** + * Override name drawing for subnet-specific coloring: + * - Main network: cyan + * - Inaccessible: gray + * - Custom name: green + * - Default: normal text color + *

+ * Also vertically center the name for main network (no location line). + */ + @Override + protected void drawName(int mouseX, int mouseY) { + String name = nameSupplier != null ? nameSupplier.get() : ""; + if (name.isEmpty()) return; + + boolean canLoad = canLoadSupplier != null && canLoadSupplier.get(); + + String displayName = trimTextToWidth(name, nameMaxWidth); + + int nameColor; + if (isMain) { + nameColor = COLOR_MAIN_NETWORK; + } else if (!canLoad) { + nameColor = COLOR_NAME_INACCESSIBLE; + } else if (hasCustomNameSupplier != null && hasCustomNameSupplier.get()) { + nameColor = GuiConstants.COLOR_CUSTOM_NAME; + } else { + nameColor = GuiConstants.COLOR_TEXT_NORMAL; + } + + // Main network has no location line, so center the name vertically + int nameX = directionSupplier != null ? (ARROW_X + ARROW_WIDTH) : GuiConstants.HEADER_NAME_X; + int nameY = isMain ? (y + 5) : (y + 1); + fontRenderer.drawString(displayName, nameX, nameY, nameColor); + + // Check name hover for rename interaction + int nameWidth = fontRenderer.getStringWidth(displayName); + if (mouseX >= nameX && mouseX < nameX + nameWidth + && mouseY >= nameY && mouseY < nameY + 9) { + nameHovered = true; + } + } + + // ---- Click handling ---- + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible) return false; + + // Star click (left-click only) + if (button == 0 && starHovered && onStarClick != null) { + onStarClick.run(); + return true; + } + + // Load button click (left-click only) + if (button == 0 && loadButtonHovered && onLoadClick != null) { + boolean canLoad = canLoadSupplier != null && canLoadSupplier.get(); + if (canLoad) { + onLoadClick.run(); + return true; + } + } + + // Delegate to base for rename (right-click on name) and double-click (header area) + return super.handleClick(mouseX, mouseY, button); + } + + // ---- Tooltips ---- + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return Collections.emptyList(); + + // Main network tooltip + if (isMain) { + List lines = new ArrayList<>(); + lines.add(I18n.format("cellterminal.subnet.main_network")); + lines.add("§e" + I18n.format("cellterminal.subnet.click_load_main")); + return lines; + } + + // Star tooltip + if (starHovered) { + return Collections.singletonList(I18n.format("cellterminal.subnet.controls.star")); + } + + // Load button tooltip + if (loadButtonHovered) return getLoadButtonTooltip(); + + // Default tooltip (if any) + return super.getTooltip(mouseX, mouseY); + } + + private List getLoadButtonTooltip() { + boolean canLoad = canLoadSupplier != null && canLoadSupplier.get(); + List lines = new ArrayList<>(); + + if (!canLoad) { + // Distinguish between no power and no access + // We don't have direct access to hasPower/isAccessible here, + // so use the generic disabled tooltip + lines.add("§c" + I18n.format("cellterminal.subnet.load.disabled")); + } else { + lines.add(I18n.format("cellterminal.subnet.load.tooltip")); + } + + return lines; + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/header/TempAreaHeader.java b/src/main/java/com/cellterminal/gui/widget/header/TempAreaHeader.java new file mode 100644 index 0000000..70680fd --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/header/TempAreaHeader.java @@ -0,0 +1,327 @@ +package com.cellterminal.gui.widget.header; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.client.resources.I18n; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; + +import com.cellterminal.gui.GuiConstants; + + +/** + * Temp area header widget for the Cell Terminal (tab 3). + *

+ * Unlike other headers, the temp area header has: + *

    + *
  • An interactive cell slot (insert/extract cells by clicking/shift-clicking)
  • + *
  • A "Send" button (transfers cell to selected network)
  • + *
  • No expand/collapse button (content visibility depends on cell presence)
  • + *
  • No priority field or location string
  • + *
+ * + * The icon position at {@link GuiConstants#GUI_INDENT} becomes a cell slot with + * background and hover highlight, supporting cell insertion and extraction. + *

+ * The name is vertically centered (y+5) instead of top-aligned (y+1) since + * there is no location text below. + * + * @see AbstractHeader + * @see StorageHeader + */ +public class TempAreaHeader extends AbstractHeader { + + // Send button layout + public static final int SEND_BUTTON_X = 150; + private static final int SEND_BUTTON_Y_OFFSET = 2; + private static final int SEND_BUTTON_WIDTH = 28; + private static final int SEND_BUTTON_HEIGHT = 12; + + // Send button colors + private static final int COLOR_SEND_BUTTON = 0xFF5599CC; + private static final int COLOR_SEND_BUTTON_HOVER = 0xFF77BBEE; + + // Name width limit (before send button area, leaves 4px gap) + // HEADER_NAME_X = GUI_INDENT + 20 = 42, so max name ends at 42 + 104 = 146 + private static final int MAX_NAME_WIDTH = SEND_BUTTON_X - GuiConstants.HEADER_NAME_X - 4; + + private static final int SIZE = GuiConstants.MINI_SLOT_SIZE; + private static final ResourceLocation TEXTURE = + new ResourceLocation("cellterminal", "textures/guis/atlas.png"); + + /** Supplier for whether a cell is inserted in this slot */ + private Supplier hasCellSupplier; + + /** Callback when the cell slot is clicked (insert/extract) */ + private CellSlotClickCallback cellSlotCallback; + + /** Callback when the send button is clicked */ + private Runnable onSendClick; + + // Hover state + private boolean cellSlotHovered = false; + private boolean sendButtonHovered = false; + + /** + * Callback for cell slot interactions. + */ + @FunctionalInterface + public interface CellSlotClickCallback { + void onCellSlotClicked(int mouseButton); + } + + public TempAreaHeader(int y, FontRenderer fontRenderer, RenderItem itemRender) { + super(y, fontRenderer, itemRender); + this.nameMaxWidth = MAX_NAME_WIDTH; + } + + // ---- Configuration ---- + + public void setHasCellSupplier(Supplier supplier) { + this.hasCellSupplier = supplier; + } + + public void setCellSlotCallback(CellSlotClickCallback callback) { + this.cellSlotCallback = callback; + } + + public void setOnSendClick(Runnable callback) { + this.onSendClick = callback; + } + + // ---- Rendering ---- + + @Override + protected int drawHeaderContent(int mouseX, int mouseY) { + cellSlotHovered = false; + sendButtonHovered = false; + + // Draw upgrade cards + if (cardsDisplay != null) cardsDisplay.draw(mouseX, mouseY); + + // Draw send button if a cell is present + boolean hasCell = hasCellSupplier != null && hasCellSupplier.get(); + if (hasCell) drawSendButton(mouseX, mouseY); + + // Return hover right bound (the entire row before send button) + return hasCell ? SEND_BUTTON_X : GuiConstants.CONTENT_RIGHT_EDGE; + } + + /** + * Override icon drawing to render an interactive cell slot instead. + * The cell slot has a background, hover highlight, and supports click interactions. + */ + @Override + protected void drawIcon() { + int slotX = GuiConstants.GUI_INDENT; + + // Draw slot background + drawSlotBackground(slotX, y); + + // Draw cell item if present + ItemStack icon = iconSupplier != null ? iconSupplier.get() : ItemStack.EMPTY; + if (!icon.isEmpty()) renderItemStack(icon, slotX, y); + } + + @Override + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + nameHovered = false; + headerHovered = false; + + // Draw horizontal separator line at the top of the header (between cells) + Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y, + GuiConstants.COLOR_SEPARATOR); + + // Draw selection background (below everything else) + boolean isSelected = selectedSupplier != null && selectedSupplier.get(); + if (isSelected) { + Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, + y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_SELECTION); + } + + // Subclass-specific drawing + int hoverRightBound = drawHeaderContent(mouseX, mouseY); + + // Header hover highlight + headerHovered = mouseX >= GuiConstants.GUI_INDENT && mouseX < hoverRightBound + && mouseY >= y && mouseY < y + GuiConstants.ROW_HEIGHT; + if (headerHovered) { + Gui.drawRect(GuiConstants.GUI_INDENT, y, hoverRightBound, y + GuiConstants.ROW_HEIGHT, + GuiConstants.COLOR_STORAGE_HEADER_HOVER); + } + + // Draw cell slot (instead of plain icon) + drawIcon(); + + // Check cell slot hover + checkCellSlotHover(mouseX, mouseY); + + // Draw name (vertically centered since no location text) + drawTempName(mouseX, mouseY); + + // Draw tree connector if content follows + if (drawConnector) { + Gui.drawRect(TREE_LINE_X, y + GuiConstants.HEADER_CONNECTOR_Y_OFFSET, + TREE_LINE_X + 1, y + GuiConstants.ROW_HEIGHT, + GuiConstants.COLOR_TREE_LINE); + } + } + + /** + * Check if the cell slot is hovered and draw highlight. + */ + private void checkCellSlotHover(int mouseX, int mouseY) { + int slotX = GuiConstants.GUI_INDENT; + if (mouseX >= slotX && mouseX < slotX + SIZE && mouseY >= y && mouseY < y + SIZE) { + cellSlotHovered = true; + drawSlotHoverHighlight(slotX, y); + } + } + + /** + * Draw the temp area name with vertical centering (y+5 instead of y+1). + */ + private void drawTempName(int mouseX, int mouseY) { + boolean hasCell = hasCellSupplier != null && hasCellSupplier.get(); + String name; + + if (hasCell) { + name = nameSupplier != null ? nameSupplier.get() : ""; + } else { + name = I18n.format("gui.cellterminal.temp_area.drop_cell"); + } + + if (name.isEmpty()) return; + + String displayName = trimTextToWidth(name, nameMaxWidth); + + boolean isSelected = selectedSupplier != null && selectedSupplier.get(); + boolean hasCustomName = hasCustomNameSupplier != null && hasCustomNameSupplier.get(); + int nameColor; + if (isSelected) { + nameColor = GuiConstants.COLOR_NAME_SELECTED; + } else if (!hasCell) { + nameColor = GuiConstants.COLOR_TEXT_PLACEHOLDER; + } else if (hasCustomName) { + nameColor = GuiConstants.COLOR_CUSTOM_NAME; + } else { + nameColor = GuiConstants.COLOR_TEXT_NORMAL; + } + + fontRenderer.drawString(displayName, GuiConstants.HEADER_NAME_X, y + 5, nameColor); + + // Name hover only makes sense when a cell is present (for rename interaction) + if (!hasCell) return; + + // Use full name area width for easier click targeting (not just actual text width) + if (mouseX >= GuiConstants.HEADER_NAME_X && mouseX < GuiConstants.HEADER_NAME_X + nameMaxWidth + && mouseY >= y + 5 && mouseY < y + 14) { + nameHovered = true; + } + } + + /** + * Draw the "Send" button with 3D border effect. + */ + private void drawSendButton(int mouseX, int mouseY) { + int btnX = SEND_BUTTON_X; + int btnY = y + SEND_BUTTON_Y_OFFSET; + + sendButtonHovered = mouseX >= btnX && mouseX < btnX + SEND_BUTTON_WIDTH + && mouseY >= btnY && mouseY < btnY + SEND_BUTTON_HEIGHT; + + int btnColor = sendButtonHovered ? COLOR_SEND_BUTTON_HOVER : COLOR_SEND_BUTTON; + Gui.drawRect(btnX, btnY, btnX + SEND_BUTTON_WIDTH, btnY + SEND_BUTTON_HEIGHT, btnColor); + + // 3D border effect + Gui.drawRect(btnX, btnY, btnX + SEND_BUTTON_WIDTH, btnY + 1, + GuiConstants.COLOR_BUTTON_HIGHLIGHT); + Gui.drawRect(btnX, btnY, btnX + 1, btnY + SEND_BUTTON_HEIGHT, + GuiConstants.COLOR_BUTTON_HIGHLIGHT); + Gui.drawRect(btnX, btnY + SEND_BUTTON_HEIGHT - 1, btnX + SEND_BUTTON_WIDTH, btnY + SEND_BUTTON_HEIGHT, + GuiConstants.COLOR_BUTTON_SHADOW); + Gui.drawRect(btnX + SEND_BUTTON_WIDTH - 1, btnY, btnX + SEND_BUTTON_WIDTH, btnY + SEND_BUTTON_HEIGHT, + GuiConstants.COLOR_BUTTON_SHADOW); + + // Button text (centered) + String sendText = I18n.format("gui.cellterminal.temp_area.send"); + int textX = btnX + (SEND_BUTTON_WIDTH - fontRenderer.getStringWidth(sendText)) / 2; + fontRenderer.drawString(sendText, textX, btnY + 2, 0x000000); + } + + // ---- Click handling ---- + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible) return false; + + // Send button click (left-click only) + if (button == 0 && sendButtonHovered && onSendClick != null) { + onSendClick.run(); + return true; + } + + // Cell slot click (supports left and right click) + if (cellSlotHovered && cellSlotCallback != null) { + cellSlotCallback.onCellSlotClicked(button); + return true; + } + + // Name click for rename, cards click, and header selection for quick-add (from base) + return super.handleClick(mouseX, mouseY, button); + } + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return Collections.emptyList(); + + // Send button tooltip + if (sendButtonHovered) { + return Collections.singletonList(I18n.format("gui.cellterminal.temp_area.send.tooltip")); + } + + // Cards tooltip (from base) + return super.getTooltip(mouseX, mouseY); + } + + @Override + public ItemStack getHoveredItemStack(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return ItemStack.EMPTY; + + // Cell slot hover provides the cell item tooltip + if (cellSlotHovered) { + ItemStack icon = iconSupplier != null ? iconSupplier.get() : ItemStack.EMPTY; + if (!icon.isEmpty()) return icon; + } + + return ItemStack.EMPTY; + } + + // ---- Slot rendering helpers ---- + + private void drawSlotBackground(int slotX, int slotY) { + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + + // Mini slot: left half of slot (y uv 0-15) + int texX = GuiConstants.MINI_SLOT_X; + int texY = GuiConstants.MINI_SLOT_Y; + Gui.drawScaledCustomSizeModalRect( + slotX, slotY, texX, texY, SIZE, SIZE, SIZE, SIZE, + GuiConstants.ATLAS_WIDTH, GuiConstants.ATLAS_HEIGHT); + } + + private void drawSlotHoverHighlight(int slotX, int slotY) { + Gui.drawRect(slotX, slotY, slotX + SIZE, slotY + SIZE, GuiConstants.COLOR_HOVER_HIGHLIGHT); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/line/AbstractLine.java b/src/main/java/com/cellterminal/gui/widget/line/AbstractLine.java new file mode 100644 index 0000000..0ee9d77 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/line/AbstractLine.java @@ -0,0 +1,176 @@ +package com.cellterminal.gui.widget.line; + +import java.util.Collections; +import java.util.List; + +import net.minecraft.client.gui.Gui; + +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.widget.AbstractWidget; +import com.cellterminal.gui.widget.button.SmallButton; + + +/** + * Base class for all line widgets in the Cell Terminal GUI. + *

+ * A line is a single row in the scrollable content area. It handles: + * - Tree line rendering (vertical + horizontal connectors) + * - Optional small button at the tree line junction + * - Base layout and hover detection + *

+ * Subclasses add slot grids, cell slots, cards, and tab-specific elements. + * + *

Tree line model

+ * Each row draws its own tree connector going upward to the previous row: + *
+ *   |   <- vertical line UP to the row above, cut at its exposed cut point
+ *   +-- <- horizontal branch at this row
+ *       <- nothing below; the next row handles the vertical line up to us
+ * 
+ * The parent tab widget manages the cut point propagation between rows: + *
    + *
  1. Set {@code lineAboveCutY} on each row (from the previous row's {@link #getTreeLineCutY()})
  2. + *
  3. Each row draws its vertical + branch based on that single value
  4. + *
  5. Edge cases (first row after header, scrolled content) are handled by the tab
  6. + *
+ */ +public abstract class AbstractLine extends AbstractWidget { + + /** X position of the vertical tree line */ + protected static final int TREE_LINE_X = GuiConstants.GUI_INDENT + 7; + + /** Width of the horizontal tree branch */ + protected static final int TREE_BRANCH_WIDTH = 10; + + // Tree line state (set by parent tab before drawing) + protected boolean drawTreeLine = true; + + /** + * Y coordinate where the vertical line going UP should start. + * Set by the parent tab from the previous row's {@link #getTreeLineCutY()}, + * or from the visible top / header connector Y for edge cases. + */ + protected int lineAboveCutY = GuiConstants.CONTENT_START_Y; + + /** Whether this is the first row for a cell (controls cell slot, cards, etc.) */ + protected boolean isFirstRow = true; + + /** Optional button at the tree line junction (e.g. DoPartition, ClearPartition) */ + protected SmallButton treeButton; + + /** X offset for tree button (default: -5 from TREE_LINE_X). Storage buses use -3 for tighter layout. */ + protected int treeButtonXOffset = -5; + + protected AbstractLine(int x, int y, int width) { + super(x, y, width, GuiConstants.ROW_HEIGHT); + } + + /** + * Configure the tree line parameters for this line. + * Called by the parent tab widget before each draw cycle, + * since connectivity depends on neighboring lines. + * + * @param drawTreeLine Whether to draw tree lines at all + * @param lineAboveCutY Y limit from the row above (or header/visible top) + */ + public void setTreeLineParams(boolean drawTreeLine, int lineAboveCutY) { + this.drawTreeLine = drawTreeLine; + this.lineAboveCutY = lineAboveCutY; + } + + /** + * Get the Y coordinate below which the next row can draw its vertical line + * up to us. This accounts for any button or icon at the tree junction. + * + * @return Y coordinate of the bottom edge of the junction area + */ + public int getTreeLineCutY() { + if (treeButton != null) { + // Button extends from y+5 to y+5+SIZE (buttonY-1 to buttonY+SIZE+1) + return y + 5 + GuiConstants.SMALL_BUTTON_SIZE; + } + + // Default: bottom of the horizontal branch + return y + 9; + } + + /** + * Set the optional button at the tree line junction. + */ + public void setTreeButton(SmallButton button) { + this.treeButton = button; + } + + /** + * Set the X offset for the tree button relative to TREE_LINE_X. + * Default is -5. Storage bus tabs use -3 for tighter layout. + */ + public void setTreeButtonXOffset(int offset) { + this.treeButtonXOffset = offset; + } + + /** + * Get the tree junction button, if any. + */ + public SmallButton getTreeButton() { + return treeButton; + } + + /** + * Draw the tree line connector for this row. + * Draws a vertical line UP from {@code lineAboveCutY} to the junction, + * a horizontal branch, and an optional button covering the junction. + */ + protected void drawTreeLines(int mouseX, int mouseY) { + if (!drawTreeLine) return; + + int branchY = y + 8; + + // Where the vertical line ends depends on whether there's a button + // covering the junction. If there is, stop at the top of the button + // background so the button is drawn on top cleanly. + int verticalEndY = (treeButton != null) ? y + 5 : branchY; + + // Vertical line from the row above's cut point down to our junction + if (lineAboveCutY < verticalEndY) { + Gui.drawRect(TREE_LINE_X, lineAboveCutY, TREE_LINE_X + 1, verticalEndY, GuiConstants.COLOR_TREE_LINE); + } + + // Horizontal branch + Gui.drawRect(TREE_LINE_X, branchY, TREE_LINE_X + TREE_BRANCH_WIDTH, branchY + 1, GuiConstants.COLOR_TREE_LINE); + + // Draw tree junction button if present (covers part of tree line) + if (treeButton != null) { + int buttonX = TREE_LINE_X + treeButtonXOffset; + int buttonY = y + 5; + treeButton.setPosition(buttonX, buttonY); + + treeButton.draw(mouseX, mouseY); + } + } + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible) return false; + + // Check tree button first + if (treeButton != null && treeButton.isVisible() + && treeButton.isHovered(mouseX, mouseY)) { + return treeButton.handleClick(mouseX, mouseY, button); + } + + return false; + } + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return Collections.emptyList(); + + // Check tree button tooltip + if (treeButton != null && treeButton.isHovered(mouseX, mouseY)) { + return treeButton.getTooltip(mouseX, mouseY); + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/line/CellSlotsLine.java b/src/main/java/com/cellterminal/gui/widget/line/CellSlotsLine.java new file mode 100644 index 0000000..513aa22 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/line/CellSlotsLine.java @@ -0,0 +1,207 @@ +package com.cellterminal.gui.widget.line; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.item.ItemStack; + +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.widget.CardsDisplay; + + +/** + * A slot line with an additional cell slot on the left. + *

+ * This is used in Tab 2 (Inventory) and Tab 3 (Partition) where each cell + * occupies one or more rows. The first row shows: + * - Cell slot (the cell item itself, clickable for insert/extract) + * - Upgrade card icons (at the left of the cell slot, only if cell is filled) + * - Content/partition slots (to the right of cell slot) + * - Tree junction button (DoPartition or ClearPartition) + *

+ * If the cell slot is empty (no cell inserted), the content/partition slots + * are not rendered - only the empty cell slot is shown. + *

+ * Continuation rows for multi-row cells are handled by {@link ContinuationLine}. + * + * @see SlotsLine + * @see ContinuationLine + */ +public class CellSlotsLine extends SlotsLine { + + /** Supplier for the cell item stack in the cell slot */ + private Supplier cellItemSupplier; + + /** Whether the cell slot is filled (controls slot visibility) */ + private Supplier cellFilledSupplier; + + /** Cards display widget for upgrade icons */ + private CardsDisplay cardsDisplay; + + /** Callback when a cell is inserted/extracted from the cell slot */ + private CellSlotClickCallback cellSlotCallback; + + // Cell slot hover tracking + private boolean cellSlotHovered = false; + + /** + * Callback for cell slot interactions. + */ + @FunctionalInterface + public interface CellSlotClickCallback { + /** + * Called when the cell slot is clicked (insert/extract cell). + * @param mouseButton Mouse button (0=left, 1=right) + */ + void onCellSlotClicked(int mouseButton); + } + + /** + * @param y Y position relative to GUI + * @param slotsPerRow Number of slots per row (8 for cells) + * @param slotsXOffset X offset where content/partition slots start + * @param mode Content or Partition + * @param startIndex Starting data index + * @param fontRenderer Font renderer + * @param itemRender Item renderer + */ + public CellSlotsLine(int y, int slotsPerRow, int slotsXOffset, SlotMode mode, + int startIndex, FontRenderer fontRenderer, RenderItem itemRender) { + super(y, slotsPerRow, slotsXOffset, mode, startIndex, fontRenderer, itemRender); + } + + public void setCellItemSupplier(Supplier supplier) { + this.cellItemSupplier = supplier; + } + + public void setCellFilledSupplier(Supplier supplier) { + this.cellFilledSupplier = supplier; + } + + public void setCardsDisplay(CardsDisplay cards) { + this.cardsDisplay = cards; + } + + public void setCellSlotCallback(CellSlotClickCallback callback) { + this.cellSlotCallback = callback; + } + + @Override + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + // Draw selection background first (below everything else) + boolean isSelected = selectedSupplier != null && selectedSupplier.get(); + if (isSelected) { + net.minecraft.client.gui.Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, + y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_SELECTION); + } + + // Draw tree lines (background) + drawTreeLines(mouseX, mouseY); + + cellSlotHovered = false; + + // Always draw the cell slot + drawCellSlot(mouseX, mouseY); + + // Draw upgrade cards next to the cell slot + // Position cards to the left of the cell slot (same y, x based on card layout) + if (cardsDisplay != null) cardsDisplay.draw(mouseX, mouseY); + + // Only draw content/partition slots if cell is filled + boolean cellFilled = cellFilledSupplier != null && cellFilledSupplier.get(); + if (!cellFilled) return; + + // Reset slot hover state and draw slots + hoveredSlotIndex = -1; + hoveredStack = ItemStack.EMPTY; + partitionTargets.clear(); + + if (mode == SlotMode.CONTENT) { + drawContentSlots(mouseX, mouseY); + } else { + drawPartitionSlots(mouseX, mouseY); + } + } + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + // Tree button first + if (treeButton != null && treeButton.isVisible() + && treeButton.isHovered(mouseX, mouseY)) { + return treeButton.handleClick(mouseX, mouseY, button); + } + + // Cards click + if (cardsDisplay != null && cardsDisplay.isHovered(mouseX, mouseY)) { + return cardsDisplay.handleClick(mouseX, mouseY, button); + } + + // Cell slot click + if (cellSlotHovered && cellSlotCallback != null) { + cellSlotCallback.onCellSlotClicked(button); + return true; + } + + // Content/partition slot click + if (hoveredSlotIndex >= 0 && slotClickCallback != null) { + slotClickCallback.onSlotClicked(hoveredSlotIndex, button); + return true; + } + + return false; + } + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return Collections.emptyList(); + + // Tree button tooltip + if (treeButton != null && treeButton.isHovered(mouseX, mouseY)) { + return treeButton.getTooltip(mouseX, mouseY); + } + + // Cards tooltip + if (cardsDisplay != null && cardsDisplay.isHovered(mouseX, mouseY)) { + return cardsDisplay.getTooltip(mouseX, mouseY); + } + + // Item tooltips handled separately via getHoveredItemStack() + return Collections.emptyList(); + } + + @Override + public ItemStack getHoveredItemStack(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return ItemStack.EMPTY; + + // Cell slot hover takes priority (it's visually to the left) + if (cellSlotHovered) { + ItemStack cellItem = cellItemSupplier != null ? cellItemSupplier.get() : ItemStack.EMPTY; + if (!cellItem.isEmpty()) return cellItem; + } + + // Content/partition slot hover + return hoveredStack; + } + + // ---- Private helpers ---- + + private void drawCellSlot(int mouseX, int mouseY) { + int cellX = GuiConstants.CELL_INDENT; + drawSlotBackground(cellX, y); + + ItemStack cellItem = cellItemSupplier != null ? cellItemSupplier.get() : ItemStack.EMPTY; + if (!cellItem.isEmpty()) renderItemStack(cellItem, cellX, y); + + // Check hover + if (mouseX >= cellX && mouseX < cellX + GuiConstants.MINI_SLOT_SIZE + && mouseY >= y && mouseY < y + GuiConstants.MINI_SLOT_SIZE) { + drawSlotHoverHighlight(cellX, y); + cellSlotHovered = true; + } + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/line/ContinuationLine.java b/src/main/java/com/cellterminal/gui/widget/line/ContinuationLine.java new file mode 100644 index 0000000..58990e2 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/line/ContinuationLine.java @@ -0,0 +1,113 @@ +package com.cellterminal.gui.widget.line; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.item.ItemStack; + +import com.cellterminal.gui.GuiConstants; + + +/** + * A continuation row for multi-row cells. + *

+ * When a cell has more items than fit in a single row, additional rows + * are rendered using ContinuationLine. These rows have: + * - Tree line connectors (vertical line only, no horizontal branch) + * - Content/partition slots only (no cell slot, no cards, no button) + *

+ * Unlike first rows which draw a horizontal branch from the tree line to the + * cell slot or junction button, continuation rows only draw the vertical + * connector since they have no junction element to point at. + *

+ * The spec states: "nothing, as it's a continuation row. No Cell, no cards, no button." + * + * @see CellSlotsLine + * @see SlotsLine + */ +public class ContinuationLine extends SlotsLine { + + /** + * Whether to draw the horizontal branch from the tree line to the row content. + * Default is true, matching standard tree rendering. Tabs where continuation rows + * have no junction element (e.g. CellContentTab, TempArea) set this to false + * so the horizontal branch doesn't point at empty space. + */ + private boolean drawHorizontalBranch = true; + + /** + * @param y Y position relative to GUI + * @param slotsPerRow Number of slots per row + * @param slotsXOffset X offset where slots start + * @param mode Content or Partition + * @param startIndex Starting data index for this row + * @param fontRenderer Font renderer + * @param itemRender Item renderer + */ + public ContinuationLine(int y, int slotsPerRow, int slotsXOffset, SlotMode mode, + int startIndex, FontRenderer fontRenderer, RenderItem itemRender) { + super(y, slotsPerRow, slotsXOffset, mode, startIndex, fontRenderer, itemRender); + + // Continuation rows are never the "first row" for a cell + this.isFirstRow = false; + } + + /** + * Set whether to draw the horizontal branch at the tree junction. + * Tabs with no junction element on continuation rows (CellContentTab, TempArea) + * should call this with false. Tabs like StorageBus that have elements at the + * junction should leave the default (true). + */ + public void setDrawHorizontalBranch(boolean drawHorizontalBranch) { + this.drawHorizontalBranch = drawHorizontalBranch; + } + + /** + * Override tree line drawing to optionally skip the horizontal branch. + * When drawHorizontalBranch is false, only the vertical connector is drawn + * since there is no junction element for the branch to point at. + */ + @Override + protected void drawTreeLines(int mouseX, int mouseY) { + if (!drawTreeLine) return; + + if (!drawHorizontalBranch) { + // Only draw the vertical connector through this row (no horizontal branch, no button) + int verticalEndY = y + 9; // Same as default getTreeLineCutY() for button-less lines + if (lineAboveCutY < verticalEndY) { + Gui.drawRect(TREE_LINE_X, lineAboveCutY, TREE_LINE_X + 1, verticalEndY, GuiConstants.COLOR_TREE_LINE); + } + + return; + } + + // Default: draw both vertical and horizontal connectors + super.drawTreeLines(mouseX, mouseY); + } + + @Override + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + // Draw selection background first (below everything else) + boolean isSelected = selectedSupplier != null && selectedSupplier.get(); + if (isSelected) { + Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, + y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_SELECTION); + } + + // Draw tree lines (vertical up + horizontal branch, like all rows) + drawTreeLines(mouseX, mouseY); + + // Reset hover state and draw slots + hoveredSlotIndex = -1; + hoveredStack = ItemStack.EMPTY; + partitionTargets.clear(); + + if (mode == SlotMode.CONTENT) { + drawContentSlots(mouseX, mouseY); + } else { + drawPartitionSlots(mouseX, mouseY); + } + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/line/SlotsLine.java b/src/main/java/com/cellterminal/gui/widget/line/SlotsLine.java new file mode 100644 index 0000000..02f5899 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/line/SlotsLine.java @@ -0,0 +1,425 @@ +package com.cellterminal.gui.widget.line; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; + +import appeng.util.ReadableNumberConverter; + +import com.cellterminal.gui.ComparisonUtils; +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.widget.AbstractWidget; + + +/** + * A line widget that renders a grid of content or partition slots. + *

+ * Supports two modes: + * - **Content mode**: Shows item contents with count labels and partition indicators ("P"). + * Click toggles the item into/out of partition. + * - **Partition mode**: Shows partition entries with amber tint. + * Click sets/clears individual partition slots. Supports JEI ghost ingredient drop. + *

+ * Configuration: + * - {@code slotsPerRow}: Number of slots per row (8 for cells, 9 for storage buses) + * - {@code slotsXOffset}: X offset from GUI left for the first slot + * - {@code startIndex}: Index into the data list for the first slot in this row + * + * @see CellSlotsLine + * @see ContinuationLine + */ +public class SlotsLine extends AbstractLine { + + /** + * Determines the behavior and visual style of the slot grid. + */ + public enum SlotMode { + /** Show item contents with counts and partition indicators */ + CONTENT, + /** Show partition entries with amber tint, supports drag-and-drop */ + PARTITION + } + + /** + * Callback for slot interactions (click on content or partition slot). + */ + @FunctionalInterface + public interface SlotClickCallback { + /** + * @param slotIndex The absolute index into the data list + * @param mouseButton Mouse button (0=left, 1=right) + */ + void onSlotClicked(int slotIndex, int mouseButton); + } + + /** + * Tracks a visible partition slot for JEI ghost ingredient integration. + */ + public static class PartitionSlotTarget { + public final int absoluteIndex; + public final int absX; + public final int absY; + public final int width; + public final int height; + + public PartitionSlotTarget(int absoluteIndex, int absX, int absY, int width, int height) { + this.absoluteIndex = absoluteIndex; + this.absX = absX; + this.absY = absY; + this.width = width; + this.height = height; + } + } + + private static final int SIZE = GuiConstants.MINI_SLOT_SIZE; + private static final ResourceLocation TEXTURE = + new ResourceLocation("cellterminal", "textures/guis/atlas.png"); + + protected final int slotsPerRow; + protected final int slotsXOffset; + protected final SlotMode mode; + protected final FontRenderer fontRenderer; + protected final RenderItem itemRender; + + /** Supplier for the items to display (content or partition list) */ + protected Supplier> itemsSupplier; + + /** Supplier for the partition list (used in content mode for the "P" indicator) */ + protected Supplier> partitionSupplier; + + /** Supplier for item counts (used in content mode, index-aligned with items) */ + protected Supplier countProvider; + + /** Starting index into the data list for this row */ + protected int startIndex; + + /** Maximum number of slots allowed (e.g., MAX_CELL_PARTITION_SLOTS) */ + protected int maxSlots = Integer.MAX_VALUE; + + /** Absolute GUI position offsets for JEI target registration */ + protected int guiLeft; + protected int guiTop; + + // Hover tracking (computed during draw, consumed by tooltip/click) + protected int hoveredSlotIndex = -1; + protected ItemStack hoveredStack = ItemStack.EMPTY; + protected int hoveredAbsX; + protected int hoveredAbsY; + + // JEI targets accumulated during draw + protected final List partitionTargets = new ArrayList<>(); + + protected SlotClickCallback slotClickCallback; + + /** Supplier for the selection state (selected lines get a highlight overlay) */ + protected Supplier selectedSupplier; + + /** Whether to draw a horizontal separator line at the top of this row */ + protected boolean drawTopSeparator = false; + + /** + * @param y Y position relative to GUI + * @param slotsPerRow Number of slots per row (8 for cells, 9 for buses) + * @param slotsXOffset X offset from GUI left where slots start + * @param mode Content or Partition mode + * @param startIndex Index into data list for first slot + * @param fontRenderer Font renderer + * @param itemRender Item renderer + */ + public SlotsLine(int y, int slotsPerRow, int slotsXOffset, SlotMode mode, + int startIndex, FontRenderer fontRenderer, RenderItem itemRender) { + super(0, y, GuiConstants.CONTENT_RIGHT_EDGE); + this.slotsPerRow = slotsPerRow; + this.slotsXOffset = slotsXOffset; + this.mode = mode; + this.startIndex = startIndex; + this.fontRenderer = fontRenderer; + this.itemRender = itemRender; + } + + public void setItemsSupplier(Supplier> supplier) { + this.itemsSupplier = supplier; + } + + public void setPartitionSupplier(Supplier> supplier) { + this.partitionSupplier = supplier; + } + + public void setCountProvider(Supplier provider) { + this.countProvider = provider; + } + + public void setSlotClickCallback(SlotClickCallback callback) { + this.slotClickCallback = callback; + } + + /** + * Set the selection state supplier. When selected, the line gets a + * selection highlight overlay (for batch keybind operations like quick-add). + */ + public void setSelectedSupplier(Supplier supplier) { + this.selectedSupplier = supplier; + } + + public void setMaxSlots(int maxSlots) { + this.maxSlots = maxSlots; + } + + /** + * Set whether to draw a horizontal separator line at the top of this row. + * Used for the first partition row in temp area to visually separate from content rows. + */ + public void setDrawTopSeparator(boolean draw) { + this.drawTopSeparator = draw; + } + + public void setGuiOffsets(int guiLeft, int guiTop) { + this.guiLeft = guiLeft; + this.guiTop = guiTop; + } + + /** + * Get the JEI partition slot targets accumulated during the last draw. + * Only populated in PARTITION mode. + */ + public List getPartitionTargets() { + return Collections.unmodifiableList(partitionTargets); + } + + @Override + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + // Draw horizontal separator at top if requested (before selection background) + if (drawTopSeparator) { + Gui.drawRect(GuiConstants.GUI_INDENT, y - 1, GuiConstants.CONTENT_RIGHT_EDGE, y, + GuiConstants.COLOR_SEPARATOR); + } + + // Draw selection background first (below everything else) + boolean isSelected = selectedSupplier != null && selectedSupplier.get(); + if (isSelected) { + Gui.drawRect(GuiConstants.GUI_INDENT, y, GuiConstants.CONTENT_RIGHT_EDGE, + y + GuiConstants.ROW_HEIGHT, GuiConstants.COLOR_SELECTION); + } + + // Draw tree lines first (background layer) + drawTreeLines(mouseX, mouseY); + + // Reset hover state + hoveredSlotIndex = -1; + hoveredStack = ItemStack.EMPTY; + partitionTargets.clear(); + + // Draw slot grid + if (mode == SlotMode.CONTENT) { + drawContentSlots(mouseX, mouseY); + } else { + drawPartitionSlots(mouseX, mouseY); + } + } + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + // Let tree button handle click first + if (super.handleClick(mouseX, mouseY, button)) return true; + + if (!visible || hoveredSlotIndex < 0) return false; + if (slotClickCallback == null) return false; + + slotClickCallback.onSlotClicked(hoveredSlotIndex, button); + + return true; + } + + @Override + public List getTooltip(int mouseX, int mouseY) { + // Check tree button tooltip first + List buttonTooltip = super.getTooltip(mouseX, mouseY); + if (!buttonTooltip.isEmpty()) return buttonTooltip; + + // Slot tooltip is handled by the parent tab/GUI since it requires + // rendering an item tooltip (which needs the full GUI context) + return Collections.emptyList(); + } + + @Override + public ItemStack getHoveredItemStack(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return ItemStack.EMPTY; + + return hoveredStack; + } + + // ---- Content slot rendering ---- + + protected void drawContentSlots(int mouseX, int mouseY) { + List items = itemsSupplier != null ? itemsSupplier.get() : Collections.emptyList(); + List partition = partitionSupplier != null ? partitionSupplier.get() : Collections.emptyList(); + ContentCountProvider counts = countProvider != null ? countProvider.get() : null; + + for (int x = slotsXOffset; x < slotsXOffset + (slotsPerRow * SIZE); x += SIZE) { + drawSlotBackground(x, y); + } + + int slots = Integer.min(startIndex + slotsPerRow, items.size()) - startIndex; + for (int i = 0; i < slots; i++) { + int absIndex = startIndex + i; + int slotX = slotsXOffset + (i * SIZE); + + ItemStack stack = items.get(absIndex); + if (stack.isEmpty()) continue; + + renderItemStack(stack, slotX, y); + + // Draw partition indicator "P" if item is in partition + if (ComparisonUtils.isInPartition(stack, partition)) { + drawPartitionIndicator(slotX, y); + } + + // Draw item count + if (counts != null) { + long count = counts.getCount(absIndex); + drawItemCount(count, slotX, y); + } + + // Check hover + if (mouseX >= slotX && mouseX < slotX + SIZE && mouseY >= y && mouseY < y + SIZE) { + drawSlotHoverHighlight(slotX, y); + hoveredSlotIndex = absIndex; + hoveredStack = stack; + hoveredAbsX = guiLeft + mouseX; + hoveredAbsY = guiTop + mouseY; + } + } + } + + // ---- Partition slot rendering ---- + + protected void drawPartitionSlots(int mouseX, int mouseY) { + List partition = itemsSupplier != null ? itemsSupplier.get() : Collections.emptyList(); + + // Only draw slot backgrounds for slots that are within the valid range + for (int i = 0; i < slotsPerRow; i++) { + int absIndex = startIndex + i; + if (absIndex >= maxSlots) break; + + int slotBgX = slotsXOffset + (i * SIZE); + drawPartitionSlotBackground(slotBgX, y); + } + + for (int i = 0; i < slotsPerRow; i++) { + int absIndex = startIndex + i; + if (absIndex >= maxSlots) break; + + int slotX = slotsXOffset + (i * SIZE); + + // Register JEI ghost target + partitionTargets.add(new PartitionSlotTarget( + absIndex, guiLeft + slotX, guiTop + y, SIZE, SIZE)); + + // Draw partition item if present + ItemStack partItem = absIndex < partition.size() ? partition.get(absIndex) : ItemStack.EMPTY; + if (!partItem.isEmpty()) { + renderItemStack(partItem, slotX, y); + } + + // Check hover + if (mouseX >= slotX && mouseX < slotX + SIZE && mouseY >= y && mouseY < y + SIZE) { + drawSlotHoverHighlight(slotX, y); + hoveredSlotIndex = absIndex; + + if (!partItem.isEmpty()) { + hoveredStack = partItem; + hoveredAbsX = guiLeft + mouseX; + hoveredAbsY = guiTop + mouseY; + } + } + } + } + + // ---- Drawing helpers ---- + + protected void drawSlotBackground(int slotX, int slotY) { + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + + int texX = GuiConstants.MINI_SLOT_X; + int texY = GuiConstants.MINI_SLOT_Y; + Gui.drawScaledCustomSizeModalRect( + slotX, slotY, texX, texY, SIZE, SIZE, SIZE, SIZE, + GuiConstants.ATLAS_WIDTH, GuiConstants.ATLAS_HEIGHT); + } + + protected void drawPartitionSlotBackground(int slotX, int slotY) { + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + + // Partition variant uses the right half of the texture (x uv 16-31) + int texX = GuiConstants.MINI_SLOT_X + SIZE; + int texY = GuiConstants.MINI_SLOT_Y; + Gui.drawScaledCustomSizeModalRect( + slotX, slotY, texX, texY, SIZE, SIZE, SIZE, SIZE, + GuiConstants.ATLAS_WIDTH, GuiConstants.ATLAS_HEIGHT); + } + + protected void drawSlotHoverHighlight(int slotX, int slotY) { + Gui.drawRect(slotX + 1, slotY + 1, slotX + SIZE - 1, slotY + SIZE - 1, GuiConstants.COLOR_HOVER_HIGHLIGHT); + } + + protected void renderItemStack(ItemStack stack, int renderX, int renderY) { + AbstractWidget.renderItemStack(itemRender, stack, renderX, renderY); + } + + protected void drawItemCount(long count, int slotX, int slotY) { + String countStr = formatItemCount(count); + if (countStr.isEmpty()) return; + + int countWidth = fontRenderer.getStringWidth(countStr); + int textX = slotX + SIZE - 1; + int textY = slotY + SIZE - 5; + + GlStateManager.disableDepth(); + GlStateManager.pushMatrix(); + GlStateManager.scale(0.5f, 0.5f, 0.5f); + fontRenderer.drawStringWithShadow(countStr, textX * 2 - countWidth, textY * 2, 0xFFFFFF); + GlStateManager.popMatrix(); + GlStateManager.enableDepth(); + } + + protected void drawPartitionIndicator(int slotX, int slotY) { + GlStateManager.disableLighting(); + GlStateManager.disableDepth(); + GlStateManager.pushMatrix(); + GlStateManager.scale(0.5f, 0.5f, 0.5f); + fontRenderer.drawStringWithShadow("P", (slotX + 1) * 2, (slotY + 1) * 2, GuiConstants.COLOR_PARTITION_INDICATOR); + GlStateManager.popMatrix(); + GlStateManager.enableDepth(); + } + + private String formatItemCount(long count) { + if (count < 1000) return String.valueOf(count); + + return ReadableNumberConverter.INSTANCE.toWideReadableForm(count); + } + + /** + * Provider interface for getting item counts by index. + * Separates count access from the data model to support + * different backends (CellInfo, StorageBusInfo, etc.). + */ + @FunctionalInterface + public interface ContentCountProvider { + long getCount(int index); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/line/TerminalLine.java b/src/main/java/com/cellterminal/gui/widget/line/TerminalLine.java new file mode 100644 index 0000000..5d8cc9c --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/line/TerminalLine.java @@ -0,0 +1,363 @@ +package com.cellterminal.gui.widget.line; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; + +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.rename.InlineRenameManager; +import com.cellterminal.gui.rename.Renameable; +import com.cellterminal.gui.widget.AbstractWidget; +import com.cellterminal.gui.widget.CardsDisplay; +import com.cellterminal.gui.widget.DoubleClickTracker; + + +/** + * Terminal tab (Tab 1) line widget. + *

+ * Each line represents a single cell in the terminal overview. Shows: + * - Tree line connector to parent storage + * - Upgrade card icons (left of cell) + * - Cell item icon + * - Cell name (clickable to rename) + * - Byte usage bar + * - Action buttons: Eject, Inventory, Partition (textured from atlas.png) + *

+ * Unlike other line types, terminal lines are single-row-per-cell and don't + * have content/partition slot grids. + */ +public class TerminalLine extends AbstractLine { + + /** + * Button hover type constants for click handling. + */ + public static final int HOVER_NONE = 0; + public static final int HOVER_INVENTORY = 1; + public static final int HOVER_PARTITION = 2; + public static final int HOVER_EJECT = 3; + + private static final int SIZE = GuiConstants.TAB1_BUTTON_SIZE; + private static final ResourceLocation TEXTURE = + new ResourceLocation("cellterminal", "textures/guis/atlas.png"); + + // Texture column indices for each button type + private static final int TAB1_COL_EJECT = 0; + private static final int TAB1_COL_INVENTORY = 1; + private static final int TAB1_COL_PARTITION = 2; + + // Max pixel width for cell name (from name start to Eject button, with gap) + private static final int CELL_NAME_MAX_PIXEL_WIDTH = + GuiConstants.BUTTON_EJECT_X - (GuiConstants.CELL_INDENT + GuiConstants.CELL_NAME_X_OFFSET) - 4; + + /** + * Callback for terminal line button actions. + */ + public interface TerminalLineCallback { + void onEjectClicked(); + void onInventoryClicked(); + void onPartitionClicked(); + /** Called on double-click for highlight in world */ + void onNameDoubleClicked(); + } + + private final FontRenderer fontRenderer; + private final RenderItem itemRender; + + /** Supplier for the cell item (the cell itself) */ + private Supplier cellItemSupplier; + + /** Supplier for the cell display name */ + private Supplier cellNameSupplier; + + /** Supplier for whether the cell has a custom name */ + private Supplier hasCustomNameSupplier; + + /** Supplier for byte usage percentage (0.0 - 1.0) */ + private Supplier byteUsageSupplier; + + /** Cards display widget */ + private CardsDisplay cardsDisplay; + + /** Callback for button actions */ + private TerminalLineCallback callback; + + /** Renameable target for right-click rename */ + private Renameable renameable; + + /** Rename field X position */ + private int renameFieldX; + + /** Rename field right edge */ + private int renameFieldRightEdge; + + // Hover tracking (computed during draw) + private int hoveredButton = HOVER_NONE; + private boolean nameHovered = false; + + /** Target ID for double-click tracking (stored in DoubleClickTracker for persistence across rebuilds) */ + private long doubleClickTargetId = -1; + + /** + * @param y Y position relative to GUI + * @param fontRenderer Font renderer + * @param itemRender Item renderer + */ + public TerminalLine(int y, FontRenderer fontRenderer, RenderItem itemRender) { + super(0, y, GuiConstants.CONTENT_RIGHT_EDGE); + this.fontRenderer = fontRenderer; + this.itemRender = itemRender; + } + + public void setCellItemSupplier(Supplier supplier) { + this.cellItemSupplier = supplier; + } + + public void setCellNameSupplier(Supplier supplier) { + this.cellNameSupplier = supplier; + } + + public void setHasCustomNameSupplier(Supplier supplier) { + this.hasCustomNameSupplier = supplier; + } + + public void setByteUsageSupplier(Supplier supplier) { + this.byteUsageSupplier = supplier; + } + + public void setCardsDisplay(CardsDisplay cards) { + this.cardsDisplay = cards; + } + + public void setCallback(TerminalLineCallback callback) { + this.callback = callback; + } + + /** + * Set the rename info for this line. When the name area is right-clicked, + * the line triggers InlineRenameManager directly. + */ + public void setRenameInfo(Renameable target, int fieldX, int fieldRightEdge) { + this.renameable = target; + this.renameFieldX = fieldX; + this.renameFieldRightEdge = fieldRightEdge; + } + + /** + * Set the target ID for double-click tracking. + *

+ * Since widgets are recreated every frame, we use {@link DoubleClickTracker} + * for centralized tracking keyed by target ID. + * + * @param targetId Unique identifier for this target (use DoubleClickTracker.cellTargetId()) + */ + public void setDoubleClickTargetId(long targetId) { + this.doubleClickTargetId = targetId; + } + + /** + * Get the currently hovered button type. + */ + public int getHoveredButton() { + return hoveredButton; + } + + @Override + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + hoveredButton = HOVER_NONE; + nameHovered = false; + + // Draw tree lines + drawTreeLines(mouseX, mouseY); + + // Draw upgrade cards to the left of the cell icon + if (cardsDisplay != null) cardsDisplay.draw(mouseX, mouseY); + + // Draw cell icon + ItemStack cellItem = cellItemSupplier != null ? cellItemSupplier.get() : ItemStack.EMPTY; + if (!cellItem.isEmpty()) AbstractWidget.renderItemStack(itemRender, cellItem, GuiConstants.CELL_INDENT, y); + + // Draw cell name + drawCellName(mouseX, mouseY); + + // Draw usage bar + drawUsageBar(); + + // Draw action buttons + drawActionButtons(mouseX, mouseY); + } + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible) return false; + + // Cards click (left-click only) + if (button == 0 && cardsDisplay != null && cardsDisplay.isHovered(mouseX, mouseY)) { + return cardsDisplay.handleClick(mouseX, mouseY, button); + } + + if (callback == null) return false; + + // Left-click only for action buttons + if (button == 0) { + switch (hoveredButton) { + case HOVER_EJECT: + callback.onEjectClicked(); + return true; + case HOVER_INVENTORY: + callback.onInventoryClicked(); + return true; + case HOVER_PARTITION: + callback.onPartitionClicked(); + return true; + default: + break; + } + } + + // Name rename - RIGHT-click only, handled directly via InlineRenameManager + if (button == 1 && nameHovered && renameable != null && renameable.isRenameable()) { + InlineRenameManager.getInstance().startEditing( + renameable, y, renameFieldX, renameFieldRightEdge); + return true; + } + + // Double-click for highlight in world (left-click) - full line area excluding buttons + // Uses centralized DoubleClickTracker since widgets are recreated every frame + if (button == 0 && callback != null && doubleClickTargetId != -1) { + // Check if in the main line area (not on buttons) + boolean inLineArea = mouseX >= GuiConstants.GUI_INDENT && mouseX < GuiConstants.BUTTON_EJECT_X + && mouseY >= y && mouseY < y + GuiConstants.ROW_HEIGHT; + if (inLineArea && DoubleClickTracker.isDoubleClick(doubleClickTargetId)) { + callback.onNameDoubleClicked(); + return true; + } + } + + return false; + } + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return Collections.emptyList(); + + // Cards tooltip + if (cardsDisplay != null && cardsDisplay.isHovered(mouseX, mouseY)) { + return cardsDisplay.getTooltip(mouseX, mouseY); + } + + return Collections.emptyList(); + } + + @Override + public ItemStack getHoveredItemStack(int mouseX, int mouseY) { + if (!visible || !isHovered(mouseX, mouseY)) return ItemStack.EMPTY; + + // Check if hovering the cell icon + int cellX = GuiConstants.CELL_INDENT; + if (mouseX >= cellX && mouseX < cellX + GuiConstants.MINI_SLOT_SIZE + && mouseY >= y && mouseY < y + GuiConstants.MINI_SLOT_SIZE) { + ItemStack cellItem = cellItemSupplier != null ? cellItemSupplier.get() : ItemStack.EMPTY; + if (!cellItem.isEmpty()) return cellItem; + } + + return ItemStack.EMPTY; + } + + // ---- Drawing helpers ---- + + private void drawCellName(int mouseX, int mouseY) { + String name = cellNameSupplier != null ? cellNameSupplier.get() : ""; + if (name.isEmpty()) return; + + // Truncate to fit before the Eject button + name = AbstractWidget.trimTextToWidth(fontRenderer, name, CELL_NAME_MAX_PIXEL_WIDTH); + + boolean hasCustomName = hasCustomNameSupplier != null && hasCustomNameSupplier.get(); + int nameColor = hasCustomName ? GuiConstants.COLOR_CUSTOM_NAME : GuiConstants.COLOR_TEXT_NORMAL; + + int nameX = GuiConstants.CELL_INDENT + GuiConstants.CELL_NAME_X_OFFSET; + int nameY = y + 1; + + fontRenderer.drawString(name, nameX, nameY, nameColor); + + // Check name hover for rename interaction + int nameWidth = fontRenderer.getStringWidth(name); + if (mouseX >= nameX && mouseX < nameX + nameWidth + && mouseY >= nameY && mouseY < nameY + 9) { + nameHovered = true; + } + } + + private void drawUsageBar() { + float usage = byteUsageSupplier != null ? byteUsageSupplier.get() : 0f; + + int barX = GuiConstants.CELL_INDENT + GuiConstants.CELL_NAME_X_OFFSET; + int barY = y + 10; + + // Background + Gui.drawRect(barX, barY, barX + GuiConstants.USAGE_BAR_WIDTH, barY + GuiConstants.USAGE_BAR_HEIGHT, + GuiConstants.COLOR_USAGE_BAR_BACKGROUND); + + // Fill + int filledWidth = (int) (GuiConstants.USAGE_BAR_WIDTH * usage); + if (filledWidth > 0) { + int fillColor = getUsageColor(usage); + Gui.drawRect(barX, barY, barX + filledWidth, barY + GuiConstants.USAGE_BAR_HEIGHT, fillColor); + } + } + + private void drawActionButtons(int mouseX, int mouseY) { + boolean ejectHovered = isButtonHovered(mouseX, mouseY, GuiConstants.BUTTON_EJECT_X); + boolean invHovered = isButtonHovered(mouseX, mouseY, GuiConstants.BUTTON_INVENTORY_X); + boolean partHovered = isButtonHovered(mouseX, mouseY, GuiConstants.BUTTON_PARTITION_X); + + drawTexturedButton(GuiConstants.BUTTON_EJECT_X, y + 1, TAB1_COL_EJECT, ejectHovered); + drawTexturedButton(GuiConstants.BUTTON_INVENTORY_X, y + 1, TAB1_COL_INVENTORY, invHovered); + drawTexturedButton(GuiConstants.BUTTON_PARTITION_X, y + 1, TAB1_COL_PARTITION, partHovered); + + if (ejectHovered) { + hoveredButton = HOVER_EJECT; + } else if (invHovered) { + hoveredButton = HOVER_INVENTORY; + } else if (partHovered) { + hoveredButton = HOVER_PARTITION; + } + } + + private boolean isButtonHovered(int mouseX, int mouseY, int buttonX) { + return mouseX >= buttonX && mouseX < buttonX + SIZE + && mouseY >= y + 1 && mouseY < y + 1 + SIZE; + } + + /** + * Draw a textured button from atlas.png. + */ + private void drawTexturedButton(int drawX, int drawY, int column, boolean hovered) { + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F); + GlStateManager.enableBlend(); + + int texX = GuiConstants.TAB1_BUTTON_X + column * SIZE; + int texY = GuiConstants.TAB1_BUTTON_Y + (hovered ? SIZE : 0); + Gui.drawScaledCustomSizeModalRect( + drawX, drawY, texX, texY, SIZE, SIZE, SIZE, SIZE, + GuiConstants.ATLAS_WIDTH, GuiConstants.ATLAS_HEIGHT); + } + + private int getUsageColor(float percent) { + if (percent > 0.9f) return GuiConstants.COLOR_USAGE_HIGH; + if (percent > 0.75f) return GuiConstants.COLOR_USAGE_MEDIUM; + + return GuiConstants.COLOR_USAGE_LOW; + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/tab/AbstractTabWidget.java b/src/main/java/com/cellterminal/gui/widget/tab/AbstractTabWidget.java new file mode 100644 index 0000000..5d9102c --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/tab/AbstractTabWidget.java @@ -0,0 +1,587 @@ +package com.cellterminal.gui.widget.tab; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.item.ItemStack; + +import mezz.jei.api.gui.IGhostIngredientHandler; + +import com.cellterminal.client.CellInfo; +import com.cellterminal.client.CellContentRow; +import com.cellterminal.client.SearchFilterMode; +import com.cellterminal.client.StorageInfo; +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.handler.TerminalDataManager; +import com.cellterminal.gui.widget.AbstractWidget; +import com.cellterminal.gui.widget.CardsDisplay; +import com.cellterminal.gui.widget.IWidget; +import com.cellterminal.gui.widget.line.AbstractLine; +import com.cellterminal.gui.widget.line.SlotsLine; +import com.cellterminal.gui.widget.header.AbstractHeader; +import com.cellterminal.network.PacketUpgradeCell; + + +/** + * Base class for all tab container widgets in the Cell Terminal GUI. + *

+ * A tab widget manages the visible window of rows in the scrollable content area. + * Each frame, it builds a list of {@link IWidget} instances (headers and lines) + * for the currently visible scroll region and draws them in order. + * + *

Responsibilities

+ *
    + *
  • Translate data objects to header/line widgets for each visible row
  • + *
  • Propagate tree line cut-Y values between consecutive rows
  • + *
  • Set header connector state based on content below
  • + *
  • Delegate draw/click/tooltip/key events to visible row widgets
  • + *
+ * + *

Tree line propagation

+ * After all visible row widgets are created, {@link #propagateTreeLines(List, int)} + * iterates them and chains the cut-Y values: + *
    + *
  1. Headers expose {@link AbstractHeader#getConnectorY()} as the starting cut-Y
  2. + *
  3. Lines receive the previous row's cut-Y via + * {@link AbstractLine#setTreeLineParams(boolean, int)}
  4. + *
  5. Lines expose {@link AbstractLine#getTreeLineCutY()} for the next row
  6. + *
  7. The first visible row uses CONTENT_START_Y as lineAboveCutY when content + * exists above the scroll window
  8. + *
+ * + *

Scroll integration

+ * The tab does not own the scroll position, it is managed by the parent GUI. + * The tab receives the scroll offset and visible row count, and builds widgets + * accordingly, positioned at CONTENT_START_Y + i * ROW_HEIGHT. + * + * @see com.cellterminal.gui.widget.header.AbstractHeader + * @see com.cellterminal.gui.widget.line.AbstractLine + */ +public abstract class AbstractTabWidget extends AbstractWidget { + + protected final FontRenderer fontRenderer; + protected final RenderItem itemRender; + + /** Absolute GUI position offsets (needed for JEI targets and priority fields) */ + protected int guiLeft; + protected int guiTop; + + /** Number of rows visible in the scroll window */ + protected int rowsVisible = GuiConstants.DEFAULT_ROWS; + + /** + * Visible row widgets for the current frame. + * Rebuilt each frame by {@link #buildVisibleRows(List, int)}. + */ + protected final List visibleRows = new ArrayList<>(); + + /** + * Y coordinate from which to draw a bottom continuation tree line, + * or -1 if there is no content below the visible area. + * Set by {@link #propagateTreeLines(List, int)} and drawn by {@link #draw(int, int)}. + */ + protected int bottomContinuationFromY = -1; + + /** + * Maps each visible row widget to its source data object (the lineData from buildVisibleRows). + * Used by the parent GUI to identify what data is under the mouse for upgrade insertion, + * inline rename, priority field positioning, etc. + */ + protected final Map widgetDataMap = new HashMap<>(); + + protected AbstractTabWidget(FontRenderer fontRenderer, RenderItem itemRender) { + super(0, GuiConstants.CONTENT_START_Y, + GuiConstants.CONTENT_RIGHT_EDGE, GuiConstants.DEFAULT_ROWS * GuiConstants.ROW_HEIGHT); + this.fontRenderer = fontRenderer; + this.itemRender = itemRender; + } + + // ---- Configuration ---- + + public void setGuiOffsets(int guiLeft, int guiTop) { + this.guiLeft = guiLeft; + this.guiTop = guiTop; + } + + public void setRowsVisible(int rowsVisible) { + this.rowsVisible = rowsVisible; + this.height = rowsVisible * GuiConstants.ROW_HEIGHT; + } + + /** + * Get the number of items that fit in the visible scroll area. + * Defaults to {@link #rowsVisible} (one item per 18px row). + * Tabs with non-standard row heights (e.g., NetworkTools at 36px per tool) + * override this to return the correct number. + */ + public int getVisibleItemCount() { + return rowsVisible; + } + + // ---- Row building ---- + + /** + * Build the list of visible row widgets for the current scroll window. + * Called once per frame before draw. Subclasses implement + * {@link #createRowWidget(Object, int, List, int)} to map line data → widget. + * + * @param lines All line data objects for this tab (from DataManager) + * @param scrollOffset The current scroll position (index of first visible line) + */ + public void buildVisibleRows(List lines, int scrollOffset) { + visibleRows.clear(); + widgetDataMap.clear(); + + int end = Math.min(scrollOffset + rowsVisible, lines.size()); + for (int i = scrollOffset; i < end; i++) { + int rowY = GuiConstants.CONTENT_START_Y + (i - scrollOffset) * GuiConstants.ROW_HEIGHT; + Object lineData = lines.get(i); + IWidget widget = createRowWidget(lineData, rowY, lines, i); + if (widget != null) { + visibleRows.add(widget); + widgetDataMap.put(widget, lineData); + } + } + + propagateTreeLines(lines, scrollOffset); + } + + /** + * Create a widget for a single line data object. + * Subclasses map their data types (StorageInfo, CellInfo, CellContentRow, etc.) + * to the appropriate widget class, configuring suppliers and callbacks. + * + * @param lineData The data object for this row + * @param y The Y position for this row (CONTENT_START_Y + offset * ROW_HEIGHT) + * @param allLines All lines in the tab (for look-ahead when setting header state) + * @param lineIndex Index of lineData in allLines + * @return A configured widget, or null to skip this row + */ + protected abstract IWidget createRowWidget(Object lineData, int y, List allLines, int lineIndex); + + /** + * Propagate tree line cut-Y values between consecutive visible rows. + * Handles edge cases: + *
    + *
  • First visible row with content above: lineAboveCutY = CONTENT_START_Y
  • + *
  • Header → first content line: lineAboveCutY = header's connectorY
  • + *
  • Content → content: lineAboveCutY = previous line's getTreeLineCutY()
  • + *
+ */ + protected void propagateTreeLines(List allLines, int scrollOffset) { + // Track the "cut Y" from the previous row. + // If scrolled and previous (off-screen) row is in the same group, + // the vertical line should start from the top of the visible area. + int lastCutY = GuiConstants.CONTENT_START_Y; + boolean hasContentAbove = scrollOffset > 0 && isContentLine(allLines, scrollOffset - 1); + + for (int i = 0; i < visibleRows.size(); i++) { + IWidget widget = visibleRows.get(i); + + if (widget instanceof AbstractHeader) { + AbstractHeader header = (AbstractHeader) widget; + // Header's connector state: is the next visible line a content row? + boolean contentBelow = hasContentBelow(allLines, scrollOffset + i); + header.setDrawConnector(contentBelow); + lastCutY = header.getConnectorY(); + + } else if (widget instanceof AbstractLine) { + AbstractLine line = (AbstractLine) widget; + + // For the first visible row that is a content line with content above, + // draw tree line from top of visible area + if (i == 0 && hasContentAbove) { + line.setTreeLineParams(true, GuiConstants.CONTENT_START_Y); + } else { + line.setTreeLineParams(true, lastCutY); + } + + lastCutY = line.getTreeLineCutY(); + } + } + + // Draw a bottom continuation line if there is more content below the visible window + int lastVisibleIndex = scrollOffset + visibleRows.size() - 1; + if (lastVisibleIndex + 1 < allLines.size() && isContentLine(allLines, lastVisibleIndex + 1)) { + bottomContinuationFromY = lastCutY; + } else { + bottomContinuationFromY = -1; + } + } + + /** + * Check whether a line at the given index is a content line (not a header). + * Subclasses override to know their data types. + */ + protected abstract boolean isContentLine(List allLines, int index); + + /** + * Check whether the line after the given index is a content line + * (used to set header drawConnector). + */ + protected boolean hasContentBelow(List allLines, int currentIndex) { + int nextIndex = currentIndex + 1; + if (nextIndex >= allLines.size()) return false; + + return isContentLine(allLines, nextIndex); + } + + // ---- Drawing ---- + + @Override + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + // All widgets in visibleRows are guaranteed non-null and visible + for (IWidget widget : visibleRows) { + widget.draw(mouseX, mouseY); + } + + // Draw bottom tree continuation line when content exists below the visible window + if (bottomContinuationFromY >= 0) { + int treeLineX = GuiConstants.GUI_INDENT + 7; + int bottomY = GuiConstants.CONTENT_START_Y + rowsVisible * GuiConstants.ROW_HEIGHT; + Gui.drawRect(treeLineX, bottomContinuationFromY, treeLineX + 1, bottomY, + GuiConstants.COLOR_TREE_LINE); + } + } + + // ---- Event handling ---- + + @Override + public boolean handleClick(int mouseX, int mouseY, int button) { + if (!visible) return false; + + // Process clicks in reverse order (last drawn = on top = gets first click) + for (int i = visibleRows.size() - 1; i >= 0; i--) { + IWidget widget = visibleRows.get(i); + if (!widget.isHovered(mouseX, mouseY)) continue; + if (widget.handleClick(mouseX, mouseY, button)) return true; + } + + return false; + } + + @Override + public boolean handleKey(char typedChar, int keyCode) { + if (!visible) return false; + + // First let visible row widgets handle the key (e.g., inline editors) + for (int i = visibleRows.size() - 1; i >= 0; i--) { + IWidget widget = visibleRows.get(i); + if (widget.handleKey(typedChar, keyCode)) return true; + } + + // Then try tab-specific keybinds (quick-partition, add-to-bus, etc.) + return handleTabKeyTyped(keyCode); + } + + @Override + public List getTooltip(int mouseX, int mouseY) { + if (!visible) return Collections.emptyList(); + + for (int i = visibleRows.size() - 1; i >= 0; i--) { + IWidget widget = visibleRows.get(i); + if (!widget.isHovered(mouseX, mouseY)) continue; + + List tooltip = widget.getTooltip(mouseX, mouseY); + if (!tooltip.isEmpty()) return tooltip; + } + + return Collections.emptyList(); + } + + @Override + public ItemStack getHoveredItemStack(int mouseX, int mouseY) { + if (!visible) return ItemStack.EMPTY; + + for (int i = visibleRows.size() - 1; i >= 0; i--) { + IWidget widget = visibleRows.get(i); + if (!widget.isHovered(mouseX, mouseY)) continue; + + ItemStack stack = widget.getHoveredItemStack(mouseX, mouseY); + if (!stack.isEmpty()) return stack; + } + + return ItemStack.EMPTY; + } + + // ---- Cards helper ---- + + /** + * Create a CardsDisplay for a cell's upgrade icons, positioned at the left margin. + * Shared by TerminalTabWidget, CellContentTabWidget, and TempAreaTabWidget. + * + * @param cell The cell whose upgrades to display + * @param rowY The Y position of the row this card display belongs to + * @param cardClickCallback Click handler receiving (cell, upgradeSlotIndex), or null + * @return A configured CardsDisplay, or null if no upgrades + */ + protected CardsDisplay createCellCardsDisplay(CellInfo cell, int rowY, + BiConsumer cardClickCallback) { + int slotCount = cell.getUpgradeSlotCount(); + if (slotCount <= 0) return null; + + // Build a slot-indexed array so empty slots are visually present + ItemStack[] slotStacks = new ItemStack[slotCount]; + java.util.Arrays.fill(slotStacks, ItemStack.EMPTY); + List upgrades = cell.getUpgrades(); + for (int i = 0; i < upgrades.size(); i++) { + int slotIdx = cell.getUpgradeSlotIndex(i); + if (slotIdx >= 0 && slotIdx < slotCount) slotStacks[slotIdx] = upgrades.get(i); + } + + List entries = new ArrayList<>(); + for (int i = 0; i < slotCount; i++) { + entries.add(new CardsDisplay.CardEntry(slotStacks[i], i)); + } + + CardsDisplay cards = new CardsDisplay(GuiConstants.CARDS_X, rowY, () -> entries, itemRender); + + if (cardClickCallback != null) { + cards.setClickCallback(slotIndex -> cardClickCallback.accept(cell, slotIndex)); + } + + return cards; + } + + // ---- JEI integration ---- + + /** + * Collect all JEI partition slot targets from visible SlotsLine widgets. + * The parent GUI calls this to provide ghost ingredient targets. + * + * @return Unmodifiable list of partition slot targets + */ + public List getPartitionTargets() { + List targets = new ArrayList<>(); + + for (IWidget widget : visibleRows) { + if (widget instanceof SlotsLine) { + targets.addAll(((SlotsLine) widget).getPartitionTargets()); + } + } + + return Collections.unmodifiableList(targets); + } + + /** + * Get the source data object for the widget under the mouse cursor. + * Used by the parent GUI for upgrade insertion, inline rename, and + * other interactions that need to know the original data context. + * + * @return The data object, or null if nothing is hovered + */ + public Object getDataForHoveredRow(int mouseX, int mouseY) { + for (int i = visibleRows.size() - 1; i >= 0; i--) { + IWidget widget = visibleRows.get(i); + if (widget.isHovered(mouseX, mouseY)) return widgetDataMap.get(widget); + } + + return null; + } + + /** + * Get the data map from widgets to their source data objects. + * Used for priority field positioning. The parent GUI iterates + * visible rows and maps headers back to their StorageInfo/StorageBusInfo objects. + */ + public Map getWidgetDataMap() { + return Collections.unmodifiableMap(widgetDataMap); + } + + // ======================================================================== + // Tab controller responsibilities + // ======================================================================== + + /** The GUI context, set once during initialization via {@link #init(GuiContext)}. */ + protected GuiContext guiContext; + + /** + * Initialize this tab widget with its GUI context. + * Called once during {@code initTabWidgets()} in the parent GUI. + * Subclasses should wire their internal callbacks in this method. + */ + public void init(GuiContext context) { + this.guiContext = context; + } + + /** + * Get the lines data for this tab from the data manager. + * Each tab knows which line list it needs. + */ + public abstract List getLines(TerminalDataManager dataManager); + + /** + * Get the effective search mode when this tab is active. + * Tabs that force a specific mode (e.g., Partition forces PARTITION) + * override this to ignore the user-selected mode. + * + * @param userSelectedMode The mode the user has selected in the search button + * @return The effective search filter mode for this tab + */ + public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { + return userSelectedMode; + } + + /** + * Whether the search mode button should be visible when this tab is active. + * Tabs that force a search mode should return false. + */ + public boolean showSearchModeButton() { + return true; + } + + /** + * Get the controls help text lines for this tab. + * Displayed in the controls help widget to the left of the GUI. + * + * @return List of localized help text lines (empty strings for spacing) + */ + public abstract List getHelpLines(); + + /** + * Get the icon ItemStack to display on this tab's button. + * + * @return The tab icon, or ItemStack.EMPTY for no icon + */ + public abstract ItemStack getTabIcon(); + + /** + * Get the tooltip text for this tab's button. + * + * @return The localized tooltip text + */ + public abstract String getTabTooltip(); + + /** + * Handle a tab-specific keybind. + * Called when no higher-priority handler (rename, priority, search, modals) consumed the key. + * + * @param keyCode The key code that was pressed + * @return true if the key was handled + */ + public boolean handleTabKeyTyped(int keyCode) { + return false; + } + + /** + * Whether this tab requires server-side polling for data updates. + */ + public boolean requiresServerPolling() { + return false; + } + + // ---- Upgrade support ---- + + /** + * Handle an upgrade item click on a hovered row. + * Called when the player is holding an upgrade and left-clicks in the content area. + * Subclasses override to handle their specific data types. + * + * @param hoveredData The data object under the mouse + * @param heldStack The upgrade item the player is holding + * @param isShiftClick Whether shift is held + * @return true if the click was handled + */ + public boolean handleUpgradeClick(Object hoveredData, ItemStack heldStack, boolean isShiftClick) { + // Default: try cell-based upgrade insertion + if (hoveredData instanceof CellInfo) { + CellInfo cell = (CellInfo) hoveredData; + if (!cell.canAcceptUpgrade(heldStack)) return false; + + StorageInfo storage = guiContext.getDataManager().getStorageMap().get(cell.getParentStorageId()); + if (storage == null) return false; + + guiContext.sendPacket(new com.cellterminal.network.PacketUpgradeCell( + storage.getId(), cell.getSlot(), false)); + + return true; + } + + if (hoveredData instanceof StorageInfo) { + StorageInfo storage = (StorageInfo) hoveredData; + // Check if any cell could potentially accept (client-side heuristic) + boolean anyCanAccept = storage.getCells().stream() + .anyMatch(cell -> cell.canAcceptUpgrade(heldStack)); + if (!anyCanAccept) return false; + + // Let the server iterate through cells and find one that actually accepts + // Use shiftClick=true so server handles the cell selection + guiContext.sendPacket(new com.cellterminal.network.PacketUpgradeCell( + storage.getId(), -1, true)); + + return true; + } + + if (hoveredData instanceof CellContentRow) { + CellInfo cell = ((CellContentRow) hoveredData).getCell(); + if (cell != null && cell.canAcceptUpgrade(heldStack)) { + StorageInfo storage = guiContext.getDataManager().getStorageMap().get(cell.getParentStorageId()); + if (storage != null) { + guiContext.sendPacket(new com.cellterminal.network.PacketUpgradeCell( + storage.getId(), cell.getSlot(), false)); + + return true; + } + } + } + + return false; + } + + /** + * Handle shift-click upgrade insertion (find first visible target). + * Subclasses override for bus-based tabs. + * + * @param heldStack The upgrade item + * @return true if handled + */ + public boolean handleShiftUpgradeClick(ItemStack heldStack) { + // Delegate entirely to the server: send storageId=-1 and shiftClick=true. + // The server iterates all storages sorted by distance and finds the first + // cell that can actually accept the upgrade. + guiContext.sendPacket(new PacketUpgradeCell(-1, -1, true)); + + return true; + } + + /** + * Handle a shift-click on an upgrade item in a player inventory slot. + * This is the container-level shift-click path (from player inventory into the terminal) + * as opposed to the content-area shift-click path ({@link #handleShiftUpgradeClick}). + *

+ * The source slot index is needed so the server knows where to extract the upgrade from. + * Default implementation finds the first visible cell that can accept the upgrade. + * Storage bus tabs override this to find the first visible bus instead. + * + * @param upgradeStack The upgrade item being shift-clicked + * @param sourceSlotIndex The inventory slot index the upgrade is coming from + * @return true if the upgrade was handled and should not propagate + */ + public boolean handleInventorySlotShiftClick(ItemStack upgradeStack, int sourceSlotIndex) { + // Delegate entirely to the server: send storageId=-1 and shiftClick=true. + // The server iterates all storages sorted by distance and finds the first + // cell that can actually accept the upgrade. + guiContext.sendPacket(new PacketUpgradeCell(-1, -1, true, sourceSlotIndex)); + + return true; + } + + // ---- JEI ghost targets ---- + + /** + * Get JEI ghost ingredient targets for this tab. + * Only partition-related tabs provide targets. Default returns empty list. + */ + public List> getPhantomTargets(Object ingredient) { + return Collections.emptyList(); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/tab/CellContentTabWidget.java b/src/main/java/com/cellterminal/gui/widget/tab/CellContentTabWidget.java new file mode 100644 index 0000000..4fa8adb --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/tab/CellContentTabWidget.java @@ -0,0 +1,491 @@ +package com.cellterminal.gui.widget.tab; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.lwjgl.input.Keyboard; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.client.resources.I18n; +import net.minecraft.item.ItemStack; + +import appeng.api.AEApi; + +import mezz.jei.api.gui.IGhostIngredientHandler; + +import com.cellterminal.client.CellContentRow; +import com.cellterminal.client.CellInfo; +import com.cellterminal.client.EmptySlotInfo; +import com.cellterminal.client.KeyBindings; +import com.cellterminal.client.SearchFilterMode; +import com.cellterminal.client.StorageInfo; +import com.cellterminal.client.TabStateManager; +import com.cellterminal.config.CellTerminalServerConfig; +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.PriorityFieldManager; +import com.cellterminal.gui.handler.JeiGhostHandler; +import com.cellterminal.gui.handler.QuickPartitionHandler; +import com.cellterminal.gui.handler.TerminalDataManager; +import com.cellterminal.gui.widget.CardsDisplay; +import com.cellterminal.gui.widget.DoubleClickTracker; +import com.cellterminal.gui.widget.IWidget; +import com.cellterminal.gui.widget.button.ButtonType; +import com.cellterminal.gui.widget.button.SmallButton; +import com.cellterminal.gui.widget.header.StorageHeader; +import com.cellterminal.gui.widget.line.CellSlotsLine; +import com.cellterminal.gui.widget.line.ContinuationLine; +import com.cellterminal.gui.widget.line.SlotsLine; +import com.cellterminal.integration.ThaumicEnergisticsIntegration; +import com.cellterminal.network.PacketExtractUpgrade; +import com.cellterminal.network.PacketInsertCell; +import com.cellterminal.network.PacketPartitionAction; +import com.cellterminal.network.PacketPickupCell; + + +/** + * Tab widget for the Inventory (Tab 1) and Partition (Tab 2) tabs. + *

+ * Both tabs display the same structure (StorageInfo headers + cell content rows) + * but differ in the slot mode (CONTENT vs PARTITION) and tree button type: + *

    + *
  • Inventory tab: Shows cell contents with counts. Tree button is + * DO_PARTITION (adds item to partition). Content slots show "P" indicator + * for items that are in the partition.
  • + *
  • Partition tab: Shows cell partitions with amber tint. Tree button is + * CLEAR_PARTITION (removes partition entry). Supports JEI ghost drops.
  • + *
+ * + * Each row in the line list is one of: + *
    + *
  • {@link StorageInfo} → {@link StorageHeader}
  • + *
  • {@link CellContentRow} (first row) → {@link CellSlotsLine} (with cell slot + cards)
  • + *
  • {@link CellContentRow} (continuation) → {@link ContinuationLine}
  • + *
  • {@link EmptySlotInfo} → {@link CellSlotsLine} (empty cell slot placeholder)
  • + *
+ */ +public class CellContentTabWidget extends AbstractTabWidget { + + /** Slots per row for cell content: 8 */ + private static final int SLOTS_PER_ROW = GuiConstants.CELL_SLOTS_PER_ROW; + + /** + * X offset where content/partition slots begin. + * After cell slot (16px) + cards area + gap. + */ + private static final int SLOTS_X_OFFSET = GuiConstants.CELL_INDENT + 20; + + private final SlotsLine.SlotMode slotMode; + private final ButtonType treeButtonType; + private final boolean isPartitionMode; + + /** + * @param slotMode CONTENT for Inventory tab, PARTITION for Partition tab + */ + public CellContentTabWidget(SlotsLine.SlotMode slotMode, + FontRenderer fontRenderer, RenderItem itemRender) { + super(fontRenderer, itemRender); + this.slotMode = slotMode; + this.isPartitionMode = (slotMode == SlotsLine.SlotMode.PARTITION); + this.treeButtonType = isPartitionMode ? ButtonType.CLEAR_PARTITION : ButtonType.DO_PARTITION; + } + + // ---- Tab controller methods ---- + + @Override + public List getLines(TerminalDataManager dataManager) { + return isPartitionMode ? dataManager.getPartitionLines() : dataManager.getInventoryLines(); + } + + @Override + public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { + return isPartitionMode ? SearchFilterMode.PARTITION : SearchFilterMode.INVENTORY; + } + + @Override + public boolean showSearchModeButton() { + return false; + } + + @Override + public List getHelpLines() { + List lines = new ArrayList<>(); + + if (isPartitionMode) { + lines.add(I18n.format("gui.cellterminal.controls.keybind_targets")); + lines.add(""); + + String notSet = I18n.format("gui.cellterminal.controls.key_not_set"); + + String autoKey = KeyBindings.QUICK_PARTITION_AUTO.isBound() + ? KeyBindings.QUICK_PARTITION_AUTO.getDisplayName() : notSet; + lines.add(I18n.format("gui.cellterminal.controls.key_auto", autoKey)); + if (!autoKey.equals(notSet)) { + lines.add(I18n.format("gui.cellterminal.controls.auto_warning")); + } + lines.add(""); + + String itemKey = KeyBindings.QUICK_PARTITION_ITEM.isBound() + ? KeyBindings.QUICK_PARTITION_ITEM.getDisplayName() : notSet; + lines.add(I18n.format("gui.cellterminal.controls.key_item", itemKey)); + + String fluidKey = KeyBindings.QUICK_PARTITION_FLUID.isBound() + ? KeyBindings.QUICK_PARTITION_FLUID.getDisplayName() : notSet; + lines.add(I18n.format("gui.cellterminal.controls.key_fluid", fluidKey)); + + String essentiaKey = KeyBindings.QUICK_PARTITION_ESSENTIA.isBound() + ? KeyBindings.QUICK_PARTITION_ESSENTIA.getDisplayName() : notSet; + lines.add(I18n.format("gui.cellterminal.controls.key_essentia", essentiaKey)); + + if (!essentiaKey.equals(notSet) && !ThaumicEnergisticsIntegration.isModLoaded()) { + lines.add(I18n.format("gui.cellterminal.controls.essentia_warning")); + } + + lines.add(""); + lines.add(I18n.format("gui.cellterminal.controls.jei_drag")); + lines.add(I18n.format("gui.cellterminal.controls.click_to_remove")); + } else { + lines.add(I18n.format("gui.cellterminal.controls.partition_indicator")); + lines.add(I18n.format("gui.cellterminal.controls.click_partition_toggle")); + } + + lines.add(I18n.format("gui.cellterminal.controls.double_click_storage")); + lines.add(I18n.format("gui.cellterminal.right_click_rename")); + + return lines; + } + + @Override + public ItemStack getTabIcon() { + if (isPartitionMode) { + return AEApi.instance().definitions().blocks().cellWorkbench() + .maybeStack(1).orElse(ItemStack.EMPTY); + } + + return AEApi.instance().definitions().blocks().chest() + .maybeStack(1).orElse(ItemStack.EMPTY); + } + + @Override + public String getTabTooltip() { + if (isPartitionMode) { + return I18n.format("gui.cellterminal.tab.partition.tooltip"); + } + + return I18n.format("gui.cellterminal.tab.inventory.tooltip"); + } + + @Override + public boolean handleTabKeyTyped(int keyCode) { + if (!isPartitionMode) return false; + + QuickPartitionHandler.PartitionType type = null; + + if (KeyBindings.QUICK_PARTITION_AUTO.isActiveAndMatches(keyCode)) { + type = QuickPartitionHandler.PartitionType.AUTO; + } else if (KeyBindings.QUICK_PARTITION_ITEM.isActiveAndMatches(keyCode)) { + type = QuickPartitionHandler.PartitionType.ITEM; + } else if (KeyBindings.QUICK_PARTITION_FLUID.isActiveAndMatches(keyCode)) { + type = QuickPartitionHandler.PartitionType.FLUID; + } else if (KeyBindings.QUICK_PARTITION_ESSENTIA.isActiveAndMatches(keyCode)) { + type = QuickPartitionHandler.PartitionType.ESSENTIA; + } + + if (type == null) return false; + + QuickPartitionHandler.QuickPartitionResult result = QuickPartitionHandler.attemptQuickPartition( + type, getLines(guiContext.getDataManager()), guiContext.getDataManager().getStorageMap()); + + if (result.success) { + guiContext.showSuccess(result.message); + if (result.scrollToLine >= 0) guiContext.scrollToLine(result.scrollToLine); + } else { + guiContext.showError(result.message); + } + + return true; + } + + // ---- JEI ghost targets ---- + + @Override + public List> getPhantomTargets(Object ingredient) { + if (!isPartitionMode) return Collections.emptyList(); + + List> targets = new ArrayList<>(); + for (java.util.Map.Entry entry : getWidgetDataMap().entrySet()) { + IWidget widget = entry.getKey(); + Object data = entry.getValue(); + if (!(widget instanceof SlotsLine)) continue; + + SlotsLine slotsLine = (SlotsLine) widget; + List slotTargets = slotsLine.getPartitionTargets(); + if (slotTargets.isEmpty()) continue; + if (!(data instanceof CellContentRow)) continue; + + CellInfo cell = ((CellContentRow) data).getCell(); + for (SlotsLine.PartitionSlotTarget slot : slotTargets) { + targets.add(new IGhostIngredientHandler.Target() { + @Override + public Rectangle getArea() { + return new Rectangle(slot.absX, slot.absY, slot.width, slot.height); + } + + @Override + public void accept(Object ing) { + ItemStack stack = JeiGhostHandler.convertJeiIngredientToItemStack( + ing, cell.isFluid(), cell.isEssentia()); + if (!stack.isEmpty()) { + guiContext.sendPacket(new PacketPartitionAction( + cell.getParentStorageId(), cell.getSlot(), + PacketPartitionAction.Action.ADD_ITEM, slot.absoluteIndex, stack)); + } + } + }); + } + } + + return targets; + } + + // ---- Row building ---- + + @Override + protected IWidget createRowWidget(Object lineData, int y, List allLines, int lineIndex) { + if (lineData instanceof StorageInfo) { + return createStorageHeader((StorageInfo) lineData, y); + } + + if (lineData instanceof CellContentRow) { + return createCellContentLine((CellContentRow) lineData, y); + } + + if (lineData instanceof EmptySlotInfo) { + return createEmptySlotLine((EmptySlotInfo) lineData, y); + } + + return null; + } + + @Override + protected boolean isContentLine(List allLines, int index) { + if (index < 0 || index >= allLines.size()) return false; + + Object line = allLines.get(index); + + return line instanceof CellContentRow || line instanceof EmptySlotInfo; + } + + // ---- Storage header creation ---- + + private StorageHeader createStorageHeader(StorageInfo storage, int y) { + StorageHeader header = new StorageHeader(y, fontRenderer, itemRender); + header.setIconSupplier(storage::getBlockItem); + header.setNameSupplier(storage::getName); + header.setHasCustomNameSupplier(storage::hasCustomName); + header.setLocationSupplier(storage::getLocationString); + + // Use TabStateManager for expand/collapse state (persists across rebuilds) + // Determine tab type based on slot mode (INVENTORY or PARTITION) + TabStateManager.TabType tabType = isPartitionMode + ? TabStateManager.TabType.PARTITION + : TabStateManager.TabType.INVENTORY; + header.setExpandedSupplier(() -> + TabStateManager.getInstance().isExpanded(tabType, storage.getId())); + + // Rename info: header handles right-click directly via InlineRenameManager + int renameRightEdge = GuiConstants.CONTENT_RIGHT_EDGE + - PriorityFieldManager.FIELD_WIDTH - PriorityFieldManager.RIGHT_MARGIN - 4; + header.setRenameInfo(storage, GuiConstants.GUI_INDENT + 20 - 2, 0, renameRightEdge); + header.setOnNameDoubleClick(() -> guiContext.highlightInWorld( + storage.getPos(), storage.getDimension(), storage.getName()), + DoubleClickTracker.storageTargetId(storage.getId())); + header.setOnExpandToggle(() -> { + TabStateManager.getInstance().toggleExpanded(tabType, storage.getId()); + guiContext.rebuildAndUpdateScrollbar(); + }); + + // Priority field: header registers its own field with the singleton during draw + header.setPrioritizable(storage); + header.setGuiOffsets(guiLeft, guiTop); + + return header; + } + + // ---- Cell content line creation ---- + + private IWidget createCellContentLine(CellContentRow row, int y) { + CellInfo cell = row.getCell(); + + if (row.isFirstRow()) { + return createFirstRow(cell, row.getStartIndex(), y); + } + + return createContinuationRow(cell, row.getStartIndex(), y); + } + + /** + * First row of a cell: cell slot + upgrade cards + content/partition slots + tree button. + */ + private CellSlotsLine createFirstRow(CellInfo cell, int startIndex, int y) { + CellSlotsLine line = new CellSlotsLine( + y, SLOTS_PER_ROW, SLOTS_X_OFFSET, slotMode, + startIndex, fontRenderer, itemRender + ); + + // Cell slot configuration + line.setCellItemSupplier(cell::getCellItem); + line.setCellFilledSupplier(() -> !cell.getCellItem().isEmpty()); + + // Cell slot click (insert/extract cell) + line.setCellSlotCallback(button -> { + if (button != 0) return; + + ItemStack heldStack = guiContext.getHeldStack(); + if (cell.getCellItem().isEmpty() && !heldStack.isEmpty()) { + // Insert held cell into this slot + guiContext.sendPacket(new PacketInsertCell( + cell.getParentStorageId(), cell.getSlot())); + } else if (!cell.getCellItem().isEmpty()) { + // Pick up cell: shift = to inventory, normal click = to cursor (swap/pickup) + boolean toInventory = guiContext.isShiftDown(); + guiContext.sendPacket(new PacketPickupCell( + cell.getParentStorageId(), cell.getSlot(), toInventory)); + } + }); + + // Configure content/partition data suppliers + configureSlotData(line, cell); + + // Upgrade cards + CardsDisplay cards = createCellCardsDisplay(cell, y, this::handleCardClick); + if (cards != null) line.setCardsDisplay(cards); + + // Tree junction button (DoPartition or ClearPartition) + SmallButton treeBtn = new SmallButton(0, 0, treeButtonType, () -> { + if (isPartitionMode) { + guiContext.sendPacket(new PacketPartitionAction( + cell.getParentStorageId(), cell.getSlot(), + PacketPartitionAction.Action.CLEAR_ALL)); + } else { + guiContext.sendPacket(new PacketPartitionAction( + cell.getParentStorageId(), cell.getSlot(), + PacketPartitionAction.Action.SET_ALL_FROM_CONTENTS)); + } + }); + line.setTreeButton(treeBtn); + + // GUI offsets for JEI targets + line.setGuiOffsets(guiLeft, guiTop); + + return line; + } + + /** + * Continuation row (not the first row for a cell): just content/partition slots. + */ + private ContinuationLine createContinuationRow(CellInfo cell, int startIndex, int y) { + ContinuationLine line = new ContinuationLine( + y, SLOTS_PER_ROW, SLOTS_X_OFFSET, slotMode, + startIndex, fontRenderer, itemRender + ); + + // Tabs 2/3 have no junction element (cell slot/button) on continuation rows, + // so the horizontal branch would point at empty space. + line.setDrawHorizontalBranch(false); + configureSlotData(line, cell); + line.setGuiOffsets(guiLeft, guiTop); + + return line; + } + + /** + * Configure the content/partition data suppliers on a SlotsLine. + */ + private void configureSlotData(SlotsLine line, CellInfo cell) { + if (slotMode == SlotsLine.SlotMode.CONTENT) { + line.setItemsSupplier(cell::getContents); + line.setPartitionSupplier(cell::getPartition); + line.setCountProvider(() -> cell::getContentCount); + } else { + // Partition mode: partition list is the items source + line.setItemsSupplier(cell::getPartition); + line.setMaxSlots((int) cell.getTotalTypes()); + } + + line.setSlotClickCallback((slotIndex, mouseButton) -> { + if (mouseButton != 0) return; + + if (isPartitionMode) { + ItemStack heldStack = guiContext.getHeldStack(); + List partition = cell.getPartition(); + boolean slotOccupied = slotIndex < partition.size() && !partition.get(slotIndex).isEmpty(); + + if (!heldStack.isEmpty()) { + guiContext.sendPacket(new PacketPartitionAction( + cell.getParentStorageId(), cell.getSlot(), + PacketPartitionAction.Action.ADD_ITEM, slotIndex, heldStack)); + } else if (slotOccupied) { + guiContext.sendPacket(new PacketPartitionAction( + cell.getParentStorageId(), cell.getSlot(), + PacketPartitionAction.Action.REMOVE_ITEM, slotIndex)); + } + } else { + // Content mode: toggle partition for content item + List contents = cell.getContents(); + if (slotIndex < contents.size() && !contents.get(slotIndex).isEmpty()) { + guiContext.sendPacket(new PacketPartitionAction( + cell.getParentStorageId(), cell.getSlot(), + PacketPartitionAction.Action.TOGGLE_ITEM, contents.get(slotIndex))); + } + } + }); + } + + // ---- Empty slot line creation ---- + + /** + * Empty cell slot: shows an empty cell slot placeholder (no content slots). + */ + private CellSlotsLine createEmptySlotLine(EmptySlotInfo emptySlot, int y) { + CellSlotsLine line = new CellSlotsLine( + y, SLOTS_PER_ROW, SLOTS_X_OFFSET, slotMode, + 0, fontRenderer, itemRender + ); + + // Empty cell slot (no item, not filled) + line.setCellItemSupplier(() -> ItemStack.EMPTY); + line.setCellFilledSupplier(() -> false); + + // Cell slot click (insert cell into empty slot) + line.setCellSlotCallback(button -> { + if (button != 0) return; + + ItemStack heldStack = guiContext.getHeldStack(); + if (!heldStack.isEmpty()) { + guiContext.sendPacket(new PacketInsertCell( + emptySlot.getParentStorageId(), emptySlot.getSlot())); + } + }); + + line.setGuiOffsets(guiLeft, guiTop); + + return line; + } + + // ---- Upgrade card click handling ---- + + private void handleCardClick(CellInfo cell, int upgradeSlotIndex) { + if (CellTerminalServerConfig.isInitialized() + && !CellTerminalServerConfig.getInstance().isUpgradeExtractEnabled()) { + guiContext.showError("cellterminal.error.upgrade_extract_disabled"); + return; + } + + boolean toInventory = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + guiContext.sendPacket(PacketExtractUpgrade.forCell( + cell.getParentStorageId(), cell.getSlot(), upgradeSlotIndex, toInventory)); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/tab/GuiContext.java b/src/main/java/com/cellterminal/gui/widget/tab/GuiContext.java new file mode 100644 index 0000000..18b52b2 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/tab/GuiContext.java @@ -0,0 +1,92 @@ +package com.cellterminal.gui.widget.tab; + +import java.util.Set; + +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraft.util.math.BlockPos; + +import com.cellterminal.client.CellInfo; +import com.cellterminal.gui.handler.TerminalDataManager; + + +/** + * Minimal interface that the parent GUI provides to tab widgets. + *

+ * Replaces the old pattern of wiring dozens of individual callbacks per tab. + * Tab widgets use this to communicate upward to the GUI for actions that require + * GUI-level state (scrollbar, popups, modals, network packets). + *

+ * The intent is to keep this as small as possible: each tab should handle its own + * logic internally whenever feasible, and only call back to the GUI for truly + * shared operations (sending network packets, opening popups, etc.). + */ +public interface GuiContext { + + // ---- Data access ---- + + /** Get the terminal data manager for accessing cell/bus/line data. */ + TerminalDataManager getDataManager(); + + /** Get the item stack currently held by the player's cursor. */ + ItemStack getHeldStack(); + + /** Get the slot under the mouse cursor, or null if none. */ + Slot getSlotUnderMouse(); + + /** Whether shift key is held. */ + boolean isShiftDown(); + + // ---- Network packet helpers ---- + + /** Send a packet to the server. */ + void sendPacket(Object packet); + + // ---- GUI-level actions ---- + + /** Trigger a full rebuild of lines and scrollbar update. */ + void rebuildAndUpdateScrollbar(); + + /** Scroll to a specific line index. */ + void scrollToLine(int lineIndex); + + /** Open an inventory preview popup for a cell. */ + void openInventoryPopup(CellInfo cell); + + /** Open a partition preview popup for a cell. */ + void openPartitionPopup(CellInfo cell); + + /** Show an overlay error message. */ + void showError(String translationKey, Object... args); + + /** Show an overlay success message. */ + void showSuccess(String translationKey, Object... args); + + /** Show an overlay warning message. */ + void showWarning(String translationKey, Object... args); + + // ---- Highlight in world ---- + + /** + * Highlight a block position in the world (double-click on headers). + * @param pos The block position to highlight + * @param dimension The dimension ID where the block is located + * @param displayName A display name to show in the success message + */ + void highlightInWorld(BlockPos pos, int dimension, String displayName); + + /** + * Highlight a cell's parent storage in the world (double-click on cells). + * Finds the storage containing the cell and highlights its position. + * @param cell The cell to highlight + */ + void highlightCellInWorld(CellInfo cell); + + // ---- Selection state (for multi-select keybinds) ---- + + /** Get the set of selected storage bus IDs. */ + Set getSelectedStorageBusIds(); + + /** Get the set of selected temp cell slot indices. */ + Set getSelectedTempCellSlots(); +} diff --git a/src/main/java/com/cellterminal/gui/widget/tab/NetworkToolsTabWidget.java b/src/main/java/com/cellterminal/gui/widget/tab/NetworkToolsTabWidget.java new file mode 100644 index 0000000..435cf2b --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/tab/NetworkToolsTabWidget.java @@ -0,0 +1,159 @@ +package com.cellterminal.gui.widget.tab; + +import java.util.ArrayList; +import java.util.List; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.client.resources.I18n; +import net.minecraft.item.ItemStack; + +import appeng.api.AEApi; + +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.handler.TerminalDataManager; +import com.cellterminal.gui.networktools.INetworkTool; +import com.cellterminal.gui.networktools.NetworkToolRegistry; +import com.cellterminal.gui.widget.IWidget; +import com.cellterminal.gui.widget.NetworkToolRowWidget; + + +/** + * Tab widget for the Network Tools tab (Tab 6). + *

+ * Displays a scrollable list of network tools, each rendered as an independent + * {@link NetworkToolRowWidget}. Each row is 36px tall (2x the standard 18px row height), + * so this tab overrides scroll calculations accordingly. + *

+ * Tools come from {@link NetworkToolRegistry} rather than the data manager. + * The tool context (filters, storages) is fresh-fetched from the GUI context each frame. + */ +public class NetworkToolsTabWidget extends AbstractTabWidget { + + /** Cached tab icon (lazy initialized) */ + private ItemStack tabIcon = null; + + public NetworkToolsTabWidget(FontRenderer fontRenderer, RenderItem itemRender) { + super(fontRenderer, itemRender); + } + + // ---- Scroll calculation ---- + + @Override + public int getVisibleItemCount() { + int contentHeight = rowsVisible * GuiConstants.ROW_HEIGHT; + + return contentHeight / NetworkToolRowWidget.ROW_HEIGHT; + } + + // ---- Row building ---- + + /** + * Build visible tool rows for the current scroll window. + * Overrides the standard row building to use TOOL_ROW_HEIGHT spacing. + */ + @Override + public void buildVisibleRows(List lines, int scrollOffset) { + visibleRows.clear(); + widgetDataMap.clear(); + + int visibleCount = getVisibleItemCount(); + int end = Math.min(scrollOffset + visibleCount + 1, lines.size()); + + for (int i = scrollOffset; i < end; i++) { + int rowY = GuiConstants.CONTENT_START_Y + + (i - scrollOffset) * NetworkToolRowWidget.ROW_HEIGHT; + Object lineData = lines.get(i); + IWidget widget = createRowWidget(lineData, rowY, lines, i); + if (widget != null) { + visibleRows.add(widget); + widgetDataMap.put(widget, lineData); + } + } + + // No tree line propagation needed for tool rows + } + + @Override + protected IWidget createRowWidget(Object lineData, int y, List allLines, int lineIndex) { + if (!(lineData instanceof INetworkTool)) return null; + + INetworkTool tool = (INetworkTool) lineData; + NetworkToolRowWidget row = new NetworkToolRowWidget(tool, y, fontRenderer, itemRender); + + // Wire up context supplier. Lazily fetches tool context each frame + row.setContextSupplier(() -> guiContext != null + ? ((NetworkToolGuiContext) guiContext).createNetworkToolContext() + : null); + + // Wire up run button click → show confirmation modal + row.setOnRunClicked(() -> { + if (guiContext != null) { + ((NetworkToolGuiContext) guiContext).showNetworkToolConfirmation(tool); + } + }); + + return row; + } + + @Override + protected boolean isContentLine(List allLines, int index) { + // All lines are tools (no headers/content distinction) + return true; + } + + // ---- Tab controller methods ---- + + @Override + @SuppressWarnings("unchecked") + public List getLines(TerminalDataManager dataManager) { + List tools = NetworkToolRegistry.getAllTools(); + + return (List) (List) tools; + } + + @Override + public boolean showSearchModeButton() { + return true; + } + + @Override + public List getHelpLines() { + List lines = new ArrayList<>(); + + lines.add(I18n.format("gui.cellterminal.networktools.warning.caution")); + lines.add(I18n.format("gui.cellterminal.networktools.warning.irreversible")); + lines.add(""); + lines.add(I18n.format("gui.cellterminal.networktools.help.read_tooltip")); + + return lines; + } + + @Override + public ItemStack getTabIcon() { + if (tabIcon == null) { + tabIcon = AEApi.instance().definitions().items().networkTool() + .maybeStack(1).orElse(ItemStack.EMPTY); + } + + return tabIcon; + } + + @Override + public String getTabTooltip() { + return I18n.format("gui.cellterminal.tab.network_tools.tooltip"); + } + + /** + * Extended GUI context interface for network tools. + * The parent GUI (GuiCellTerminalBase) implements this to provide + * tool-specific callbacks that the tab widget needs. + */ + public interface NetworkToolGuiContext extends GuiContext { + /** Create a fresh ToolContext for tool preview and execution. */ + INetworkTool.ToolContext createNetworkToolContext(); + + /** Show the confirmation modal for a network tool. */ + void showNetworkToolConfirmation(INetworkTool tool); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/tab/StorageBusTabWidget.java b/src/main/java/com/cellterminal/gui/widget/tab/StorageBusTabWidget.java new file mode 100644 index 0000000..79ce1ee --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/tab/StorageBusTabWidget.java @@ -0,0 +1,622 @@ +package com.cellterminal.gui.widget.tab; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.lwjgl.input.Keyboard; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.client.resources.I18n; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; + +import appeng.api.AEApi; +import appeng.fluids.items.FluidDummyItem; + +import mezz.jei.api.gui.IGhostIngredientHandler; + +import com.cellterminal.client.KeyBindings; +import com.cellterminal.client.SearchFilterMode; +import com.cellterminal.client.StorageBusContentRow; +import com.cellterminal.client.StorageBusInfo; +import com.cellterminal.client.TabStateManager; +import com.cellterminal.config.CellTerminalServerConfig; +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.handler.JeiGhostHandler; +import com.cellterminal.gui.handler.TerminalDataManager; +import com.cellterminal.gui.handler.QuickPartitionHandler; +import com.cellterminal.gui.overlay.MessageHelper; +import com.cellterminal.gui.widget.CardsDisplay; +import com.cellterminal.gui.widget.DoubleClickTracker; +import com.cellterminal.gui.widget.IWidget; +import com.cellterminal.gui.widget.button.ButtonType; +import com.cellterminal.gui.widget.button.SmallButton; +import com.cellterminal.gui.widget.header.StorageBusHeader; +import com.cellterminal.gui.widget.line.ContinuationLine; +import com.cellterminal.gui.widget.line.SlotsLine; +import com.cellterminal.integration.ThaumicEnergisticsIntegration; +import com.cellterminal.network.CellTerminalNetwork; +import com.cellterminal.network.PacketExtractUpgrade; +import com.cellterminal.network.PacketStorageBusIOMode; +import com.cellterminal.network.PacketStorageBusPartitionAction; +import com.cellterminal.network.PacketUpgradeStorageBus; + + +/** + * Tab widget for Storage Bus Inventory (Tab 4) and Storage Bus Partition (Tab 5) tabs. + *

+ * Both tabs display storage bus groups with expandable content rows. Each row is either: + *

    + *
  • {@link StorageBusInfo} → {@link StorageBusHeader} (name, location, IO mode, expand)
  • + *
  • {@link StorageBusContentRow} (first row) → {@link SlotsLine} with tree button
  • + *
  • {@link StorageBusContentRow} (continuation) → {@link ContinuationLine}
  • + *
+ * + *

Slot mode differences

+ *
    + *
  • Inventory tab: Shows bus contents with counts. Tree button is + * DO_PARTITION (adds item to partition). Content slots show "P" indicator + * for items that are in the partition.
  • + *
  • Partition tab: Shows bus partitions with amber tint. Tree button is + * CLEAR_PARTITION (removes partition entry). Supports JEI ghost drops.
  • + *
+ * + * Storage bus tabs use 9 slots per row at a narrower X offset (no inline cell slot). + */ +public class StorageBusTabWidget extends AbstractTabWidget { + + /** Slots per row for storage buses: 9 */ + private static final int SLOTS_PER_ROW = GuiConstants.STORAGE_BUS_SLOTS_PER_ROW; + + /** X offset for content/partition slots */ + private static final int SLOTS_X_OFFSET = GuiConstants.CELL_INDENT + 4; + + private final SlotsLine.SlotMode slotMode; + private final ButtonType treeButtonType; + private final boolean isPartitionMode; + + /** + * @param slotMode CONTENT for Storage Bus Inventory tab, PARTITION for Storage Bus Partition tab + */ + public StorageBusTabWidget(SlotsLine.SlotMode slotMode, + FontRenderer fontRenderer, RenderItem itemRender) { + super(fontRenderer, itemRender); + this.slotMode = slotMode; + this.isPartitionMode = (slotMode == SlotsLine.SlotMode.PARTITION); + this.treeButtonType = isPartitionMode ? ButtonType.CLEAR_PARTITION : ButtonType.DO_PARTITION; + } + + // ---- Tab controller methods ---- + + @Override + public List getLines(TerminalDataManager dataManager) { + return isPartitionMode ? dataManager.getStorageBusPartitionLines() : dataManager.getStorageBusInventoryLines(); + } + + @Override + public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { + return isPartitionMode ? SearchFilterMode.PARTITION : SearchFilterMode.INVENTORY; + } + + @Override + public boolean showSearchModeButton() { + return false; + } + + @Override + public boolean requiresServerPolling() { + return true; + } + + @Override + public List getHelpLines() { + List lines = new ArrayList<>(); + + if (isPartitionMode) { + lines.add(I18n.format("gui.cellterminal.controls.storage_bus_add_key", + KeyBindings.ADD_TO_STORAGE_BUS.getDisplayName())); + lines.add(I18n.format("gui.cellterminal.controls.storage_bus_capacity")); + lines.add(""); + lines.add(I18n.format("gui.cellterminal.controls.jei_drag")); + lines.add(I18n.format("gui.cellterminal.controls.click_to_remove")); + } else { + lines.add(I18n.format("gui.cellterminal.controls.filter_indicator")); + lines.add(I18n.format("gui.cellterminal.controls.click_to_remove")); + } + + lines.add(I18n.format("gui.cellterminal.controls.double_click_storage")); + lines.add(I18n.format("gui.cellterminal.right_click_rename")); + + return lines; + } + + @Override + public ItemStack getTabIcon() { + // Returns the base storage bus icon; composite overlay is handled by TabRenderingHandler + return AEApi.instance().definitions().parts().storageBus() + .maybeStack(1).orElse(ItemStack.EMPTY); + } + + @Override + public String getTabTooltip() { + if (isPartitionMode) { + return I18n.format("gui.cellterminal.tab.storage_bus_partition.tooltip"); + } + + return I18n.format("gui.cellterminal.tab.storage_bus_inventory.tooltip"); + } + + @Override + public boolean handleTabKeyTyped(int keyCode) { + if (!isPartitionMode) return false; + if (!KeyBindings.ADD_TO_STORAGE_BUS.isActiveAndMatches(keyCode)) return false; + + return handleAddToStorageBusKeybind( + guiContext.getSelectedStorageBusIds(), + guiContext.getSlotUnderMouse(), + guiContext.getDataManager().getStorageBusMap()); + } + + // ---- Upgrade support ---- + + @Override + public boolean handleUpgradeClick(Object hoveredData, ItemStack heldStack, boolean isShiftClick) { + if (isShiftClick) { + // Shift-click: server finds first bus that can accept + guiContext.sendPacket(new PacketUpgradeStorageBus(0, true)); + return true; + } + + StorageBusInfo bus = null; + if (hoveredData instanceof StorageBusInfo) bus = (StorageBusInfo) hoveredData; + else if (hoveredData instanceof StorageBusContentRow) bus = ((StorageBusContentRow) hoveredData).getStorageBus(); + + if (bus != null) { + guiContext.sendPacket(new PacketUpgradeStorageBus(bus.getId(), false)); + return true; + } + + return false; + } + + @Override + public boolean handleShiftUpgradeClick(ItemStack heldStack) { + // Shift-click: server finds first bus that can accept + guiContext.sendPacket(new PacketUpgradeStorageBus(0, true)); + return true; + } + + @Override + public boolean handleInventorySlotShiftClick(ItemStack upgradeStack, int sourceSlotIndex) { + // Shift-click from player inventory: find first visible bus that can accept + List lines = getLines(guiContext.getDataManager()); + Set checkedBusIds = new HashSet<>(); + + for (Object line : lines) { + StorageBusInfo bus = null; + + if (line instanceof StorageBusInfo) { + bus = (StorageBusInfo) line; + } else if (line instanceof StorageBusContentRow) { + bus = ((StorageBusContentRow) line).getStorageBus(); + } + + if (bus == null) continue; + if (!checkedBusIds.add(bus.getId())) continue; + if (!bus.canAcceptUpgrade(upgradeStack)) continue; + + guiContext.sendPacket(new PacketUpgradeStorageBus(bus.getId(), true, sourceSlotIndex)); + + return true; + } + + return false; + } + + // ---- JEI ghost targets ---- + + @Override + public List> getPhantomTargets(Object ingredient) { + if (!isPartitionMode) return Collections.emptyList(); + + List> targets = new ArrayList<>(); + for (Map.Entry entry : getWidgetDataMap().entrySet()) { + IWidget widget = entry.getKey(); + Object data = entry.getValue(); + if (!(widget instanceof SlotsLine)) continue; + + SlotsLine slotsLine = (SlotsLine) widget; + List slotTargets = slotsLine.getPartitionTargets(); + if (slotTargets.isEmpty()) continue; + if (!(data instanceof StorageBusContentRow)) continue; + + StorageBusInfo bus = ((StorageBusContentRow) data).getStorageBus(); + for (SlotsLine.PartitionSlotTarget slot : slotTargets) { + targets.add(new IGhostIngredientHandler.Target() { + @Override + public Rectangle getArea() { + return new Rectangle(slot.absX, slot.absY, slot.width, slot.height); + } + + @Override + public void accept(Object ing) { + ItemStack stack = JeiGhostHandler.convertJeiIngredientForStorageBus( + ing, bus.isFluid(), bus.isEssentia()); + if (!stack.isEmpty()) { + guiContext.sendPacket(new PacketStorageBusPartitionAction( + bus.getId(), + PacketStorageBusPartitionAction.Action.ADD_ITEM, + slot.absoluteIndex, stack)); + } + } + }); + } + } + + return targets; + } + + // ---- Row building ---- + + @Override + protected IWidget createRowWidget(Object lineData, int y, List allLines, int lineIndex) { + if (lineData instanceof StorageBusInfo) { + return createBusHeader((StorageBusInfo) lineData, y); + } + + if (lineData instanceof StorageBusContentRow) { + return createContentLine((StorageBusContentRow) lineData, y); + } + + return null; + } + + @Override + protected boolean isContentLine(List allLines, int index) { + if (index < 0 || index >= allLines.size()) return false; + + return allLines.get(index) instanceof StorageBusContentRow; + } + + // ---- Storage bus header creation ---- + + private StorageBusHeader createBusHeader(StorageBusInfo bus, int y) { + StorageBusHeader header = new StorageBusHeader(y, fontRenderer, itemRender); + header.setIconSupplier(bus::getConnectedInventoryIcon); + header.setNameSupplier(bus::getLocalizedName); + header.setHasCustomNameSupplier(bus::hasCustomName); + // Use TabStateManager for expand/collapse state (persists across rebuilds) + TabStateManager.TabType tabType = isPartitionMode + ? TabStateManager.TabType.STORAGE_BUS_PARTITION + : TabStateManager.TabType.STORAGE_BUS_INVENTORY; + header.setExpandedSupplier(() -> + TabStateManager.getInstance().isBusExpanded(tabType, bus.getId())); + header.setLocationSupplier(bus::getLocationString); + header.setAccessModeSupplier(bus::getAccessRestriction); + header.setSupportsIOModeSupplier(bus::supportsIOMode); + + // Upgrade cards + CardsDisplay cards = createBusCards(bus, y); + if (cards != null) header.setCardsDisplay(cards); + + // Rename info: header handles right-click directly via InlineRenameManager + header.setRenameInfo(bus, GuiConstants.GUI_INDENT + 20 - 2, 0, GuiConstants.BUTTON_IO_MODE_X - 4); + header.setOnNameDoubleClick(() -> guiContext.highlightInWorld( + bus.getPos(), bus.getDimension(), bus.getLocalizedName()), + DoubleClickTracker.storageBusTargetId(bus.getId())); + header.setOnExpandToggle(() -> { + TabStateManager.getInstance().toggleBusExpanded(tabType, bus.getId()); + guiContext.rebuildAndUpdateScrollbar(); + }); + header.setOnIOModeClick(() -> + guiContext.sendPacket(new PacketStorageBusIOMode(bus.getId()))); + + // Header selection for quick-add (partition tab only) + if (isPartitionMode) { + header.setOnHeaderClick(() -> { + long busId = bus.getId(); + Set selected = guiContext.getSelectedStorageBusIds(); + if (selected.contains(busId)) { + selected.remove(busId); + } else { + selected.add(busId); + } + }); + header.setSelectedSupplier(() -> guiContext.getSelectedStorageBusIds().contains(bus.getId())); + } + + // Priority field: header registers its own field with the singleton during draw + header.setPrioritizable(bus); + header.setGuiOffsets(guiLeft, guiTop); + + return header; + } + + // ---- Content line creation ---- + + private IWidget createContentLine(StorageBusContentRow row, int y) { + StorageBusInfo bus = row.getStorageBus(); + + if (row.isFirstRow()) { + return createFirstRow(bus, row.getStartIndex(), y); + } + + return createContinuationRow(bus, row.getStartIndex(), y); + } + + /** + * First content row: SlotsLine with tree junction button. + */ + private SlotsLine createFirstRow(StorageBusInfo bus, int startIndex, int y) { + SlotsLine line = new SlotsLine( + y, SLOTS_PER_ROW, SLOTS_X_OFFSET, slotMode, + startIndex, fontRenderer, itemRender + ); + + configureSlotData(line, bus); + + // Tree junction button + SmallButton treeBtn = new SmallButton(0, 0, treeButtonType, () -> { + if (isPartitionMode) { + guiContext.sendPacket(new PacketStorageBusPartitionAction( + bus.getId(), PacketStorageBusPartitionAction.Action.CLEAR_ALL)); + } else { + guiContext.sendPacket(new PacketStorageBusPartitionAction( + bus.getId(), PacketStorageBusPartitionAction.Action.SET_ALL_FROM_CONTENTS)); + } + }); + line.setTreeButton(treeBtn); + line.setTreeButtonXOffset(-3); // 2px to the right vs cells for tighter storage bus layout + + line.setGuiOffsets(guiLeft, guiTop); + + // Selection highlight (partition mode only) + if (isPartitionMode) { + line.setSelectedSupplier(() -> guiContext.getSelectedStorageBusIds().contains(bus.getId())); + } + + return line; + } + + /** + * Continuation row (not the first row for this bus). + */ + private ContinuationLine createContinuationRow(StorageBusInfo bus, int startIndex, int y) { + ContinuationLine line = new ContinuationLine( + y, SLOTS_PER_ROW, SLOTS_X_OFFSET, slotMode, + startIndex, fontRenderer, itemRender + ); + + configureSlotData(line, bus); + line.setGuiOffsets(guiLeft, guiTop); + + // Selection highlight (partition mode only) + if (isPartitionMode) { + line.setSelectedSupplier(() -> guiContext.getSelectedStorageBusIds().contains(bus.getId())); + } + + return line; + } + + /** + * Configure content/partition data suppliers on a SlotsLine. + */ + private void configureSlotData(SlotsLine line, StorageBusInfo bus) { + if (slotMode == SlotsLine.SlotMode.CONTENT) { + line.setItemsSupplier(bus::getContents); + line.setPartitionSupplier(bus::getPartition); + line.setCountProvider(() -> bus::getContentCount); + } else { + line.setItemsSupplier(bus::getPartition); + line.setMaxSlots(GuiConstants.MAX_STORAGE_BUS_PARTITION_SLOTS); + } + + line.setSlotClickCallback((slotIndex, mouseButton) -> { + if (mouseButton != 0) return; + + if (isPartitionMode) { + ItemStack heldStack = guiContext.getHeldStack(); + List partition = bus.getPartition(); + boolean slotOccupied = slotIndex < partition.size() && !partition.get(slotIndex).isEmpty(); + + if (!heldStack.isEmpty()) { + guiContext.sendPacket(new PacketStorageBusPartitionAction( + bus.getId(), PacketStorageBusPartitionAction.Action.ADD_ITEM, + slotIndex, heldStack)); + } else if (slotOccupied) { + guiContext.sendPacket(new PacketStorageBusPartitionAction( + bus.getId(), PacketStorageBusPartitionAction.Action.REMOVE_ITEM, slotIndex)); + } + } else { + // Content mode: toggle partition for content item + List contents = bus.getContents(); + if (slotIndex < contents.size() && !contents.get(slotIndex).isEmpty()) { + guiContext.sendPacket(new PacketStorageBusPartitionAction( + bus.getId(), PacketStorageBusPartitionAction.Action.TOGGLE_ITEM, + contents.get(slotIndex))); + } + } + }); + } + + // ---- Keybind handling ---- + + /** + * Handle the add-to-storage-bus keybind. + * Adds the hovered item to the partition of all selected storage buses. + * Converts items for fluid/essentia buses, finds empty slots. + * + * @param selectedBusIds The set of selected storage bus IDs + * @param hoveredSlot The slot the mouse is over (or null) + * @param storageBusMap Map of storage bus IDs to info + * @return true if the keybind was handled + */ + private static boolean handleAddToStorageBusKeybind(Set selectedBusIds, + Slot hoveredSlot, + Map storageBusMap) { + if (selectedBusIds.isEmpty()) { + if (Minecraft.getMinecraft().player != null) { + MessageHelper.warning("cellterminal.storage_bus.no_selection"); + } + + return true; + } + + // Try to get item from inventory slot first + ItemStack stack = ItemStack.EMPTY; + + if (hoveredSlot != null && hoveredSlot.getHasStack()) { + stack = hoveredSlot.getStack(); + } + + // If no inventory item, try JEI/bookmark + if (stack.isEmpty()) { + QuickPartitionHandler.HoveredIngredient jeiItem = QuickPartitionHandler.getHoveredIngredient(); + if (jeiItem != null && !jeiItem.stack.isEmpty()) stack = jeiItem.stack; + } + + if (stack.isEmpty()) { + if (Minecraft.getMinecraft().player != null) { + MessageHelper.warning("cellterminal.storage_bus.no_item"); + } + + return true; + } + + // Add to all selected storage buses + int successCount = 0; + int invalidItemCount = 0; + int noSlotCount = 0; + + for (Long busId : selectedBusIds) { + StorageBusInfo storageBus = storageBusMap.get(busId); + if (storageBus == null) continue; + + // Convert the item for non-item bus types first to check validity + ItemStack stackToSend = stack; + boolean validForBusType = true; + + if (storageBus.isFluid()) { + // For fluid buses, need FluidDummyItem or fluid container + if (!(stack.getItem() instanceof FluidDummyItem)) { + FluidStack fluid = net.minecraftforge.fluids.FluidUtil.getFluidContained(stack); + // Can't use this item on fluid bus + if (fluid == null) { + invalidItemCount++; + validForBusType = false; + } + } + } else if (storageBus.isEssentia()) { + // For essentia buses, need ItemDummyAspect or essentia container + ItemStack essentiaRep = ThaumicEnergisticsIntegration.tryConvertEssentiaContainerToAspect(stack); + // Can't use this item on essentia bus + if (essentiaRep.isEmpty()) { + invalidItemCount++; + validForBusType = false; + } else { + stackToSend = essentiaRep; + } + } + + if (!validForBusType) continue; + + // Find first empty slot in this storage bus + List partition = storageBus.getPartition(); + int availableSlots = storageBus.getAvailableConfigSlots(); + int targetSlot = -1; + + for (int i = 0; i < availableSlots; i++) { + if (i >= partition.size() || partition.get(i).isEmpty()) { + targetSlot = i; + break; + } + } + + if (targetSlot < 0) { + noSlotCount++; + continue; + } + + CellTerminalNetwork.INSTANCE.sendToServer( + new PacketStorageBusPartitionAction( + busId, + PacketStorageBusPartitionAction.Action.ADD_ITEM, + targetSlot, + stackToSend + ) + ); + successCount++; + } + + if (successCount == 0 && Minecraft.getMinecraft().player != null) { + // Show appropriate error message based on what failed + if (invalidItemCount > 0 && noSlotCount == 0) { + MessageHelper.error("cellterminal.storage_bus.invalid_item"); + } else if (noSlotCount > 0 && invalidItemCount == 0) { + MessageHelper.error("cellterminal.storage_bus.partition_full"); + } else { + // Mixed: some were invalid, some were full + MessageHelper.error("cellterminal.storage_bus.partition_full"); + } + } + + return true; + } + + // ---- Cards helper ---- + + private CardsDisplay createBusCards(StorageBusInfo bus, int rowY) { + List entries = buildCardEntries(bus); + if (entries.isEmpty()) return null; + + // Position cards at left margin (x=3) + CardsDisplay cards = new CardsDisplay(GuiConstants.CARDS_X, rowY, () -> entries, itemRender); + + cards.setClickCallback(slotIndex -> handleBusCardClick(bus, slotIndex)); + + return cards; + } + + private List buildCardEntries(StorageBusInfo bus) { + List entries = new ArrayList<>(); + int slotCount = bus.getUpgradeSlotCount(); + + // Build a slot-indexed array so empty slots are visually present + ItemStack[] slotStacks = new ItemStack[slotCount]; + java.util.Arrays.fill(slotStacks, ItemStack.EMPTY); + for (int i = 0; i < bus.getUpgrades().size(); i++) { + int slotIdx = bus.getUpgradeSlotIndex(i); + if (slotIdx >= 0 && slotIdx < slotCount) { + slotStacks[slotIdx] = bus.getUpgrades().get(i); + } + } + + for (int i = 0; i < slotCount; i++) { + entries.add(new CardsDisplay.CardEntry(slotStacks[i], i)); + } + + return entries; + } + + // ---- Upgrade card click handling ---- + + private void handleBusCardClick(StorageBusInfo bus, int upgradeSlotIndex) { + if (CellTerminalServerConfig.isInitialized() + && !CellTerminalServerConfig.getInstance().isUpgradeExtractEnabled()) { + guiContext.showError("cellterminal.error.upgrade_extract_disabled"); + return; + } + + boolean toInventory = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + guiContext.sendPacket(PacketExtractUpgrade.forStorageBus( + bus.getId(), upgradeSlotIndex, toInventory)); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/tab/SubnetOverviewTabWidget.java b/src/main/java/com/cellterminal/gui/widget/tab/SubnetOverviewTabWidget.java new file mode 100644 index 0000000..882f57d --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/tab/SubnetOverviewTabWidget.java @@ -0,0 +1,648 @@ +package com.cellterminal.gui.widget.tab; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.client.resources.I18n; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; + +import net.minecraftforge.common.util.Constants; + +import mezz.jei.api.gui.IGhostIngredientHandler; + +import com.cellterminal.client.SubnetConnectionEntry; +import com.cellterminal.client.SubnetConnectionRow; +import com.cellterminal.client.SubnetInfo; +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.handler.JeiGhostHandler; +import com.cellterminal.gui.handler.TerminalDataManager; +import com.cellterminal.gui.widget.DoubleClickTracker; +import com.cellterminal.gui.widget.IWidget; +import com.cellterminal.gui.widget.button.ButtonType; +import com.cellterminal.gui.widget.button.SmallButton; +import com.cellterminal.gui.widget.header.AbstractHeader; +import com.cellterminal.gui.widget.header.SubnetHeader; +import com.cellterminal.gui.widget.line.AbstractLine; +import com.cellterminal.gui.widget.line.ContinuationLine; +import com.cellterminal.gui.widget.line.SlotsLine; +import com.cellterminal.network.CellTerminalNetwork; +import com.cellterminal.network.PacketHighlightBlock; +import com.cellterminal.network.PacketSubnetAction; +import com.cellterminal.network.PacketSubnetPartitionAction; +import com.cellterminal.gui.overlay.MessageHelper; + + +/** + * Tab widget for the subnet overview mode. + *

+ * This is a pseudo-tab: it has no tab button in the tab bar, but it uses the + * full widget infrastructure (proper IWidget rows, tree line propagation, + * InlineRenameManager, DoubleClickTracker) just like any real tab. + *

+ * Layout follows the same pattern as {@link TempAreaTabWidget}: + *

    + *
  • {@link SubnetInfo} → {@link SubnetHeader} (main network only, no connections)
  • + *
  • {@link SubnetConnectionEntry} → {@link SubnetHeader} (per connection, with direction arrow)
  • + *
  • {@link SubnetConnectionRow} (content) → {@link SlotsLine}/{@link ContinuationLine}
  • + *
  • {@link SubnetConnectionRow} (partition) → {@link SlotsLine}/{@link ContinuationLine}
  • + *
+ *

+ * Activated via the back button. + */ +public class SubnetOverviewTabWidget extends AbstractTabWidget { + + private static final int SLOTS_PER_ROW = 9; + private static final int SLOTS_X_OFFSET = GuiConstants.CELL_INDENT + 4; + + // Subnet data + private final List subnetList = new ArrayList<>(); + private final List subnetLines = new ArrayList<>(); + + // Context for GUI-level operations + private SubnetOverviewContext subnetContext; + + /** + * Comparator for sorting subnets: main network first, then favorites, then by dimension and distance. + */ + private static final Comparator SUBNET_COMPARATOR = (a, b) -> { + // Main network always first + if (a.isMainNetwork()) return -1; + if (b.isMainNetwork()) return 1; + + // Favorites next + if (a.isFavorite() != b.isFavorite()) return a.isFavorite() ? -1 : 1; + + // Then by dimension + if (a.getDimension() != b.getDimension()) { + return Integer.compare(a.getDimension(), b.getDimension()); + } + + // Then by distance from origin + double distA = a.getPrimaryPos().distanceSq(0, 0, 0); + double distB = b.getPrimaryPos().distanceSq(0, 0, 0); + + return Double.compare(distA, distB); + }; + + /** + * Callback interface for GUI-level operations that this pseudo-tab cannot perform directly. + */ + public interface SubnetOverviewContext { + + /** Switch the terminal to view a different network. */ + void switchToNetwork(long networkId); + + /** Request fresh subnet list from the server. */ + void requestSubnetList(); + } + + public SubnetOverviewTabWidget(FontRenderer fontRenderer, RenderItem itemRender) { + super(fontRenderer, itemRender); + } + + /** + * Set the subnet-specific context for GUI callbacks. + * Must be called during initialization (after init(GuiContext)). + */ + public void setSubnetContext(SubnetOverviewContext context) { + this.subnetContext = context; + } + + // ======================================================================== + // Subnet data management + // ======================================================================== + + /** + * Handle subnet list update from server. + * Parses the NBT data, sorts subnets, and rebuilds flattened display lines. + */ + public void handleSubnetListUpdate(NBTTagCompound data) { + this.subnetList.clear(); + + // Always add the main network as first entry + this.subnetList.add(SubnetInfo.createMainNetwork()); + + if (data.hasKey("subnets")) { + NBTTagList subnetNbtList = data.getTagList("subnets", Constants.NBT.TAG_COMPOUND); + for (int i = 0; i < subnetNbtList.tagCount(); i++) { + NBTTagCompound subnetNbt = subnetNbtList.getCompoundTagAt(i); + this.subnetList.add(new SubnetInfo(subnetNbt)); + } + } + + this.subnetList.sort(SUBNET_COMPARATOR); + buildSubnetLines(); + } + + /** + * Build the flattened subnet lines list from the sorted subnet list. + *

+ * Each connection gets its own header ({@link SubnetConnectionEntry}), + * followed by content rows and partition rows, just like + * TempArea has TempCellInfo → CellContentRow(content) → CellContentRow(partition). + */ + private void buildSubnetLines() { + this.subnetLines.clear(); + + for (SubnetInfo subnet : this.subnetList) { + // Main network gets a single header with no connections + if (subnet.isMainNetwork()) { + this.subnetLines.add(subnet); + continue; + } + + // Each connection gets its own header + content/partition rows + for (int connIdx = 0; connIdx < subnet.getConnections().size(); connIdx++) { + SubnetInfo.ConnectionPoint conn = subnet.getConnections().get(connIdx); + this.subnetLines.add(new SubnetConnectionEntry(subnet, conn, connIdx)); + + // Content + partition rows (like TempArea's CellContentRow with isPartitionRow flag) + List rows = + SubnetInfo.buildConnectionContentRows(subnet, conn, connIdx, SLOTS_PER_ROW); + this.subnetLines.addAll(rows); + } + } + } + + /** + * Called when entering subnet overview mode. + * Rebuilds lines from existing data (if any) for immediate display, + * and requests fresh data from the server. + */ + public void onEnterOverview() { + // Rebuild lines from existing data for immediate display (avoids flicker) + if (!this.subnetList.isEmpty()) buildSubnetLines(); + + // Request fresh subnet list from server + if (subnetContext != null) subnetContext.requestSubnetList(); + } + + // ======================================================================== + // Row widget creation (mirrors TempAreaTabWidget pattern) + // ======================================================================== + + @Override + protected IWidget createRowWidget(Object lineData, int y, List allLines, int lineIndex) { + // Main network header (SubnetInfo directly, no connection) + if (lineData instanceof SubnetInfo) { + return createMainNetworkHeader((SubnetInfo) lineData, y); + } + + // Per-connection header (with direction arrow) + if (lineData instanceof SubnetConnectionEntry) { + return createConnectionHeader((SubnetConnectionEntry) lineData, y); + } + + // Content or partition row (SlotsLine / ContinuationLine) + if (lineData instanceof SubnetConnectionRow) { + return createContentLine((SubnetConnectionRow) lineData, y); + } + + return null; + } + + @Override + protected boolean isContentLine(List allLines, int index) { + if (index < 0 || index >= allLines.size()) return false; + + return allLines.get(index) instanceof SubnetConnectionRow; + } + + /** + * Check if a row at the given index is a partition row (vs content row). + * Only meaningful for SubnetConnectionRow entries. + */ + private boolean isPartitionRow(List allLines, int index) { + if (index < 0 || index >= allLines.size()) return false; + + Object line = allLines.get(index); + return (line instanceof SubnetConnectionRow) && ((SubnetConnectionRow) line).isPartitionRow(); + } + + // ---- Tree line propagation (dual content/partition cut-Y, mirrors TempAreaTabWidget) ---- + + @Override + protected void propagateTreeLines(List allLines, int scrollOffset) { + int lastContentCutY = GuiConstants.CONTENT_START_Y; + int lastPartitionCutY = GuiConstants.CONTENT_START_Y; + boolean hasContentAbove = scrollOffset > 0 && isContentLine(allLines, scrollOffset - 1); + + // Track whether any non-partition content rows appeared since the last header. + // When false at the first partition row, the partition connects directly to the header + // rather than using a zero-length self-referencing vertical line (which would leave a gap). + boolean hadContentInSection = false; + + for (int i = 0; i < visibleRows.size(); i++) { + IWidget widget = visibleRows.get(i); + int lineIndex = scrollOffset + i; + + if (widget instanceof AbstractHeader) { + AbstractHeader header = (AbstractHeader) widget; + // If no content rows below but partition rows exist, connector still needed + boolean hasAnyContentBelow = (lineIndex + 1) < allLines.size() + && isContentLine(allLines, lineIndex + 1); + header.setDrawConnector(hasAnyContentBelow); + lastContentCutY = header.getConnectorY(); + // Reset partition cut Y and content tracking for each new header (new connection) + lastPartitionCutY = GuiConstants.CONTENT_START_Y; + hadContentInSection = false; + + } else if (widget instanceof AbstractLine) { + AbstractLine line = (AbstractLine) widget; + boolean currentIsPartition = isPartitionRow(allLines, lineIndex); + boolean prevIsPartition = lineIndex > 0 && isPartitionRow(allLines, lineIndex - 1); + + if (currentIsPartition) { + if (!prevIsPartition) { + if (hadContentInSection) { + // First partition row after content rows: zero-length vertical line + // (visual break between content and partition sections) + line.setTreeLineParams(true, line.getY() + 5); + } else { + // First partition row with NO content rows above (directly after header): + // connect to the header's connector Y so the tree line reaches up + line.setTreeLineParams(true, lastContentCutY); + } + } else { + // Continuation partition row: draw tree line connecting to previous partition + line.setTreeLineParams(true, lastPartitionCutY); + } + lastPartitionCutY = line.getTreeLineCutY(); + } else if (i == 0 && hasContentAbove) { + // First visible row with content above + line.setTreeLineParams(true, GuiConstants.CONTENT_START_Y); + lastContentCutY = line.getTreeLineCutY(); + hadContentInSection = true; + } else { + line.setTreeLineParams(true, lastContentCutY); + lastContentCutY = line.getTreeLineCutY(); + hadContentInSection = true; + } + } + } + } + + // ---- Header creation ---- + + /** + * Create a SubnetHeader for the main network entry (no connections, no arrow). + */ + private SubnetHeader createMainNetworkHeader(SubnetInfo subnet, int y) { + SubnetHeader header = new SubnetHeader(y, fontRenderer, itemRender, true); + + header.setNameSupplier(subnet::getDisplayName); + header.setHasCustomNameSupplier(subnet::hasCustomName); + header.setIsFavoriteSupplier(subnet::isFavorite); + header.setCanLoadSupplier(() -> true); + + // Load button: switch to main network + header.setOnLoadClick(() -> { + if (subnetContext != null) subnetContext.switchToNetwork(0); + }); + + return header; + } + + /** + * Create a SubnetHeader for a connection entry (with direction arrow, per-connection). + */ + private SubnetHeader createConnectionHeader(SubnetConnectionEntry entry, int y) { + SubnetInfo subnet = entry.getSubnet(); + SubnetInfo.ConnectionPoint conn = entry.getConnection(); + SubnetHeader header = new SubnetHeader(y, fontRenderer, itemRender); + + header.setNameSupplier(subnet::getDisplayName); + header.setHasCustomNameSupplier(subnet::hasCustomName); + header.setIsFavoriteSupplier(subnet::isFavorite); + header.setCanLoadSupplier(() -> subnet.isAccessible() && subnet.hasPower()); + + // Direction arrow (→ for outbound, ← for inbound) + header.setDirectionSupplier(conn::isOutbound); + + // Icon: the local part's icon (Storage Bus for outbound, Interface for inbound) + header.setIconSupplier(conn::getLocalIcon); + + // Location text: connection position and dimension + header.setLocationSupplier(() -> I18n.format("cellterminal.subnet.pos", + conn.getPos().getX(), conn.getPos().getY(), conn.getPos().getZ(), conn.getDimension())); + + // Star click: toggle favorite for the whole subnet, re-sort and rebuild + header.setOnStarClick(() -> { + boolean newFavorite = !subnet.isFavorite(); + subnet.setFavorite(newFavorite); + CellTerminalNetwork.INSTANCE.sendToServer( + PacketSubnetAction.toggleFavorite(subnet.getId(), newFavorite)); + this.subnetList.sort(SUBNET_COMPARATOR); + buildSubnetLines(); + if (guiContext != null) guiContext.rebuildAndUpdateScrollbar(); + }); + + // Load button: switch to this subnet's network + header.setOnLoadClick(() -> { + if (subnetContext != null) subnetContext.switchToNetwork(subnet.getId()); + }); + + // Rename info: InlineRenameManager handles right-click on name + header.setRenameInfo(subnet, GuiConstants.HEADER_NAME_X - 2, 0, + GuiConstants.CONTENT_RIGHT_EDGE - fontRenderer.getStringWidth( + I18n.format("cellterminal.subnet.load")) - 12); + + // Double-click to highlight connection position in world + header.setOnNameDoubleClick( + () -> highlightConnectionInWorld(subnet, conn), + DoubleClickTracker.subnetTargetId(subnet.getId() + entry.getConnectionIndex())); + + return header; + } + + // ---- Content / Partition line creation (mirrors TempAreaTabWidget.createContentLine) ---- + + /** + * Create a SlotsLine or ContinuationLine for a content/partition row. + */ + private IWidget createContentLine(SubnetConnectionRow row, int y) { + boolean isPartition = row.isPartitionRow(); + SlotsLine.SlotMode mode = isPartition ? SlotsLine.SlotMode.PARTITION : SlotsLine.SlotMode.CONTENT; + + if (row.isFirstRow()) return createFirstRow(row, mode, isPartition, y); + + return createContinuationRow(row, mode, y); + } + + /** + * First content or partition row: SlotsLine with tree junction button. + * Content first row gets DO_PARTITION, partition first row gets CLEAR_PARTITION. + */ + private SlotsLine createFirstRow(SubnetConnectionRow row, SlotsLine.SlotMode mode, + boolean isPartition, int y) { + SlotsLine line = new SlotsLine( + y, SLOTS_PER_ROW, SLOTS_X_OFFSET, mode, + row.getStartIndex(), fontRenderer, itemRender + ); + + configureSlotData(line, row, mode); + + // Tree junction button + ButtonType buttonType = isPartition ? ButtonType.CLEAR_PARTITION : ButtonType.DO_PARTITION; + SubnetInfo.ConnectionPoint conn = row.getConnection(); + SmallButton treeBtn = new SmallButton(0, 0, buttonType, () -> { + if (isPartition) { + guiContext.sendPacket(new PacketSubnetPartitionAction( + row.getSubnet().getId(), conn.getPos().toLong(), conn.getSide().ordinal(), + PacketSubnetPartitionAction.Action.CLEAR_ALL)); + } else if (row.usesSubnetInventory()) { + // Outbound connection: set partition from the subnet's entire ME storage + guiContext.sendPacket(new PacketSubnetPartitionAction( + row.getSubnet().getId(), conn.getPos().toLong(), conn.getSide().ordinal(), + PacketSubnetPartitionAction.Action.SET_ALL_FROM_SUBNET_INVENTORY)); + } else { + guiContext.sendPacket(new PacketSubnetPartitionAction( + row.getSubnet().getId(), conn.getPos().toLong(), conn.getSide().ordinal(), + PacketSubnetPartitionAction.Action.SET_ALL_FROM_CONTENTS)); + } + }); + line.setTreeButton(treeBtn); + line.setGuiOffsets(guiLeft, guiTop); + + return line; + } + + /** + * Continuation row (not the first for this content/partition section). + */ + private ContinuationLine createContinuationRow(SubnetConnectionRow row, + SlotsLine.SlotMode mode, int y) { + ContinuationLine line = new ContinuationLine( + y, SLOTS_PER_ROW, SLOTS_X_OFFSET, mode, + row.getStartIndex(), fontRenderer, itemRender + ); + + configureSlotData(line, row, mode); + line.setGuiOffsets(guiLeft, guiTop); + + return line; + } + + /** + * Configure content/partition data suppliers on a SlotsLine. + */ + private void configureSlotData(SlotsLine line, SubnetConnectionRow row, SlotsLine.SlotMode mode) { + SubnetInfo.ConnectionPoint conn = row.getConnection(); + SubnetInfo subnet = row.getSubnet(); + + if (mode == SlotsLine.SlotMode.CONTENT) { + if (row.usesSubnetInventory()) { + // Outbound connection: content is the subnet's ME storage + line.setItemsSupplier(subnet::getInventory); + line.setCountProvider(() -> subnet::getInventoryCount); + } else { + line.setItemsSupplier(conn::getContent); + } + line.setPartitionSupplier(conn::getPartition); + } else { + line.setItemsSupplier(conn::getPartition); + line.setMaxSlots(conn.getMaxPartitionSlots()); + } + + line.setSlotClickCallback((slotIndex, mouseButton) -> { + if (mouseButton != 0) return; + + if (mode == SlotsLine.SlotMode.CONTENT) { + // Content slot: toggle partition for that item + List contents = row.usesSubnetInventory() + ? subnet.getInventory() : conn.getContent(); + if (slotIndex < contents.size() && !contents.get(slotIndex).isEmpty()) { + guiContext.sendPacket(new PacketSubnetPartitionAction( + subnet.getId(), conn.getPos().toLong(), conn.getSide().ordinal(), + PacketSubnetPartitionAction.Action.TOGGLE_ITEM, + contents.get(slotIndex))); + } + } else { + // Partition slot: add/remove item + ItemStack heldStack = guiContext.getHeldStack(); + List partition = conn.getPartition(); + boolean slotOccupied = slotIndex < partition.size() && !partition.get(slotIndex).isEmpty(); + + if (!heldStack.isEmpty()) { + guiContext.sendPacket(new PacketSubnetPartitionAction( + subnet.getId(), conn.getPos().toLong(), conn.getSide().ordinal(), + PacketSubnetPartitionAction.Action.ADD_ITEM, + slotIndex, heldStack)); + } else if (slotOccupied) { + guiContext.sendPacket(new PacketSubnetPartitionAction( + subnet.getId(), conn.getPos().toLong(), conn.getSide().ordinal(), + PacketSubnetPartitionAction.Action.REMOVE_ITEM, slotIndex)); + } + } + }); + } + + // ---- JEI ghost target integration ---- + + /** + * Get JEI ghost ingredient targets for partition slots in the subnet overview. + * Wraps visible partition SlotsLine targets into proper IGhostIngredientHandler.Target + * instances that send PacketSubnetPartitionAction.ADD_ITEM on accept. + */ + @Override + public List> getPhantomTargets(Object ingredient) { + List> targets = new ArrayList<>(); + + for (Map.Entry entry : getWidgetDataMap().entrySet()) { + IWidget widget = entry.getKey(); + Object data = entry.getValue(); + if (!(widget instanceof SlotsLine)) continue; + + SlotsLine slotsLine = (SlotsLine) widget; + List slotTargets = slotsLine.getPartitionTargets(); + if (slotTargets.isEmpty()) continue; + if (!(data instanceof SubnetConnectionRow)) continue; + + SubnetConnectionRow row = (SubnetConnectionRow) data; + if (!row.isPartitionRow()) continue; + + SubnetInfo subnet = row.getSubnet(); + SubnetInfo.ConnectionPoint conn = row.getConnection(); + + for (SlotsLine.PartitionSlotTarget slot : slotTargets) { + targets.add(new IGhostIngredientHandler.Target() { + @Override + public Rectangle getArea() { + return new Rectangle(slot.absX, slot.absY, slot.width, slot.height); + } + + @Override + public void accept(Object ing) { + // Subnet connections are always item-based storage buses for now + ItemStack stack = JeiGhostHandler.convertJeiIngredientForStorageBus( + ing, false, false); + if (!stack.isEmpty()) { + guiContext.sendPacket(new PacketSubnetPartitionAction( + subnet.getId(), conn.getPos().toLong(), conn.getSide().ordinal(), + PacketSubnetPartitionAction.Action.ADD_ITEM, + slot.absoluteIndex, stack)); + } + } + }); + } + } + + return targets; + } + + /** + * Get phantom targets for JEI ghost ingredient support on partition slots. + * Returns SlotsLine.PartitionSlotTarget instances for all visible partition rows. + */ + public List getPartitionTargets() { + List targets = new ArrayList<>(); + + for (IWidget widget : visibleRows) { + if (widget instanceof SlotsLine) { + SlotsLine slotsLine = (SlotsLine) widget; + targets.addAll(slotsLine.getPartitionTargets()); + } + } + + return targets; + } + + // ---- Utility ---- + + /** + * Highlight a connection point's position in the world. + */ + private void highlightConnectionInWorld(SubnetInfo subnet, SubnetInfo.ConnectionPoint conn) { + if (subnet == null || subnet.isMainNetwork()) return; + + if (subnet.getDimension() != Minecraft.getMinecraft().player.dimension) { + MessageHelper.error("cellterminal.error.different_dimension"); + return; + } + + CellTerminalNetwork.INSTANCE.sendToServer( + new PacketHighlightBlock(conn.getPos(), subnet.getDimension()) + ); + + MessageHelper.success("gui.cellterminal.highlighted", + conn.getPos().getX(), + conn.getPos().getY(), + conn.getPos().getZ(), + subnet.getDisplayName()); + } + + // ======================================================================== + // Tab overrides: Data and metadata + // ======================================================================== + + /** + * Return the flattened subnet lines for scrollbar calculation. + * The data manager is not used as subnet data comes from handleSubnetListUpdate. + */ + @Override + public List getLines(TerminalDataManager dataManager) { + return subnetLines; + } + + @Override + public List getHelpLines() { + List lines = new ArrayList<>(); + + lines.add(I18n.format("cellterminal.subnet.controls.title")); + lines.add(""); + lines.add(I18n.format("cellterminal.subnet.controls.click")); + lines.add(I18n.format("cellterminal.subnet.controls.dblclick")); + lines.add(I18n.format("cellterminal.subnet.controls.star")); + lines.add(I18n.format("cellterminal.subnet.controls.rename")); + lines.add(I18n.format("cellterminal.subnet.controls.esc")); + + return lines; + } + + /** + * No tab button for this pseudo-tab. + */ + @Override + public ItemStack getTabIcon() { + return ItemStack.EMPTY; + } + + /** + * No tooltip for this pseudo-tab (no tab button). + */ + @Override + public String getTabTooltip() { + return ""; + } + + /** + * No search mode button needed in subnet overview. + */ + @Override + public boolean showSearchModeButton() { + return false; + } + + // ======================================================================== + // Accessors + // ======================================================================== + + /** Get the subnet list (for external queries). */ + public List getSubnetList() { + return Collections.unmodifiableList(subnetList); + } + + /** Check if the subnet list is empty (no data received yet). */ + public boolean hasSubnetData() { + return !subnetList.isEmpty(); + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/tab/TempAreaTabWidget.java b/src/main/java/com/cellterminal/gui/widget/tab/TempAreaTabWidget.java new file mode 100644 index 0000000..fa7f59a --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/tab/TempAreaTabWidget.java @@ -0,0 +1,747 @@ +package com.cellterminal.gui.widget.tab; + +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.lwjgl.input.Keyboard; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.client.resources.I18n; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraftforge.fluids.FluidStack; + +import appeng.api.AEApi; +import appeng.fluids.items.FluidDummyItem; + +import mezz.jei.api.gui.IGhostIngredientHandler; + +import com.cellterminal.client.CellContentRow; +import com.cellterminal.client.CellInfo; +import com.cellterminal.client.KeyBindings; +import com.cellterminal.client.SearchFilterMode; +import com.cellterminal.client.TempCellInfo; +import com.cellterminal.config.CellTerminalServerConfig; +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.handler.JeiGhostHandler; +import com.cellterminal.gui.handler.TerminalDataManager; +import com.cellterminal.gui.handler.QuickPartitionHandler; +import com.cellterminal.gui.overlay.MessageHelper; +import com.cellterminal.gui.widget.CardsDisplay; +import com.cellterminal.gui.widget.IWidget; +import com.cellterminal.gui.widget.button.ButtonType; +import com.cellterminal.gui.widget.button.SmallButton; +import com.cellterminal.gui.widget.header.AbstractHeader; +import com.cellterminal.gui.widget.header.TempAreaHeader; +import com.cellterminal.gui.widget.line.AbstractLine; +import com.cellterminal.gui.widget.line.ContinuationLine; +import com.cellterminal.gui.widget.line.SlotsLine; +import com.cellterminal.integration.ThaumicEnergisticsIntegration; +import com.cellterminal.network.CellTerminalNetwork; +import com.cellterminal.network.PacketExtractUpgrade; +import com.cellterminal.network.PacketTempCellAction; +import com.cellterminal.network.PacketTempCellPartitionAction; + + +/** + * Tab widget for the Temp Area tab (Tab 3). + *

+ * The temp area shows temporary cell storage slots. Each slot has a header + * (with a cell drop zone and Send button) followed by interleaved content + * and partition rows for the inserted cell. + * + *

Line list structure

+ *
+ * TempCellInfo → TempAreaHeader (cell slot + Send button)
+ * ├─ CellContentRow (content, first) → SlotsLine + DO_PARTITION button
+ * ├─ CellContentRow (content, continuation) → ContinuationLine
+ * ├─ CellContentRow (partition, first) → SlotsLine + CLEAR_PARTITION button
+ * └─ CellContentRow (partition, continuation) → ContinuationLine
+ * [next TempCellInfo...]
+ * 
+ * + * The temp area uses 9 slots per row (same as storage bus tabs) at a narrower + * X offset since there is no inline cell slot in the content rows. + */ +public class TempAreaTabWidget extends AbstractTabWidget { + + /** Slots per row for temp area: 9 (matches storage bus layout) */ + private static final int SLOTS_PER_ROW = GuiConstants.STORAGE_BUS_SLOTS_PER_ROW; + + /** X offset for content/partition slots (no inline cell slot) */ + private static final int SLOTS_X_OFFSET = GuiConstants.CELL_INDENT + 4; + + public TempAreaTabWidget(FontRenderer fontRenderer, RenderItem itemRender) { + super(fontRenderer, itemRender); + } + + // ---- Tab controller methods ---- + + @Override + public List getLines(TerminalDataManager dataManager) { + return dataManager.getTempAreaLines(); + } + + @Override + public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { + // Temp area respects the user's selected search mode + return userSelectedMode; + } + + @Override + public boolean showSearchModeButton() { + return true; + } + + @Override + public List getHelpLines() { + List lines = new ArrayList<>(); + + lines.add(I18n.format("gui.cellterminal.controls.temp_area.drag_cell")); + lines.add(I18n.format("gui.cellterminal.controls.temp_area.send_cell")); + lines.add(""); + lines.add(I18n.format("gui.cellterminal.controls.temp_area.add_key", + KeyBindings.ADD_TO_STORAGE_BUS.getDisplayName())); + lines.add(""); + lines.add(I18n.format("gui.cellterminal.controls.jei_drag")); + lines.add(I18n.format("gui.cellterminal.controls.click_to_remove")); + + return lines; + } + + @Override + public ItemStack getTabIcon() { + return AEApi.instance().definitions().items().cell64k() + .maybeStack(1).orElse(ItemStack.EMPTY); + } + + @Override + public String getTabTooltip() { + return I18n.format("gui.cellterminal.tab.temp_area.tooltip"); + } + + @Override + public boolean handleTabKeyTyped(int keyCode) { + if (!KeyBindings.ADD_TO_STORAGE_BUS.isActiveAndMatches(keyCode)) return false; + + return handleAddToTempCellKeybind( + guiContext.getSelectedTempCellSlots(), + guiContext.getSlotUnderMouse(), + getLines(guiContext.getDataManager())); + } + + // ---- JEI ghost targets ---- + + @Override + public List> getPhantomTargets(Object ingredient) { + List> targets = new ArrayList<>(); + + for (Map.Entry entry : getWidgetDataMap().entrySet()) { + IWidget widget = entry.getKey(); + Object data = entry.getValue(); + if (!(widget instanceof SlotsLine)) continue; + + SlotsLine slotsLine = (SlotsLine) widget; + List slotTargets = slotsLine.getPartitionTargets(); + if (slotTargets.isEmpty()) continue; + if (!(data instanceof CellContentRow)) continue; + + CellInfo cell = ((CellContentRow) data).getCell(); + int tempSlotIndex = findTempSlotIndexForCell(cell); + if (tempSlotIndex < 0) continue; + + for (SlotsLine.PartitionSlotTarget slot : slotTargets) { + targets.add(new IGhostIngredientHandler.Target() { + @Override + public Rectangle getArea() { + return new Rectangle(slot.absX, slot.absY, slot.width, slot.height); + } + + @Override + public void accept(Object ing) { + ItemStack stack = JeiGhostHandler.convertJeiIngredientToItemStack( + ing, cell.isFluid(), cell.isEssentia()); + if (!stack.isEmpty()) { + guiContext.sendPacket(new PacketTempCellPartitionAction( + tempSlotIndex, + PacketTempCellPartitionAction.Action.ADD_ITEM, + slot.absoluteIndex, stack)); + } + } + }); + } + } + + return targets; + } + + // ---- Row building ---- + + @Override + protected IWidget createRowWidget(Object lineData, int y, List allLines, int lineIndex) { + if (lineData instanceof TempCellInfo) { + return createTempAreaHeader((TempCellInfo) lineData, y); + } + + if (lineData instanceof CellContentRow) { + return createContentLine((CellContentRow) lineData, y); + } + + return null; + } + + @Override + protected boolean isContentLine(List allLines, int index) { + if (index < 0 || index >= allLines.size()) return false; + + return allLines.get(index) instanceof CellContentRow; + } + + /** + * Check if a line at the given index is a partition row (vs content row). + */ + private boolean isPartitionRow(List allLines, int index) { + if (index < 0 || index >= allLines.size()) return false; + + Object line = allLines.get(index); + if (line instanceof CellContentRow) return ((CellContentRow) line).isPartitionRow(); + + return false; + } + + /** + * Check if there's a non-partition content row below the given index. + * Used to determine whether to draw the header connector. + */ + private boolean hasNonPartitionContentBelow(List allLines, int headerIndex) { + int nextIndex = headerIndex + 1; + if (nextIndex >= allLines.size()) return false; + + Object next = allLines.get(nextIndex); + if (next instanceof CellContentRow) { + return !((CellContentRow) next).isPartitionRow(); + } + + return false; + } + + @Override + protected void propagateTreeLines(List allLines, int scrollOffset) { + // Track the "cut Y" from the previous row - separate for content and partition sections + int lastContentCutY = GuiConstants.CONTENT_START_Y; + int lastPartitionCutY = GuiConstants.CONTENT_START_Y; + boolean hasContentAbove = scrollOffset > 0 && isContentLine(allLines, scrollOffset - 1); + + for (int i = 0; i < visibleRows.size(); i++) { + IWidget widget = visibleRows.get(i); + int lineIndex = scrollOffset + i; + + if (widget instanceof AbstractHeader) { + AbstractHeader header = (AbstractHeader) widget; + // Only draw connector if non-partition content rows follow below + // The vertical line connects header to content, NOT to partition + boolean hasContentBelow = hasNonPartitionContentBelow(allLines, lineIndex); + header.setDrawConnector(hasContentBelow); + lastContentCutY = header.getConnectorY(); + // Reset partition cut Y for each new header (new temp cell) + lastPartitionCutY = GuiConstants.CONTENT_START_Y; + + } else if (widget instanceof AbstractLine) { + AbstractLine line = (AbstractLine) widget; + boolean currentIsPartition = isPartitionRow(allLines, lineIndex); + boolean prevIsPartition = lineIndex > 0 && isPartitionRow(allLines, lineIndex - 1); + + if (currentIsPartition) { + if (!prevIsPartition) { + // First partition row after content: draw horizontal line and button, but NO vertical line + // Set lineAboveCutY to row's own junction so vertical line has zero length + line.setTreeLineParams(true, line.getY() + 5); + } else { + // Continuation partition row: draw tree line connecting to previous partition + line.setTreeLineParams(true, lastPartitionCutY); + } + } else if (i == 0 && hasContentAbove) { + // First visible row with content above + line.setTreeLineParams(true, GuiConstants.CONTENT_START_Y); + } else { + line.setTreeLineParams(true, lastContentCutY); + } + + lastPartitionCutY = line.getTreeLineCutY(); + } + } + + // Draw a bottom continuation line if there is more content of the SAME TYPE + // below the visible window. Don't draw between content→partition transitions, + // as they are separate tree branches. + int lastVisibleIndex = scrollOffset + visibleRows.size() - 1; + boolean nextIsContent = lastVisibleIndex + 1 < allLines.size() + && isContentLine(allLines, lastVisibleIndex + 1); + + if (nextIsContent) { + boolean lastIsPartition = isPartitionRow(allLines, lastVisibleIndex); + boolean nextIsPartition = isPartitionRow(allLines, lastVisibleIndex + 1); + + // Only continue the line if both rows are the same type (both content or both partition) + if (lastIsPartition == nextIsPartition) { + bottomContinuationFromY = lastIsPartition ? lastPartitionCutY : lastContentCutY; + } else { + bottomContinuationFromY = -1; + } + } else { + bottomContinuationFromY = -1; + } + } + + // ---- TempAreaHeader creation ---- + + private TempAreaHeader createTempAreaHeader(TempCellInfo tempCell, int y) { + TempAreaHeader header = new TempAreaHeader(y, fontRenderer, itemRender); + header.setIconSupplier(tempCell::getCellStack); + header.setHasCellSupplier(tempCell::hasCell); + + // Name comes from the cell info when present + CellInfo cellInfo = tempCell.getCellInfo(); + if (cellInfo != null) { + header.setNameSupplier(cellInfo::getDisplayName); + header.setHasCustomNameSupplier(cellInfo::hasCustomName); + + // Upgrade cards + CardsDisplay cards = createCellCardsDisplay(cellInfo, y, this::handleCardClick); + if (cards != null) header.setCardsDisplay(cards); + } + + // Cell slot click (insert/extract/swap cell) + header.setCellSlotCallback(button -> { + if (button != 0) return; + + ItemStack heldStack = guiContext.getHeldStack(); + if (tempCell.isEmpty() && !heldStack.isEmpty()) { + // Empty slot + holding cell = insert + guiContext.sendPacket(new PacketTempCellAction( + PacketTempCellAction.Action.INSERT, tempCell.getTempSlotIndex())); + } else if (!tempCell.isEmpty() && heldStack.isEmpty()) { + // Occupied slot + empty hand = extract + boolean toInventory = guiContext.isShiftDown(); + guiContext.sendPacket(new PacketTempCellAction( + PacketTempCellAction.Action.EXTRACT, tempCell.getTempSlotIndex(), toInventory)); + } else if (!tempCell.isEmpty() && !heldStack.isEmpty()) { + // Occupied slot + holding cell = swap + guiContext.sendPacket(new PacketTempCellAction( + PacketTempCellAction.Action.SWAP, tempCell.getTempSlotIndex())); + } + }); + + // Send button + header.setOnSendClick(() -> + guiContext.sendPacket(new PacketTempCellAction( + PacketTempCellAction.Action.SEND, tempCell.getTempSlotIndex()))); + + // Name click (rename): header handles right-click directly via InlineRenameManager + // yOffset = 4 so field background (editingY + 1) aligns with name at y + 5 + if (cellInfo != null) { + header.setRenameInfo(cellInfo, GuiConstants.HEADER_NAME_X - 2, 4, + TempAreaHeader.SEND_BUTTON_X - 4); + } + + // Header selection for quick-add + Set selectedSlots = guiContext.getSelectedTempCellSlots(); + header.setOnHeaderClick(() -> { + if (!tempCell.hasCell()) return; + + int slotIndex = tempCell.getTempSlotIndex(); + if (selectedSlots.contains(slotIndex)) { + selectedSlots.remove(slotIndex); + } else { + // Validate same type as existing selection + if (!selectedSlots.isEmpty()) { + TempCellInfo existingTempCell = findExistingSelectedTempCell(selectedSlots); + if (existingTempCell != null && existingTempCell.getCellInfo() != null) { + CellInfo existingCell = existingTempCell.getCellInfo(); + CellInfo newCell = tempCell.getCellInfo(); + if (newCell != null) { + boolean sameType = (newCell.isFluid() == existingCell.isFluid()) + && (newCell.isEssentia() == existingCell.isEssentia()); + if (!sameType) { + guiContext.showError("gui.cellterminal.temp_area.mixed_cell_selection"); + return; + } + } + } + } + + selectedSlots.add(slotIndex); + } + }); + header.setSelectedSupplier(() -> + selectedSlots.contains(tempCell.getTempSlotIndex())); + + return header; + } + + // ---- Content line creation ---- + + private IWidget createContentLine(CellContentRow row, int y) { + CellInfo cell = row.getCell(); + boolean isPartition = row.isPartitionRow(); + SlotsLine.SlotMode mode = isPartition ? SlotsLine.SlotMode.PARTITION : SlotsLine.SlotMode.CONTENT; + + if (row.isFirstRow()) { + return createFirstContentRow(cell, row.getStartIndex(), mode, isPartition, y); + } + + return createContinuationRow(cell, row.getStartIndex(), mode, y); + } + + /** + * First content or partition row: SlotsLine with tree junction button. + * Content first row gets DO_PARTITION, partition first row gets CLEAR_PARTITION. + */ + private SlotsLine createFirstContentRow(CellInfo cell, int startIndex, + SlotsLine.SlotMode mode, boolean isPartition, int y) { + SlotsLine line = new SlotsLine( + y, SLOTS_PER_ROW, SLOTS_X_OFFSET, mode, + startIndex, fontRenderer, itemRender + ); + + int tempSlotIndex = findTempSlotIndexForCell(cell); + + configureSlotData(line, cell, mode, tempSlotIndex); + + // Tree junction button + ButtonType buttonType = isPartition ? ButtonType.CLEAR_PARTITION : ButtonType.DO_PARTITION; + SmallButton treeBtn = new SmallButton(0, 0, buttonType, () -> { + if (tempSlotIndex < 0) return; + + if (isPartition) { + guiContext.sendPacket(new PacketTempCellPartitionAction( + tempSlotIndex, PacketTempCellPartitionAction.Action.CLEAR_ALL)); + } else { + guiContext.sendPacket(new PacketTempCellPartitionAction( + tempSlotIndex, PacketTempCellPartitionAction.Action.SET_ALL_FROM_CONTENTS)); + } + }); + line.setTreeButton(treeBtn); + + line.setGuiOffsets(guiLeft, guiTop); + + // Selection highlight + if (tempSlotIndex >= 0) { + Set selectedSlots = guiContext.getSelectedTempCellSlots(); + line.setSelectedSupplier(() -> selectedSlots.contains(tempSlotIndex)); + } + + return line; + } + + /** + * Continuation row (not the first for this content/partition section). + */ + private ContinuationLine createContinuationRow(CellInfo cell, int startIndex, + SlotsLine.SlotMode mode, int y) { + ContinuationLine line = new ContinuationLine( + y, SLOTS_PER_ROW, SLOTS_X_OFFSET, mode, + startIndex, fontRenderer, itemRender + ); + + int tempSlotIndex = findTempSlotIndexForCell(cell); + configureSlotData(line, cell, mode, tempSlotIndex); + line.setGuiOffsets(guiLeft, guiTop); + + // Selection highlight + if (tempSlotIndex >= 0) { + Set selectedSlots = guiContext.getSelectedTempCellSlots(); + line.setSelectedSupplier(() -> selectedSlots.contains(tempSlotIndex)); + } + + return line; + } + + /** + * Configure content/partition data suppliers on a SlotsLine. + */ + private void configureSlotData(SlotsLine line, CellInfo cell, SlotsLine.SlotMode mode, int tempSlotIndex) { + if (mode == SlotsLine.SlotMode.CONTENT) { + line.setItemsSupplier(cell::getContents); + line.setPartitionSupplier(cell::getPartition); + line.setCountProvider(() -> cell::getContentCount); + } else { + line.setItemsSupplier(cell::getPartition); + line.setMaxSlots((int) cell.getTotalTypes()); + } + + line.setSlotClickCallback((slotIndex, mouseButton) -> { + if (mouseButton != 0 || tempSlotIndex < 0) return; + + if (mode == SlotsLine.SlotMode.CONTENT) { + // Content slot: toggle partition for that item + List contents = cell.getContents(); + if (slotIndex < contents.size() && !contents.get(slotIndex).isEmpty()) { + guiContext.sendPacket(new PacketTempCellPartitionAction( + tempSlotIndex, PacketTempCellPartitionAction.Action.TOGGLE_ITEM, + contents.get(slotIndex))); + } + } else { + // Partition slot: add/remove item from partition + ItemStack heldStack = guiContext.getHeldStack(); + List partition = cell.getPartition(); + boolean slotOccupied = slotIndex < partition.size() && !partition.get(slotIndex).isEmpty(); + + if (!heldStack.isEmpty()) { + guiContext.sendPacket(new PacketTempCellPartitionAction( + tempSlotIndex, PacketTempCellPartitionAction.Action.ADD_ITEM, + slotIndex, heldStack)); + } else if (slotOccupied) { + guiContext.sendPacket(new PacketTempCellPartitionAction( + tempSlotIndex, PacketTempCellPartitionAction.Action.REMOVE_ITEM, slotIndex)); + } + } + }); + } + + // ---- Upgrade card click handling ---- + + private void handleCardClick(CellInfo cell, int upgradeSlotIndex) { + if (CellTerminalServerConfig.isInitialized() + && !CellTerminalServerConfig.getInstance().isUpgradeExtractEnabled()) { + guiContext.showError("cellterminal.error.upgrade_extract_disabled"); + return; + } + + int tempSlotIndex = findTempSlotIndexForCell(cell); + if (tempSlotIndex < 0) return; + + boolean toInventory = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + guiContext.sendPacket(PacketExtractUpgrade.forTempCell(tempSlotIndex, upgradeSlotIndex, toInventory)); + } + + // ---- Keybind handling ---- + + /** + * Handle the add-to-temp-cell keybind (same key as ADD_TO_STORAGE_BUS). + * Adds the hovered item to all selected temp cells' partitions. + * Matches storage bus behavior: converts items for fluid/essentia cells, finds empty slots. + * + * @param selectedTempCellSlots Set of selected temp cell slot indexes + * @param hoveredSlot The slot the mouse is over (or null) + * @param tempAreaLines List of temp area line objects + * @return true if the keybind was handled + */ + private static boolean handleAddToTempCellKeybind(Set selectedTempCellSlots, + Slot hoveredSlot, + List tempAreaLines) { + if (selectedTempCellSlots.isEmpty()) { + if (Minecraft.getMinecraft().player != null) { + MessageHelper.warning("gui.cellterminal.temp_area.no_selection"); + } + + return true; + } + + // Get the item to add + ItemStack stack = ItemStack.EMPTY; + if (hoveredSlot != null && hoveredSlot.getHasStack()) stack = hoveredSlot.getStack(); + + // Try JEI/bookmark if no inventory item + if (stack.isEmpty()) { + QuickPartitionHandler.HoveredIngredient jeiItem = QuickPartitionHandler.getHoveredIngredient(); + if (jeiItem != null && !jeiItem.stack.isEmpty()) stack = jeiItem.stack; + } + + if (stack.isEmpty()) { + if (Minecraft.getMinecraft().player != null) { + MessageHelper.warning("gui.cellterminal.temp_area.no_item"); + } + + return true; + } + + // Add to all selected temp cells + int successCount = 0; + int invalidItemCount = 0; + int noSlotCount = 0; + + for (Integer tempSlotIndex : selectedTempCellSlots) { + // Find the TempCellInfo for this slot + TempCellInfo tempCell = findTempCellBySlot(tempAreaLines, tempSlotIndex); + if (tempCell == null || tempCell.getCellInfo() == null) continue; + + CellInfo cellInfo = tempCell.getCellInfo(); + + // Convert the item for non-item cell types first to check validity + ItemStack stackToSend = stack; + boolean validForCellType = true; + + if (cellInfo.isFluid()) { + // For fluid cells, need FluidDummyItem or fluid container + if (!(stack.getItem() instanceof FluidDummyItem)) { + FluidStack fluid = net.minecraftforge.fluids.FluidUtil.getFluidContained(stack); + // Can't use this item on fluid cell + if (fluid == null) { + invalidItemCount++; + validForCellType = false; + } + } + } else if (cellInfo.isEssentia()) { + // For essentia cells, need ItemDummyAspect or essentia container + ItemStack essentiaRep = ThaumicEnergisticsIntegration.tryConvertEssentiaContainerToAspect(stack); + // Can't use this item on essentia cell + if (essentiaRep.isEmpty()) { + invalidItemCount++; + validForCellType = false; + } else { + stackToSend = essentiaRep; + } + } + + if (!validForCellType) continue; + + // Find first empty slot in this cell's partition + // For cells, use totalTypes as the maximum config slots (always 63 for standard cells) + List partition = cellInfo.getPartition(); + int availableSlots = (int) cellInfo.getTotalTypes(); + int targetSlot = -1; + + for (int i = 0; i < availableSlots; i++) { + if (i >= partition.size() || partition.get(i).isEmpty()) { + targetSlot = i; + break; + } + } + + if (targetSlot < 0) { + noSlotCount++; + continue; + } + + // Send packet to add item to this temp cell's partition at specific slot + CellTerminalNetwork.INSTANCE.sendToServer( + new PacketTempCellPartitionAction( + tempSlotIndex, + PacketTempCellPartitionAction.Action.ADD_ITEM, + targetSlot, + stackToSend + ) + ); + successCount++; + } + + if (successCount == 0 && Minecraft.getMinecraft().player != null) { + // Show appropriate error message based on what failed + if (invalidItemCount > 0 && noSlotCount == 0) { + MessageHelper.error("gui.cellterminal.temp_area.invalid_item"); + } else if (noSlotCount > 0 && invalidItemCount == 0) { + MessageHelper.error("gui.cellterminal.temp_area.partition_full"); + } else { + // Mixed or other failure + MessageHelper.error("gui.cellterminal.temp_area.add_failed"); + } + } + + return true; + } + + /** + * Find TempCellInfo for a given slot index in the temp area lines. + */ + private static TempCellInfo findTempCellBySlot(List lines, int slotIndex) { + for (Object line : lines) { + if (line instanceof TempCellInfo) { + TempCellInfo tempCell = (TempCellInfo) line; + if (tempCell.getTempSlotIndex() == slotIndex) return tempCell; + } + } + + return null; + } + + // ---- Upgrade support ---- + + @Override + public boolean handleUpgradeClick(Object hoveredData, ItemStack heldStack, boolean isShiftClick) { + // Handle TempCellInfo (header) - specific temp cell slot + if (hoveredData instanceof TempCellInfo) { + TempCellInfo tempCell = (TempCellInfo) hoveredData; + CellInfo cellInfo = tempCell.getCellInfo(); + if (cellInfo == null || !cellInfo.canAcceptUpgrade(heldStack)) return false; + + guiContext.sendPacket(new PacketTempCellAction( + PacketTempCellAction.Action.UPGRADE, tempCell.getTempSlotIndex())); + + return true; + } + + // Handle CellContentRow (content/partition row) - find parent temp cell + if (hoveredData instanceof CellContentRow) { + CellInfo cell = ((CellContentRow) hoveredData).getCell(); + if (cell == null || !cell.canAcceptUpgrade(heldStack)) return false; + + int tempSlotIndex = findTempSlotIndexForCell(cell); + if (tempSlotIndex < 0) return false; + + guiContext.sendPacket(new PacketTempCellAction( + PacketTempCellAction.Action.UPGRADE, tempSlotIndex)); + + return true; + } + + return false; + } + + @Override + public boolean handleShiftUpgradeClick(ItemStack heldStack) { + // Shift-click with upgrade: server finds first temp cell that can accept + guiContext.sendPacket(new PacketTempCellAction( + PacketTempCellAction.Action.UPGRADE, -1, true)); + + return true; + } + + @Override + public boolean handleInventorySlotShiftClick(ItemStack upgradeStack, int sourceSlotIndex) { + // Delegate entirely to the server: send tempSlotIndex=-1 so the server + // iterates all temp cells and finds the first one that can actually accept + // the upgrade, avoiding client-side mispredictions. + guiContext.sendPacket(new PacketTempCellAction( + PacketTempCellAction.Action.UPGRADE, -1, sourceSlotIndex)); + + return true; + } + + // ---- Helpers ---- + + /** + * Find the temp slot index for a given cell by searching through temp area lines. + */ + private int findTempSlotIndexForCell(CellInfo cell) { + for (Object line : getLines(guiContext.getDataManager())) { + if (line instanceof TempCellInfo) { + TempCellInfo tempCell = (TempCellInfo) line; + if (tempCell.getCellInfo() == cell) return tempCell.getTempSlotIndex(); + } + } + + return -1; + } + + /** + * Find an existing selected temp cell from the selected temp cell slots set. + */ + private TempCellInfo findExistingSelectedTempCell(Set selectedSlots) { + for (Integer slotIndex : selectedSlots) { + for (Object line : getLines(guiContext.getDataManager())) { + if (line instanceof TempCellInfo) { + TempCellInfo tempCell = (TempCellInfo) line; + if (tempCell.getTempSlotIndex() == slotIndex) return tempCell; + } + } + } + + return null; + } +} diff --git a/src/main/java/com/cellterminal/gui/widget/tab/TerminalTabWidget.java b/src/main/java/com/cellterminal/gui/widget/tab/TerminalTabWidget.java new file mode 100644 index 0000000..50f7e02 --- /dev/null +++ b/src/main/java/com/cellterminal/gui/widget/tab/TerminalTabWidget.java @@ -0,0 +1,258 @@ +package com.cellterminal.gui.widget.tab; + +import java.util.ArrayList; +import java.util.List; + +import org.lwjgl.input.Keyboard; + +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.renderer.RenderItem; +import net.minecraft.client.resources.I18n; +import net.minecraft.item.ItemStack; + +import appeng.api.AEApi; + +import com.cellterminal.client.CellInfo; +import com.cellterminal.client.SearchFilterMode; +import com.cellterminal.client.StorageInfo; +import com.cellterminal.client.TabStateManager; +import com.cellterminal.gui.GuiConstants; +import com.cellterminal.gui.PriorityFieldManager; +import com.cellterminal.gui.handler.TerminalDataManager; +import com.cellterminal.gui.widget.CardsDisplay; +import com.cellterminal.gui.widget.DoubleClickTracker; +import com.cellterminal.gui.widget.IWidget; +import com.cellterminal.gui.widget.header.StorageHeader; +import com.cellterminal.gui.widget.line.TerminalLine; +import com.cellterminal.network.PacketEjectCell; +import com.cellterminal.network.PacketExtractUpgrade; +import com.cellterminal.config.CellTerminalServerConfig; + + +/** + * Tab widget for the Terminal tab (Tab 0). + *

+ * Displays storage groups with expandable cell lists. Each row is either: + *

    + *
  • {@link StorageInfo} → {@link StorageHeader} (name, location, expand/collapse)
  • + *
  • {@link CellInfo} → {@link TerminalLine} (cell icon, name, usage bar, action buttons)
  • + *
+ * + * The terminal tab provides per-cell action buttons (Eject, Inventory, Partition) + * that navigate to other tabs or trigger server-side actions. + */ +public class TerminalTabWidget extends AbstractTabWidget { + + // ---- Hover preview state (updated during draw) ---- + + /** The cell whose action button is currently hovered, or null if none */ + private CellInfo previewCell; + + /** Which button is hovered: 0=none, 1=inventory, 2=partition, 3=eject */ + private int previewType; + + public TerminalTabWidget(FontRenderer fontRenderer, RenderItem itemRender) { + super(fontRenderer, itemRender); + } + + // ---- Hover preview accessors ---- + + /** + * Get the cell whose action button is currently hovered (for popup preview). + * Updated during each draw() call. + */ + public CellInfo getPreviewCell() { + return previewCell; + } + + /** + * Get which action button is hovered: 0=none, 1=inventory, 2=partition, 3=eject. + */ + public int getPreviewType() { + return previewType; + } + + // ---- Drawing (with hover preview tracking) ---- + + @Override + public void draw(int mouseX, int mouseY) { + // Reset preview state before drawing + previewCell = null; + previewType = 0; + + super.draw(mouseX, mouseY); + + // After drawing, scan visible TerminalLine widgets for hovered buttons + for (IWidget widget : visibleRows) { + if (!(widget instanceof TerminalLine)) continue; + + TerminalLine line = (TerminalLine) widget; + int hoveredBtn = line.getHoveredButton(); + if (hoveredBtn != TerminalLine.HOVER_NONE) { + Object data = widgetDataMap.get(widget); + if (data instanceof CellInfo) { + previewCell = (CellInfo) data; + previewType = hoveredBtn; + } + break; + } + } + } + + // ---- Row building ---- + + @Override + protected IWidget createRowWidget(Object lineData, int y, List allLines, int lineIndex) { + if (lineData instanceof StorageInfo) { + return createStorageHeader((StorageInfo) lineData, y); + } + + if (lineData instanceof CellInfo) { + return createTerminalLine((CellInfo) lineData, y); + } + + return null; + } + + @Override + protected boolean isContentLine(List allLines, int index) { + if (index < 0 || index >= allLines.size()) return false; + + return allLines.get(index) instanceof CellInfo; + } + + // ---- Storage header creation ---- + + private StorageHeader createStorageHeader(StorageInfo storage, int y) { + StorageHeader header = new StorageHeader(y, fontRenderer, itemRender); + header.setIconSupplier(storage::getBlockItem); + header.setNameSupplier(storage::getName); + header.setHasCustomNameSupplier(storage::hasCustomName); + header.setLocationSupplier(storage::getLocationString); + + // Use TabStateManager for expand/collapse state (persists across rebuilds) + header.setExpandedSupplier(() -> + TabStateManager.getInstance().isExpanded(TabStateManager.TabType.TERMINAL, storage.getId())); + + // Rename info: header handles right-click directly via InlineRenameManager + int renameRightEdge = GuiConstants.CONTENT_RIGHT_EDGE + - PriorityFieldManager.FIELD_WIDTH - PriorityFieldManager.RIGHT_MARGIN - 4; + header.setRenameInfo(storage, GuiConstants.GUI_INDENT + 20 - 2, 0, renameRightEdge); + header.setOnNameDoubleClick(() -> guiContext.highlightInWorld( + storage.getPos(), storage.getDimension(), storage.getName()), + DoubleClickTracker.storageTargetId(storage.getId())); + header.setOnExpandToggle(() -> { + TabStateManager.getInstance().toggleExpanded(TabStateManager.TabType.TERMINAL, storage.getId()); + guiContext.rebuildAndUpdateScrollbar(); + }); + + // Priority field: header registers its own field with the singleton during draw + header.setPrioritizable(storage); + header.setGuiOffsets(guiLeft, guiTop); + + return header; + } + + // ---- Terminal line creation ---- + + private TerminalLine createTerminalLine(CellInfo cell, int y) { + TerminalLine line = new TerminalLine(y, fontRenderer, itemRender); + line.setCellItemSupplier(cell::getCellItem); + line.setCellNameSupplier(cell::getDisplayName); + line.setHasCustomNameSupplier(cell::hasCustomName); + line.setByteUsageSupplier(cell::getByteUsagePercent); + + // Set target ID for double-click tracking (parent storage + slot = unique cell ID) + line.setDoubleClickTargetId(DoubleClickTracker.cellTargetId( + cell.getParentStorageId(), cell.getSlot())); + + // Create upgrade cards display + CardsDisplay cards = createCellCards(cell, y); + if (cards != null) line.setCardsDisplay(cards); + + // Rename info: line handles right-click directly via InlineRenameManager + line.setRenameInfo(cell, GuiConstants.CELL_INDENT + 18 - 2, GuiConstants.BUTTON_EJECT_X - 4); + + // Wire up action callbacks directly to GuiContext + line.setCallback(new TerminalLine.TerminalLineCallback() { + @Override + public void onEjectClicked() { + guiContext.sendPacket(new PacketEjectCell( + cell.getParentStorageId(), cell.getSlot())); + } + + @Override + public void onInventoryClicked() { + guiContext.openInventoryPopup(cell); + } + + @Override + public void onPartitionClicked() { + guiContext.openPartitionPopup(cell); + } + + @Override + public void onNameDoubleClicked() { + // Highlight the parent storage in-world + guiContext.highlightCellInWorld(cell); + } + }); + + return line; + } + + // ---- Cards helper ---- + + private CardsDisplay createCellCards(CellInfo cell, int rowY) { + return createCellCardsDisplay(cell, rowY, this::handleCardClick); + } + + private void handleCardClick(CellInfo cell, int upgradeSlotIndex) { + if (CellTerminalServerConfig.isInitialized() + && !CellTerminalServerConfig.getInstance().isUpgradeExtractEnabled()) { + guiContext.showError("cellterminal.error.upgrade_extract_disabled"); + return; + } + + boolean toInventory = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + guiContext.sendPacket(PacketExtractUpgrade.forCell( + cell.getParentStorageId(), cell.getSlot(), upgradeSlotIndex, toInventory)); + } + + // ---- Tab controller methods ---- + + @Override + public List getLines(TerminalDataManager dataManager) { + return dataManager.getLines(); + } + + @Override + public SearchFilterMode getEffectiveSearchMode(SearchFilterMode userSelectedMode) { + return userSelectedMode; + } + + @Override + public boolean showSearchModeButton() { + return true; + } + + @Override + public List getHelpLines() { + List lines = new ArrayList<>(); + lines.add(I18n.format("gui.cellterminal.controls.double_click_storage_cell")); + lines.add(I18n.format("gui.cellterminal.right_click_rename")); + + return lines; + } + + @Override + public ItemStack getTabIcon() { + return AEApi.instance().definitions().parts().interfaceTerminal() + .maybeStack(1).orElse(ItemStack.EMPTY); + } + + @Override + public String getTabTooltip() { + return I18n.format("gui.cellterminal.tab.terminal.tooltip"); + } +} diff --git a/src/main/java/com/cellterminal/integration/AE2WUTIntegration.java b/src/main/java/com/cellterminal/integration/AE2WUTIntegration.java index 2d213eb..eb052c8 100644 --- a/src/main/java/com/cellterminal/integration/AE2WUTIntegration.java +++ b/src/main/java/com/cellterminal/integration/AE2WUTIntegration.java @@ -127,7 +127,7 @@ public static void registerRecipeIngredient() { private static void registerRecipeIngredientInternal() { // Add to the ingredient list so WUT recognizes our terminal in crafting ItemStack ingredient = new ItemStack(ItemRegistry.WIRELESS_CELL_TERMINAL); - AllWUTRecipe.itemList.put((int) getCellTerminalMode(), ingredient); + AllWUTRecipe.itemList.put(getCellTerminalMode(), ingredient); } /** @@ -185,23 +185,6 @@ private static int[] getWUTModesInternal(ItemStack stack) { return stack.getTagCompound().getIntArray("modes"); } - /** - * Get the current mode of a WUT item. - */ - public static byte getWUTCurrentMode(ItemStack stack) { - if (!isModLoaded()) return 0; - - return getWUTCurrentModeInternal(stack); - } - - @Optional.Method(modid = MODID) - private static byte getWUTCurrentModeInternal(ItemStack stack) { - if (!(stack.getItem() instanceof ItemWirelessUniversalTerminal)) return 0; - if (!stack.hasTagCompound()) return 0; - - return stack.getTagCompound().getByte("mode"); - } - /** * Open a different terminal mode in the WUT. * Called when the user clicks a mode switching button. @@ -233,7 +216,7 @@ public static ItemStack getWUTModeIcon(byte mode) { @Optional.Method(modid = MODID) private static ItemStack getWUTModeIconInternal(byte mode) { // Use AllWUTRecipe.itemList which has the actual terminal ItemStacks for all modes - ItemStack icon = AllWUTRecipe.itemList.get((int) mode); + ItemStack icon = AllWUTRecipe.itemList.get(mode); return icon != null ? icon : ItemStack.EMPTY; } diff --git a/src/main/java/com/cellterminal/integration/ECOAEExtensionIntegration.java b/src/main/java/com/cellterminal/integration/ECOAEExtensionIntegration.java index 4358a9e..556d454 100644 --- a/src/main/java/com/cellterminal/integration/ECOAEExtensionIntegration.java +++ b/src/main/java/com/cellterminal/integration/ECOAEExtensionIntegration.java @@ -143,20 +143,17 @@ private NBTTagCompound createEStorageDriveData(Object driveObj, int channelPrior github.kasuminova.ecoaeextension.common.tile.ecotech.estorage.EStorageCellDrive drive = (github.kasuminova.ecoaeextension.common.tile.ecotech.estorage.EStorageCellDrive) driveObj; - TileEntity te = drive; - if (te.getWorld() == null) return null; - // Generate unique ID for this drive - long id = te.getPos().toLong() ^ ((long) te.getWorld().provider.getDimension() << 48); + long id = drive.getPos().toLong() ^ ((long) drive.getWorld().provider.getDimension() << 48); // Register with callback for server-side tracking // Note: We use EStorageDriveWrapper to handle IChestOrDrive interface - if (callback != null) callback.register(id, te, new EStorageDriveWrapper(drive)); + if (callback != null) callback.register(id, drive, new EStorageDriveWrapper(drive)); NBTTagCompound storageData = new NBTTagCompound(); storageData.setLong("id", id); - storageData.setLong("pos", te.getPos().toLong()); - storageData.setInteger("dim", te.getWorld().provider.getDimension()); + storageData.setLong("pos", drive.getPos().toLong()); + storageData.setInteger("dim", drive.getWorld().provider.getDimension()); // Use localized name or default storageData.setString("name", "tile.ecoaeextension.estorage_cell_drive.name"); @@ -165,7 +162,7 @@ private NBTTagCompound createEStorageDriveData(Object driveObj, int channelPrior storageData.setInteger("priority", channelPriority); // Get block item for display - ItemStack blockItem = getBlockItem(te); + ItemStack blockItem = getBlockItem(drive); if (!blockItem.isEmpty()) { NBTTagCompound blockNbt = new NBTTagCompound(); blockItem.writeToNBT(blockNbt); diff --git a/src/main/java/com/cellterminal/integration/StorageDrawersIntegration.java b/src/main/java/com/cellterminal/integration/StorageDrawersIntegration.java index 59f1f6f..fb8368f 100644 --- a/src/main/java/com/cellterminal/integration/StorageDrawersIntegration.java +++ b/src/main/java/com/cellterminal/integration/StorageDrawersIntegration.java @@ -60,7 +60,7 @@ public static List tryGetItemRepositoryContents(TileEntity targe if (repo == null) return null; NonNullList records = repo.getAllItems(); - if (records == null || records.isEmpty()) return new ArrayList<>(); + if (records.isEmpty()) return new ArrayList<>(); List result = new ArrayList<>(); for (IItemRepository.ItemRecord record : records) { @@ -72,22 +72,6 @@ public static List tryGetItemRepositoryContents(TileEntity targe return result; } - /** - * Check if a TileEntity supports IItemRepository. - * - * @param targetTile The tile entity to check - * @param side The side to check from (can be null) - * @return true if IItemRepository is supported - */ - public static boolean hasItemRepository(TileEntity targetTile, EnumFacing side) { - if (!isModLoaded() || targetTile == null) return false; - if (ITEM_REPOSITORY_CAPABILITY == null) return false; - - IItemRepository repo = targetTile.getCapability(ITEM_REPOSITORY_CAPABILITY, side); - - return repo != null; - } - /** * Simple data class to hold item + count pairs. */ diff --git a/src/main/java/com/cellterminal/integration/ThaumicEnergisticsIntegration.java b/src/main/java/com/cellterminal/integration/ThaumicEnergisticsIntegration.java index e9b33b4..e232243 100644 --- a/src/main/java/com/cellterminal/integration/ThaumicEnergisticsIntegration.java +++ b/src/main/java/com/cellterminal/integration/ThaumicEnergisticsIntegration.java @@ -134,7 +134,7 @@ private static NBTTagCompound tryPopulateEssentiaCellInternal(ICellHandler cellH return cellData; } catch (Exception e) { - CellTerminal.LOGGER.debug("Failed to get Essentia cell data: " + e.getMessage()); + CellTerminal.LOGGER.debug("Failed to get Essentia cell data: {}", e.getMessage()); return null; } @@ -247,7 +247,7 @@ private static void setAllFromEssentiaContentsInternal(IItemHandler configInv, O ItemHandlerUtil.setStackInSlot(configInv, slot++, stack.asItemStackRepresentation()); } } catch (Exception e) { - CellTerminal.LOGGER.debug("Failed to set Essentia partition from contents: " + e.getMessage()); + CellTerminal.LOGGER.debug("Failed to set Essentia partition from contents: {}", e.getMessage()); } } @@ -294,7 +294,7 @@ private static void collectUniqueEssentiaFromCellInternal(ItemStack cellStack, } } } catch (Exception e) { - CellTerminal.LOGGER.debug("Failed to collect unique essentia from cell: " + e.getMessage()); + CellTerminal.LOGGER.debug("Failed to collect unique essentia from cell: {}", e.getMessage()); } } @@ -346,7 +346,7 @@ private static void extractAllEssentiaFromCellToBigTrackerInternal(ItemStack cel cellInv.persist(); } catch (Exception e) { - CellTerminal.LOGGER.debug("Failed to extract essentia from cell: " + e.getMessage()); + CellTerminal.LOGGER.debug("Failed to extract essentia from cell: {}", e.getMessage()); } } @@ -382,7 +382,7 @@ private static void injectEssentiaIntoCellInternal(ItemStack cellStack, Object e appeng.api.config.Actionable.MODULATE, null); cellInv.persist(); } catch (Exception e) { - CellTerminal.LOGGER.debug("Failed to inject essentia into cell: " + e.getMessage()); + CellTerminal.LOGGER.debug("Failed to inject essentia into cell: {}", e.getMessage()); } } @@ -466,7 +466,7 @@ private static long simulateEssentiaInjectionInternal(ItemStack cellStack, Objec return essentiaStack.getStackSize() - rejected.getStackSize(); } catch (Exception e) { - CellTerminal.LOGGER.debug("Failed to simulate essentia injection: " + e.getMessage()); + CellTerminal.LOGGER.debug("Failed to simulate essentia injection: {}", e.getMessage()); return 0; } } @@ -587,7 +587,7 @@ private static void collectEssentiaWithCountsInternal(ItemStack cellStack, tracker.add(key, stack); } } catch (Exception e) { - CellTerminal.LOGGER.debug("Failed to collect essentia with counts from cell: " + e.getMessage()); + CellTerminal.LOGGER.debug("Failed to collect essentia with counts from cell: {}", e.getMessage()); } } @@ -657,7 +657,7 @@ private static ItemStack tryConvertEssentiaContainerToAspectInternal(ItemStack i return aeEssentiaStack.asItemStackRepresentation(); } catch (Exception e) { - CellTerminal.LOGGER.debug("Failed to convert essentia container to aspect: " + e.getMessage()); + CellTerminal.LOGGER.debug("Failed to convert essentia container to aspect: {}", e.getMessage()); return ItemStack.EMPTY; } @@ -731,8 +731,6 @@ private static ItemStack tryConvertJeiIngredientToEssentiaInternal(Object ingred IStorageChannel essentiaChannel = AEApi.instance().storage().getStorageChannel(thaumicenergistics.api.storage.IEssentiaStorageChannel.class); - if (essentiaChannel == null) return ItemStack.EMPTY; - // Handle IAEEssentiaStack directly if (ingredient instanceof thaumicenergistics.api.storage.IAEEssentiaStack) { thaumicenergistics.api.storage.IAEEssentiaStack essentiaStack = @@ -751,7 +749,7 @@ private static ItemStack tryConvertJeiIngredientToEssentiaInternal(Object ingred return ItemStack.EMPTY; } catch (Exception e) { - CellTerminal.LOGGER.debug("Failed to convert JEI ingredient to essentia: " + e.getMessage()); + CellTerminal.LOGGER.debug("Failed to convert JEI ingredient to essentia: {}", e.getMessage()); return ItemStack.EMPTY; } @@ -856,14 +854,11 @@ private static NBTTagCompound tryCreateEssentiaStorageBusDataInternal(Object mac AEApi.instance().storage().getStorageChannel( thaumicenergistics.api.storage.IEssentiaStorageChannel.class); - if (essentiaChannel != null) { - thaumicenergistics.api.storage.IAEEssentiaStack aeStack = - essentiaChannel.createStack(aspect); + thaumicenergistics.api.storage.IAEEssentiaStack aeStack = essentiaChannel.createStack(aspect); - if (aeStack != null) { - ItemStack itemRep = aeStack.asItemStackRepresentation(); - if (!itemRep.isEmpty()) itemRep.writeToNBT(partNbt); - } + if (aeStack != null) { + ItemStack itemRep = aeStack.asItemStackRepresentation(); + if (!itemRep.isEmpty()) itemRep.writeToNBT(partNbt); } } @@ -898,19 +893,16 @@ private static NBTTagCompound tryCreateEssentiaStorageBusDataInternal(Object mac int amount = aspects.getAmount(aspect); if (amount <= 0) continue; - if (essentiaChannel != null) { - thaumicenergistics.api.storage.IAEEssentiaStack aeStack = - essentiaChannel.createStack(aspect); - - if (aeStack != null) { - ItemStack itemRep = aeStack.asItemStackRepresentation(); - if (!itemRep.isEmpty()) { - NBTTagCompound stackNbt = new NBTTagCompound(); - itemRep.writeToNBT(stackNbt); - stackNbt.setLong("Cnt", amount); - contentsList.appendTag(stackNbt); - count++; - } + thaumicenergistics.api.storage.IAEEssentiaStack aeStack = essentiaChannel.createStack(aspect); + + if (aeStack != null) { + ItemStack itemRep = aeStack.asItemStackRepresentation(); + if (!itemRep.isEmpty()) { + NBTTagCompound stackNbt = new NBTTagCompound(); + itemRep.writeToNBT(stackNbt); + stackNbt.setLong("Cnt", amount); + contentsList.appendTag(stackNbt); + count++; } } } @@ -937,7 +929,7 @@ private static NBTTagCompound tryCreateEssentiaStorageBusDataInternal(Object mac return busData; } catch (Exception e) { - CellTerminal.LOGGER.debug("Failed to create essentia storage bus data: " + e.getMessage()); + CellTerminal.LOGGER.debug("Failed to create essentia storage bus data: {}", e.getMessage()); return null; } @@ -1028,7 +1020,7 @@ private static void handleEssentiaStorageBusPartitionInternal(Object storageBus, break; } } catch (Exception e) { - CellTerminal.LOGGER.debug("Failed to handle essentia storage bus partition: " + e.getMessage()); + CellTerminal.LOGGER.debug("Failed to handle essentia storage bus partition: {}", e.getMessage()); } } @@ -1080,7 +1072,7 @@ private static int findEmptyAspectSlot(thaumicenergistics.util.EssentiaFilter co @Optional.Method(modid = MODID) private static void clearAspectConfig(thaumicenergistics.util.EssentiaFilter config) { int slot = 0; - for (thaumcraft.api.aspects.Aspect a : config) config.setAspect(null, slot++); + for (thaumcraft.api.aspects.Aspect ignored : config) config.setAspect(null, slot++); } /** @@ -1128,7 +1120,7 @@ private static boolean essentiaStorageBusHasPartitionInternal(Object storageBus) if (a != null) return true; } } catch (Exception e) { - CellTerminal.LOGGER.debug("Error checking essentia storage bus partition: " + e.getMessage()); + CellTerminal.LOGGER.debug("Error checking essentia storage bus partition: {}", e.getMessage()); } return false; diff --git a/src/main/java/com/cellterminal/integration/WUTModeSwitcher.java b/src/main/java/com/cellterminal/integration/WUTModeSwitcher.java index 28303d2..6350658 100644 --- a/src/main/java/com/cellterminal/integration/WUTModeSwitcher.java +++ b/src/main/java/com/cellterminal/integration/WUTModeSwitcher.java @@ -4,13 +4,14 @@ import java.util.ArrayList; import java.util.List; +import com.cellterminal.gui.GuiConstants; + import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.item.ItemStack; -import net.minecraft.util.ResourceLocation; import net.minecraftforge.fml.relauncher.Side; import net.minecraftforge.fml.relauncher.SideOnly; @@ -33,9 +34,6 @@ public class WUTModeSwitcher { private WUTModeButton toggleButton; private boolean expanded = false; private Rectangle exclusionArea = new Rectangle(); - private int baseX; - private int baseY; - private int maxY; // Maximum Y before we'd go off-screen public WUTModeSwitcher(WirelessTerminalGuiObject wth) { this.wirelessTerminalGuiObject = wth; @@ -57,9 +55,10 @@ public int initButtons(List buttonList, int startButtonId, int guiLef if (modes == null || modes.length == 0) return startButtonId; // Position toggle button on the right side, below the title/search bar area - this.baseX = guiLeft + 208 + 2; // Just to the right of the main GUI - this.baseY = guiTop + 18; // Below the title/search bar area - this.maxY = Math.max(baseY + 100, screenHeight - BUTTON_SIZE - 4); // Leave margin at bottom + int baseX = guiLeft + GuiConstants.GUI_WIDTH + 2; // Just to the right of the main GUI + int baseY = guiTop + 18; // Below the title/search bar area + // Maximum Y before we'd go off-screen + int maxY = Math.max(baseY + 100, screenHeight - BUTTON_SIZE - 4); // Leave margin at bottom // Create toggle button (shows arrows to indicate expandable) toggleButton = new WUTModeButton(startButtonId++, baseX, baseY, (byte) -1, true); @@ -95,14 +94,6 @@ public int initButtons(List buttonList, int startButtonId, int guiLef return startButtonId; } - /** - * Backwards compatible version without style parameters. - */ - public int initButtons(List buttonList, int startButtonId, int guiLeft, int guiTop, int guiHeight) { - // Default to screen height = guiTop + guiHeight + some margin - return initButtons(buttonList, startButtonId, guiLeft, guiTop, guiHeight, guiTop + guiHeight + 50, false); - } - /** * Update the JEI exclusion area based on current button states. */ @@ -201,14 +192,14 @@ public boolean hasButtons() { /** * Custom button for WUT mode switching. - * Uses AE2's states.png texture for consistent styling with other terminal buttons. * Toggle button shows 2x scaled black arrows; mode buttons show terminal icons. + *

+ * TODO: Once atlas textures are added for WUT mode buttons, convert this to + * extend GuiAtlasButton instead of GuiButton (see GuiTerminalStyleButton for reference). */ @SideOnly(Side.CLIENT) private static class WUTModeButton extends GuiButton { - private static final ResourceLocation STATES_TEXTURE = new ResourceLocation("appliedenergistics2", "textures/guis/states.png"); - private final byte mode; private final boolean isToggle; @@ -228,6 +219,8 @@ public void drawButton(Minecraft mc, int mouseX, int mouseY, float partialTicks) this.hovered = mouseX >= this.x && mouseY >= this.y && mouseX < this.x + this.width && mouseY < this.y + this.height; + // TODO: add a proper texture for the buttons, under the red one in atlas.png + GlStateManager.color(1.0f, 1.0f, 1.0f, 1.0f); GlStateManager.enableBlend(); diff --git a/src/main/java/com/cellterminal/integration/storage/AbstractStorageScanner.java b/src/main/java/com/cellterminal/integration/storage/AbstractStorageScanner.java index a808527..7530974 100644 --- a/src/main/java/com/cellterminal/integration/storage/AbstractStorageScanner.java +++ b/src/main/java/com/cellterminal/integration/storage/AbstractStorageScanner.java @@ -19,18 +19,6 @@ */ public abstract class AbstractStorageScanner implements IStorageScanner { - - /** - * Maximum number of config/partition slots. - * This is the standard AE2 limit for both cells and storage buses. - */ - public static final int MAX_PARTITION_SLOTS = 63; - - /** - * Number of content slots per row for cell displays. - */ - public static final int CELL_SLOTS_PER_ROW = 8; - @Override public abstract String getId(); diff --git a/src/main/java/com/cellterminal/integration/storage/StorageScannerRegistry.java b/src/main/java/com/cellterminal/integration/storage/StorageScannerRegistry.java index 77b00f7..daf3ad1 100644 --- a/src/main/java/com/cellterminal/integration/storage/StorageScannerRegistry.java +++ b/src/main/java/com/cellterminal/integration/storage/StorageScannerRegistry.java @@ -42,17 +42,6 @@ public static void register(IStorageScanner scanner) { CellTerminal.LOGGER.info("Registered storage scanner: {}", scanner.getId()); } - /** - * Scan all storage devices from all registered scanners. - * - * @param grid the ME network grid to scan - * @param storageList the list to append storage data to - * @param callback callback to register storage trackers - */ - public static void scanAllStorages(IGrid grid, NBTTagList storageList, CellDataHandler.StorageTrackerCallback callback) { - scanAllStorages(grid, storageList, callback, Integer.MAX_VALUE); - } - /** * Scan all storage devices from all registered scanners with a slot limit. * @@ -74,21 +63,4 @@ public static void scanAllStorages(IGrid grid, NBTTagList storageList, CellDataH } } - /** - * Get all registered scanners (for debugging). - * - * @return copy of the scanner list - */ - public static List getScanners() { - return new ArrayList<>(scanners); - } - - /** - * Check if any scanners are registered. - * - * @return true if at least one scanner is available - */ - public static boolean hasAnyScanners() { - return scanners.stream().anyMatch(IStorageScanner::isAvailable); - } } diff --git a/src/main/java/com/cellterminal/integration/storagebus/StorageBusScannerRegistry.java b/src/main/java/com/cellterminal/integration/storagebus/StorageBusScannerRegistry.java index d1ee475..80359e9 100644 --- a/src/main/java/com/cellterminal/integration/storagebus/StorageBusScannerRegistry.java +++ b/src/main/java/com/cellterminal/integration/storagebus/StorageBusScannerRegistry.java @@ -40,11 +40,4 @@ public static void scanAll(IGrid grid, NBTTagList out, Map getScanners() { - return new ArrayList<>(scanners); - } } diff --git a/src/main/java/com/cellterminal/integration/subnet/AE2SubnetScanner.java b/src/main/java/com/cellterminal/integration/subnet/AE2SubnetScanner.java index d9a305c..052779f 100644 --- a/src/main/java/com/cellterminal/integration/subnet/AE2SubnetScanner.java +++ b/src/main/java/com/cellterminal/integration/subnet/AE2SubnetScanner.java @@ -1,6 +1,7 @@ package com.cellterminal.integration.subnet; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import net.minecraft.item.ItemStack; @@ -15,12 +16,20 @@ import appeng.api.AEApi; import appeng.api.networking.IGrid; import appeng.api.networking.IGridNode; +import appeng.api.networking.storage.IStorageGrid; import appeng.api.parts.IPart; import appeng.api.parts.IPartHost; +import appeng.api.storage.IMEMonitor; +import appeng.api.storage.channels.IFluidStorageChannel; +import appeng.api.storage.channels.IItemStorageChannel; import appeng.api.storage.IStorageMonitorableAccessor; +import appeng.api.storage.data.IAEFluidStack; +import appeng.api.storage.data.IAEItemStack; +import appeng.api.storage.data.IItemList; import appeng.api.util.AEPartLocation; import appeng.capabilities.Capabilities; import appeng.fluids.parts.PartFluidStorageBus; +import appeng.parts.misc.PartInterface; import appeng.parts.misc.PartStorageBus; import appeng.tile.misc.TileInterface; @@ -54,7 +63,7 @@ public boolean isAvailable() { } @Override - public void scanSubnets(IGrid grid, NBTTagList out, Map trackerMap, int playerId) { + public void scanSubnets(IGrid grid, NBTTagList out, Map trackerMap, int playerId, int slotLimit) { if (grid == null) return; // Temporary map to group connections by target subnet grid @@ -65,7 +74,6 @@ public void scanSubnets(IGrid grid, NBTTagList out, Map tra scanFluidStorageBuses(grid, subnetsByGrid, playerId); // Scan inbound connections: Interface on main <- Storage Bus on subnet - // This is the MORE COMMON pattern where subnets pull from main network scanInboundConnections(grid, subnetsByGrid, playerId); // Convert to NBT and populate tracker map @@ -73,7 +81,7 @@ public void scanSubnets(IGrid grid, NBTTagList out, Map tra IGrid subnetGrid = entry.getKey(); SubnetTracker tracker = entry.getValue(); - NBTTagCompound subnetNbt = createSubnetNBT(subnetGrid, tracker, playerId); + NBTTagCompound subnetNbt = createSubnetNBT(subnetGrid, tracker, playerId, slotLimit); out.appendTag(subnetNbt); trackerMap.put(tracker.id, tracker); } @@ -81,13 +89,12 @@ public void scanSubnets(IGrid grid, NBTTagList out, Map tra /** * Scan for inbound connections where a subnet's Storage Bus points at our Interface. - * This is the common "subnet pulling from main" pattern. *

* We scan all TileInterfaces on the main grid, then check adjacent tiles for * Storage Buses from other grids. */ private void scanInboundConnections(IGrid mainGrid, Map subnetsByGrid, int playerId) { - // Scan TileInterface blocks + // Scan full-block TileInterface for (IGridNode node : mainGrid.getMachines(TileInterface.class)) { if (!node.isActive()) continue; @@ -115,6 +122,30 @@ private void scanInboundConnections(IGrid mainGrid, Map su tracker.addInboundConnection(iface, ifaceTile, facing); } } + + // Scan cable-attached PartInterface (only check the one side the part faces) + for (IGridNode node : mainGrid.getMachines(PartInterface.class)) { + if (!node.isActive()) continue; + + PartInterface iface = (PartInterface) node.getMachine(); + TileEntity ifaceTile = iface.getTileEntity(); + if (ifaceTile == null) continue; + + // PartInterface faces one specific direction — only check that side + EnumFacing facing = iface.getSide().getFacing(); + if (facing == null) continue; + + World world = ifaceTile.getWorld(); + BlockPos adjacentPos = ifaceTile.getPos().offset(facing); + TileEntity adjacentTile = world.getTileEntity(adjacentPos); + if (adjacentTile == null) continue; + + IGrid remoteGrid = checkForRemoteStorageBus(adjacentTile, facing.getOpposite(), mainGrid); + if (remoteGrid == null) continue; + + SubnetTracker tracker = getOrCreateTracker(subnetsByGrid, remoteGrid); + tracker.addInboundConnection(iface, ifaceTile, facing); + } } /** @@ -220,10 +251,14 @@ private void scanFluidStorageBuses(IGrid mainGrid, Map sub /** * Create NBT data for a subnet. */ - private NBTTagCompound createSubnetNBT(IGrid subnetGrid, SubnetTracker tracker, int playerId) { + private NBTTagCompound createSubnetNBT(IGrid subnetGrid, SubnetTracker tracker, int playerId, int slotLimit) { // Use base class method for common fields NBTTagCompound nbt = createBaseSubnetNBT(subnetGrid, tracker, playerId); + // Subnet inventory contents (queried from the subnet's ME storage) + NBTTagList inventoryList = collectSubnetInventory(subnetGrid, slotLimit); + nbt.setTag("inventory", inventoryList); + // Connection points NBTTagList connectionsList = new NBTTagList(); for (int i = 0; i < tracker.connectionParts.size(); i++) { @@ -240,6 +275,78 @@ private NBTTagCompound createSubnetNBT(IGrid subnetGrid, SubnetTracker tracker, return nbt; } + /** + * Collect item and fluid inventories from a subnet's ME storage grid. + * This queries the subnet's IStorageGrid for all stored items and fluids, + * aggregated across all storage devices (drives, chests, etc.) on the subnet. + * + * @param subnetGrid The subnet's grid to query + * @param slotLimit Maximum number of item types to include + * @return NBTTagList of inventory contents with "Cnt" counts + */ + private NBTTagList collectSubnetInventory(IGrid subnetGrid, int slotLimit) { + NBTTagList inventoryList = new NBTTagList(); + + IStorageGrid storageGrid; + try { + storageGrid = subnetGrid.getCache(IStorageGrid.class); + } catch (Exception e) { + return inventoryList; + } + if (storageGrid == null) return inventoryList; + + int count = 0; + + // Collect item storage + try { + IItemStorageChannel itemChannel = AEApi.instance().storage().getStorageChannel(IItemStorageChannel.class); + IMEMonitor itemMonitor = storageGrid.getInventory(itemChannel); + if (itemMonitor != null) { + IItemList storageList = itemMonitor.getStorageList(); + for (IAEItemStack aeStack : storageList) { + if (count >= slotLimit) break; + if (aeStack.getStackSize() <= 0) continue; + + ItemStack stack = aeStack.createItemStack(); + stack.setCount(1); + NBTTagCompound stackNbt = new NBTTagCompound(); + stack.writeToNBT(stackNbt); + stackNbt.setLong("Cnt", aeStack.getStackSize()); + inventoryList.appendTag(stackNbt); + count++; + } + } + } catch (Exception e) { + // Silently continue - some grids may not have item storage + } + + // Collect fluid storage + try { + IFluidStorageChannel fluidChannel = AEApi.instance().storage().getStorageChannel(IFluidStorageChannel.class); + IMEMonitor fluidMonitor = storageGrid.getInventory(fluidChannel); + if (fluidMonitor != null) { + IItemList fluidList = fluidMonitor.getStorageList(); + for (IAEFluidStack aeFluid : fluidList) { + if (count >= slotLimit) break; + if (aeFluid.getStackSize() <= 0) continue; + + ItemStack fluidRep = aeFluid.asItemStackRepresentation(); + if (fluidRep.isEmpty()) continue; + + NBTTagCompound stackNbt = new NBTTagCompound(); + fluidRep.writeToNBT(stackNbt); + stackNbt.setLong("Cnt", aeFluid.getStackSize()); + inventoryList.appendTag(stackNbt); + count++; + } + } + } catch (Exception e) { + // Silently continue - some grids may not have fluid storage + } + + return inventoryList; + } + /** * Create NBT for a single connection point. */ @@ -248,6 +355,7 @@ private NBTTagCompound createConnectionNBT(Object part, TileEntity hostTile, IGr NBTTagCompound nbt = new NBTTagCompound(); nbt.setLong("pos", hostTile.getPos().toLong()); + nbt.setInteger("dim", hostTile.getWorld().provider.getDimension()); nbt.setBoolean("outbound", isOutbound); if (part instanceof PartStorageBus) { @@ -299,7 +407,6 @@ private NBTTagCompound createConnectionNBT(Object part, TileEntity hostTile, IGr } else if (part instanceof TileInterface) { // Inbound connection: Interface on main <- Storage Bus on subnet - TileInterface iface = (TileInterface) part; nbt.setInteger("side", connectionSide != null ? connectionSide.ordinal() : 0); // Local icon (interface) @@ -321,6 +428,33 @@ private NBTTagCompound createConnectionNBT(Object part, TileEntity hostTile, IGr } } + // For inbound, filter comes from the remote storage bus + addInboundFilter(nbt, hostTile, connectionSide, subnetGrid); + + } else if (part instanceof PartInterface) { + // Inbound connection: PartInterface (cable-attached) on main <- Storage Bus on subnet + PartInterface iface = (PartInterface) part; + nbt.setInteger("side", connectionSide != null ? connectionSide.ordinal() : iface.getSide().ordinal()); + + // Local icon (interface part) + ItemStack localIcon = AEApi.instance().definitions().parts().iface().maybeStack(1).orElse(ItemStack.EMPTY); + if (!localIcon.isEmpty()) { + NBTTagCompound iconNbt = new NBTTagCompound(); + localIcon.writeToNBT(iconNbt); + nbt.setTag("localIcon", iconNbt); + } + + // Remote icon (the storage bus on subnet side) + if (connectionSide != null) { + BlockPos targetPos = hostTile.getPos().offset(connectionSide); + ItemStack remoteIcon = getBlockItemStack(hostTile.getWorld(), targetPos); + if (!remoteIcon.isEmpty()) { + NBTTagCompound iconNbt = new NBTTagCompound(); + remoteIcon.writeToNBT(iconNbt); + nbt.setTag("remoteIcon", iconNbt); + } + } + // For inbound, filter comes from the remote storage bus addInboundFilter(nbt, hostTile, connectionSide, subnetGrid); } @@ -330,30 +464,33 @@ private NBTTagCompound createConnectionNBT(Object part, TileEntity hostTile, IGr /** * Add storage bus filter configuration to connection NBT. + * Sends ALL slots (including empty) to preserve slot positions for editing. + * The "filter" key is always sent for storage bus connections so the client shows partition rows. */ private void addStorageBusFilter(NBTTagCompound nbt, PartStorageBus bus) { IItemHandler configInv = bus.getInventoryByName("config"); if (configInv == null) return; NBTTagList filterList = new NBTTagList(); - int slotsToUse = 9; // Only 9 slots for now since we don't want to overload the UI with too many filter items - // TODO: increase? + int totalSlots = configInv.getSlots(); - for (int i = 0; i < configInv.getSlots() && i < slotsToUse; i++) { + // Send all slots including empties to preserve positions for partition editing + for (int i = 0; i < totalSlots; i++) { ItemStack filterItem = configInv.getStackInSlot(i); - if (filterItem.isEmpty()) continue; - NBTTagCompound itemNbt = new NBTTagCompound(); filterItem.writeToNBT(itemNbt); filterList.appendTag(itemNbt); } - if (filterList.tagCount() > 0) nbt.setTag("filter", filterList); + // Always set the filter key so the client knows partition rows should be shown + nbt.setTag("filter", filterList); + nbt.setInteger("maxPartitionSlots", totalSlots); } /** * Add filter from the remote storage bus for inbound connections. * This finds the storage bus on the subnet that is pointing at our interface. + * For inbound connections, the filter key is always sent so partition rows are shown. */ private void addInboundFilter(NBTTagCompound nbt, TileEntity hostTile, EnumFacing connectionSide, IGrid subnetGrid) { if (connectionSide == null || hostTile == null) return; @@ -377,6 +514,10 @@ private void addInboundFilter(NBTTagCompound nbt, TileEntity hostTile, EnumFacin addStorageBusFilter(nbt, (PartStorageBus) part); return; } + + // Fluid storage bus: no item partition editing supported yet + // FIXME: support fluid storage bus + if (part instanceof PartFluidStorageBus) return; } } diff --git a/src/main/java/com/cellterminal/integration/subnet/AbstractSubnetScanner.java b/src/main/java/com/cellterminal/integration/subnet/AbstractSubnetScanner.java index ef35eed..f423a2f 100644 --- a/src/main/java/com/cellterminal/integration/subnet/AbstractSubnetScanner.java +++ b/src/main/java/com/cellterminal/integration/subnet/AbstractSubnetScanner.java @@ -59,7 +59,10 @@ protected IGrid getGridFromTile(TileEntity tile) { TileInterface iface = (TileInterface) tile; IGridNode node = iface.getGridNode(AEPartLocation.INTERNAL); - if (node != null && node.getGrid() != null) return node.getGrid(); + if (node != null) { + node.getGrid(); + return node.getGrid(); + } } // Handle part interfaces on cable buses (PartInterface attached to cables) diff --git a/src/main/java/com/cellterminal/integration/subnet/ISubnetScanner.java b/src/main/java/com/cellterminal/integration/subnet/ISubnetScanner.java index 768acc3..23c2223 100644 --- a/src/main/java/com/cellterminal/integration/subnet/ISubnetScanner.java +++ b/src/main/java/com/cellterminal/integration/subnet/ISubnetScanner.java @@ -38,6 +38,7 @@ public interface ISubnetScanner { * @param out NBTTagList to append subnet connection data to * @param trackerMap Map to populate with subnet trackers for server-side operations * @param playerId The player ID for security permission checks + * @param slotLimit Maximum number of inventory item types to include per subnet */ - void scanSubnets(IGrid grid, NBTTagList out, Map trackerMap, int playerId); + void scanSubnets(IGrid grid, NBTTagList out, Map trackerMap, int playerId, int slotLimit); } diff --git a/src/main/java/com/cellterminal/integration/subnet/SubnetScannerRegistry.java b/src/main/java/com/cellterminal/integration/subnet/SubnetScannerRegistry.java index b6419c7..5a093ad 100644 --- a/src/main/java/com/cellterminal/integration/subnet/SubnetScannerRegistry.java +++ b/src/main/java/com/cellterminal/integration/subnet/SubnetScannerRegistry.java @@ -44,30 +44,18 @@ public static void register(ISubnetScanner scanner) { * @param out NBTTagList to append subnet data to * @param trackerMap Map to populate with subnet trackers * @param playerId The player ID for security permission checks + * @param slotLimit Maximum number of inventory item types to include per subnet */ - public static void scanAll(IGrid grid, NBTTagList out, Map trackerMap, int playerId) { + public static void scanAll(IGrid grid, NBTTagList out, Map trackerMap, int playerId, int slotLimit) { for (ISubnetScanner scanner : scanners) { if (!scanner.isAvailable()) continue; try { - scanner.scanSubnets(grid, out, trackerMap, playerId); + scanner.scanSubnets(grid, out, trackerMap, playerId, slotLimit); } catch (Exception e) { CellTerminal.LOGGER.error("Error scanning subnets with {}: {}", scanner.getId(), e.getMessage()); } } } - /** - * Check if any subnet scanners are available. - */ - public static boolean hasAnyScanners() { - return scanners.stream().anyMatch(ISubnetScanner::isAvailable); - } - - /** - * Get all registered scanners. - */ - public static List getScanners() { - return new ArrayList<>(scanners); - } } diff --git a/src/main/java/com/cellterminal/items/ItemWirelessCellTerminal.java b/src/main/java/com/cellterminal/items/ItemWirelessCellTerminal.java index 7c32669..edf96ae 100644 --- a/src/main/java/com/cellterminal/items/ItemWirelessCellTerminal.java +++ b/src/main/java/com/cellterminal/items/ItemWirelessCellTerminal.java @@ -28,7 +28,6 @@ import appeng.api.config.ViewItems; import appeng.api.features.ILocatable; import appeng.api.features.IWirelessTermHandler; -import appeng.api.storage.ICellWorkbenchItem; import appeng.api.util.IConfigManager; import appeng.core.AEConfig; import appeng.core.localization.GuiText; @@ -113,6 +112,12 @@ public void addCheckedInformation(ItemStack stack, World world, List lin lines.add(I18n.format("item.cellterminal.cell_terminal.tooltip")); + // Show temp cell count if any are stored + int tempCellCount = getTempCellCount(stack); + if (tempCellCount > 0) { + lines.add(I18n.format("item.cellterminal.wireless_cell_terminal.temp_cells", tempCellCount)); + } + if (stack.hasTagCompound()) { NBTTagCompound tag = Platform.openNbtData(stack); String encKey = tag.getString("encryptionKey"); @@ -182,22 +187,17 @@ public IGuiHandler getGuiHandler(ItemStack is) { // TEMP CELL STORAGE // ======================================== - private static final String NBT_TEMP_CELLS = "tempCells"; - private static final int MAX_TEMP_CELLS = 16; - /** - * Check if a stack is a valid storage cell. + * NBT key for temp cell storage. Used by both Wireless Cell Terminal and WUT integration. */ - public static boolean isValidTempCellItem(ItemStack stack) { - if (stack.isEmpty()) return true; - - return stack.getItem() instanceof ICellWorkbenchItem; - } + public static final String NBT_TEMP_CELLS = "cellTerminalTempCells"; + public static final int MAX_TEMP_CELLS = 16; /** - * Get the number of temp cells stored in this terminal. + * Get the number of temp cells stored in a terminal ItemStack. + * Works with both Wireless Cell Terminal and Wireless Universal Terminal. */ - public int getTempCellCount(ItemStack terminal) { + public static int getTempCellCount(ItemStack terminal) { NBTTagCompound nbt = Platform.openNbtData(terminal); if (!nbt.hasKey(NBT_TEMP_CELLS)) return 0; @@ -213,8 +213,9 @@ public int getTempCellCount(ItemStack terminal) { /** * Get a temp cell at the given slot index. + * Works with both Wireless Cell Terminal and Wireless Universal Terminal. */ - public ItemStack getTempCell(ItemStack terminal, int slot) { + public static ItemStack getTempCell(ItemStack terminal, int slot) { if (slot < 0 || slot >= MAX_TEMP_CELLS) return ItemStack.EMPTY; NBTTagCompound nbt = Platform.openNbtData(terminal); @@ -228,8 +229,9 @@ public ItemStack getTempCell(ItemStack terminal, int slot) { /** * Set a temp cell at the given slot index. + * Works with both Wireless Cell Terminal and Wireless Universal Terminal. */ - public void setTempCell(ItemStack terminal, int slot, ItemStack cell) { + public static void setTempCell(ItemStack terminal, int slot, ItemStack cell) { if (slot < 0 || slot >= MAX_TEMP_CELLS) return; NBTTagCompound nbt = Platform.openNbtData(terminal); @@ -252,46 +254,10 @@ public void setTempCell(ItemStack terminal, int slot, ItemStack cell) { nbt.setTag(NBT_TEMP_CELLS, cellList); } - /** - * Get the first empty temp cell slot, or -1 if full. - */ - public int getFirstEmptyTempSlot(ItemStack terminal) { - NBTTagCompound nbt = Platform.openNbtData(terminal); - NBTTagList cellList = nbt.getTagList(NBT_TEMP_CELLS, Constants.NBT.TAG_COMPOUND); - - for (int i = 0; i < MAX_TEMP_CELLS; i++) { - if (i >= cellList.tagCount()) return i; - - ItemStack cell = new ItemStack(cellList.getCompoundTagAt(i)); - if (cell.isEmpty()) return i; - } - - return -1; - } - - /** - * Clear all temp cells and return them as a list (for dropping). - */ - public List clearAllTempCells(ItemStack terminal) { - List dropped = new java.util.ArrayList<>(); - NBTTagCompound nbt = Platform.openNbtData(terminal); - - if (nbt.hasKey(NBT_TEMP_CELLS)) { - NBTTagList cellList = nbt.getTagList(NBT_TEMP_CELLS, Constants.NBT.TAG_COMPOUND); - for (int i = 0; i < cellList.tagCount(); i++) { - ItemStack cell = new ItemStack(cellList.getCompoundTagAt(i)); - if (!cell.isEmpty()) dropped.add(cell); - } - nbt.removeTag(NBT_TEMP_CELLS); - } - - return dropped; - } - /** * Get maximum temp cell slots. */ - public int getMaxTempCells() { + public static int getMaxTempCells() { return MAX_TEMP_CELLS; } diff --git a/src/main/java/com/cellterminal/network/CellTerminalNetwork.java b/src/main/java/com/cellterminal/network/CellTerminalNetwork.java index fccf541..3b0231e 100644 --- a/src/main/java/com/cellterminal/network/CellTerminalNetwork.java +++ b/src/main/java/com/cellterminal/network/CellTerminalNetwork.java @@ -209,6 +209,14 @@ public static void init() { Side.SERVER ); + // Client -> Server: Subnet connection partition editing + INSTANCE.registerMessage( + PacketSubnetPartitionAction.Handler.class, + PacketSubnetPartitionAction.class, + packetId++, + Side.SERVER + ); + // Server -> Client: GUI-safe feedback messages (overlay + chat) INSTANCE.registerMessage( PacketPlayerFeedback.Handler.class, diff --git a/src/main/java/com/cellterminal/network/PacketExtractUpgrade.java b/src/main/java/com/cellterminal/network/PacketExtractUpgrade.java index 6aa7dce..7be777f 100644 --- a/src/main/java/com/cellterminal/network/PacketExtractUpgrade.java +++ b/src/main/java/com/cellterminal/network/PacketExtractUpgrade.java @@ -19,7 +19,8 @@ public class PacketExtractUpgrade implements IMessage { public enum TargetType { CELL, - STORAGE_BUS + STORAGE_BUS, + TEMP_CELL } private TargetType targetType; @@ -66,9 +67,26 @@ public static PacketExtractUpgrade forStorageBus(long storageBusId, int upgradeI return packet; } + /** + * Create an extract request for a temp cell upgrade. + * @param tempSlotIndex The temp area slot index containing the cell + * @param upgradeIndex The upgrade slot index to extract from + * @param toInventory If true, put in inventory; if false, put in hand + */ + public static PacketExtractUpgrade forTempCell(int tempSlotIndex, int upgradeIndex, boolean toInventory) { + PacketExtractUpgrade packet = new PacketExtractUpgrade(); + packet.targetType = TargetType.TEMP_CELL; + packet.targetId = tempSlotIndex; + packet.cellSlot = 0; + packet.upgradeIndex = upgradeIndex; + packet.toInventory = toInventory; + + return packet; + } + @Override public void fromBytes(ByteBuf buf) { - this.targetType = buf.readBoolean() ? TargetType.STORAGE_BUS : TargetType.CELL; + this.targetType = TargetType.values()[buf.readByte()]; this.targetId = buf.readLong(); this.cellSlot = buf.readInt(); this.upgradeIndex = buf.readInt(); @@ -77,7 +95,7 @@ public void fromBytes(ByteBuf buf) { @Override public void toBytes(ByteBuf buf) { - buf.writeBoolean(targetType == TargetType.STORAGE_BUS); + buf.writeByte(targetType.ordinal()); buf.writeLong(targetId); buf.writeInt(cellSlot); buf.writeInt(upgradeIndex); diff --git a/src/main/java/com/cellterminal/network/PacketInsertCell.java b/src/main/java/com/cellterminal/network/PacketInsertCell.java index 26abd80..090e418 100644 --- a/src/main/java/com/cellterminal/network/PacketInsertCell.java +++ b/src/main/java/com/cellterminal/network/PacketInsertCell.java @@ -19,9 +19,6 @@ public class PacketInsertCell implements IMessage { private long storageId; private int targetSlot; // -1 for first available - public PacketInsertCell() { - } - public PacketInsertCell(long storageId, int targetSlot) { this.storageId = storageId; this.targetSlot = targetSlot; diff --git a/src/main/java/com/cellterminal/network/PacketOpenWirelessTerminal.java b/src/main/java/com/cellterminal/network/PacketOpenWirelessTerminal.java index 3d97666..2e5d572 100644 --- a/src/main/java/com/cellterminal/network/PacketOpenWirelessTerminal.java +++ b/src/main/java/com/cellterminal/network/PacketOpenWirelessTerminal.java @@ -80,9 +80,7 @@ private void tryOpenTerminal(EntityPlayerMP player) { } // Check baubles slots - if (Loader.isModLoaded("baubles")) { - if (tryOpenFromBaubles(player)) return; - } + if (Loader.isModLoaded("baubles")) tryOpenFromBaubles(player); // No terminal found, ignore } diff --git a/src/main/java/com/cellterminal/network/PacketSetPriority.java b/src/main/java/com/cellterminal/network/PacketSetPriority.java index bc8ed7c..bddf705 100644 --- a/src/main/java/com/cellterminal/network/PacketSetPriority.java +++ b/src/main/java/com/cellterminal/network/PacketSetPriority.java @@ -49,9 +49,7 @@ public static class Handler implements IMessageHandler { - handleGuiPriority(player, message); - }); + player.getServerWorld().addScheduledTask(() -> handleGuiPriority(player, message)); return null; } diff --git a/src/main/java/com/cellterminal/network/PacketSlotLimitChange.java b/src/main/java/com/cellterminal/network/PacketSlotLimitChange.java index 01c0171..cd6ef5a 100644 --- a/src/main/java/com/cellterminal/network/PacketSlotLimitChange.java +++ b/src/main/java/com/cellterminal/network/PacketSlotLimitChange.java @@ -17,25 +17,29 @@ public class PacketSlotLimitChange implements IMessage { private int cellLimit; private int busLimit; + private int subnetLimit; public PacketSlotLimitChange() { } - public PacketSlotLimitChange(int cellLimit, int busLimit) { + public PacketSlotLimitChange(int cellLimit, int busLimit, int subnetLimit) { this.cellLimit = cellLimit; this.busLimit = busLimit; + this.subnetLimit = subnetLimit; } @Override public void fromBytes(ByteBuf buf) { this.cellLimit = buf.readInt(); this.busLimit = buf.readInt(); + this.subnetLimit = buf.readInt(); } @Override public void toBytes(ByteBuf buf) { buf.writeInt(cellLimit); buf.writeInt(busLimit); + buf.writeInt(subnetLimit); } public static class Handler implements IMessageHandler { @@ -45,7 +49,7 @@ public IMessage onMessage(PacketSlotLimitChange message, MessageContext ctx) { ctx.getServerHandler().player.getServerWorld().addScheduledTask(() -> { if (ctx.getServerHandler().player.openContainer instanceof ContainerCellTerminalBase) { ContainerCellTerminalBase container = (ContainerCellTerminalBase) ctx.getServerHandler().player.openContainer; - container.setSlotLimits(message.cellLimit, message.busLimit); + container.setSlotLimits(message.cellLimit, message.busLimit, message.subnetLimit); } }); diff --git a/src/main/java/com/cellterminal/network/PacketSubnetPartitionAction.java b/src/main/java/com/cellterminal/network/PacketSubnetPartitionAction.java new file mode 100644 index 0000000..1140e7d --- /dev/null +++ b/src/main/java/com/cellterminal/network/PacketSubnetPartitionAction.java @@ -0,0 +1,128 @@ +package com.cellterminal.network; + +import io.netty.buffer.ByteBuf; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; + +import net.minecraftforge.fml.common.network.ByteBufUtils; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; +import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; +import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; + +import com.cellterminal.container.ContainerCellTerminalBase; + + +/** + * Packet for modifying partition of a storage bus connection in the subnet overview. + * Identifies the target by subnet ID + connection position + side. + */ +public class PacketSubnetPartitionAction implements IMessage { + + public enum Action { + ADD_ITEM, + REMOVE_ITEM, + SET_ALL_FROM_CONTENTS, + CLEAR_ALL, + TOGGLE_ITEM, + /** Set partition from the subnet's entire ME storage inventory (for outbound connections). */ + SET_ALL_FROM_SUBNET_INVENTORY + } + + private long subnetId; + private long pos; + private int side; + private Action action; + private int partitionSlot; + private ItemStack itemStack; + + public PacketSubnetPartitionAction() { + this.itemStack = ItemStack.EMPTY; + } + + /** + * Actions that don't need a specific partition slot or item (CLEAR_ALL, SET_ALL_FROM_CONTENTS). + */ + public PacketSubnetPartitionAction(long subnetId, long pos, int side, Action action) { + this(subnetId, pos, side, action, -1, ItemStack.EMPTY); + } + + /** + * REMOVE_ITEM action on a specific partition slot. + */ + public PacketSubnetPartitionAction(long subnetId, long pos, int side, Action action, int partitionSlot) { + this(subnetId, pos, side, action, partitionSlot, ItemStack.EMPTY); + } + + /** + * ADD_ITEM action on a specific partition slot with an item. + */ + public PacketSubnetPartitionAction(long subnetId, long pos, int side, Action action, + int partitionSlot, ItemStack itemStack) { + this.subnetId = subnetId; + this.pos = pos; + this.side = side; + this.action = action; + this.partitionSlot = partitionSlot; + this.itemStack = itemStack != null ? itemStack : ItemStack.EMPTY; + } + + /** + * TOGGLE_ITEM action without specific partition slot. + */ + public PacketSubnetPartitionAction(long subnetId, long pos, int side, Action action, ItemStack itemStack) { + this(subnetId, pos, side, action, -1, itemStack); + } + + @Override + public void fromBytes(ByteBuf buf) { + this.subnetId = buf.readLong(); + this.pos = buf.readLong(); + this.side = buf.readInt(); + this.action = Action.values()[buf.readByte()]; + this.partitionSlot = buf.readInt(); + + if (buf.readBoolean()) { + NBTTagCompound nbt = ByteBufUtils.readTag(buf); + this.itemStack = new ItemStack(nbt); + } else { + this.itemStack = ItemStack.EMPTY; + } + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeLong(subnetId); + buf.writeLong(pos); + buf.writeInt(side); + buf.writeByte(action.ordinal()); + buf.writeInt(partitionSlot); + + if (!itemStack.isEmpty()) { + buf.writeBoolean(true); + NBTTagCompound nbt = new NBTTagCompound(); + itemStack.writeToNBT(nbt); + ByteBufUtils.writeTag(buf, nbt); + } else { + buf.writeBoolean(false); + } + } + + public static class Handler implements IMessageHandler { + + @Override + public IMessage onMessage(PacketSubnetPartitionAction message, MessageContext ctx) { + ctx.getServerHandler().player.getServerWorld().addScheduledTask(() -> { + if (ctx.getServerHandler().player.openContainer instanceof ContainerCellTerminalBase) { + ContainerCellTerminalBase container = + (ContainerCellTerminalBase) ctx.getServerHandler().player.openContainer; + container.handleSubnetPartitionAction( + message.subnetId, message.pos, message.side, + message.action, message.partitionSlot, message.itemStack); + } + }); + + return null; + } + } +} diff --git a/src/main/java/com/cellterminal/network/PacketTempCellAction.java b/src/main/java/com/cellterminal/network/PacketTempCellAction.java index 0c540b9..9b16d33 100644 --- a/src/main/java/com/cellterminal/network/PacketTempCellAction.java +++ b/src/main/java/com/cellterminal/network/PacketTempCellAction.java @@ -26,7 +26,11 @@ public enum Action { /** Extract cell from temp area back to player inventory */ EXTRACT, /** Send cell from temp area to first available slot in ME network */ - SEND + SEND, + /** Insert an upgrade card into a temp cell */ + UPGRADE, + /** Swap cell in temp area with cell held on cursor */ + SWAP } private Action action; diff --git a/src/main/java/com/cellterminal/part/PartCellTerminal.java b/src/main/java/com/cellterminal/part/PartCellTerminal.java index 825e15b..8548a4c 100644 --- a/src/main/java/com/cellterminal/part/PartCellTerminal.java +++ b/src/main/java/com/cellterminal/part/PartCellTerminal.java @@ -12,7 +12,6 @@ import net.minecraftforge.items.IItemHandler; import appeng.api.parts.IPartModel; -import appeng.api.storage.ICellWorkbenchItem; import appeng.parts.PartModel; import appeng.parts.reporting.AbstractPartDisplay; import appeng.tile.inventory.AppEngInternalInventory; @@ -92,38 +91,6 @@ public AppEngInternalInventory getTempCellInventory() { return this.tempCellInventory; } - /** - * Check if a slot accepts the given item (must be a storage cell). - */ - public boolean isValidTempCellItem(ItemStack stack) { - if (stack.isEmpty()) return true; - - return stack.getItem() instanceof ICellWorkbenchItem; - } - - /** - * Get the number of currently stored temp cells. - */ - public int getTempCellCount() { - int count = 0; - for (int i = 0; i < tempCellInventory.getSlots(); i++) { - if (!tempCellInventory.getStackInSlot(i).isEmpty()) count++; - } - - return count; - } - - /** - * Get the first empty slot index, or -1 if full. - */ - public int getFirstEmptyTempSlot() { - for (int i = 0; i < tempCellInventory.getSlots(); i++) { - if (tempCellInventory.getStackInSlot(i).isEmpty()) return i; - } - - return -1; - } - @Override public IItemHandler getInventoryByName(String name) { if ("tempCells".equals(name)) return this.tempCellInventory; diff --git a/src/main/java/com/cellterminal/proxy/ClientProxy.java b/src/main/java/com/cellterminal/proxy/ClientProxy.java index d12aef0..54b3699 100644 --- a/src/main/java/com/cellterminal/proxy/ClientProxy.java +++ b/src/main/java/com/cellterminal/proxy/ClientProxy.java @@ -11,6 +11,7 @@ import com.cellterminal.client.KeyBindings; import com.cellterminal.client.KeyInputHandler; import com.cellterminal.client.UpgradeTooltipHandler; +import com.cellterminal.client.WUTTooltipHandler; import com.cellterminal.integration.AE2WUTIntegration; @@ -42,6 +43,9 @@ public void init(FMLInitializationEvent event) { // Register upgrade tooltip handler MinecraftForge.EVENT_BUS.register(new UpgradeTooltipHandler()); + // Register WUT tooltip handler (shows temp cell count on WUT items) + MinecraftForge.EVENT_BUS.register(new WUTTooltipHandler()); + // Register key input handler for world-context keybinds (wireless terminal) MinecraftForge.EVENT_BUS.register(new KeyInputHandler()); diff --git a/src/main/java/com/cellterminal/util/SafeMath.java b/src/main/java/com/cellterminal/util/SafeMath.java index 9f60be8..3885b53 100644 --- a/src/main/java/com/cellterminal/util/SafeMath.java +++ b/src/main/java/com/cellterminal/util/SafeMath.java @@ -45,10 +45,7 @@ public static long subtractExact(final long a, final long b) { * @return true if a + b would overflow a long */ public static boolean wouldOverflow(final long a, final long b) { - if (b > 0 && a > Long.MAX_VALUE - b) return true; - if (b < 0 && a < Long.MIN_VALUE - b) return true; - - return false; + return (b > 0 && a > Long.MAX_VALUE - b) || (b < 0 && a < Long.MIN_VALUE - b); } /** diff --git a/src/main/resources/assets/cellterminal/lang/en_us.lang b/src/main/resources/assets/cellterminal/lang/en_us.lang index 2ad2e6c..6456b80 100644 --- a/src/main/resources/assets/cellterminal/lang/en_us.lang +++ b/src/main/resources/assets/cellterminal/lang/en_us.lang @@ -2,6 +2,7 @@ item.cellterminal.cell_terminal.name=Cell Terminal item.cellterminal.wireless_cell_terminal.name=Wireless Cell Terminal item.cellterminal.cell_terminal.tooltip=§bFor all your partitioning needs! +item.cellterminal.wireless_cell_terminal.temp_cells=§7Temporary Cells: §f%d # GUI gui.cellterminal.cell_terminal.title=Cell Terminal @@ -279,6 +280,13 @@ gui.cellterminal.search_help.field_double_click=§bDouble-click the search box t gui.cellterminal.cell.partitionall=Set Contents to Partition gui.cellterminal.cell.clearpartition=Clear Partitions +# Small widget button tooltips +gui.cellterminal.button.do_partition=Add All to Partition +gui.cellterminal.button.clear_partition=Clear Partition +gui.cellterminal.button.read_only=Extract Only +gui.cellterminal.button.write_only=Insert Only +gui.cellterminal.button.read_write=Extract/Insert + # Controls help (bottom left widget) gui.cellterminal.controls.jei_drag=Drag from JEI to add. gui.cellterminal.controls.click_to_remove=Click to remove from slot. @@ -368,30 +376,18 @@ gui.cellterminal.networktools.mass_partition_bus.confirm=This will set filters o # Subnet View gui.cellterminal.subnet.default_name=%d, %d, %d -cellterminal.subnet.none=No subnets connected cellterminal.subnet.double_click_highlight=Double-click to highlight in world -cellterminal.subnet.right_click_rename=Right-click to rename -cellterminal.subnet.outbound=%d outbound (Storage Bus → Subnet) -cellterminal.subnet.inbound=%d inbound (Subnet → Interface) -cellterminal.subnet.pos=Position: %d, %d, %d -cellterminal.subnet.dim=Dimension: %d -cellterminal.subnet.no_power=No Power -cellterminal.subnet.no_access=Access Denied (Security) +cellterminal.subnet.pos=%d, %d, %d [%d] cellterminal.subnet.back=Back cellterminal.subnet.back.desc=Return to previous network view cellterminal.subnet.overview=Subnet Overview +cellterminal.subnet.overview.desc=Overview of all connected sub-networks cellterminal.subnet.main_network=Main Network cellterminal.subnet.load=Load cellterminal.subnet.load.tooltip=Load this subnet's storage view +cellterminal.subnet.load.disabled=Cannot load subnet (no power or access denied) cellterminal.subnet.click_load_main=Click Load to return to main network -# Connection Row -cellterminal.subnet.direction.outbound=Outbound Connection (Storage Bus → Subnet) -cellterminal.subnet.direction.inbound=Inbound Connection (Subnet → Interface) -cellterminal.subnet.connection.pos=Connection at: %d, %d, %d -cellterminal.subnet.filter_count=%d filter item(s) -cellterminal.subnet.no_filter=No filter configured - # Subnet Controls Help cellterminal.subnet.controls.title=§lSubnet Controls cellterminal.subnet.controls.click=Load Button: View subnet diff --git a/src/main/resources/assets/cellterminal/textures/guis/atlas.png b/src/main/resources/assets/cellterminal/textures/guis/atlas.png new file mode 100644 index 0000000..80e02f0 Binary files /dev/null and b/src/main/resources/assets/cellterminal/textures/guis/atlas.png differ