diff --git a/gui/griffon-app/views/com/muwire/gui/ChatRoomView.groovy b/gui/griffon-app/views/com/muwire/gui/ChatRoomView.groovy index 95da964a..106d96d0 100644 --- a/gui/griffon-app/views/com/muwire/gui/ChatRoomView.groovy +++ b/gui/griffon-app/views/com/muwire/gui/ChatRoomView.groovy @@ -32,6 +32,7 @@ class ChatRoomView { def parent def sayField def roomTextArea + def textScrollPane def membersTable def lastMembersTableSortEvent @@ -42,7 +43,7 @@ class ChatRoomView { borderLayout() panel(constraints : BorderLayout.CENTER) { gridLayout(rows : 1, cols : 1) - scrollPane { + textScrollPane = scrollPane { roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true) } } @@ -72,7 +73,7 @@ class ChatRoomView { } panel { gridLayout(rows : 1, cols : 1) - scrollPane { + textScrollPane = scrollPane { roomTextArea = textArea(editable : false, lineWrap : true, wrapStyleWord : true) } } @@ -87,6 +88,8 @@ class ChatRoomView { } } + + SmartScroller smartScroller = new SmartScroller(textScrollPane) } void mvcGroupInit(Map args) { diff --git a/gui/src/main/java/com/muwire/gui/SmartScroller.java b/gui/src/main/java/com/muwire/gui/SmartScroller.java new file mode 100644 index 00000000..7ebae19c --- /dev/null +++ b/gui/src/main/java/com/muwire/gui/SmartScroller.java @@ -0,0 +1,174 @@ +package com.muwire.gui; + +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; + } +}