<|start_filename|>src/test/java/mnm/mods/util/test/LocationTests.java<|end_filename|>
package mnm.mods.util.test;

import static org.junit.Assert.*;

import org.junit.Test;

import mnm.mods.util.ILocation;
import mnm.mods.util.Location;

public class LocationTests {

    @Test
    public void contains() {

        // inside
        ILocation loc1 = new Location(1, 1, 16, 16);
        ILocation loc2 = new Location(2, 2, 15, 15);
        assertTrue(loc1.contains(loc2));
        assertTrue(loc2.contains(loc1));

        // corner intersection
        loc1 = new Location(15, 0, 12, 6);
        loc2 = new Location(17, 4, 12, 6);
        assertTrue(loc1.contains(loc2));
        assertTrue(loc2.contains(loc1));

        // sides
        loc1 = new Location(16, 16, 16, 16);
        loc2 = new Location(18, 16, 16, 16);
        assertTrue(loc1.contains(loc2));
        assertTrue(loc2.contains(loc1));

        loc1 = new Location(5, 5, 10, 2);
        loc2 = new Location(16, 9, 1, 1);
        assertFalse(loc1.contains(loc2));
        assertFalse(loc2.contains(loc1));

        // edge cases
        loc1 = new Location(2, 1, 1, 1);
        loc2 = new Location(1, 1, 1, 1);
        assertFalse(loc1.contains(loc2));
        assertFalse(loc2.contains(loc1));

        loc1 = new Location(16, 16, 16, 16);
        loc2 = new Location(16, 32, 16, 16);
        assertFalse(loc1.contains(loc2));
        assertFalse(loc2.contains(loc1));
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/BorderLayout.java<|end_filename|>
package mnm.mods.util.gui;

import com.google.common.collect.Maps;
import mnm.mods.util.ILocation;
import mnm.mods.util.Location;

import java.awt.Dimension;
import java.util.EnumMap;
import java.util.Map.Entry;

/**
 * A recreation of Border Layout.
 *
 * @author <NAME>
 * @see java.awt.BorderLayout
 */
public class BorderLayout implements ILayout {

    private EnumMap<Position, GuiComponent> components = Maps.newEnumMap(Position.class);

    @Override
    public void addComponent(GuiComponent comp, Object constraints) {
        if (constraints == null || constraints instanceof Position) {
            addComponent((Position) constraints, comp);
        } else {
            throw new IllegalArgumentException(
                    "cannot add to layout: constraint must be a Position enum");
        }
    }

    private synchronized void addComponent(Position constraint, GuiComponent comp) {
        if (constraint == null) {
            constraint = Position.CENTER;
        }

        components.put(constraint, comp);
    }

    @Override
    public synchronized void removeComponent(GuiComponent comp) {
        components.entrySet().stream()
                .filter(c -> c.getValue().equals(comp))
                .findFirst()
                .ifPresent(c -> components.remove(c.getKey()));
    }

    @Override
    public void layoutComponents(GuiPanel parent) {
        ILocation pbounds = parent.getLocation();
        GuiComponent center = components.get(Position.CENTER);
        GuiComponent north = components.get(Position.NORTH);
        GuiComponent south = components.get(Position.SOUTH);
        GuiComponent west = components.get(Position.WEST);
        GuiComponent east = components.get(Position.EAST);

        if (north != null) {
            north.setLocation(new Location(0, 0, pbounds.getWidth(), north.getMinimumSize().height));
        }

        if (west != null) {
            Location bounds = new Location();
            bounds.setWidth(west.getMinimumSize().width);

            if (north != null) {
                bounds.setYPos(north.getLocation().getHeight());
            }

            if (south == null) {
                if (north == null) {
                    bounds.setHeight(pbounds.getHeight());
                } else {
                    bounds.setHeight(pbounds.getHeight() - north.getLocation().getHeight());
                }
            } else {
                if (north == null) {
                    bounds.setHeight(pbounds.getHeight() - south.getLocation().getHeight());
                } else {
                    bounds.setHeight(pbounds.getHeight() - south.getLocation().getHeight() - north.getLocation().getHeight());
                }
            }
            west.setLocation(bounds);
        }

        if (center != null) {
            Location bounds = new Location();

            if (north != null) {
                bounds.setYPos(north.getLocation().getHeight() + 1);
            }

            if (west != null) {
                bounds.setXPos(west.getLocation().getWidth());
            }

            if (east == null) {
                if (west == null) {
                    bounds.setWidth(pbounds.getWidth());
                } else {
                    bounds.setWidth(pbounds.getWidth() - west.getLocation().getWidth());
                }
            } else {
                if (west == null) {
                    bounds.setWidth(pbounds.getWidth() - east.getLocation().getWidth());
                } else {
                    bounds.setWidth(pbounds.getWidth() - east.getLocation().getWidth() - west.getLocation().getWidth());
                }
            }

            if (south == null) {
                if (north == null) {
                    bounds.setHeight(pbounds.getHeight());
                } else {
                    bounds.setHeight(pbounds.getHeight() - north.getLocation().getHeight());
                }
            } else {
                if (north == null) {
                    bounds.setHeight(pbounds.getHeight() - south.getLocation().getHeight() - 1);
                } else {
                    bounds.setHeight(pbounds.getHeight() - south.getLocation().getHeight() - north.getLocation().getHeight() - 2);
                }
            }
            center.setLocation(bounds);
        }

        if (east != null) {
            Location bounds = new Location();

            bounds.setXPos(pbounds.getWidth() - east.getMinimumSize().width);
            bounds.setWidth(east.getMinimumSize().width);
            if (north != null) {
                bounds.setYPos(north.getLocation().getHeight());
            }
            if (south == null) {
                if (north == null) {
                    bounds.setHeight(pbounds.getHeight());
                } else {
                    bounds.setHeight(pbounds.getHeight() - north.getMinimumSize().height);
                }
            } else {
                if (north == null) {
                    bounds.setHeight(pbounds.getHeight() - south.getMinimumSize().height);
                } else {
                    bounds.setHeight(pbounds.getHeight() - south.getMinimumSize().height - north.getMinimumSize().height);
                }
            }
            east.setLocation(bounds);
        }

        if (south != null) {

            int x = 0;
            int y = pbounds.getHeight() - south.getLocation().getHeight();
            int width = pbounds.getWidth();
            int height = south.getMinimumSize().height;

            south.setLocation(new Location(x, y, width, height));
        }
    }

    @Override
    public Dimension getLayoutSize() {
        int width = 0;
        int height = 0;
        for (Entry<Position, GuiComponent> comp : components.entrySet()) {
            Dimension size = comp.getValue().getMinimumSize();
            switch (comp.getKey()) {
            case CENTER:
                width += size.width;
                height += size.height;
                break;
            case EAST:
            case WEST:
                width += size.width;
                break;
            case NORTH:
            case SOUTH:
                height += size.height;
                break;
            }
        }
        return new Dimension(width, height);
    }

    public enum Position {
        NORTH,
        SOUTH,
        EAST,
        WEST,
        CENTER
    }
}


<|start_filename|>src/main/java/mnm/mods/util/Location.java<|end_filename|>
package mnm.mods.util;


public class Location implements ILocation {

    private int xPos;
    private int yPos;
    private int width;
    private int height;

    public Location() {
        this(0, 0, 1, 1);
    }

    public Location(int xPos, int yPos, int width, int height) {
        this.xPos = xPos;
        this.yPos = yPos;
        this.width = width;
        this.height = height;
    }

    @Override
    public int getXPos() {
        return this.xPos;
    }

    @Override
    public int getYPos() {
        return this.yPos;
    }

    @Override
    public int getWidth() {
        return this.width;
    }

    @Override
    public int getHeight() {
        return this.height;
    }

    public Location setXPos(int xPos) {
        this.xPos = xPos;
        return this;
    }

    public Location setYPos(int yPos) {
        this.yPos = yPos;
        return this;
    }

    public Location setWidth(int width) {
        this.width = width;
        return this;
    }

    public Location setHeight(int height) {
        this.height = height;
        return this;
    }

    public Location move(int x, int y) {
        this.xPos += x;
        this.yPos += y;
        return this;
    }

    public Location scale(float scale) {
        this.xPos *= scale;
        this.yPos *= scale;
        this.width *= scale;
        this.height *= scale;
        return this;
    }

    @Override
    public String toString() {
        return "Location [xPos=" + xPos + ", yPos=" + yPos + ", width=" + width + ", height=" + height + "]";
    }

    @Override
    public int hashCode() {
        // only ImmutableLocation should be used in Maps
        final int prime = 31;
        int result = 1;
        result = prime * result + height;
        result = prime * result + width;
        result = prime * result + xPos;
        result = prime * result + yPos;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || !(obj instanceof ILocation))
            return false;
        ILocation other = (ILocation) obj;
        return height == other.getHeight()
                && width == other.getWidth()
                && xPos == other.getXPos()
                && yPos == other.getYPos();
    }

    @Override
    public ILocation asImmutable() {
        return new ImmutableLocation(getXPos(), getYPos(), getWidth(), getHeight());
    }

    public static Location copyOf(ILocation location) {
        return new Location(location.getXPos(), location.getYPos(), location.getWidth(), location.getHeight());
    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/IGuiInput.java<|end_filename|>
package mnm.mods.util.gui;

/**
 * An input for guis. Provides a getter and setter
 *
 * @author Matthew
 * @param <T> The input type
 */
public interface IGuiInput<T> {

    /**
     * Gets the value.
     *
     * @return The value
     */
    T getValue();

    /**
     * Sets the value
     *
     * @param value The value
     */
    void setValue(T value);
}


<|start_filename|>src/main/java/mnm/mods/util/Translatable.java<|end_filename|>
package mnm.mods.util;

import net.minecraft.client.resources.I18n;

/**
 * Translatable strings. Translations are done via
 * {@link I18n#format(String, Object...)}.
 *
 * @author Matthew
 */
public interface Translatable {

    /**
     * Gets the unlocalized string for this translation
     *
     * @return The untranslated string
     */
    String getUnlocalized();

    /**
     * Translates this string.
     *
     * </pre>
     *
     * @param params Translation parameters
     * @return The translated string
     */
    default String translate(Object... params) {
        return I18n.format(getUnlocalized(), params);
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/util/TabbyRef.java<|end_filename|>
package mnm.mods.tabbychat.util;

import com.google.common.primitives.Doubles;
import mnm.mods.util.update.VersionData;

public final class TabbyRef {

    // Core
    public static final String MOD_ID = "TabbyChat2";
    public static final String MOD_NAME = "TabbyChat 2";
    public static final String MOD_VERSION = "@VERSION@";
    public static final double MOD_REVISION = getRevision();
    public static final String UPDATE_URL = "https://raw.githubusercontent.com/killjoy1221/Version/master/TabbyChat2.json";
    public static final String URL = "https://minecraft.curseforge.com/projects/tabbychat-2";

    private static double getRevision() {
        Double d = Doubles.tryParse("@REVISION@");
        return d != null ? d : -1;
    }

    public static VersionData getVersionData() {
        return new VersionData(MOD_NAME, UPDATE_URL, URL, MOD_REVISION);
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiWrappedComponent.java<|end_filename|>
package mnm.mods.util.gui;

import com.google.common.eventbus.EventBus;
import mnm.mods.util.Color;
import mnm.mods.util.ILocation;
import net.minecraft.util.text.ITextComponent;

import java.awt.Dimension;
import java.util.Optional;
import javax.annotation.Nonnull;

public class GuiWrappedComponent<T extends GuiComponent> extends GuiComponent {

    private final T wrapper;

    public GuiWrappedComponent(@Nonnull T wrap) {
        this.wrapper = wrap;
        this.getBus().register(this);
    }

    public T getComponent() {
        return wrapper;
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        wrapper.drawComponent(mouseX, mouseY);
    }

    @Override
    public void updateComponent() {
        wrapper.updateComponent();
    }

    @Override
    public void handleMouseInput() {
        wrapper.handleMouseInput();
    }

    @Override
    public void handleKeyboardInput() {
        wrapper.handleKeyboardInput();
    }

    @Override
    public ILocation getLocation() {
        return wrapper.getLocation();
    }

    @Override
    public void setLocation(ILocation location) {
        wrapper.setLocation(location);
    }

    @Override
    public EventBus getBus() {
        return wrapper.getBus();
    }

    @Override
    public Optional<GuiPanel> getParent() {
        return wrapper.getParent();
    }

    @Override
    void setParent(GuiPanel guiPanel) {
        wrapper.setParent(guiPanel);
    }

    @Override
    public void setMinimumSize(Dimension size) {
        wrapper.setMinimumSize(size);
    }

    @Override
    public Dimension getMinimumSize() {
        return wrapper.getMinimumSize();
    }

    @Override
    public void setScale(float scale) {
        wrapper.setScale(scale);
    }

    @Override
    public float getScale() {
        return wrapper.getScale();
    }

    @Override
    public Optional<Color> getPrimaryColor() {
        return wrapper.getPrimaryColor();
    }

    @Override
    public void setPrimaryColor(Color color) {
        wrapper.setPrimaryColor(color);
    }

    @Override
    public Optional<Color> getSecondaryColor() {
        return wrapper.getSecondaryColor();
    }

    @Override
    public void setSecondaryColor(Color color) {
        wrapper.setSecondaryColor(color);
    }

    @Override
    public boolean isEnabled() {
        return wrapper.isEnabled();
    }

    @Override
    public void setEnabled(boolean enabled) {
        wrapper.setEnabled(enabled);
    }

    @Override
    public boolean isVisible() {
        return wrapper.isVisible();
    }

    @Override
    public void setVisible(boolean visible) {
        wrapper.setVisible(visible);
    }

    @Override
    public boolean isHovered() {
        return wrapper.isHovered();
    }

    @Override
    public void setCaption(ITextComponent text) {
        wrapper.setCaption(text);
    }

    @Override
    public Optional<ITextComponent> getCaptionText() {
        return wrapper.getCaptionText();
    }

    @Override
    public boolean isFocusable() {
        return wrapper.isFocusable();
    }

    @Override
    public boolean isFocused() {
        return wrapper.isFocused();
    }

    @Override
    public void setFocused(boolean focused) {
        wrapper.setFocused(focused);
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/gui/ChatInput.java<|end_filename|>
package mnm.mods.tabbychat.api.gui;

import java.util.List;

/**
 * The chat input is responsible for storing the chat to be sent before it gets
 * sent in a user friendly way. Seriously.. it's a wrapping text box.
 */
public interface ChatInput extends IGui {

    /**
     * Get the contents of the text field wrapped with the width.
     *
     * @return The wrapped lines
     */
    List<String> getWrappedLines();

    void setText(String text);

    String getText();
}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiRectangle.java<|end_filename|>
package mnm.mods.util.gui;

import mnm.mods.util.ILocation;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.util.ResourceLocation;

/**
 * Represents a colored area. Transparency is represented using a transparency
 * grid.
 *
 * @author Matthew
 */
public class GuiRectangle extends GuiComponent {

    private static final ResourceLocation TRANSPARENCY = new ResourceLocation("mnmutils", "textures/transparency.png");

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        GlStateManager.enableAlpha();
        mc.getTextureManager().bindTexture(TRANSPARENCY);
        ILocation loc = getLocation();
        Gui.drawModalRectWithCustomSizedTexture(0, 0, 0, 0, loc.getWidth(), loc.getHeight(), 5, 5);
        Gui.drawRect(0, 0, loc.getWidth(), loc.getHeight(), getPrimaryColorProperty().getHex());
        GlStateManager.disableBlend();
        super.drawComponent(mouseX, mouseY);
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/util/ChatVisibility.java<|end_filename|>
package mnm.mods.tabbychat.util;

public enum ChatVisibility {

    ALWAYS,
    NORMAL,
    HIDDEN

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/gui/ChatBox.java<|end_filename|>
package mnm.mods.tabbychat.gui;

import com.google.common.eventbus.Subscribe;
import com.mumfrey.liteloader.core.LiteLoader;
import mnm.mods.tabbychat.TabbyChat;
import mnm.mods.tabbychat.api.gui.ChatGui;
import mnm.mods.tabbychat.settings.TabbySettings;
import mnm.mods.tabbychat.util.ScaledDimension;
import mnm.mods.util.ILocation;
import mnm.mods.util.Location;
import mnm.mods.util.gui.BorderLayout;
import mnm.mods.util.gui.GuiPanel;
import mnm.mods.util.gui.events.GuiMouseEvent;
import mnm.mods.util.gui.events.GuiMouseEvent.MouseEvent;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.math.MathHelper;
import org.lwjgl.input.Mouse;

import java.awt.Point;
import java.awt.Rectangle;

public class ChatBox extends GuiPanel implements ChatGui {

    public static final ResourceLocation GUI_LOCATION = new ResourceLocation("tabbychat", "textures/chatbox.png");

    private ChatArea chatArea;
    private ChatTray pnlTray;
    private TextBox txtChatInput;

    private boolean dragMode;
    private Point drag;
    private Location tempbox;

    public ChatBox(ILocation rect) {
        super(new BorderLayout());
        this.addComponent(pnlTray = new ChatTray(), BorderLayout.Position.NORTH);
        this.addComponent(chatArea = new ChatArea(), BorderLayout.Position.CENTER);
        this.addComponent(txtChatInput = new TextBox(), BorderLayout.Position.SOUTH);
        this.addComponent(new Scrollbar(chatArea), BorderLayout.Position.EAST);
        super.setLocation(rect);
        super.updateComponent();
    }

    @Subscribe
    public void killjoysMovingCompanyForAllYourFurnitureMovingNeeds(GuiMouseEvent event) {
        ILocation bounds = getLocation();

        // divide by scale because smaller scales make the point movement larger
        int x = bounds.getXPos() + event.getMouseX();
        int y = bounds.getYPos() + event.getMouseY();

        if (event.getType() == MouseEvent.CLICK) {
            if (Mouse.isButtonDown(0) && (pnlTray.isHovered() || (GuiScreen.isAltKeyDown() && isHovered()))) {
                dragMode = !pnlTray.isHandleHovered();
                drag = new Point(x, y);
                tempbox = bounds.copy();
            }
        }

        if (drag != null) {
            if (event.getType() == MouseEvent.RELEASE) {
                drag = null;
                tempbox = null;
            } else if (event.getType() == MouseEvent.DRAG) {
                if (!dragMode) {
                    setLocation(new Location(
                            tempbox.getXPos(),
                            tempbox.getYPos() + y - drag.y,
                            tempbox.getWidth() + x - drag.x,
                            tempbox.getHeight() - y + drag.y));
                    this.chatArea.markDirty();
                } else {
                    setLocation(getLocation().copy()
                            .setXPos(tempbox.getXPos() + x - drag.x)
                            .setYPos(tempbox.getYPos() + y - drag.y));
                }
            }
        }
    }

    @Override
    public float getScale() {
        return TabbyChat.getInstance().getChatGui().getChatScale();
    }

    @Override
    public void updateComponent() {
        ILocation bounds = getLocation();
        ILocation point = getActualLocation();

        float scale = getActualScale();
        ScaledResolution sr = new ScaledResolution(mc);

        // original dims
        final int x = point.getXPos();
        final int y = point.getYPos();
        final int w = (int) (bounds.getWidth() * scale);
        final int h = (int) (bounds.getHeight() * scale);

        // the new dims
        int w1 = w;
        int h1 = h;
        int x1 = x;
        int y1 = y;

        final int SCREEN_W = sr.getScaledWidth();
        final int SCREEN_H = sr.getScaledHeight();

        // limits for sizes
        // FIXME 500 and 400 max is because of texture limit
        final int MIN_W = 50;
        final int MIN_H = 50;
        final int MAX_W = Math.min(500, SCREEN_W);
        final int MAX_H = Math.min(400, SCREEN_H);

        final int HOTBAR = 25;

        // calculate width and height first
        // used to calculate max x and y
        w1 = Math.max(MIN_W, w1);
        w1 = Math.min(MAX_W, w1);
        // this is different because height anchor is at the top
        // so is affected at the bottom.
        if (h1 < MIN_H) {
            y1 -= MIN_H - h1;
            h1 = MIN_H;
        }
        if (h1 > MAX_H) {
            y1 += h1 - MAX_H;
            h1 = MAX_H;
        }

        // limits for position
        final int MIN_X = 0;
        final int MIN_Y = 0;
        final int MAX_X = SCREEN_W - w1;
        final int MAX_Y = SCREEN_H - h1 - HOTBAR;

        // calculate x and y coordinates
        x1 = Math.max(MIN_X, x1);
        x1 = Math.min(MAX_X, x1);
        y1 = Math.max(MIN_Y, y1);
        y1 = Math.min(MAX_Y, y1);

        // reset the location if it changed.
        if (x1 != x || y1 != y || w1 != w || h1 != h) {
            setLocation(new Location(
                    MathHelper.ceil(x1 / scale),
                    MathHelper.ceil(y1 / scale),
                    MathHelper.ceil(w1 / scale),
                    MathHelper.ceil(h1 / scale)));
        }
        super.updateComponent();
    }

    @Override
    public void setLocation(ILocation location) {
        super.setLocation(location);
        // save bounds
        TabbySettings sett = TabbyChat.getInstance().settings;
        sett.advanced.chatX.set(location.getXPos());
        sett.advanced.chatY.set(location.getYPos());
        sett.advanced.chatW.set(location.getWidth());
        sett.advanced.chatH.set(location.getHeight());
        LiteLoader.getInstance().writeConfig(sett);
    }

    @Override
    public void onClosed() {
        super.onClosed();
        updateComponent();
    }

    public int getWidth() {
        return getBounds().width;
    }

    @Override
    public ChatArea getChatArea() {
        return this.chatArea;
    }

    @Override
    public ChatTray getTray() {
        return this.pnlTray;
    }

    @Override
    public TextBox getChatInput() {
        return this.txtChatInput;
    }

    public void onScreenHeightResize(int oldWidth, int oldHeight, int newWidth, int newHeight) {

        if (oldWidth == 0 || oldHeight == 0)
            return; // first time!

        // measure the distance from the bottom, then subtract from new height

        ScaledDimension oldDim = new ScaledDimension(oldWidth, oldHeight);
        ScaledDimension newDim = new ScaledDimension(newWidth, newHeight);

        int bottom = oldDim.getScaledHeight() - getLocation().getYPos();
        int y = newDim.getScaledHeight() - bottom;
        this.setLocation(getLocation().copy().setYPos(y));
        this.updateComponent();
    }

    @Override
    public Rectangle getBounds() {
        return getLocation().asRectangle();
    }

}


<|start_filename|>src/main/java/mnm/mods/util/config/Value.java<|end_filename|>
package mnm.mods.util.config;

import java.util.Objects;

/**
 * A wrapper that saves a default value.
 *
 * @param <T> The type to wrap
 */
public class Value<T> {

    protected T value;

    public Value() {}

    public Value(T val) {
        set(val);
    }

    /**
     * Sets the value.
     *
     * @param val The new value
     */
    public void set(T val) {
        this.value = val;
    }

    /**
     * Gets the value.
     *
     * @return The value
     */
    public T get() {
        return this.value;
    }

    @Override
    public String toString() {
        return Objects.toString(this.value);
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/VersionData.java<|end_filename|>
package mnm.mods.tabbychat.api;

public interface VersionData {
    
    String getVersion();
    
    double getRevision();
    
}


<|start_filename|>src/main/java/mnm/mods/util/gui/events/EventHelper.java<|end_filename|>
package mnm.mods.util.gui.events;

import java.util.function.Consumer;

import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe;

public class EventHelper {

    public static <T> Listener<T> register(EventBus bus, Class<T> event, Consumer<T> listener) {
      Listener<T> lis = new Listener<>(event, listener);
      bus.register(bus);
      return lis;
    }

    public static class Listener<T> {

        private Class<T> clazz;
        private Consumer<T> listener;

        private Listener(Class<T> clazz, Consumer<T> listener) {
            this.clazz = clazz;
            this.listener = listener;
        }

        @SuppressWarnings("unchecked")
        @Subscribe
        public void event(Object o) {
            if (clazz.isInstance(o)) {
                listener.accept((T) o);
            }
        }
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/extra/ChatLogging.java<|end_filename|>
package mnm.mods.tabbychat.extra;

import com.google.common.eventbus.Subscribe;
import mnm.mods.tabbychat.TabbyChat;
import mnm.mods.tabbychat.api.events.ChatMessageEvent.ChatReceivedEvent;
import mnm.mods.util.IPUtils;
import net.minecraft.client.Minecraft;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipUtils;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.InetSocketAddress;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Collection;

public class ChatLogging {

    private static final SimpleDateFormat LOG_NAME_FORMAT = new SimpleDateFormat("yyyy'-'MM'-'dd");
    private static final SimpleDateFormat LOG_FORMAT = new SimpleDateFormat("'['HH':'mm':'ss'] '");
    private final File directory;

    private Calendar date;
    private InetSocketAddress server;
    private File logFile;
    private PrintStream out;

    public ChatLogging(File dir) {
        this.directory = dir;
        compressLogs();
    }

    @Subscribe
    public void onChatRecieved(ChatReceivedEvent message) {
        if (TabbyChat.getInstance().settings.general.logChat.get()) {

            checkLog();
            if (out == null) {
                return;
            }

            out.println(LOG_FORMAT.format(Calendar.getInstance().getTime()) + message.text.getUnformattedText());
        }
    }

    /**
     * Checks that the date hasn't changed. If it has, updates date and out and
     * compressed the previous file.
     */
    private void checkLog() {
        Calendar cal = Calendar.getInstance();
        if (shouldChangeLogFile()) {
            Calendar prev = date;
            date = cal;
            server = TabbyChat.getInstance().getCurrentServer();
            try {

                File old = logFile;
                String server = getLogFolder();
                logFile = findFile(new File(directory, server));
                logFile.getParentFile().mkdirs();
                logFile.createNewFile();
                IOUtils.closeQuietly(out);
                this.out = new PrintStream(new FileOutputStream(logFile, true), true, "UTF-8");

                // compress log
                if (prev != null && prev.get(Calendar.DATE) != date.get(Calendar.DATE)) {
                    gzipFile(old);
                }

            } catch (IOException e) {
                TabbyChat.getLogger().warn("Unable to create log file", e);
                this.date = null;
                this.out = null;
            }
        }
    }

    private boolean shouldChangeLogFile() {

        if (date == null) {
            return true;
        }
        boolean day = Calendar.getInstance().get(Calendar.DATE) != date.get(Calendar.DATE);
        boolean ip = TabbyChat.getInstance().getCurrentServer() != server;
        return day || ip;
    }

    private void compressLogs() {
        if (!directory.exists()) {
            return;
        }
        Collection<File> logs = FileUtils.listFiles(directory, new String[] { "log" }, true);
        for (File file : logs) {
            try {
                gzipFile(file);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static void gzipFile(File file) throws IOException {
        if (file == null) {
            return;
        }
        String name = GzipUtils.getCompressedFilename(file.getName());
        File dest = new File(file.getParentFile(), name);
        try (FileOutputStream os = new FileOutputStream(dest)) {
            // read the contents
            String contents = FileUtils.readFileToString(file, Charsets.UTF_8);
            try (OutputStream gzip = new GzipCompressorOutputStream(os)) {
                // write / compress
                IOUtils.write(contents, gzip, Charsets.UTF_8);
            }

        } finally {
            file.delete(); // delete the file
        }
    }

    private String getLogFolder() {
        String ip;
        if (Minecraft.getMinecraft().isSingleplayer() || server == null) {
            ip = "singleplayer";
        } else {
            String url = server.getHostName();
            ip = IPUtils.parse(url).getFileSafeAddress();
        }
        return ip;
    }

    private static File findFile(File dir) {
        String date = LOG_NAME_FORMAT.format(Calendar.getInstance().getTime());
        File file = new File(dir, date + ".log");
        int i = 0;
        while (fileExists(file)) {
            i++;
            file = new File(dir, date + "-" + i + ".log");
        }

        return file;
    }

    private static boolean fileExists(File file) {
        if (!file.exists()) {
            String gzip = GzipUtils.getCompressedFilename(file.getName());
            file = new File(file.getParentFile(), gzip);
        }
        return file.exists();
    }
}


<|start_filename|>src/main/java/mnm/mods/util/text/FancyTextStyle.java<|end_filename|>
package mnm.mods.util.text;

import mnm.mods.util.Color;

public class FancyTextStyle {

    private Color color;
    private Color underline;
    private Color highlight;

    public Color getColor() {
        if (underline == null)
            return Color.WHITE;
        return color;
    }

    public void setColor(Color color) {
        this.color = color;
    }

    public Color getUnderline() {
        if (underline == null)
            return Color.of(0);
        return underline;
    }

    public void setUnderline(Color underline) {
        this.underline = underline;
    }

    public Color getHighlight() {
        if (highlight == null)
            return Color.of(0);
        return highlight;
    }

    public void setHighlight(Color highlight) {
        this.highlight = highlight;
    }

    public FancyTextStyle createCopy() {
        FancyTextStyle fcs = new FancyTextStyle();
        fcs.highlight = highlight;
        fcs.underline = underline;
        return fcs;
    }

    @Override
    public String toString() {
        return String.format("FancyStyle{color=%s, underline=%s, hightlight=%s}", color, underline, highlight);
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/gui/IGui.java<|end_filename|>
package mnm.mods.tabbychat.api.gui;

import java.awt.Rectangle;

/**
 *
 * @author Matthew
 *
 * @param <Gui> The gui type, such as GuiComponent or GuiPanel
 */
public interface IGui {

    /**
     * Gets the bounds of this gui relative to the parent.
     *
     * @return The size and position
     */
    Rectangle getBounds();

    /**
     * Gets the scale of this gui.
     *
     * @return The scale
     */
    float getScale();
}

<|start_filename|>src/main/java/mnm/mods/tabbychat/api/filters/Filter.java<|end_filename|>
package mnm.mods.tabbychat.api.filters;

import net.minecraft.util.StringUtils;
import net.minecraft.util.text.ITextComponent;

import java.util.regex.Pattern;

/**
 * A filter is used to filter chat.
 */
public interface Filter {

    /**
     * Gets the pattern that will trigger this filter.
     *
     * @return The pattern
     */
    Pattern getPattern();

    void action(FilterEvent event);

    /**
     * Used to convert the component to the string.
     * <p>Default implementation also strips any control/color codes.</p>
     *
     * @param string The text component to be processed
     * @return The string which will be used for the
     */
    default String prepareText(ITextComponent string) {
        return StringUtils.stripControlCodes(string.getUnformattedText());
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/ChatManager.java<|end_filename|>
package mnm.mods.tabbychat;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import mnm.mods.tabbychat.api.Channel;
import mnm.mods.tabbychat.api.ChannelStatus;
import mnm.mods.tabbychat.api.Chat;
import mnm.mods.tabbychat.api.Message;
import mnm.mods.tabbychat.gui.ChatBox;
import mnm.mods.tabbychat.gui.TextBox;
import mnm.mods.tabbychat.settings.AdvancedSettings;
import mnm.mods.tabbychat.settings.GeneralServerSettings;
import mnm.mods.util.Location;
import mnm.mods.util.config.ValueMap;
import mnm.mods.util.text.TextBuilder;
import net.minecraft.client.Minecraft;
import net.minecraft.util.EnumTypeAdapterFactory;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.Style;
import net.minecraft.util.text.TextFormatting;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.reflect.TypeUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.time.Instant;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

public class ChatManager implements Chat {

    public static final int MAX_CHAT_LENGTH = 256;

    private Gson gson = new GsonBuilder()
            .excludeFieldsWithoutExposeAnnotation()
            .registerTypeHierarchyAdapter(ITextComponent.class, new ITextComponent.Serializer())
            .registerTypeAdapter(Style.class, new Style.Serializer())
            .registerTypeAdapterFactory(new EnumTypeAdapterFactory())
            .create();

    private ChatBox chatbox;

    private Map<String, Channel> allChannels = Maps.newHashMap();
    private Map<String, Channel> allPms = Maps.newHashMap();
    private List<Channel> channels = Lists.newLinkedList();
    private Channel active = ChatChannel.DEFAULT_CHANNEL;

    private Map<Channel, List<Message>> messages = Maps.newHashMap();

    public ChatManager(TabbyChat tc) {
        AdvancedSettings settings = tc.settings.advanced;
        int x = settings.chatX.get();
        int y = settings.chatY.get();
        int width = settings.chatW.get();
        int height = settings.chatH.get();

        this.chatbox = new ChatBox(new Location(x, y, width, height));

        if (!this.channels.contains(ChatChannel.DEFAULT_CHANNEL)) {
            this.channels.add(ChatChannel.DEFAULT_CHANNEL);
            chatbox.getTray().addChannel(ChatChannel.DEFAULT_CHANNEL);
        }
    }

    @Override
    public Channel getChannel(String name) {
        return getChannel(name, false);
    }

    @Override
    public Channel getChannel(String name, boolean pm) {
        return pm ? getPmChannel(name) : getChatChannel(name);
    }

    private Channel getChatChannel(String name) {
        return getChannel(name, false, this.allChannels, TabbyChat.getInstance().serverSettings.channels);
    }

    private Channel getPmChannel(String name) {
        Channel channel = getChannel(name, true, this.allPms, TabbyChat.getInstance().serverSettings.pms);
        if (channel.getPrefix().isEmpty()) {
            channel.setPrefix("/msg " + name);
        }
        return channel;
    }

    private Channel getChannel(String name, boolean pm, Map<String, Channel> from, ValueMap<ChatChannel> setting) {
        if (!from.containsKey(name)) {
            // fetch from settings
            ChatChannel chan = setting.get(name);
            if (chan == null || chan.getName() == null) {
                chan = new ChatChannel(name, pm);
                setting.get().put(chan.getName(), chan);
            }
            from.put(name, chan);
            messages.put(chan, chan.getMessages());
        }
        return from.get(name);
    }

    @Override
    public void addChannel(Channel channel) {
        if (!this.channels.contains(channel)) {
            this.channels.add(channel);
            chatbox.getTray().addChannel(channel);
        }
        save();
    }

    @Override
    public void removeChannel(Channel channel) {
        if (channels.contains(channel) && !channel.equals(ChatChannel.DEFAULT_CHANNEL)) {
            channels.remove(channel);
            chatbox.getTray().removeChannel(channel);
        }
        if (getActiveChannel() == channel) {
            setActiveChannel(ChatChannel.DEFAULT_CHANNEL);
        }
        save();
    }

    @Override
    public List<Channel> getChannels() {
        return ImmutableList.copyOf(channels);
    }

    @Override
    public void removeMessages(int id) {
        for (Channel channel : this.channels) {
            channel.removeMessages(id);
        }
        save();
    }

    @Override
    public void clearMessages() {
        for (Channel channel : channels) {
            channel.clear();
        }

        this.channels.clear();
        this.channels.add(ChatChannel.DEFAULT_CHANNEL);

        chatbox.getTray().clear();
    }

    @Override
    public Channel getActiveChannel() {
        return active;
    }

    @Override
    public void setActiveChannel(Channel channel) {
        TextBox text = chatbox.getChatInput();


        if (active.isPrefixHidden()
                ? text.getText().trim().isEmpty()
                : text.getText().trim().equals(active.getPrefix())) {
            // text is the prefix, so remove it.
            text.setText("");
            if (!channel.isPrefixHidden() && !channel.getPrefix().isEmpty()) {
                // target has prefix visible
                text.getTextField().getTextField().setText(channel.getPrefix() + " ");
            }
        }
        // set max text length
        boolean hidden = channel.isPrefixHidden();
        int prefLength = hidden ? channel.getPrefix().length() + 1 : 0;

        text.getTextField().getTextField().setMaxStringLength(MAX_CHAT_LENGTH - prefLength);

        // reset scroll
        // TODO per-channel scroll settings?
        if (channel != active) {
            chatbox.getChatArea().resetScroll();
        }
        active.setStatus(null);
        active = channel;
        active.setStatus(ChannelStatus.ACTIVE);

        runActivationCommand(channel);

    }

    private void runActivationCommand(Channel channel) {
        String cmd = channel.getCommand();
        if (cmd.isEmpty()) {

            GeneralServerSettings settings = TabbyChat.getInstance().serverSettings.general;
            String pat = channel.isPm() ? settings.messageCommand.get() : settings.channelCommand.get();

            if (pat.isEmpty()) {
                return;
            }
            String name = channel.getName();
            if (channel == ChatChannel.DEFAULT_CHANNEL) {
                name = TabbyChat.getInstance().serverSettings.general.defaultChannel.get();
            }
            // insert the channel name
            cmd = pat.replace("{}", name);

        }
        if (cmd.startsWith("/")) {
            if (cmd.length() > MAX_CHAT_LENGTH) {
                cmd = cmd.substring(0, MAX_CHAT_LENGTH);
            }
            Minecraft.getMinecraft().player.sendChatMessage(cmd);
        }
    }

    private boolean loading;

    public void loadFrom(File dir) throws IOException {
        loading = true;
        try {
            loadFrom_(dir);
        } finally {
            loading = false;
        }
    }

    private synchronized void loadFrom_(File dir) throws IOException {
        File file = new File(dir, "data.gz");
        if (!file.exists()) {
            return;
        }
        InputStream fin = null;
        InputStream gzin = null;
        String data;
        try {
            fin = new FileInputStream(file);
            gzin = new GzipCompressorInputStream(fin);
            data = IOUtils.toString(gzin, Charsets.UTF_8);
        } finally {
            IOUtils.closeQuietly(fin);
            IOUtils.closeQuietly(gzin);
        }

        clearMessages();
        allChannels.clear();
        allPms.clear();

        JsonObject root = gson.fromJson(data, JsonObject.class);
        Type type = TypeUtils.parameterize(List.class, ChatMessage.class);

        List<Message> def = gson.fromJson(root.get("default"), type);
        ChatChannel.DEFAULT_CHANNEL.getMessages().addAll(def);

        JsonObject chans = root.get("chans").getAsJsonObject();
        readJson(chans, false);
        JsonObject pms = root.get("pms").getAsJsonObject();
        readJson(pms, true);

        // active channels
        JsonObject active = root.get("active").getAsJsonObject();
        JsonArray achans = active.get("chans").getAsJsonArray();
        for (JsonElement e : achans) {
            addChannel(getChannel(e.getAsString(), false));
        }
        JsonArray apms = active.get("pms").getAsJsonArray();
        for (JsonElement e : apms) {
            addChannel(getChannel(e.getAsString(), true));
        }

        String time;
        if (root.has("datetime")) {
            Instant datetime = Instant.ofEpochSecond(root.get("datetime").getAsLong());
            time = datetime.toString();
        } else {
            time = "UNKNOWN";
        }
        ITextComponent chat = new TextBuilder()
                .text("Chat log from " + time)
                .format(TextFormatting.GRAY)
                .build();
        for (Channel c : getChannels()) {
            if (!c.getMessages().isEmpty()) {
                c.addMessage(chat, -1);
            }
        }
    }

    private void readJson(JsonObject obj, boolean pm) {
        for (Entry<String, JsonElement> entry : obj.entrySet()) {
            Channel chan = getChannel(entry.getKey(), pm);
            Type type = TypeUtils.parameterize(List.class, ChatMessage.class);
            List<Message> list = gson.fromJson(entry.getValue(), type);
            chan.getMessages().addAll(list);
        }
    }

    void save() {
        if (loading) {
            return;
        }
        try {
            saveTo(TabbyChat.getInstance().serverSettings.getFile().getParentFile());
        } catch (IOException e) {
            TabbyChat.getLogger().warn("Error while saving chat data", e);
        }
    }

    private synchronized void saveTo(File dir) throws IOException {
        JsonObject root = new JsonObject();
        root.addProperty("datetime", Instant.now().getEpochSecond());
        root.add("default", gson.toJsonTree(ChatChannel.DEFAULT_CHANNEL.getMessages()));

        JsonObject chans = new JsonObject();
        root.add("chans", chans);
        JsonObject pms = new JsonObject();
        root.add("pms", pms);

        for (Channel c : messages.keySet()) {
            JsonObject obj = c.isPm() ? pms : chans;
            obj.add(c.getName(), gson.toJsonTree(c.getMessages()));
        }

        // active channels
        JsonObject active = new JsonObject();
        root.add("active", active);

        JsonArray apms = new JsonArray();
        JsonArray achans = new JsonArray();
        active.add("chans", achans);
        active.add("pms", apms);

        for (Channel c : channels) {
            if (c == ChatChannel.DEFAULT_CHANNEL) {
                continue;
            }
            JsonArray array = c.isPm() ? apms : achans;
            array.add(new JsonPrimitive(c.getName()));
        }

        OutputStream fout = null;
        GzipCompressorOutputStream gzout = null;
        try {
            File file = new File(dir, "data.gz");
            file.getParentFile().mkdirs();
            fout = new FileOutputStream(file);
            gzout = new GzipCompressorOutputStream(fout);
            String data = gson.toJson(root);
            IOUtils.write(data, gzout, Charsets.UTF_8);
            gzout.finish();
        } finally {
            IOUtils.closeQuietly(fout);
            IOUtils.closeQuietly(gzout);
        }
    }

    public ChatBox getChatBox() {
        return this.chatbox;
    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/config/GuiSettingColor.java<|end_filename|>
package mnm.mods.util.gui.config;

import com.google.common.eventbus.Subscribe;
import mnm.mods.util.Color;
import mnm.mods.util.ILocation;
import mnm.mods.util.config.Value;
import mnm.mods.util.gui.GuiSelectColor;
import mnm.mods.util.gui.events.ActionPerformedEvent;
import net.minecraft.client.gui.Gui;
import net.minecraft.util.ResourceLocation;

import java.util.function.Consumer;

/**
 * A setting for colors. When clicked, shows a {@link GuiSelectColor}.
 *
 * @author Matthew
 */
public class GuiSettingColor extends GuiSetting<Color> implements Consumer<Color> {

    private static final ResourceLocation TRANSPARENCY = new ResourceLocation("mnmutils",
            "textures/transparency.png");

    private Color value;

    public GuiSettingColor(Value<Color> setting) {
        super(setting);
    }

    @Subscribe
    public void selectColor(ActionPerformedEvent event) {
        getParent().ifPresent(p -> p.setOverlay(new GuiSelectColor(GuiSettingColor.this, getValue())));
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        ILocation loc = getLocation();
        mc.getTextureManager().bindTexture(TRANSPARENCY);
        Gui.drawModalRectWithCustomSizedTexture(0, 0, 0, 0, loc.getWidth(), loc.getHeight(), 6, 6);
        Gui.drawRect(0, 0, loc.getWidth(), loc.getHeight(), getValue().getHex());
        Gui.drawRect(0, 0, loc.getWidth(), 1, 0xffaabbcc);
        Gui.drawRect(1, 0, 0, loc.getHeight(), 0xffaabbcc);
        Gui.drawRect(loc.getWidth() - 1, 0, loc.getWidth(), loc.getHeight(), 0xffaabbcc);
        Gui.drawRect(0, loc.getHeight(), loc.getWidth(), loc.getHeight() - 1, 0xffaabbcc);
    }

    @Override
    public void accept(Color input) {
        setValue(input);
        getParent().ifPresent( p -> p.setOverlay(null));
    }

    @Override
    public Color getValue() {
        return value;
    }

    @Override
    public void setValue(Color color) {
        this.value = color;
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/extra/spell/UserDictionary.java<|end_filename|>
package mnm.mods.tabbychat.extra.spell;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;

import com.google.common.io.CharStreams;
import com.google.common.io.Closeables;
import com.swabunga.spell.engine.SpellDictionaryHashMap;

/**
 * Dictionary that supports {@code #} comments
 */
public class UserDictionary extends SpellDictionaryHashMap {

    public UserDictionary(File file) throws IOException {
        super(file);
    }

    @Override
    protected void createDictionary(BufferedReader in) throws IOException {
        try {
            for (String line : CharStreams.readLines(in)) {
                if (!line.startsWith("#") && !line.trim().isEmpty()) {
                    putWord(line);
                }
            }
        } finally {
            Closeables.closeQuietly(in);
        }
    }
}


<|start_filename|>src/main/java/mnm/mods/util/DefaultChatProxy.java<|end_filename|>
package mnm.mods.util;

import net.minecraft.client.Minecraft;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextComponentTranslation;

public class DefaultChatProxy implements IChatProxy {

    @Override
    public void addToChat(String channel, ITextComponent msg) {
        ITextComponent text = new TextComponentTranslation("[%s] %s", channel, msg);
        Minecraft.getMinecraft().ingameGUI.getChatGUI().printChatMessage(text);
    }
}


<|start_filename|>src/main/java/mnm/mods/util/IChatProxy.java<|end_filename|>
package mnm.mods.util;

import net.minecraft.util.text.ITextComponent;

public interface IChatProxy {

    void addToChat(String channel, ITextComponent message);
}


<|start_filename|>src/main/java/mnm/mods/util/gui/config/GuiSettingStringList.java<|end_filename|>
package mnm.mods.util.gui.config;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import mnm.mods.util.config.ValueList;
import mnm.mods.util.gui.GuiText;
import mnm.mods.util.gui.GuiWrappedComponent;
import mnm.mods.util.gui.IGuiInput;
import mnm.mods.util.gui.config.GuiSetting.GuiSettingWrapped;

import java.util.List;

public class GuiSettingStringList extends GuiSettingWrapped<List<String>, GuiSettingStringList.GuiStringList> {

    public GuiSettingStringList(ValueList<String> setting, String split, String join) {
        super(setting, new GuiStringList(split, join));
    }

    public GuiSettingStringList(ValueList<String> setting, String split) {
        this(setting, split, split);
    }

    public GuiSettingStringList(ValueList<String> setting) {
        this(setting, ",", ", ");
    }

    public static class GuiStringList extends GuiWrappedComponent<GuiText> implements IGuiInput<List<String>> {

        private String split;
        private String join;

        public GuiStringList(String split, String join) {
            super(new GuiText());
            this.split = split;
            this.join = join;
        }

        @Override
        public List<String> getValue() {
            return Splitter.on(split).omitEmptyStrings().trimResults().splitToList(getComponent().getValue());
        }

        @Override
        public void setValue(List<String> value) {
            getComponent().setValue(Joiner.on(join).skipNulls().join(value));
        }

        @Deprecated
        public GuiText getText() {
            return getComponent();
        }
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/util/ChannelPatterns.java<|end_filename|>
package mnm.mods.tabbychat.util;

import net.minecraft.client.resources.I18n;

import java.util.regex.Pattern;

public enum ChannelPatterns {

    ANGLES(Translation.DELIMS_ANGLES, "^\\<%s\\>"),
    BRACES(Translation.DELIMS_BRACES, "^\\{%s\\}"),
    BRACKETS(Translation.DELIMS_BRACKETS, "^\\[%s\\]"),
    PARENS(Translation.DELIMS_PARENTHESIS, "^\\(%s\\)"),
    ANGLESPARENS(Translation.DELIMS_ANGLES_PARENS, "^<\\(%s\\) ?.{3,30}>"),
    ANGLESBRACKETS(Translation.DELIMS_ANGLES_BRAKETS, "^<\\[%s\\] ?.{3,30}>");

    private final String translation;
    private final Pattern pattern;

    ChannelPatterns(String title, String pattern) {
        this.translation = title;
        this.pattern = Pattern.compile(String.format(pattern, "(.{1,16}?)"));
    }

    public Pattern getPattern() {
        return pattern;
    }

    @Override
    public String toString() {
        return I18n.format(translation);
    }
}


<|start_filename|>src/main/java/mnm/mods/util/config/ValueSerializer.java<|end_filename|>
package mnm.mods.util.config;

import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import org.apache.commons.lang3.reflect.TypeUtils;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;

class ValueSerializer implements JsonSerializer<Value<?>>, JsonDeserializer<Value<?>> {

    @Override
    public Value<?> deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException {
        Type arg = ((ParameterizedType) type).getActualTypeArguments()[0];
        type = ((ParameterizedType) type).getRawType();
        if (Value.class.equals(type)) {
            return value(json, arg, context);
        }
        if (ValueList.class.equals(type)) {
            return list(json, arg, context);
        }
        if (ValueMap.class.equals(type)) {
            return map(json, arg, context);
        }
        return null;
    }

    private <T> Value<T> value(JsonElement json, Type type, JsonDeserializationContext context) {
        Value<T> s = new Value<>();
        s.set(context.deserialize(json, type));
        return s;
    }

    private <T> ValueList<T> list(JsonElement json, Type type, JsonDeserializationContext context) {
        ValueList<T> l = new ValueList<>();
        List<T> list = context.deserialize(json, TypeUtils.parameterize(List.class, type));
        l.set(list);
        return l;
    }

    private <T> ValueMap<T> map(JsonElement json, Type type, JsonDeserializationContext context) {
        ValueMap<T> m = new ValueMap<>();
        Map<String, T> map = context.deserialize(json, TypeUtils.parameterize(Map.class, String.class, type));
        m.set(map);
        return m;
    }

    @Override
    public JsonElement serialize(Value<?> src, Type typeOfSrc, JsonSerializationContext context) {
        return context.serialize(src.get());
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/core/mixin/IChatTabCompleter.java<|end_filename|>
package mnm.mods.tabbychat.core.mixin;

import net.minecraft.client.gui.GuiTextField;
import net.minecraft.util.TabCompleter;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;

@Mixin(TabCompleter.class)
public interface IChatTabCompleter {

    @Accessor
    void setTextField(GuiTextField textfield);
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/gui/Scrollbar.java<|end_filename|>
package mnm.mods.tabbychat.gui;

import mnm.mods.tabbychat.core.GuiNewChatTC;
import mnm.mods.util.gui.GuiComponent;
import net.minecraft.client.gui.Gui;

public class Scrollbar extends GuiComponent {

    private ChatArea chat;

    public Scrollbar(ChatArea chat) {
        this.chat = chat;
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        if (GuiNewChatTC.getInstance().getChatOpen()) {
            int scroll = chat.getScrollPos();
            int max = chat.getBounds().height;
            int lines = max / mc.fontRenderer.FONT_HEIGHT;
            int total = chat.getChat().size();
            if (total <= lines) {
                return;
            }
            total -= lines;
            int size = Math.max(max / 2 - total, 10);
            float perc = Math.abs((float) scroll / (float) total - 1) * Math.abs((float) size / (float) max - 1);
            int pos = (int) (perc * max);

            Gui.drawRect(0, pos, 1, pos + size, -1);
            super.drawComponent(mouseX, mouseY);
        }
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/config/GuiSetting.java<|end_filename|>
package mnm.mods.util.gui.config;

import mnm.mods.util.config.Value;
import mnm.mods.util.gui.GuiComponent;
import mnm.mods.util.gui.GuiWrappedComponent;
import mnm.mods.util.gui.IGuiInput;

import javax.annotation.Nonnull;

/**
 * A base setting gui wrapper.
 *
 * @author Matthew
 * @param <T> The setting type
 */
public abstract class GuiSetting<T> extends GuiComponent implements IGuiInput<T> {

    private final Value<T> setting;

    GuiSetting(Value<T> setting) {
        this.setting = setting;
        this.setValue(setting.get());
    }

    /**
     * Gets the setting object.
     *
     * @return The setting object
     */
    public Value<T> getSetting() {
        return this.setting;
    }

    @Override
    public void updateComponent() {
        getSetting().set(getValue());
    }

    private abstract static class Wrapped<T, W extends GuiComponent> extends GuiWrappedComponent<W> implements IGuiInput<T> {

        private final Value<T> setting;

        Wrapped(Value<T> setting, @Nonnull W wrap) {
            super(wrap);
            this.setting = setting;
        }

        public Value<T> getSetting() {
            return this.setting;
        }

        @Override
        public void updateComponent() {
            super.updateComponent();
            getSetting().set(getValue());
        }

    }

    public static class GuiSettingWrapped<T, Wrapper extends GuiComponent & IGuiInput<T>>
            extends Wrapped<T, Wrapper> {

        GuiSettingWrapped(Value<T> setting, @Nonnull Wrapper input) {
            super(setting, input);
            input.setValue(setting.get());
        }

        @Override
        public T getValue() {
            return getComponent().getValue();
        }

        @Override
        public void setValue(T value) {
            getComponent().setValue(value);
        }

    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/events/GuiRenderEvent.java<|end_filename|>
package mnm.mods.util.gui.events;

import mnm.mods.util.gui.GuiComponent;

public class GuiRenderEvent extends AbstractMouseEvent {

    private float ticks;

    public GuiRenderEvent(GuiComponent component, int mouseX, int mouseY, float ticks) {
        super(component, mouseX, mouseY);
        this.ticks = ticks;
    }

    public float getTicks() {
        return ticks;
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/events/ChatMessageEvent.java<|end_filename|>
package mnm.mods.tabbychat.api.events;

import com.google.common.collect.Sets;
import mnm.mods.tabbychat.api.Channel;
import net.minecraft.util.text.ITextComponent;

import java.util.Set;

public abstract class ChatMessageEvent {

    /**
     * Used to listen to chat and modify it. Can also select which channels it
     * goes to.
     */
    public static class ChatReceivedEvent extends ChatMessageEvent {

        public ITextComponent text;
        public int id;
        public Set<Channel> channels = Sets.newHashSet();

        public ChatReceivedEvent(ITextComponent text, int id) {
            this.text = text;
            this.id = id;
        }
    }
}


<|start_filename|>src/main/java/mnm/mods/util/ImmutableLocation.java<|end_filename|>
package mnm.mods.util;


import javax.annotation.concurrent.Immutable;

@Immutable
public final class ImmutableLocation implements ILocation {

    private final int xPos;
    private final int yPos;
    private final int width;
    private final int height;

    public ImmutableLocation(int xPos, int yPos, int width, int height) {
        this.xPos = xPos;
        this.yPos = yPos;
        this.width = width;
        this.height = height;
    }

    @Override
    public int getXPos() {
        return this.xPos;
    }

    @Override
    public int getYPos() {
        return this.yPos;
    }

    @Override
    public int getWidth() {
        return this.width;
    }

    @Override
    public int getHeight() {
        return this.height;
    }

    @Override
    public String toString() {
        return "ImmutableLocation [xPos=" + xPos + ", yPos=" + yPos + ", width=" + width + ", height=" + height + "]";
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + height;
        result = prime * result + width;
        result = prime * result + xPos;
        result = prime * result + yPos;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof ILocation))
            return false;
        ILocation other = (ILocation) obj;
        return height == other.getHeight()
                && width == other.getWidth()
                && xPos == other.getXPos()
                && yPos == other.getYPos();
    }

    @Override
    public ILocation asImmutable() {
        return this;
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/settings/TabbySettings.java<|end_filename|>
package mnm.mods.tabbychat.settings;

import com.google.gson.GsonBuilder;
import com.mumfrey.liteloader.modconfig.ConfigStrategy;
import com.mumfrey.liteloader.modconfig.ExposableOptions;

import mnm.mods.tabbychat.util.TabbyRef;
import mnm.mods.util.config.SettingsFile;
import net.minecraft.util.EnumTypeAdapterFactory;

@ExposableOptions(strategy = ConfigStrategy.Unversioned)
public class TabbySettings extends SettingsFile {

    public GeneralSettings general = new GeneralSettings();
    public AdvancedSettings advanced = new AdvancedSettings();

    public TabbySettings() {
        super(TabbyRef.MOD_ID, "tabbychat");
    }

    @Override
    public void setupGsonSerialiser(GsonBuilder gsonBuilder) {
        super.setupGsonSerialiser(gsonBuilder);
        gsonBuilder.registerTypeAdapterFactory(new EnumTypeAdapterFactory());
    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiSliderColor.java<|end_filename|>
package mnm.mods.util.gui;

import mnm.mods.util.Color;
import mnm.mods.util.ILocation;
import net.minecraft.client.gui.Gui;

/**
 * The color variant of {@link GuiSlider}.
 *
 * @author Matthew
 */
public class GuiSliderColor extends GuiSlider {

    private Model model;
    private Color base;

    public GuiSliderColor(double value, boolean vertical, Model model, Color base) {
        super(value, vertical);
        this.model = model;
        this.base = base;
    }

    @Override
    protected void drawMid(ILocation loc) {
        if (isVertical()) {
            for (int i = 0; i < loc.getHeight(); i++) {
                int color = (int) ((double) i / (double) loc.getHeight() * 255D);
                color = Math.abs(color - 256);
                color = getColor(color);
                Gui.drawRect(0, i, loc.getWidth(), i + 1, color);
            }
        } else {
            for (int i = 0; i < loc.getWidth(); i++) {
                int color = (int) ((double) i / (double) loc.getWidth() * 255D);
                // color = Math.abs(color - 256);
                color = getColor(color);
                Gui.drawRect(i, 0, i + 1, loc.getHeight(), color);
            }
        }
    }

    @Override
    public String getFormattedValue() {
        return Integer.toString((int) (getValue() * 255D));
    }

    private int getColor(int i) {
        i %= 256;
        switch (model) {
        case RED:
            return Color.getColor(i, base.getGreen(), base.getBlue(), base.getAlpha());
        case GREEN:
            return Color.getColor(base.getRed(), i, base.getBlue(), base.getAlpha());
        case BLUE:
            return Color.getColor(base.getRed(), base.getGreen(), i, base.getAlpha());
        case ALPHA:
            return Color.getColor(base.getRed(), base.getGreen(), base.getBlue(), i);
        }
        return -1;
    }

    public void setBase(Color color) {
        this.base = color;
    }

    /**
     * Base colors from the rgba color model
     */
    public enum Model {
        RED,
        GREEN,
        BLUE,
        ALPHA
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/Chat.java<|end_filename|>
package mnm.mods.tabbychat.api;

import java.util.List;

/**
 * Represents the Chat.
 */
public interface Chat {

    /**
     * Gets a {@link Channel} of the given name. Creates a new one if it doesn't
     * exist. Is the equivalent of calling {@code getChannel(name, false);}
     *
     * @param name Then name of the channel
     * @return The channel
     */
    Channel getChannel(String name);

    /**
     * Gets a {@link Channel} of the given name and gives it the specified PM
     * status. Creates a new one if it doesn't exist. This allows a channel to
     * have the same name as a player without their tabs being merged.
     *
     * @param name The name of the channel
     * @param pm Whether it is for PMs or not
     * @return The channel
     */
    Channel getChannel(String name, boolean pm);

    /**
     * Adds a channel to be displayed on the tray.
     *
     * @param channel The channel to be added
     */
    void addChannel(Channel channel);

    /**
     * Removes a {@link Channel} from the tab list.
     *
     * @param channel The channel to remove
     */
    void removeChannel(Channel channel);

    /**
     * Gets an array of all {@link Channel}s currently displayed.
     *
     * @return An array of channels
     */
    List<Channel> getChannels();

    /**
     * Gets the active {@link Channel}. The active channel is the channel whose
     * messages are shown.
     *
     * @return The active channel
     */
    Channel getActiveChannel();

    /**
     * Sets the active {@link Channel}.
     *
     * @param channel The new active channel
     */
    void setActiveChannel(Channel channel);

    /**
     * Clears all the messages in the chat and removes all channels.
     */
    void clearMessages();

    /**
     * Removes all messages with the given id.
     *
     * @param id The id
     */
    void removeMessages(int id);

}


<|start_filename|>src/main/java/mnm/mods/util/text/AbstractChatBuilder.java<|end_filename|>
package mnm.mods.util.text;

import mnm.mods.util.Color;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.Style;
import net.minecraft.util.text.TextComponentScore;
import net.minecraft.util.text.TextComponentSelector;
import net.minecraft.util.text.TextComponentString;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.util.text.event.ClickEvent;
import net.minecraft.util.text.event.HoverEvent;

public abstract class AbstractChatBuilder implements ITextBuilder {

    protected ITextComponent current;

    @Override
    public ITextBuilder format(TextFormatting f) {
        checkCreated();
        if (f.isColor()) {
            current.getStyle().setColor(f);
        } else if (f.isFancyStyling()) {
            if (f == TextFormatting.BOLD) {
                current.getStyle().setBold(true);
            } else if (f == TextFormatting.ITALIC) {
                current.getStyle().setItalic(true);
            } else if (f == TextFormatting.UNDERLINE) {
                current.getStyle().setUnderlined(true);
            } else if (f == TextFormatting.STRIKETHROUGH) {
                current.getStyle().setStrikethrough(true);
            } else if (f == TextFormatting.OBFUSCATED) {
                current.getStyle().setObfuscated(true);
            }
        } else if (f == TextFormatting.RESET) {
            current.setStyle(new Style());
        }
        return this;
    }

    @Override
    public ITextBuilder color(Color color) {
        asFancy().getFancyStyle().setColor(color);
        return this;
    }

    @Override
    public ITextBuilder underline(Color color) {
        asFancy().getFancyStyle().setUnderline(color);
        return this;
    }

    @Override
    public ITextBuilder highlight(Color color) {
        asFancy().getFancyStyle().setHighlight(color);
        return this;
    }

    private FancyTextComponent asFancy() {
        if (!(current instanceof FancyTextComponent)) {
            current = new FancyTextComponent(current);
        }
        return (FancyTextComponent) current;
    }

    @Override
    public ITextBuilder click(ClickEvent event) {
        checkCreated();
        current.getStyle().setClickEvent(event);
        return this;
    }

    @Override
    public ITextBuilder hover(HoverEvent event) {
        checkCreated();
        current.getStyle().setHoverEvent(event);
        return this;
    }

    @Override
    public ITextBuilder insertion(String insertion) {
        checkCreated();
        current.getStyle().setInsertion(insertion);
        return this;
    }

    private void checkCreated() {
        if (current == null) {
            throw new IllegalStateException("A chat component has not been created yet.");
        }
    }

    @Override
    public ITextBuilder score(String player, String objective) {
        return append(new TextComponentScore(player, objective));
    }

    @Override
    public ITextBuilder text(String text) {
        return append(new TextComponentString(text));
    }

    @Override
    public ITextBuilder selector(Selector selector) {
        return append(new TextComponentSelector(selector.toString()));
    }

    @Override
    public ITextBuilder translation(String key) {
        return new TranslationBuilder(this, key);
    }

    @Override
    public ITextBuilder quickTranslate(String key) {
        return translation(key).end();
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/settings/AdvancedSettings.java<|end_filename|>
package mnm.mods.tabbychat.settings;

import mnm.mods.tabbychat.util.ChatVisibility;
import mnm.mods.util.config.Value;
import mnm.mods.util.config.ValueObject;

public class AdvancedSettings extends ValueObject {

    public Value<Integer> chatX = value(5);
    public Value<Integer> chatY = value(17);
    public Value<Integer> chatW = value(300);
    public Value<Integer> chatH = value(160);
    public Value<Float> unfocHeight = value(0.5F);
    public Value<Integer> fadeTime = value(200);
    public Value<Integer> historyLen = value(100);
    public Value<Boolean> hideTag = value(false);
    public Value<Boolean> keepChatOpen = value(false);
    public Value<Boolean> spelling = value(true);
    public Value<ChatVisibility> visibility = value(ChatVisibility.NORMAL);
}


<|start_filename|>src/main/java/mnm/mods/util/text/FancyTextComponent.java<|end_filename|>
package mnm.mods.util.text;

import com.google.common.collect.Streams;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.Style;
import net.minecraft.util.text.TextComponentString;

import java.util.Iterator;
import java.util.List;

public class FancyTextComponent implements ITextComponent {

    private final ITextComponent text;
    private FancyTextStyle style;

    public FancyTextComponent(ITextComponent parent) {
        if (parent instanceof FancyTextComponent)
            throw new IllegalArgumentException("Parent text cannot be fancy");
        this.text = parent;
    }

    public FancyTextComponent(String string) {
        this(new TextComponentString(string));
    }

    @Override
    public String getUnformattedComponentText() {
        return text.getUnformattedComponentText();
    }

    @Override
    public ITextComponent createCopy() {
        ITextComponent text = this.text.createCopy();
        FancyTextComponent fcc = new FancyTextComponent(text);
        fcc.setFancyStyle(getFancyStyle().createCopy());
        return fcc;
    }

    @Override
    public ITextComponent appendSibling(ITextComponent component) {
        text.appendSibling(component);
        return this;
    }

    @Override
    public ITextComponent appendText(String text) {
        this.text.appendText(text);
        return this;
    }

    @Override
    public Style getStyle() {
        return text.getStyle();
    }

    @Override
    public ITextComponent setStyle(Style style) {
        text.setStyle(style);
        return this;
    }

    @Override
    public String getFormattedText() {
        return text.getFormattedText();
    }

    @Override
    public List<ITextComponent> getSiblings() {
        return text.getSiblings();
    }

    @Override
    public String getUnformattedText() {
        return text.getUnformattedText();
    }

    @Override
    public Iterator<ITextComponent> iterator() {
        // don't iterate using the vanilla components
        return Streams.stream(this.text.iterator())
                .map(it -> it instanceof FancyTextComponent ? it
                        : new FancyTextComponent(it).setFancyStyle(this.getFancyStyle())).iterator();
    }

    public ITextComponent getText() {
        return text;
    }

    public FancyTextStyle getFancyStyle() {
        if (this.style == null)
            this.style = new FancyTextStyle();
        return this.style;
    }

    public FancyTextComponent setFancyStyle(FancyTextStyle style) {
        this.style = style;
        return this;
    }

    @Override
    public String toString() {
        return String.format("FancyText{text=%s, fancystyle=%s}", text, style);
    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/events/GuiKeyboardEvent.java<|end_filename|>
package mnm.mods.util.gui.events;

import mnm.mods.util.gui.GuiComponent;

import org.lwjgl.input.Keyboard;

/**
 * An event for a keyboard adapter
 *
 * @author Matthew
 */
public class GuiKeyboardEvent extends GuiEvent {

    /**
     * The character typed
     */
    private final char character;
    /**
     * The key code typed
     *
     * @see Keyboard
     */
    private final int key;
    /**
     * The time in miliseconds the key was held for.
     */
    private final long time;

    public GuiKeyboardEvent(GuiComponent component, int key, char character, long time) {
        super(component);
        this.key = key;
        this.character = character;
        this.time = time;
    }

    public char getCharacter() {
        return character;
    }

    public int getKey() {
        return key;
    }

    public long getTime() {
        return time;
    }
}


<|start_filename|>src/main/java/mnm/mods/util/MnmUtils.java<|end_filename|>
package mnm.mods.util;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.mumfrey.liteloader.LiteMod;

import java.util.Set;

public class MnmUtils {

    private Set<Class<? extends LiteMod>> disabledUpdates = Sets.newIdentityHashSet();
    private IChatProxy chatProxy = new DefaultChatProxy();

    public IChatProxy getChatProxy() {
        return chatProxy;
    }

    public void setChatProxy(IChatProxy chatProxy) {
        if (chatProxy == null) {
            chatProxy = new DefaultChatProxy();
        }
        this.chatProxy = chatProxy;
    }

    public Set<Class<? extends LiteMod>> getDisabledUpdates() {
        return ImmutableSet.copyOf(disabledUpdates);
    }

    public void disableUpdateCheck(Class<? extends LiteMod> mod) {
        this.disabledUpdates.add(mod);
    }

}


<|start_filename|>src/main/java/mnm/mods/util/config/SettingsFile.java<|end_filename|>
package mnm.mods.util.config;

import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.internal.Excluder;
import com.mumfrey.liteloader.core.runtime.Obf;
import com.mumfrey.liteloader.modconfig.AdvancedExposable;
import com.mumfrey.liteloader.util.PrivateFields;
import net.minecraft.util.EnumTypeAdapterFactory;

import java.io.File;
import java.io.IOException;

/**
 * Used for creating settings and saving/loading them in the JSON format. Start
 * by creating fields. Mark anything you don't wish to be serialized with {@code transient}.
 * If your setting requires special handling for serialization, override
 * {@link #setupGsonSerialiser(GsonBuilder)} and use it to customize the {@link Gson}
 * object to your liking.
 *
 * @author <NAME>
 */
public abstract class SettingsFile extends ValueObject implements AdvancedExposable {

    private transient final String path;
    private transient File file;

    public SettingsFile(String path, String name) {
        this.path = String.format("%s/%s.json", path, name);
    }

    @Override
    public void setupGsonSerialiser(GsonBuilder gsonBuilder) {
        new PrivateFields<GsonBuilder, Excluder>(GsonBuilder.class, new Obf("excluder") {}) {}
                .set(gsonBuilder, Excluder.DEFAULT); // grr
        gsonBuilder
                .registerTypeAdapterFactory(new EnumTypeAdapterFactory())
                .registerTypeHierarchyAdapter(Value.class, new ValueSerializer());
    }

    @Override
    public File getConfigFile(File configFile, File configFileLocation, String defaultFileName) {
        if (file == null)
            file = new File(configFileLocation, path);
        try {
            // create the paths to it
            Files.createParentDirs(file);
        } catch (IOException ignored) {}
        return file;
    }

    public File getFile() {
        return file;
    }
}


<|start_filename|>src/main/resources/mixin.tabbychat.json<|end_filename|>
{
	"required": true,
	"minVersion": "0.6",
	"compatibilityLevel": "JAVA_8",
	"package": "mnm.mods.tabbychat.core.mixin",
	"refmap": "mixin.tabbychat.refmap.json",
	"mixins": [
		"IChatTabCompleter",
		"MixinChatTabCompleter",
		"MixinGuiChat",
		"IGuiIngame"
	]
}


<|start_filename|>src/main/java/mnm/mods/util/config/ValueObject.java<|end_filename|>
package mnm.mods.util.config;

public abstract class ValueObject {

    public static <T> Value<T> value(T t) {
        return new Value<>(t);
    }

    public static <T> ValueList<T> list() {
        return new ValueList<>();
    }

    public static <T> ValueMap<T> map() {
        return new ValueMap<>();
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/gui/settings/SettingsButton.java<|end_filename|>
package mnm.mods.tabbychat.gui.settings;

import mnm.mods.util.ILocation;
import mnm.mods.util.Location;
import mnm.mods.util.gui.GuiButton;
import mnm.mods.util.gui.config.SettingPanel;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.renderer.GlStateManager;

import java.awt.Dimension;
import javax.annotation.Nonnull;

public class SettingsButton extends GuiButton {

    private SettingPanel<?> settings;
    private int displayX = 30;
    private boolean active;

    SettingsButton(SettingPanel<?> settings) {
        super(settings.getDisplayString());
        this.settings = settings;
        this.setLocation(new Location(0, 0, 75, 20));
        settings.getSecondaryColor().ifPresent(this::setSecondaryColor);
    }

    public SettingPanel<?> getSettings() {
        return settings;
    }

    public void setActive(boolean active) {
        this.active = active;
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        if (active && displayX > 20) {
            displayX -= 2;
        } else if (!active && displayX < 30) {
            displayX += 2;
        }
        ILocation loc = this.getLocation();
        GlStateManager.enableAlpha();
        getSecondaryColor().ifPresent(color ->
                Gui.drawRect(displayX - 30, 2, loc.getWidth() + displayX - 30, loc.getHeight() - 2, color.getHex()));
        String string = mc.fontRenderer.trimStringToWidth(getText(), loc.getWidth());
        mc.fontRenderer.drawString(string, displayX - 20, 6, getPrimaryColorProperty().getHex());
    }

    @Nonnull
    @Override
    public Dimension getMinimumSize() {
        return getLocation().getSize();
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/util/ChatTextUtils.java<|end_filename|>
package mnm.mods.tabbychat.util;

import com.google.common.collect.Lists;
import mnm.mods.tabbychat.ChatMessage;
import mnm.mods.tabbychat.api.Message;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiUtilRenderComponents;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextComponentString;

import java.util.Iterator;
import java.util.List;

public class ChatTextUtils {

    public static List<ITextComponent> split(ITextComponent chat, int width) {
        FontRenderer fr = Minecraft.getMinecraft().fontRenderer;
        return GuiUtilRenderComponents.splitText(chat, width, fr, false, false);
    }

    public static List<Message> split(List<Message> list, int width) {
        if (width <= 8) // ignore, characters are larger than width
            return Lists.newArrayList(list);
        // prevent concurrent modification caused by chat thread
        synchronized (list) {
            List<Message> result = Lists.newArrayList();
            Iterator<Message> iter = list.iterator();
            while (iter.hasNext() && result.size() <= 100) {
                Message line = iter.next();
                List<ITextComponent> chatlist = split(line.getMessageWithOptionalTimestamp(), width);
                for (int i = chatlist.size() - 1; i >= 0; i--) {
                    ITextComponent chat = chatlist.get(i);
                    result.add(new ChatMessage(line.getCounter(), chat, line.getID(), false));
                }
            }
            return result;
        }
    }

    /**
     * Returns a ChatComponent that is a sub-component of another one. It begins
     * at the specified index and extends to the end of the componenent.
     *
     * @param chat The chat to subchat
     * @param beginIndex The beginning index, inclusive
     * @return The end of the chat
     * @see String#substring(int)
     */
    public static ITextComponent subChat(ITextComponent chat, int beginIndex) {
        ITextComponent rchat = null;
        Iterator<ITextComponent> ichat = chat.iterator();
        int pos = 0;
        while (ichat.hasNext()) {
            ITextComponent part = ichat.next();
            String s = part.getUnformattedComponentText();

            int len = s.length();
            if (len + pos >= beginIndex) {
                if (pos < beginIndex) {
                    ITextComponent schat = new TextComponentString(s.substring(beginIndex - pos));
                    schat.setStyle(part.getStyle().createShallowCopy());
                    part = schat;
                }
                if (rchat == null) {
                    rchat = part;
                } else {
                    rchat.appendSibling(part);
                }
            }
            pos += len;
        }
        return rchat;
    }
}


<|start_filename|>src/main/java/mnm/mods/util/update/UpdateChecker.java<|end_filename|>
package mnm.mods.util.update;

import com.google.gson.Gson;
import mnm.mods.util.IChatProxy;
import mnm.mods.util.text.ITextBuilder;
import mnm.mods.util.text.TextBuilder;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.util.text.event.ClickEvent;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URL;

/**
 * Update checker for several mods.
 *
 * @author Matthew
 */
public class UpdateChecker extends Thread {

    private static final Logger logger = LogManager.getLogger("Updates");

    private IChatProxy chatProxy;
    private VersionData data;

    private UpdateChecker(IChatProxy chat, VersionData data) {
        super("Update Checker");
        this.chatProxy = chat;
        this.data = data;
    }

    @Override
    public void run() {
        InputStream in = null;
        Reader reader = null;
        UpdateResponse response;
        try {
            URL url = new URL(data.getUpdateUrl());
            in = url.openStream();
            reader = new InputStreamReader(in);
            response = new Gson().fromJson(reader, UpdateResponse.class);
        } catch (IOException e) {
            // failure
            logger.warn("Update check failed.", e);
            return;
        } finally {
            IOUtils.closeQuietly(reader);
            IOUtils.closeQuietly(in);
        }

        if (data.isOutdated(response.minecraft)) {
            notifyUser(data, response);
        } else {
            logger.info("Update check for " + data.getName() + " finished. None found.");
        }
    }

    private void notifyUser(VersionData data, UpdateResponse response) {
        ITextBuilder builder = new TextBuilder()
                .translation("update.available")
                .text(data.getName())
                .format(TextFormatting.GOLD)
                .end()
                .text(" ");
        if (data.getUrl() != null)
            builder.translation("update.clickhere").end()
                    .format(TextFormatting.LIGHT_PURPLE)
                    .click(new ClickEvent(ClickEvent.Action.OPEN_URL, data.getUrl()))
                    .text(". ");
        ITextComponent msg = builder
                .text(response.minecraft.version)
                .text(" - ")
                .text(response.minecraft.changes)
                .build();
        LogManager.getLogger("Updates").info(msg.getUnformattedText());
        this.chatProxy.addToChat("Updates", msg);
    }

    public static void runUpdateCheck(IChatProxy chat, VersionData version) {
        new UpdateChecker(chat, version).start();

    }
}


<|start_filename|>src/main/java/mnm/mods/util/TexturedModal.java<|end_filename|>
package mnm.mods.util;

import net.minecraft.util.ResourceLocation;

/**
 * Defines a modal with a resource location, u and v values, a width and a
 * height.
 */
public class TexturedModal {

    private final ResourceLocation resource;
    private final int xPos;
    private final int yPos;
    private final int width;
    private final int height;

    public TexturedModal(ResourceLocation resource, int xPos, int yPos, int width, int height) {
        this.resource = resource;
        this.xPos = xPos;
        this.yPos = yPos;
        this.width = width;
        this.height = height;
    }

    public ResourceLocation getResourceLocation() {
        return resource;
    }

    public int getXPos() {
        return xPos;
    }

    public int getYPos() {
        return yPos;
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/gui/ChatScreen.java<|end_filename|>
package mnm.mods.tabbychat.api.gui;

import com.google.common.eventbus.EventBus;

public interface ChatScreen {

    EventBus getBus();
}


<|start_filename|>src/main/java/mnm/mods/util/gui/events/ActionPerformedEvent.java<|end_filename|>
package mnm.mods.util.gui.events;

import mnm.mods.util.gui.GuiComponent;

public class ActionPerformedEvent extends GuiEvent {

    public ActionPerformedEvent(GuiComponent component) {
        super(component);
    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiComponent.java<|end_filename|>
package mnm.mods.util.gui;

import com.google.common.eventbus.EventBus;
import mnm.mods.util.Color;
import mnm.mods.util.ILocation;
import mnm.mods.util.Location;
import mnm.mods.util.TexturedModal;
import mnm.mods.util.gui.events.ActionPerformedEvent;
import mnm.mods.util.gui.events.GuiKeyboardEvent;
import mnm.mods.util.gui.events.GuiMouseEvent;
import mnm.mods.util.gui.events.GuiMouseEvent.MouseEvent;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.util.text.ITextComponent;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.logging.log4j.LogManager;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;

import java.awt.Dimension;
import java.awt.Point;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.annotation.Nullable;

/**
 * The base class for all gui components.
 *
 * @author Matthew
 */
public abstract class GuiComponent extends Gui {

    private boolean enabled = true;
    private boolean visible = true;
    private boolean hovered;
    private boolean focused;
    private int touchValue;
    private int lastButton;
    private long lastButtonTime;

    protected Minecraft mc = Minecraft.getMinecraft();

    private Color secondaryColor;
    private Color primaryColor;
    private GuiPanel parent;
    private ILocation location = new Location();
    private Dimension minimumSize = new Dimension();
    private float scale = 1;
    private ITextComponent caption;

    private EventBus bus = new EventBus((exception, context) -> LogManager.getLogger().throwing(exception));

    public GuiComponent() {
        this.bus.register(this);
    }

    /**
     * Draws this component on screen.
     *
     * @param mouseX The mouse x
     * @param mouseY The mouse y
     */
    public void drawComponent(int mouseX, int mouseY) {
    }

    public void drawCaption(int x, int y) {
        getCaptionText()
                .map(ITextComponent::getFormattedText)
                .filter(((Predicate<String>) String::isEmpty).negate())
                .filter(t -> this.isHovered())
                .ifPresent(text -> this.drawCaption(text, x, y));
    }

    protected void drawCaption(String caption, int x, int y) {
        caption = StringEscapeUtils.unescapeJava(caption);
        String[] list = caption.split("\n\r?");

        int w = 0;
        // find the largest width
        for (String s : list) {
            w = Math.max(w, (int) (mc.fontRenderer.getStringWidth(s) * getActualScale()));
        }
        y -= mc.fontRenderer.FONT_HEIGHT * list.length;

        Point point = getActualLocation().getPoint();
        ScaledResolution sr = new ScaledResolution(mc);
        int w2 = w;
        int x2 = x;
        while (x2 - 8 + point.x + w2 + 20 > sr.getScaledWidth()) {
            x--;
            w2--;
        }
        // put it on top
        GlStateManager.pushMatrix();
        Gui.drawRect(x - 2, y - 2, x + w + 2, y + mc.fontRenderer.FONT_HEIGHT * list.length + 1,
                0xcc333333);
        drawBorders(x - 2, y - 2, x + w + 2, y + mc.fontRenderer.FONT_HEIGHT * list.length + 1,
                0xccaaaaaa);
        for (String s : list) {
            mc.fontRenderer.drawStringWithShadow(s, x, y, this.getPrimaryColorProperty().getHex());
            y += mc.fontRenderer.FONT_HEIGHT;
        }
        GlStateManager.popMatrix();
    }

    protected void drawBorders(int x1, int y1, int x2, int y2, int color) {
        this.drawVerticalLine(x1 - 1, y1 - 1, y2 + 1, color); // left
        this.drawHorizontalLine(x1 - 1, x2, y1 - 1, color); // top
        this.drawVerticalLine(x2, y1 - 1, y2 + 1, color); // right
        this.drawHorizontalLine(x1, x2 - 1, y2, color); // bottom
    }

    /**
     * Draws borders around the provided points. Uses a brightened background
     * color with 0xaa transparency.
     *
     * @param x1 left point
     * @param y1 upper point
     * @param x2 right point
     * @param y2 lower point
     */
    protected void drawBorders(int x1, int y1, int x2, int y2) {
        Color color = getSecondaryColorProperty();
        int r = color.getRed();
        int g = color.getGreen();
        int b = color.getBlue();
        double amt = .75;
        r += luminance(r, amt);
        g += luminance(g, amt);
        b += luminance(b, amt);
        color = Color.of(r, g, b, 0xaa);
        drawBorders(x1, y1, x2, y2, color.getHex());
    }

    private static int luminance(int o, double amt) {
        return (int) ((255 - o) * amt);
    }

    /**
     * Updates the component. Called when it is called on the {@link GuiScreen}.
     */
    public void updateComponent() {
    }

    /**
     * Handles the mouse input and sends it to the mouse and action listeners.
     */
    public void handleMouseInput() {
        if (!isEnabled()) {
            this.hovered = false;
            return;
        }
        if (mc.currentScreen != null) {
            float scale = getActualScale();
            Point point = scalePoint(new Point(Mouse.getX(), Mouse.getY()), mc.currentScreen);
            ILocation actual = getActualLocation();
            // adjust for position and scale
            int x = (int) ((point.x - actual.getXPos()) / scale);
            int y = (int) ((point.y - actual.getYPos()) / scale);

            int button = Mouse.getEventButton();
            int scroll = Mouse.getEventDWheel();

            this.hovered = point.x >= actual.getXPos() && point.x <= actual.getXWidth()
                    && point.y >= actual.getYPos() && point.y <= actual.getYHeight();

            getBus().post(new GuiMouseEvent(this, MouseEvent.RAW, x, y, button, scroll));

            if (Mouse.getEventButtonState()) {
                if (this.mc.gameSettings.touchscreen && this.touchValue++ > 0) {
                    return;
                }
                lastButton = button;
                lastButtonTime = Minecraft.getSystemTime();
                if (isHovered()) {
                    getBus().post(new GuiMouseEvent(this, MouseEvent.CLICK, x, y, button, 0));
                    getBus().post(new ActionPerformedEvent(this));
                }
            } else if (button != -1) {
                if (this.mc.gameSettings.touchscreen && --this.touchValue > 0) {
                    return;
                }
                lastButton = -1;
                if (isHovered())
                    getBus().post(new GuiMouseEvent(this, MouseEvent.RELEASE, x, y, button, 0));
            } else if (lastButton != -1 && lastButtonTime > 0) {
                long buttonTime = Minecraft.getSystemTime() - this.lastButtonTime;
                getBus().post(new GuiMouseEvent(this, MouseEvent.DRAG, x, y, this.lastButton, buttonTime));
            }

            if (scroll != 0) {
                getBus().post(new GuiMouseEvent(this, MouseEvent.SCROLL, x, y, -1, -1, scroll));
            }
        }
    }

    /**
     * Handles the keyboard input and sends it to the keyboard listeners.
     */
    public void handleKeyboardInput() {
        int key = Keyboard.getEventKey();
        char character = Keyboard.getEventCharacter();
        long time = Keyboard.getEventNanoseconds();
        GuiKeyboardEvent event = new GuiKeyboardEvent(this, key, character, time);
        getBus().post(event);
    }

    /**
     * Called when the screen is closed.
     */
    public void onClosed() {
        this.hovered = false;
    }

    /**
     * Gets the current location of this component.
     *
     * @return The current immutable location.
     */
    public ILocation getLocation() {
        return this.location;
    }

    /**
     * Sets the location of this component. In order to maintain encapsulation,
     * it is wrapped as an immutable.
     *
     * @param location The new location
     */
    public void setLocation(ILocation location) {
        this.location = location.asImmutable();
    }

    /**
     * Gets the event bus used for the gui events for this component. This
     * replaces the listener interfaces previously used. They should easily port
     * over by adding a {@link com.google.common.eventbus.Subscribe} annotation
     * to the method.
     *
     * @return The event bus
     */
    public EventBus getBus() {
        return bus;
    }

    /**
     * Gets the parent of this component. Will return {@code null} until it is
     * added to a panel by being used as the parameter to
     * {@link GuiPanel#addComponent(GuiComponent)} or
     * {@link GuiPanel#addComponent(GuiComponent, Object)}.
     *
     * @return The parent or null if there is none
     */
    public Optional<GuiPanel> getParent() {
        return Optional.ofNullable(this.parent);
    }


    /**
     * Sets the parent of this component. Should only be used by
     * {@link GuiPanel}.
     *
     * @param guiPanel The parent
     */
    void setParent(@Nullable GuiPanel guiPanel) {
        this.parent = guiPanel;
    }

    /**
     * Gets the top-most parent of this component. May return null if this
     * component has no parent and is not a {@link GuiPanel}.
     *
     * @return The root panel or {@code null}.
     */
    private Optional<GuiPanel> getRootPanel() {
        Optional<GuiPanel> panel = getParent();
        while (true) {
            Optional<GuiPanel> parent = panel.flatMap(GuiComponent::getParent);
            if (!parent.isPresent())
                break;
            panel = parent;
        }
        if (panel.isPresent())
            return panel;
        else if (this instanceof GuiPanel)
            return Optional.of((GuiPanel) this);
        else
            return Optional.empty();
    }

    public ILocation getActualLocation() {
        Location location = this.getLocation().copy();
        location.scale(getActualScale());
        getParent().map(GuiComponent::getActualLocation).ifPresent(loc1 ->
                location.move(loc1.getXPos(), loc1.getYPos())
        );
        return location;
    }

    public void setMinimumSize(Dimension size) {
        this.minimumSize = size;
    }

    public Dimension getMinimumSize() {
        return minimumSize;
    }

    /**
     * Sets the scale for this component.
     *
     * @param scale The scale
     */
    public void setScale(float scale) {
        this.scale = scale;
    }

    /**
     * Gets the scale for this component.
     *
     * @return The scale
     */
    public float getScale() {
        return scale;
    }

    /**
     * Gets the total scale of this component. Takes into account all the
     * parents.
     *
     * @return The scale
     */
    protected float getActualScale() {
        return getScale() * getParent().map(GuiComponent::getActualScale).orElse(1F);
    }

    public Optional<Color> getPrimaryColor() {
        return Optional.ofNullable(this.primaryColor);
    }

    public void setPrimaryColor(@Nullable Color color) {
        this.primaryColor = color;
    }

    public Optional<Color> getSecondaryColor() {
        return Optional.ofNullable(this.secondaryColor);
    }

    public void setSecondaryColor(@Nullable Color color) {
        this.secondaryColor = color;
    }

    public Color getPrimaryColorProperty() {
        return getProperty(GuiComponent::getPrimaryColor, Color.WHITE);
    }

    public Color getSecondaryColorProperty() {
        return getProperty(GuiComponent::getSecondaryColor, Color.of(0));
    }

    /**
     * Gets if this is enabled. Disabled components will not handle mouse or
     * keyboard events.
     *
     * @return True if enabled, false if disabled.
     */
    public boolean isEnabled() {
        return enabled;
    }

    /**
     * Sets if this is enabled or not. Disabled components will not handle mouse
     * or keyboard events.
     *
     * @param enabled True for enabled, false for disabled
     */
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    /**
     * Gets if this component is visible. Non-visible components are not
     * rendered.
     *
     * @return The visibility state
     */
    public boolean isVisible() {
        return visible;
    }

    /**
     * Sets this component's visibility. Non-visible components are not
     * rendered.
     *
     * @param visible The visibility state
     */
    public void setVisible(boolean visible) {
        if (!visible) {
            this.onClosed();
        }
        this.visible = visible;
    }

    /**
     * Returns if the cursor is hovered over this component.
     *
     * @return THe hover state
     */
    public boolean isHovered() {
        return hovered && getParent().map(GuiComponent::isHovered).orElse(true);
    }

    public void setCaption(@Nullable ITextComponent text) {
        this.caption = text;
    }

    /**
     * Gets the caption which is shown when the mouse is hovering over this
     * component.
     *
     * @return The caption
     * @deprecated Use {@link #getCaptionText()} instead.
     */
    @Nullable
    @Deprecated
    public String getCaption() {
        return getCaptionText().map(ITextComponent::getFormattedText).orElse(null);
    }

    public Optional<ITextComponent> getCaptionText() {
        return Optional.ofNullable(caption);
    }

    /**
     * Gets the current focus of this component.
     *
     * @return The focus
     */
    public boolean isFocused() {
        return focused;
    }

    /**
     * Sets the component's focus
     *
     * @param focused The focus value
     */
    public void setFocused(boolean focused) {
        // check if is focusable
        if (isFocusable()) {
            if (focused) {
                // unfocus everything before focusing this one
                getRootPanel().ifPresent(GuiPanel::unfocusAll);
            }
            this.focused = focused;
        }
        // otherwise ignore
    }

    /**
     * Denotes if this is focusable. Override to change.
     *
     * @return Whether this is focusable
     */
    public boolean isFocusable() {
        return false;
    }

    private <T> T getProperty(final Function<GuiComponent, Optional<T>> prop, T def) {
        return getProperty(prop).orElse(def);
    }

    private <T> Optional<T> getProperty(final Function<GuiComponent, Optional<T>> prop) {
        Optional<T> result = prop.apply(this);
        Optional<GuiPanel> parent = getParent();
        if (!result.isPresent()) {
            result = parent.flatMap((GuiComponent p) -> p.getProperty(prop));
        }
        return result;
    }

    protected static Point scalePoint(Point point, GuiScreen screen) {
        Minecraft mc = Minecraft.getMinecraft();
        int x = point.x * screen.width / mc.displayWidth;
        int y = screen.height - point.y * screen.height / mc.displayHeight - 1;
        return new Point(x, y);
    }

    protected void drawModalCorners(TexturedModal modal) {
        ILocation location = getLocation();
        int x = 0;
        int y = 0;

        int w = location.getWidth() / 2;
        int w2 = location.getWidth() - w;
        int h = location.getHeight() / 2;
        int h2 = location.getHeight() - h + 1;

        int mx = modal.getXPos();
        int my = modal.getYPos();
        int mw = modal.getWidth() - w2;
        int mh = modal.getHeight() - h2;

        // bind the texture
        mc.getTextureManager().bindTexture(modal.getResourceLocation());

        // top left
        drawTexturedModalRect(x, y, mx, my, w, h);
        // top right
        drawTexturedModalRect(x + w, y, mx + mw, my, w2, h);
        // bottom left
        drawTexturedModalRect(x, y + h, mx, my + mh, w, h2);
        // bottom right
        drawTexturedModalRect(x + w, y + h, mx + mw, my + mh, w2, h2);

    }
}


<|start_filename|>src/test/java/mnm/mods/tabbychat/test/ChatTextUtilsTest.java<|end_filename|>
package mnm.mods.tabbychat.test;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

import mnm.mods.tabbychat.util.ChatTextUtils;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextComponentString;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.util.text.event.ClickEvent;

public class ChatTextUtilsTest {

    @Test
    public void testSubChat() {
        assertEquals(makeChat(false), ChatTextUtils.subChat(makeChat(true), 7));
    }

    private static ITextComponent makeChat(boolean tag) {

        ITextComponent chat = new TextComponentString(tag ? "[test] " : "");
        chat.getStyle().setBold(true);
        {
            ITextComponent colored = new TextComponentString("This should be green. ");
            colored.getStyle().setColor(TextFormatting.GREEN);
            chat.appendSibling(colored);
        }
        chat.appendText(" ");
        {
            ITextComponent link = new TextComponentString("This is a link.");
            link.getStyle().setClickEvent(new ClickEvent(ClickEvent.Action.OPEN_URL, "http://google.com/"));
            chat.appendSibling(link);
        }
        return chat;
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/core/mixin/IGuiIngame.java<|end_filename|>
package mnm.mods.tabbychat.core.mixin;

import net.minecraft.client.gui.GuiIngame;
import net.minecraft.client.gui.GuiNewChat;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;

@Mixin(GuiIngame.class)
public interface IGuiIngame {

    @Accessor
    void setPersistantChatGUI(GuiNewChat chat);
}


<|start_filename|>src/main/java/mnm/mods/util/gui/FlowLayout.java<|end_filename|>
package mnm.mods.util.gui;

import com.google.common.collect.Lists;
import mnm.mods.util.ILocation;
import mnm.mods.util.Location;

import java.awt.Dimension;
import java.util.List;

/**
 * A layout that puts items side-by-side and left-to-right.
 *
 * @author Matthew
 */
public class FlowLayout implements ILayout {

    private List<GuiComponent> components = Lists.newArrayList();

    @Override
    public void addComponent(GuiComponent comp, Object constraints) {
        components.add(comp);
    }

    @Override
    public void removeComponent(GuiComponent comp) {
        components.remove(comp);
    }

    @Override
    public void layoutComponents(GuiPanel parent) {
        parent.getParent().ifPresent(p -> {
            int xPos = 0;
            int yPos = 0;
            int maxH = 0;
            for (GuiComponent comp : components) {
                Dimension size = comp.getMinimumSize();
                if (xPos + size.width >= p.getLocation().getWidth()) {
                    // wrapping
                    xPos = 0;
                    yPos += maxH;
                    maxH = 0;
                }
                comp.setLocation(new Location(xPos, yPos, size.width, size.height));

                maxH = Math.max(maxH, size.height);
                xPos += size.width;
            }
        });
    }

    @Override
    public Dimension getLayoutSize() {
        int width = 0;
        int height = 0;

        for (GuiComponent comp : components) {
            ILocation loc = comp.getLocation();
            width = Math.max(width, loc.getXPos() + loc.getWidth());
            height = Math.max(height, loc.getYPos() + loc.getHeight());
        }
        return new Dimension(width, height);
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/config/GuiSettingEnum.java<|end_filename|>
package mnm.mods.util.gui.config;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.eventbus.Subscribe;
import mnm.mods.util.Color;
import mnm.mods.util.ILocation;
import mnm.mods.util.config.Value;
import mnm.mods.util.gui.events.GuiMouseEvent;
import mnm.mods.util.gui.events.GuiMouseEvent.MouseEvent;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.resources.I18n;
import org.apache.commons.lang3.ArrayUtils;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;

/**
 * A setting for set values, such as enums. Values should either have a
 * toString() method or provide a names array during construction.
 *
 * @param <T> The type
 * @author Matthew
 */
public class GuiSettingEnum<T> extends GuiSetting<T> {

    private final List<T> values;
    private final Function<T, String> namer;

    private T value;

    private String text;

    public GuiSettingEnum(Value<T> setting, T[] values) {
        this(setting, Arrays.asList(values), Objects::toString);
    }

    @Deprecated
    public GuiSettingEnum(Value<T> setting, T[] values, String[] names) {
        this(setting, Arrays.asList(values), val -> {
            int i = ArrayUtils.indexOf(values, val);
            return names[i];
        });
        Preconditions.checkArgument(values.length == names.length);
    }

    public GuiSettingEnum(Value<T> setting, List<T> values, Function<T, String> namer) {
        super(setting);
        this.namer = namer;
        this.values = ImmutableList.copyOf(values);

        setValue(setting.get());
        setSecondaryColor(Color.DARK_GRAY);

    }

    @Subscribe
    public void activate(GuiMouseEvent event) {
        if (event.getType() == MouseEvent.CLICK) {
            int selected = this.values.indexOf(value);
            int max = this.values.size();
            int mov = 0;

            if (event.getButton() == 0) {
                // Left click, go forward
                mov = 1;
            } else if (event.getButton() == 1) {
                // Right click, go backward
                mov = -1;
            }

            if (mov != 0) {
                // find the index and increment or decrement
                int id = selected + mov;
                id = id < 0 ? (max - id) % max : id % max;

                setValue(values.get(id));
            }
        }
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        ILocation loc = this.getLocation();
        Gui.drawRect(0, 0, loc.getWidth(), loc.getHeight(), 0xff000000);
        String string = mc.fontRenderer.trimStringToWidth(text, loc.getWidth());
        int xPos = loc.getWidth() / 2 - mc.fontRenderer.getStringWidth(string) / 2;
        int yPos = loc.getHeight() / 2 - 4;
        mc.fontRenderer.drawString(string, xPos, yPos, getPrimaryColorProperty().getHex());
        drawBorders(0, -1, loc.getWidth(), loc.getHeight() + 1);
    }

    @Override
    public T getValue() {
        return value;
    }

    @Override
    public void setValue(T value) {
        this.value = value;

        String text = this.value.toString();
        if (namer != null) {
            text = namer.apply(this.value);
        }
        this.text = I18n.format(text);
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/gui/ChatHandle.java<|end_filename|>
package mnm.mods.tabbychat.gui;

import mnm.mods.util.Color;
import mnm.mods.util.ILocation;
import mnm.mods.util.Location;
import mnm.mods.util.gui.GuiComponent;

import java.awt.Dimension;
import javax.annotation.Nonnull;

public class ChatHandle extends GuiComponent {

    ChatHandle() {
        setLocation(new Location(0, 0, 10, 10));
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        ILocation loc = getLocation();
        this.drawHorizontalLine(3, loc.getWidth() - 4, 3, getPrimaryColorProperty().getHex());
        this.drawVerticalLine(loc.getWidth() - 4, 3, loc.getHeight() - 3, getPrimaryColorProperty().getHex());
    }

    @Nonnull
    @Override
    public Color getPrimaryColorProperty() {
        int opac = (int)(mc.gameSettings.chatOpacity * 255);
        return isHovered() ? Color.of(255, 255, 160, opac) : Color.of(255, 255, 255, opac);
    }

    @Nonnull
    @Override
    public Dimension getMinimumSize() {
        return new Dimension(12, 12);
    }
}


<|start_filename|>src/test/java/mnm/mods/tabbychat/test/PatternTests.java<|end_filename|>
package mnm.mods.tabbychat.test;

import com.google.common.collect.Lists;
import mnm.mods.tabbychat.util.ChannelPatterns;
import org.junit.Assert;
import org.junit.Test;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class PatternTests {


    @Test
    public void testAngles() throws Exception {
        test(ChannelPatterns.ANGLES, "<%s> %s");
    }
    @Test
    public void testBraces() throws Exception {
        test(ChannelPatterns.BRACES, "{%s} %s");
    }

    @Test
    public void testBrackets() throws Exception {
        test(ChannelPatterns.BRACKETS, "[%s] %s");
    }
    @Test
    public void testParens() throws Exception {
        test(ChannelPatterns.PARENS, "(%s) %s");
    }

    @Test
    public void testAnglesParens() throws Exception {
        test(ChannelPatterns.ANGLESPARENS, "<(%s) Someone> %s");
    }
    @Test
    public void testAnglesBrackets() throws Exception {
        test(ChannelPatterns.ANGLESBRACKETS, "<[%s] Someone> %s");
    }

    private void test(ChannelPatterns pattern, String format) {
        List<ChanPattern> list = setupChannelPatterns(pattern, format);
        for (ChanPattern chan : list) {
            Assert.assertEquals(chan.channel, chan.testChannel());
        }
    }

    private List<ChanPattern> setupChannelPatterns(ChannelPatterns pattern, String chan) {
        String[] channels = {
                "global",
                "g",
                "local"
        };
        String[] messages = {
                "<JoyJoy> Hello there",
                "[Admin] <MrTCP> ur ba&ned"
        };
        List<ChanPattern> received = Lists.newArrayList();
        for (String channel : channels) {
            for (String message : messages) {
                String msg = String.format(chan, channel, message);
                received.add(new ChanPattern(pattern, channel, msg));
            }
        }
        return received;
    }

    private class ChanPattern {
        final ChannelPatterns pattern;
        final String channel;
        final String message;

        public ChanPattern(ChannelPatterns pattern, String channel, String message) {
            this.pattern = pattern;
            this.channel = channel;
            this.message = message;
        }

        public String testChannel() {
            Pattern pattern = this.pattern.getPattern();
            Matcher matcher = pattern.matcher(message);
            if (matcher.find()) {
                return matcher.group(1);
            }
            return null;
        }

    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/events/AbstractMouseEvent.java<|end_filename|>
package mnm.mods.util.gui.events;

import mnm.mods.util.gui.GuiComponent;

/**
 * Abstract class for events that contain the current mouse coordinates.
 */
abstract class AbstractMouseEvent extends GuiEvent {

    private int mouseX;
    private int mouseY;

    AbstractMouseEvent(GuiComponent component, int mouseX, int mouseY) {
        super(component);
        this.mouseX = mouseX;
        this.mouseY = mouseY;
    }

    public int getMouseX() {
        return mouseX;
    }

    public int getMouseY() {
        return mouseY;
    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/config/GuiSettingBoolean.java<|end_filename|>
package mnm.mods.util.gui.config;

import mnm.mods.util.config.Value;
import mnm.mods.util.gui.GuiCheckbox;
import mnm.mods.util.gui.config.GuiSetting.GuiSettingWrapped;

/**
 * A gui input for booleans as a checkbox.
 *
 * @author Matthew
 */
public class GuiSettingBoolean extends GuiSettingWrapped<Boolean, GuiCheckbox> {

    public GuiSettingBoolean(Value<Boolean> setting) {
        super(setting, new GuiCheckbox());
    }
}


<|start_filename|>src/main/java/mnm/mods/util/update/UpdateResponse.java<|end_filename|>
package mnm.mods.util.update;

import com.google.gson.annotations.SerializedName;

public class UpdateResponse {

    @SerializedName("@MCVERSION@")
    public Version minecraft;

    public class Version {
        double revision;
        String version;
        String changes;
    }
}


<|start_filename|>src/main/java/mnm/mods/util/config/ValueList.java<|end_filename|>
package mnm.mods.util.config;

import com.google.common.collect.Lists;

import java.util.Iterator;
import java.util.List;

public class ValueList<T> extends Value<List<T>> implements Iterable<T> {

    ValueList() {
        set(Lists.newArrayList());
    }

    public void add(T value) {
        this.get().add(value);
    }

    public void add(int index, T value) {
        this.get().add(index, value);
    }

    public void remove(T value) {
        this.get().remove(value);
    }

    public void remove(int index) {
        this.get().remove(index);
    }

    public void clear() {
        this.get().clear();
    }

    public T get(int index) {
        return this.get().get(index);
    }

    @Override
    public void set(List<T> val) {
        super.set(Lists.newArrayList(val));
    }

    @Override
    public Iterator<T> iterator() {
        return get().iterator();
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/gui/settings/GuiSettingsScreen.java<|end_filename|>
package mnm.mods.tabbychat.gui.settings;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.eventbus.Subscribe;
import com.mumfrey.liteloader.core.LiteLoader;
import mnm.mods.tabbychat.ChatManager;
import mnm.mods.tabbychat.TabbyChat;
import mnm.mods.util.Color;
import mnm.mods.util.ILocation;
import mnm.mods.util.Location;
import mnm.mods.util.config.SettingsFile;
import mnm.mods.util.gui.BorderLayout;
import mnm.mods.util.gui.ComponentScreen;
import mnm.mods.util.gui.GuiButton;
import mnm.mods.util.gui.GuiComponent;
import mnm.mods.util.gui.GuiPanel;
import mnm.mods.util.gui.VerticalLayout;
import mnm.mods.util.gui.config.SettingPanel;
import mnm.mods.util.gui.events.ActionPerformedEvent;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Gui;

import java.util.List;
import java.util.Map;
import java.util.function.Supplier;

public class GuiSettingsScreen extends ComponentScreen {

    private static Map<Class<? extends SettingPanel<?>>, Supplier<? extends SettingPanel<?>>> settings = Maps.newLinkedHashMap();

    static {
        registerSetting(GuiSettingsGeneral.class, GuiSettingsGeneral::new);
        registerSetting(GuiSettingsServer.class, GuiSettingsServer::new);
        registerSetting(GuiSettingsChannel.class, GuiSettingsChannel::new);
        registerSetting(GuiAdvancedSettings.class, GuiAdvancedSettings::new);
    }

    private List<SettingPanel<?>> panels = Lists.newArrayList();

    private GuiPanel panel;

    private GuiPanel settingsList;
    private SettingPanel<?> selectedSetting;

    public GuiSettingsScreen(SettingPanel<?> setting) {
        this.selectedSetting = setting;

        for (Map.Entry<Class<? extends SettingPanel<?>>, Supplier<? extends SettingPanel<?>>> sett : settings.entrySet()) {
            try {
                if (setting != null && setting.getClass() == sett.getKey()) {
                    panels.add(setting);
                } else {
                    panels.add(sett.getValue().get());
                }
            } catch (Exception e) {
                TabbyChat.getLogger().error("Unable to add " + sett.getKey().getName() + " as a setting.", e);
            }
        }
    }

    @Override
    public void initGui() {


        getPanel().addComponent(panel = new GuiPanel(new BorderLayout()));

        int x = this.width / 2 - 300 / 2;
        int y = this.height / 2 - 200 / 2;
        panel.setLocation(new Location(x, y, 300, 200));

        GuiPanel panel = new GuiPanel(new BorderLayout());
        this.panel.addComponent(panel, BorderLayout.Position.WEST);
        panel.addComponent(settingsList = new GuiPanel(new VerticalLayout()), BorderLayout.Position.WEST);

        GuiButton close = new GuiButton("Close");
        close.setLocation(new Location(0, 0, 40, 10));
        close.setSecondaryColor(Color.of(0, 255, 0, 127));
        close.getBus().register(new Object() {
            @Subscribe
            public void closeTheScreen(ActionPerformedEvent event) {
                Minecraft.getMinecraft().displayGuiScreen(null);
            }
        });
        panel.addComponent(close, BorderLayout.Position.SOUTH);

        {
            // Populate the settings
            for (SettingPanel<?> sett : panels) {
                SettingsButton button = new SettingsButton(sett);
                button.getBus().register(new Object() {
                    @Subscribe
                    public void switchToThisPanel(ActionPerformedEvent event) {
                        selectSetting(((SettingsButton) event.getComponent()).getSettings());
                    }
                });
                settingsList.addComponent(button);
                sett.initGUI();
            }
        }
        SettingPanel<?> panelClass;
        if (selectedSetting == null) {
            panelClass = panels.get(0);
        } else {
            panelClass = selectedSetting;
        }
        selectSetting(panelClass);
    }

    @Override
    public void onGuiClosed() {
        super.onGuiClosed();
        for (SettingPanel<?> settingPanel : panels) {
            SettingsFile config = settingPanel.getSettings();
            LiteLoader.getInstance().writeConfig(config);
        }
        ((ChatManager) TabbyChat.getInstance().getChat()).getChatBox().getChatArea().markDirty();
    }

    @Override
    public void setWorldAndResolution(Minecraft mc, int width, int height) {
        this.panels.forEach(GuiPanel::clearComponents);
        super.setWorldAndResolution(mc, width, height);
    }

    private void deactivateAll() {
        for (GuiComponent comp : settingsList) {
            if (comp instanceof SettingsButton) {
                ((SettingsButton) comp).setActive(false);
            }
        }
    }

    private <T extends SettingPanel<?>> void activate(Class<T> settingClass) {
        for (GuiComponent comp : settingsList) {
            if (comp instanceof SettingsButton
                    && ((SettingsButton) comp).getSettings().getClass().equals(settingClass)) {
                ((SettingsButton) comp).setActive(true);
                break;
            }
        }
    }

    @Override
    public void drawScreen(int mouseX, int mouseY, float tick) {
        // drawDefaultBackground();
        ILocation rect = panel.getLocation();
        Gui.drawRect(rect.getXPos(), rect.getYPos(), rect.getXWidth(), rect.getYHeight(), Integer.MIN_VALUE);
        super.drawScreen(mouseX, mouseY, tick);
    }

    private void selectSetting(SettingPanel<?> setting) {
//        setting.clearComponents();
        deactivateAll();
        panel.removeComponent(selectedSetting);
        selectedSetting = setting;
        activate(setting.getClass());
        this.panel.addComponent(this.selectedSetting, BorderLayout.Position.CENTER);
    }

    private static <T extends SettingPanel<?>> void registerSetting(Class<T> settings, Supplier<T> constructor) {
        if (!GuiSettingsScreen.settings.containsKey(settings)) {
            GuiSettingsScreen.settings.put(settings, constructor);
        }
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/gui/settings/GuiSettingsChannel.java<|end_filename|>
package mnm.mods.tabbychat.gui.settings;

import static mnm.mods.tabbychat.util.Translation.*;

import com.google.common.eventbus.Subscribe;
import mnm.mods.tabbychat.TabbyChat;
import mnm.mods.tabbychat.api.Channel;
import mnm.mods.tabbychat.api.TabbyAPI;
import mnm.mods.tabbychat.settings.ServerSettings;
import mnm.mods.util.Color;
import mnm.mods.util.Location;
import mnm.mods.util.gui.BorderLayout;
import mnm.mods.util.gui.GuiButton;
import mnm.mods.util.gui.GuiCheckbox;
import mnm.mods.util.gui.GuiComponent;
import mnm.mods.util.gui.GuiGridLayout;
import mnm.mods.util.gui.GuiLabel;
import mnm.mods.util.gui.GuiPanel;
import mnm.mods.util.gui.GuiScrollingPanel;
import mnm.mods.util.gui.GuiText;
import mnm.mods.util.gui.VerticalLayout;
import mnm.mods.util.gui.config.SettingPanel;
import mnm.mods.util.gui.events.ActionPerformedEvent;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.text.TextComponentTranslation;

public class GuiSettingsChannel extends SettingPanel<ServerSettings> {

    private Channel channel;

    private GuiScrollingPanel channels;
    private GuiPanel panel;

    private GuiText alias;
    private GuiText prefix;
    private GuiCheckbox hidePrefix;
    private GuiText command;

    GuiSettingsChannel() {
        this(null);
    }

    public GuiSettingsChannel(Channel channel) {
        this.channel = channel;
        this.setLayout(new BorderLayout());
        this.setDisplayString(I18n.format(CHANNEL_TITLE));
        this.setSecondaryColor(Color.of(0, 15, 100, 65));

    }

    @Override
    public void initGUI() {
        channels = new GuiScrollingPanel();
        channels.setLocation(new Location(0, 0, 60, 200));
        channels.getContentPanel().setLayout(new VerticalLayout());
        for (Channel channel : getSettings().channels.get().values()) {
            channels.getContentPanel().addComponent(new ChannelButton(channel));
        }
        this.addComponent(channels, BorderLayout.Position.WEST);
        panel = new GuiPanel();
        panel.setLayout(new GuiGridLayout(8, 20));
        this.addComponent(panel, BorderLayout.Position.CENTER);

        this.select(channel);
    }

    private void select(Channel channel) {

        for (GuiComponent comp : channels.getContentPanel()) {
            if (((ChannelButton) comp).channel == channel) {
                comp.setEnabled(false);
            } else {
                comp.setEnabled(true);
            }
        }

        int pos = 1;

        this.channel = channel;
        this.panel.clearComponents();
        if (channel == null) {
            if (channels.getContentPanel().getComponentCount() > 0) {
                this.panel.addComponent(new GuiLabel(new TextComponentTranslation(CHANNEL_SELECT)), new int[] { 1, pos });
            } else {
                this.panel.addComponent(new GuiLabel(new TextComponentTranslation(CHANNEL_NONE)), new int[] { 1, pos });
            }
            return;
        }
        this.panel.addComponent(
                new GuiLabel(new TextComponentTranslation(CHANNEL_LABEL, channel.getName())),
                new int[] { 1, pos });

        pos += 3;
        this.panel.addComponent(new GuiLabel(new TextComponentTranslation(CHANNEL_ALIAS)), new int[] { 1, pos });
        this.panel.addComponent(alias = new GuiText(), new int[] { 3, pos, 4, 1 });
        alias.setValue(channel.getAlias());

        pos += 2;
        this.panel.addComponent(new GuiLabel(new TextComponentTranslation(CHANNEL_PREFIX)), new int[] { 1, pos });
        this.panel.addComponent(prefix = new GuiText(), new int[] { 3, pos, 4, 1 });
        prefix.setValue(channel.getPrefix());

        pos += 2;
        this.panel.addComponent(hidePrefix = new GuiCheckbox(), new int[] { 1, pos });
        hidePrefix.setValue(channel.isPrefixHidden());
        this.panel.addComponent(new GuiLabel(new TextComponentTranslation(CHANNEL_HIDE_PREFIX)), new int[] { 2, pos });

        pos += 2;
        this.panel.addComponent(command = new GuiText(), new int[] { 3, pos, 4, 1 });
        command.setValue(channel.getCommand());
        this.panel.addComponent(new GuiLabel(new TextComponentTranslation(CHANNEL_COMMAND)), new int[] { 1, pos });

        GuiButton accept = new GuiButton(I18n.format("gui.done"));
        accept.getBus().register(new Object() {
            @Subscribe
            public void somebodySaveMe(ActionPerformedEvent event) {
                save();
            }
        });
        this.panel.addComponent(accept, new int[] { 2, 15, 4, 2 });

        GuiButton forget = new GuiButton(I18n.format(CHANNEL_FORGET));
        forget.getBus().register(new Object() {
            @Subscribe
            public void oohShinyObject(ActionPerformedEvent event) {
                Channel channel = GuiSettingsChannel.this.channel;
                // remove from chat
                TabbyAPI.getAPI().getChat().removeChannel(channel);
                // remove from settings file
                getSettings().channels.get().remove(channel.getName());
                if (!channel.isPm()) {
                    // don't add this channel again.
                    getSettings().general.ignoredChannels.add(channel.getName());
                }
                // remove from settings gui
                for (GuiComponent comp : channels.getContentPanel()) {
                    if (comp instanceof ChannelButton && ((ChannelButton) comp).channel == channel) {
                        channels.getContentPanel().removeComponent(comp);
                        break;
                    }
                }
                select(null);
            }
        });
        this.panel.addComponent(forget, new int[] { 2, 17, 4, 2 });
    }

    private void save() {
        channel.setAlias(alias.getValue());
        channel.setPrefix(prefix.getValue());
        channel.setPrefixHidden(hidePrefix.getValue());
        channel.setCommand(command.getValue());
    }

    @Override
    public ServerSettings getSettings() {
        return TabbyChat.getInstance().serverSettings;
    }

    public class ChannelButton extends GuiButton {

        private Channel channel;

        ChannelButton(Channel channel) {
            super(channel.getName());
            this.channel = channel;
            setLocation(new Location(0, 0, 60, 15));
        }

        @Subscribe
        public void margeChangeTheChannel(ActionPerformedEvent event) {
            select(channel);
        }
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/TabbyAPI.java<|end_filename|>
package mnm.mods.tabbychat.api;

import com.google.common.eventbus.EventBus;

/**
 * Represents the main object for TabbyChat
 */
public abstract class TabbyAPI {

    private static TabbyAPI instance;

    protected TabbyAPI() {
        instance = this;
    }

    /**
     * Gets the API.
     *
     * @return The api
     */
    public static TabbyAPI getAPI() {
        return instance;
    }

    /**
     * Retrieve TabbyChat's version data. Version data contains the readable
     * version string and comparable double.
     *
     * @return The version data
     */
    public abstract VersionData getVersionData();

    /**
     * Gets the chat.
     *
     * @return The chat
     */
    public abstract Chat getChat();

    /**
     * Gets the common event bus used for chat messages. For gui related things,
     * use the Component's.
     *
     * @return The event bus
     */
    public abstract EventBus getBus();

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/extra/filters/UserFilter.java<|end_filename|>
package mnm.mods.tabbychat.extra.filters;

import mnm.mods.tabbychat.api.Channel;
import mnm.mods.tabbychat.api.TabbyAPI;
import mnm.mods.tabbychat.api.filters.Filter;
import mnm.mods.tabbychat.api.filters.FilterEvent;
import mnm.mods.tabbychat.api.filters.FilterSettings;
import net.minecraft.client.Minecraft;
import net.minecraft.client.audio.PositionedSoundRecord;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.SoundEvent;
import net.minecraft.util.text.ITextComponent;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.RegEx;

public class UserFilter implements Filter {

    private String name = "New Filter";
    private FilterSettings settings = new FilterSettings();
    private String pattern = ".*";

    private transient Pattern expression;

    public void setPattern(@RegEx String pattern) throws PatternSyntaxException {
        testPatternUnsafe(pattern);
        this.pattern = pattern;
        this.expression = null;
    }

    public void testPattern(String pattern) throws UserPatternException {
        try {
            testPatternUnsafe(pattern);
        } catch (PatternSyntaxException e) {
            throw new UserPatternException(e);
        }
    }

    private void testPatternUnsafe(String pattern) throws PatternSyntaxException {
        Pattern.compile(resolvePattern(pattern));
    }

    @Override
    public Pattern getPattern() {
        if (expression == null) {
            this.expression = Pattern.compile(resolvePattern(pattern), settings.getFlags());
        }
        return expression;
    }

    private String resolvePattern(String pattern) {
        String resolved = resolveVariables(pattern);
        if (!settings.isRegex()) {
            resolved = Pattern.quote(resolved);
        }
        return resolved;
    }

    public String getRawPattern() {
        return this.pattern;
    }

    @RegEx
    private static String resolveVariables(String pattern) {
        Matcher matcher = Pattern.compile("\\$\\{([\\w\\d]+)}").matcher(pattern);
        StringBuffer buffer = new StringBuffer();
        while (matcher.find()) {
            String key = matcher.group(1);
            String var = FilterAddon.getVariable(key);
            matcher.appendReplacement(buffer, var);
        }
        matcher.appendTail(buffer);

        return pattern;
    }

    @Override
    public void action(FilterEvent event) {

        if (settings.isRemove()) {
            // remove
            event.channels.clear();
        }
        // add channels
        for (String name : settings.getChannels()) {
            // replace group tokens in channel name
            Matcher matcher = Pattern.compile("\\$(\\d+)").matcher(name);
            while (matcher.find()) {
                // find groups
                int group = Integer.parseInt(matcher.group(1));
                if (group > 0 && event.matcher.groupCount() >= group) {
                    String groupText = event.matcher.group(group);
                    if (groupText != null) {
                        name = name.replace(matcher.group(), groupText);
                        continue;
                    }
                }
                name = null;
                break;
            }
            // skip this because there were missing or out of bounds groups.
            if (name == null)
                continue;

            // PMs start with a '@' character
            boolean pm = name.startsWith("@");
            // remove # or @ from the start of the channel name
            if (name.startsWith("#") || name.startsWith("@"))
                name = name.substring(1);

            Channel channel = TabbyAPI.getAPI().getChat().getChannel(name, pm);
            event.channels.add(channel);
        }
        // play sound
        if (settings.isSoundNotification()) {
            settings.getSoundName()
                    .map(ResourceLocation::new)
                    .map(SoundEvent.REGISTRY::getObject)
                    .map(sndEvent -> PositionedSoundRecord.getMasterRecord(sndEvent, 1.0F))
                    .ifPresent(Minecraft.getMinecraft().getSoundHandler()::playSound);

        }
    }

    @Override
    public String prepareText(ITextComponent string) {
        return settings.isRaw() ? string.getFormattedText() : Filter.super.prepareText(string);
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public FilterSettings getSettings() {
        return settings;
    }

    class UserPatternException extends Exception {
        private UserPatternException(PatternSyntaxException e) {
            super(e);
        }
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/gui/ChatTray.java<|end_filename|>
package mnm.mods.tabbychat.gui;

import com.google.common.collect.Maps;
import com.google.common.eventbus.Subscribe;
import mnm.mods.tabbychat.ChatChannel;
import mnm.mods.tabbychat.TabbyChat;
import mnm.mods.tabbychat.api.Channel;
import mnm.mods.tabbychat.api.ChannelStatus;
import mnm.mods.tabbychat.api.gui.IGui;
import mnm.mods.tabbychat.core.GuiNewChatTC;
import mnm.mods.util.Color;
import mnm.mods.util.TexturedModal;
import mnm.mods.util.config.Value;
import mnm.mods.util.gui.BorderLayout;
import mnm.mods.util.gui.FlowLayout;
import mnm.mods.util.gui.GuiComponent;
import mnm.mods.util.gui.GuiPanel;
import mnm.mods.util.gui.ILayout;
import mnm.mods.util.gui.events.ActionPerformedEvent;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.renderer.GlStateManager;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.util.Map;
import javax.annotation.Nonnull;

public class ChatTray extends GuiPanel implements IGui {

    private final static TexturedModal MODAL = new TexturedModal(ChatBox.GUI_LOCATION, 0, 14, 254, 202);

    private GuiPanel tabList = new GuiPanel(new FlowLayout());
    private GuiComponent handle = new ChatHandle();

    private Map<Channel, GuiComponent> map = Maps.newHashMap();


    ChatTray() {
        super(new BorderLayout());
        this.addComponent(tabList, BorderLayout.Position.CENTER);
        ChatPanel controls = new ChatPanel(new FlowLayout());
        controls.addComponent(new ToggleButton());
        controls.addComponent(handle);
        this.addComponent(controls, BorderLayout.Position.EAST);

    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        GlStateManager.color(1, 1, 1, mc.gameSettings.chatOpacity);
        if (GuiNewChatTC.getInstance().getChatOpen()) {
            drawModalCorners(MODAL);
        }
        super.drawComponent(mouseX, mouseY);
    }

    @Override
    public void updateComponent() {
        super.updateComponent();
        getParent()
                .map(GuiComponent::getSecondaryColorProperty)
                .map(color -> Color.of(color.getRed(), color.getGreen(), color.getBlue(), color.getAlpha() / 4 * 3))
                .ifPresent(this::setSecondaryColor);
    }

    public void addChannel(Channel channel) {
        GuiComponent gc = new ChatTab(channel);
        map.put(channel, gc);
        tabList.addComponent(gc);
    }

    public void removeChannel(final Channel channel) {
        GuiComponent gc = map.get(channel);
        this.tabList.removeComponent(gc);
        map.remove(channel);
    }

    public void clear() {
        this.tabList.clearComponents();

        addChannel(ChatChannel.DEFAULT_CHANNEL);
        ChatChannel.DEFAULT_CHANNEL.setStatus(ChannelStatus.ACTIVE);
    }

    @Nonnull
    @Override
    public Dimension getMinimumSize() {
        return tabList.getLayout()
                .map(ILayout::getLayoutSize)
                .orElseGet(super::getMinimumSize);
    }

    boolean isHandleHovered() {
        return handle.isHovered();
    }

    @Override
    public Rectangle getBounds() {
        return this.getLocation().asRectangle();
    }

    private class ToggleButton extends GuiComponent {

        private Value<Boolean> value;

        ToggleButton() {
            this.value = TabbyChat.getInstance().settings.advanced.keepChatOpen;
        }

        @Override
        public void drawComponent(int mouseX, int mouseY) {
            GlStateManager.enableBlend();
            int opac = (int)(mc.gameSettings.chatOpacity * 255) << 24;
            drawBorders(4, 4, 8, 8, 0x999999 | opac);
            if (value.get()) {
                Gui.drawRect(5, 5, 7, 7, 0xaaaaaa | opac);
            }
        }

        @Subscribe
        public void action(ActionPerformedEvent event) {
            value.set(!value.get());
        }

        @Override
        @Nonnull
        public Dimension getMinimumSize() {
            return new Dimension(8, 8);
        }
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/ChannelStatus.java<|end_filename|>
package mnm.mods.tabbychat.api;

/**
 * An enum for chanel statuses. A null value means the channel is currently
 * inactive. Only one status can be in effect at a time.
 * <p>
 * Setting a status with a lower priority than the current one will not take
 * effect. Priority is determined by the ordinal; a lower ordinal has a higher
 * priority. e.g. {@link #ACTIVE} has the highest priority while {@link #JOINED}
 * has the lowest.
 * <p>
 * {@code null} is special and has both highest and lowest priorities. The
 * status will always be set when the old value or new value is null.
 */
public enum ChannelStatus {

    /**
     * For the active channel.
     */
    ACTIVE,
    /**
     * For when your name is mentioned.
     */
    PINGED,
    /**
     * For unread messages.
     */
    UNREAD,
    /**
     * For when players join or leave the channel.
     */
    JOINED
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/gui/TextBox.java<|end_filename|>
package mnm.mods.tabbychat.gui;

import com.google.common.eventbus.Subscribe;
import mnm.mods.tabbychat.ChatManager;
import mnm.mods.tabbychat.TabbyChat;
import mnm.mods.tabbychat.api.gui.ChatInput;
import mnm.mods.tabbychat.core.GuiNewChatTC;
import mnm.mods.tabbychat.extra.spell.Spellcheck;
import mnm.mods.tabbychat.extra.spell.SpellingFormatter;
import mnm.mods.util.Color;
import mnm.mods.util.TexturedModal;
import mnm.mods.util.gui.GuiComponent;
import mnm.mods.util.gui.GuiText;
import mnm.mods.util.gui.events.GuiMouseEvent;
import mnm.mods.util.text.FancyFontRenderer;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiTextField;
import net.minecraft.client.renderer.BufferBuilder;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.client.renderer.vertex.DefaultVertexFormats;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextComponentString;
import org.lwjgl.opengl.GL11;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.util.List;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;

public class TextBox extends GuiComponent implements ChatInput {

    private static final TexturedModal MODAL = new TexturedModal(ChatBox.GUI_LOCATION, 0, 219, 254, 37);

    private FontRenderer fr = mc.fontRenderer;
    // Dummy textField
    private GuiText textField = new GuiText(new GuiTextField(0, fr, 0, 0, 0, 0) {
        @Override
        public void drawTextBox() {
            // noop
        }
    });
    private int cursorCounter;
    private Spellcheck spellcheck;

    TextBox() {
        textField.getTextField().setMaxStringLength(ChatManager.MAX_CHAT_LENGTH);
        textField.setFocused(true);
        textField.getTextField().setCanLoseFocus(false);

        spellcheck = TabbyChat.getInstance().getSpellcheck();
    }

    @Override
    public void onClosed() {
        this.textField.setValue("");
        super.onClosed();
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        GlStateManager.enableBlend();
        drawModalCorners(MODAL);
        GlStateManager.disableBlend();

        drawText();
        drawCursor();

    }

    private void drawCursor() {
        GuiTextField textField = this.textField.getTextField();

        // keeps track of all the characters. Used to compensate for spaces
        int totalPos = 0;

        // The current pixel row. adds FONT_HEIGHT each iteration
        int line = 0;

        // The position of the cursor
        int pos = textField.getCursorPosition();
        // the position of the selection
        int sel = textField.getSelectionEnd();

        // make the position and selection in order
        int start = Math.min(pos, sel);
        int end = Math.max(pos, sel);

        for (String text : getWrappedLines()) {

            // cursor drawing
            if (pos >= 0 && pos <= text.length()) {
                // cursor is on this line
                int c = fr.getStringWidth(text.substring(0, pos));
                boolean cursorBlink = this.cursorCounter / 6 % 3 != 0;
                if (cursorBlink) {
                    if (textField.getCursorPosition() < this.textField.getValue().length()) {
                        drawVerticalLine(c + 3, line - 2, line + fr.FONT_HEIGHT + 1, 0xffd0d0d0);
                    } else {
                        fr.drawString("_", c + 2, line + 1, getPrimaryColorProperty().getHex());
                    }

                }
            }

            // selection highlighting

            // the start of the highlight.
            int x = -1;
            // the end of the highlight.
            int w = -1;

            // test the start
            if (start >= 0 && start <= text.length()) {
                x = fr.getStringWidth(text.substring(0, start));
            }

            // test the end
            if (end >= 0 && end <= text.length()) {
                w = fr.getStringWidth(text.substring(start < 0 ? 0 : start, end)) + 2;
            }

            final int LINE_Y = line + fr.FONT_HEIGHT + 2;

            if (w != 0) {
                if (x >= 0 && w > 0) {
                    // start and end on same line
                    drawSelectionBox(x + 2, line, x + w, LINE_Y);
                } else {
                    if (x >= 0) {
                        // started on this line
                        drawSelectionBox(x + 2, line, x + fr.getStringWidth(text.substring(start)) + 1, LINE_Y);
                    }
                    if (w >= 0) {
                        // ends on this line
                        drawSelectionBox(2, line, w, LINE_Y);
                    }
                    if (start < 0 && end > text.length()) {
                        // full line
                        drawSelectionBox(1, line, fr.getStringWidth(text), LINE_Y);
                    }
                }
            }

            // keep track of the lines
            totalPos += text.length();
            boolean space = getText().length() > totalPos && getText().charAt(totalPos) == ' ';

            // prepare all the markers for the next line.
            pos -= text.length();
            start -= text.length();
            end -= text.length();

            if (space) {
                // compensate for spaces
                pos--;
                start--;
                end--;
                totalPos++;
            }
            line = LINE_Y;
        }

    }

    private void drawText() {
        FancyFontRenderer ffr = new FancyFontRenderer(fr);
        int yPos = 1;
        List<ITextComponent> lines = getFormattedLines();
        for (ITextComponent line : lines) {
            Color color = Color.WHITE;
            ffr.drawChat(line, 3, yPos, color.getHex(), false);
            yPos += fr.FONT_HEIGHT + 2;
        }

    }

    /**
     * Draws the blue selection box. Adapted from {@link GuiTextField#drawSelectionBox(int, int, int, int)}
     */
    private void drawSelectionBox(int x1, int y1, int x2, int y2) {
        if (x1 < x2) {
            int i = x1;
            x1 = x2;
            x2 = i;
        }

        if (y1 < y2) {
            int j = y1;
            y1 = y2;
            y2 = j;
        }

        x2 = Math.min(x2, this.getLocation().getXWidth());
        x1 = Math.min(x1, this.getLocation().getXWidth());

        Tessellator tessellator = Tessellator.getInstance();
        BufferBuilder bufferbuilder = tessellator.getBuffer();
        GlStateManager.color(0.0F, 0.0F, 255.0F, 255.0F);
        GlStateManager.disableTexture2D();
        GlStateManager.enableColorLogic();
        GlStateManager.colorLogicOp(GlStateManager.LogicOp.OR_REVERSE);
        bufferbuilder.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION);
        bufferbuilder.pos(x1, y2, 0.0D).endVertex();
        bufferbuilder.pos(x2, y2, 0.0D).endVertex();
        bufferbuilder.pos(x2, y1, 0.0D).endVertex();
        bufferbuilder.pos(x1, y1, 0.0D).endVertex();
        tessellator.draw();
        GlStateManager.disableColorLogic();
        GlStateManager.enableTexture2D();
    }


    @Override
    public void updateComponent() {
        this.cursorCounter++;
    }

    @Override
    public List<String> getWrappedLines() {
        return fr.listFormattedStringToWidth(textField.getValue(), getBounds().width);
    }

    private List<ITextComponent> getFormattedLines() {
        List<String> lines = getWrappedLines();
        if (TabbyChat.getInstance().settings.advanced.spelling.get()) {
            spellcheck.checkSpelling(textField.getValue());
            return lines.stream()
                    .map(new SpellingFormatter(spellcheck))
                    .collect(Collectors.toList());
        }
        return lines.stream()
                .map(TextComponentString::new)
                .collect(Collectors.toList());
    }

    @Override
    @Nonnull
    public Dimension getMinimumSize() {
        return new Dimension(100, (fr.FONT_HEIGHT + 2) * getWrappedLines().size());
    }

    public GuiText getTextField() {
        return textField;
    }

    @Override
    public String getText() {
        return textField.getValue();
    }

    @Override
    public void setText(String text) {
        textField.setValue(text);
    }

    @Subscribe
    public void onMouseClick(GuiMouseEvent event) {
        mouseClicked(event.getMouseX(), event.getMouseY(), event.getButton());
    }


    private void mouseClicked(int x, int y, int mouseButton) {
        if (mouseButton == 0) {
            Rectangle bounds = this.getBounds();

            int width = bounds.width - 1;
            int row = y / (fr.FONT_HEIGHT + 2);

            List<String> lines = getWrappedLines();
            if (row < 0 || row >= lines.size() || x < 0 || x > width) {
                return;
            }
            int index = 0;
            for (int i = 0; i < row; i++) {
                index += lines.get(i).length();
                // check for spaces because trailing spaces are trimmed
                if (getText().charAt(index)==' ') {
                    index++;
                }
            }
            index += fr.trimStringToWidth(lines.get(row), x - 3).length();
            textField.getTextField().setCursorPosition(index);
        }
    }

    @Override
    public Rectangle getBounds() {
        return this.getLocation().asRectangle();
    }

    @Override
    public boolean isVisible() {
        return super.isVisible() && GuiNewChatTC.getInstance().getChatOpen();
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/util/TimeStamps.java<|end_filename|>
package mnm.mods.tabbychat.util;

import mnm.mods.util.Translatable;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public enum TimeStamps implements Translatable {

    MILITARY("[HHmm]", "[2359]"),
    MILITARYWITHCOLON("[HH:mm]", "[23:59]"),
    STANDARD("[hh:mm]", "[12:00]"),
    STANDARDWITHMARKER("[hh:mma]", "[12:00PM]"),
    MILITARYSECONDS("[HH:mm:ss]", "[23:59:01]"),
    STANDARDSECONDS("[hh:mm:ss]", "[12:00:01]"),
    STANDARDSECONDSMARKER("[hh:mm:ssa]", "[12:00:01PM]");

    private String code;
    private String maxTime;
    private DateFormat format;

    TimeStamps(String _code, String _maxTime) {
        this.code = _code;
        this.maxTime = _maxTime;
        format = new SimpleDateFormat(code);
    }

    public String format(Date date) {
        return format.format(date);
    }

    @Override
    public String getUnlocalized() {
        return this.maxTime;
    }

    @Override
    public String translate(Object... params) {
        return this.maxTime;
    }

    public String toCode() {
        return this.code;
    }

    @Override
    public String toString() {
        return translate();
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/extra/ChatAddonAntiSpam.java<|end_filename|>
package mnm.mods.tabbychat.extra;

import com.google.common.collect.Maps;
import com.google.common.eventbus.Subscribe;
import mnm.mods.tabbychat.TabbyChat;
import mnm.mods.tabbychat.api.Channel;
import mnm.mods.tabbychat.api.events.MessageAddedToChannelEvent;
import org.apache.commons.lang3.StringUtils;

import java.util.Map;

public class ChatAddonAntiSpam {

    private Map<Channel, Counter> messageMap = Maps.newHashMap();

    @Subscribe
    public void onMessageAdded(MessageAddedToChannelEvent event) {

        boolean enabled = TabbyChat.getInstance().settings.general.antiSpam.get();
        double prejudice = TabbyChat.getInstance().settings.general.antiSpamPrejudice.get();

        if (enabled && event.id == 0) {
            Channel channel = event.channel;
            Counter counter = this.messageMap.computeIfAbsent(channel, k -> new Counter());
            String chat = event.text.getUnformattedText();

            if (getDifference(chat, counter.lastMessage) <= prejudice) {
                counter.spamCounter++;
                event.text.appendText(" [" + counter.spamCounter + "x]");
                channel.removeMessageAt(0);
            } else {
                counter.lastMessage = chat;
                counter.spamCounter = 1;
            }
        }
    }

    private class Counter {
        private String lastMessage = "";
        private int spamCounter = 1;
    }

    private static double getDifference(String s1, String s2) {
        double avgLen = (s1.length() + s2.length()) / 2D;
        if (avgLen == 0) {
            return 0;
        }
        return StringUtils.getLevenshteinDistance(s1.toLowerCase(), s2.toLowerCase()) / avgLen;
    }
}


<|start_filename|>src/main/java/mnm/mods/util/Color.java<|end_filename|>
package mnm.mods.util;

import com.google.gson.annotations.SerializedName;

import java.util.Random;

/**
 * Represents a color and provides an easy way to convert to and from html color
 * codes. A few default colors have been provided.
 */
public class Color {

    public static final Color BLACK = of(0xff000000);
    public static final Color DARK_BLUE = of(0xff0000aa);
    public static final Color DARK_GREEN = of(0xff00aa00);
    public static final Color DARK_AQUA = of(0xff00aaaa);
    public static final Color DARK_RED = of(0xffaa0000);
    public static final Color DARK_PURPLE = of(0xffaa00aa);
    public static final Color GOLD = of(0xffffaa00);
    public static final Color GRAY = of(0xffaaaaaa);
    public static final Color DARK_GRAY = of(0xff555555);
    public static final Color BLUE = of(0xff5555ff);
    public static final Color GREEN = of(0xff55ff55);
    public static final Color AQUA = of(0xff55ffff);
    public static final Color RED = of(0xffff5555);
    public static final Color LIGHT_PURPLE = of(0xffff55ff);
    public static final Color YELLOW = of(0xffffff55);
    public static final Color WHITE = of(0xffffffff);

    private static Random random = new Random();

    @SerializedName("r")
    private final int red;
    @SerializedName("g")
    private final int green;
    @SerializedName("b")
    private final int blue;
    @SerializedName("a")
    private final int alpha;

    /**
     * Creates a color using an html color code.
     *
     * @param hexColor The html color code
     */
    private Color(int hexColor) {
        this.alpha = hexColor >> 24 & 255;
        this.red = hexColor >> 16 & 255;
        this.green = hexColor >> 8 & 255;
        this.blue = hexColor & 255;
    }

    /**
     * Creates a color using RGBA color format.
     *
     * @param red Red color
     * @param green Green color
     * @param blue Blue color
     * @param alpha Transparency
     */
    private Color(int red, int green, int blue, int alpha) {
        this.red = red % 256;
        this.green = green % 256;
        this.blue = blue % 256;
        this.alpha = alpha % 256;
    }

    /**
     * Gets the html hex color code of this color usable by Minecraft's
     * {@link net.minecraft.client.gui.Gui}.
     *
     * @return The html hex code.
     */
    public int getHex() {
        int alphaI = alpha << 24;
        int redI = red << 16;
        int greenI = green << 8;
        return alphaI + redI + greenI + blue;
    }

    /**
     * Gets the red value.
     *
     * @return The red value
     */
    public int getRed() {
        return red;
    }

    /**
     * Gets the green value.
     *
     * @return The green value
     */
    public int getGreen() {
        return green;
    }

    /**
     * Gets the blue value.
     *
     * @return The blue value
     */
    public int getBlue() {
        return blue;
    }

    /**
     * Gets the alpha value.
     *
     * @return The alpha value
     */
    public int getAlpha() {
        return alpha;
    }

    /**
     * Convenience method for getting the html code for a RGBA color. Creates a
     * new object and calls {@link Color#getHex()}.
     *
     * @param red The red value
     * @param green The green value
     * @param blue The blue value
     * @param alpha The alpha value
     * @return The hexadecimal representation of this color.
     */
    public static int getColor(int red, int green, int blue, int alpha) {
        return of(red, green, blue, alpha).getHex();
    }

    /**
     * Generates a random color.
     *
     * @return A random color
     */
    public static Color random() {
        return of(random.nextInt());
    }

    public static Color of(int hexColor) {
        return new Color(hexColor);
    }

    public static Color of(int red, int green, int blue, int alpha) {
        return new Color(red, green, blue, alpha);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + alpha;
        result = prime * result + blue;
        result = prime * result + green;
        result = prime * result + red;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || !(obj instanceof Color))
            return false;
        Color other = (Color) obj;
        return alpha == other.alpha
                && blue == other.blue
                && green == other.green
                && red == other.red;
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/settings/GeneralServerSettings.java<|end_filename|>
package mnm.mods.tabbychat.settings;

import mnm.mods.tabbychat.util.ChannelPatterns;
import mnm.mods.tabbychat.util.MessagePatterns;
import mnm.mods.util.config.Value;
import mnm.mods.util.config.ValueList;
import mnm.mods.util.config.ValueObject;

public class GeneralServerSettings extends ValueObject {

    public Value<Boolean> channelsEnabled = value(true);
    public Value<Boolean> pmEnabled = value(true);
    public Value<ChannelPatterns> channelPattern = value(ChannelPatterns.BRACKETS);
    public Value<MessagePatterns> messegePattern = value(MessagePatterns.WHISPERS);
    public Value<Boolean> useDefaultTab = value(true);
    public ValueList<String> ignoredChannels = list();
    public Value<String> defaultChannel = value("");
    public Value<String> channelCommand = value("");
    public Value<String> messageCommand = value("");
}


<|start_filename|>src/main/java/mnm/mods/util/gui/config/GuiSettingNumber.java<|end_filename|>
package mnm.mods.util.gui.config;

import mnm.mods.util.config.Value;
import mnm.mods.util.gui.GuiNumericUpDown;
import mnm.mods.util.gui.config.GuiSetting.GuiSettingWrapped;

/**
 * A base gui setting for numbers. It wraps a {@link GuiNumericUpDown}
 *
 * @author Matthew
 * @param <T>
 * @see GuiSettingDouble
 * @see GuiSettingInt
 */
public abstract class GuiSettingNumber<T extends Number> extends GuiSettingWrapped<T, GuiNumericUpDown<T>> {

    private GuiSettingNumber(Value<T> setting, GuiNumericUpDown<T> input) {
        super(setting, input);
    }

    /**
     * Gui setting for integers
     *
     * @author Matthew
     */
    public static class GuiSettingInt extends GuiSettingNumber<Integer> {

        public GuiSettingInt(Value<Integer> setting) {
            super(setting, new GuiNumericUpDown.IntUpDown());
        }
    }

    /**
     * Gui setting for doubles
     *
     * @author Matthew
     */
    public static class GuiSettingDouble extends GuiSettingNumber<Double> {

        public GuiSettingDouble(Value<Double> setting) {
            super(setting, new GuiNumericUpDown.DoubleUpDown());
        }
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/core/mixin/MixinChatTabCompleter.java<|end_filename|>
package mnm.mods.tabbychat.core.mixin;

import mnm.mods.tabbychat.util.Translation;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiChat.ChatTabCompleter;
import net.minecraft.client.gui.GuiTextField;
import net.minecraft.util.TabCompleter;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextComponentTranslation;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

@Mixin(ChatTabCompleter.class)
public abstract class MixinChatTabCompleter extends TabCompleter {

    @Shadow
    @Final
    private Minecraft client;

    public MixinChatTabCompleter(GuiTextField textFieldIn) {
        super(textFieldIn, false);
    }

    @Inject(
            method = "complete()V",
            at = @At("HEAD"),
            cancellable = true)
    private void onComplete(CallbackInfo ci) {
        if (this.didComplete) {

            if (this.completions.size() > 20 && this.requestedCompletions) {
                ci.cancel();

                ITextComponent chat = new TextComponentTranslation(Translation.WARN_COMPLETIONS, this.completions.size());
                client.ingameGUI.getChatGUI().printChatMessageWithOptionalDeletion(chat, 1);
            }
        }
    }

}


<|start_filename|>src/main/java/mnm/mods/util/text/FancyFontRenderer.java<|end_filename|>
package mnm.mods.util.text;

import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.Gui;
import net.minecraft.util.text.ITextComponent;

public class FancyFontRenderer extends Gui {

    private final FontRenderer fontRenderer;

    public FancyFontRenderer(FontRenderer fr) {
        this.fontRenderer = fr;
    }

    public void drawChat(ITextComponent chat, int x, int y) {
        this.drawChat(chat, x, y, true);
    }

    public void drawChat(ITextComponent chat, int x, int y, boolean shadow) {
        drawChat(chat, x, y, -1, shadow);
    }

    public void drawChat(ITextComponent chat, int x, int y, int color) {
        this.drawChat(chat, x, y, color, true);
    }

    public void drawChat(ITextComponent chat, int x, int y, int color, boolean shadow) {

        int x1 = x;
        for (ITextComponent c : chat) {
            if (c instanceof FancyTextComponent) {
                FancyTextComponent fcc = (FancyTextComponent) c;
                for (String s : c.getUnformattedText().split("\r?\n")) {
                    int length = fontRenderer.getStringWidth(s);
                    drawRect(x1, y, x1 + length, y - fontRenderer.FONT_HEIGHT, fcc.getFancyStyle().getHighlight().getHex());
                    drawHorizontalLine(x1, x1 + length, y + fontRenderer.FONT_HEIGHT - 1, fcc.getFancyStyle().getUnderline().getHex());
                }
            }
            x1 += fontRenderer.getStringWidth(c.getUnformattedComponentText());
        }
        for (String s : chat.getUnformattedText().split("\r?\n")) {
            fontRenderer.drawString(s, x, y, color, shadow);
            y += fontRenderer.FONT_HEIGHT;
        }
    }

}


<|start_filename|>src/main/java/mnm/mods/util/text/ITextBuilder.java<|end_filename|>
package mnm.mods.util.text;

import mnm.mods.util.Color;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextFormatting;
import net.minecraft.util.text.event.ClickEvent;
import net.minecraft.util.text.event.HoverEvent;

import javax.annotation.Nullable;

public interface ITextBuilder {

    /**
     * Sets the formatting in the style of the current chat.
     *
     * @param f The format
     * @return This builder
     */
    ITextBuilder format(TextFormatting f);

    ITextBuilder color(Color color);

    ITextBuilder underline(Color color);

    ITextBuilder highlight(Color color);

    ITextBuilder click(ClickEvent event);

    ITextBuilder hover(HoverEvent event);

    ITextBuilder insertion(String insertion);

    ITextBuilder score(String player, String objective);

    ITextBuilder text(String text);

    ITextBuilder selector(Selector selector);

    /**
     * Starts the creation of a translation. After calling, any call to
     * {@link #append(ITextComponent)} won't be immediately appended to the
     * component. Instead, it will be added to a list of arguments for the
     * translation. To end the translation and allow it to be appended to the
     * chat, call {@link #end()}.
     *
     * @param key The translation key
     * @return A translation builder
     */
    ITextBuilder translation(String key);

    /**
     * Quickly translates the given key with zero arguments or formatting.
     * Immidiently ends the translation. Same as calling
     * {@code translation(key).end()}
     *
     * @param key
     * @return
     */
    ITextBuilder quickTranslate(String key);

    /**
     * Used for builders that build multiple chats.
     *
     * @return This builder
     */
    ITextBuilder next();

    /**
     * Ends a translation so it can be appended to the chat.
     *
     * @return
     */
    ITextBuilder end();

    /**
     * Appends the current chat to the chat and makes the provided value the
     * current.
     *
     * @param chat The new current value
     * @return
     */
    ITextBuilder append(@Nullable ITextComponent chat);

    /**
     * Appends the current chat (if any) to the chat and returns the built chat.
     *
     * @return The chat
     */
    ITextComponent build();

}


<|start_filename|>src/main/java/mnm/mods/util/gui/config/SettingPanel.java<|end_filename|>
package mnm.mods.util.gui.config;

import mnm.mods.util.config.SettingsFile;
import mnm.mods.util.gui.GuiPanel;

/**
 * Base class for a setting panel.
 */
public abstract class SettingPanel<T extends SettingsFile> extends GuiPanel {

    private String displayString;

    /**
     * Called when this category is activated and displayed.
     */
    public void initGUI() {}

    /**
     * Sets the display string for this category.
     *
     * @param string The display string
     */
    public void setDisplayString(String string) {
        this.displayString = string;
    }

    /**
     * Gets the display string for this category. It is shown with the button
     * used to activate it.
     *
     * @return The display string
     */
    public String getDisplayString() {
        return this.displayString;
    }

    /**
     * Gets the {@link SettingsFile} used for this category. Used for loading
     * and saving the settings file.
     *
     * @return The settings
     */
    public abstract T getSettings();


}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/Message.java<|end_filename|>
package mnm.mods.tabbychat.api;

import java.util.Date;

import net.minecraft.util.text.ITextComponent;

/**
 * Represents a message.
 */
public interface Message {

    /**
     * Gets the message
     *
     * @return The message
     */
    ITextComponent getMessage();

    /**
     * Gets the message with a time stamp if they are enabled.
     *
     * @return The time-stamped message
     */
    ITextComponent getMessageWithOptionalTimestamp();

    /**
     * Gets the update counter used for this message.
     *
     * @return The counter
     */
    int getCounter();

    /**
     * Gets the ID of this message. 0 is a normal message.
     *
     * @return The ID of this message
     */
    int getID();

    /**
     * Gets the date that this message was sent.
     *
     * @return The date
     */
    Date getDate();


}


<|start_filename|>src/main/java/mnm/mods/tabbychat/util/ScaledDimension.java<|end_filename|>
package mnm.mods.tabbychat.util;

import net.minecraft.client.Minecraft;
import net.minecraft.util.math.MathHelper;

/**
 * Modified version of ScaledResolution. Supports arbitrary dimensions
 * 
 * @author Matthew
 */
public class ScaledDimension {

    private final double scaledWidthD;
    private final double scaledHeightD;
    private int scaledWidth;
    private int scaledHeight;
    private int scaleFactor;

    public ScaledDimension(int width, int height) {
        this.scaledWidth = width;
        this.scaledHeight = height;
        this.scaleFactor = 1;
        Minecraft mc = Minecraft.getMinecraft();
        boolean unicode = mc.isUnicode();
        int scale = mc.gameSettings.guiScale;

        if (scale == 0) {
            scale = 1000;
        }

        while (this.scaleFactor < scale && this.scaledWidth / (this.scaleFactor + 1) >= 320 && this.scaledHeight / (this.scaleFactor + 1) >= 240) {
            ++this.scaleFactor;
        }

        if (unicode && this.scaleFactor % 2 != 0 && this.scaleFactor != 1) {
            --this.scaleFactor;
        }

        this.scaledWidthD = (double) this.scaledWidth / (double) this.scaleFactor;
        this.scaledHeightD = (double) this.scaledHeight / (double) this.scaleFactor;
        this.scaledWidth = MathHelper.ceil(this.scaledWidthD);
        this.scaledHeight = MathHelper.ceil(this.scaledHeightD);
    }

    public int getScaledWidth() {
        return this.scaledWidth;
    }

    public int getScaledHeight() {
        return this.scaledHeight;
    }

    public double getScaledWidth_double() {
        return this.scaledWidthD;
    }

    public double getScaledHeight_double() {
        return this.scaledHeightD;
    }

    public int getScaleFactor() {
        return this.scaleFactor;
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/filters/FilterSettings.java<|end_filename|>
package mnm.mods.tabbychat.api.filters;

import com.google.common.base.Strings;

import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/**
 * Defines the settings used by filters.
 */
public class FilterSettings {

    // destinations
    private final Set<String> channels = new HashSet<>();
    private boolean remove;
    private boolean raw = true;
    private boolean regex;
    private int flags;

    // notifications
    private boolean soundNotification = false;
    private String soundName = "";

    public Set<String> getChannels() {
        return channels;
    }

    public boolean isRemove() {
        return remove;
    }

    public void setRemove(boolean value) {
        this.remove = value;
    }

    public boolean isRaw() {
        return raw;
    }

    public void setRaw(boolean raw) {
        this.raw = raw;
    }

    public boolean isRegex() {
        return regex;
    }

    public void setRegex(boolean regex) {
        this.regex = regex;
    }

    public boolean isCaseInsensitive() {
        return getFlag(Pattern.CASE_INSENSITIVE);
    }

    public void setCaseInsensitive(boolean value) {
        setFlag(Pattern.CASE_INSENSITIVE, value);
    }

    private void setFlag(int flag, boolean value) {
        if (value) {
            this.flags |= flag;
        } else {
            this.flags &= ~flag;
        }
    }

    private boolean getFlag(int flag) {
        return (flags & flag) != 0;
    }

    public int getFlags() {
        return flags;
    }

    public boolean isSoundNotification() {
        return soundNotification;
    }

    public void setSoundNotification(boolean sound) {
        this.soundNotification = sound;
    }

    public Optional<String> getSoundName() {
        return Optional.ofNullable(soundName);
    }

    public void setSoundName(@Nullable String soundName) {
        this.soundName = Strings.isNullOrEmpty(soundName) ? null : soundName;
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/extra/filters/GuiFilterEditor.java<|end_filename|>
package mnm.mods.tabbychat.extra.filters;

import static mnm.mods.tabbychat.util.Translation.*;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.eventbus.Subscribe;
import mnm.mods.tabbychat.api.filters.FilterSettings;
import mnm.mods.util.Color;
import mnm.mods.util.gui.GuiButton;
import mnm.mods.util.gui.GuiCheckbox;
import mnm.mods.util.gui.GuiGridLayout;
import mnm.mods.util.gui.GuiLabel;
import mnm.mods.util.gui.GuiPanel;
import mnm.mods.util.gui.GuiText;
import mnm.mods.util.gui.events.ActionPerformedEvent;
import mnm.mods.util.gui.events.GuiKeyboardEvent;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.SoundEvent;
import net.minecraft.util.text.TextComponentString;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.util.text.TextFormatting;
import org.lwjgl.input.Keyboard;

import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;

public class GuiFilterEditor extends GuiPanel {

    private class ToggleButton extends GuiButton {
        private boolean active;

        private ToggleButton(String text) {
            super(text);
        }

        @Override
        public String getText() {
            String text = super.getText();
            TextFormatting color = active ? TextFormatting.GREEN : TextFormatting.RED;
            return color + text;
        }

        @Subscribe
        public void actionPerformed(ActionPerformedEvent event) {
            active ^= true;
        }
    }

    private UserFilter filter;
    private Consumer<UserFilter> consumer;

    private GuiText txtName;
    private GuiCheckbox chkRemove;
    private GuiText txtDestinations;
    private GuiCheckbox chkSound;
    private GuiText txtSound;
    private GuiText txtPattern;
    private GuiLabel lblError;

    private ToggleButton btnRegexp;
    private ToggleButton btnIgnoreCase;
    private ToggleButton btnRaw;

    public GuiFilterEditor(UserFilter filter, Consumer<UserFilter> consumer) {
        this.setLayout(new GuiGridLayout(20, 15));
        this.filter = filter;
        this.consumer = consumer;

        String pattern = filter.getRawPattern();
        FilterSettings settings = filter.getSettings();

        int pos = 0;

        this.addComponent(new GuiLabel(new TextComponentTranslation(FILTER_TITLE)), new int[]{8, pos, 1, 2});

        pos += 2;
        this.addComponent(new GuiLabel(new TextComponentTranslation(FILTER_NAME)), new int[]{1, pos});
        this.addComponent(txtName = new GuiText(), new int[]{5, pos, 10, 1});
        txtName.setValue(filter.getName());

        pos += 2;
        this.addComponent(new GuiLabel(new TextComponentTranslation(FILTER_DESTINATIONS)), new int[]{1, pos});
        this.addComponent(txtDestinations = new GuiText(), new int[]{8, pos, 10, 1});
        txtDestinations.setValue(Joiner.on(", ").join(settings.getChannels()));
        txtDestinations.setCaption(new TextComponentTranslation(FILTER_DESTIONATIONS_DESC));

        pos += 1;
        this.addComponent(btnRegexp = new ToggleButton(".*"), new int[]{1, pos, 2, 1});
        btnRegexp.active = filter.getSettings().isRegex();
        btnRegexp.setCaption(new TextComponentTranslation(FILTER_REGEX));
        this.addComponent(btnIgnoreCase = new ToggleButton("Aa"), new int[]{3, pos, 2, 1});
        btnIgnoreCase.active = settings.isCaseInsensitive();
        btnIgnoreCase.setCaption(new TextComponentTranslation(FILTER_IGNORE_CASE));
        this.addComponent(btnRaw = new ToggleButton("&0"), new int[]{5, pos, 2, 1});
        btnRaw.active = settings.isRaw();
        btnRaw.setCaption(new TextComponentTranslation(FILTER_RAW_INPUT));

        pos += 2;
        this.addComponent(new GuiLabel(new TextComponentTranslation(FILTER_HIDE)), new int[]{2, pos});
        this.addComponent(chkRemove = new GuiCheckbox(), new int[]{1, pos});
        chkRemove.setValue(settings.isRemove());

        pos += 1;
        this.addComponent(new GuiLabel(new TextComponentTranslation(FILTER_AUDIO_NOTIFY)), new int[]{2, pos});
        this.addComponent(chkSound = new GuiCheckbox(), new int[]{1, pos});
        chkSound.setValue(settings.isSoundNotification());

        pos += 1;
        this.addComponent(txtSound = new GuiText(), new int[]{3, pos, 14, 1});
        txtSound.setValue(settings.getSoundName().orElse(""));
        txtSound.getBus().register(new Object() {
            private int pos;

            @Subscribe
            public void suggestSounds(GuiKeyboardEvent event) {
                final int max = 10;
                if (Keyboard.isKeyDown(Keyboard.KEY_DOWN)) {
                    pos++;
                } else if (Keyboard.isKeyDown(Keyboard.KEY_UP)) {
                    pos--;
                }

                // suggest sounds
                final String val = txtSound.getValue().toLowerCase()
                        .substring(0, txtSound.getTextField().getCursorPosition());
                List<String> list = SoundEvent.REGISTRY.getKeys().stream()
                        .map(Object::toString)
                        .filter(s -> s.contains(val))
                        .collect(Collectors.toList());

                pos = Math.min(pos, list.size() - max);
                pos = Math.max(pos, 0);
                if (list.size() > max) {
                    list = list.subList(pos, pos + max);
                }
                txtSound.setHint(Joiner.on('\n').join(list));
                if ((Keyboard.isKeyDown(Keyboard.KEY_RETURN) || Keyboard.isKeyDown(Keyboard.KEY_NUMPADENTER)) && !list.isEmpty()) {
                    txtSound.setValue(list.get(0));
                    txtSound.setFocused(false);
                }
            }
        });

        GuiButton play = new GuiButton("\u25b6") {
            @Override
            public SoundEvent getSound() {
                return SoundEvent.REGISTRY.getObject(new ResourceLocation(txtSound.getValue()));
            }
        };
        this.addComponent(play, new int[]{18, pos, 2, 1});

        pos += 2;
        this.addComponent(new GuiLabel(new TextComponentTranslation(FILTER_EXPRESSION)), new int[]{1, pos});
        this.addComponent(txtPattern = new GuiText(), new int[]{8, pos, 12, 1});

        txtPattern.setValue(pattern == null ? "" : pattern);

        pos++;
        this.addComponent(lblError = new GuiLabel(), new int[]{4, pos});

        GuiButton accept = new GuiButton(I18n.format("gui.done"));
        accept.getBus().register(new Object() {
            @Subscribe
            public void finish(ActionPerformedEvent event) {
                accept();
            }
        });
        this.addComponent(accept, new int[]{5, 14, 4, 1});

        GuiButton cancel = new GuiButton(I18n.format("gui.cancel"));
        cancel.getBus().register(new Object() {
            @Subscribe
            public void stopEverything(ActionPerformedEvent event) {
                cancel();
            }
        });
        this.addComponent(cancel, new int[]{1, 14, 4, 1});
    }

    private void accept() {
        filter.setName(txtName.getValue());
        filter.setPattern(txtPattern.getValue());
        FilterSettings sett = filter.getSettings();
        sett.getChannels().clear();
        sett.getChannels().addAll(Splitter.on(",")
                .omitEmptyStrings()
                .trimResults()
                .splitToList(txtDestinations.getValue()));
        sett.setRemove(chkRemove.getValue());
        sett.setCaseInsensitive(btnIgnoreCase.active);
        sett.setRegex(btnRegexp.active);
        sett.setRaw(btnRaw.active);

        sett.setSoundNotification(chkSound.getValue());
        sett.setSoundName(txtSound.getValue());

        consumer.accept(filter);
        cancel();
    }

    private void cancel() {
        getParent().ifPresent(p -> p.setOverlay(null));
    }

    @Subscribe
    public void updateError(GuiKeyboardEvent event) {
        txtPattern.setPrimaryColor(Color.WHITE);
        lblError.setText(null);
        if (txtPattern.isFocused() && btnRegexp.active) {
            // check valid regex
            try {
                filter.testPattern(txtPattern.getValue());
            } catch (UserFilter.UserPatternException e) {
                txtPattern.setPrimaryColor(Color.RED);
                String string = e.getCause().getLocalizedMessage();
                lblError.setText(new TextComponentString(string));
            }
        }
    }
}


<|start_filename|>src/main/java/mnm/mods/util/ILocation.java<|end_filename|>
package mnm.mods.util;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;

public interface ILocation {

    int getXPos();

    int getYPos();

    int getWidth();

    int getHeight();

    ILocation asImmutable();

    default int getXWidth() {
        return getXPos() + getWidth();
    }

    default int getYHeight() {
        return getYPos() + getHeight();
    }

    default Location copy() {
        return Location.copyOf(this);
    }

    default Point getPoint() {
        return new Point(getXPos(), getYPos());
    }

    default Dimension getSize() {
        return new Dimension(this.getWidth(), this.getHeight());
    }

    default Rectangle asRectangle() {
        return new Rectangle(getXPos(), getYPos(), getWidth(), getHeight());
    }

    default boolean contains(ILocation r) {

        return this.getXPos() < r.getXWidth()
                && this.getXWidth() > r.getXPos()
                && this.getYPos() < r.getYHeight()
                && this.getYHeight() > r.getYPos();
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/gui/ChatGui.java<|end_filename|>
package mnm.mods.tabbychat.api.gui;

/**
 * The root chat box contains all the components that make up the individual
 * parts.
 */
public interface ChatGui extends IGui {

    /**
     * Gets the input portion of the chat box.
     *
     * @return The text box
     */
    ChatInput getChatInput();

    /**
     * Gets the tray of the chat box, which contains the tabs and resize handle.
     *
     * @return The tray
     */
    IGui getTray();

    /**
     * Gets the area that contains all the received chat.
     *
     * @return The chat
     */
    ReceivedChat getChatArea();
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/extra/spell/Spellcheck.java<|end_filename|>
package mnm.mods.tabbychat.extra.spell;

import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import com.swabunga.spell.engine.SpellDictionary;
import com.swabunga.spell.engine.SpellDictionaryHashMap;
import com.swabunga.spell.event.SpellCheckEvent;
import com.swabunga.spell.event.SpellChecker;
import com.swabunga.spell.event.StringWordTokenizer;
import mnm.mods.tabbychat.TabbyChat;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.client.resources.IResourceManagerReloadListener;
import net.minecraft.client.resources.Language;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Iterator;
import java.util.List;
import javax.annotation.Nonnull;

public class Spellcheck implements Iterable<SpellCheckEvent>, IResourceManagerReloadListener {

    private final File userFile;

    private SpellChecker spellCheck;
    private LangDict language;
    private SpellDictionary userDict;
    private List<SpellCheckEvent> errors = Lists.newArrayList();

    public Spellcheck(File configDir) {
        userFile = new File(configDir, "userdict.txt");
        this.loadCurrentLanguage();
    }

    public synchronized void loadDictionary(LangDict lang) throws IOException {
        if (lang == null || lang.equals(language)) {
            return;
        }
        InputStream in = null;
        Reader read = null;
        try {
            try {
                in = lang.openStream();
            } catch (FileNotFoundException e) {
                TabbyChat.getLogger().warn(e + " Falling back to English.");
                lang = LangDict.ENGLISH;
                in = lang.openStream();
            }
            read = new InputStreamReader(in);
            SpellDictionary dictionary = new SpellDictionaryHashMap(read);
            spellCheck = new SpellChecker(dictionary);
            spellCheck.setUserDictionary(userDict);
            spellCheck.addSpellCheckListener(errors::add);
            this.language = lang;
        } finally {
            Closeables.closeQuietly(in);
            Closeables.closeQuietly(read);
        }
    }

    public synchronized void loadUserDictionary() throws IOException {
        if (!userFile.exists()) {
            userFile.getParentFile().mkdirs();
            userFile.createNewFile();
            Files.write("# User dictionary, one entry per line.", userFile, Charsets.UTF_8);
        }
        this.userDict = new UserDictionary(userFile);
        // set it if it has been created yet.
        if (this.spellCheck != null) {
            this.spellCheck.setUserDictionary(this.userDict);
        }
    }

    public synchronized void addToDictionary(String word) {
        // add to user dictionary
        this.userDict.addWord(word);
    }

    public LangDict getLanguage() {
        return language;
    }

    public File getUserDictionaryFile() {
        return userFile;
    }

    public void checkSpelling(String string) {
        if (spellCheck != null) {
            this.errors.clear();
            this.spellCheck.checkSpelling(new StringWordTokenizer(string));
        }
    }

    @Override
    public void onResourceManagerReload(@Nonnull IResourceManager resourceManager) {
        loadCurrentLanguage();
    }

    private void loadCurrentLanguage() {
        Language lang = Minecraft.getMinecraft().getLanguageManager().getCurrentLanguage();
        try {
            loadUserDictionary();
            loadDictionary(LangDict.fromLanguage(lang));
        } catch (IOException e) {
            TabbyChat.getLogger().warn("Error while loading dictionary.", e);
        }
    }

    @Override
    public Iterator<SpellCheckEvent> iterator() {
        return ImmutableList.copyOf(errors).iterator();
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/events/MessageAddedToChannelEvent.java<|end_filename|>
package mnm.mods.tabbychat.api.events;

import mnm.mods.tabbychat.api.Channel;
import net.minecraft.util.text.ITextComponent;

public class MessageAddedToChannelEvent {

    public ITextComponent text;
    public int id;
    public final Channel channel;

    public MessageAddedToChannelEvent(ITextComponent text, int id, Channel channel) {
        this.text = text;
        this.id = id;
        this.channel = channel;
    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiGridLayout.java<|end_filename|>
package mnm.mods.util.gui;

import com.google.common.collect.Maps;
import mnm.mods.util.ILocation;
import mnm.mods.util.Location;
import org.apache.logging.log4j.LogManager;

import java.awt.Dimension;
import java.util.Map;
import java.util.Map.Entry;

/**
 * A layout which places components along a grid. Add components with an int[]
 * array to define bounds. Array can be of length 2 or 4, the last 2 being
 * optional. If they are not included, they default to 1.
 * <p>
 * The format of the array is <code>{ xPos, yPos, width*, height* }</code>. <br>
 * * indicates optional
 * <p>
 * <b>Example Usage:</b>
 *
 * <pre>
 * GuiPanel panel = new GuiPanel(new GuiGridLayout(10, 10));
 *
 * // creates a label and places it at x:0 y:0 and size w:1 h:1.
 * GuiLabel label = new GuiLabel(&quot;Label&quot;);
 * panel.addComponent(label, new int[] { 0, 0 });
 *
 * // creates a button and places it at x:1 y:1 with size w:3, h:2.
 * GuiButton button = new GuiButton(&quot;button&quot;);
 * panel.addComponent(button, new int[] { 1, 1, 3, 2 });
 * </pre>
 *
 * @author Matthew
 */
public class GuiGridLayout implements ILayout {

    private int cols;
    private int rows;

    private Map<ILocation, GuiComponent> grid = Maps.newHashMap();

    public GuiGridLayout(int columns, int rows) {
        this.cols = columns;
        this.rows = rows;
    }

    @Override
    public void addComponent(GuiComponent comp, Object constraints) {
        if (constraints == null) {
            throw new IllegalArgumentException("component requires constraints.");
        } else if (!(constraints instanceof int[])) {
            throw new IllegalArgumentException("Constraints must be an int array");
        }
        addComponent(comp, (int[]) constraints);
    }

    public void addComponent(GuiComponent comp, int[] constraints) {
        if (constraints.length != 2 && constraints.length != 4) {
            throw new IllegalArgumentException("Constraints must have either 2 or 4 elements.");
        }
        int x = constraints[0];
        int y = constraints[1];
        int w = 1;
        int h = 1;
        if (constraints.length == 4) {
            w = constraints[2];
            h = constraints[3];
        }
        ILocation rect = new Location(x, y, w, h);
        try {
            checkBoundsIfValid(rect);
            grid.put(rect.asImmutable(), comp);
        } catch (Exception e) {
            LogManager.getLogger().catching(e);
        }
    }

    /**
     * Checks if the rectangle is valid. A rectangle is valid if x and y are
     * above 0 and x + width < cols and y + height < rows
     */
    private void checkBoundsIfValid(ILocation b) {
        if (b.getXWidth() - 1 > cols || b.getYHeight() - 1 > rows || b.getXPos() < 0 || b.getYPos() < 0) {
            throw new IndexOutOfBoundsException(String.format(
                    "x:%s y:%s w:%s h:%s cols:%s rows:%s", b.getXPos(), b.getYPos(), b.getWidth(), b.getHeight(), cols, rows));
        }
        for (ILocation r : grid.keySet()) {
            if (b.contains(r)) {
                throw new IllegalArgumentException("Area " + b + " already contains "
                        + grid.get(r).getClass().getName());
            }
        }
    }

    @Override
    public void removeComponent(GuiComponent comp) {
        ILocation remove = null;
        for (Entry<ILocation, GuiComponent> entry : grid.entrySet()) {
            if (entry.getValue() == comp) {
                remove = entry.getKey();
                break;
            }
        }
        grid.remove(remove);
    }

    @Override
    public void layoutComponents(GuiPanel parent) {
        Dimension size = parent.getLocation().getSize();
        int colW = size.width / cols;
        int rowH = size.height / rows;
        for (Map.Entry<ILocation, GuiComponent> entry : grid.entrySet()) {
            ILocation bounds = entry.getKey();
            int x = bounds.getXPos() * colW;
            int width = bounds.getWidth() * colW;
            int y = bounds.getYPos() * rowH;
            int height = bounds.getHeight() * rowH;
            entry.getValue().setLocation(new Location(x, y, width, height));
        }
    }

    @Override
    public Dimension getLayoutSize() {
        return new Dimension(cols, rows);
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiSlider.java<|end_filename|>
package mnm.mods.util.gui;

import com.google.common.eventbus.Subscribe;
import mnm.mods.util.ILocation;
import mnm.mods.util.gui.events.GuiMouseEvent;
import mnm.mods.util.gui.events.GuiMouseEvent.MouseEvent;
import net.minecraft.client.gui.Gui;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.input.Mouse;

/**
 * A slider for double values. Click and drag or scroll to change the value.
 *
 * @author Matthew
 */
public class GuiSlider extends GuiComponent implements IGuiInput<Double> {

    private static final ResourceLocation TRANSPARENCY = new ResourceLocation("mnmutils",
            "textures/transparency.png");

    private boolean vertical;
    private double value;

    public GuiSlider(double value, boolean vertical) {
        this.vertical = vertical;
        this.setValue(value);
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        GlStateManager.enableBlend();
        ILocation loc = getLocation();
        Gui.drawRect(0, 0, loc.getWidth(), loc.getHeight(), -1);
        mc.getTextureManager().bindTexture(TRANSPARENCY);
        Gui.drawModalRectWithCustomSizedTexture(1, 1, 0, 0, loc.getWidth() - 2, loc.getHeight() - 2, 6, 6);
        drawMid(loc);
        if (vertical) {
            int nook = Math.abs((int) (loc.getHeight() * getValue()) - loc.getHeight());
            Gui.drawRect(-1, nook - 1, loc.getWidth() + 1, nook + 2, 0xffffffff);
            Gui.drawRect(0, nook, loc.getWidth(), nook + 1, 0xff000000);
        } else {
            int nook = (int) (loc.getWidth() * getValue());
            Gui.drawRect(nook, 0, nook + 1, loc.getHeight(), 0xff000000);
        }
        int midX = loc.getWidth() / 2;
        int midY = loc.getHeight() / 2;
        drawCenteredString(mc.fontRenderer, getFormattedValue(), midX, midY, -1);
        GlStateManager.disableAlpha();
        GlStateManager.disableBlend();
        super.drawComponent(mouseX, mouseY);
    }

    protected void drawMid(ILocation loc) {
        Gui.drawRect(1, 1, loc.getWidth() - 1, loc.getHeight() - 1, getPrimaryColorProperty().getHex());
    }

    public String getFormattedValue() {
        return String.format("%%%.0f", getValue() * 100);
    }

    @Subscribe
    public void moveSlider(GuiMouseEvent event) {
        if (event.getMouseX() < 0
                || event.getMouseY() < 0
                || event.getMouseX() > getLocation().getWidth()
                || event.getMouseY() > getLocation().getHeight()) {
            return;
        }
        if ((event.getType() == MouseEvent.CLICK || event.getType() == MouseEvent.DRAG)
                && Mouse.isButtonDown(0)) {
            double val;
            if (vertical) {
                int y = event.getMouseY();
                val = Math.abs((double) y / (double) getLocation().getHeight() - 1);
            } else {
                int x = event.getMouseX();
                val = (double) x / (double) getLocation().getWidth();
            }
            setValue(val);
        }
        if (event.getType() == MouseEvent.SCROLL) {
            setValue(getValue() + event.getScroll() / 7360D);
        }
    }

    @Override
    public void setValue(Double value) {
        if (value < 0) {
            value = 0D;
        }
        if (value > 1) {
            value = 1D;
        }
        this.value = value;
    }

    @Override
    public Double getValue() {
        return value;
    }

    public boolean isVertical() {
        return vertical;
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/settings/ServerSettings.java<|end_filename|>
package mnm.mods.tabbychat.settings;

import com.mumfrey.liteloader.modconfig.ConfigStrategy;
import com.mumfrey.liteloader.modconfig.ExposableOptions;
import mnm.mods.tabbychat.ChatChannel;
import mnm.mods.tabbychat.extra.filters.UserFilter;
import mnm.mods.tabbychat.util.TabbyRef;
import mnm.mods.util.IPUtils;
import mnm.mods.util.config.SettingsFile;
import mnm.mods.util.config.ValueList;
import mnm.mods.util.config.ValueMap;

import java.net.InetSocketAddress;

@ExposableOptions(strategy = ConfigStrategy.Unversioned)
public class ServerSettings extends SettingsFile {

    public GeneralServerSettings general = new GeneralServerSettings();
    public ValueList<UserFilter> filters = list();
    public ValueMap<ChatChannel> channels = map();
    public ValueMap<ChatChannel> pms = map();

    private transient final InetSocketAddress ip;

    public ServerSettings(InetSocketAddress url) {
        super(TabbyRef.MOD_ID + "/" + getIPForFileName(url), "server");
        this.ip = url;
    }

    private static String getIPForFileName(InetSocketAddress addr) {
        String ip;
        if (addr == null) {
            ip = "singleplayer";
        } else {
            String url = addr.getHostName();
            ip = "multiplayer/" + IPUtils.parse(url).getFileSafeAddress();
        }
        return ip;
    }

    public InetSocketAddress getIP() {
        return this.ip;
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/events/GuiMouseEvent.java<|end_filename|>
package mnm.mods.util.gui.events;

import mnm.mods.util.gui.GuiComponent;

/**
 * A event for the mouse adapter.
 *
 * @author Matthew
 */
public class GuiMouseEvent extends AbstractMouseEvent {

    private MouseEvent type;
    private int button;
    private long buttonTime;
    private int scroll;

    public GuiMouseEvent(GuiComponent component, MouseEvent event, int mouseX, int mouseY, int button, long buttonTime) {
        this(component, event, mouseX, mouseY, button, buttonTime, 0);
    }

    public GuiMouseEvent(GuiComponent component, MouseEvent type, int mouseX, int mouseY, int button, long buttonTime, int scroll) {
        super(component, mouseX, mouseY);
        this.type = type;
        this.button = button;
        this.scroll = scroll;
    }

    public MouseEvent getType() {
        return type;
    }

    public int getButton() {
        return button;
    }
    
    public long getButtonTime() {
        return buttonTime;
    }

    public int getScroll() {
        return scroll;
    }

    public enum MouseEvent {
        RAW,
        CLICK,
        RELEASE,
        SCROLL,
        DRAG,;
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/gui/ChatArea.java<|end_filename|>
package mnm.mods.tabbychat.gui;

import com.google.common.collect.Lists;
import com.google.common.eventbus.Subscribe;
import mnm.mods.tabbychat.ChatChannel;
import mnm.mods.tabbychat.TabbyChat;
import mnm.mods.tabbychat.api.Message;
import mnm.mods.tabbychat.api.gui.ReceivedChat;
import mnm.mods.tabbychat.core.GuiNewChatTC;
import mnm.mods.tabbychat.util.ChatTextUtils;
import mnm.mods.tabbychat.util.ChatVisibility;
import mnm.mods.util.Color;
import mnm.mods.util.ILocation;
import mnm.mods.util.TexturedModal;
import mnm.mods.util.gui.GuiComponent;
import mnm.mods.util.gui.events.GuiMouseEvent;
import mnm.mods.util.gui.events.GuiMouseEvent.MouseEvent;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.GuiUtilRenderComponents;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextComponentString;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.util.List;

public class ChatArea extends GuiComponent implements ReceivedChat {

    private static final TexturedModal MODAL = new TexturedModal(ChatBox.GUI_LOCATION, 0, 14, 254, 205);

    private ChatChannel channel;
    private List<Message> messages = Lists.newLinkedList();
    private boolean dirty;
    private int scrollPos = 0;

    public ChatArea() {
        this.setMinimumSize(new Dimension(300, 160));
    }

    @Subscribe
    public void superScrollingAction(GuiMouseEvent event) {
        if (event.getType() == MouseEvent.SCROLL) {
            // Scrolling
            int scroll = event.getScroll();
            // One tick = 120
            if (scroll != 0) {
                if (scroll > 1) {
                    scroll = 1;
                }
                if (scroll < -1) {
                    scroll = -1;
                }
                if (GuiScreen.isShiftKeyDown()) {
                    scroll *= 7;
                }
                scroll(scroll);

            }
        }
    }

    @Override
    public void onClosed() {
        resetScroll();
        super.onClosed();
    }

    @Override
    public ILocation getLocation() {
        List<Message> visible = getVisibleChat();
        int height = visible.size() * mc.fontRenderer.FONT_HEIGHT;
        ChatVisibility vis = TabbyChat.getInstance().settings.advanced.visibility.get();

        if (GuiNewChatTC.getInstance().getChatOpen() || vis == ChatVisibility.ALWAYS) {
            return super.getLocation();
        } else if (height != 0) {
            int y = super.getLocation().getHeight() - height;
            return super.getLocation().copy().move(0, y - 2).setHeight(height + 2);
        }
        return super.getLocation();
    }

    @Override
    public boolean isVisible() {

        List<Message> visible = getVisibleChat();
        int height = visible.size() * mc.fontRenderer.FONT_HEIGHT;
        ChatVisibility vis = TabbyChat.getInstance().settings.advanced.visibility.get();

        return mc.gameSettings.chatVisibility != EntityPlayer.EnumChatVisibility.HIDDEN
                && (GuiNewChatTC.getInstance().getChatOpen() || vis == ChatVisibility.ALWAYS || height != 0);
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {

        List<Message> visible = getVisibleChat();
        GlStateManager.enableBlend();
        float opac = mc.gameSettings.chatOpacity;
        GlStateManager.color(1, 1, 1, opac);

        drawModalCorners(MODAL);

        zLevel = 100;
        // TODO abstracted padding
        int xPos = getBounds().x + 3;
        int yPos = getBounds().height;
        for (Message line : visible) {
            yPos -= mc.fontRenderer.FONT_HEIGHT;
            drawChatLine(line, xPos, yPos);
        }
        zLevel = 0;
        GlStateManager.disableAlpha();
        GlStateManager.disableBlend();
    }

    private void drawChatLine(Message line, int xPos, int yPos) {
        String text = line.getMessageWithOptionalTimestamp().getFormattedText();
        mc.fontRenderer.drawStringWithShadow(text, xPos, yPos, Color.WHITE.getHex() + (getLineOpacity(line) << 24));
    }

    public void setChannel(ChatChannel channel) {
        this.channel = channel;
        this.markDirty();
    }

    public void markDirty() {
        this.dirty = true;
    }

    public List<Message> getChat() {
        if (!dirty) {
            return this.messages;
        }
        this.dirty = false;
        this.messages = ChatTextUtils.split(channel.getMessages(), getBounds().width - 6);
        return this.messages;

    }

    private List<Message> getVisibleChat() {
        List<Message> lines = getChat();

        List<Message> messages = Lists.newArrayList();
        int length = 0;

        int pos = getScrollPos();
        float unfoc = TabbyChat.getInstance().settings.advanced.unfocHeight.get();
        float div = GuiNewChatTC.getInstance().getChatOpen() ? 1 : unfoc;
        while (pos < lines.size() && length < super.getLocation().getHeight() * div - 10) {
            Message line = lines.get(pos);

            if (GuiNewChatTC.getInstance().getChatOpen()) {
                messages.add(line);
            } else if (getLineOpacity(line) > 3) {
                messages.add(line);
            } else {
                break;
            }

            pos++;
            length += mc.fontRenderer.FONT_HEIGHT;
        }

        return messages;
    }

    private int getLineOpacity(Message line) {
        ChatVisibility vis = TabbyChat.getInstance().settings.advanced.visibility.get();
        if (vis == ChatVisibility.ALWAYS)
            return 4;
        if (vis == ChatVisibility.HIDDEN && !GuiNewChatTC.getInstance().getChatOpen())
            return 0;
        int opacity = (int) (mc.gameSettings.chatOpacity * 255);

        double age = mc.ingameGUI.getUpdateCounter() - line.getCounter();
        if (!mc.ingameGUI.getChatGUI().getChatOpen()) {
            double opacPerc = age / TabbyChat.getInstance().settings.advanced.fadeTime.get();
            opacPerc = 1.0D - opacPerc;
            opacPerc *= 10.0D;

            opacPerc = Math.max(0, opacPerc);
            opacPerc = Math.min(1, opacPerc);

            opacPerc *= opacPerc;
            opacity = (int) (opacity * opacPerc);
        }
        return opacity;
    }

    @Override
    public void scroll(int scr) {
        setScrollPos(getScrollPos() + scr);
    }

    @Override
    public void setScrollPos(int scroll) {
        List<Message> list = getChat();
        scroll = Math.min(scroll, list.size() - GuiNewChatTC.getInstance().getLineCount());
        scroll = Math.max(scroll, 0);

        this.scrollPos = scroll;
    }

    @Override
    public int getScrollPos() {
        return scrollPos;
    }

    @Override
    public void resetScroll() {
        setScrollPos(0);
    }

    @Override
    public ITextComponent getChatComponent(int clickX, int clickY) {
        if (GuiNewChatTC.getInstance().getChatOpen()) {
            Point point = scalePoint(new Point(clickX, clickY), mc.currentScreen);
            ILocation actual = getActualLocation();
            // check that cursor is in bounds.
            if (point.x > actual.getXPos() && point.y > actual.getYPos()
                    && point.x < actual.getXPos() + actual.getWidth()
                    && point.y < actual.getYPos() + actual.getHeight()) {


                float scale = getActualScale();
                float size = mc.fontRenderer.FONT_HEIGHT * scale;
                float bottom = (actual.getYPos() + actual.getHeight());

                // The line to get
                int linePos = MathHelper.floor((point.y - bottom) / -size) + scrollPos;

                // Iterate through the chat component, stopping when the desired
                // x is reached.
                List<Message> list = this.getChat();
                if (linePos >= 0 && linePos < list.size()) {
                    Message chatline = list.get(linePos);
                    float x = actual.getXPos() + 3;

                    for (ITextComponent ichatcomponent : chatline.getMessageWithOptionalTimestamp()) {
                        if (ichatcomponent instanceof TextComponentString) {

                            // get the text of the component, no children.
                            String text = ichatcomponent.getUnformattedComponentText();
                            // clean it up
                            String clean = GuiUtilRenderComponents.removeTextColorsIfConfigured(text, false);
                            // get it's width, then scale it.
                            x += this.mc.fontRenderer.getStringWidth(clean) * scale;

                            if (x > point.x) {
                                return ichatcomponent;
                            }
                        }
                    }
                }
            }
        }
        return null;
    }

    @Override
    public Rectangle getBounds() {
        return getLocation().asRectangle();
    }

}


<|start_filename|>src/main/java/mnm/mods/util/update/VersionData.java<|end_filename|>
package mnm.mods.util.update;

import javax.annotation.Nullable;

public class VersionData {

    private String name;
    private String updateUrl;
    private String url;
    private double revision;

    public VersionData(String name, String updateUrl, String url, double revision) {
        this.name = name;
        this.updateUrl = updateUrl;
        this.url = url;
        this.revision = revision;
    }

    public String getName() {
        return name;
    }

    String getUpdateUrl() {
        return updateUrl;
    }

    String getUrl() {
        return this.url;
    }

    private int compareTo(double arg0) {
        return Double.compare(revision, arg0);
    }

    boolean isOutdated(@Nullable UpdateResponse.Version update) {
        return update != null && compareTo(update.revision) < 0;
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/extra/filters/ChannelFilter.java<|end_filename|>
package mnm.mods.tabbychat.extra.filters;

import mnm.mods.tabbychat.TabbyChat;
import mnm.mods.tabbychat.api.Channel;
import mnm.mods.tabbychat.api.TabbyAPI;
import mnm.mods.tabbychat.api.filters.Filter;
import mnm.mods.tabbychat.api.filters.FilterEvent;
import mnm.mods.tabbychat.settings.GeneralServerSettings;

import java.util.regex.Pattern;
import javax.annotation.Nonnull;

public class ChannelFilter implements Filter {

    @Nonnull
    @Override
    public Pattern getPattern() {
        return TabbyChat.getInstance().serverSettings.general.channelPattern.get().getPattern();
    }

    @Override
    public void action(FilterEvent event) {

        GeneralServerSettings general = TabbyChat.getInstance().serverSettings.general;
        if (general.channelsEnabled.get()) {
            String chan = event.matcher.group(1);
            Channel dest = TabbyAPI.getAPI().getChat().getChannel(chan);
            event.channels.add(dest);

        }

    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiLabel.java<|end_filename|>
package mnm.mods.util.gui;

import mnm.mods.util.text.FancyFontRenderer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.util.text.ITextComponent;

/**
 * Gui component label used to show text on the screen.
 *
 * @author Matthew
 */
public class GuiLabel extends GuiComponent {

    private FancyFontRenderer fr;
    private ITextComponent text;
    private float angle;

    public GuiLabel() {
        this.fr = new FancyFontRenderer(Minecraft.getMinecraft().fontRenderer);
    }

    /**
     * Creates a label from a chat component.
     *
     * @param chat The text
     */
    public GuiLabel(ITextComponent chat) {
        this();
        this.setText(chat);
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        if (getText() == null)
            return;
        GlStateManager.pushMatrix();
        GlStateManager.rotate(angle, 0, 0, angle);
        if (angle < 180) {
            GlStateManager.translate(-angle / 1.5, -angle / 4, 0);
        } else {
            GlStateManager.translate(-angle / 15, angle / 40, 0);
        }

        fr.drawChat(getText(), 0, 1, getPrimaryColorProperty().getHex(), true);

        GlStateManager.popMatrix();
        super.drawComponent(mouseX, mouseY);
    }

    /**
     * Sets the string of this label
     *
     * @param text The string
     */
    public void setText(ITextComponent text) {
        this.text = text;
    }

    /**
     * Gets the string of this label
     *
     * @return The string
     */
    public ITextComponent getText() {
        return text;
    }

    public float getAngle() {
        return angle;
    }

    public void setAngle(float angle) {
        this.angle = angle;
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/ComponentScreen.java<|end_filename|>
package mnm.mods.util.gui;

import mnm.mods.util.Location;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreen;

import java.io.IOException;

/**
 * A panel wrapper for a screen.
 */
public class ComponentScreen extends GuiScreen {

    private final GuiPanel PANEL = new GuiPanel();

    @Override
    public void drawScreen(int mouseX, int mouseY, float tick) {
        PANEL.drawComponent(mouseX, mouseY);
        super.drawScreen(mouseX, mouseY, tick);
        PANEL.drawCaption(mouseX, mouseY);
    }

    @Override
    public void updateScreen() {
        PANEL.updateComponent();
    }

    @Override
    public void handleKeyboardInput() throws IOException {
        super.handleKeyboardInput();
        PANEL.handleKeyboardInput();
    }

    @Override
    public void handleMouseInput() throws IOException {
        super.handleMouseInput();
        PANEL.handleMouseInput();
    }

    @Override
    public void onGuiClosed() {
        PANEL.unfocusAll();
        super.onGuiClosed();
    }

    @Override
    public void setWorldAndResolution(Minecraft mc, int width, int height) {
        PANEL.setLocation(new Location(0, 0, width, height));
        PANEL.clearComponents();
        super.setWorldAndResolution(mc, width, height);
    }

    /**
     * Gets the main panel on this screen. Add things to this.
     * 
     * @return The main panel
     */
    protected GuiPanel getPanel() {
        return PANEL;
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/ChatMessage.java<|end_filename|>
package mnm.mods.tabbychat;

import java.util.Calendar;
import java.util.Date;

import com.google.gson.annotations.Expose;

import mnm.mods.tabbychat.api.Message;
import mnm.mods.tabbychat.settings.GeneralSettings;
import mnm.mods.tabbychat.util.TimeStamps;
import net.minecraft.client.gui.ChatLine;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextComponentString;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.util.text.TextFormatting;

public class ChatMessage implements Message {

    @Expose
    private ITextComponent message;
    @Expose
    private int id;
    private transient int counter;
    @Expose
    private Date date;

    public ChatMessage(int updatedCounter, ITextComponent chat, int id, boolean isNew) {
        // super(updatedCounter, chat, id);
        this.message = chat;
        this.id = id;
        this.counter = updatedCounter;
        if (isNew) {
            this.date = Calendar.getInstance().getTime();
        }
    }

    public ChatMessage(ChatLine chatline) {
        this(chatline.getUpdatedCounter(), chatline.getChatComponent(), chatline.getChatLineID(), true);
    }

    @Override
    public ITextComponent getMessage() {
        return this.message;
    }

    @Override
    public ITextComponent getMessageWithOptionalTimestamp() {
        ITextComponent chat;
        GeneralSettings settings = TabbyChat.getInstance().settings.general;
        if (date != null && settings.timestampChat.get()) {
            chat = new TextComponentString("");

            TimeStamps stamp = settings.timestampStyle.get();
            TextFormatting format = settings.timestampColor.get();
            chat = new TextComponentTranslation("%s %s", format + stamp.format(date), getMessage());
        } else {
            chat = getMessage();
        }
        return chat;
    }

    @Override
    public int getCounter() {
        return this.counter;
    }

    @Override
    public int getID() {
        return this.id;
    }

    @Override
    public Date getDate() {
        return this.date;
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiButton.java<|end_filename|>
package mnm.mods.util.gui;

import com.google.common.eventbus.Subscribe;
import mnm.mods.util.ILocation;
import mnm.mods.util.TexturedModal;
import mnm.mods.util.gui.events.ActionPerformedEvent;
import net.minecraft.client.audio.PositionedSoundRecord;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.renderer.GlStateManager;
import net.minecraft.client.renderer.OpenGlHelper;
import net.minecraft.init.SoundEvents;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.SoundEvent;
import org.lwjgl.opengl.GL11;

import java.awt.Dimension;
import javax.annotation.Nonnull;

/**
 * A {@link net.minecraft.client.gui.GuiButton} for the GuiComponent system.
 */
public class GuiButton extends GuiComponent {

    private static final ResourceLocation WIDGETS = new ResourceLocation("textures/gui/widgets.png");
    private static final TexturedModal MODAL_NORMAL = new TexturedModal(WIDGETS, 0, 66, 200, 20);
    private static final TexturedModal MODAL_HOVER = new TexturedModal(WIDGETS, 0, 86, 200, 20);
    private static final TexturedModal MODAL_DISABLE = new TexturedModal(WIDGETS, 0, 46, 200, 20);

    private String text = "";
    private SoundEvent sound;

    /**
     * Instantiates a new button with {@code text} as the display string.
     *
     * @param text The display string
     */
    public GuiButton(String text) {
        this.setText(text);
        setSound(SoundEvents.UI_BUTTON_CLICK);
    }

    @Subscribe
    public void onClick(ActionPerformedEvent action) {
        SoundEvent sound = getSound();
        if (sound != null) {
            mc.getSoundHandler().playSound(PositionedSoundRecord.getMasterRecord(sound, 1.0F));
        }
    }

    /**
     * Sets the display text for this button.
     *
     * @param text The new text
     */
    public void setText(String text) {
        if (text == null) {
            text = "";
        }
        this.text = text;
    }

    /**
     * Gets the display text for this button.
     *
     * @return The text
     */
    public String getText() {
        return this.text;
    }

    public void setSound(SoundEvent sound) {
        this.sound = sound;
    }

    public SoundEvent getSound() {
        return sound;
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        FontRenderer fontrenderer = mc.fontRenderer;
        ILocation bounds = getLocation();

        mc.getTextureManager().bindTexture(WIDGETS);
        GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);

        TexturedModal modal = this.getHoverState(isHovered());
        GlStateManager.enableBlend();
        OpenGlHelper.glBlendFunc(770, 771, 1, 0);
        GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);

        this.drawModalCorners(modal);

        int textColor = 0xE0E0E0;

        if (!this.isEnabled()) {
            textColor = 0xA0A0A0;
        } else if (this.isHovered()) {
            textColor = 0xFFFFA0;
        }

        this.drawCenteredString(fontrenderer, getText(), bounds.getWidth() / 2, (bounds.getHeight() - 8) / 2,
                textColor);

        super.drawComponent(mouseX, mouseY);
    }

    private TexturedModal getHoverState(boolean hovered) {
        TexturedModal modal = GuiButton.MODAL_NORMAL;

        if (!this.isEnabled()) {
            modal = GuiButton.MODAL_DISABLE;
        } else if (hovered) {
            modal = GuiButton.MODAL_HOVER;
        }

        return modal;
    }

    @Nonnull
    @Override
    public Dimension getMinimumSize() {
        return new Dimension(mc.fontRenderer.getStringWidth(this.getText()) + 8, 20);
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiNumericUpDown.java<|end_filename|>
package mnm.mods.util.gui;

import com.google.common.eventbus.Subscribe;
import mnm.mods.util.Color;
import mnm.mods.util.ILocation;
import mnm.mods.util.gui.events.ActionPerformedEvent;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextComponentString;

import java.text.NumberFormat;
import javax.annotation.Nonnull;

/**
 * Input for numbers, also known as a slider. Use {@link DoubleUpDown} for
 * doubles and {@link IntUpDown} for integers.
 *
 * @author Matthew
 * @param <T> The number type.
 */
public abstract class GuiNumericUpDown<T extends Number> extends GuiPanel implements IGuiInput<T> {

    private double min = Double.MIN_VALUE;
    private double max = Double.MAX_VALUE;
    private double interval = 1;

    private double value;

    private NumberFormat format = NumberFormat.getNumberInstance();

    private GuiNumericUpDown() {
        setLayout(new BorderLayout());

        {
            GuiPanel text = new GuiPanel();
            GuiRectangle rect = new GuiRectangle() {
                @Nonnull
                @Override
                public ILocation getLocation() {
                    return getParent().map(GuiComponent::getLocation).orElseGet(super::getLocation);
                }
            };
            rect.setPrimaryColor(Color.BLACK);
            text.addComponent(rect);
            GuiLabel label = new GuiLabel() {
                @Override
                public ITextComponent getText() {
                    return new TextComponentString(format.format(getValue()));
                }
            };
            label.setLocation(label.getLocation().copy().setXPos(5).setYPos(0));
            text.addComponent(label);

            addComponent(text, BorderLayout.Position.CENTER);
        }
        {
            GuiPanel pnlButtons = new GuiPanel(new GuiGridLayout(1, 2));
            GuiButton up = new UpDown("\u2191", 1); // up arrow
            GuiButton down = new UpDown("\u2193", -1); // down arrow
            pnlButtons.addComponent(up, new int[] { 0, 0 });
            pnlButtons.addComponent(down, new int[] { 0, 1 });

            addComponent(pnlButtons, BorderLayout.Position.EAST);
        }
    }

    /**
     * Increments the value by {@code n}. Use a negative number to go down.
     * <p>
     * Essentially runs <code>value = value + (n * interval)</code>
     *
     * @param n The amount to increment
     */
    public void increment(int n) {
        setDouble(getDouble() + n * getInterval());
    }

    /**
     * Sets the number format used drawing the value.
     *
     * @param numberFormat The format
     */
    public void setFormat(NumberFormat numberFormat) {
        this.format = numberFormat;
    }

    /**
     * Gets the minimum value.
     *
     * @return The min
     */
    public double getMin() {
        return min;
    }

    /**
     * Sets the minimum value.
     *
     * @param min The min
     */
    public void setMin(double min) {
        this.min = min;
    }

    /**
     * Gets the maximum value.
     *
     * @return The max
     */
    public double getMax() {
        return max;
    }

    /**
     * Sets the maximum value.
     *
     * @param max The max
     */
    public void setMax(double max) {
        this.max = max;
    }

    /**
     * Gets the interval, which is the amount the value is changed on each
     * increment.
     *
     * @return The interval
     */
    public double getInterval() {
        return interval;
    }

    /**
     * Sets the interval, which is the amount the value is changed on each
     * increment.
     *
     * @param interval The interval
     */

    public void setInterval(double interval) {
        this.interval = interval;
    }

    /**
     * Gets the value as a double.
     *
     * @return The double value
     */
    public double getDouble() {
        return value;
    }

    /**
     * Sets the value as a double.
     *
     * @param value The new value
     */
    public void setDouble(double value) {
        value = Math.max(value, getMin());
        value = Math.min(value, getMax());
        this.value = value;
    }

    private class UpDown extends GuiButton {

        private int direction;

        public UpDown(String text, int direction) {
            super(text);
            this.direction = direction;
            setSecondaryColor(Color.DARK_GRAY);
        }

        @Nonnull
        @Override
        public ILocation getLocation() {
            return super.getLocation().copy()
                        .setWidth(6)
                        .setHeight(6).asImmutable();
        }

        @Subscribe
        public void action(ActionPerformedEvent event) {
            increment(direction);
        }
    }

    /**
     * A Numeric Up Down for integers.
     *
     * @author Matthew
     */
    public static class IntUpDown extends GuiNumericUpDown<Integer> {
        @Override
        public Integer getValue() {
            return (int) getDouble();
        }

        @Override
        public void setValue(Integer i) {
            setDouble(i);
        }
    }

    /**
     * A Numeric Up Down for doubles.
     *
     * @author Matthew
     */
    public static class DoubleUpDown extends GuiNumericUpDown<Double> {
        @Override
        public Double getValue() {
            return getDouble();
        }

        @Override
        public void setValue(Double d) {
            setDouble(d);
        }
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/gui/ChatPanel.java<|end_filename|>
package mnm.mods.tabbychat.gui;

import mnm.mods.tabbychat.core.GuiNewChatTC;
import mnm.mods.util.gui.GuiPanel;
import mnm.mods.util.gui.ILayout;

public class ChatPanel extends GuiPanel {

    public ChatPanel() {
        super();
    }

    public ChatPanel(ILayout layout) {
        super(layout);
    }

    @Override
    public boolean isVisible() {
        return super.isVisible() && GuiNewChatTC.getInstance().getChatOpen();
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/TabbedChatProxy.java<|end_filename|>
package mnm.mods.tabbychat;

import mnm.mods.tabbychat.api.Channel;
import mnm.mods.tabbychat.api.ChannelStatus;
import mnm.mods.tabbychat.api.TabbyAPI;
import mnm.mods.util.IChatProxy;
import net.minecraft.util.text.ITextComponent;

public class TabbedChatProxy implements IChatProxy {

    public static final IChatProxy INSTANCE = new TabbedChatProxy();

    @Override
    public void addToChat(String strchannel, ITextComponent msg) {
        Channel channel = TabbyAPI.getAPI().getChat().getChannel(strchannel);
        channel.addMessage(msg);
        channel.setStatus(ChannelStatus.UNREAD);
    }

}


<|start_filename|>src/main/java/mnm/mods/util/text/TextBuilder.java<|end_filename|>
package mnm.mods.util.text;

import net.minecraft.util.text.ITextComponent;

import javax.annotation.Nullable;

public class TextBuilder extends AbstractChatBuilder {

    private ITextComponent chat;

    /**
     * Used for builders that build multiple chats.
     *
     * @return This builder
     */
    @Override
    public ITextBuilder next() {
        throw new UnsupportedOperationException();
    }

    /**
     * Ends a translation so it can be appended to the chat.
     *
     * @return
     */
    @Override
    public ITextBuilder end() {
        throw new UnsupportedOperationException();
    }

    /**
     * Appends the current chat to the chat and makes the provided value the
     * current.
     *
     * @param chat The new current value
     * @return
     */
    @Override
    public TextBuilder append(@Nullable ITextComponent chat) {

        if (current != null) {
            if (this.chat == null)
                this.chat = current;
            else
                this.chat.appendSibling(current);
        }
        current = chat;
        return this;
    }

    /**
     * Appends the current chat (if any) to the chat and returns the built chat.
     *
     * @return The chat
     */
    @Override
    public ITextComponent build() {
        return append(null).chat;
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/Channel.java<|end_filename|>
package mnm.mods.tabbychat.api;

import net.minecraft.util.text.ITextComponent;

import java.util.List;
import javax.annotation.Nullable;

/**
 * Represents a channel.
 */
public interface Channel {

    /**
     * Gets the name of this channel.
     *
     * @return The name
     */
    String getName();

    /**
     * Gets whether this channel is used for a personal message.
     *
     * @return PM status
     */
    boolean isPm();

    /**
     * Gets the alias that is displayed on the channel tab.
     *
     * @return The alias
     */
    String getAlias();

    /**
     * Sets the alias that is displayed on the channel tab.
     *
     * @param alias The new alias
     */
    void setAlias(String alias);

    /**
     * Gets the prefix that is inserted before the input.
     *
     * @return The prefix
     */
    String getPrefix();

    /**
     * Sets the prefix that is inserted before the input.
     *
     * @param prefix The new prefix
     */
    void setPrefix(String prefix);

    /**
     * Gets whether the prefix is hidden or not. A hidden prefix will not be
     * displayed while typing. Defaults to false.
     *
     * @return True if the prefix is hidden
     */
    boolean isPrefixHidden();

    /**
     * Sets whether the prefix is hidden or not. A hidden prefix will not be
     * displayed while typing.
     *
     * @param hidden Whether to hide the prefix
     */
    void setPrefixHidden(boolean hidden);

    String getCommand();

    void setCommand(String cmd);

    /**
     * Gets the channel's current status. A null indicates that there is no
     * status.
     *
     * @return The status or null
     */
    @Nullable
    ChannelStatus getStatus();

    /**
     * Sets the channel's current status.
     *
     * @param status The new status
     */
    void setStatus(@Nullable ChannelStatus status);

    /**
     * Opens the settings panel for this channel.
     */
    void openSettings();

    /**
     * Gets a list of {@link Message}s in this {@link Channel}.
     *
     * @return A list of messages
     */
    List<Message> getMessages();

    /**
     * Adds a message to the channel.
     *
     * @param text The message
     */
    void addMessage(ITextComponent text);

    /**
     * Adds a message to the channel with optional deletion. Any messages
     * previously added with the same id will be removed.
     *
     * @param text The message to send
     * @param id The id of the message
     */
    void addMessage(ITextComponent text, int id);

    /**
     * Removes messages at the given position.
     *
     * @param pos The position
     */
    void removeMessageAt(int pos);

    /**
     * Removes all messages with the given id.
     *
     * @param id The id
     */
    void removeMessages(int id);

    /**
     * Clears this channel of all message.
     */
    void clear();

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/settings/GeneralSettings.java<|end_filename|>
package mnm.mods.tabbychat.settings;

import mnm.mods.tabbychat.util.TimeStamps;
import mnm.mods.util.config.Value;
import mnm.mods.util.config.ValueObject;
import net.minecraft.util.text.TextFormatting;

public class GeneralSettings extends ValueObject {

    public Value<Boolean> logChat = value(true);
    public Value<Boolean> splitLog = value(true);
    public Value<Boolean> timestampChat = value(false);
    public Value<TimeStamps> timestampStyle = value(TimeStamps.MILITARYSECONDS);
    public Value<TextFormatting> timestampColor = value(TextFormatting.WHITE);
    public Value<Boolean> antiSpam = value(false);
    public Value<Double> antiSpamPrejudice = value(0D);
    public Value<Boolean> unreadFlashing = value(true);
    public Value<Boolean> checkUpdates = value(true);
}


<|start_filename|>src/main/java/mnm/mods/util/gui/VerticalLayout.java<|end_filename|>
package mnm.mods.util.gui;

import java.awt.Dimension;
import java.util.List;

import com.google.common.collect.Lists;

import mnm.mods.util.Location;

/**
 * Displays components top to bottom. Like {@link FlowLayout}, but vertical.
 * 
 * @author Matthew
 */
public class VerticalLayout implements ILayout {

    private List<GuiComponent> list = Lists.newArrayList();

    @Override
    public void addComponent(GuiComponent comp, Object constraints) {
        if (constraints != null) {
            if (constraints instanceof Integer) {
                list.add((Integer) constraints, comp);
            } else {
                throw new IllegalArgumentException("Illegal constraint of type: "
                        + constraints.getClass().getName() + ". Only int accepted.");
            }
        } else {
            list.add(comp);
        }
    }

    @Override
    public void removeComponent(GuiComponent comp) {
        list.remove(comp);
    }

    @Override
    public void layoutComponents(GuiPanel parent) {
        int y = 0;
        for (GuiComponent comp : list) {
            Location loc = comp.getLocation().copy();
            loc.setXPos(0);
            loc.setYPos(y);
            comp.setLocation(loc);
            y += comp.getMinimumSize().height;
        }
    }

    @Override
    public Dimension getLayoutSize() {
        int width = 0;
        int height = 0;
        for (GuiComponent comp : list) {
            width = Math.max(width, comp.getLocation().getWidth());
            height += comp.getMinimumSize().height;
        }
        return new Dimension(width, height);
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/util/MessagePatterns.java<|end_filename|>
package mnm.mods.tabbychat.util;

import static mnm.mods.tabbychat.util.Translation.*;
import net.minecraft.client.resources.I18n;

public enum MessagePatterns {

    ARROW(FORMAT_MESSAGE_ARROW,
            "^\\[%s[ ]?-\\>[ ]?me\\]",
            "^\\[me[ ]?-\\>[ ]?%s\\]"),
    TO_FROM(FORMAT_MESSAGE_TO_FROM,
            "^From %s ?:",
            "^To %s ?:"),
    WHISPERS(FORMAT_MESSAGE_WHISPER,
            "^%s whispers to you:",
            "^You whisper to %s:"),
    DISABLED(FORMAT_MESSAGE_DISABLED, "a^", "a^");

    private static final String PLAYER_PATTERN = "(.{3,30})";

    private final String translation;
    private final String incoming;
    private final String outgoing;

    MessagePatterns(String translation, String incoming, String outgoing) {
        this.translation = translation;
        this.incoming = String.format(incoming, PLAYER_PATTERN);
        this.outgoing = String.format(outgoing, PLAYER_PATTERN);
    }

    public String getIncoming() {
        return incoming;
    }

    public String getOutgoing() {
        return outgoing;
    }

    @Override
    public String toString() {
        return I18n.format(translation);
    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/events/GuiEvent.java<|end_filename|>
package mnm.mods.util.gui.events;

import mnm.mods.util.gui.GuiComponent;

/**
 * A class for the gui events.
 *
 * @author Matthew
 */
public abstract class GuiEvent {

    private GuiComponent component;

    public GuiEvent(GuiComponent component) {
        this.component = component;
    }

    public GuiComponent getComponent() {
        return component;
    }
}


<|start_filename|>src/main/java/mnm/mods/util/config/ValueMap.java<|end_filename|>
package mnm.mods.util.config;

import com.google.common.collect.Maps;

import java.util.Map;

public class ValueMap<T> extends Value<Map<String, T>> {

    ValueMap() {
        this.set(Maps.newHashMap());
    }

    public void set(String key, T value) {
        this.get().put(key, value);
    }

    public T get(String key) {
        return this.get().get(key);
    }

    @Override
    public void set(Map<String, T> val) {
        super.set(Maps.newHashMap(val));
    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/config/GuiSettingString.java<|end_filename|>
package mnm.mods.util.gui.config;

import mnm.mods.util.config.Value;
import mnm.mods.util.gui.GuiText;
import mnm.mods.util.gui.config.GuiSetting.GuiSettingWrapped;

/**
 * A Gui input that wraps a {@link GuiText}.
 *
 * @author Matthew
 */
public class GuiSettingString extends GuiSettingWrapped<String, GuiText> {

    public GuiSettingString(Value<String> setting) {
        super(setting, new GuiText());
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/ILayout.java<|end_filename|>
package mnm.mods.util.gui;

import java.awt.Dimension;

/**
 * A layout is used on a {@link GuiPanel} to easily arrange it's components in a
 * certain way.
 */
public interface ILayout {

    /**
     * Tells this layout that a component was added.
     *
     * @param comp The component
     * @param constraints The optional constraints, may be null
     */
    void addComponent(GuiComponent comp, Object constraints);

    /**
     * Tells this layout that a component was removed.
     *
     * @param comp The component
     */
    void removeComponent(GuiComponent comp);

    /**
     * Called to layout the components. Called before the panel is drawn.
     */
    void layoutComponents(GuiPanel parent);

    /**
     * Gets the estimated size of this layout.
     *
     * @return The size.
     */
    Dimension getLayoutSize();

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/gui/settings/GuiSettingsGeneral.java<|end_filename|>
package mnm.mods.tabbychat.gui.settings;

import static mnm.mods.tabbychat.util.Translation.*;

import mnm.mods.tabbychat.TabbyChat;
import mnm.mods.tabbychat.settings.GeneralSettings;
import mnm.mods.tabbychat.settings.TabbySettings;
import mnm.mods.tabbychat.util.TimeStamps;
import mnm.mods.util.Color;
import mnm.mods.util.gui.GuiGridLayout;
import mnm.mods.util.gui.GuiLabel;
import mnm.mods.util.gui.config.GuiSettingBoolean;
import mnm.mods.util.gui.config.GuiSettingEnum;
import mnm.mods.util.gui.config.GuiSettingNumber.GuiSettingDouble;
import mnm.mods.util.gui.config.SettingPanel;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.text.TextComponentTranslation;
import net.minecraft.util.text.TextFormatting;

import java.text.NumberFormat;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class GuiSettingsGeneral extends SettingPanel<TabbySettings> {

    public GuiSettingsGeneral() {
        setLayout(new GuiGridLayout(10, 20));
        setDisplayString(I18n.format(SETTINGS_GENERAL));
        setSecondaryColor(Color.of(255, 0, 255, 64));
    }

    @Override
    public void initGUI() {
        GeneralSettings sett = getSettings().general;

        int pos = 1;
        addComponent(new GuiLabel(new TextComponentTranslation(LOG_CHAT)), new int[] { 2, pos });
        GuiSettingBoolean chkLogChat = new GuiSettingBoolean(sett.logChat);
        chkLogChat.setCaption(new TextComponentTranslation(LOG_CHAT_DESC));
        addComponent(chkLogChat, new int[] { 1, pos });

        addComponent(new GuiLabel(new TextComponentTranslation(SPLIT_LOG)), new int[] { 7, pos });
        GuiSettingBoolean chkSplitLog = new GuiSettingBoolean(sett.splitLog);
        chkSplitLog.setCaption(new TextComponentTranslation(SPLIT_LOG_DESC));
        addComponent(chkSplitLog, new int[] { 6, pos });

        pos += 2;
        addComponent(new GuiLabel(new TextComponentTranslation(TIMESTAMP)), new int[] { 2, pos });
        addComponent(new GuiSettingBoolean(sett.timestampChat), new int[] { 1, pos });

        pos += 2;
        addComponent(new GuiLabel(new TextComponentTranslation(TIMESTAMP_STYLE)), new int[] { 3, pos });
        addComponent(new GuiSettingEnum<>(sett.timestampStyle, TimeStamps.values()), new int[] { 5, pos, 4, 1 });

        pos += 2;
        addComponent(new GuiLabel(new TextComponentTranslation(TIMESTAMP_COLOR)), new int[] { 3, pos });
        addComponent(new GuiSettingEnum<>(sett.timestampColor, getColors(), GuiSettingsGeneral::getColorName), new int[] { 5, pos, 4, 1 });

        pos += 2;
        addComponent(new GuiLabel(new TextComponentTranslation(ANTI_SPAM)), new int[] { 2, pos });
        GuiSettingBoolean chkSpam = new GuiSettingBoolean(sett.antiSpam);
        chkSpam.setCaption(new TextComponentTranslation(ANTI_SPAM_DESC));
        addComponent(chkSpam, new int[] { 1, pos });

        pos += 2;
        addComponent(new GuiLabel(new TextComponentTranslation(SPAM_PREJUDICE)), new int[] { 3, pos });
        GuiSettingDouble nud = new GuiSettingDouble(sett.antiSpamPrejudice);
        nud.getComponent().setMin(0);
        nud.getComponent().setMax(1);
        nud.getComponent().setInterval(0.05);
        nud.getComponent().setFormat(NumberFormat.getPercentInstance());
        nud.setCaption(new TextComponentTranslation(SPAM_PREJUDICE_DESC));
        addComponent(nud, new int[] { 6, pos, 2, 1 });

        pos += 2;
        addComponent(new GuiLabel(new TextComponentTranslation(UNREAD_FLASHING)), new int[] { 2, pos });
        addComponent(new GuiSettingBoolean(sett.unreadFlashing), new int[] { 1, pos });

        pos += 2;
        addComponent(new GuiLabel(new TextComponentTranslation(CHECK_UPDATES)), new int[] { 2, pos });
        addComponent(new GuiSettingBoolean(sett.checkUpdates), new int[] { 1, pos });
    }

    private static List<TextFormatting> getColors() {
        return Stream.of(TextFormatting.values())
                .filter(TextFormatting::isColor)
                .collect(Collectors.toList());
    }

    private static String getColorName(TextFormatting input) {
        return "colors." + input.getFriendlyName();
    }

    @Override
    public TabbySettings getSettings() {
        return TabbyChat.getInstance().settings;
    }

}


<|start_filename|>src/main/java/mnm/mods/util/text/Selector.java<|end_filename|>
package mnm.mods.util.text;

public enum Selector {

    PLAYER('p'),
    ALL('a'),
    ENTITY('e'),
    RANDOM('r');

    private char id;

    Selector(char c) {
        this.id = c;
    }

    @Override
    public String toString() {
        return "@" + id;
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/extra/spell/LangDict.java<|end_filename|>
package mnm.mods.tabbychat.extra.spell;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import org.apache.commons.lang3.Validate;

import mnm.mods.tabbychat.TabbyChat;
import net.minecraft.client.resources.Language;

public class LangDict {

    public static final LangDict ENGLISH = new LangDict("en_US");

    private final String language;

    LangDict(String lang) {
        Validate.notNull(lang, "Language cannot be null!");
        language = lang;
    }

    public boolean isClasspath() {
        return getClass().getClassLoader().getResource(getPath()) != null;
    }

    public boolean isConfig() {
        File lang = new File(TabbyChat.getInstance().getDataFolder(), getPath());
        return lang.exists() && lang.isFile();
    }

    public InputStream openStream() throws IOException {
        InputStream in;
        if (isClasspath()) {
            in = getClass().getClassLoader().getResourceAsStream(getPath());
        } else if (isConfig()) {
            File f = new File(TabbyChat.getInstance().getDataFolder(), getPath());
            in = new FileInputStream(f);
        } else {
            // it doesn't exist.
            throw new FileNotFoundException("Dictionary for " + language + " does not exist.");
        }
        return in;
    }

    private String getPath() {
        return String.format("dicts/%sx.dic", language);
    }

    @Override
    public int hashCode() {
        return language.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || !(obj instanceof LangDict))
            return false;
        LangDict other = (LangDict) obj;
        return language.equals(other.language);
    }

    public static LangDict fromLanguage(Language lang) {
        String code = "en_US";
        if (lang != null)
            code = lang.getLanguageCode();
        return new LangDict(code);
    }
}


<|start_filename|>src/main/java/mnm/mods/util/LiteModMnmUtils.java<|end_filename|>
package mnm.mods.util;

import com.mumfrey.liteloader.LiteMod;

import java.io.File;

public class LiteModMnmUtils implements LiteMod {

    private MnmUtils utils;

    @Override
    public String getName() {
        return "MnmUtils";
    }

    @Override
    public String getVersion() {
        return "@VERSION@";
    }

    @SuppressWarnings("deprecation")
    @Override
    public void init(File arg0) {
        this.utils = new MnmUtils();
    }

    @Override
    public void upgradeSettings(String arg0, File arg1, File arg2) {
    }

    public MnmUtils getUtils() {
        return utils;
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/filters/FilterEvent.java<|end_filename|>
package mnm.mods.tabbychat.api.filters;

import java.util.Set;
import java.util.regex.Matcher;

import com.google.common.collect.Sets;

import mnm.mods.tabbychat.api.Channel;
import net.minecraft.util.text.ITextComponent;

public class FilterEvent {

    public final Matcher matcher;
    public ITextComponent text;
    public Set<Channel> channels = Sets.newHashSet();

    public FilterEvent(Matcher matcher, Set<Channel> channels, ITextComponent text) {
        this.matcher = matcher;
        this.text = text;
        this.channels = channels;
    }
}


<|start_filename|>src/main/java/mnm/mods/util/IPUtils.java<|end_filename|>
package mnm.mods.util;

import io.netty.util.NetUtil;

import java.net.InetSocketAddress;
import java.net.SocketAddress;

/**
 * Class for dealing with IPs.
 */
public class IPUtils {

    private static final int DEFAULT_PORT = 25565;

    private String host;
    private int port;
    private boolean ipv6;

    /**
     * Creates a new IP address with the given ip and port.
     *
     * @param address The ip
     * @param port The port
     * @param ipv6 If the address is ipv6
     */
    public IPUtils(String address, int port, boolean ipv6) {
        this.host = address;
        this.port = port;
        this.ipv6 = ipv6;
    }

    /**
     * Parses a string address. Default port is 25565.
     *
     * @param ipString The full string address with or without port
     * @return The parsed IP
     */
    public static IPUtils parse(String ipString) {
        IPUtils result;
        switch (getType(ipString)) {
        case NAME:
        case IPV4:
            if (ipString.contains(":")) {
                String host = ipString.substring(0, ipString.lastIndexOf(':'));
                int port = Integer.parseInt(ipString.substring(ipString.lastIndexOf(':') + 1));
                result = new IPUtils(host, port, false);
            } else {
                result = new IPUtils(ipString, DEFAULT_PORT, false);
            }
            break;
        case IPV6:
            if (ipString.startsWith("[") && ipString.contains("]:")) {
                String host = ipString.substring(0, ipString.lastIndexOf(':'));
                int port = Integer.parseInt(ipString.substring(ipString.lastIndexOf(':') + 1));
                result = new IPUtils(host, port, true);
            } else {
                result = new IPUtils(ipString, DEFAULT_PORT, true);
            }
            break;
        default:
            throw new RuntimeException("This shouldn't have happened");
        }
        if (result.getHost().isEmpty()) {
            result.host = "localhost";
            result.ipv6 = false;
        }
        return result;
    }

    private static ConnectionType getType(String ipAddress) {
        ConnectionType result;
        if (NetUtil.isValidIpV4Address(ipAddress)) {
            result = ConnectionType.IPV4;
        } else if (NetUtil.isValidIpV6Address(ipAddress)) {
            result = ConnectionType.IPV6;
        } else {
            result = ConnectionType.NAME;
        }
        return result;
    }

    /**
     * Gets the host part of this address.
     *
     * @return The host
     */
    public String getHost() {
        return this.host;
    }

    /**
     * Gets the port of this address.
     *
     * @return The port
     */
    public int getPort() {
        return this.port;
    }

    /**
     * Gets if this ip has a port.
     *
     * @return True if ip has a port, false if it doesn't.
     */
    public boolean hasPort() {
        return this.port != DEFAULT_PORT;
    }

    /**
     * Gets if this ip is IPv6.
     *
     * @return True if IP is IPv6, false if it isn't.
     */
    public boolean isIPv6() {
        return this.ipv6;
    }

    /**
     * Gets the full address of this ip. If it has a port, it is included.
     *
     * @return The full address
     */
    public String getAddress() {
        return this.host + (hasPort() ? "" : ":" + port);
    }

    /**
     * Gets the {@link SocketAddress} for this IP.
     *
     * @return The SocketAddress
     */
    public SocketAddress getSocketAddress() {
        return InetSocketAddress.createUnresolved(host, port);
    }

    /**
     * Gets the file safe address of this IP. Replaces all the colons (:) with
     * underscores (_) and surrounds the port in parentheses.
     *
     * @return A string usable as a file name.
     */
    public String getFileSafeAddress() {
        return this.host.replace(':', '_') + (hasPort() ? "" : "(" + port + ")");
    }

    private enum ConnectionType {
        IPV4,
        IPV6,
        NAME
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiSelectColor.java<|end_filename|>
package mnm.mods.util.gui;

import com.google.common.eventbus.Subscribe;
import mnm.mods.util.Color;
import mnm.mods.util.config.Value;
import mnm.mods.util.gui.config.GuiSettingString;
import mnm.mods.util.gui.events.ActionPerformedEvent;
import mnm.mods.util.gui.events.GuiKeyboardEvent;
import mnm.mods.util.gui.events.GuiMouseEvent;
import mnm.mods.util.gui.events.GuiMouseEvent.MouseEvent;
import mnm.mods.util.text.TextBuilder;
import net.minecraft.client.resources.I18n;
import net.minecraft.util.text.TextFormatting;
import org.lwjgl.input.Keyboard;

import java.math.BigInteger;
import java.util.function.Consumer;

/**
 * A gui used to select a color.
 *
 * @author Matthew
 */
public class GuiSelectColor extends GuiPanel {

    private Color color;

    private GuiSliderColor sliderRed;
    private GuiSliderColor sliderGreen;
    private GuiSliderColor sliderBlue;
    private GuiSliderColor sliderAlpha;

    private GuiSettingString string;

    private GuiRectangle current = new GuiRectangle();
    private GuiRectangle selected = new GuiRectangle();

    /**
     * Creates a new instance with a color and callback.
     *
     * @param callback_ Called when apply is clicked
     * @param color The starting color
     */
    public GuiSelectColor(final Consumer<Color> callback_, Color color) {
        this.current.setPrimaryColor(color);
        this.selected.setPrimaryColor(color);
        this.current.getBus().register(new Object() {
            @Subscribe
            public void accept(GuiMouseEvent event) {
                if (event.getType() == MouseEvent.CLICK) {
                    Color color = current.getPrimaryColorProperty();
                    setColor(color);
                }
            }
        });
        this.setLayout(new GuiGridLayout(20, 20));

        this.addComponent(sliderRed = new GuiSliderColor(color.getRed() / 255D, true, GuiSliderColor.Model.RED, color), new int[] { 1, 1, 2, 10 });
        this.addComponent(sliderGreen = new GuiSliderColor(color.getGreen() / 255D, true, GuiSliderColor.Model.GREEN, color), new int[] { 4, 1, 2, 10 });
        this.addComponent(sliderBlue = new GuiSliderColor(color.getBlue() / 255D, true, GuiSliderColor.Model.BLUE, color), new int[] { 7, 1, 2, 10 });
        this.addComponent(sliderAlpha = new GuiSliderColor(color.getAlpha() / 255D, true, GuiSliderColor.Model.ALPHA, color), new int[] { 10, 1, 2, 10 });

        GuiLabel label;

        label = new GuiLabel();
        label.setText(new TextBuilder().quickTranslate("colors.red").format(TextFormatting.RED).build());
        label.setAngle(300);
        this.addComponent(label, new int[] { 1, 12 });

        label = new GuiLabel();
        label.setText(new TextBuilder().quickTranslate("colors.green").format(TextFormatting.GREEN).build());
        label.setAngle(300);
        this.addComponent(label, new int[] { 4, 12 });

        label = new GuiLabel();
        label.setText(new TextBuilder().quickTranslate("colors.blue").format(TextFormatting.BLUE).build());
        label.setAngle(300);
        this.addComponent(label, new int[] { 7, 12 });

        label = new GuiLabel();
        label.setText(new TextBuilder().quickTranslate("colors.alpha").format(TextFormatting.WHITE).build());
        label.setAngle(300);
        this.addComponent(label, new int[] { 10, 12 });

        this.addComponent(current, new int[] { 14, 1, 6, 3 });
        this.addComponent(selected, new int[] { 14, 4, 6, 3 });

        string = new GuiSettingString(new Value<>(""));
        string.getComponent().getTextField().setMaxStringLength(8);
        string.getBus().register(new Object() {
            @Subscribe
            public void accept(GuiKeyboardEvent event) {
                if (string.isFocused() && event.getKey() == Keyboard.KEY_RETURN) {
                    String hex = string.getValue();
                    if (hex.matches("^[0-9a-fA-F]{1,8}$")) { // valid hex
                        int c = new BigInteger(hex, 16).intValue();
                        setColor(Color.of(c));
                    }
                }
            }
        });
        this.addComponent(string, new int[] { 14, 8, 6, 2 });

        GuiButton random = new GuiButton(I18n.format("createWorld.customize.custom.randomize"));
        random.getBus().register(new Object() {
            @Subscribe
            public void action(ActionPerformedEvent event) {
                setColor(Color.random());
            }
        });
        this.addComponent(random, new int[] { 13, 11, 8, 2 });

        GuiButton cancel = new GuiButton(I18n.format("gui.cancel"));
        cancel.getBus().register(new Object() {
            @Subscribe
            public void action(ActionPerformedEvent event) {
                // Close
                getParent().ifPresent(parent -> parent.setOverlay(null));
            }
        });
        this.addComponent(cancel, new int[] { 13, 13, 4, 2 });
        GuiButton apply = new GuiButton(I18n.format("gui.done"));
        apply.getBus().register(new Object() {
            @Subscribe
            public void action(ActionPerformedEvent event) {
                callback_.accept(GuiSelectColor.this.color);
            }
        });
        this.addComponent(apply, new int[] { 17, 13, 4, 2 });

        setColor(color);
    }

    public void setColor(Color color) {
        sliderRed.setValue(color.getRed() / 255D);
        sliderGreen.setValue(color.getGreen() / 255D);
        sliderBlue.setValue(color.getBlue() / 255D);
        sliderAlpha.setValue(color.getAlpha() / 255D);
    }

    @Override
    public void updateComponent() {
        super.updateComponent();

        int r = (int) (sliderRed.getValue() * 255);
        int g = (int) (sliderGreen.getValue() * 255);
        int b = (int) (sliderBlue.getValue() * 255);
        int a = (int) (sliderAlpha.getValue() * 255);

        Color color = Color.of(r, g, b, a);
        if (!color.equals(this.color)) {
            this.color = color;
            selected.setPrimaryColor(color);

            sliderRed.setBase(color);
            sliderGreen.setBase(color);
            sliderBlue.setBase(color);
            sliderAlpha.setBase(color);

            string.setValue(Integer.toHexString(color.getHex()));
        }
    }
}


<|start_filename|>src/test/java/mnm/mods/util/test/ColorTest.java<|end_filename|>
package mnm.mods.util.test;

import static org.junit.Assert.*;

import org.junit.Test;

import mnm.mods.util.Color;

public class ColorTest {

    @Test
    public void testHex() {
        Color c = Color.of(0x44771200);
        assertEquals(c.getRed(), 0x77);
        assertEquals(c.getGreen(), 0x12);
        assertEquals(c.getBlue(), 0x00);
        assertEquals(c.getAlpha(), 0x44);

        assertEquals(c.getHex(), 0x44771200);
    }

    @Test
    public void testEquals() {
        assertEquals(Color.RED, Color.of(0xffff5555));
        assertEquals(Color.of(0xffff5555), Color.of(0xff, 0x55, 0x55, 0xff));
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/ChatChannel.java<|end_filename|>
package mnm.mods.tabbychat;

import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import mnm.mods.tabbychat.api.Channel;
import mnm.mods.tabbychat.api.ChannelStatus;
import mnm.mods.tabbychat.api.Message;
import mnm.mods.tabbychat.api.events.MessageAddedToChannelEvent;
import mnm.mods.tabbychat.gui.ChatArea;
import mnm.mods.tabbychat.gui.settings.GuiSettingsChannel;
import mnm.mods.tabbychat.util.ChannelPatterns;
import mnm.mods.tabbychat.util.ChatTextUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.util.StringUtils;
import net.minecraft.util.text.ITextComponent;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Matcher;

public class ChatChannel implements Channel {

    public static final Channel DEFAULT_CHANNEL = new ChatChannel("*", false) {
        // Don't mess with this channel
        @Override
        public void setAlias(String alias) {}

        @Override
        public void setPrefix(String prefix) {}

        @Override
        public void setPrefixHidden(boolean hidden) {}

        @Override
        public void setCommand(String command) {}

        @Override
        public void openSettings() {
            // There are no settings for this channel
            TabbyChat.getInstance().openSettings(null);
        }
    };

    private transient List<Message> messages;

    private final String name;
    private final boolean isPm;
    private String alias;

    private String prefix = "";
    private boolean prefixHidden = false;

    private String command = "";

    private transient ChannelStatus status;

    ChatChannel(String name, boolean pm) {
        this.name = name;
        this.isPm = pm;
        this.alias = this.name;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public boolean isPm() {
        return isPm;
    }

    @Override
    public String getAlias() {
        return this.alias;
    }

    @Override
    public void setAlias(String alias) {
        this.alias = alias;
    }

    @Override
    public String getPrefix() {
        return Strings.nullToEmpty(this.prefix);
    }

    @Override
    public void setPrefix(String prefix) {
        this.prefix = StringUtils.stripControlCodes(prefix);
    }

    @Override
    public boolean isPrefixHidden() {
        return this.prefixHidden;
    }

    @Override
    public void setPrefixHidden(boolean hidden) {
        this.prefixHidden = hidden;
    }

    @Override
    public String getCommand() {
        return Strings.nullToEmpty(command);
    }

    @Override
    public void setCommand(String command) {
        this.command = command;
    }

    @Override
    public ChannelStatus getStatus() {
        return status;
    }

    @Override
    public void setStatus(ChannelStatus status) {
        // priorities
        if (status == null || this.status == null
                || status.ordinal() < this.status.ordinal()) {
            this.status = status;
        }
        if (status == ChannelStatus.ACTIVE) {
            getManager().getChatBox().getChatArea().setChannel(this);
        }
    }

    @Override
    public void openSettings() {
        TabbyChat.getInstance().openSettings(new GuiSettingsChannel(this));
    }

    @Override
    public List<Message> getMessages() {
        if (messages == null) {
            // dumb gson
            messages = Collections.synchronizedList(Lists.<Message> newArrayList());
        }
        return messages;
    }

    @Override
    public void addMessage(ITextComponent chat) {
        addMessage(chat, 0);
    }

    @Override
    public void addMessage(ITextComponent chat, int id) {
        List<Channel> channels = TabbyChat.getInstance().getChat().getChannels();
        if (!channels.contains(this)) {
            TabbyChat.getInstance().getChat().addChannel(this);
        }
        if (id != 0) {
            removeMessages(id);
        }
        MessageAddedToChannelEvent event = new MessageAddedToChannelEvent(chat.createCopy(), id, this);
        TabbyChat.getInstance().getBus().post(event);
        if (event.text == null) {
            return;
        }
        if (TabbyChat.getInstance().settings.advanced.hideTag.get() && this != DEFAULT_CHANNEL) {
            ChannelPatterns pattern = TabbyChat.getInstance().serverSettings.general.channelPattern.get();
            Matcher matcher = pattern.getPattern().matcher(event.text.getUnformattedText());
            if (matcher.find()) {
                event.text = ChatTextUtils.subChat(event.text, matcher.end());
            }
        }

        int uc = Minecraft.getMinecraft().ingameGUI.getUpdateCounter();
        Message msg = new ChatMessage(uc, event.text, id, true);
        this.getMessages().add(0, msg);

        // compensate scrolling
        ChatArea chatbox = ((ChatManager) TabbyChat.getInstance().getChat()).getChatBox().getChatArea();
        if (getStatus() == ChannelStatus.ACTIVE && chatbox.getScrollPos() > 0 && id == 0) {
            chatbox.scroll(1);
        }

        trim(TabbyChat.getInstance().settings.advanced.historyLen.get());

        ((ChatManager) TabbyChat.getInstance().getChat()).save();
        dirty();
    }

    private void trim(int size) {
        Iterator<Message> iter = this.getMessages().iterator();

        for (int i = 0; iter.hasNext(); i++) {
            iter.next();
            if (i > size) {
                iter.remove();
            }
        }
    }

    @Override
    public void removeMessageAt(int pos) {
        this.getMessages().remove(pos);
        ((ChatManager) TabbyChat.getInstance().getChat()).save();
        dirty();
    }

    @Override
    public void removeMessages(int id) {
        this.getMessages().removeIf(msg -> msg.getID() == id);
        ((ChatManager) TabbyChat.getInstance().getChat()).save();
        dirty();
    }

    @Override
    public void clear() {
        this.getMessages().clear();
        dirty();
    }

    private void dirty() {
        if (this.getStatus() == ChannelStatus.ACTIVE)
            getManager().getChatBox().getChatArea().markDirty();
    }

    private static ChatManager getManager() {
        return (ChatManager) TabbyChat.getInstance().getChat();
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + (isPm ? 1231 : 1237);
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (!(obj instanceof ChatChannel))
            return false;
        ChatChannel other = (ChatChannel) obj;
        if (isPm != other.isPm)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        return true;
    }

    @Override
    public String toString() {
        return (isPm ? "@" : "#") + name;
    }

}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiText.java<|end_filename|>
package mnm.mods.util.gui;

import com.google.common.eventbus.Subscribe;
import com.mumfrey.liteloader.client.overlays.IGuiTextField;
import mnm.mods.util.Color;
import mnm.mods.util.ILocation;
import mnm.mods.util.gui.events.GuiKeyboardEvent;
import mnm.mods.util.gui.events.GuiMouseEvent;
import mnm.mods.util.gui.events.GuiMouseEvent.MouseEvent;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiTextField;
import org.apache.commons.lang3.StringUtils;
import org.lwjgl.input.Keyboard;

import javax.annotation.Nonnull;

/**
 * A gui component that wraps {@link GuiTextField}.
 *
 * @author Matthew
 */
public class GuiText extends GuiComponent implements IGuiInput<String> {

    private final GuiTextField textField;
    private String hint;

    public GuiText() {
        this(new GuiTextField(0, Minecraft.getMinecraft().fontRenderer, 0, 0, 1, 1));
    }

    public GuiText(@Nonnull GuiTextField textField) {
        this.textField = textField;
        // This text field must not be calibrated for someone of your...
        // generous..ness
        // I'll add a few 0s to the maximum length...
        this.textField.setMaxStringLength(10000);

        // you look great, by the way.

    }

    @Subscribe
    public void textboxClick(GuiMouseEvent event) {
        if (event.getType() == MouseEvent.CLICK) {
            setFocused(true);

            int x = event.getMouseX();
            int y = event.getMouseY();

            // send to text field.
            textField.mouseClicked(x, y, 0);
        }
    }

    @Subscribe
    public void textboxType(GuiKeyboardEvent event) {
        if (Keyboard.isKeyDown(event.getKey())) {
            textField.textboxKeyTyped(event.getCharacter(), event.getKey());
        }
    }

    @Override
    public void setLocation(ILocation bounds) {
        updateTextbox(bounds);
        super.setLocation(bounds);
    }

    private void updateTextbox(ILocation loc) {
        int width = loc.getWidth();
        int height = loc.getHeight();
        // this interface is provided by liteloader. (Thanks, mum)
        IGuiTextField field = (IGuiTextField) this.textField;
        field.setInternalWidth(width);
        field.setHeight(height);
    }

    @Override
    public boolean isFocusable() {
        return true;
    }

    @Override
    public void setFocused(boolean focused) {
        super.setFocused(focused);
        textField.setFocused(focused);
    }

    @Override
    public void updateComponent() {
        super.updateComponent();
        textField.updateCursorCounter();
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        textField.drawTextBox();

        super.drawComponent(mouseX, mouseY);
        if (isFocused() && !StringUtils.isEmpty(getHint())) {
            // draw the hint above.
            drawCaption(getHint(), 1, -5);
        }
    }

    @Override
    public void setPrimaryColor(Color foreColor) {
        textField.setTextColor(foreColor.getHex());
        super.setPrimaryColor(foreColor);
    }

    @Override
    public String getValue() {
        return textField.getText();
    }

    @Override
    public void setValue(String value) {
        textField.setText(value);
        textField.setCursorPositionZero();
    }

    public String getHint() {
        return hint;
    }

    public void setHint(String hint) {
        this.hint = hint;
    }

    public GuiTextField getTextField() {
        return textField;
    }
}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiScrollingPanel.java<|end_filename|>
package mnm.mods.util.gui;

import static org.lwjgl.opengl.GL11.*;

import com.google.common.eventbus.Subscribe;
import mnm.mods.util.ILocation;
import mnm.mods.util.Location;
import mnm.mods.util.gui.BorderLayout.Position;
import mnm.mods.util.gui.events.GuiMouseEvent;
import mnm.mods.util.gui.events.GuiMouseEvent.MouseEvent;
import net.minecraft.client.gui.Gui;

import java.awt.Dimension;
import javax.annotation.Nonnull;

/**
 * TODO: Horizontal scrolling
 *
 * @author Matthew
 */
public class GuiScrollingPanel extends GuiPanel {

    private GuiPanel panel;

    public GuiScrollingPanel() {
        super(new BorderLayout());
        this.panel = new GuiPanel();
        this.panel.setLocation(new Location(0, 0, 100000, 100000));
        GuiPanel panel = new GuiPanel();
        panel.addComponent(this.panel);
        this.addComponent(panel, Position.CENTER);
        this.addComponent(new Scrollbar(), Position.EAST);
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        ILocation actual = getActualLocation();
        ILocation rect = getLocation();

        glEnable(GL_SCISSOR_TEST);
        glScissor(actual.getXPos() * 2, mc.displayHeight - rect.getHeight() * 2 - actual.getYPos() * 2, rect.getWidth() * 2, rect.getHeight() * 2);

        super.drawComponent(mouseX, mouseY);

        glDisable(GL_SCISSOR_TEST);
    }

    @Subscribe
    public void scroll(GuiMouseEvent event) {
        if (event.getType() == MouseEvent.SCROLL) {
            Location rect = panel.getLocation().copy();
            int scr = rect.getYPos() + event.getScroll() / 12;
            rect.setYPos(scr);

            panel.getParent().map(GuiComponent::getLocation).ifPresent(prect -> {
                Dimension dim = panel.getMinimumSize();
                if (rect.getYPos() + dim.height < prect.getHeight()) {
                    rect.setYPos(prect.getHeight() - dim.height);
                }
            });
            if (rect.getYPos() > 0)
                rect.setYPos(0);

            panel.setLocation(rect);
        }
    }

    public GuiPanel getContentPanel() {
        return panel;
    }

    @Nonnull
    @Override
    public Dimension getMinimumSize() {
        return getLocation().getSize();
    }

    // TODO Make draggable
    private class Scrollbar extends GuiComponent {

        @Override
        public void drawComponent(int mouseX, int mouseY) {
            int scroll = panel.getLocation().getYPos();
            int max = GuiScrollingPanel.this.getLocation().getHeight();
            int total = panel.getMinimumSize().height;
            if (total <= max) {
                return;
            }
            total -= max;
            Gui.drawRect(0, 20, 10, 10, -1);
            int size = Math.max(max / 2, 10);
            float perc = ((float) scroll / (float) total) * ((float) size / (float) max);
            int pos = (int) (-perc * max);

            Gui.drawRect(-1, pos, 0, pos + size - 1, -1);
            super.drawComponent(mouseX, mouseY);
        }
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/events/ChatScreenEvents.java<|end_filename|>
package mnm.mods.tabbychat.api.events;

import net.minecraft.client.gui.GuiChat;

public abstract class ChatScreenEvents {

    public final GuiChat chatScreen;

    public ChatScreenEvents(GuiChat chat) {
        this.chatScreen = chat;
    }

    /**
     * Event for when the chat screen gets initialized.
     */
    public static class ChatInitEvent extends ChatScreenEvents {

        public ChatInitEvent(GuiChat chat) {
            super(chat);
        }
    }

    public static class ChatUpdateEvent extends ChatScreenEvents {

        public ChatUpdateEvent(GuiChat chat) {
            super(chat);
        }
    }

    public static class ChatCloseEvent extends ChatScreenEvents {

        public ChatCloseEvent(GuiChat chat) {
            super(chat);
        }
    }    
}

<|start_filename|>src/main/java/mnm/mods/util/text/TranslationBuilder.java<|end_filename|>
package mnm.mods.util.text;

import com.google.common.collect.Lists;
import net.minecraft.util.text.ITextComponent;
import net.minecraft.util.text.TextComponentTranslation;

import java.util.List;
import javax.annotation.Nullable;

class TranslationBuilder extends AbstractChatBuilder {

    private final ITextBuilder parent;
    private final String translationKey;
    private List<Object> translationArgs = Lists.newArrayList();

    private ITextComponent buffer;

    TranslationBuilder(ITextBuilder parent, String key) {
        this.parent = parent;
        this.translationKey = key;
    }

    @Override
    public ITextBuilder next() {
        translationArgs.add(append(null).buffer);
        buffer = null;
        return this;
    }

    @Override
    public ITextBuilder end() {
        ITextComponent buffer = append(null).buffer;
        if (buffer != null)
            translationArgs.add(buffer);
        return parent.append(new TextComponentTranslation(translationKey, translationArgs.toArray()));
    }

    @Override
    public ITextComponent build() {
        throw new IllegalStateException("Translation in progress.");
    }

    @Override
    public TranslationBuilder append(@Nullable ITextComponent chat) {
        if (current != null) {
            if (this.buffer == null)
                buffer = current;
            else
                this.buffer.appendSibling(current);
        }
        current = chat;
        return this;
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/extra/filters/MessageFilter.java<|end_filename|>
package mnm.mods.tabbychat.extra.filters;

import mnm.mods.tabbychat.TabbyChat;
import mnm.mods.tabbychat.api.Channel;
import mnm.mods.tabbychat.api.TabbyAPI;
import mnm.mods.tabbychat.api.filters.Filter;
import mnm.mods.tabbychat.api.filters.FilterEvent;
import mnm.mods.tabbychat.util.MessagePatterns;

import java.util.regex.Pattern;
import javax.annotation.Nonnull;

/**
 * Base class for filters that just need to set the
 */
public class MessageFilter implements Filter {

    @Nonnull
    @Override
    public Pattern getPattern() {

        MessagePatterns messege = TabbyChat.getInstance().serverSettings.general.messegePattern.get();
        String pattern = String.format("(?:%s|%s)", messege.getOutgoing(), messege.getIncoming());
        return Pattern.compile(pattern);
    }

    @Override
    public void action(FilterEvent event) {

        if (TabbyChat.getInstance().serverSettings.general.pmEnabled.get()) {
            // 0 = whole message, 1 = outgoing recipient, 2 = incoming recipient
            String player = event.matcher.group(1);
            // For when it's an incoming message.
            if (player == null) {
                player = event.matcher.group(2);
            }
            Channel dest = TabbyAPI.getAPI().getChat().getChannel(player, true);
            event.channels.add(dest);
        }
    }
}


<|start_filename|>src/main/java/mnm/mods/tabbychat/extra/spell/SpellingFormatter.java<|end_filename|>
package mnm.mods.tabbychat.extra.spell;

import java.util.Iterator;
import java.util.function.Function;

import com.swabunga.spell.event.SpellCheckEvent;

import mnm.mods.util.Color;
import mnm.mods.util.text.ITextBuilder;
import mnm.mods.util.text.TextBuilder;
import net.minecraft.util.text.ITextComponent;

public class SpellingFormatter implements Function<String, ITextComponent> {

    private final Iterator<SpellCheckEvent> spelling;

    private SpellCheckEvent event;
    private int totalLength;

    public SpellingFormatter(Spellcheck sp) {
        spelling = sp.iterator();
    }

    @Override
    public ITextComponent apply(String text) {
        if (text == null)
            return null;
        ITextBuilder b = new TextBuilder();
        int prev = 0;
        int length = totalLength;
        // save where we are at.
        totalLength += text.length();
        while (spelling.hasNext() || event != null) {
            if (event == null)
                event = spelling.next();
            int start = event.getWordContextPosition() - length;
            int end = start + event.getInvalidWord().length();

            if (start < 0) {
                // error started on previous line
                start = prev;
            }
            if (start > text.length()) {
                // no more errors on this line
                break;
            }
            b.text(text.substring(prev, start));

            if (end > text.length()) {
                // error goes to next line
                return b.text(text.substring(start)).underline(Color.RED).build();
            }
            b.text(text.substring(start, end)).underline(Color.RED);

            prev = end;
            event = null;
        }
        // no more errors.
        totalLength++;
        return b.text(text.substring(prev)).build();
    }

}


<|start_filename|>src/main/java/mnm/mods/tabbychat/api/gui/ReceivedChat.java<|end_filename|>
package mnm.mods.tabbychat.api.gui;

import net.minecraft.util.text.ITextComponent;

/**
 * This contains all the previously received chat from the current server.
 */
public interface ReceivedChat extends IGui {

    /**
     * Gets the {@link IChatComponent} that is at the specified coordinates.
     * Values should be relative to the screen, not the component.
     *
     * @param clickX The x position
     * @param clickY The y position
     * @return The chat component
     */
    ITextComponent getChatComponent(int clickX, int clickY);

    /**
     * Scrolls the chat the specified number of lines. Positive goes up,
     * negative goes down.
     *
     * @param scroll The number of lines to scroll
     */
    void scroll(int scroll);

    /**
     * Sets where the scroll position should be at. The value is adjusted for
     * invalid positions. Scroll must be >= 0 and <= the number of received
     * messages minus the height of the box.
     *
     * @param scroll The scroll pos
     */
    void setScrollPos(int scroll);

    /**
     * Gets the current scroll potision
     *
     * @return The scroll pos
     */
    int getScrollPos();

    /**
     * Resets the scroll position to 0. Same as calling {@code setScrollPos(0);}
     */
    void resetScroll();
}


<|start_filename|>src/main/java/mnm/mods/util/gui/GuiCheckbox.java<|end_filename|>
package mnm.mods.util.gui;

import com.google.common.eventbus.Subscribe;

import mnm.mods.util.Color;
import mnm.mods.util.Location;
import mnm.mods.util.gui.events.ActionPerformedEvent;
import net.minecraft.client.gui.Gui;

/**
 * A checkbox, representing a boolean input.
 *
 * @author Matthew
 */
public class GuiCheckbox extends GuiComponent implements IGuiInput<Boolean> {

    private boolean value;

    public GuiCheckbox() {
        this.setLocation(new Location(0, 0, 9, 9));
        setSecondaryColor(Color.of(0x99ffffa0));
    }

    @Override
    public void drawComponent(int mouseX, int mouseY) {
        int centerX = 4;
        int centerY = 4;
        int width = 9;
        int height = 9;

        int fore = getPrimaryColorProperty().getHex();
        int back = getSecondaryColorProperty().getHex();

        // Background
        Gui.drawRect(1, 0, width - 1, 1, back); // top
        Gui.drawRect(1, height - 1, width - 1, height, back); // bottom
        Gui.drawRect(0, 1, 1, height - 1, back); // left
        Gui.drawRect(width - 1, 1, width, height - 1, back); // right
        Gui.drawRect(1, 1, width - 1, height - 1, 0xff000000); // background

        if (getValue()) {
            // draw check
            Gui.drawRect(centerX - 2, centerY, centerX - 1, centerY + 1, fore);
            Gui.drawRect(centerX - 1, centerY + 1, centerX, centerY + 2, fore);
            Gui.drawRect(centerX, centerY + 2, centerX + 1, centerY + 3, fore);
            Gui.drawRect(centerX + 1, centerY + 2, centerX + 2, centerY, fore);
            Gui.drawRect(centerX + 2, centerY, centerX + 3, centerY - 2, fore);
            Gui.drawRect(centerX + 3, centerY - 2, centerX + 4, centerY - 4, fore);
        }
        super.drawComponent(mouseX, mouseY);
    }

    @Override
    public Boolean getValue() {
        return value;
    }

    @Override
    public void setValue(Boolean value) {
        this.value = value;
    }

    @Subscribe
    public void onClick(ActionPerformedEvent event) {
        setValue(!getValue());
    }
}
