diff --git a/src/main/java/tech/nevets/dlite/ChatWindow.java b/src/main/java/tech/nevets/dlite/ChatWindow.java index c008475..c7815af 100644 --- a/src/main/java/tech/nevets/dlite/ChatWindow.java +++ b/src/main/java/tech/nevets/dlite/ChatWindow.java @@ -6,21 +6,34 @@ package tech.nevets.dlite; import java.awt.*; import java.awt.event.*; -import java.io.IOException; import javax.swing.*; import javax.swing.plaf.BorderUIResource; import net.miginfocom.swing.*; +import tech.nevets.dlite.components.SmartScroller; /** * @author steven */ public class ChatWindow extends JPanel { + private static ChatWindow instance; + private boolean connected = false; private boolean darkModeBool = true; public ChatWindow() { initComponents(); + instance = this; + } + + public static ChatWindow getInstance() { + return instance; + } + + public void resetConnection() { + connected = false; + connStatus.setText("Disconnected: Socket Reset"); + connectBtn.setText("Connect"); } private void connectBtnMouseClicked(MouseEvent e) { @@ -40,7 +53,7 @@ public class ChatWindow extends JPanel { return; } chat.setText(""); - conn.setComponents(chat, scrollPane); + conn.setComponents(chat); connStatus.setText("Connected"); connectBtn.setText("Disconnect"); connected = true; @@ -111,6 +124,7 @@ public class ChatWindow extends JPanel { connectBtn = new JButton(); scrollPane = new JScrollPane(); chat = new JTextArea(); + new SmartScroller(scrollPane, SmartScroller.VERTICAL, SmartScroller.END); messageBox = new JTextField(); sendBtn = new JButton(); diff --git a/src/main/java/tech/nevets/dlite/ChatWindow.jfd b/src/main/java/tech/nevets/dlite/ChatWindow.jfd index fad3bf7..44d834f 100644 --- a/src/main/java/tech/nevets/dlite/ChatWindow.jfd +++ b/src/main/java/tech/nevets/dlite/ChatWindow.jfd @@ -57,6 +57,9 @@ new FormModel { "editable": false "font": new java.awt.Font( "Tahoma", 0, 11 ) "lineWrap": true + auxiliary() { + "JavaCodeGenerator.postCreateCode": "new SmartScroller(scrollPane, SmartScroller.VERTICAL, SmartScroller.END);" + } } ) }, new FormLayoutConstraints( class net.miginfocom.layout.CC ) { "value": "cell 0 2 3 1,grow" @@ -81,4 +84,3 @@ new FormModel { } ) } } - diff --git a/src/main/java/tech/nevets/dlite/Connection.java b/src/main/java/tech/nevets/dlite/Connection.java index 3f42b15..c51baa2 100644 --- a/src/main/java/tech/nevets/dlite/Connection.java +++ b/src/main/java/tech/nevets/dlite/Connection.java @@ -17,7 +17,6 @@ public class Connection { private String username; private JTextArea chat; - private JScrollBar scrollBar; public Connection(String username, String address) throws Exception { String[] addrSplit = address.split(":"); @@ -46,16 +45,16 @@ public class Connection { } public void sendMessage(String message) { - out.println(username + "> " + message); + out.println(message); } - public void setComponents(JTextArea chat, JScrollPane scrollBar) { + public void setComponents(JTextArea chat) { this.chat = chat; - this.scrollBar = scrollBar.getVerticalScrollBar(); new Thread(() -> { try { is.transferTo(new ChatWriter()); } catch (IOException ignored) {} + close(); }).start(); } @@ -65,6 +64,7 @@ public class Connection { } catch (IOException e) { e.printStackTrace(); } + ChatWindow.getInstance().resetConnection(); } private class ChatWriter extends OutputStream { @@ -79,7 +79,8 @@ public class Connection { @Override public void flush() { chat.append(buf); - scrollBar.setValue(scrollBar.getMaximum()); + Notifier.messageReceived(Main.getFrame()); + //Notifier.sendNotification(buf.substring(0, buf.indexOf(">") - 1), buf.substring(buf.indexOf(">"))); buf = ""; } } diff --git a/src/main/java/tech/nevets/dlite/Main.java b/src/main/java/tech/nevets/dlite/Main.java index e389a38..53cae62 100644 --- a/src/main/java/tech/nevets/dlite/Main.java +++ b/src/main/java/tech/nevets/dlite/Main.java @@ -21,7 +21,7 @@ public class Main { ChatWindow window = new ChatWindow(); frame = new JFrame(); frame.add(window); - frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.pack(); frame.setTitle("Discord Lite"); frame.setIconImage(Toolkit.getDefaultToolkit().createImage(Main.class.getResource("/icon.png"))); @@ -31,6 +31,10 @@ public class Main { frame.setVisible(true); } + public static JFrame getFrame() { + return frame; + } + public static void setDarkMode(boolean darkMode) { try { if (darkMode) { diff --git a/src/main/java/tech/nevets/dlite/Notifier.java b/src/main/java/tech/nevets/dlite/Notifier.java new file mode 100644 index 0000000..87fbb9a --- /dev/null +++ b/src/main/java/tech/nevets/dlite/Notifier.java @@ -0,0 +1,13 @@ +package tech.nevets.dlite; + +import javax.swing.*; +import java.awt.*; + +public class Notifier { + + public static void messageReceived(JFrame frame) { + if (!frame.isFocused()) { + Taskbar.getTaskbar().requestWindowUserAttention(frame); + } + } +} diff --git a/src/main/java/tech/nevets/dlite/components/SmartScroller.java b/src/main/java/tech/nevets/dlite/components/SmartScroller.java new file mode 100644 index 0000000..72dd94c --- /dev/null +++ b/src/main/java/tech/nevets/dlite/components/SmartScroller.java @@ -0,0 +1,174 @@ +package tech.nevets.dlite.components; + +import java.awt.Component; +import java.awt.event.*; +import javax.swing.*; +import javax.swing.text.*; + +/** + * The SmartScroller will attempt to keep the viewport positioned based on + * the users interaction with the scrollbar. The normal behaviour is to keep + * the viewport positioned to see new data as it is dynamically added. + * + * Assuming vertical scrolling and data is added to the bottom: + * + * - when the viewport is at the bottom and new data is added, + * then automatically scroll the viewport to the bottom + * - when the viewport is not at the bottom and new data is added, + * then do nothing with the viewport + * + * Assuming vertical scrolling and data is added to the top: + * + * - when the viewport is at the top and new data is added, + * then do nothing with the viewport + * - when the viewport is not at the top and new data is added, then adjust + * the viewport to the relative position it was at before the data was added + * + * Similiar logic would apply for horizontal scrolling. + */ +public class SmartScroller implements AdjustmentListener +{ + public final static int HORIZONTAL = 0; + public final static int VERTICAL = 1; + + public final static int START = 0; + public final static int END = 1; + + private int viewportPosition; + + private JScrollBar scrollBar; + private boolean adjustScrollBar = true; + + private int previousValue = -1; + private int previousMaximum = -1; + + /** + * Convenience constructor. + * Scroll direction is VERTICAL and viewport position is at the END. + * + * @param scrollPane the scroll pane to monitor + */ + public SmartScroller(JScrollPane scrollPane) + { + this(scrollPane, VERTICAL, END); + } + + /** + * Convenience constructor. + * Scroll direction is VERTICAL. + * + * @param scrollPane the scroll pane to monitor + * @param viewportPosition valid values are START and END + */ + public SmartScroller(JScrollPane scrollPane, int viewportPosition) + { + this(scrollPane, VERTICAL, viewportPosition); + } + + /** + * Specify how the SmartScroller will function. + * + * @param scrollPane the scroll pane to monitor + * @param scrollDirection indicates which JScrollBar to monitor. + * Valid values are HORIZONTAL and VERTICAL. + * @param viewportPosition indicates where the viewport will normally be + * positioned as data is added. + * Valid values are START and END + */ + public SmartScroller(JScrollPane scrollPane, int scrollDirection, int viewportPosition) + { + if (scrollDirection != HORIZONTAL + && scrollDirection != VERTICAL) + throw new IllegalArgumentException("invalid scroll direction specified"); + + if (viewportPosition != START + && viewportPosition != END) + throw new IllegalArgumentException("invalid viewport position specified"); + + this.viewportPosition = viewportPosition; + + if (scrollDirection == HORIZONTAL) + scrollBar = scrollPane.getHorizontalScrollBar(); + else + scrollBar = scrollPane.getVerticalScrollBar(); + + scrollBar.addAdjustmentListener( this ); + + // Turn off automatic scrolling for text components + + Component view = scrollPane.getViewport().getView(); + + if (view instanceof JTextComponent) + { + JTextComponent textComponent = (JTextComponent)view; + DefaultCaret caret = (DefaultCaret)textComponent.getCaret(); + caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE); + } + } + + @Override + public void adjustmentValueChanged(final AdjustmentEvent e) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + checkScrollBar(e); + } + }); + } + + /* + * Analyze every adjustment event to determine when the viewport + * needs to be repositioned. + */ + private void checkScrollBar(AdjustmentEvent e) + { + // The scroll bar listModel contains information needed to determine + // whether the viewport should be repositioned or not. + + JScrollBar scrollBar = (JScrollBar)e.getSource(); + BoundedRangeModel listModel = scrollBar.getModel(); + int value = listModel.getValue(); + int extent = listModel.getExtent(); + int maximum = listModel.getMaximum(); + + boolean valueChanged = previousValue != value; + boolean maximumChanged = previousMaximum != maximum; + + // Check if the user has manually repositioned the scrollbar + + if (valueChanged && !maximumChanged) + { + if (viewportPosition == START) + adjustScrollBar = value != 0; + else + adjustScrollBar = value + extent >= maximum; + } + + // Reset the "value" so we can reposition the viewport and + // distinguish between a user scroll and a program scroll. + // (ie. valueChanged will be false on a program scroll) + + if (adjustScrollBar && viewportPosition == END) + { + // Scroll the viewport to the end. + scrollBar.removeAdjustmentListener( this ); + value = maximum - extent; + scrollBar.setValue( value ); + scrollBar.addAdjustmentListener( this ); + } + + if (adjustScrollBar && viewportPosition == START) + { + // Keep the viewport at the same relative viewportPosition + scrollBar.removeAdjustmentListener( this ); + value = value + maximum - previousMaximum; + scrollBar.setValue( value ); + scrollBar.addAdjustmentListener( this ); + } + + previousValue = value; + previousMaximum = maximum; + } +} \ No newline at end of file diff --git a/src/main/resources/icon-old.ico b/src/main/resources/icon-old.ico new file mode 100644 index 0000000..194d307 Binary files /dev/null and b/src/main/resources/icon-old.ico differ diff --git a/src/main/resources/icon-old.png b/src/main/resources/icon-old.png new file mode 100644 index 0000000..36c8fc8 Binary files /dev/null and b/src/main/resources/icon-old.png differ diff --git a/src/main/resources/icon.ico b/src/main/resources/icon.ico index 194d307..b5870d6 100644 Binary files a/src/main/resources/icon.ico and b/src/main/resources/icon.ico differ diff --git a/src/main/resources/icon.png b/src/main/resources/icon.png index 36c8fc8..7090d35 100644 Binary files a/src/main/resources/icon.png and b/src/main/resources/icon.png differ