package codechicken.nei; import codechicken.core.gui.GuiScrollSlot; import codechicken.lib.gui.GuiDraw; import codechicken.lib.vec.Rectangle4i; import codechicken.nei.ItemList.AnyMultiItemFilter; import codechicken.nei.ItemList.ItemsLoadedCallback; import codechicken.nei.ItemList.NothingItemFilter; import codechicken.nei.SearchField.ISearchProvider; import codechicken.nei.api.API; import codechicken.nei.api.ItemFilter; import codechicken.nei.api.ItemFilter.ItemFilterProvider; import codechicken.nei.api.ItemInfo; import codechicken.nei.guihook.GuiContainerManager; import net.minecraft.client.Minecraft; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.EnumChatFormatting; import java.awt.*; import java.util.*; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; public class SubsetWidget extends Button implements ItemFilterProvider, ItemsLoadedCallback, ISearchProvider { public static class SubsetState { int state = 2; ArrayList<ItemStack> items = new ArrayList<ItemStack>(); } public static class SubsetTag { protected class SubsetSlot extends GuiScrollSlot { public SubsetSlot() { super(0, 0, 0, 0); setSmoothScroll(false); } @Override public int getSlotHeight(int slot) { return 18; } @Override protected int getNumSlots() { return children.size()+state.items.size(); } @Override protected void slotClicked(int slot, int button, int mx, int my, int count) { if(slot < sorted.size()) { SubsetTag tag = sorted.get(slot); if (NEIClientUtils.shiftKey()) LayoutManager.searchField.setText("@" + tag.fullname); else if (button == 0 && count >= 2) SubsetWidget.showOnly(tag); else SubsetWidget.setHidden(tag, button == 1); } else { ItemStack item = state.items.get(slot-sorted.size()); if(NEIClientUtils.controlKey()) NEIClientUtils.cheatItem(item, button, -1); else SubsetWidget.setHidden(state.items.get(slot-sorted.size()), button == 1); } } @Override protected void drawSlot(int slot, int x, int y, int mx, int my, float frame) { int w = windowBounds().width; Rectangle4i r = new Rectangle4i(x, y, w, getSlotHeight(slot)); if(slot < sorted.size()) { SubsetTag tag = sorted.get(slot); LayoutManager.getLayoutStyle().drawSubsetTag(tag.displayName(), x, y, r.w, r.h, tag.state.state, r.contains(mx, my)); } else { ItemStack stack = state.items.get(slot-sorted.size()); boolean hidden = SubsetWidget.isHidden(stack); int itemx = w/2-8; int itemy = 1; LayoutManager.getLayoutStyle().drawSubsetTag(null, x, y, r.w, r.h, hidden ? 0 : 2, false); GuiContainerManager.drawItem(x+itemx, y+itemy, stack); if(new Rectangle4i(itemx, itemy, 16, 16).contains(mx, my)) SubsetWidget.hoverStack = stack; } } @Override public void drawOverlay(float frame) { } @Override public void drawBackground(float frame) { drawRect(x, y, x+width, y+height, 0xFF202020); } @Override public int scrollbarAlignment() { return -1; } @Override public void drawScrollbar(float frame) { if(hasScrollbar()) super.drawScrollbar(frame); } @Override public int scrollbarGuideAlignment() { return 0; } } public final String fullname; public final ItemFilter filter; public TreeMap<String, SubsetTag> children = new TreeMap<String, SubsetTag>(); public List<SubsetTag> sorted = Collections.emptyList(); private int childwidth; protected String displayName; protected SubsetState state = new SubsetState(); protected final SubsetSlot slot = new SubsetSlot(); private SubsetTag selectedChild; private int visible; public SubsetTag(String fullname) { this(fullname, new NothingItemFilter()); } public SubsetTag(String fullname, ItemFilter filter) { assert filter != null : "Filter cannot be null"; this.fullname = EnumChatFormatting.getTextWithoutFormattingCodes(fullname); this.filter = filter; if(fullname != null) { int idx = fullname.lastIndexOf('.'); displayName = idx < 0 ? fullname : fullname.substring(idx + 1); } } public String displayName() { return displayName; } public String name() { int idx = fullname.indexOf('.'); return idx < 0 ? fullname : fullname.substring(idx+1); } public String parent() { int idx = fullname.lastIndexOf('.'); return idx < 0 ? null : fullname.substring(0, idx); } private SubsetTag getTag(String name) { int idx = name.indexOf('.'); String childname = idx > 0 ? name.substring(0, idx) : name; SubsetTag child = children.get(childname.toLowerCase()); if(child == null) return null; return idx > 0 ? child.getTag(name.substring(idx+1)) : child; } private void recacheChildren() { sorted = new ArrayList<SubsetTag>(children.values()); childwidth = 0; for(SubsetTag tag : sorted) childwidth = Math.max(childwidth, tag.nameWidth()+2); } private void addTag(SubsetTag tag) { String name = fullname == null ? tag.fullname : tag.fullname.substring(fullname.length()+1); int idx = name.indexOf('.'); if(idx < 0) {//add or replace tag SubsetTag prev = children.put(name.toLowerCase(), tag); if(prev != null) {//replaced, load children tag.children = prev.children; tag.sorted = prev.sorted; } recacheChildren(); } else { String childname = name.substring(0, idx); SubsetTag child = children.get(childname.toLowerCase()); if(child == null) children.put(childname.toLowerCase(), child = new SubsetTag(fullname == null ? childname : fullname+'.'+childname)); recacheChildren(); child.addTag(tag); } } protected void cacheState() { state = SubsetWidget.getState(this); for(SubsetTag tag : sorted) tag.cacheState(); } public void addFilters(List<ItemFilter> filters) { if(filter != null) filters.add(filter); for(SubsetTag child : sorted) child.addFilters(filters); } public void search(List<SubsetTag> tags, Pattern p) { if(fullname != null && p.matcher(fullname.toLowerCase()).find()) tags.add(this); else for(SubsetTag child : sorted) child.search(tags, p); } public void updateVisiblity(int mx, int my) { if(selectedChild != null) { selectedChild.updateVisiblity(mx, my); if(!selectedChild.isVisible()) selectedChild = null; } if(slot.contains(mx, my) && (selectedChild == null || !selectedChild.contains(mx, my))) { int mslot = slot.getClickedSlot(my); if(mslot >= 0 && mslot < sorted.size()) { SubsetTag mtag = sorted.get(mslot); if(mtag != null) { if(mtag != selectedChild && selectedChild != null) selectedChild.setHidden(); selectedChild = mtag; selectedChild.setVisible(); } } setVisible(); } if(selectedChild == null) countdownVisible(); } public void setHidden() { visible = 0; slot.mouseReleased(0, 0, 0);//cancel any scrolling if(selectedChild != null) { selectedChild.setHidden(); selectedChild = null; } } public void setVisible() { visible = 10; cacheState(); } private void countdownVisible() { if(visible > 0 && --visible == 0) setHidden(); } public void resize(int x, int pwidth, int y) { int mheight = area.h; int dheight = area.y2()-y; int cheight = slot.contentHeight(); int height = cheight; if(cheight > mheight) { y = area.y; height = mheight; } else if(cheight > dheight) { y = area.y2() - cheight; } height = height/slot.getSlotHeight(0)*slot.getSlotHeight(0);//floor to a multiple of slot height int width = childwidth; if(!state.items.isEmpty()) width = Math.max(childwidth, 18); if(slot.contentHeight() > height) width+=slot.scrollbarDim().width; boolean fitLeft = x-width >= area.x1(); boolean fitRight = x+width+pwidth <= area.x2(); if(pwidth >= 0 ? !fitRight && fitLeft : !fitLeft) pwidth*=-1;//swap x += pwidth >= 0 ? pwidth : -width; slot.setSize(x, y, width, height); slot.setMargins(slot.hasScrollbar() ? slot.scrollbarDim().width : 0, 0, 0, 0); if(selectedChild != null) { y = slot.getSlotY(sorted.indexOf(selectedChild))-slot.scrolledPixels()+slot.y; selectedChild.resize(x, pwidth >= 0 ? width : -width, y); } } protected int nameWidth() { return Minecraft.getMinecraft().fontRendererObj.getStringWidth(displayName()); } public boolean isVisible() { return visible > 0; } public void draw(int mx, int my) { slot.draw(mx, my, 0); if(selectedChild != null) selectedChild.draw(mx, my); } public boolean contains(int px, int py) { return slot.contains(px, py) || selectedChild != null && selectedChild.contains(px, py); } public void mouseClicked(int mx, int my, int button) { if(selectedChild != null && selectedChild.contains(mx, my)) selectedChild.mouseClicked(mx, my, button); else if(slot.contains(mx, my)) slot.mouseClicked(mx, my, button); } public void mouseDragged(int mx, int my, int button, long heldTime) { slot.mouseDragged(mx, my, button, heldTime); if(selectedChild != null) selectedChild.mouseDragged(mx, my, button, heldTime); } public void mouseUp(int mx, int my, int button) { slot.mouseReleased(mx, my, button); if(selectedChild != null) selectedChild.mouseUp(mx, my, button); } public boolean mouseScrolled(int mx, int my, int scroll) { if(slot.hasScrollbar() && slot.contains(mx, my)) { slot.scroll(scroll); return true; } if(selectedChild != null && selectedChild.mouseScrolled(mx, my, scroll)) return true; if(slot.hasScrollbar() && !contains(mx, my)) { slot.scroll(scroll); return true; } return false; } public boolean isScrolling() { return slot.isScrolling() || selectedChild != null && selectedChild.isScrolling(); } } protected static final SubsetTag root = new SubsetTag(null); public static Rectangle4i area = new Rectangle4i(); public static ItemStack hoverStack; private static HashMap<String, SubsetState> subsetState = new HashMap<String, SubsetState>(); /** * All operations on this variable should be synchronised. */ private static final ItemStackSet hiddenItems = new ItemStackSet(); private static final AtomicReference<NBTTagList> dirtyHiddenItems = new AtomicReference<NBTTagList>(); public static SubsetState getState(SubsetTag tag) { SubsetState state = subsetState.get(tag.fullname); return state == null ? new SubsetState() : state; } public static void addTag(SubsetTag tag) { updateState.stop(); synchronized (root) { root.addTag(tag); updateState.reallocate(); } } public static SubsetTag getTag(String name) { return name == null ? root : root.getTag(name); } public static boolean isHidden(ItemStack item) { synchronized (hiddenItems) { return hiddenItems.contains(item); } } private static void _setHidden(SubsetTag tag, boolean hidden) { for(ItemStack item : getState(tag).items) _setHidden(item, hidden); for(SubsetTag child : tag.sorted) _setHidden(child, hidden); } private static void _setHidden(ItemStack item, boolean hidden) { if(hidden) hiddenItems.add(item); else hiddenItems.remove(item); } public static void showOnly(SubsetTag tag) { synchronized (hiddenItems) { for(ItemStack item : ItemList.items) _setHidden(item, true); setHidden(tag, false); } } public static void setHidden(SubsetTag tag, boolean hidden) { synchronized (hiddenItems) { _setHidden(tag, hidden); updateHiddenItems(); } } public static void setHidden(ItemStack item, boolean hidden) { synchronized (hiddenItems) { _setHidden(item, hidden); updateHiddenItems(); } } public static void unhideAll() { synchronized (hiddenItems) { hiddenItems.clear(); updateHiddenItems(); } } private static void updateHiddenItems() { prepareDirtyHiddenItems.restart(); updateState.restart(); } public static void loadHidden() { synchronized (hiddenItems) { hiddenItems.clear(); } List<ItemStack> itemList = new LinkedList<ItemStack>(); try { NBTTagList list = NEIClientConfig.world.nbt.getTagList("hiddenItems", 10); for(int i = 0; i < list.tagCount(); i++) itemList.add(ItemStack.loadItemStackFromNBT(list.getCompoundTagAt(i))); } catch (Exception e) { NEIClientConfig.logger.error("Error loading hiddenItems", e); return; } synchronized (hiddenItems) { for(ItemStack item : itemList) hiddenItems.add(item); } updateState.restart(); } private static void saveHidden() { NBTTagList list = dirtyHiddenItems.getAndSet(null); if(list != null) { NEIClientConfig.world.nbt.setTag("hiddenItems", list); NEIClientConfig.world.saveNBT(); } } private static final RestartableTask prepareDirtyHiddenItems = new RestartableTask("NEI Subset Save Thread") { private List<ItemStack> getList() { synchronized (hiddenItems) { return hiddenItems.values(); } } @Override public void execute() { NBTTagList list = new NBTTagList(); for(ItemStack item : getList()) { if(interrupted()) return; NBTTagCompound tag = new NBTTagCompound(); item.writeToNBT(tag); list.appendTag(tag); } dirtyHiddenItems.set(list); } }; private static final UpdateStateTask updateState = new UpdateStateTask(); private static class UpdateStateTask extends RestartableTask { private volatile boolean reallocate; public UpdateStateTask() { super("NEI Subset Item Allocation"); } @Override public void clearTasks() { super.clearTasks(); reallocate = false; } public synchronized void reallocate() { reallocate = true; restart(); } @Override public void execute() { HashMap<String, SubsetState> state = new HashMap<String, SubsetState>(); List<SubsetTag> tags = new LinkedList<SubsetTag>(); synchronized (root) { cloneStates(root, tags, state); if(interrupted()) return; } if(reallocate) { for (ItemStack item : ItemList.items) { if(interrupted()) return; if(ItemInfo.isHidden(item)) continue; for (SubsetTag tag : tags) if (tag.filter.matches(item)) state.get(tag.fullname).items.add(item); } } synchronized (root) { calculateVisibility(root, state); if(interrupted()) return; } subsetState = state; ItemList.updateFilter.restart(); } private void cloneStates(SubsetTag tag, List<SubsetTag> tags, HashMap<String, SubsetState> state) { for(SubsetTag child : tag.sorted) { if(interrupted()) return; cloneStates(child, tags, state); } tags.add(tag); SubsetState sstate = new SubsetState(); if(!reallocate) sstate.items = SubsetWidget.getState(tag).items; state.put(tag.fullname, sstate); } private void calculateVisibility(SubsetTag tag, Map<String, SubsetState> state) { SubsetState sstate = state.get(tag.fullname); int hidden = 0; for(SubsetTag child : tag.sorted) { if(interrupted()) return; calculateVisibility(child, state); int cstate = state.get(child.fullname).state; if(cstate == 1) sstate.state = 1; else if(cstate == 0) hidden++; } if(sstate.state == 1) return; List<ItemStack> items = sstate.items; for(ItemStack item : items) { if(interrupted()) return; if (isHidden(item)) hidden++; } if(hidden == tag.sorted.size()+items.size()) sstate.state = 0; else if(hidden > 0) sstate.state = 1; } } private long lastclicktime; public SubsetWidget() { super("NEI Subsets"); API.addItemFilter(this); API.addSearchProvider(this); ItemList.loadCallbacks.add(this); } @Override public void draw(int mx, int my) { super.draw(mx, my); area.set(x, y + h, w, LayoutManager.searchField.y - h - y); //23 for the search box hoverStack = null; if (root.isVisible()) { root.resize(area.x, 0, area.y); root.cacheState(); root.draw(mx, my); } } @Override public void update() { Point mouse = GuiDraw.getMousePosition(); updateVisiblity(mouse.x, mouse.y); saveHidden(); } private void updateVisiblity(int mx, int my) { if(!root.isVisible() || root.isScrolling()) return; root.updateVisiblity(mx, my); if(!root.isVisible() && bounds().contains(mx, my)) root.setVisible(); } @Override public boolean contains(int px, int py) { return super.contains(px, py) || root.isVisible() && root.contains(px, py); } @Override public boolean handleClick(int mx, int my, int button) { if(root.isVisible() && root.contains(mx, my)) { root.mouseClicked(mx, my, button); return true; } if(button == 0) { if(System.currentTimeMillis() - lastclicktime < 500) unhideAll(); else root.setVisible(); NEIClientUtils.playClickSound(); lastclicktime = System.currentTimeMillis(); } return true; } /** * Not called */ @Override public boolean onButtonPress(boolean rightclick) { return false; } @Override public void mouseDragged(int mx, int my, int button, long heldTime) { if(root.isVisible()) root.mouseDragged(mx, my, button, heldTime); } @Override public void mouseUp(int mx, int my, int button) { if(root.isVisible()) root.mouseUp(mx, my, button); } @Override public boolean onMouseWheel(int i, int mx, int my) { return root.isVisible() && root.mouseScrolled(mx, my, -i); } @Override public void onGuiClick(int mx, int my) { if(!contains(mx, my)) root.setHidden(); } @Override public ItemStack getStackMouseOver(int mx, int my) { return hoverStack; } @Override public ItemFilter getFilter() { return new ItemFilter()//synchronise access on hiddenItems { @Override public boolean matches(ItemStack item) { synchronized (hiddenItems) { return !hiddenItems.matches(item); } } }; } @Override public boolean isPrimary() { return true; } @Override public ItemFilter getFilter(String searchText) { if(!searchText.startsWith("@")) return null; searchText = searchText.substring(1); AnyMultiItemFilter filter = new AnyMultiItemFilter(); SubsetTag tag = getTag(searchText); if(tag != null) tag.addFilters(filter.filters); else { Pattern p = SearchField.getPattern(searchText); if(p == null) return null; List<SubsetTag> matching = new LinkedList<SubsetTag>(); root.search(matching, p); if(matching.isEmpty()) return null; for(SubsetTag tag2 : matching) tag2.addFilters(filter.filters); } return filter; } @Override public void itemsLoaded() { updateState.reallocate(); } }