diff --git a/.gitignore b/.gitignore index 6ea54cb..13b8e83 100644 --- a/.gitignore +++ b/.gitignore @@ -84,10 +84,10 @@ Logs/* .vscode/* out/* lib/* -themes/* -src/urChatBasic/backend/JoinClassLoader.java -src/urChatBasic/frontend/utils/UIManagerDefaults.java +themes release +test-output +report # not tested -build/tag-build-win.ps1 +build/tag-build-win.ps1 \ No newline at end of file diff --git a/README.md b/README.md index db5abb9..788e0fb 100644 --- a/README.md +++ b/README.md @@ -5,14 +5,39 @@ urChat is a Java based IRC Client designed around simplicity and minimal resourc Contributions ====== -**Currently targeting ![Milestone v0.4.0](https://github.com/matty-r/urChat/milestone/3)** +**Currently targeting ![Milestone v0.4.0](https://github.com/matty-r/urChat/milestone/3)** If you would like to assist in the development of urChat take a look at the Issues associated with the project. Please let me know if you wish to tackle a certain issue. +Test/Code Coverage Dependencies +====== +Dependencies required only for running the tests: + +Create a **lib/test** directory with the following files: + +* [Junit 4](https://repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.jar) +* [Hamcrest Core](https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar) +* [TestNG 7.8.0](https://repo1.maven.org/maven2/org/testng/testng/7.8.0/testng-7.8.0.jar) +* [JCommander](https://repo1.maven.org/maven2/com/beust/jcommander/1.82/jcommander-1.82.jar) +* [SLF4J](https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9.jar) + +Extract jacocoagent.jar and jacococli.jar into the **lib/coverage** directory + +* [Jacoco](https://search.maven.org/remotecontent?filepath=org/jacoco/jacoco/0.8.11/jacoco-0.8.11.zip) + Usage ====== Ensure you've got Java 17 available on your system, download and run the latest JAR release (https://github.com/matty-r/urChat/releases). If you'd like to try out the Theme functionality, create a 'themes' directory next to the urChat.jar and download the FlatLAF.jar release and place within that directory. The theme can be selected under the client options page. +Test Usage +====== + +Using the testng.xml - must be in the same directory as urchat.jar +* java -cp "urTestRunner.jar" org.testng.TestNG testng.xml + +Without testng.xml +* java -jar urTestRunner.jar + Screenshots ====== diff --git a/build/test-build-linux.sh b/build/test-build-linux.sh new file mode 100755 index 0000000..a6dcf54 --- /dev/null +++ b/build/test-build-linux.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Save the current directory to a variable +initial_dir=$(pwd) + +# Clone the repository into a temporary directory +temp_dir=$(mktemp -d) +git clone . "$temp_dir" +cd "$temp_dir" + +# Compile the Java files for the main jar +find src -name "*.java" -exec javac -d "bin" {} + + +cd "bin" + +# Copy the images directory +cp -r "$initial_dir/src/images" "." + +# Create a manifest file specifying the main class +echo "Main-Class: urChatBasic.frontend.DriverGUI" > ../manifest.txt + +# Create the JAR file with the manifest and compiled class files, includes the lib and images directory in the created JAR file +jar -cfm "urchat.jar" ../manifest.txt . + +# Delete all the files not needed to compile the test runner +rm -rf "images" +rm -rf "urChatBasic" + +# Copy the lib directory +cp -r "$initial_dir/lib" "$temp_dir/" + +# Copy the test libs +cp -r "$temp_dir/lib/test/" "$temp_dir/bin/" + +# Compile the Java files for the urTestRunner, using the urchat.jar as a source of the lib files +find ../tests -name "*.java" -exec javac -cp "urchat.jar:test/*" -d . {} + + +# Extract the test libs to be included in urTestRunner.jar +mkdir -p "$temp_dir/extracted_libs" +cd "$temp_dir/extracted_libs" + +for file in "$temp_dir"/lib/test/*.jar; do + jar xf "$file" +done + +cd "$temp_dir/bin" + +# Move the main.jar back into the temp dir (we don't want it included in urTestRunner) +mv "urchat.jar" "$temp_dir" + +# Delete the test libs +rm -rf "test" + +# Create a manifest file for urTestRunner +echo "Main-Class: URTestRunner" > ../testmanifest.txt +echo "Class-Path: urchat.jar test/*" >> ../testmanifest.txt + +# Compile to urTestRunner.jar using the testmanifest.txt, the contents of the current directory (/bin) plus the extracted_libs +jar -cfm "urTestRunner.jar" "$temp_dir/testmanifest.txt" . -C "$temp_dir/extracted_libs/" . + +mv "urTestRunner.jar" "$temp_dir" + +cd "$temp_dir" + +mkdir -p "report" + +# run with jacoco agent to build coverage.exec +java -javaagent:lib/coverage/jacocoagent.jar=destfile=coverage.exec -cp "urchat.jar:urTestRunner.jar" org.testng.TestNG ./build/testng_release.xml + +# build html report pointing to the source .java files +java -jar lib/coverage/jacococli.jar report coverage.exec --classfiles urchat.jar --html report --sourcefiles src/ + +# Move the JARs to the release directory +mkdir -p "$initial_dir/release" +mv "$temp_dir/urchat.jar" "$initial_dir/release" +mv "$temp_dir/urTestRunner.jar" "$initial_dir/release" + +# Jacoco Output +mv "report" "$initial_dir" + +# TestNG Output +mv "test-output" "$initial_dir" + +# Clean up the temporary directory +cd "$initial_dir" + +rm -rf "$temp_dir" diff --git a/build/testng.xml b/build/testng.xml new file mode 100644 index 0000000..226e038 --- /dev/null +++ b/build/testng.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/testng_release.xml b/build/testng_release.xml new file mode 100644 index 0000000..a90cd1f --- /dev/null +++ b/build/testng_release.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/urChatBasic/UIManagerDefaults.java b/src/urChatBasic/UIManagerDefaults.java new file mode 100644 index 0000000..0ec91a2 --- /dev/null +++ b/src/urChatBasic/UIManagerDefaults.java @@ -0,0 +1,747 @@ +package urChatBasic; +/* + * This programs uses the information found in the UIManager + * to create a table of key/value pairs for each Swing component. + */ + +import java.awt.*; +import java.awt.event.*; +import java.awt.image.*; +import java.util.*; +import javax.swing.*; +import javax.swing.border.*; +import javax.swing.plaf.*; +import javax.swing.table.*; + +public class UIManagerDefaults implements ActionListener, ItemListener +{ + private final static String[] COLUMN_NAMES = {"Key", "Value", "Sample"}; + private static String selectedItem; + + private JComponent contentPane; + private JMenuBar menuBar; + private JComboBox comboBox; + private JRadioButton byComponent; + private JTable table; + private TreeMap> items; + private HashMap models; + + /* + * Constructor + */ + public UIManagerDefaults() + { + items = new TreeMap>(); + models = new HashMap(); + + contentPane = new JPanel( new BorderLayout() ); + contentPane.add(buildNorthComponent(), BorderLayout.NORTH); + contentPane.add(buildCenterComponent(), BorderLayout.CENTER); + + resetComponents(); + } + + /* + * The content pane should be added to a high level container + */ + public JComponent getContentPane() + { + return contentPane; + } + + /* + * A menu can also be added which provides the ability to switch + * between different LAF's. + */ + public JMenuBar getMenuBar() + { + if (menuBar == null) + menuBar = createMenuBar(); + + return menuBar; + } + + /* + * This panel is added to the North of the content pane + */ + private JComponent buildNorthComponent() + { + comboBox = new JComboBox(); + + JLabel label = new JLabel("Select Item:"); + label.setDisplayedMnemonic('S'); + label.setLabelFor( comboBox ); + + byComponent = new JRadioButton("By Component", true); + byComponent.setMnemonic('C'); + byComponent.addActionListener( this ); + + JRadioButton byValueType = new JRadioButton("By Value Type"); + byValueType.setMnemonic('V'); + byValueType.addActionListener( this ); + + ButtonGroup group = new ButtonGroup(); + group.add(byComponent); + group.add(byValueType); + + JPanel panel = new JPanel(); + panel.setBorder( new EmptyBorder(15, 0, 15, 0) ); + panel.add( label ); + panel.add( comboBox ); + panel.add( byComponent ); + panel.add( byValueType ); + return panel; + } + + /* + * This panel is added to the Center of the content pane + */ + private JComponent buildCenterComponent() + { + DefaultTableModel model = new DefaultTableModel(COLUMN_NAMES, 0); + table = new JTable(model); + table.setAutoCreateColumnsFromModel( false ); + table.getColumnModel().getColumn(0).setPreferredWidth(250); + table.getColumnModel().getColumn(1).setPreferredWidth(500); + table.getColumnModel().getColumn(2).setPreferredWidth(100); + table.getColumnModel().getColumn(2).setCellRenderer( new SampleRenderer() ); + Dimension d = table.getPreferredSize(); + d.height = 350; + table.setPreferredScrollableViewportSize( d ); + + return new JScrollPane( table ); + } + + /* + * When the LAF is changed we need to reset the content pane + */ + public void resetComponents() + { + items.clear(); + models.clear(); + ((DefaultTableModel)table.getModel()).setRowCount(0); + + buildItemsMap(); + + Vector comboBoxItems = new Vector(50); + Iterator keys = items.keySet().iterator(); + + while (keys.hasNext()) + { + Object key = keys.next(); + comboBoxItems.add( (String)key ); + } + + comboBox.removeItemListener( this ); + comboBox.setModel( new DefaultComboBoxModel( comboBoxItems ) ); + comboBox.setSelectedIndex(-1); + comboBox.addItemListener( this ); + comboBox.requestFocusInWindow(); + + if (selectedItem != null) + comboBox.setSelectedItem(selectedItem); + } + + /* + * The item map will contain items for each component or + * items for each attribute type. + */ + private TreeMap buildItemsMap() + { + UIDefaults defaults = UIManager.getLookAndFeelDefaults(); + + // Build of Map of items and a Map of attributes for each item + + for ( Enumeration enumm = defaults.keys(); enumm.hasMoreElements(); ) + { + Object key = enumm.nextElement(); + Object value = defaults.get( key ); + + String itemName = getItemName(key.toString(), value); + + if (itemName == null) continue; + + // Get the attribute map for this componenent, or + // create a map when one is not found + + TreeMap attributeMap = items.get( itemName ); + + if (attributeMap == null) + { + attributeMap = new TreeMap(); + items.put(itemName, attributeMap); + } + + // Add the attribute to the map for this componenent + + attributeMap.put(key.toString(), value ); + } + + return items; + } + + /* + * Parse the key to determine the item name to use + */ + private String getItemName(String key, Object value) + { + // Seems like this is an old check required for JDK1.4.2 + + if (key.startsWith("class") || key.startsWith("javax")) + return null; + + if (byComponent.isSelected()) + return getComponentName(key, value); + else + return getValueName(key, value); + } + + private String getComponentName(String key, Object value) + { + // The key is of the form: + // "componentName.componentProperty", or + // "componentNameUI", or + // "someOtherString" + + String componentName; + + int pos = componentNameEndOffset( key ); + + if (pos != -1) + { + componentName = key.substring( 0, pos ); + } + else if (key.endsWith( "UI" ) ) + { + componentName = key.substring( 0, key.length() - 2 ); + } + else if (value instanceof ColorUIResource) + { + componentName = "System Colors"; + } + else + { + componentName = "Miscellaneous"; + } + + // Fix inconsistency + + if (componentName.equals("Checkbox")) + { + componentName = "CheckBox"; + } + + return componentName; + } + + private int componentNameEndOffset(String key) + { + // Handle Nimbus properties first + + // "ComboBox.scrollPane", "Table.editor" and "Tree.cellEditor" + // have different format even within the Nimbus properties. + // (the component name is specified in quotes) + + if (key.startsWith("\"")) + return key.indexOf("\"", 1) + 1; + + int pos = key.indexOf( ":" ); + + if (pos != -1) + return pos; + + pos = key.indexOf( "[" ); + + if (pos != -1) + return pos; + + // Handle normal properties + + return key.indexOf( "." ); + } + + private String getValueName(String key, Object value) + { + if (value instanceof Icon) + return "Icon"; + else if (value instanceof Font) + return "Font"; + else if (value instanceof Border) + return "Border"; + else if (value instanceof Color) + return "Color"; + else if (value instanceof Insets) + return "Insets"; + else if (value instanceof Boolean) + return "Boolean"; + else if (value instanceof Dimension) + return "Dimension"; + else if (value instanceof Number) + return "Number"; + else if (value instanceof Painter) + return "Painter"; + else if (key.endsWith("UI")) + return "UI"; + else if (key.endsWith("InputMap")) + return "InputMap"; + else if (key.endsWith("RightToLeft")) + return "InputMap"; + else if (key.endsWith("radient")) + return "Gradient"; + else + { + return "The Rest"; + } + } + + /** + * Create menu bar + */ + private JMenuBar createMenuBar() + { + JMenuBar menuBar = new JMenuBar(); + + menuBar.add( createFileMenu() ); + menuBar.add( createLAFMenu() ); + + return menuBar; + } + + /** + * Create menu items for the Application menu + */ + private JMenu createFileMenu() + { + JMenu menu = new JMenu("Application"); + menu.setMnemonic('A'); + + menu.addSeparator(); + menu.add( new ExitAction() ); + + return menu; + } + + /** + * Create menu items for the Look & Feel menu + */ + private JMenu createLAFMenu() + { + ButtonGroup bg = new ButtonGroup(); + + JMenu menu = new JMenu("Look & Feel"); + menu.setMnemonic('L'); + + String lafId = UIManager.getLookAndFeel().getID(); + UIManager.LookAndFeelInfo[] lafInfo = UIManager.getInstalledLookAndFeels(); + + for (int i = 0; i < lafInfo.length; i++) + { + String laf = lafInfo[i].getClassName(); + String name= lafInfo[i].getName(); + + Action action = new ChangeLookAndFeelAction(this, laf, name); + JRadioButtonMenuItem mi = new JRadioButtonMenuItem( action ); + menu.add( mi ); + bg.add( mi ); + + if (name.equals(lafId)) + { + mi.setSelected(true); + } + } + + return menu; + } + + /* + * Implement the ActionListener interface + */ + public void actionPerformed(ActionEvent e) + { + selectedItem = null; + resetComponents(); + comboBox.requestFocusInWindow(); + } + + /* + * Implement the ItemListener interface + */ + public void itemStateChanged(ItemEvent e) + { + String itemName = (String)e.getItem(); + changeTableModel( itemName ); + updateRowHeights(); + selectedItem = itemName; + } + + /* + * Change the TabelModel in the table for the selected item + */ + private void changeTableModel(String itemName) + { + // The model has been created previously so just use it + + DefaultTableModel model = models.get( itemName ); + + if (model != null) + { + table.setModel( model ); + return; + } + + // Create a new model for the requested item + // and add the attributes of the item to the model + + model = new DefaultTableModel(COLUMN_NAMES, 0); + Map attributes = (Map)items.get( itemName ); + + Iterator ai = attributes.keySet().iterator(); + + while (ai.hasNext()) + { + String attribute = (String)ai.next(); + Object value = attributes.get(attribute); + + Vector row = new Vector(3); + row.add(attribute); + + if (value != null) + { + row.add( value.toString() ); + + if (value instanceof Icon) + value = new SafeIcon( (Icon)value ); + + row.add( value ); + } + else + { + row.add( "null" ); + row.add( "" ); + } + + model.addRow( row ); + } + + table.setModel( model ); + models.put(itemName, model); + } + + /* + * Some rows containing icons, may need to be sized taller to fully + * display the icon. + */ + private void updateRowHeights() + { + for (int row = 0; row < table.getRowCount(); row++) + { + int rowHeight = table.getRowHeight(); + + for (int column = 0; column < table.getColumnCount(); column++) + { + Component comp = table.prepareRenderer(table.getCellRenderer(row, column), row, column); + rowHeight = Math.max(rowHeight, comp.getPreferredSize().height); + } + + table.setRowHeight(row, rowHeight); + } + } + + /** + * Thanks to Jeanette for the use of this code found at: + * + * https://jdnc-incubator.dev.java.net/source/browse/jdnc-incubator/src/kleopatra/java/org/jdesktop/swingx/renderer/UIPropertiesViewer.java?rev=1.2&view=markup + * + * Some ui-icons misbehave in that they unconditionally class-cast to the + * component type they are mostly painted on. Consequently they blow up if + * we are trying to paint them anywhere else (f.i. in a renderer). + * + * This Icon is an adaption of a cool trick by Darryl Burke found at + * http://tips4java.wordpress.com/2008/12/18/icon-table-cell-renderer + * + * The base idea is to instantiate a component of the type expected by the icon, + * let it paint into the graphics of a bufferedImage and create an ImageIcon from it. + * In subsequent calls the ImageIcon is used. + * + */ + public static class SafeIcon implements Icon + { + private Icon wrappee; + private Icon standIn; + + public SafeIcon(Icon wrappee) + { + this.wrappee = wrappee; + } + + @Override + public int getIconHeight() + { + return wrappee.getIconHeight(); + } + + @Override + public int getIconWidth() + { + return wrappee.getIconWidth(); + } + + @Override + public void paintIcon(Component c, Graphics g, int x, int y) + { + if (standIn == this) + { + paintFallback(c, g, x, y); + } + else if (standIn != null) + { + standIn.paintIcon(c, g, x, y); + } + else + { + try + { + wrappee.paintIcon(c, g, x, y); + } + catch (ClassCastException e) + { + createStandIn(e, x, y); + standIn.paintIcon(c, g, x, y); + } + } + } + + /** + * @param e + */ + private void createStandIn(ClassCastException e, int x, int y) + { + try + { + Class clazz = getClass(e); + JComponent standInComponent = getSubstitute(clazz); + standIn = createImageIcon(standInComponent, x, y); + } + catch (Exception e1) + { + // something went wrong - fallback to this painting + standIn = this; + } + } + + private Icon createImageIcon(JComponent standInComponent, int x, int y) + { + BufferedImage image = new BufferedImage(getIconWidth(), getIconHeight(), BufferedImage.TYPE_INT_ARGB); + Graphics g = image.createGraphics(); + try + { + wrappee.paintIcon(standInComponent, g, 0, 0); + return new ImageIcon(image); + } + finally + { + g.dispose(); + } + } + + /** + * @param clazz + * @throws IllegalAccessException + */ + private JComponent getSubstitute(Class clazz) throws IllegalAccessException + { + JComponent standInComponent; + + try + { + standInComponent = (JComponent) clazz.newInstance(); + } + catch (InstantiationException e) + { + standInComponent = new AbstractButton() {}; + ((AbstractButton) standInComponent).setModel(new DefaultButtonModel()); + } + return standInComponent; + } + + private Class getClass(ClassCastException e) throws ClassNotFoundException + { + String className = e.getMessage(); + className = className.substring(className.lastIndexOf(" ") + 1); + return Class.forName(className); + + } + + private void paintFallback(Component c, Graphics g, int x, int y) + { + g.drawRect(x, y, getIconWidth(), getIconHeight()); + g.drawLine(x, y, x + getIconWidth(), y + getIconHeight()); + g.drawLine(x + getIconWidth(), y, x, y + getIconHeight()); + } + + } + + + /* + * Render the value based on its class. + */ + class SampleRenderer extends JLabel implements TableCellRenderer + { + public SampleRenderer() + { + super(); + setHorizontalAlignment( SwingConstants.CENTER ); + setOpaque(true); + } + + public Component getTableCellRendererComponent( + JTable table, Object sample, boolean isSelected, boolean hasFocus, int row, int column) + { + setBackground( null ); + setBorder( null ); + setIcon( null ); + setText( "" ); + + if ( sample instanceof Color ) + { + setBackground( (Color)sample ); + } + else if ( sample instanceof Border ) + { + setBorder( (Border)sample ); + } + else if ( sample instanceof Font ) + { + setText( "Sample" ); + setFont( (Font)sample ); + } + else if ( sample instanceof Icon ) + { + setIcon( (Icon)sample ); + } + + return this; + } + + /* + * Some icons are painted using inner classes and are not meant to be + * shared by other items. This code will catch the + * ClassCastException that is thrown. + */ + public void paint(Graphics g) + { + try + { + super.paint(g); + } + catch(Exception e) + { +// System.out.println(e); +// System.out.println(e.getStackTrace()[0]); + } + } + } + + /* + * Change the LAF and recreate the UIManagerDefaults so that the properties + * of the new LAF are correctly displayed. + */ + class ChangeLookAndFeelAction extends AbstractAction + { + private UIManagerDefaults defaults; + private String laf; + + protected ChangeLookAndFeelAction(UIManagerDefaults defaults, String laf, String name) + { + this.defaults = defaults; + this.laf = laf; + putValue(Action.NAME, name); + putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME)); + } + + public void actionPerformed(ActionEvent e) + { + try + { + UIManager.setLookAndFeel( laf ); + defaults.resetComponents(); + + JMenuItem mi = (JMenuItem) e.getSource(); + JPopupMenu popup = (JPopupMenu) mi.getParent(); + JRootPane rootPane = SwingUtilities.getRootPane( popup.getInvoker() ); + SwingUtilities.updateComponentTreeUI( rootPane ); + + // Use custom decorations when supported by the LAF + + JFrame frame = (JFrame)SwingUtilities.windowForComponent(rootPane); + frame.dispose(); + + if (UIManager.getLookAndFeel().getSupportsWindowDecorations()) + { + frame.setUndecorated(true); + frame.getRootPane().setWindowDecorationStyle(JRootPane.FRAME); + } + else + { + frame.setUndecorated(false); + } + + frame.setVisible(true); + } + catch (Exception ex) + { + System.out.println("Failed loading L&F: " + laf); + System.out.println(ex); + } + } + } + + /* + * Close the frame + */ + class ExitAction extends AbstractAction + { + public ExitAction() + { + putValue(Action.NAME, "Exit"); + putValue(Action.SHORT_DESCRIPTION, getValue(Action.NAME)); + putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_X)); + } + + public void actionPerformed(ActionEvent e) + { + System.exit(0); + } + } + + /* + * Build a GUI using the content pane and menu bar of UIManagerDefaults + */ + public static void createAndShowGUI() + { + UIManagerDefaults application = new UIManagerDefaults(); + + JFrame.setDefaultLookAndFeelDecorated(true); + JFrame frame = new JFrame("UIManager Defaults"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setJMenuBar( application.getMenuBar() ); + frame.getContentPane().add(application.getContentPane()); + frame.pack(); + frame.setLocationRelativeTo( null ); + frame.setVisible( true ); + } + + /* + * UIManagerDefaults Main. Called only if we're an application. + */ + public static void main(String[] args) + { + SwingUtilities.invokeLater(new Runnable() + { + public void run() + { + createAndShowGUI(); + } + }); + } +} diff --git a/src/urChatBasic/backend/Connection.java b/src/urChatBasic/backend/Connection.java index f7d35e8..e3ade07 100644 --- a/src/urChatBasic/backend/Connection.java +++ b/src/urChatBasic/backend/Connection.java @@ -180,17 +180,17 @@ public void sendClientText(String clientText, String fromChannel) throws IOExcep outText = "PRIVMSG " + tempTextArray[1] + " :" + clientText.replace("/msg " + tempTextArray[1] + " ", "") + "\r\n"; - // if (clientText.toLowerCase().startsWith("/msg nickserv identify")) - // { - // clientText = "*** HIDDEN NICKSERV IDENTIFY ***"; - // } - // server.printPrivateText(tempTextArray[1], clientText.replace("/msg " + tempTextArray[1] + " ", ""),getServer().getNick()); String msgPrefix = ":"+ getServer().getNick()+"!~"+ getServer().getNick()+"@urchatclient"; - clientMessage = messageHandler.new Message(msgPrefix + " " +outText); - gui.setCurrentTab(tempTextArray[1]); + if(!tempTextArray[1].equalsIgnoreCase(getServer().getNick())) + { + clientMessage = messageHandler.new Message(msgPrefix + " " +outText); + } + + // TODO: Set current tab to this new priv tab + // gui.setCurrentTab(tempTextArray[1]); } else if (clientText.toLowerCase().startsWith("/whois")) { outText = "WHOIS " + tempTextArray[1] + "\r\n"; diff --git a/src/urChatBasic/backend/LookAndFeelLoader.java b/src/urChatBasic/backend/LookAndFeelLoader.java index 9dcc015..8d1e24d 100644 --- a/src/urChatBasic/backend/LookAndFeelLoader.java +++ b/src/urChatBasic/backend/LookAndFeelLoader.java @@ -14,7 +14,7 @@ import javax.swing.UIManager; import urChatBasic.base.Constants; -public class LookAndFeelLoader { +public class LookAndFeelLoader { public URLClassLoader cl; public LookAndFeelLoader(ClassLoader parentLoader) throws IOException { @@ -71,12 +71,12 @@ public LookAndFeelLoader(ClassLoader parentLoader) throws IOException { try { UIManager.installLookAndFeel(classShortName, className); } catch (Exception installEx) { - System.out.println(installEx.getMessage()); + Constants.LOGGER.log(Level.WARNING, installEx.getMessage()); } } } } catch (NoClassDefFoundError | Exception classEx) { - System.out.println(classEx.getMessage()); + Constants.LOGGER.log(Level.WARNING, classEx.getMessage()); } } } diff --git a/src/urChatBasic/backend/MessageHandler.java b/src/urChatBasic/backend/MessageHandler.java index 3d3c289..805faac 100644 --- a/src/urChatBasic/backend/MessageHandler.java +++ b/src/urChatBasic/backend/MessageHandler.java @@ -140,7 +140,7 @@ private void addRanges() rangeIDs.add(new IDRange(412, 415, new BadPrivateMessage())); rangeIDs.add(new IDRange(371, 376, new GeneralServerMessage())); rangeIDs.add(new IDRange(251, 256, new GeneralServerMessage())); - rangeIDs.add(new IDRange(471, 475, new JoinFailureMessage())); + rangeIDs.add(new IDRange(471, 477, new JoinFailureMessage())); } private void addSingles() @@ -149,7 +149,7 @@ private void addSingles() singleIDs.add(new IDSingle(353, new UsersListMessage())); singleIDs.add(new IDSingle(322, new CommandResponseMessage())); singleIDs.add(new IDSingle((new int[] {311, 319, 312, 317, 318, 301, 671, 330, 338, 378}), new WhoIsMessage())); - singleIDs.add(new IDSingle((new int[] {004, 265, 266, 250, 422, 477, 331, 900}), new GeneralServerMessage())); + singleIDs.add(new IDSingle((new int[] {004, 265, 266, 250, 422, 331, 900}), new GeneralServerMessage())); singleIDs.add(new IDSingle(366, new GeneralChannelMessage())); singleIDs.add(new IDSingle((new int[] {432, 433, 451}), new InvalidNickMessage())); singleIDs.add(new IDSingle((new int[] {401, 403}), new NoSuchChannelMessage())); @@ -532,6 +532,7 @@ public class JoinFailureMessage implements MessageBase public void messageExec(Message myMessage) { printServerText(myMessage.body); + serverBase.callForAttention(); } } diff --git a/src/urChatBasic/backend/utils/URPreferencesUtil.java b/src/urChatBasic/backend/utils/URPreferencesUtil.java new file mode 100644 index 0000000..5751bdd --- /dev/null +++ b/src/urChatBasic/backend/utils/URPreferencesUtil.java @@ -0,0 +1,197 @@ +package urChatBasic.backend.utils; + +import java.awt.Color; +import java.awt.Font; +import java.awt.font.TextAttribute; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.logging.Level; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; +import javax.swing.UIManager; +import urChatBasic.base.Constants; +import urChatBasic.frontend.DriverGUI; +import urChatBasic.frontend.utils.URColour; + +public class URPreferencesUtil { + + /** + * Uses the defaultFont for the returned font if there is no font saved. + * TODO: This should really go through an enum of font keys and apply them all instead? + * @param defaultFont + * @param settingsPath + * @return + */ + public static Font loadStyleFont(Font defaultFont, Preferences settingsPath) + { + // Font savedFont = defaultFont; + // int savedFontBoldItalic = 0; + + // if (settingsPath.getBoolean(Constants.KEY_FONT_BOLD, defaultFont.isBold())) + // savedFontBoldItalic = Font.BOLD; + // if (settingsPath.getBoolean(Constants.KEY_FONT_ITALIC, defaultFont.isItalic())) + // savedFontBoldItalic |= Font.ITALIC; + + // savedFont = new Font(settingsPath.get(Constants.KEY_FONT_FAMILY, defaultFont.getFamily()), + // savedFontBoldItalic, settingsPath.getInt(Constants.KEY_FONT_SIZE, defaultFont.getSize())); + + // return savedFont; + Font savedFont = defaultFont; + + Map fontMap = new Hashtable(); + + fontMap.put(TextAttribute.WEIGHT, settingsPath.getBoolean(Constants.KEY_FONT_BOLD, defaultFont.isBold()) ? TextAttribute.WEIGHT_BOLD : TextAttribute.WEIGHT_REGULAR); + fontMap.put(TextAttribute.POSTURE, settingsPath.getBoolean(Constants.KEY_FONT_ITALIC, defaultFont.isItalic()) ? TextAttribute.POSTURE_OBLIQUE : TextAttribute.POSTURE_REGULAR); + fontMap.put(TextAttribute.UNDERLINE, settingsPath.getBoolean(Constants.KEY_FONT_UNDERLINE, URStyle.isUnderline(defaultFont)) ? TextAttribute.UNDERLINE_ON : -1); + + savedFont = new Font(settingsPath.get(Constants.KEY_FONT_FAMILY, defaultFont.getFamily()), Font.PLAIN, settingsPath.getInt(Constants.KEY_FONT_SIZE, defaultFont.getSize())) + .deriveFont(fontMap); + + return savedFont; + } + + /** + * Uses the defaultStyle for the returned colours if there aren't any colours saved. + * @param defaultStyle + * @param settingsPath + * @return + */ + public static Map loadStyleColours(URStyle defaultStyle, Preferences settingsPath) + { + Map colourMap = new HashMap(); + colourMap.put(Constants.KEY_FONT_FOREGROUND, defaultStyle.getForeground()); + colourMap.put(Constants.KEY_FONT_BACKGROUND, defaultStyle.getBackground()); + + String loadedForeground = settingsPath.get(Constants.KEY_FONT_FOREGROUND, URColour.hexEncode(defaultStyle.getForeground())); + String loadedBackground = settingsPath.get(Constants.KEY_FONT_BACKGROUND, URColour.hexEncode(defaultStyle.getBackground())); + + colourMap.replace(Constants.KEY_FONT_FOREGROUND, URColour.hexDecode(loadedForeground)); + colourMap.replace(Constants.KEY_FONT_BACKGROUND, URColour.hexDecode(loadedBackground)); + + return colourMap; + } + + /** + * TODO: Add a fallbackSettingsPath in order to have somewhere else to load the targetStyle from if the baseSettingsPath doesn't exist. + * @param targetStyle + * @param baseSettingsPath + * @return + */ + public static URStyle loadStyle(final URStyle targetStyle, Preferences baseSettingsPath) + { + // targetStyle = targetStyle.clone(); + // Default to the profile path node + Preferences stylePrefPath = baseSettingsPath; + if(targetStyle.getAttribute("name") == null) + targetStyle.addAttribute("name", ""); + + try + { + if(baseSettingsPath.nodeExists(targetStyle.getAttribute("name").toString())) + stylePrefPath = baseSettingsPath.node(targetStyle.getAttribute("name").toString()); + else if (DriverGUI.gui != null) + stylePrefPath = DriverGUI.gui.getProfilePath().node(targetStyle.getAttribute("name").toString()); + else + stylePrefPath = baseSettingsPath.node(targetStyle.getAttribute("name").toString()); + } catch (BackingStoreException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + Constants.LOGGER.log(Level.INFO, "Load Style Path: " + stylePrefPath.toString()); + Font loadedFont = loadStyleFont(targetStyle.getFont(), stylePrefPath); + Map loadedColours = loadStyleColours(targetStyle, stylePrefPath); + + targetStyle.setFont(loadedFont); + targetStyle.setForeground(loadedColours.get(Constants.KEY_FONT_FOREGROUND)); + targetStyle.setBackground(loadedColours.get(Constants.KEY_FONT_BACKGROUND)); + + return targetStyle.clone(); + } + + public static void deleteStyleFont(URStyle targetStyle, Preferences baseSettingsPath) + { + Preferences settingsPath = baseSettingsPath.node(targetStyle.getName()); + try + { + Constants.LOGGER.log(Level.INFO, "Removing font keys: " + settingsPath.absolutePath()); + settingsPath.remove(Constants.KEY_FONT_BOLD); + settingsPath.remove(Constants.KEY_FONT_ITALIC); + settingsPath.remove(Constants.KEY_FONT_FAMILY); + settingsPath.remove(Constants.KEY_FONT_SIZE); + settingsPath.remove(Constants.KEY_FONT_UNDERLINE); + } catch (Exception e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public static void deleteStyleColours(URStyle targetStyle, Preferences baseSettingsPath) + { + Preferences settingsPath = baseSettingsPath.node(targetStyle.getName()); + try + { + Constants.LOGGER.log(Level.INFO, "Removing font colours: " + settingsPath.absolutePath()); + settingsPath.remove(Constants.KEY_FONT_FOREGROUND); + settingsPath.remove(Constants.KEY_FONT_BACKGROUND); + } catch (Exception e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + public static void saveStyle(URStyle targetStyle, Preferences baseSettingsPath) + { + Preferences stylePrefPath = baseSettingsPath.node(targetStyle.getAttribute("name").toString()); + Constants.LOGGER.log(Level.INFO, "Save Style Path: " + stylePrefPath.toString()); + saveStyleFont(targetStyle.getFont(), stylePrefPath); + saveStyleColours(targetStyle.getForeground(), targetStyle.getBackground(), stylePrefPath); + } + + /** + * TODO: This should be deprecated and just use a saveStyle method. + * @param newFont + * @param settingsPath + */ + private static void saveStyleFont(Font newFont, Preferences settingsPath) + { + // TODO: Don't safe if it's the default font + settingsPath.putBoolean(Constants.KEY_FONT_BOLD, newFont.isBold()); + settingsPath.putBoolean(Constants.KEY_FONT_ITALIC, newFont.isItalic()); + settingsPath.putBoolean(Constants.KEY_FONT_UNDERLINE, URStyle.isUnderline(newFont)); + settingsPath.put(Constants.KEY_FONT_FAMILY, newFont.getFamily()); + settingsPath.putInt(Constants.KEY_FONT_SIZE, newFont.getSize()); + } + + /** + * Removes the saved colours if they've been set to the default, this ensures we're not accidentally + * overriding the default theme colours when the theme is changed. + * @param foreground + * @param background + * @param settingsPath + */ + private static void saveStyleColours(Color foreground, Color background, Preferences settingsPath) + { + // Don't save if it's the default colours + Color defaultForeground = UIManager.getColor(Constants.DEFAULT_FOREGROUND_STRING); + Color defaultBackground = UIManager.getColor(Constants.DEFAULT_BACKGROUND_STRING); + + if(URColour.hexEncode(defaultForeground).equals(URColour.hexEncode(foreground))) + { + settingsPath.remove(Constants.KEY_FONT_FOREGROUND); + } else { + settingsPath.put(Constants.KEY_FONT_FOREGROUND, URColour.hexEncode(foreground)); + } + + if(URColour.hexEncode(defaultBackground).equals(URColour.hexEncode(background))) + { + settingsPath.remove(Constants.KEY_FONT_BACKGROUND); + } else { + settingsPath.put(Constants.KEY_FONT_BACKGROUND, URColour.hexEncode(background)); + } + } +} diff --git a/src/urChatBasic/backend/utils/URStyle.java b/src/urChatBasic/backend/utils/URStyle.java new file mode 100644 index 0000000..6f15ee8 --- /dev/null +++ b/src/urChatBasic/backend/utils/URStyle.java @@ -0,0 +1,151 @@ +package urChatBasic.backend.utils; + +import java.awt.Color; +import java.awt.Font; +import java.awt.font.TextAttribute; +import java.util.Hashtable; +import java.util.Map; +import java.util.prefs.Preferences; +import javax.swing.UIManager; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; +import urChatBasic.base.Constants; + +public class URStyle extends SimpleAttributeSet +{ + + // /** + // * Create a URStyle with defaults + // */ + // public URStyle (String name) + // { + // this(name, Constants.DEFAULT_FONT_GENERAL); + // } + + /** + * Create a URStyle based on defaultFont + * + * @param defaultFont + */ + public URStyle(String name, Font defaultFont) + { + super(); + this.addAttribute("name", name); + setFont(defaultFont); + setForeground(UIManager.getColor(Constants.DEFAULT_FOREGROUND_STRING)); + setBackground(UIManager.getColor(Constants.DEFAULT_BACKGROUND_STRING)); + } + + /** + * Create a URStyle based on defaultFont + * + * @param defaultFont + */ + public URStyle(String name, Font defaultFont, Color defaultForeground, Color defaultBackground) + { + super(); + this.addAttribute("name", name); + setFont(defaultFont); + setForeground(defaultForeground); + setBackground(defaultBackground); + } + + public String getName() + { + return getAttribute("name").toString(); + } + + public Font getFont() + { + // int savedFontBoldItalic = 0; + + // if (StyleConstants.isBold(this)) + // savedFontBoldItalic = Font.BOLD; + // if (StyleConstants.isItalic(this)) + // savedFontBoldItalic |= Font.ITALIC; + + Map fontMap = new Hashtable(); + + fontMap.put(TextAttribute.WEIGHT, StyleConstants.isBold(this) ? TextAttribute.WEIGHT_BOLD : TextAttribute.WEIGHT_REGULAR); + fontMap.put(TextAttribute.POSTURE, StyleConstants.isItalic(this) ? TextAttribute.POSTURE_OBLIQUE : TextAttribute.POSTURE_REGULAR); + fontMap.put(TextAttribute.UNDERLINE, StyleConstants.isUnderline(this) ? TextAttribute.UNDERLINE_ON : -1); + + Font styleFont = new Font(StyleConstants.getFontFamily(this), Font.PLAIN, StyleConstants.getFontSize(this)) + .deriveFont(fontMap); + + return styleFont; + // Font styleFont = new Font(StyleConstants.getFontFamily(this), savedFontBoldItalic, + // StyleConstants.getFontSize(this)); + + // return styleFont; + } + + public void setFont(Font newFont) + { + StyleConstants.setFontFamily(this, newFont.getFamily()); + StyleConstants.setBold(this, newFont.isBold()); + StyleConstants.setItalic(this, newFont.isItalic()); + StyleConstants.setFontSize(this, newFont.getSize()); + StyleConstants.setUnderline(this, isUnderline(newFont)); + } + + public Color getForeground() + { + return StyleConstants.getForeground(this); + } + + + public boolean isUnderline() + { + return StyleConstants.isUnderline(this); + } + + // https://docs.oracle.com/javase/6/docs/api/java/awt/font/TextAttribute.html#UNDERLINE + public static boolean isUnderline(Font targetFont) + { + if (targetFont != null && targetFont.getAttributes().get(TextAttribute.UNDERLINE) != null) + return (int) targetFont.getAttributes().get(TextAttribute.UNDERLINE) == TextAttribute.UNDERLINE_ON; + + return false; + } + + public Color getBackground() + { + return StyleConstants.getBackground(this); + } + + public void setForeground(Color newColour) + { + StyleConstants.setForeground(this, newColour); + } + + public void setBackground(Color newColour) + { + StyleConstants.setBackground(this, newColour); + } + + public void load(Preferences prefPath) + { + URStyle loadedStyle = URPreferencesUtil.loadStyle(this, prefPath); + setFont(loadedStyle.getFont()); + setForeground(loadedStyle.getForeground()); + setBackground(loadedStyle.getBackground()); + } + + public void save(Preferences prefPath) + { + URPreferencesUtil.saveStyle(this, prefPath); + } + + public boolean equals(URStyle otherStyle) + { + return getFont().equals(otherStyle.getFont()) && getForeground().equals(otherStyle.getForeground()) + && getBackground().equals(otherStyle.getBackground()); + } + + @Override + public URStyle clone() + { + return (URStyle) super.clone(); + } +} diff --git a/src/urChatBasic/backend/utils/URUncaughtExceptionHandler.java b/src/urChatBasic/backend/utils/URUncaughtExceptionHandler.java new file mode 100644 index 0000000..f7c944f --- /dev/null +++ b/src/urChatBasic/backend/utils/URUncaughtExceptionHandler.java @@ -0,0 +1,12 @@ +package urChatBasic.backend.utils; + +import java.util.logging.Level; +import urChatBasic.base.Constants; + +public class URUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler { + + @Override + public void uncaughtException(Thread t, Throwable e) { + Constants.LOGGER.log(Level.SEVERE, "Uncaught exception in thread: " + t.getName(), e); + } +} \ No newline at end of file diff --git a/src/urChatBasic/base/Constants.java b/src/urChatBasic/base/Constants.java index f136dc4..2e79cea 100644 --- a/src/urChatBasic/base/Constants.java +++ b/src/urChatBasic/base/Constants.java @@ -10,21 +10,21 @@ import java.util.prefs.Preferences; import javax.swing.JLabel; import javax.swing.UIManager; +import javax.swing.text.StyleConstants; import urChatBasic.backend.Connection; import urChatBasic.base.capabilities.CapabilityTypes; import urChatBasic.frontend.DriverGUI; import urChatBasic.frontend.components.URVersionLabel; +import urChatBasic.frontend.utils.URColour; /** * Used to store constants that are the same and do not change often. These are things used commonly * across the [front,back]end * - * @author goofybud16 - * */ public class Constants { - public static String UR_VERSION = "v0.3.0"; + public static String UR_VERSION = "v0.4.0"; public static String URL_SEPARATOR = "/"; public static final URL RESOURCES_DIR = DriverGUI.class.getResource(URL_SEPARATOR + "images" + URL_SEPARATOR); public static final String THEMES_DIR = "themes" + URL_SEPARATOR; @@ -34,7 +34,10 @@ public class Constants private static Handler LOGGER_TO_FILE; public static Logger LOGGER = Logger.getLogger("Main"); public static String LOGFILE_NAME = "Errors.log"; - private static final Font DEFAULT_FONT = new Font(new JLabel().getFont().getFamily(), 0, new JLabel().getFont().getSize()); + private static final JLabel DEFAULT_LABEL = new JLabel(); + private static final Font DEFAULT_FONT = new Font(DEFAULT_LABEL.getFont().getFamily(), 0, DEFAULT_LABEL.getFont().getSize()); + public static final String DEFAULT_FOREGROUND_STRING = "TextArea.foreground"; + public static final String DEFAULT_BACKGROUND_STRING = "TextArea.background"; // Preferences public static final Preferences BASE_PREFS = Preferences.userNodeForPackage(DriverGUI.class).node("profiles"); @@ -53,6 +56,7 @@ public class Constants public static final String KEY_NICK_NAME = "nick name"; public static final String KEY_REAL_NAME = "real name"; public static final String KEY_TIME_STAMPS = "show time stamps"; + public static final String KEY_TIME_STAMP_FORMAT = "timestamp format"; public static final String KEY_LAF_NAME = "laf name"; public static final String KEY_EVENT_TICKER_ACTIVE = "show event ticker"; public static final String KEY_USERS_LIST_ACTIVE = "show users list"; @@ -71,7 +75,10 @@ public class Constants public static final String KEY_FONT_FAMILY = "font family"; public static final String KEY_FONT_BOLD = "font bold"; public static final String KEY_FONT_ITALIC = "font italic"; + public static final String KEY_FONT_UNDERLINE = "font underline"; public static final String KEY_FONT_SIZE = "font size"; + public static final String KEY_FONT_FOREGROUND = "font foreground"; + public static final String KEY_FONT_BACKGROUND = "font background"; public static final String KEY_WINDOW_X = "window position x"; public static final String KEY_WINDOW_Y = "window position y"; public static final String KEY_WINDOW_WIDTH = "window position width"; @@ -91,6 +98,7 @@ public class Constants public static final String DEFAULT_NICK_NAME = "urChatClient"; public static final String DEFAULT_REAL_NAME = "urChatClient"; public static final Boolean DEFAULT_TIME_STAMPS = true; + public static final String DEFAULT_TIME_STAMP_FORMAT = "[HHmm]"; public static final String DEFAULT_LAF_NAME = UIManager.getSystemLookAndFeelClassName(); public static final Boolean DEFAULT_EVENT_TICKER_ACTIVE = true; public static final Boolean DEFAULT_CLICKABLE_LINKS_ENABLED = true; @@ -106,6 +114,8 @@ public class Constants public static final String DEFAULT_LIMIT_SERVER_LINES_COUNT = "500"; public static final Boolean DEFAULT_LOG_CLIENT_TEXT = true; public static final Font DEFAULT_FONT_GENERAL = DEFAULT_FONT; + public static final String DEFAULT_FONT_FOREGROUND = URColour.hexEncode(DEFAULT_LABEL.getForeground()); + public static final String DEFAULT_FONT_BACKGROUND = URColour.hexEncode(DEFAULT_LABEL.getBackground()); public static final int DEFAULT_EVENT_TICKER_DELAY = 10; public static final int DEFAULT_WINDOW_X = 0; public static final int DEFAULT_WINDOW_Y = 0; @@ -120,7 +130,7 @@ public class Constants public static final String END_MESSAGE = "\r\n"; // We 'must' match against http(s) in order to define the correct protocol to be used public static final String URL_REGEX = "((http:\\/\\/|https:\\/\\/)(www.)?(([a-zA-Z0-9-]){2,}\\.){1,4}([a-zA-Z]){2,6}(\\/([a-zA-Z-_\\/\\.0-9#:?=&;,]*)?)?)"; - + public static final String CHANNEL_REGEX = "(?:^|\s)(#([^\s,]+)(?!,))(?:$|\s)"; // Used to identify a message to be printed from the Event ticker // like a "user joins room" type message public static final String EVENT_USER = "****"; @@ -129,10 +139,29 @@ public class Constants public static final int MAIN_WIDTH = 500; public static final int MAIN_HEIGHT = 400; + public enum Size { + LARGE, + MEDIUM, + SMALL, + NONE + } + + // TODO: put all the font prefs in an enum? + public enum FONT_PREFS { + KEY_FONT_FAMILY(StyleConstants.FontFamily.toString(), DEFAULT_FONT.getFamily()); + + String keyStr = ""; + String defaultStr = ""; + + FONT_PREFS (String keyStr, String defaultStr) + { + this.keyStr = keyStr; + this.defaultStr = defaultStr; + } + } + /** * Used to initialize some values that may throw exceptions. - * - * @author goofybud16 */ public static void init() { diff --git a/src/urChatBasic/base/IRCRoomBase.java b/src/urChatBasic/base/IRCRoomBase.java index a42a7bc..452b9de 100644 --- a/src/urChatBasic/base/IRCRoomBase.java +++ b/src/urChatBasic/base/IRCRoomBase.java @@ -4,6 +4,7 @@ import urChatBasic.frontend.DriverGUI; import urChatBasic.frontend.IRCActions; import urChatBasic.frontend.IRCPrivate; +import urChatBasic.frontend.IRCServer; import urChatBasic.frontend.IRCUser; import urChatBasic.frontend.LineFormatter; import urChatBasic.frontend.LineFormatter.ClickableText; @@ -18,6 +19,9 @@ import java.text.*; import java.util.*; import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.prefs.Preferences; import javax.swing.*; @@ -80,16 +84,19 @@ public class IRCRoomBase extends JPanel private String lastUserToComplete = null; private List autoCompleteNames = new ArrayList(); - // Room Text Area + // Text Area private JTextPane channelTextArea = new JTextPane(); private JScrollPane channelScroll = new JScrollPane(channelTextArea); + private BlockingQueue messageQueue = new ArrayBlockingQueue<>(20); + public boolean messageQueueInProgress = false; private LineFormatter lineFormatter; // Users list area - // TODO: Users should be created per Server, and instead have a property to hold what channels they're in - private List usersArray = new ArrayList(); - private UsersListModel usersListModel = new UsersListModel(usersArray); + // TODO: Users should be created per Server, and instead have a property to hold what channels + // they're in + private ConcurrentHashMap usersMap = new ConcurrentHashMap<>(); + private UsersListModel usersListModel = new UsersListModel(); @SuppressWarnings("unchecked") private JList usersList = new JList(usersListModel); private JScrollPane userScroller = new JScrollPane(usersList); @@ -161,17 +168,18 @@ public void setServer(IRCServerBase server) private void initRoom() { - if(null != getServer()) + if (null != getServer()) { roomPrefs = gui.getFavouritesPath().node(getServer().getName()).node(roomName); - fontDialog = new FontDialog(roomName, gui.getFont(), roomPrefs); + fontDialog = new FontDialog(roomName, gui.getStyle(), roomPrefs); - lineFormatter = new LineFormatter(getFontPanel().getFont(), getServer().getNick()); - } else { + lineFormatter = new LineFormatter(getFontPanel().getStyle(), channelTextArea , getServer(), roomPrefs); + } else + { roomPrefs = gui.getFavouritesPath().node(roomName); - fontDialog = new FontDialog(roomName, gui.getFont(), roomPrefs); + fontDialog = new FontDialog(roomName, gui.getStyle(), roomPrefs); - lineFormatter = new LineFormatter(getFontPanel().getFont(), null); + lineFormatter = new LineFormatter(getFontPanel().getStyle() , channelTextArea, null, roomPrefs); } setFont(getFontPanel().getFont()); @@ -206,13 +214,13 @@ private void setupMainPanel() setupUsersList(); // mainPanel.add(userScroller, BorderLayout.LINE_END); - //Create a split pane with the two scroll panes in it. - mainResizer = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, - channelScroll, userScroller); + // Create a split pane with the two scroll panes in it. + mainResizer = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, channelScroll, userScroller); mainResizer.setOneTouchExpandable(true); // This should be set to where the minimum size of the userScroller would end up - mainResizer.setDividerLocation(gui.getWidth() - (userScroller.getPreferredSize().width + mainResizer.getDividerSize())); + mainResizer.setDividerLocation( + gui.getWidth() - (userScroller.getPreferredSize().width + mainResizer.getDividerSize())); // Left most panel (channelScroll pane), gets the extra space when resizing the window mainResizer.setResizeWeight(1); @@ -230,16 +238,26 @@ public FontPanel getFontPanel() return fontDialog.getFontPanel(); } + public void resetLineFormatter() + { + lineFormatter = new LineFormatter(getFontPanel().getStyle() , channelTextArea, getServer(), roomPrefs); + } + private void setupMainTextArea() { - channelScroll.setPreferredSize(new Dimension(Constants.MAIN_WIDTH - usersListWidth, Constants.MAIN_HEIGHT - BOTTOM_HEIGHT)); + channelScroll.setPreferredSize( + new Dimension(Constants.MAIN_WIDTH - usersListWidth, Constants.MAIN_HEIGHT - BOTTOM_HEIGHT)); channelScroll.setLocation(0, 0); channelScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); channelTextArea.addMouseListener(new ChannelClickListener()); channelTextArea.addMouseMotionListener(new ChannelMovementListener()); + // channelTextArea.getDocument().addDocumentListener(new LimitLinesDocumentListener(gui.getLimitChannelLinesCount())); + // channelTextArea.getDocument().addDocumentListener(new LineLimitListener()); channelTextArea.setEditable(false); channelTextArea.setFont(getFontPanel().getFont()); channelTextArea.setEditorKit(new StyledEditorKit()); + // This is needed because the channelTextArea isn't the same after it was initialized. + resetLineFormatter(); } private void setupUsersList() @@ -305,7 +323,7 @@ public void mouseClicked(MouseEvent e) Element ele = doc.getCharacterElement(channelTextArea.viewToModel2D((e.getPoint()))); AttributeSet as = ele.getAttributes(); ClickableText isClickableText = (ClickableText) as.getAttribute("clickableText"); - if(isClickableText != null) + if (isClickableText != null) { if (SwingUtilities.isRightMouseButton(e) && isClickableText.rightClickMenu() != null) { @@ -320,7 +338,8 @@ public void mouseClicked(MouseEvent e) class ChannelMovementListener extends MouseAdapter { - public void mouseMoved(MouseEvent e) { + public void mouseMoved(MouseEvent e) + { StyledDocument doc = (StyledDocument) channelTextArea.getDocument(); Element wordElement = doc.getCharacterElement(channelTextArea.viewToModel2D((e.getPoint()))); AttributeSet wordAttributeSet = wordElement.getAttributes(); @@ -328,7 +347,8 @@ public void mouseMoved(MouseEvent e) { if (isClickableText != null && gui.isClickableLinksEnabled()) { channelTextArea.setCursor(new Cursor(Cursor.HAND_CURSOR)); - } else { + } else + { channelTextArea.setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); } } @@ -336,6 +356,9 @@ public void mouseMoved(MouseEvent e) { public void createEvent(String eventText) { + if (gui.isJoinsQuitsMainEnabled()) + printText(eventText, Constants.EVENT_USER); + eventTickerTimer.setDelay(gui.getEventTickerDelay()); if (gui.isJoinsQuitsTickerEnabled()) { @@ -372,9 +395,6 @@ public void createEvent(String eventText) if (!(eventTickerTimer.isRunning())) eventTickerTimer.start(); } - - if (gui.isJoinsQuitsMainEnabled()) - printText(eventText, Constants.EVENT_USER); } public void callForAttention() @@ -382,68 +402,153 @@ public void callForAttention() myActions.callForAttention(); } - // TODO: Change this to accept IRCUser instead - public void printText(String line, String fromUser) - { - if(null == channelTextArea) - { - System.out.println("Cant print, shutting down"); - return; + class MessagePair { + private String line; + private String fromUser; + + public MessagePair(String line, String fromUser) { + this.line = line; + this.fromUser = fromUser; } - DateFormat chatDateFormat = new SimpleDateFormat("HHmm"); - Date chatDate = new Date(); - String timeLine = ""; + public String getLine() { + return line; + } - if (gui.isTimeStampsEnabled()) - timeLine = "[" + chatDateFormat.format(chatDate) + "]"; + public String getUser() { + return fromUser; + } + } - if (gui.isChannelHistoryEnabled()) - { - try - { - writeHistoryFile(line); - } catch (IOException e) - { - Constants.LOGGER.log(Level.WARNING, e.getLocalizedMessage()); - } + // TODO: Change this to accept IRCUser instead + public void printText(String line, String fromUser) { + try { + messageQueue.put(new MessagePair(line, fromUser)); + + if(!messageQueueInProgress) + handleMessageQueue(); + + } catch (InterruptedException e) { + e.printStackTrace(); } + } - StyledDocument doc = (StyledDocument) channelTextArea.getDocument(); - IRCUser fromIRCUser = getCreatedUsers(fromUser); + public boolean messageQueueWorking() + { + return (!messageQueue.isEmpty() || messageQueueInProgress); + } - // If we received a message from a user that isn't in the channel - // then add them to the users list. - // But don't add them if it's from the Event Ticker - if (fromIRCUser == null) + public void handleMessageQueue() + { + SwingUtilities.invokeLater(new Runnable() { - if (!fromUser.equals(Constants.EVENT_USER)) + public void run() { - addToUsersList(getName(), fromUser); - fromIRCUser = getCreatedUsers(fromUser); - } - } + while (!messageQueue.isEmpty()) + { + try + { + messageQueueInProgress = true; + MessagePair messagePair = messageQueue.take(); + if(null == messagePair) + { + messageQueueInProgress = false; + continue; + } - if (fromUser.equals(Constants.EVENT_USER) || !fromIRCUser.isMuted()) - { - lineFormatter.formattedDocument(doc, timeLine, fromIRCUser, fromUser, line); + String line = messagePair.getLine(); + String fromUser = messagePair.getUser(); - if (lineFormatter.nameStyle.getAttribute("name") == lineFormatter.highStyle().getAttribute("name")) - { - callForAttention(); - } + Document document = lineFormatter.getDocument(); + Element root = lineFormatter.getDocument().getDefaultRootElement(); - // Always alert on IRCPrivate messages - if(this instanceof IRCPrivate) - { - callForAttention(); + int lineLimit = gui.getLimitChannelLinesCount(); + + if(IRCRoomBase.this instanceof IRCServer) + lineLimit = gui.getLimitServerLinesCount(); + + if(null != messagePair && root.getElementCount() > lineLimit) + { + Element firstLine = root.getElement(0); + int endIndex = firstLine.getEndOffset(); + + try + { + document.remove(0, endIndex); + } + catch(BadLocationException ble) + { + Constants.LOGGER.log(Level.WARNING, ble.getLocalizedMessage()); + } + } + + if (null == channelTextArea) + { + Constants.LOGGER.log(Level.WARNING, "ChannelTextArea hasn't initialized or has disappeared.. not printing text."); + return; + } + + if (gui.isChannelHistoryEnabled()) + { + try + { + writeHistoryFile(line); + } catch (IOException e) + { + Constants.LOGGER.log(Level.WARNING, e.getLocalizedMessage()); + } + } + + StyledDocument doc = channelTextArea.getStyledDocument(); + IRCUser fromIRCUser = getCreatedUser(fromUser); + + // If we received a message from a user that isn't in the channel + // then add them to the users list. + // But don't add them if it's from the Event Ticker + if (fromIRCUser == null) + { + if (!fromUser.equals(Constants.EVENT_USER)) + { + // TODO: Re-add later? + // addToUsersList(getName(), fromUser); + // fromIRCUser = getCreatedUsers(fromUser); + // Constants.LOGGER.log(Level.WARNING, "Message from a user that isn't in the user list!"); + fromIRCUser = new IRCUser(server, fromUser); + } + } + + + if (fromUser.equals(Constants.EVENT_USER) || !fromIRCUser.isMuted()) + { + lineFormatter.formattedDocument(new Date(), fromIRCUser, fromUser, line); + + if (server.getNick() != null && line.indexOf(server.getNick()) > -1) + { + callForAttention(); + } + + // Always alert on IRCPrivate messages + if (IRCRoomBase.this instanceof IRCPrivate) + { + callForAttention(); + } + + // TODO: Scrolls to the bottom of the channelTextArea on message received, this should be + // disabled + // when the user has scrolled up + channelTextArea.setCaretPosition(channelTextArea.getDocument().getLength()); + messageQueueInProgress = false; + } + } catch (InterruptedException e) + { + Constants.LOGGER.log(Level.WARNING, e.getLocalizedMessage()); + } + } } + }); + - // TODO: Scrolls to the bottom of the channelTextArea on message received, this should be disabled - // when the user has scrolled up - channelTextArea.setCaretPosition(channelTextArea.getDocument().getLength()); - } } /** @@ -457,8 +562,9 @@ public void toggleUsersList(Boolean showIt) if (usersListShown == showIt || usersListShown == null) { // userScroller.setVisible(showIt); - if(showIt) - mainResizer.setDividerLocation(gui.getWidth() - (userScroller.getPreferredSize().width + mainResizer.getDividerSize())); + if (showIt) + mainResizer.setDividerLocation( + gui.getWidth() - (userScroller.getPreferredSize().width + mainResizer.getDividerSize())); else mainResizer.setDividerLocation(gui.getWidth()); } @@ -488,30 +594,8 @@ public void toggleEventTicker(Boolean showIt) * @param roomName * @return IRCChannel */ - public IRCUser getCreatedUsers(String userName) - { - for (IRCUser tempUser : usersArray) - { - if (tempUser.getName().equalsIgnoreCase(userName)) - return tempUser; - } ; - - return null; - } - - public void doLimitLines() - { - if (gui.isLimitedChannelActivity()) - { - String[] tempText = channelTextArea.getText().split("\n"); - int linesCount = tempText.length; - - if (linesCount >= gui.getLimitChannelLinesCount()) - { - String newText = channelTextArea.getText().replace(tempText[0] + "\n", ""); - channelTextArea.setText(newText); - } - } + public IRCUser getCreatedUser(String userName) { + return usersMap.get(userName.toLowerCase()); } public void disableFocus() @@ -525,44 +609,34 @@ public void enableFocus() } // Adds users to the list in the users array[] - public void addToUsersList(final String channel, final String[] users) - { - // Removed as Runnable(), not sure it was necessary - // TODO: maybe readd Runnable - if (users.length >= 0 && null != getServer()) - { - for (int x = 0; x < users.length; x++) - { - String tempUserName = users[x]; - if (users[x].startsWith(":")) - tempUserName = tempUserName.substring(1); - - IRCUser newUser = getServer().getIRCUser(tempUserName); - - if (!usersArray.contains(newUser)) - usersArray.add(newUser); + public void addToUsersList(final String[] users) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + // Removed as Runnable(), not sure it was necessary + // TODO: maybe readd Runnable + if (users.length >= 0 && null != getServer()) { + for (int x = 0; x < users.length; x++) { + String tempUserName = users[x]; + if (users[x].startsWith(":")) + tempUserName = tempUserName.substring(1); + + IRCUser newUser = getServer().getIRCUser(tempUserName); + + if (null != newUser) { + usersMap.put(newUser.getName().toLowerCase(), newUser); + usersListModel.addUser(newUser); + } + } + } + usersListModel.sort(); } - } - usersListModel.sort(); + }); } // Adds a single user, good for when a user joins the channel - public void addToUsersList(final String channel, final String user) + public void addToUsersList(final String user) { - // Removed as Runnable(), not sure it was necessary - String thisUser = user; - if (user.startsWith(":")) - thisUser = user.substring(1); - - IRCUser newUser = getServer().getIRCUser(thisUser); - - if (!usersArray.contains(newUser)) - { - usersArray.add(newUser); - usersList.setSelectedIndex(0); - createEvent("++ " + thisUser + " has entered " + channel); - usersListModel.sort(); - } + addToUsersList(new String[]{user}); } public String getChannelTopic(String roomName) @@ -587,27 +661,17 @@ public void run() thisUser = user.substring(1); - int index = 0; - while(index < usersArray.size()) - { - if (usersArray.get(index).getName().matches(thisUser)) - { - usersArray.remove(index); - createEvent("-- " + thisUser + " has quit " + channel); - } else { - index++; - } - } - + usersMap.remove(thisUser.toLowerCase()); + usersListModel.removeUser(thisUser); usersListModel.sort(); } }); } /** Clear the users list */ - public void clearUsersList(String channel) + public void clearUsersList() { - usersArray.clear(); + usersMap.clear(); } @@ -626,7 +690,7 @@ public void writeHistoryFile(String line) throws IOException { if (gui.saveChannelHistory()) { - if(historyFileName == null || historyFileName.isEmpty()) + if (historyFileName == null || historyFileName.isEmpty()) { historyFileName = historyDateFormat.format(todayDate) + " " + getName() + ".log"; } @@ -646,7 +710,7 @@ public void renameUser(final String oldUserName, final String newUserName) { public void run() { - IRCUser tempUser = getCreatedUsers(oldUserName); + IRCUser tempUser = getCreatedUser(oldUserName); if (tempUser != null) { createEvent("!! " + oldUserName + " changed name to " + newUserName); @@ -677,7 +741,8 @@ public View create(Element elem) { String kind = elem.getName(); - return switch (kind) { + return switch (kind) + { case AbstractDocument.ContentElementName -> new WrapLabelView(elem); case AbstractDocument.ParagraphElementName -> new ParagraphView(elem); case AbstractDocument.SectionElementName -> new BoxView(elem, View.Y_AXIS); @@ -697,7 +762,8 @@ public WrapLabelView(Element elem) public float getMinimumSpan(int axis) { - return switch (axis) { + return switch (axis) + { case View.X_AXIS -> 0; case View.Y_AXIS -> super.getMinimumSpan(axis); default -> throw new IllegalArgumentException("Invalid axis: " + axis); @@ -767,7 +833,7 @@ private class AddAsFavourite implements ActionListener @Override public void actionPerformed(ActionEvent arg0) { - if(null != getServer()) + if (null != getServer()) { if (!gui.isFavourite(IRCRoomBase.this)) { @@ -785,7 +851,7 @@ private class QuitItem implements ActionListener @Override public void actionPerformed(ActionEvent arg0) { - if(null != getServer()) + if (null != getServer()) { getServer().sendClientText("/part i'm outta here", getName()); } @@ -820,10 +886,12 @@ private class ToggleHideUsersListItem implements ActionListener public void actionPerformed(ActionEvent arg0) { - if(mainResizer.getDividerLocation() <= gui.getWidth() - (userScroller.getPreferredSize().width + mainResizer.getDividerSize()) ) + if (mainResizer.getDividerLocation() <= gui.getWidth() + - (userScroller.getPreferredSize().width + mainResizer.getDividerSize())) { usersListShown = false; - } else { + } else + { usersListShown = true; } toggleUsersList(usersListShown); @@ -845,13 +913,13 @@ private class SendTextListener implements ActionListener @Override public void actionPerformed(ActionEvent arg0) { - if (!getUserTextBox().getText().trim().isEmpty()) - { - sendClientText(clientTextBox.getText(), getName()); - if (gui.isClientHistoryEnabled()) - userHistory.add(clientTextBox.getText()); - } - clientTextBox.setText(""); + if (!getUserTextBox().getText().trim().isEmpty()) + { + sendClientText(clientTextBox.getText(), getName()); + if (gui.isClientHistoryEnabled()) + userHistory.add(clientTextBox.getText()); + } + clientTextBox.setText(""); } } @@ -885,7 +953,9 @@ public void run() { fontDialog.getFontPanel().setDefaultFont(f); - lineFormatter.setFont((StyledDocument) channelTextArea.getDocument(),fontDialog.getFontPanel().getFont()); + lineFormatter.setFont(fontDialog.getFontPanel().getFont()); + // TODO: Should this updateStyles if the font is changed? + // lineFormatter.updateStyles((StyledDocument) channelTextArea.getDocument(), 0); } }); } else @@ -900,12 +970,12 @@ private class SaveFontListener implements ActionListener public void actionPerformed(ActionEvent arg0) { // fontDialog.saveFont(fontDialog.getFont()); - fontDialog.getFontPanel().setFont(fontDialog.getFontPanel().getFont(), true); + fontDialog.getFontPanel().setFont(fontDialog.getFontPanel().getStyle(), true); setFont(fontDialog.getFontPanel().getFont()); } } - public void quitRoom () + public void quitRoom() { eventTickerTimer.stop(); tickerPanel.setVisible(false); @@ -915,7 +985,7 @@ public void quitRoom () repaint(); } - public boolean userIsTyping () + public boolean userIsTyping() { return !clientTextBox.getText().isEmpty(); } @@ -963,9 +1033,8 @@ public void keyPressed(KeyEvent e) } // If usersArray and clientText isn't empty. - if (usersArray.size() > 0 && clientTextBox.getText().length() > 0) - { - usersArray.stream() + if (!usersMap.isEmpty() && clientTextBox.getText().length() > 0) { + usersMap.values().stream() .filter(user -> user.getName().toLowerCase().replace("@", "") .startsWith(startingCharacters.toLowerCase())) .forEach(user -> autoCompleteNames.add(user.getName())); @@ -1017,9 +1086,12 @@ public void keyPressed(KeyEvent e) } else { int nextTextInt = 0; - switch (e.getKeyCode()) { - case KeyEvent.VK_UP -> { - if (!userHistory.isEmpty()) { + switch (e.getKeyCode()) + { + case KeyEvent.VK_UP -> + { + if (!userHistory.isEmpty()) + { nextTextInt = userHistory.indexOf(clientTextBox.getText()) - 1; if (nextTextInt < 0) nextTextInt = userHistory.size() - 1; @@ -1027,8 +1099,10 @@ public void keyPressed(KeyEvent e) clientTextBox.setText(userHistory.get(nextTextInt)); } } - case KeyEvent.VK_DOWN -> { - if (!userHistory.isEmpty()) { + case KeyEvent.VK_DOWN -> + { + if (!userHistory.isEmpty()) + { nextTextInt = userHistory.indexOf(clientTextBox.getText()) + 1; if (nextTextInt > userHistory.size() - 1) nextTextInt = 0; @@ -1037,7 +1111,8 @@ public void keyPressed(KeyEvent e) } } case KeyEvent.VK_ESCAPE -> clientTextBox.setText(""); - default -> { + default -> + { if (lastUserToComplete != null) lastUserToComplete = null; if (startingCharacters != null) @@ -1068,10 +1143,12 @@ public void run() if (IRCRoomBase.this.tickerPanel.isVisible()) { Iterator labelIterator = eventLabels.iterator(); - while (labelIterator.hasNext()) { + while (labelIterator.hasNext()) + { JLabel tempLabel = labelIterator.next(); tempLabel.setLocation(tempLabel.getX() - EVENT_VELOCITY, 0); - if (tempLabel.getX() + tempLabel.getWidth() < 0) { + if (tempLabel.getX() + tempLabel.getWidth() < 0) + { labelIterator.remove(); // Safely remove the element tickerPanel.remove(tempLabel); } @@ -1080,7 +1157,7 @@ public void run() if (eventLabels.isEmpty()) eventTickerTimer.stop(); - if(DriverGUI.frame.isFocused()) + if (DriverGUI.frame.isFocused()) { tickerPanel.revalidate(); tickerPanel.repaint(); @@ -1089,7 +1166,8 @@ public void run() { eventTickerTimer.stop(); - for (JLabel tempLabel : eventLabels) { + for (JLabel tempLabel : eventLabels) + { tickerPanel.remove(tempLabel); } @@ -1123,7 +1201,8 @@ class UsersMouseListener extends MouseInputAdapter { public void mouseClicked(MouseEvent e) { - if (SwingUtilities.isRightMouseButton(e)){ + if (SwingUtilities.isRightMouseButton(e)) + { final int index = usersList.locationToIndex(e.getPoint()); if (index > -1) { diff --git a/src/urChatBasic/base/IRCServerBase.java b/src/urChatBasic/base/IRCServerBase.java index d138bf7..ee94adb 100644 --- a/src/urChatBasic/base/IRCServerBase.java +++ b/src/urChatBasic/base/IRCServerBase.java @@ -161,4 +161,6 @@ public interface IRCServerBase * @param newUser */ public abstract void renameUser (String oldUserName, String newUserName); + + public void callForAttention(); } diff --git a/src/urChatBasic/base/UserGUIBase.java b/src/urChatBasic/base/UserGUIBase.java index a3cf359..33a7194 100644 --- a/src/urChatBasic/base/UserGUIBase.java +++ b/src/urChatBasic/base/UserGUIBase.java @@ -1,6 +1,5 @@ package urChatBasic.base; -import java.awt.Graphics; import java.util.prefs.Preferences; import urChatBasic.base.capabilities.CapTypeBase; diff --git a/src/urChatBasic/base/capabilities/CapTypeBase.java b/src/urChatBasic/base/capabilities/CapTypeBase.java index 8e9ed6b..078e2e6 100644 --- a/src/urChatBasic/base/capabilities/CapTypeBase.java +++ b/src/urChatBasic/base/capabilities/CapTypeBase.java @@ -1,7 +1,5 @@ package urChatBasic.base.capabilities; -import java.util.ArrayList; - public interface CapTypeBase { enum Category { AUTHENTICATION diff --git a/src/urChatBasic/frontend/DriverGUI.java b/src/urChatBasic/frontend/DriverGUI.java index b27e969..9ee1c03 100644 --- a/src/urChatBasic/frontend/DriverGUI.java +++ b/src/urChatBasic/frontend/DriverGUI.java @@ -11,6 +11,7 @@ import javax.swing.JFrame; import javax.swing.SwingUtilities; import urChatBasic.backend.LookAndFeelLoader; +import urChatBasic.backend.utils.URUncaughtExceptionHandler; import urChatBasic.base.Constants; public class DriverGUI @@ -24,22 +25,27 @@ public static void main(String[] args) throws IOException { Constants.init(); - URL imgPath = new URL(Constants.RESOURCES_DIR + "urChat Icon.png"); + try { + URL imgPath = new URL(Constants.RESOURCES_DIR + "urChat Icon.png"); - img = new ImageIcon(imgPath); + img = new ImageIcon(imgPath); + } catch (Exception e) + { + Constants.LOGGER.log(Level.INFO, "No Icon found."); + } Constants.LOGGER.log(Level.INFO, "Starting up.."); LookAndFeelLoader lafLoader = new LookAndFeelLoader(Thread.currentThread().getContextClassLoader()); contextClassLoader = lafLoader.cl; Thread.currentThread().setContextClassLoader(contextClassLoader); + Thread.currentThread().setUncaughtExceptionHandler(new URUncaughtExceptionHandler()); createGUI(); startGUI(); } - final public static String getMemoryReport() { final Runtime r = Runtime.getRuntime(); diff --git a/src/urChatBasic/frontend/IRCActions.java b/src/urChatBasic/frontend/IRCActions.java index 8bb33ce..237b075 100644 --- a/src/urChatBasic/frontend/IRCActions.java +++ b/src/urChatBasic/frontend/IRCActions.java @@ -4,7 +4,6 @@ import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; -import javax.swing.JPanel; import javax.swing.Timer; import urChatBasic.base.IRCActionsBase; import urChatBasic.base.IRCRoomBase; diff --git a/src/urChatBasic/frontend/IRCPrivate.java b/src/urChatBasic/frontend/IRCPrivate.java index 3f0bb27..c4addd1 100644 --- a/src/urChatBasic/frontend/IRCPrivate.java +++ b/src/urChatBasic/frontend/IRCPrivate.java @@ -42,12 +42,12 @@ public void actionPerformed(ActionEvent arg0) { if (!clientTextBox.getText().trim().isEmpty()) { - String messagePrefix = ""; if (!clientTextBox.getText().startsWith("/")) - messagePrefix = "/msg " + getName() + " "; - server.sendClientText(messagePrefix + clientTextBox.getText(), getName()); + { + String messagePrefix = "/msg " + getName() + " "; + clientTextBox.setText(messagePrefix + clientTextBox.getText()); + } } - clientTextBox.setText(""); } } } diff --git a/src/urChatBasic/frontend/IRCServer.java b/src/urChatBasic/frontend/IRCServer.java index be5d441..d6cfb2d 100644 --- a/src/urChatBasic/frontend/IRCServer.java +++ b/src/urChatBasic/frontend/IRCServer.java @@ -15,7 +15,6 @@ import java.util.Iterator; import java.util.List; import java.util.logging.Level; -import java.util.stream.IntStream; import javax.swing.ImageIcon; import javax.swing.JMenuItem; import javax.swing.JPopupMenu; @@ -61,6 +60,8 @@ public IRCServer(String serverName, String nick, String login, String password, String proxyPort, Boolean useSOCKS) { super(serverName); + setServer(this); + resetLineFormatter(); myMenu = new ServerPopUp(); hideUsersList(); @@ -279,7 +280,7 @@ public void actionPerformed(ActionEvent arg0) { if (IRCServer.this.isConnected()) { - System.out.println("send quit message"); + Constants.LOGGER.log(Level.INFO, "send quit message"); // Send the /quit message, which disconnects and remove the gui elements sendClientText("/quit Goodbye cruel world", getName()); } else @@ -372,8 +373,8 @@ public String getName() public IRCUser getIRCUser(String userName) { for (IRCRoomBase tempChannel : createdRooms) - if (tempChannel.getCreatedUsers(userName) != null) - return tempChannel.getCreatedUsers(userName); + if (tempChannel.getCreatedUser(userName) != null) + return tempChannel.getCreatedUser(userName); return new IRCUser(this, userName); } @@ -449,7 +450,6 @@ public IRCRoomBase getCreatedRoom(String roomName, boolean asPrivate) return null; } - /* * (non-Javadoc) * @@ -591,7 +591,7 @@ public void addToUsersList(final String channelName, final String[] users) { IRCChannel tempChannel = getCreatedChannel(channelName); if (tempChannel != null) - tempChannel.addToUsersList(tempChannel.getName(), users); + tempChannel.addToUsersList(users); } } @@ -604,13 +604,7 @@ public void addToUsersList(final String channelName, final String[] users) @Override public void addToUsersList(final String channelName, final String user) { - String thisUser = user; - if (user.startsWith(":")) - thisUser = user.substring(1); - - IRCChannel tempChannel = getCreatedChannel(channelName); - if (tempChannel != null) - tempChannel.addToUsersList(tempChannel.getName(), thisUser); + addToUsersList(channelName, new String[]{user}); } diff --git a/src/urChatBasic/frontend/LineFormatter.java b/src/urChatBasic/frontend/LineFormatter.java index 7768e80..b6b82c5 100644 --- a/src/urChatBasic/frontend/LineFormatter.java +++ b/src/urChatBasic/frontend/LineFormatter.java @@ -5,12 +5,20 @@ import java.awt.Font; import java.awt.event.ActionEvent; import java.net.URL; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; +import java.util.prefs.Preferences; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.AbstractAction; +import javax.swing.JOptionPane; import javax.swing.JPopupMenu; +import javax.swing.JTextPane; import javax.swing.UIManager; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; @@ -18,134 +26,315 @@ import javax.swing.text.SimpleAttributeSet; import javax.swing.text.StyleConstants; import javax.swing.text.StyledDocument; +import urChatBasic.backend.utils.URStyle; import urChatBasic.base.Constants; +import urChatBasic.base.IRCServerBase; +import urChatBasic.frontend.dialogs.YesNoDialog; import urChatBasic.frontend.utils.URColour; public class LineFormatter { private String myNick; - private Font myFont; - public SimpleAttributeSet defaultStyle; - public SimpleAttributeSet timeStyle; - public SimpleAttributeSet nameStyle; - public SimpleAttributeSet lineStyle; - protected UserGUI gui = DriverGUI.gui; - - public LineFormatter(Font myFont, String myNick) + private URStyle targetStyle; + private Color myForeground; + private Color myBackground; + private IRCServerBase myServer; + private Preferences formatterPrefs; + private URStyle urlStyle; + private URStyle channelStyle; + private URStyle timeStyle; + private URStyle lineStyle; + private URStyle nickStyle; + private URStyle highStyle; + private URStyle mediumStyle; + private URStyle lowStyle; + private JTextPane docOwner; + private StyledDocument doc; + public URStyle myStyle; + private Map formatterStyles = new HashMap<>(); + + + public LineFormatter(URStyle baseStyle, JTextPane docOwner ,final IRCServerBase server, Preferences formatterPrefs) { - this.myNick = myNick; - this.myFont = myFont; - defaultStyle = defaultStyle(); - timeStyle = defaultStyle(); - nameStyle = defaultStyle(); - lineStyle = defaultStyle(); + // TODO: Need to load attributes from formatterPrefs + this.formatterPrefs = formatterPrefs; + + this.docOwner = docOwner; + + // The JTextPane is technically 'disabled', so we need to change the colour to be the enabled colour. + this.docOwner.setBackground(UIManager.getColor(Constants.DEFAULT_BACKGROUND_STRING)); + doc = this.docOwner.getStyledDocument(); + + if (null != server) + { + myNick = server.getNick(); + myServer = server; + } else + { + myNick = null; + } + + targetStyle = new URStyle(myNick, baseStyle.getFont()); + targetStyle.setForeground(baseStyle.getForeground()); + targetStyle.setBackground(baseStyle.getBackground()); + + // TODO: should we be using something like UIManager + // UIManager.getFont for the default fonts isntead? + myForeground = targetStyle.getForeground(); + myBackground = targetStyle.getBackground(); + + // TODO: split this mess out to a method + timeStyle = defaultStyle(null, true); + lineStyle = defaultStyle(null, true); + nickStyle = nickStyle(true); + myStyle = myStyle(true); + channelStyle = channelStyle(true); + urlStyle = urlStyle(true); + highStyle = highStyle(true); + mediumStyle = mediumStyle(true); + lowStyle = lowStyle(true); + + formatterStyles.put(timeStyle.getName(), timeStyle); + formatterStyles.put(lineStyle.getName(), lineStyle); + formatterStyles.put(nickStyle.getName(), nickStyle); + formatterStyles.put(myStyle.getName(), myStyle); + formatterStyles.put(channelStyle.getName(), channelStyle); + formatterStyles.put(urlStyle.getName(), urlStyle); + formatterStyles.put(highStyle.getName(), highStyle); + formatterStyles.put(mediumStyle.getName(), mediumStyle); + formatterStyles.put(lowStyle.getName(), lowStyle); } - public void setFont(StyledDocument doc, Font newFont) + public void setFont(Font newFont) { - myFont = newFont; - if(doc.getLength() > 0) - updateStyles(doc, 0); + targetStyle.setFont(newFont); + if (doc.getLength() > 0) + updateStyles(0); } - public SimpleAttributeSet defaultStyle() + public URStyle defaultStyle(String name, boolean load) { - SimpleAttributeSet defaultStyle = new SimpleAttributeSet(); - defaultStyle.addAttribute("name", "defaultStyle"); + if (name == null) + name = "defaultStyle"; + URStyle tempStyle = new URStyle(name, targetStyle.getFont()); + tempStyle.addAttribute("type", "default"); // get the contrasting colour of the background colour - StyleConstants.setForeground(defaultStyle, URColour.getContrastColour(UIManager.getColor("Panel.background"))); - StyleConstants.setFontFamily(defaultStyle, myFont.getFamily()); - StyleConstants.setFontSize(defaultStyle, myFont.getSize()); - StyleConstants.setBold(defaultStyle, myFont.isBold()); - StyleConstants.setItalic(defaultStyle, myFont.isItalic()); + // StyleConstants.setForeground(defaultStyle, new Color(formatterPrefs.node(name).getInt("font + // foreground", + // URColour.getContrastColour(UIManager.getColor(Constants.DEFAULT_BACKGROUND_STRING)).getRGB()))); + + StyleConstants.setFontFamily(tempStyle, targetStyle.getFont().getFamily()); + StyleConstants.setFontSize(tempStyle, targetStyle.getFont().getSize()); + StyleConstants.setBold(tempStyle, targetStyle.getFont().isBold()); + StyleConstants.setItalic(tempStyle, targetStyle.getFont().isItalic()); + + StyleConstants.setForeground(tempStyle, myForeground); - return defaultStyle; + if (load) + tempStyle.load(formatterPrefs); + + return tempStyle; } - public SimpleAttributeSet lowStyle() + public URStyle lowStyle(boolean load) { - SimpleAttributeSet tempStyle = defaultStyle(); - tempStyle.addAttribute("name", "lowStyle"); - StyleConstants.setForeground(tempStyle, Color.LIGHT_GRAY); + String name = "lowStyle"; + + URStyle tempStyle = defaultStyle(name, load); + + + StyleConstants.setForeground(tempStyle, UIManager.getColor(Constants.DEFAULT_BACKGROUND_STRING).darker()); + + if (StyleConstants.getForeground(tempStyle).getRGB() == myForeground.getRGB()) + if (URColour.useDarkColour(UIManager.getColor(Constants.DEFAULT_BACKGROUND_STRING))) + { + StyleConstants.setForeground(tempStyle, UIManager.getColor(Constants.DEFAULT_BACKGROUND_STRING).darker()); + } else + { + StyleConstants.setForeground(tempStyle, UIManager.getColor(Constants.DEFAULT_BACKGROUND_STRING).brighter()); + } + + if (load) + tempStyle.load(formatterPrefs); + return tempStyle; } - public SimpleAttributeSet mediumStyle() + public URStyle mediumStyle(boolean load) { + String name = "mediumStyle"; - SimpleAttributeSet tempStyle = defaultStyle(); - tempStyle.addAttribute("name", "mediumStyle"); - // StyleConstants.setBackground(tempStyle, Color.YELLOW); + URStyle tempStyle = defaultStyle(name, load); + + if (load) + tempStyle.load(formatterPrefs); return tempStyle; } - public SimpleAttributeSet highStyle() + public URStyle highStyle(boolean load) { - SimpleAttributeSet tempStyle = defaultStyle(); - tempStyle.addAttribute("name", "highStyle"); + String name = "highStyle"; + + URStyle tempStyle = defaultStyle(name, load); + + StyleConstants.setBackground(tempStyle, UIManager.getColor("CheckBoxMenuItem.selectionBackground")); + StyleConstants.setForeground(tempStyle, + URColour.getContrastColour(UIManager.getColor("CheckBoxMenuItem.selectionBackground"))); - StyleConstants.setBackground(tempStyle, UIManager.getColor("CheckBoxMenuItem.selectionBackground")); // TODO: Get highlight colour? - StyleConstants.setForeground(tempStyle, URColour.getContrastColour(UIManager.getColor("CheckBoxMenuItem.selectionBackground"))); StyleConstants.setBold(tempStyle, true); StyleConstants.setItalic(tempStyle, true); + if (load) + tempStyle.load(formatterPrefs); + return tempStyle; } - public SimpleAttributeSet urlStyle() + public URStyle urlStyle(boolean load) { - SimpleAttributeSet tempStyle = defaultStyle(); + String name = "urlStyle"; + + URStyle tempStyle = defaultStyle(name, load); - tempStyle.addAttribute("name", "urlStyle"); + tempStyle.addAttribute("name", name); tempStyle.addAttribute("type", "url"); + StyleConstants.setForeground(tempStyle, UIManager.getColor("CheckBoxMenuItem.selectionBackground")); + StyleConstants.setBold(tempStyle, true); + StyleConstants.setUnderline(tempStyle, true); + + if (load) + tempStyle.load(formatterPrefs); + return tempStyle; + } + + // TODO: urlStyle and channelStyle don't load the correct styling in the fontPanel + + public URStyle channelStyle(boolean load) + { + String name = "channelStyle"; + + URStyle tempStyle = defaultStyle(name, load); + + tempStyle.addAttribute("name", name); + tempStyle.addAttribute("type", "channel"); + + StyleConstants.setForeground(tempStyle, UIManager.getColor("CheckBoxMenuItem.selectionBackground")); + StyleConstants.setBold(tempStyle, true); StyleConstants.setUnderline(tempStyle, true); + if (load) + tempStyle.load(formatterPrefs); + return tempStyle; } - public SimpleAttributeSet myStyle() + public URStyle myStyle(boolean load) { - SimpleAttributeSet tempStyle = defaultStyle(); - tempStyle.addAttribute("name", "myStyle"); + String name = "myStyle"; + + URStyle tempStyle = defaultStyle(name, load); + tempStyle.addAttribute("type", "myNick"); + // StyleConstants.setForeground(tempStyle, Color.GREEN); - StyleConstants.setForeground(tempStyle, URColour.getInvertedColour(UIManager.getColor("CheckBoxMenuItem.selectionBackground"))); + StyleConstants.setForeground(tempStyle, + URColour.getInvertedColour(UIManager.getColor("CheckBoxMenuItem.selectionBackground"))); StyleConstants.setBold(tempStyle, true); StyleConstants.setUnderline(tempStyle, true); + if (load) + tempStyle.load(formatterPrefs); + + return tempStyle; + } + + public URStyle nickStyle(boolean load) + { + String name = "nickStyle"; + + URStyle tempStyle = defaultStyle(name, load); + tempStyle.addAttribute("type", "nick"); + + StyleConstants.setUnderline(tempStyle, true); + + if (load) + tempStyle.load(formatterPrefs); + return tempStyle; } + public void setNick(String myNick) + { + this.myNick = myNick; + } + public class ClickableText extends AbstractAction { private String textLink; - private SimpleAttributeSet attributeSet; + private URStyle attributeSet; private IRCUser fromUser; - ClickableText(String textLink, SimpleAttributeSet attributeSet, IRCUser fromUser) + ClickableText(String textLink, URStyle attributeSet, IRCUser fromUser) { this.textLink = textLink; this.attributeSet = attributeSet; - if(fromUser != null) + if (fromUser != null) { this.fromUser = fromUser; } } + @Override + public String toString() + { + return textLink; + } + public void execute() { - if (!textLink.isEmpty() && gui.isClickableLinksEnabled() && attributeSet.getAttribute("type").equals("url")) + if (!textLink.isEmpty() && attributeSet.getAttribute("type").equals("url")) { - // TODO: This should really pop up a dialog to confirm you want to open the link - try { - Desktop.getDesktop().browse(new URL(textLink).toURI()); - } catch (Exception e) { + try + { + AtomicBoolean doOpenLink = new AtomicBoolean(false); + + YesNoDialog confirmOpenLink = new YesNoDialog("Are you sure you want to open " + textLink + "?", + "Open Link", JOptionPane.QUESTION_MESSAGE, + e -> doOpenLink.set(e.getActionCommand().equalsIgnoreCase("Yes"))); + + confirmOpenLink.setVisible(true); + + if (doOpenLink.get()) + Desktop.getDesktop().browse(new URL(textLink).toURI()); + } catch (Exception e) + { + e.printStackTrace(); + } + } else if (!textLink.isEmpty() && attributeSet.getAttribute("type").equals("channel")) + { + try + { + AtomicBoolean doJoinChannel = new AtomicBoolean(false); + + YesNoDialog confirmOpenLink = + new YesNoDialog("Are you sure you want to join channel " + textLink + "?", "Join Channel", + JOptionPane.QUESTION_MESSAGE, + e -> doJoinChannel.set(e.getActionCommand().equalsIgnoreCase("Yes"))); + + confirmOpenLink.setVisible(true); + + if (doJoinChannel.get()) + { + myServer.sendClientText("/join " + textLink, ""); + } + } catch (Exception e) + { e.printStackTrace(); } } @@ -153,7 +342,7 @@ public void execute() public JPopupMenu rightClickMenu() { - if(attributeSet.getAttribute("type").equals("IRCUser")) + if (attributeSet.getAttribute("type").equals("IRCUser")) { fromUser.createPopUp(); return fromUser.myMenu; @@ -171,77 +360,217 @@ public void actionPerformed(ActionEvent e) } } - private void appendString(StyledDocument doc, String insertedString, SimpleAttributeSet style) throws BadLocationException + // Inserts the string at the position + private void insertString(String insertedString, SimpleAttributeSet style, int position) + throws BadLocationException { - int position = doc.getLength(); + // remove the existing attributes + style.removeAttribute("styleStart"); + style.removeAttribute("styleLength"); + // add an attribute so we know when the style is expected to start and end. style.addAttribute("styleStart", position); style.addAttribute("styleLength", insertedString.length()); - style.addAttribute("docLength", doc.getLength()); doc.insertString(position, insertedString, style); } - private SimpleAttributeSet getStyle(String styleName) + // Adds the string (with all needed attributes) to the end of the document + private void appendString(String insertedString, SimpleAttributeSet style) + throws BadLocationException { - switch (styleName) { + int position = doc.getLength(); + + insertString(insertedString, style, position); + } + + public URStyle getStyleDefault(String styleName) + { + switch (styleName) + { case "mediumStyle": - return mediumStyle(); + return mediumStyle(false); case "highStyle": - return highStyle(); + return highStyle(false); + case "nickStyle": + return nickStyle(false); case "myStyle": - return myStyle(); + return myStyle(false); case "lowStyle": - return lowStyle(); + return lowStyle(false); + case "urlStyle": + return urlStyle(false); + case "channelStyle": + return channelStyle(false); default: - return defaultStyle(); + return defaultStyle(null, true); } } + public URStyle getStyle(String styleName, boolean load) + { + // TODO: Might need to readjust this again? + URStyle currentStyle = formatterStyles.get(styleName).clone(); + if(load) + { + currentStyle.load(formatterPrefs); + } + + return currentStyle; + + } - public void updateStyles(StyledDocument doc, int startPosition) + /** + * Reloads all the styles, then updates the doc + * @param startPosition + */ + public void updateStyles(int startPosition) { - AttributeSet textStyle = doc.getCharacterElement(startPosition).getAttributes(); + targetStyle.load(formatterPrefs); + + for (URStyle formatterStyle : formatterStyles.values()) { + formatterStyle = getStyle(formatterStyle.getName(), true); + } + + Constants.LOGGER.log(Level.INFO, "Updating styles."); + updateDocStyles(startPosition); + } + + private void updateDocStyles(int startPosition) + { + SimpleAttributeSet textStyle = new SimpleAttributeSet(doc.getCharacterElement(startPosition).getAttributes()); String styleName = textStyle.getAttribute("name").toString(); - int styleStart = Integer.parseInt(textStyle.getAttribute("styleStart").toString()); + int styleStart = startPosition; int styleLength = Integer.parseInt(textStyle.getAttribute("styleLength").toString()); - SimpleAttributeSet matchingStyle = getStyle(styleName); + SimpleAttributeSet matchingStyle = getStyle(styleName, false); + + boolean isDateStyle = false; + if (null != DriverGUI.gui && null != textStyle.getAttribute("date")) + { + isDateStyle = true; + try + { + Date lineDate = (Date) textStyle.getAttribute("date"); + String newTimeString = UserGUI.getTimeLineString(lineDate) + " "; + boolean hasTime = false; + + if (null != textStyle.getAttribute("type") + && textStyle.getAttribute("type").toString().equalsIgnoreCase("time")) + { + hasTime = true; + doc.remove(styleStart, styleLength); + } + + if (DriverGUI.gui.isTimeStampsEnabled()) + { + textStyle.removeAttribute("date"); + textStyle.removeAttribute("time"); + + if (!hasTime) + doc.setCharacterAttributes(styleStart, styleLength, textStyle, true); + + SimpleAttributeSet timeStyle = getStyle(styleName, false); + timeStyle.addAttribute("date", lineDate); + timeStyle.addAttribute("type", "time"); + insertString(newTimeString, timeStyle, styleStart); + styleLength = newTimeString.length(); + } else + { + if (hasTime) + { + textStyle = new SimpleAttributeSet(doc.getCharacterElement(startPosition).getAttributes()); + + styleName = textStyle.getAttribute("name").toString(); + styleStart = startPosition; + styleLength = Integer.parseInt(textStyle.getAttribute("styleLength").toString()); + + matchingStyle = getStyle(styleName, false); + matchingStyle.addAttribute("date", lineDate); + + isDateStyle = false; + } + } + } catch (BadLocationException $ble) + { + // + } + } // Copy the attributes, but only if they aren't already set - Iterator attributeIterator = textStyle.getAttributeNames().asIterator(); - while(attributeIterator.hasNext()) + Iterator attributeIterator = textStyle.getAttributeNames().asIterator(); + while (attributeIterator.hasNext()) { String nextAttributeName = attributeIterator.next().toString(); - if(matchingStyle.getAttribute(nextAttributeName) == null) + + if (matchingStyle.getAttribute(nextAttributeName) == null) { - matchingStyle.addAttribute(nextAttributeName, textStyle.getAttribute(nextAttributeName)); + Iterator matchingIterator = matchingStyle.getAttributeNames().asIterator(); + boolean needsToBeSet = true; + + while (matchingIterator.hasNext()) + { + if (matchingIterator.next().toString().equalsIgnoreCase(nextAttributeName)) + { + needsToBeSet = false; + break; + } + } + if (needsToBeSet) + matchingStyle.addAttribute(nextAttributeName, textStyle.getAttribute(nextAttributeName)); } } + if (!isDateStyle) + doc.setCharacterAttributes(styleStart, styleLength, matchingStyle, true); - doc.setCharacterAttributes(styleStart, styleLength, matchingStyle, true); + if ((styleStart + styleLength) < doc.getLength()) + updateDocStyles((styleStart + styleLength)); + } - if((styleStart + styleLength) < doc.getLength()) - updateStyles(doc, (styleStart + styleLength)); + public String getFirstLine() throws BadLocationException + { + Element root = doc.getDefaultRootElement(); + int linePos = 0; + + String finalLine = ""; + + while (finalLine.isEmpty()) + { + + if (linePos < 0) + break; + + Element line = root.getElement(linePos++); + + if (null == line) + continue; + + int start = line.getStartOffset(); + int end = line.getEndOffset(); + String text = doc.getText(start, end - start); + finalLine = text.trim(); + } + + return finalLine; } - public String getLatestLine(StyledDocument doc) throws BadLocationException + public String getLatestLine() throws BadLocationException { Element root = doc.getDefaultRootElement(); int lines = root.getElementCount(); String finalLine = ""; - while(finalLine.isEmpty()) + while (finalLine.isEmpty()) { - if(lines < 0) + if (lines < 0) break; - Element line = root.getElement( lines-- ); + Element line = root.getElement(lines--); - if(null == line) + if (null == line) continue; int start = line.getStartOffset(); @@ -253,7 +582,7 @@ public String getLatestLine(StyledDocument doc) throws BadLocationException return finalLine; } - private int getLinePosition(StyledDocument doc, String targetLine) throws BadLocationException + private int getLinePosition(String targetLine) throws BadLocationException { Element root = doc.getDefaultRootElement(); int lines = root.getElementCount(); @@ -262,14 +591,14 @@ private int getLinePosition(StyledDocument doc, String targetLine) throws BadLoc { Element line = root.getElement(i); - if(null == line) + if (null == line) continue; int start = line.getStartOffset(); int end = line.getEndOffset(); String text = doc.getText(start, end - start); - if(text.trim().equals(targetLine.trim())) + if (text.trim().equals(targetLine.trim())) { return start; } @@ -278,97 +607,176 @@ private int getLinePosition(StyledDocument doc, String targetLine) throws BadLoc return 0; } - public SimpleAttributeSet getStyleAtPosition(StyledDocument doc, int position, String relativeLine) throws BadLocationException + public SimpleAttributeSet getStyleAtPosition(int position, String relativeLine) + throws BadLocationException { - if(!relativeLine.isBlank()) - position = position + getLinePosition(doc, relativeLine); + if (!relativeLine.isBlank()) + position = position + getLinePosition(relativeLine); AttributeSet textStyle = doc.getCharacterElement(position).getAttributes(); - String styleName = textStyle.getAttribute("name").toString(); - return getStyle(styleName); + + return new SimpleAttributeSet(textStyle); + } + + private void parseClickableText(IRCUser fromUser, String line, URStyle defaultStyle) + throws BadLocationException + { + HashMap regexStrings = new HashMap<>(); + regexStrings.put(Constants.URL_REGEX, urlStyle); + regexStrings.put(Constants.CHANNEL_REGEX, channelStyle); + // final String line = getLatestLine(doc); + final int relativePosition = getLinePosition(getLatestLine()); + + ArrayList clickableLines = new ArrayList(); + + for (Map.Entry entry : regexStrings.entrySet()) + { + String regex = entry.getKey(); + + + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(line); + + // do stuff for each match + while (matcher.find()) + { + URStyle linkStyle = entry.getValue().clone(); + String clickableLine = matcher.group(1); + linkStyle.addAttribute("clickableText", new ClickableText(clickableLine, linkStyle, fromUser)); + + int styleStart = relativePosition + matcher.start(1); + int styleLength = clickableLine.length(); + + linkStyle.addAttribute("styleStart", styleStart); + linkStyle.addAttribute("styleLength", styleLength); + + clickableLines.add(linkStyle); + } + } + + clickableLines.sort((set1, set2) -> { + int styleStart1 = (int) set1.getAttribute("styleStart"); + int styleStart2 = (int) set2.getAttribute("styleStart"); + return Integer.compare(styleStart1, styleStart2); + }); + + Iterator linesIterator = clickableLines.iterator(); + String remainingLine = line; + while (linesIterator.hasNext()) + { + URStyle nextLine = linesIterator.next(); + + // Offset based on the difference between the original line and the remaining line, + // plus the relativePosition within the document. + int offset = (line.length() - remainingLine.length()) + relativePosition; + int nextLineStart = Integer.parseInt(nextLine.getAttribute("styleStart").toString()); + int nextLineLength = Integer.parseInt(nextLine.getAttribute("styleLength").toString()); + + // Append the string that comes before the next clickable text + appendString(remainingLine.substring(0, nextLineStart - offset), defaultStyle); + + appendString(nextLine.getAttribute("clickableText").toString(), nextLine); + + remainingLine = remainingLine.substring((nextLineStart + nextLineLength) - offset); + } + + appendString(remainingLine, defaultStyle); } /** * Inserts a string onto the end of the doc. - * - * @param doc - * @param timeLine * @param fromUser * @param line + * @param timeLine */ - public void formattedDocument(StyledDocument doc, String timeLine, IRCUser fromUser, String fromString, String line) + public void formattedDocument(Date lineDate, IRCUser fromUser, String fromString, String line) { + // build the timeLine string + String timeLine = UserGUI.getTimeLineString(lineDate); + final URStyle nickPositionStyle; + final URStyle linePositionStyle; + final URStyle timePositionStyle; + if (fromUser != null && null != myNick && myNick.equals(fromUser.toString())) { - nameStyle = this.myStyle(); + // This message is from me + nickPositionStyle = myStyle.clone(); + linePositionStyle = lineStyle.clone(); + timePositionStyle = timeStyle.clone(); + } else if (fromUser == null && fromString.equals(Constants.EVENT_USER)) + { + // This is an event message + nickPositionStyle = lowStyle.clone(); + linePositionStyle = lowStyle.clone(); + timePositionStyle = lowStyle.clone(); } else { - if (null != myNick && line.indexOf(myNick) > -1) - nameStyle = highStyle(); + // This message is from someone else + // Does this message have my nick in it? + if (myNick != null && line.indexOf(myNick) > -1) + nickPositionStyle = highStyle.clone(); else - nameStyle = defaultStyle(); - } + nickPositionStyle = nickStyle.clone(); - if (fromUser == null && fromString.equals(Constants.EVENT_USER)) - { - nameStyle = lowStyle(); - lineStyle = lowStyle(); - } else { - lineStyle = defaultStyle(); + linePositionStyle = lineStyle.clone(); + timePositionStyle = timeStyle.clone(); } - timeStyle = defaultStyle(); - try { // doc.insertString(doc.getLength(), timeLine, timeStyle); - appendString(doc, timeLine, timeStyle); - appendString(doc, " <", lineStyle); + // if(null != timeLine && !timeLine.isBlank()) + if (!timeLine.isBlank() && DriverGUI.gui.isTimeStampsEnabled()) + { + // add the date to the end of the string to preserve the timestamp of the line + // when updating styles + timePositionStyle.addAttribute("date", lineDate); + timePositionStyle.removeAttribute("type"); + timePositionStyle.addAttribute("type", "time"); + appendString(timeLine + " ", timePositionStyle); + timePositionStyle.removeAttribute("type"); + linePositionStyle.removeAttribute("date"); + } else + { + linePositionStyle.addAttribute("date", lineDate); + } + + appendString("<", linePositionStyle); + linePositionStyle.removeAttribute("date"); - if(fromUser != null) + if (fromUser != null) { - SimpleAttributeSet clickableNameStyle = nameStyle; + URStyle clickableNameStyle = nickPositionStyle; clickableNameStyle.addAttribute("type", "IRCUser"); - clickableNameStyle.addAttribute("clickableText", new ClickableText(fromUser.toString(), nameStyle, fromUser)); + clickableNameStyle.addAttribute("clickableText", + new ClickableText(fromUser.toString(), nickPositionStyle, fromUser)); // doc.insertString(doc.getLength(), fromUser.toString(), clickableNameStyle); - appendString(doc, fromUser.toString(), clickableNameStyle); - } else { - appendString(doc, fromString, nameStyle); + appendString(fromUser.toString(), clickableNameStyle); + } else + { + appendString(fromString, nickPositionStyle); } - appendString(doc, "> ", lineStyle); - - // find and match against any URLs that may be in the text - Pattern pattern = Pattern.compile(Constants.URL_REGEX); - Matcher matcher = pattern.matcher(line); - SimpleAttributeSet linkStyle = urlStyle(); - - while (matcher.find()) { - // pre http - appendString(doc, line.substring(0, matcher.start()), lineStyle); - - // http "clickableText" - String httpLine = line.substring(matcher.start(), matcher.end()); - linkStyle.addAttribute("clickableText", new ClickableText(httpLine, linkStyle, fromUser)); - appendString(doc, httpLine, linkStyle); - - // post http - line = line.substring(matcher.end()); - - // search again - matcher = pattern.matcher(line); - } + appendString(">", linePositionStyle); // print the remaining text - appendString(doc, line, lineStyle); + // appendString(doc, " "+line, lineStyle); + + // parse the outputted line for clickable text + parseClickableText(fromUser, " " + line, linePositionStyle); - appendString(doc, System.getProperty("line.separator"), lineStyle); + appendString(System.getProperty("line.separator"), linePositionStyle); } catch (BadLocationException e) { Constants.LOGGER.log(Level.SEVERE, e.getLocalizedMessage()); } } + public StyledDocument getDocument() + { + return doc; + } + } diff --git a/src/urChatBasic/frontend/UserGUI.java b/src/urChatBasic/frontend/UserGUI.java index 9810afe..c18ee9f 100644 --- a/src/urChatBasic/frontend/UserGUI.java +++ b/src/urChatBasic/frontend/UserGUI.java @@ -5,21 +5,31 @@ import java.util.prefs.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.text.SimpleDateFormat; import java.util.*; import java.util.List; import javax.swing.*; import javax.swing.UIManager.LookAndFeelInfo; import javax.swing.event.*; +import javax.swing.text.AttributeSet; +import javax.swing.text.Element; +import javax.swing.text.StyledDocument; +import urChatBasic.backend.utils.URStyle; +import urChatBasic.backend.utils.URUncaughtExceptionHandler; import urChatBasic.base.Constants; import urChatBasic.base.IRCRoomBase; import urChatBasic.base.IRCServerBase; import urChatBasic.frontend.dialogs.FontDialog; import urChatBasic.frontend.dialogs.MessageDialog; import urChatBasic.base.UserGUIBase; +import urChatBasic.base.Constants.Size; import urChatBasic.base.capabilities.CapTypeBase; import urChatBasic.base.capabilities.CapabilityTypes; +import urChatBasic.frontend.LineFormatter.ClickableText; import urChatBasic.frontend.components.*; public class UserGUI extends JPanel implements Runnable, UserGUIBase @@ -49,11 +59,12 @@ public class UserGUI extends JPanel implements Runnable, UserGUIBase private ProfilePicker profilePicker; // Client Options Panel - private static final JPanel optionsClientPanel = new JPanel(); - private static final JScrollPane clientScroller = new JScrollPane(optionsClientPanel); - private static final JLabel lafOptionsLabel = new JLabel("Theme:"); + private static final JPanel interfacePanel = new JPanel(); + private static final JScrollPane interfaceScroller = new JScrollPane(interfacePanel); + + private static final JComboBox lafOptions = + new JComboBox(UIManager.getInstalledLookAndFeels()); - private static final JComboBox lafOptions = new JComboBox(UIManager.getInstalledLookAndFeels()); private static final JCheckBox showEventTicker = new JCheckBox("Show Event Ticker"); private static final JCheckBox showUsersList = new JCheckBox("Show Users List"); private static final JCheckBox enableClickableLinks = new JCheckBox("Make links clickable"); @@ -65,7 +76,16 @@ public class UserGUI extends JPanel implements Runnable, UserGUIBase private static final JCheckBox limitServerLines = new JCheckBox("Limit the number of lines in Server activity"); private static final JCheckBox limitChannelLines = new JCheckBox("Limit the number of lines in channel text"); private static final JCheckBox enableTimeStamps = new JCheckBox("Time Stamp chat messages"); + + // Appearance Panel private FontPanel clientFontPanel; + private static URStyle guiStyle; + private static final JTextField timeStampField = new JTextField(); + private static final JTextPane previewTextArea = new JTextPane(); + private static final JScrollPane previewTextScroll = new JScrollPane(previewTextArea); + private static final JLabel styleLabel = new JLabel("Mouse over text to view style, right-click to edit."); + private static LineFormatter previewLineFormatter; + private static final JTextField limitServerLinesCount = new JTextField(); private static final JTextField limitChannelLinesCount = new JTextField(); @@ -79,8 +99,12 @@ public class UserGUI extends JPanel implements Runnable, UserGUIBase new JSlider(JSlider.HORIZONTAL, TICKER_DELAY_MIN, TICKER_DELAY_MAX, TICKER_DELAY_INIT); // Server Options Panel - private static final JPanel serverOptionsPanel = new JPanel(); - private static final JScrollPane serverScroller = new JScrollPane(serverOptionsPanel); + private static final JPanel connectionPanel = new JPanel(); + private static final JScrollPane connectionScroller = new JScrollPane(connectionPanel); + + // Appearance Options Panel + private static final JPanel appearancePanel = new JPanel(); + private static final JScrollPane appearanceScroller = new JScrollPane(appearancePanel); // Identification private static final JLabel userNameLabel = new JLabel("Nick:"); @@ -143,6 +167,16 @@ public int getLimitServerLinesCount() } } + public void setLimitChannelLines(int limit) + { + limitChannelLinesCount.setText(Integer.toString(limit)); + } + + public void setLimitServerLines(int limit) + { + limitServerLinesCount.setText(Integer.toString(limit)); + } + /* * (non-Javadoc) * @@ -248,7 +282,7 @@ public IRCServerBase getCreatedServer(String serverName) public void setProfileName(String newProfileName) { // save the current profile settings, if it exists - if(profilePicker.profileExists(profileName)) + if (profilePicker.profileExists(profileName)) { setClientSettings(); } @@ -368,6 +402,11 @@ public Boolean isJoinsQuitsMainEnabled() return showJoinsQuitsMainWindow.isSelected(); } + public void setJoinsQuitsMain(boolean enable) + { + showJoinsQuitsMainWindow.setSelected(enable); + } + /* * (non-Javadoc) * @@ -438,8 +477,9 @@ private void setupOptionsPanel() { optionsMainPanel.setLayout(new BorderLayout()); - optionsArray.addElement("Server"); - optionsArray.addElement("Client"); + optionsArray.addElement("Connection"); + optionsArray.addElement("Interface"); + optionsArray.addElement("Appearance"); setupLeftOptionsPanel(); setupRightOptionsPanel(); @@ -448,11 +488,9 @@ private void setupOptionsPanel() optionsMainPanel.add(optionsRightPanel, BorderLayout.CENTER); optionsList.setSelectedIndex(OPTIONS_INDEX); - // optionsClientPanel.setPreferredSize(new Dimension(500, 0)); - // serverOptionsPanel.setPreferredSize(new Dimension(200, 0)); - - optionsRightPanel.add(serverScroller, "Server"); - optionsRightPanel.add(clientScroller, "Client"); + optionsRightPanel.add(connectionScroller, "Connection"); + optionsRightPanel.add(interfaceScroller, "Interface"); + optionsRightPanel.add(appearanceScroller, "Appearance"); } /** @@ -486,89 +524,140 @@ private void setupRightOptionsPanel() // optionsRightPanel.setBackground(Color.BLACK); optionsRightPanel.setLayout(new CardLayout()); - setupServerOptionsPanelComponents(); - setupClientOptionsPanelComponents(); + setupConnectionPanel(); + setupInterfacePanel(); + setupAppearancePanel(); + } + + private static void addToPanel(JPanel targetPanel, Component newComponent, String label, Size targetSize) + { + + int topSpacing = 6; + final int TOP_ALIGNED = 0; + final int LEFT_ALIGNED = 0; + final int LEFT_SPACING = 6; + + if (null != label && !label.isBlank()) + { + addToPanel(targetPanel, new JLabel(label + ":"), null, targetSize); + // There is a label, so we want the added component to be aligned with the label + topSpacing = 0; + } + + if (targetPanel.getLayout().getClass() != SpringLayout.class) + { + targetPanel.setLayout(new SpringLayout()); + } + + SpringLayout layout = (SpringLayout) targetPanel.getLayout(); + Component[] components = targetPanel.getComponents(); + + if (components.length > 0) + { + Component previousComponent = components[components.length - 1]; + + // Add newComponent to the targetPanel + targetPanel.add(newComponent); + + // Set constraints for newComponent + layout.putConstraint(SpringLayout.NORTH, newComponent, topSpacing, SpringLayout.SOUTH, previousComponent); + layout.putConstraint(SpringLayout.WEST, newComponent, LEFT_ALIGNED, SpringLayout.WEST, previousComponent); + + if (null != targetSize && newComponent instanceof JTextField) + ((JTextField) newComponent).setColumns(12); + } else + { + // If it's the first component, align it against the targetPanel + targetPanel.add(newComponent); + + // Set constraints for newComponent when it's the first component + layout.putConstraint(SpringLayout.NORTH, newComponent, topSpacing * 2, SpringLayout.NORTH, targetPanel); + layout.putConstraint(SpringLayout.WEST, newComponent, LEFT_SPACING * 2, SpringLayout.WEST, targetPanel); + + if (null != targetSize && newComponent instanceof JTextField) + ((JTextField) newComponent).setColumns(12); + } } /** * Add the components to the Server Options Panel. */ - private void setupServerOptionsPanelComponents() + private void setupConnectionPanel() { - // serverOptionsPanel.setLayout(new BoxLayout(serverOptionsPanel, BoxLayout.PAGE_AXIS)); - setupServerOptionsLayout(); + // connectionPanel.setLayout(new BoxLayout(connectionPanel, BoxLayout.PAGE_AXIS)); + setupConnectionLayout(); // User stuff - serverOptionsPanel.add(userNameLabel); - serverOptionsPanel.add(userNameTextField); + connectionPanel.add(userNameLabel); + connectionPanel.add(userNameTextField); // userNameTextField.setPreferredSize(new Dimension(100, 24)); // userNameTextField.setMinimumSize(new Dimension(100, 0)); - serverOptionsPanel.add(realNameLabel); - serverOptionsPanel.add(realNameTextField); + connectionPanel.add(realNameLabel); + connectionPanel.add(realNameTextField); // realNameTextField.setMinimumSize(new Dimension(100, 0)); - serverOptionsPanel.add(authenticationTypeLabel); - serverOptionsPanel.add(authenticationTypeChoice); + connectionPanel.add(authenticationTypeLabel); + connectionPanel.add(authenticationTypeChoice); authenticationTypeChoice.addActionListener(new UCAuthTypeComboBoxChangeHandler()); // authenticationTypeChoice.setPreferredSize(new Dimension(200, 20)); - serverOptionsPanel.add(passwordLabel); - serverOptionsPanel.add(passwordTextField); + connectionPanel.add(passwordLabel); + connectionPanel.add(passwordTextField); passwordTextField.setEchoChar('*'); - serverOptionsPanel.add(rememberPassLabel); - serverOptionsPanel.add(rememberPassCheckBox); + connectionPanel.add(rememberPassLabel); + connectionPanel.add(rememberPassCheckBox); // passwordTextField.setPreferredSize(new Dimension(200, 20)); // Server Stuff - serverOptionsPanel.add(serverNameLabel); - serverOptionsPanel.add(servernameTextField); + connectionPanel.add(serverNameLabel); + connectionPanel.add(servernameTextField); // servernameTextField.setPreferredSize(new Dimension(100, 20)); - serverOptionsPanel.add(serverPortLabel); - serverOptionsPanel.add(serverPortTextField); + connectionPanel.add(serverPortLabel); + connectionPanel.add(serverPortTextField); // serverPortTextField.setPreferredSize(new Dimension(50, 20)); - serverOptionsPanel.add(serverUseTLSLabel); - serverOptionsPanel.add(serverTLSCheckBox); + connectionPanel.add(serverUseTLSLabel); + connectionPanel.add(serverTLSCheckBox); // serverTLSCheckBox.setPreferredSize(new Dimension(50, 20)); // Proxy Stuff - serverOptionsPanel.add(proxyHostLabel); - serverOptionsPanel.add(proxyHostNameTextField); + connectionPanel.add(proxyHostLabel); + connectionPanel.add(proxyHostNameTextField); // proxyHostNameTextField.setPreferredSize(new Dimension(100, 20)); - serverOptionsPanel.add(proxyPortLabel); - serverOptionsPanel.add(proxyPortTextField); + connectionPanel.add(proxyPortLabel); + connectionPanel.add(proxyPortTextField); // proxyPortTextField.setPreferredSize(new Dimension(50, 20)); - serverOptionsPanel.add(serverUseProxyLabel); - serverOptionsPanel.add(serverProxyCheckBox); + connectionPanel.add(serverUseProxyLabel); + connectionPanel.add(serverProxyCheckBox); // serverProxyCheckBox.setPreferredSize(new Dimension(50, 20)); // Channel Stuff - serverOptionsPanel.add(firstChannelLabel); - serverOptionsPanel.add(firstChannelTextField); + connectionPanel.add(firstChannelLabel); + connectionPanel.add(firstChannelTextField); // firstChannelTextField.setPreferredSize(new Dimension(100, 20)); - serverOptionsPanel.add(connectButton); + connectionPanel.add(connectButton); connectButton.addActionListener(new ConnectPressed()); - serverOptionsPanel.add(autoConnectToFavourites); + connectionPanel.add(autoConnectToFavourites); favouritesScroller.setPreferredSize(new Dimension(200, 100)); favouritesList.addMouseListener(new FavouritesPopClickListener()); - serverOptionsPanel.add(favouritesScroller); + connectionPanel.add(favouritesScroller); } /** * Aligns components on the Server Options Panel */ - private void setupServerOptionsLayout() + private void setupConnectionLayout() { - SpringLayout serverLayout = new SpringLayout(); - serverOptionsPanel.setLayout(serverLayout); + SpringLayout connectionLayout = new SpringLayout(); + connectionPanel.setLayout(connectionLayout); // Used to make it more obvious what is going on - // and perhaps more readable. @@ -582,178 +671,176 @@ private void setupServerOptionsLayout() // Components are aligned off the top label // User stuff - serverLayout.putConstraint(SpringLayout.NORTH, userNameLabel, TOP_SPACING * 2, SpringLayout.NORTH, - serverOptionsPanel); - serverLayout.putConstraint(SpringLayout.WEST, userNameLabel, LEFT_SPACING * 2, SpringLayout.WEST, - serverOptionsPanel); + connectionLayout.putConstraint(SpringLayout.NORTH, userNameLabel, TOP_SPACING * 2, SpringLayout.NORTH, + connectionPanel); + connectionLayout.putConstraint(SpringLayout.WEST, userNameLabel, LEFT_SPACING * 2, SpringLayout.WEST, + connectionPanel); - serverLayout.putConstraint(SpringLayout.NORTH, userNameTextField, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, userNameTextField, TOP_ALIGNED, SpringLayout.SOUTH, userNameLabel); - serverLayout.putConstraint(SpringLayout.WEST, userNameTextField, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, userNameTextField, LEFT_ALIGNED, SpringLayout.WEST, userNameLabel); - serverLayout.putConstraint(SpringLayout.NORTH, realNameLabel, TOP_SPACING, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, realNameLabel, TOP_SPACING, SpringLayout.SOUTH, userNameTextField); - serverLayout.putConstraint(SpringLayout.WEST, realNameLabel, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, realNameLabel, LEFT_ALIGNED, SpringLayout.WEST, userNameTextField); - serverLayout.putConstraint(SpringLayout.NORTH, realNameTextField, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, realNameTextField, TOP_ALIGNED, SpringLayout.SOUTH, realNameLabel); - serverLayout.putConstraint(SpringLayout.WEST, realNameTextField, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, realNameTextField, LEFT_ALIGNED, SpringLayout.WEST, realNameLabel); - serverLayout.putConstraint(SpringLayout.EAST, realNameTextField, RIGHT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.EAST, realNameTextField, RIGHT_ALIGNED, SpringLayout.EAST, userNameTextField); // Authentication Stuff - serverLayout.putConstraint(SpringLayout.NORTH, authenticationTypeLabel, TOP_SPACING, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, authenticationTypeLabel, TOP_SPACING, SpringLayout.SOUTH, realNameTextField); - serverLayout.putConstraint(SpringLayout.WEST, authenticationTypeLabel, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, authenticationTypeLabel, LEFT_ALIGNED, SpringLayout.WEST, realNameTextField); - serverLayout.putConstraint(SpringLayout.NORTH, authenticationTypeChoice, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, authenticationTypeChoice, TOP_ALIGNED, SpringLayout.SOUTH, authenticationTypeLabel); - serverLayout.putConstraint(SpringLayout.WEST, authenticationTypeChoice, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, authenticationTypeChoice, LEFT_ALIGNED, SpringLayout.WEST, authenticationTypeLabel); - serverLayout.putConstraint(SpringLayout.EAST, authenticationTypeChoice, RIGHT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.EAST, authenticationTypeChoice, RIGHT_ALIGNED, SpringLayout.EAST, realNameTextField); // Password - serverLayout.putConstraint(SpringLayout.NORTH, passwordLabel, TOP_SPACING, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, passwordLabel, TOP_SPACING, SpringLayout.SOUTH, authenticationTypeChoice); - serverLayout.putConstraint(SpringLayout.WEST, passwordLabel, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, passwordLabel, LEFT_ALIGNED, SpringLayout.WEST, authenticationTypeChoice); - serverLayout.putConstraint(SpringLayout.NORTH, passwordTextField, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, passwordTextField, TOP_ALIGNED, SpringLayout.SOUTH, passwordLabel); - serverLayout.putConstraint(SpringLayout.WEST, passwordTextField, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, passwordTextField, LEFT_ALIGNED, SpringLayout.WEST, passwordLabel); - serverLayout.putConstraint(SpringLayout.EAST, passwordTextField, RIGHT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.EAST, passwordTextField, RIGHT_ALIGNED, SpringLayout.EAST, authenticationTypeChoice); - serverLayout.putConstraint(SpringLayout.NORTH, rememberPassLabel, TOP_ALIGNED, SpringLayout.NORTH, + connectionLayout.putConstraint(SpringLayout.NORTH, rememberPassLabel, TOP_ALIGNED, SpringLayout.NORTH, passwordLabel); - serverLayout.putConstraint(SpringLayout.WEST, rememberPassLabel, LEFT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.WEST, rememberPassLabel, LEFT_ALIGNED, SpringLayout.EAST, passwordTextField); - serverLayout.putConstraint(SpringLayout.NORTH, rememberPassCheckBox, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, rememberPassCheckBox, TOP_ALIGNED, SpringLayout.SOUTH, rememberPassLabel); - serverLayout.putConstraint(SpringLayout.WEST, rememberPassCheckBox, LEFT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.WEST, rememberPassCheckBox, LEFT_ALIGNED, SpringLayout.EAST, passwordTextField); // Server stuff - serverLayout.putConstraint(SpringLayout.NORTH, serverNameLabel, TOP_SPACING, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, serverNameLabel, TOP_SPACING, SpringLayout.SOUTH, passwordTextField); - serverLayout.putConstraint(SpringLayout.WEST, serverNameLabel, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, serverNameLabel, LEFT_ALIGNED, SpringLayout.WEST, passwordTextField); - serverLayout.putConstraint(SpringLayout.NORTH, servernameTextField, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, servernameTextField, TOP_ALIGNED, SpringLayout.SOUTH, serverNameLabel); - serverLayout.putConstraint(SpringLayout.WEST, servernameTextField, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, servernameTextField, LEFT_ALIGNED, SpringLayout.WEST, serverNameLabel); - serverLayout.putConstraint(SpringLayout.NORTH, serverPortLabel, TOP_ALIGNED, SpringLayout.NORTH, + connectionLayout.putConstraint(SpringLayout.NORTH, serverPortLabel, TOP_ALIGNED, SpringLayout.NORTH, serverNameLabel); - serverLayout.putConstraint(SpringLayout.WEST, serverPortLabel, LEFT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.WEST, serverPortLabel, LEFT_ALIGNED, SpringLayout.EAST, servernameTextField); - serverLayout.putConstraint(SpringLayout.NORTH, serverPortTextField, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, serverPortTextField, TOP_ALIGNED, SpringLayout.SOUTH, serverPortLabel); - serverLayout.putConstraint(SpringLayout.WEST, serverPortTextField, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, serverPortTextField, LEFT_ALIGNED, SpringLayout.WEST, serverPortLabel); - serverLayout.putConstraint(SpringLayout.NORTH, serverUseTLSLabel, TOP_ALIGNED, SpringLayout.NORTH, + connectionLayout.putConstraint(SpringLayout.NORTH, serverUseTLSLabel, TOP_ALIGNED, SpringLayout.NORTH, serverPortLabel); - serverLayout.putConstraint(SpringLayout.WEST, serverUseTLSLabel, LEFT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.WEST, serverUseTLSLabel, LEFT_ALIGNED, SpringLayout.EAST, serverPortTextField); - serverLayout.putConstraint(SpringLayout.NORTH, serverTLSCheckBox, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, serverTLSCheckBox, TOP_ALIGNED, SpringLayout.SOUTH, serverUseTLSLabel); - serverLayout.putConstraint(SpringLayout.WEST, serverTLSCheckBox, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, serverTLSCheckBox, LEFT_ALIGNED, SpringLayout.WEST, serverUseTLSLabel); // Proxy stuff - serverLayout.putConstraint(SpringLayout.NORTH, proxyHostLabel, TOP_SPACING, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, proxyHostLabel, TOP_SPACING, SpringLayout.SOUTH, servernameTextField); - serverLayout.putConstraint(SpringLayout.WEST, proxyHostLabel, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, proxyHostLabel, LEFT_ALIGNED, SpringLayout.WEST, servernameTextField); - serverLayout.putConstraint(SpringLayout.NORTH, proxyHostNameTextField, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, proxyHostNameTextField, TOP_ALIGNED, SpringLayout.SOUTH, proxyHostLabel); - serverLayout.putConstraint(SpringLayout.WEST, proxyHostNameTextField, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, proxyHostNameTextField, LEFT_ALIGNED, SpringLayout.WEST, proxyHostLabel); - serverLayout.putConstraint(SpringLayout.EAST, proxyHostNameTextField, RIGHT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.EAST, proxyHostNameTextField, RIGHT_ALIGNED, SpringLayout.EAST, servernameTextField); - serverLayout.putConstraint(SpringLayout.NORTH, proxyPortLabel, TOP_ALIGNED, SpringLayout.NORTH, proxyHostLabel); - serverLayout.putConstraint(SpringLayout.WEST, proxyPortLabel, LEFT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.NORTH, proxyPortLabel, TOP_ALIGNED, SpringLayout.NORTH, + proxyHostLabel); + connectionLayout.putConstraint(SpringLayout.WEST, proxyPortLabel, LEFT_ALIGNED, SpringLayout.EAST, proxyHostNameTextField); - serverLayout.putConstraint(SpringLayout.NORTH, proxyPortTextField, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, proxyPortTextField, TOP_ALIGNED, SpringLayout.SOUTH, proxyPortLabel); - serverLayout.putConstraint(SpringLayout.WEST, proxyPortTextField, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, proxyPortTextField, LEFT_ALIGNED, SpringLayout.WEST, proxyPortLabel); - serverLayout.putConstraint(SpringLayout.EAST, proxyPortTextField, RIGHT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.EAST, proxyPortTextField, RIGHT_ALIGNED, SpringLayout.EAST, serverPortTextField); - serverLayout.putConstraint(SpringLayout.NORTH, serverUseProxyLabel, TOP_ALIGNED, SpringLayout.NORTH, + connectionLayout.putConstraint(SpringLayout.NORTH, serverUseProxyLabel, TOP_ALIGNED, SpringLayout.NORTH, proxyPortLabel); - serverLayout.putConstraint(SpringLayout.WEST, serverUseProxyLabel, LEFT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.WEST, serverUseProxyLabel, LEFT_ALIGNED, SpringLayout.EAST, proxyPortTextField); - serverLayout.putConstraint(SpringLayout.NORTH, serverProxyCheckBox, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, serverProxyCheckBox, TOP_ALIGNED, SpringLayout.SOUTH, serverUseProxyLabel); - serverLayout.putConstraint(SpringLayout.WEST, serverProxyCheckBox, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, serverProxyCheckBox, LEFT_ALIGNED, SpringLayout.WEST, serverUseProxyLabel); // Channel Stuff - serverLayout.putConstraint(SpringLayout.NORTH, firstChannelLabel, TOP_SPACING, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, firstChannelLabel, TOP_SPACING, SpringLayout.SOUTH, proxyHostNameTextField); - serverLayout.putConstraint(SpringLayout.WEST, firstChannelLabel, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, firstChannelLabel, LEFT_ALIGNED, SpringLayout.WEST, proxyHostNameTextField); - serverLayout.putConstraint(SpringLayout.NORTH, firstChannelTextField, TOP_ALIGNED, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, firstChannelTextField, TOP_ALIGNED, SpringLayout.SOUTH, firstChannelLabel); - serverLayout.putConstraint(SpringLayout.WEST, firstChannelTextField, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, firstChannelTextField, LEFT_ALIGNED, SpringLayout.WEST, firstChannelLabel); - serverLayout.putConstraint(SpringLayout.EAST, firstChannelTextField, RIGHT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.EAST, firstChannelTextField, RIGHT_ALIGNED, SpringLayout.EAST, proxyHostNameTextField); - serverLayout.putConstraint(SpringLayout.NORTH, connectButton, TOP_SPACING * TOP_SPACING, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, connectButton, TOP_SPACING * TOP_SPACING, SpringLayout.SOUTH, firstChannelTextField); - serverLayout.putConstraint(SpringLayout.WEST, connectButton, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, connectButton, LEFT_ALIGNED, SpringLayout.WEST, firstChannelTextField); - serverLayout.putConstraint(SpringLayout.NORTH, autoConnectToFavourites, TOP_ALIGNED, SpringLayout.NORTH, + connectionLayout.putConstraint(SpringLayout.NORTH, autoConnectToFavourites, TOP_ALIGNED, SpringLayout.NORTH, userNameLabel); - serverLayout.putConstraint(SpringLayout.WEST, autoConnectToFavourites, LEFT_SPACING, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.WEST, autoConnectToFavourites, LEFT_SPACING, SpringLayout.EAST, serverUseProxyLabel); - serverLayout.putConstraint(SpringLayout.NORTH, favouritesScroller, TOP_SPACING, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.NORTH, favouritesScroller, TOP_SPACING, SpringLayout.SOUTH, autoConnectToFavourites); - serverLayout.putConstraint(SpringLayout.WEST, favouritesScroller, LEFT_ALIGNED, SpringLayout.WEST, + connectionLayout.putConstraint(SpringLayout.WEST, favouritesScroller, LEFT_ALIGNED, SpringLayout.WEST, autoConnectToFavourites); - serverLayout.putConstraint(SpringLayout.EAST, favouritesScroller, LEFT_ALIGNED, SpringLayout.EAST, + connectionLayout.putConstraint(SpringLayout.EAST, favouritesScroller, LEFT_ALIGNED, SpringLayout.EAST, autoConnectToFavourites); - serverLayout.putConstraint(SpringLayout.SOUTH, favouritesScroller, TOP_SPACING, SpringLayout.SOUTH, + connectionLayout.putConstraint(SpringLayout.SOUTH, favouritesScroller, TOP_SPACING, SpringLayout.SOUTH, connectButton); } - private void setupClientOptionsPanelComponents() + private void setupAppearancePanel() { - - // clientScroller.setPreferredSize(new Dimension(this.getSize())); - // clientScroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); - // Settings for these are loaded with the settings API - // found in getClientSettings() - optionsClientPanel.add(lafOptionsLabel); - optionsClientPanel.add(lafOptions); + addToPanel(appearancePanel, lafOptions, "Theme", Size.MEDIUM); // Set a custom renderer to display the look and feel names - lafOptions.setRenderer(new DefaultListCellRenderer() { + lafOptions.setRenderer(new DefaultListCellRenderer() + { @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, + boolean cellHasFocus) + { LookAndFeelInfo info = (LookAndFeelInfo) value; return super.getListCellRendererComponent(list, info.getName(), index, isSelected, cellHasFocus); } @@ -761,24 +848,187 @@ public Component getListCellRendererComponent(JList list, Object value, int i lafOptions.addActionListener(new ChangeLAFListener()); - optionsClientPanel.add(showEventTicker); - optionsClientPanel.add(showUsersList); - optionsClientPanel.add(enableClickableLinks); - optionsClientPanel.add(showJoinsQuitsEventTicker); - optionsClientPanel.add(showJoinsQuitsMainWindow); - optionsClientPanel.add(logChannelText); - optionsClientPanel.add(logServerActivity); - optionsClientPanel.add(logClientText); - optionsClientPanel.add(limitServerLines); - optionsClientPanel.add(limitServerLinesCount); - optionsClientPanel.add(limitChannelLines); - optionsClientPanel.add(limitChannelLinesCount); - optionsClientPanel.add(enableTimeStamps); - - clientFontPanel = new FontPanel(getFont(), getProfilePath()); - clientFontPanel.setPreferredSize(new Dimension(500, 48)); - clientFontPanel.getSaveButton().addActionListener(new SaveFontListener()); - optionsClientPanel.add(clientFontPanel); + clientFontPanel = new FontPanel("", getStyle(), getProfilePath()); + clientFontPanel.setPreferredSize(new Dimension(700, 64)); + clientFontPanel.addActionListener(clientFontPanel.getSaveButton(), new SaveFontListener()); + + // clientFontPanel.getSaveButton().addActionListener(new SaveFontListener()); + clientFontPanel.getResetButton().addActionListener(new ResetFontListener()); + + previewTextScroll.setPreferredSize(new Dimension(700, 150)); + previewTextArea.setEditable(false); + + timeStampField.addKeyListener(new KeyListener() + { + @Override + public void keyTyped(KeyEvent e) + { + // Not used + } + + @Override + public void keyPressed(KeyEvent e) + { + // Not used + } + + @Override + public void keyReleased(KeyEvent e) + { + updatePreviewTextArea(); + } + }); + + updatePreviewTextArea(); + // private static final JLabel timeStampFontLabel = new JLabel("Timestamp Font"); + // private static final JButton otherNickFontLabel = new JButton("Other Nick Font"); + // private static final JButton userNickFontLabel = new JButton("My Nick Font"); + // private static final JButton lowStyleFontLabel = new JButton("Low Priority Text Font"); + // private static final JButton mediumStyleFontLabel = new JButton("Medium Priority Text Font"); + // private static final JButton highStyleFontLabel = new JButton("High Priority Text Font"); + + addToPanel(appearancePanel, clientFontPanel, "Profile Font", null); + addToPanel(appearancePanel, timeStampField, "Timestamp Format", Size.MEDIUM); + + addToPanel(appearancePanel, previewTextScroll, "Font Preview", null); + addToPanel(appearancePanel, styleLabel, "Preview Style", null); + // addToPanel(appearancePanel, timeStampFontButton); + } + + public void updatePreviewTextArea() + { + StyledDocument previewDoc = previewTextArea.getStyledDocument(); + + // try + // { + // // Clear all text + // previewDoc.remove(0, previewDoc.getLength()); + // } catch (BadLocationException e) + // { + // // TODO Auto-generated catch block + // e.printStackTrace(); + // } + + // previewTextArea.setFont(clientFontPanel.getFont()); + previewLineFormatter = new LineFormatter(clientFontPanel.getStyle(), previewTextArea , null, getProfilePath()); + + if (previewDoc.getLength() <= 0) + { + previewTextArea.setCaretPosition(previewTextArea.getDocument().getLength()); + previewTextArea.addMouseListener(new PreviewClickListener()); + previewTextArea.addMouseMotionListener(new PreviewMovementListener()); + IRCUser tempUser = new IRCUser(null, "matty_r"); + IRCUser tempUser2 = new IRCUser(null, System.getProperty("user.name")); + previewLineFormatter.setNick(System.getProperty("user.name")); + previewLineFormatter.formattedDocument(new Date(), null, Constants.EVENT_USER, "urChat has loaded - this is an Event"); + previewLineFormatter.formattedDocument(new Date(), tempUser, "matty_r", "Normal line. Hello, world!"); + previewLineFormatter.formattedDocument(new Date(), tempUser, "matty_r", "This is what it looks like when your nick is mentioned, " + System.getProperty("user.name") + "!"); + previewLineFormatter.formattedDocument(new Date(), tempUser2, System.getProperty("user.name"), "Go to https://github.com/matty-r/urChat"); + previewLineFormatter.formattedDocument(new Date(), tempUser2, System.getProperty("user.name"), "Join #urchatclient on irc.libera.chat or #anotherroom"); + } else + { + previewLineFormatter.updateStyles(0); + } + } + + class PreviewClickListener extends MouseInputAdapter + { + public void mouseClicked(MouseEvent mouseEvent) + { + StyledDocument doc = previewTextArea.getStyledDocument(); + Element wordElement = doc.getCharacterElement(previewTextArea.viewToModel2D((mouseEvent.getPoint()))); + AttributeSet wordAttributeSet = wordElement.getAttributes(); + ClickableText isClickableText = (ClickableText) wordAttributeSet.getAttribute("clickableText"); + + if (SwingUtilities.isRightMouseButton(mouseEvent) && wordAttributeSet.getAttribute("name") != null) + { + String styleName = styleLabel.getText(); + FontDialog styleFontDialog = new FontDialog(styleName, + previewLineFormatter.getStyleDefault(styleName), getProfilePath()); + + styleFontDialog.addSaveListener(arg0 -> { + // List actionListeners = styleFontDialog.getFontPanel().getActionListeners(); + // TODO: Need to save attributes and updateStyles after.. + // Currently runs the save after updateStyles + previewLineFormatter.updateStyles(0); + }); + + // styleFontDialog.addResetListener(new ActionListener() { + + // @Override + // public void actionPerformed(ActionEvent arg0) { + // try { + // getProfilePath().node(styleName).removeNode(); + // } catch (BackingStoreException e) { + // // TODO Auto-generated catch block + // e.printStackTrace(); + // } + + // previewLineFormatter.updateStyles(doc, 0); + // } + + // }); + + + styleFontDialog.setVisible(true); + } else if (SwingUtilities.isLeftMouseButton(mouseEvent) && null != isClickableText) + { + isClickableText.execute(); + } + } + } + + class PreviewMovementListener extends MouseAdapter + { + public void mouseMoved(MouseEvent e) + { + StyledDocument doc = previewTextArea.getStyledDocument(); + Element wordElement = doc.getCharacterElement(previewTextArea.viewToModel2D((e.getPoint()))); + AttributeSet wordAttributeSet = wordElement.getAttributes(); + ClickableText isClickableText = (ClickableText) wordAttributeSet.getAttribute("clickableText"); + + if (wordAttributeSet.getAttribute("name") != null) + styleLabel.setText(wordAttributeSet.getAttribute("name").toString()); + else + styleLabel.setText("Mouse over text to view style, right-click to edit."); + + if (isClickableText != null && isClickableLinksEnabled()) + { + previewTextArea.setCursor(new Cursor(Cursor.HAND_CURSOR)); + } else + { + previewTextArea.setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); + } + } + } + + public static String getTimeLineString(Date date) + { + SimpleDateFormat chatDateFormat = new SimpleDateFormat(timeStampField.getText()); + + return chatDateFormat.format(date); + } + + public static void setTimeLineString(String newFormat) + { + timeStampField.setText(newFormat); + } + + private void setupInterfacePanel() + { + interfacePanel.add(showEventTicker); + interfacePanel.add(showUsersList); + interfacePanel.add(enableClickableLinks); + interfacePanel.add(showJoinsQuitsEventTicker); + interfacePanel.add(showJoinsQuitsMainWindow); + interfacePanel.add(logChannelText); + interfacePanel.add(logServerActivity); + interfacePanel.add(logClientText); + interfacePanel.add(limitServerLines); + interfacePanel.add(limitServerLinesCount); + interfacePanel.add(limitChannelLines); + interfacePanel.add(limitChannelLinesCount); + interfacePanel.add(enableTimeStamps); // Turn on labels at major tick mark. eventTickerDelay.setMajorTickSpacing(10); @@ -790,20 +1040,19 @@ public Component getListCellRendererComponent(JList list, Object value, int i eventTickerDelay.setToolTipText("Event Ticker movement delay (Lower is faster)"); - optionsClientPanel.add(eventTickerLabel); - optionsClientPanel.add(eventTickerDelay); + interfacePanel.add(eventTickerLabel); + interfacePanel.add(eventTickerDelay); - setupClientOptionsLayout(); - // optionsRightPanel.add(optionsClientPanel, "Client"); + setupInterfaceLayout(); } /** * Aligns components on the Client Options Panel */ - private void setupClientOptionsLayout() + private void setupInterfaceLayout() { - SpringLayout clientLayout = new SpringLayout(); - optionsClientPanel.setLayout(clientLayout); + SpringLayout interfaceLayout = new SpringLayout(); + interfacePanel.setLayout(interfaceLayout); // Used to make it more obvious what is going on - // and perhaps more readable. @@ -816,90 +1065,80 @@ private void setupClientOptionsLayout() // Components are aligned off the top label - clientLayout.putConstraint(SpringLayout.WEST, lafOptionsLabel, LEFT_SPACING, SpringLayout.WEST, optionsClientPanel); - - clientLayout.putConstraint(SpringLayout.WEST, lafOptions, LEFT_SPACING, SpringLayout.EAST, lafOptionsLabel); - clientLayout.putConstraint(SpringLayout.NORTH, lafOptions, TOP_SPACING * 2, SpringLayout.NORTH, optionsClientPanel); + interfaceLayout.putConstraint(SpringLayout.WEST, showEventTicker, LEFT_SPACING * 2, SpringLayout.WEST, + interfacePanel); + interfaceLayout.putConstraint(SpringLayout.NORTH, showEventTicker, TOP_SPACING * 2, SpringLayout.NORTH, + interfacePanel); - int centeredLabelPosition= (int) ((int) (lafOptions.getPreferredSize().getHeight() / 2) - (lafOptions.getPreferredSize().getHeight() - lafOptionsLabel.getPreferredSize().getHeight())); + interfaceLayout.putConstraint(SpringLayout.NORTH, showUsersList, TOP_SPACING, SpringLayout.SOUTH, + showEventTicker); + interfaceLayout.putConstraint(SpringLayout.WEST, showUsersList, LEFT_ALIGNED, SpringLayout.WEST, + showEventTicker); - clientLayout.putConstraint(SpringLayout.NORTH, lafOptionsLabel, centeredLabelPosition, SpringLayout.NORTH, lafOptions); - - clientLayout.putConstraint(SpringLayout.WEST, showEventTicker, LEFT_ALIGNED, SpringLayout.WEST, lafOptionsLabel); - clientLayout.putConstraint(SpringLayout.NORTH, showEventTicker, TOP_SPACING, SpringLayout.SOUTH, lafOptions); - - clientLayout.putConstraint(SpringLayout.NORTH, showUsersList, TOP_SPACING, SpringLayout.SOUTH, showEventTicker); - clientLayout.putConstraint(SpringLayout.WEST, showUsersList, LEFT_ALIGNED, SpringLayout.WEST, showEventTicker); - - clientLayout.putConstraint(SpringLayout.NORTH, enableClickableLinks, TOP_SPACING, SpringLayout.SOUTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, enableClickableLinks, TOP_SPACING, SpringLayout.SOUTH, showUsersList); - clientLayout.putConstraint(SpringLayout.WEST, enableClickableLinks, LEFT_ALIGNED, SpringLayout.WEST, + interfaceLayout.putConstraint(SpringLayout.WEST, enableClickableLinks, LEFT_ALIGNED, SpringLayout.WEST, showUsersList); - clientLayout.putConstraint(SpringLayout.NORTH, showJoinsQuitsEventTicker, TOP_SPACING, SpringLayout.SOUTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, showJoinsQuitsEventTicker, TOP_SPACING, SpringLayout.SOUTH, enableClickableLinks); - clientLayout.putConstraint(SpringLayout.WEST, showJoinsQuitsEventTicker, LEFT_ALIGNED, SpringLayout.WEST, + interfaceLayout.putConstraint(SpringLayout.WEST, showJoinsQuitsEventTicker, LEFT_ALIGNED, SpringLayout.WEST, enableClickableLinks); - clientLayout.putConstraint(SpringLayout.NORTH, showJoinsQuitsMainWindow, TOP_SPACING, SpringLayout.SOUTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, showJoinsQuitsMainWindow, TOP_SPACING, SpringLayout.SOUTH, showJoinsQuitsEventTicker); - clientLayout.putConstraint(SpringLayout.WEST, showJoinsQuitsMainWindow, LEFT_ALIGNED, SpringLayout.WEST, + interfaceLayout.putConstraint(SpringLayout.WEST, showJoinsQuitsMainWindow, LEFT_ALIGNED, SpringLayout.WEST, showJoinsQuitsEventTicker); - clientLayout.putConstraint(SpringLayout.NORTH, logChannelText, TOP_SPACING, SpringLayout.SOUTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, logChannelText, TOP_SPACING, SpringLayout.SOUTH, showJoinsQuitsMainWindow); - clientLayout.putConstraint(SpringLayout.WEST, logChannelText, LEFT_ALIGNED, SpringLayout.WEST, + interfaceLayout.putConstraint(SpringLayout.WEST, logChannelText, LEFT_ALIGNED, SpringLayout.WEST, showJoinsQuitsMainWindow); - clientLayout.putConstraint(SpringLayout.NORTH, logServerActivity, TOP_SPACING, SpringLayout.SOUTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, logServerActivity, TOP_SPACING, SpringLayout.SOUTH, logChannelText); - clientLayout.putConstraint(SpringLayout.WEST, logServerActivity, LEFT_ALIGNED, SpringLayout.WEST, + interfaceLayout.putConstraint(SpringLayout.WEST, logServerActivity, LEFT_ALIGNED, SpringLayout.WEST, logChannelText); - clientLayout.putConstraint(SpringLayout.NORTH, logClientText, TOP_SPACING, SpringLayout.SOUTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, logClientText, TOP_SPACING, SpringLayout.SOUTH, logServerActivity); - clientLayout.putConstraint(SpringLayout.WEST, logClientText, LEFT_ALIGNED, SpringLayout.WEST, + interfaceLayout.putConstraint(SpringLayout.WEST, logClientText, LEFT_ALIGNED, SpringLayout.WEST, logServerActivity); - clientLayout.putConstraint(SpringLayout.NORTH, limitServerLines, TOP_SPACING, SpringLayout.SOUTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, limitServerLines, TOP_SPACING, SpringLayout.SOUTH, + logClientText); + interfaceLayout.putConstraint(SpringLayout.WEST, limitServerLines, LEFT_ALIGNED, SpringLayout.WEST, logClientText); - clientLayout.putConstraint(SpringLayout.WEST, limitServerLines, LEFT_ALIGNED, SpringLayout.WEST, logClientText); - clientLayout.putConstraint(SpringLayout.NORTH, limitServerLinesCount, TOP_ALIGNED, SpringLayout.NORTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, limitServerLinesCount, TOP_ALIGNED, SpringLayout.NORTH, limitServerLines); - clientLayout.putConstraint(SpringLayout.WEST, limitServerLinesCount, TOP_SPACING, SpringLayout.EAST, + interfaceLayout.putConstraint(SpringLayout.WEST, limitServerLinesCount, TOP_SPACING, SpringLayout.EAST, limitServerLines); - clientLayout.putConstraint(SpringLayout.NORTH, limitChannelLines, TOP_SPACING, SpringLayout.SOUTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, limitChannelLines, TOP_SPACING, SpringLayout.SOUTH, limitServerLines); - clientLayout.putConstraint(SpringLayout.WEST, limitChannelLines, LEFT_ALIGNED, SpringLayout.WEST, + interfaceLayout.putConstraint(SpringLayout.WEST, limitChannelLines, LEFT_ALIGNED, SpringLayout.WEST, limitServerLines); - clientLayout.putConstraint(SpringLayout.NORTH, limitChannelLinesCount, TOP_ALIGNED, SpringLayout.NORTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, limitChannelLinesCount, TOP_ALIGNED, SpringLayout.NORTH, limitChannelLines); - clientLayout.putConstraint(SpringLayout.WEST, limitChannelLinesCount, LEFT_SPACING, SpringLayout.EAST, + interfaceLayout.putConstraint(SpringLayout.WEST, limitChannelLinesCount, LEFT_SPACING, SpringLayout.EAST, limitChannelLines); - clientLayout.putConstraint(SpringLayout.NORTH, enableTimeStamps, TOP_SPACING, SpringLayout.SOUTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, enableTimeStamps, TOP_SPACING, SpringLayout.SOUTH, limitChannelLines); - clientLayout.putConstraint(SpringLayout.WEST, enableTimeStamps, LEFT_ALIGNED, SpringLayout.WEST, + interfaceLayout.putConstraint(SpringLayout.WEST, enableTimeStamps, LEFT_ALIGNED, SpringLayout.WEST, limitChannelLines); - clientLayout.putConstraint(SpringLayout.NORTH, clientFontPanel, TOP_SPACING, SpringLayout.SOUTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, eventTickerLabel, TOP_SPACING, SpringLayout.SOUTH, enableTimeStamps); - clientLayout.putConstraint(SpringLayout.WEST, clientFontPanel, LEFT_SPACING, SpringLayout.WEST, + interfaceLayout.putConstraint(SpringLayout.WEST, eventTickerLabel, LEFT_ALIGNED, SpringLayout.WEST, enableTimeStamps); - - clientLayout.putConstraint(SpringLayout.NORTH, eventTickerLabel, TOP_SPACING, SpringLayout.SOUTH, - clientFontPanel); - clientLayout.putConstraint(SpringLayout.WEST, eventTickerLabel, LEFT_ALIGNED, SpringLayout.WEST, - clientFontPanel); - - clientLayout.putConstraint(SpringLayout.NORTH, eventTickerDelay, TOP_SPACING, SpringLayout.SOUTH, + interfaceLayout.putConstraint(SpringLayout.NORTH, eventTickerDelay, TOP_SPACING, SpringLayout.SOUTH, eventTickerLabel); - clientLayout.putConstraint(SpringLayout.WEST, eventTickerDelay, LEFT_ALIGNED, SpringLayout.WEST, + interfaceLayout.putConstraint(SpringLayout.WEST, eventTickerDelay, LEFT_ALIGNED, SpringLayout.WEST, eventTickerLabel); } @@ -926,7 +1165,7 @@ public FavouritesItem(String favServer, String favChannel) this.favChannel = favChannel; settingsPath = getFavouritesPath().node(favServer).node(favChannel); - favFontDialog = new FontDialog("Font: " + favChannel, UserGUI.this.getFont(), settingsPath); + favFontDialog = new FontDialog("Font: " + favChannel, UserGUI.this.getStyle(), settingsPath); favFontDialog.addSaveListener(new SaveChannelFontListener()); createPopUp(); } @@ -947,16 +1186,16 @@ protected class SaveChannelFontListener implements ActionListener @Override public void actionPerformed(ActionEvent arg0) { - for (int index = 0; index < tabbedPane.getComponents().length; index++) + for (int index = 0; index < tabbedPane.getTabCount(); index++) { Component tab = tabbedPane.getComponentAt(index); if (tab instanceof IRCRoomBase) { IRCRoomBase tabRoom = (IRCRoomBase) tab; - if (tabRoom.getServer().equals(favServer) && tabRoom.getName().equals(favChannel)) + if (tabRoom.getServer().getName().equals(favServer) && tabRoom.getName().equals(favChannel)) { - tabRoom.getFontPanel().setFont(favFontDialog.getFontPanel().getFont(), true); + tabRoom.getFontPanel().setFont(favFontDialog.getFontPanel().getStyle(), true); tabRoom.setFont(favFontDialog.getFontPanel().getFont()); } } @@ -999,7 +1238,7 @@ public void actionPerformed(ActionEvent arg0) if (favouritesList.getSelectedIndex() > -1) { FavouritesItem tempItem = favouritesListModel.elementAt(favouritesList.getSelectedIndex()); - tempItem.favFontDialog.getFontPanel().loadFont(); + tempItem.favFontDialog.getFontPanel().loadStyle(); tempItem.favFontDialog.setVisible(true); } } @@ -1300,6 +1539,7 @@ public void setClientSettings() getProfilePath().put(Constants.KEY_NICK_NAME, userNameTextField.getText()); getProfilePath().put(Constants.KEY_REAL_NAME, realNameTextField.getText()); getProfilePath().putBoolean(Constants.KEY_TIME_STAMPS, enableTimeStamps.isSelected()); + getProfilePath().put(Constants.KEY_TIME_STAMP_FORMAT, timeStampField.getText()); getProfilePath().put(Constants.KEY_LAF_NAME, ((LookAndFeelInfo) lafOptions.getSelectedItem()).getClassName()); getProfilePath().putBoolean(Constants.KEY_EVENT_TICKER_ACTIVE, showEventTicker.isSelected()); getProfilePath().putBoolean(Constants.KEY_USERS_LIST_ACTIVE, showUsersList.isSelected()); @@ -1389,7 +1629,12 @@ public void getClientSettings(boolean loadWindowSettings) logClientText.setSelected( getProfilePath().getBoolean(Constants.KEY_LOG_CLIENT_TEXT, Constants.DEFAULT_LOG_CLIENT_TEXT)); - clientFontPanel.loadFont(); + clientFontPanel.loadStyle(); + + timeStampField + .setText(getProfilePath().get(Constants.KEY_TIME_STAMP_FORMAT, Constants.DEFAULT_TIME_STAMP_FORMAT)); + + updatePreviewTextArea(); eventTickerDelay.setValue( getProfilePath().getInt(Constants.KEY_EVENT_TICKER_DELAY, Constants.DEFAULT_EVENT_TICKER_DELAY)); @@ -1442,6 +1687,7 @@ public void removeClientSetting(String node, String key) @Override public void cleanUpSettings() { + // TODO: Clean up all nodes if they have empty keys Constants.LOGGER.log(Level.INFO, "Cleaning up settings"); try { @@ -1455,6 +1701,14 @@ public void cleanUpSettings() } } } + + for (String profileNode : getProfilePath().childrenNames()) + { + if (getProfilePath().node(profileNode) != getFavouritesPath() && getProfilePath().node(profileNode).keys().length == 0) + { + getProfilePath().node(profileNode).removeNode(); + } + } } catch (BackingStoreException e) { Constants.LOGGER.log(Level.WARNING, e.getLocalizedMessage()); @@ -1638,8 +1892,27 @@ public void actionPerformed(ActionEvent arg0) { FavouritesItem favouriteItem = favouritesList.getModel().getElementAt(index); favouriteItem.favFontDialog.getFontPanel().setDefaultFont(clientFontPanel.getFont()); - favouriteItem.favFontDialog.getFontPanel().loadFont(); + favouriteItem.favFontDialog.getFontPanel().loadStyle(); } + + // previewLineFormatter.setFont(previewTextArea.getStyledDocument(), clientFontPanel.getFont()); + previewLineFormatter.updateStyles(0); + } + } + + protected class ResetFontListener implements ActionListener + { + @Override + public void actionPerformed(ActionEvent arg0) + { + getProfilePath().put(Constants.KEY_FONT_FAMILY, Constants.DEFAULT_FONT_GENERAL.getFamily()); + getProfilePath().putBoolean(Constants.KEY_FONT_BOLD, Constants.DEFAULT_FONT_GENERAL.isBold()); + getProfilePath().putBoolean(Constants.KEY_FONT_ITALIC, Constants.DEFAULT_FONT_GENERAL.isItalic()); + getProfilePath().putInt(Constants.KEY_FONT_SIZE, Constants.DEFAULT_FONT_GENERAL.getSize()); + + clientFontPanel.loadStyle(); + + clientFontPanel.getSaveButton().doClick(); } } @@ -1687,21 +1960,25 @@ public void run() }); } - @Override - public Font getFont() + public URStyle getStyle() { if (clientFontPanel != null) { - return clientFontPanel.getFont(); + return clientFontPanel.getStyle(); } - return super.getFont(); + // Create the default style + URStyle newStyle = new URStyle("", getFont()); + guiStyle = newStyle; + return guiStyle; } private LookAndFeelInfo getLAF(String lafClassName) { - for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { - if (lafClassName.equals(info.getClassName())) { + for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) + { + if (lafClassName.equals(info.getClassName())) + { return info; } } @@ -1714,28 +1991,36 @@ private LookAndFeelInfo getLAF(String lafClassName) private void setNewLAF(String newLAFname) { + Color previousDefaultForeground = UIManager.getColor(Constants.DEFAULT_FOREGROUND_STRING); + Color previousDefaultBackground = UIManager.getColor(Constants.DEFAULT_BACKGROUND_STRING); + Font previousDefaultFont = getFont(); + // System.out.println("Setting to "+newLAFname); boolean flatLafAvailable = false; try { - // TODO: reset colours in text boxes (workaround is to resave the font) - // TODO: reset colours in context menus (seems to only affect the context menu on the username in chat) - try{ - for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) { + + try + { + for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) + { // System.out.println(info.getName()); - if (newLAFname.equals(info.getClassName())) { + if (newLAFname.equals(info.getClassName())) + { UIManager.setLookAndFeel(info.getClassName()); flatLafAvailable = true; } } - } catch(Exception e) { + } catch (Exception e) + { throw e; } } catch (Exception e) { Constants.LOGGER.log(Level.WARNING, "Failed to set Pluggable LAF! " + e.getLocalizedMessage()); - } finally { - if(!flatLafAvailable) + } finally + { + if (!flatLafAvailable) { try { @@ -1750,6 +2035,15 @@ private void setNewLAF(String newLAFname) // Required because it doesn't pickup the default ui tabbedPane.setUI((new JTabbedPane()).getUI()); + guiStyle.setFont(clientFontPanel.getFont()); + + // reset the defaults on the guiStyle if they were already at the default + if (previousDefaultForeground == guiStyle.getForeground()) + guiStyle.setForeground(UIManager.getColor(Constants.DEFAULT_FOREGROUND_STRING)); + + if (previousDefaultBackground == guiStyle.getBackground()) + guiStyle.setBackground(UIManager.getColor(Constants.DEFAULT_BACKGROUND_STRING)); + SwingUtilities.updateComponentTreeUI(DriverGUI.frame); updateExtras(); // DriverGUI.frame.dispose(); @@ -1759,6 +2053,8 @@ private void setNewLAF(String newLAFname) // Update the fonts and popup menus - these aren't under the component tree private void updateExtras() { + clientFontPanel.setStyle(guiStyle); + for (int index = 0; index < tabbedPane.getTabCount(); index++) { Component tab = tabbedPane.getComponentAt(index); @@ -1769,7 +2065,9 @@ private void updateExtras() ((IRCRoomBase) tab).getFontPanel().setDefaultFont(clientFontPanel.getFont()); SwingUtilities.updateComponentTreeUI(((IRCRoomBase) tab).myMenu); SwingUtilities.updateComponentTreeUI(((IRCRoomBase) tab).getFontPanel()); + // TODO: Update styles ((IRCRoomBase) tab).getChannelTextPane() } + } for (int index = 0; index < favouritesList.getModel().getSize(); index++) @@ -1777,6 +2075,9 @@ private void updateExtras() FavouritesItem favouriteItem = favouritesList.getModel().getElementAt(index); SwingUtilities.updateComponentTreeUI(favouriteItem.myMenu); } + + // update the styles in the preview text area + updatePreviewTextArea(); } /* @@ -1787,8 +2088,9 @@ private void updateExtras() @Override public void run() { - // Auto-generated method stub Thread.currentThread().setContextClassLoader(DriverGUI.contextClassLoader); + Thread.currentThread().setUncaughtExceptionHandler(new URUncaughtExceptionHandler()); + setNewLAF(((LookAndFeelInfo) lafOptions.getSelectedItem()).getClassName()); } } diff --git a/src/urChatBasic/frontend/UsersListModel.java b/src/urChatBasic/frontend/UsersListModel.java index b7e15b0..13b206f 100644 --- a/src/urChatBasic/frontend/UsersListModel.java +++ b/src/urChatBasic/frontend/UsersListModel.java @@ -4,22 +4,17 @@ import java.util.List; import java.util.Collections; -import javax.swing.DefaultListModel; +import javax.swing.AbstractListModel; @SuppressWarnings("rawtypes") -public class UsersListModel extends DefaultListModel +public class UsersListModel extends AbstractListModel { /** * */ private static final long serialVersionUID = 1L; // ArrayList users; - List users = new ArrayList(); - - public UsersListModel(List array) - { - users = array; - } + List users = new ArrayList(); public int getSize() { @@ -31,7 +26,21 @@ public IRCUser getElementAt(int index) return (IRCUser) users.get(index); } - @SuppressWarnings("unchecked") + public void addUser(IRCUser newUser) + { + users.add(newUser); + } + + public void removeUser(String userName) + { + users.removeIf(n -> n.getName().equalsIgnoreCase(userName)); + } + + public void removeUser(IRCUser targetUser) + { + users.remove(targetUser); + } + public List getUsersList() { return users; @@ -48,7 +57,6 @@ public void getSortedList(ArrayList array) users = array; } - @SuppressWarnings("unchecked") public void sort() { Collections.sort(users); diff --git a/src/urChatBasic/frontend/components/ColourPanel.java b/src/urChatBasic/frontend/components/ColourPanel.java new file mode 100644 index 0000000..fcf3ffe --- /dev/null +++ b/src/urChatBasic/frontend/components/ColourPanel.java @@ -0,0 +1,180 @@ +package urChatBasic.frontend.components; + +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.prefs.Preferences; +import javax.swing.*; +import javax.swing.event.*; +import urChatBasic.backend.utils.URPreferencesUtil; +import urChatBasic.backend.utils.URStyle; +import urChatBasic.frontend.utils.URColour; + +/* ColorChooserDemo.java requires no other files. */ +public class ColourPanel extends JPanel implements ChangeListener +{ + protected EventListenerList listenerList = new EventListenerList(); + protected transient ActionEvent actionEvent = null; + + protected JColorChooser tcc; + public Color selectedColor; + private URStyle targetStyle; + private URStyle defaultStyle; + private Preferences settingsPath; + private JPanel bottomPanel; + private boolean isForeground = true; + JButton saveButton; + JLabel previewLabel; + + public ColourPanel(String styleName, URStyle defaultStyle, Preferences settingsPath) + { + super(new BorderLayout()); + saveButton = new JButton("Apply & Save"); + previewLabel = new JLabel("Preview Text"); + this.settingsPath = settingsPath; + this.defaultStyle = defaultStyle.clone(); + targetStyle = defaultStyle.clone(); + loadStyle(); + + bottomPanel = createBottomPanel(); + // Set up color chooser for setting text color + tcc = new JColorChooser(targetStyle.getForeground()); + tcc.setPreviewPanel(bottomPanel); + tcc.getSelectionModel().addChangeListener(this); + // bottomPanel.setPreferredSize(new Dimension(tcc.getPreferredSize().width, 56)); + add(tcc, BorderLayout.PAGE_END); + } + + public JPanel createBottomPanel() + { + JPanel bottomPanel = new JPanel(); + JButton foregroundButton = new JButton("Toggle Background"); + JButton autoColour = new JButton("Suggest colour"); + JButton resetButton = new JButton("Reset colour"); + + + bottomPanel.setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + + gbc.gridx = 0; + gbc.gridy = 0; + gbc.gridwidth = 4; + gbc.anchor = GridBagConstraints.CENTER; + bottomPanel.add(previewLabel, gbc); + + gbc.gridx = 0; + gbc.gridy = 1; + gbc.gridwidth = 1; + gbc.fill = GridBagConstraints.HORIZONTAL; + bottomPanel.add(foregroundButton, gbc); + + gbc.gridx = 1; + gbc.gridy = 1; + bottomPanel.add(autoColour, gbc); + + gbc.gridx = 2; + gbc.gridy = 1; + bottomPanel.add(resetButton, gbc); + + gbc.gridx = 3; + gbc.gridy = 1; + bottomPanel.add(saveButton, gbc); + + foregroundButton.addActionListener(e -> { + isForeground = !isForeground; + foregroundButton.setText(isForeground ? "Toggle Background" : "Toggle Foreground"); + }); + + resetButton.addActionListener(e -> { + // URPreferencesUtil.deleteStyleColours(targetStyle, settingsPath); + // defaultStyle.load(settingsPath); + URPreferencesUtil.deleteStyleColours(targetStyle, settingsPath); + previewLabel.setFont(defaultStyle.getFont()); + setPreviewColour(defaultStyle.getForeground(), true); + setPreviewColour(defaultStyle.getBackground(), false); + }); + + autoColour.addActionListener(e -> { + if (isForeground) + { + tcc.setColor(URColour.getInvertedColour(previewLabel.getBackground())); + } else + { + tcc.setColor(URColour.getInvertedColour(previewLabel.getForeground())); + } + }); + + saveButton.addActionListener(e -> { + // Save the style first + if(targetStyle.equals(defaultStyle)) + URPreferencesUtil.deleteStyleColours(targetStyle, settingsPath); + else + URPreferencesUtil.saveStyle(targetStyle, settingsPath); + + // now fire the rest of the save listeners + fireSaveListeners(); + }); + + return bottomPanel; + } + + private void setPreviewColour (Color newColour, boolean setForeground) + { + if(setForeground) + { + previewLabel.setForeground(newColour); + targetStyle.setForeground(newColour); + } else + { + previewLabel.setOpaque(true); + + previewLabel.setBackground(newColour); + targetStyle.setBackground(newColour); + } + } + + public void addSaveListener (ActionListener actionListener) + { + listenerList.add(ActionListener.class, actionListener); + } + + protected void fireSaveListeners() + { + Object[] listeners = this.listenerList.getListenerList(); + + // Reverse order + for(int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == ActionListener.class) { + if (this.actionEvent == null) { + this.actionEvent = new ActionEvent(saveButton, i, TOOL_TIP_TEXT_KEY); + } + + ((ActionListener)listeners[i + 1]).actionPerformed(this.actionEvent); + } + } + } + + public void loadStyle() + { + targetStyle = URPreferencesUtil.loadStyle(targetStyle, settingsPath); + + // defaultBackground = colourMap.get(Constants.KEY_FONT_BACKGROUND); + // defaultForeground = colourMap.get(Constants.KEY_FONT_FOREGROUND); + // TODO: Should also be underlined etc.. + previewLabel.setFont(targetStyle.getFont()); + setPreviewColour(targetStyle.getForeground(), true); + setPreviewColour(targetStyle.getBackground(), false); + } + + public URStyle getStyle() + { + return targetStyle; + } + + public void stateChanged(ChangeEvent e) + { + selectedColor = tcc.getColor(); + + setPreviewColour(selectedColor, isForeground); + } +} diff --git a/src/urChatBasic/frontend/components/DnDTabbedPane.java b/src/urChatBasic/frontend/components/DnDTabbedPane.java index 02a64f7..6c458e8 100644 --- a/src/urChatBasic/frontend/components/DnDTabbedPane.java +++ b/src/urChatBasic/frontend/components/DnDTabbedPane.java @@ -18,7 +18,7 @@ public class DnDTabbedPane extends JTabbedPane { private static final int LINE_SIZE = 3; private static final int RWH = 20; - private static final int BUTTON_SIZE = 30; // XXX 30 is magic number of scroll button size + private static final int BUTTON_SIZE = 30; private final GhostGlassPane glassPane = new GhostGlassPane(this); protected int dragTabIndex = -1; @@ -221,7 +221,6 @@ protected void initGlassPane(Point tabPt) { protected Rectangle getTabAreaBounds() { Rectangle tabbedRect = getBounds(); - // XXX: Rectangle compRect = getSelectedComponent().getBounds(); // pointed out by daryl. NullPointerException: i.e. addTab("Tab", null) // Component comp = getSelectedComponent(); // int idx = 0; @@ -393,7 +392,6 @@ private static Optional getGhostGlassPane(Component c) { // Component c = e.getDropTargetContext().getComponent(); // System.out.println("DropTargetListener#dragExit: " + c.getName()); getGhostGlassPane(e.getDropTargetContext().getComponent()).ifPresent(glassPane -> { - // XXX: glassPane.setVisible(false); glassPane.setPoint(HIDDEN_POINT); glassPane.setTargetRect(0, 0, 0, 0); glassPane.repaint(); diff --git a/src/urChatBasic/frontend/components/FontPanel.java b/src/urChatBasic/frontend/components/FontPanel.java index 4270349..1d1ddb4 100644 --- a/src/urChatBasic/frontend/components/FontPanel.java +++ b/src/urChatBasic/frontend/components/FontPanel.java @@ -1,15 +1,24 @@ package urChatBasic.frontend.components; -import java.awt.Dimension; import java.awt.Font; import java.awt.GraphicsEnvironment; -import java.awt.GridLayout; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; +import java.awt.font.TextAttribute; +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; import java.util.prefs.Preferences; import javax.swing.*; +import urChatBasic.frontend.dialogs.ColourDialog; +import urChatBasic.backend.utils.URPreferencesUtil; +import urChatBasic.backend.utils.URStyle; import urChatBasic.base.Constants; public class FontPanel extends JPanel @@ -17,43 +26,128 @@ public class FontPanel extends JPanel /** * */ - private static final long serialVersionUID = 4044242988594083226L; + // private static final long serialVersionUID = 4044242988594083226L; private final JLabel TEXT_PREVIEW = new JLabel("A quick brown fox 0123456789"); private final String[] FONT_LIST = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames(); private final JComboBox FONT_COMBO_BOX = new JComboBox(FONT_LIST); private final Integer[] FONT_SIZES = {8, 10, 11, 12, 14, 16, 18, 20, 24, 30, 36, 40, 60, 72}; private final JComboBox SIZES_COMBO_BOX = new JComboBox<>(FONT_SIZES); private final JCheckBox MAKE_BOLD = new JCheckBox("BOLD"); + private final JCheckBox MAKE_UNDERLINE = new JCheckBox("UNDERLINE"); private final JCheckBox MAKE_ITALIC = new JCheckBox("ITALIC"); - private final JButton SAVE_BUTTON = new JButton("Save Font"); - private Font defaultFont; - // private final JButton CANCEL_BUTTON = new JButton("Cancel"); + private final JButton RESET_BUTTON = new JButton("Reset"); + private final JButton SAVE_BUTTON = new JButton("Save"); + private final JButton COLOUR_BUTTON = new JButton("Colour"); + protected ColourDialog colourDialog = null; + // private String fontType = "New Font:"; + // private JLabel fontTypeLabel = new JLabel("New Font:"); + private URStyle defaultStyle; + private URStyle targetStyle; + // TODO: Add colour picker for foreground and background + + /** + * TODO: This will be used instead for creating the action listeners: private Map actionList = new HashMap<>(); + * @see ColourPanel.fireSaveListeners() + */ + private List actionListeners = new ArrayList<>(); private Preferences settingsPath; - public FontPanel(Font defaultFont, Preferences settingsPath) + public FontPanel(String styleName, URStyle defaultStyle, Preferences settingsPath) { - setPreferredSize(new Dimension(0, 50)); - setLayout(new GridLayout(2, 6)); - + setLayout(new GridBagLayout()); setSettingsPath(settingsPath); - setDefaultFont(defaultFont); - loadFont(); + targetStyle = new URStyle(styleName, defaultStyle.getFont()); + this.defaultStyle = defaultStyle; + setDefaultFont(targetStyle.getFont()); + colourDialog = new ColourDialog(styleName, defaultStyle, settingsPath); + + RESET_BUTTON.addActionListener(new ResetListener()); + COLOUR_BUTTON.addActionListener(new ActionListener() { - add(FONT_COMBO_BOX); - add(SIZES_COMBO_BOX); - add(MAKE_BOLD); - add(MAKE_ITALIC); - add(TEXT_PREVIEW); - add(SAVE_BUTTON); + @Override + public void actionPerformed(ActionEvent arg0) + { + if(colourDialog == null) + { + ; + colourDialog.getColourPanel().addSaveListener(e -> { + // URPreferencesUtil.saveStyle(targetStyle, settingsPath); + Constants.LOGGER.log(Level.INFO, "Font Panel says: Save Colour pressed"); + }); + for (ActionListener actionListener : actionListeners) { + colourDialog.getColourPanel().addSaveListener(actionListener); + } + } - SAVE_BUTTON.addActionListener(new SaveListener()); + colourDialog.setVisible(true); + } + }); + + addActionListener(SAVE_BUTTON, new SaveListener()); FONT_COMBO_BOX.addItemListener(new FontSelectionChange()); SIZES_COMBO_BOX.addItemListener(new FontSelectionChange()); MAKE_BOLD.addActionListener(new CheckListener()); MAKE_ITALIC.addActionListener(new CheckListener()); + MAKE_UNDERLINE.addActionListener(new CheckListener()); + + // Reset the GridBagConstraints for MAIN_PANEL + GridBagConstraints c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LINE_START; // Align components to the left + // Set constraints for MAIN_PANEL + // c.gridwidth = 6; // Assuming you want MAIN_PANEL to span three columns + // c.weightx = 1.0; + // c.weighty = 1.0; + + // First Row + c.gridx = 0; + c.gridy = 0; + c.gridwidth = 1; + c.weightx = 1.0; + add(FONT_COMBO_BOX, c); + + c.gridx = 1; + c.gridy = 0; + c.gridwidth = 1; + add(SIZES_COMBO_BOX, c); + + c.gridx = 2; + c.gridy = 0; + add(COLOUR_BUTTON, c); + + c.gridx = 3; + c.gridy = 0; + add(RESET_BUTTON, c); + + c.gridx = 4; + c.gridy = 0; + add(SAVE_BUTTON, c); + + // Second Row + c = new GridBagConstraints(); + c.fill = GridBagConstraints.HORIZONTAL; + c.anchor = GridBagConstraints.LINE_START; // Align components to the left + c.gridx = 0; + c.gridy = 1; + c.gridwidth = 3; // Assuming TEXT_PREVIEW spans three columns + add(TEXT_PREVIEW, c); + + c.gridx = 1; + c.gridy = 1; + c.gridwidth = 1; + add(MAKE_BOLD, c); + + c.gridx = 2; + c.gridy = 1; + add(MAKE_ITALIC, c); + + c.gridx = 3; + c.gridy = 1; + c.gridwidth = 2; + add(MAKE_UNDERLINE, c); } public JButton getSaveButton() @@ -61,26 +155,37 @@ public JButton getSaveButton() return SAVE_BUTTON; } - public void setDefaultFont(Font f) + public JButton getResetButton() { - defaultFont = f; - loadFont(); + return RESET_BUTTON; } - public void loadFont() + public void setDefaultFont(Font f) { - Font savedFont = defaultFont; - int savedFontBoldItalic = 0; + // defaultFont = f; + defaultStyle.setFont(f); + loadStyle(); + } - if (settingsPath.getBoolean(Constants.KEY_FONT_BOLD, defaultFont.isBold())) - savedFontBoldItalic = Font.BOLD; - if (settingsPath.getBoolean(Constants.KEY_FONT_ITALIC, defaultFont.isItalic())) - savedFontBoldItalic |= Font.ITALIC; + public URStyle getStyle() + { + return targetStyle; + } - savedFont = new Font(settingsPath.get(Constants.KEY_FONT_FAMILY, defaultFont.getFamily()), - savedFontBoldItalic, settingsPath.getInt(Constants.KEY_FONT_SIZE, defaultFont.getSize())); + // Deletes the saved font, then loads the "default" font. + // The default font for a channel is the Profile Font, the default font + // the UserGUI is Constants.DEFAULT_FONT + public void resetFont() { + URPreferencesUtil.deleteStyleFont(targetStyle, settingsPath); + setStyle(defaultStyle); + loadStyle(); + } - setFont(savedFont, false); + public void loadStyle() + { + // setFont(URPreferencesUtil.loadStyleFont(defaultFont, settingsPath), false); + setStyle(URPreferencesUtil.loadStyle(targetStyle, settingsPath)); + // setFont(URPreferencesUtil.loadStyle(targetStyle, settingsPath).getFont(), false); } @Override @@ -92,33 +197,32 @@ public void setFont(Font f) TEXT_PREVIEW.setFont(f); } - public void setFont(Font newFont, Boolean saveToSettings) + public void setStyle(URStyle newStyle) { + targetStyle = newStyle.clone(); + + setFont(targetStyle, false); + } + + public void setFont(URStyle newStyle, Boolean saveToSettings) + { + Font newFont = newStyle.getFont(); + if (getFont() != newFont || saveToSettings) { MAKE_BOLD.setSelected(newFont.isBold()); - if (saveToSettings) - { - settingsPath.putBoolean(Constants.KEY_FONT_BOLD, newFont.isBold()); - } - MAKE_ITALIC.setSelected(newFont.isItalic()); - if (saveToSettings) - { - settingsPath.putBoolean(Constants.KEY_FONT_ITALIC, newFont.isItalic()); - } - + MAKE_UNDERLINE.setSelected(newStyle.isUnderline()); FONT_COMBO_BOX.setSelectedItem(newFont.getFamily()); - - if (saveToSettings) - { - settingsPath.put(Constants.KEY_FONT_FAMILY, newFont.getFamily()); - } - SIZES_COMBO_BOX.setSelectedItem(newFont.getSize()); - if (saveToSettings) + + targetStyle.setFont(newFont); + if(saveToSettings) { - settingsPath.putInt(Constants.KEY_FONT_SIZE, newFont.getSize()); + URStyle colourPanelStyle = colourDialog.getColourPanel().getStyle(); + targetStyle.setForeground(colourPanelStyle.getForeground()); + targetStyle.setBackground(colourPanelStyle.getBackground()); + targetStyle.save(settingsPath); } revalidate(); @@ -127,21 +231,39 @@ public void setFont(Font newFont, Boolean saveToSettings) } } + //https://docs.oracle.com/javase/6/docs/api/java/awt/font/TextAttribute.html private void previewFont() { - int boldItalic = 0; + Map fontMap = new Hashtable(); if (MAKE_BOLD.isSelected()) - boldItalic = Font.BOLD; + fontMap.put(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD); if (MAKE_ITALIC.isSelected()) - boldItalic |= Font.ITALIC; + fontMap.put(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE); + if (MAKE_UNDERLINE.isSelected()) + fontMap.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); setFont( new Font(FONT_COMBO_BOX.getSelectedItem().toString(), - boldItalic, + Font.PLAIN, Integer.parseInt(SIZES_COMBO_BOX.getSelectedItem().toString()) - )); + ).deriveFont(fontMap) + ); + + targetStyle.setFont(getFont()); + } + + // Override the addActionListener method to keep track of added listeners + public void addActionListener(JButton targetButton, ActionListener listener) { + actionListeners.add(listener); + targetButton.addActionListener(listener); + } + + + // Method to retrieve all added listeners + public List getActionListeners() { + return actionListeners; } class CheckListener implements ActionListener @@ -170,7 +292,17 @@ class SaveListener implements ActionListener @Override public void actionPerformed(ActionEvent e) { - FontPanel.this.setFont(TEXT_PREVIEW.getFont(), true); + // FontPanel.this.setFont(TEXT_PREVIEW.getFont(), true); + setFont(targetStyle, true); + } + } + + class ResetListener implements ActionListener + { + @Override + public void actionPerformed(ActionEvent e) + { + FontPanel.this.resetFont(); } } diff --git a/src/urChatBasic/frontend/components/FontPreview.java b/src/urChatBasic/frontend/components/FontPreview.java new file mode 100644 index 0000000..a5137c1 --- /dev/null +++ b/src/urChatBasic/frontend/components/FontPreview.java @@ -0,0 +1,17 @@ +package urChatBasic.frontend.components; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextPane; + +public class FontPreview extends JPanel { + + private JTextPane previewTextArea = new JTextPane(); + private JScrollPane previewScroll = new JScrollPane(previewTextArea); + + public FontPreview() + { + + } +} diff --git a/src/urChatBasic/frontend/components/ProfilePicker.java b/src/urChatBasic/frontend/components/ProfilePicker.java index 2d4d1c6..605fb31 100644 --- a/src/urChatBasic/frontend/components/ProfilePicker.java +++ b/src/urChatBasic/frontend/components/ProfilePicker.java @@ -45,11 +45,18 @@ public void actionPerformed(ActionEvent e) String selectedString = profileComboBox.getSelectedItem().toString(); if (DriverGUI.gui.getProfileName() != selectedString && !selectedString.isBlank()) { + //TODO Show dialog to either rename the existing profile, or create a new profile + String currentProfile = DriverGUI.gui.getProfileName(); + DriverGUI.gui.setProfileName(selectedString); if (!profileExists(selectedString)) { + // If create new profile selected profileComboBox.addItem(selectedString); + + // If rename existing profile selected... + // TODO } } else if (selectedString.isBlank()) { diff --git a/src/urChatBasic/frontend/components/URVersionLabel.java b/src/urChatBasic/frontend/components/URVersionLabel.java index cf4453b..6dfb82d 100644 --- a/src/urChatBasic/frontend/components/URVersionLabel.java +++ b/src/urChatBasic/frontend/components/URVersionLabel.java @@ -4,16 +4,22 @@ import javax.swing.JPanel; import urChatBasic.base.Constants; import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Container; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; import java.util.logging.Level; public class URVersionLabel extends JPanel { + // We are using this label as the default foreground and background colours! + private JLabel versionLabel = new JLabel(Constants.UR_VERSION); public URVersionLabel(JPanel parentPanel) @@ -41,6 +47,16 @@ public static void setVersion() { } } + public Map getColours() + { + Map colours = new HashMap<>(); + + colours.put(Constants.KEY_FONT_FOREGROUND, getForeground()); + colours.put(Constants.KEY_FONT_BACKGROUND, getBackground()); + + return colours; + } + private static String findGitFolder() { // Implement logic to find the .git folder in the project directory or its parents // For simplicity, let's assume it's in the current directory diff --git a/src/urChatBasic/frontend/dialogs/ColourDialog.java b/src/urChatBasic/frontend/dialogs/ColourDialog.java new file mode 100644 index 0000000..631800d --- /dev/null +++ b/src/urChatBasic/frontend/dialogs/ColourDialog.java @@ -0,0 +1,66 @@ +package urChatBasic.frontend.dialogs; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.prefs.Preferences; +import urChatBasic.backend.utils.URStyle; +import urChatBasic.base.DialogBase; +import urChatBasic.frontend.DriverGUI; +import urChatBasic.frontend.components.ColourPanel; + +public class ColourDialog extends DialogBase +{ + private String styleName = "Default Font"; + private ColourPanel colourPanel; + + public ColourDialog(String styleName, URStyle defaultStyle, Preferences settingsPath) + { + super(DriverGUI.frame, defaultStyle.getName(), true); + this.styleName = styleName; + initColourDialog(defaultStyle, settingsPath); + } + + public void initColourDialog(URStyle defaultStyle, Preferences settingsPath) + { + colourPanel = new ColourPanel(styleName, defaultStyle, settingsPath); + + add(colourPanel); + setContentPane(colourPanel); + pack(); + + // setSize(600, 600); + setResizable(false); + // setMaximumSize(new Dimension(600, 600)); + setLocationRelativeTo(super.getParent()); + + colourPanel.addSaveListener(e -> { + ColourDialog.this.setVisible(false); + }); + } + + public ColourPanel getColourPanel() + { + return colourPanel; + } + + @Override + public void setVisible(boolean shown) + { + if (!shown) + colourPanel.loadStyle(); + else + setLocationRelativeTo(super.getParent()); + + super.setVisible(shown); + } + + public class HideColourDialog implements ActionListener + { + @Override + public void actionPerformed(ActionEvent arg0) + { + // fontPanel.loadFont(); + colourPanel.loadStyle(); + } + } +} diff --git a/src/urChatBasic/frontend/dialogs/FontDialog.java b/src/urChatBasic/frontend/dialogs/FontDialog.java index 8f22246..e880b8a 100644 --- a/src/urChatBasic/frontend/dialogs/FontDialog.java +++ b/src/urChatBasic/frontend/dialogs/FontDialog.java @@ -1,31 +1,34 @@ package urChatBasic.frontend.dialogs; import java.awt.Dimension; -import java.awt.Font; +import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.prefs.Preferences; +import urChatBasic.backend.utils.URStyle; import urChatBasic.base.DialogBase; import urChatBasic.frontend.DriverGUI; import urChatBasic.frontend.components.FontPanel; public class FontDialog extends DialogBase { + private String styleName = "Default Font"; private FontPanel fontPanel; - public FontDialog(String title, Font defaultFont, Preferences settingsPath) + public FontDialog(String styleName, URStyle defaultStyle, Preferences settingsPath) { - super(DriverGUI.frame, title, true); - initFontDialog(defaultFont, settingsPath); + super(DriverGUI.frame, styleName, true); + this.styleName = styleName; + initFontDialog(defaultStyle, settingsPath); } - public void initFontDialog(Font defaultFont, Preferences settingsPath) + public void initFontDialog(URStyle defaultStyle, Preferences settingsPath) { setSize(600, 100); setResizable(false); setMaximumSize(new Dimension(600, 100)); setLocationRelativeTo(super.getParent()); - fontPanel = new FontPanel(defaultFont, settingsPath); + fontPanel = new FontPanel(styleName, defaultStyle, settingsPath); add(fontPanel); } @@ -38,12 +41,29 @@ public FontPanel getFontPanel() @Override public void setVisible(boolean b) { - fontPanel.loadFont(); + setLocationRelativeTo(super.getParent()); + fontPanel.loadStyle(); super.setVisible(b); } public void addSaveListener(ActionListener newActionListener) { - fontPanel.getSaveButton().addActionListener(newActionListener); + // fontPanel.getSaveButton().addActionListener(newActionListener); + fontPanel.addActionListener(fontPanel.getSaveButton(), newActionListener); + } + + public void addResetListener(ActionListener newActionListener) + { + fontPanel.getResetButton().addActionListener(newActionListener); + } + + public class ShowFontDialog implements ActionListener + { + @Override + public void actionPerformed(ActionEvent arg0) + { + fontPanel.loadStyle(); + FontDialog.this.setVisible(true); + } } } diff --git a/src/urChatBasic/frontend/dialogs/MessageDialog.java b/src/urChatBasic/frontend/dialogs/MessageDialog.java index ec1b790..fb89056 100644 --- a/src/urChatBasic/frontend/dialogs/MessageDialog.java +++ b/src/urChatBasic/frontend/dialogs/MessageDialog.java @@ -2,6 +2,8 @@ import java.awt.BorderLayout; import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.util.function.Consumer; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; @@ -16,6 +18,19 @@ public class MessageDialog extends DialogBase { public MessageDialog (String message, String title, int messageType) { super(DriverGUI.frame, title, true); + setupDialog(message, title, messageType); + setActionListener(closeButton, null); + } + + public MessageDialog (String message, String title, int messageType, Consumer actionFunction) + { + super(DriverGUI.frame, title, true); + setupDialog(message, title, messageType); + setActionListener(closeButton, actionFunction); + } + + public void setupDialog(String message, String title, int messageType) + { setSize(300, 150); setResizable(false); setMaximumSize(new Dimension(300, 150)); @@ -27,9 +42,7 @@ public MessageDialog (String message, String title, int messageType) messageLabel.setHorizontalAlignment(SwingConstants.LEFT); messageLabel.setVerticalAlignment(SwingConstants.TOP); // Top alignment for wrapping text - closeButton = new JButton("Close"); - closeButton.addActionListener(e -> dispose()); // Close the dialog dialogPanel = new JPanel(new BorderLayout()); dialogPanel.add(messageLabel, BorderLayout.CENTER); @@ -37,4 +50,19 @@ public MessageDialog (String message, String title, int messageType) add(dialogPanel); } + + private void setActionListener(JButton targetButton, Consumer actionFunction ) + { + targetButton.addActionListener(e -> { + if (actionFunction != null) { + actionFunction.accept(e); + } + dispose(); + }); + } + + public JButton getCloseButton() + { + return closeButton; + } } diff --git a/src/urChatBasic/frontend/dialogs/YesNoDialog.java b/src/urChatBasic/frontend/dialogs/YesNoDialog.java new file mode 100644 index 0000000..fb1a55c --- /dev/null +++ b/src/urChatBasic/frontend/dialogs/YesNoDialog.java @@ -0,0 +1,75 @@ +package urChatBasic.frontend.dialogs; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.util.function.Consumer; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +import urChatBasic.base.DialogBase; +import urChatBasic.frontend.DriverGUI; + +public class YesNoDialog extends DialogBase { + private JLabel messageLabel; + private JButton yesButton; + private JButton noButton; + + public YesNoDialog (String message, String title, int messageType, Consumer returnFunction) + { + super(DriverGUI.frame, title, true); + setSize(300, 150); + setResizable(false); + setMaximumSize(new Dimension(300, 150)); + setLocationRelativeTo(DriverGUI.frame); + + messageLabel = new JLabel("" + message + ""); + messageLabel.setIcon(DialogBase.getIconForMessageType(messageType)); + messageLabel.setIconTextGap(15); + messageLabel.setHorizontalAlignment(SwingConstants.LEFT); + messageLabel.setVerticalAlignment(SwingConstants.TOP); // Top alignment for wrapping text + + + yesButton = new JButton("Yes"); + noButton = new JButton("No"); + + yesButton.addActionListener(e -> { + if (returnFunction != null) { + returnFunction.accept(e); + } + dispose(); + }); + + noButton.addActionListener(e -> { + if (returnFunction != null) { + returnFunction.accept(e); + } + dispose(); + }); + + dialogPanel = new JPanel(new BorderLayout()); + dialogPanel.add(messageLabel, BorderLayout.CENTER); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.TRAILING)); + buttonPanel.add(noButton); + buttonPanel.add(yesButton); + + dialogPanel.add(buttonPanel, BorderLayout.SOUTH); + + + + add(dialogPanel); + } + + public JButton getNoButton() + { + return noButton; + } + + public JButton getYesButton() + { + return yesButton; + } +} diff --git a/src/urChatBasic/frontend/utils/URColour.java b/src/urChatBasic/frontend/utils/URColour.java index 470884e..45c59de 100644 --- a/src/urChatBasic/frontend/utils/URColour.java +++ b/src/urChatBasic/frontend/utils/URColour.java @@ -4,7 +4,7 @@ public class URColour { - private static boolean useDarkColour(Color sourceColour) + public static boolean useDarkColour(Color sourceColour) { // Counting the perceptive luminance - human eye favors green color... double luminance = (0.299 * sourceColour.getRed() + 0.587 * sourceColour.getGreen() + 0.114 * sourceColour.getBlue()) / 255; @@ -38,4 +38,16 @@ public static Color getInvertedColour(Color sourceColour) { // Create and return a new Color object return new Color(r, g, b); } + + public static String hexEncode (Color sourceColor) + { + String hex = Integer.toHexString(sourceColor.getRGB()); + return hex = "#" + hex.substring(2, hex.length()); + } + + public static Color hexDecode (String hexString) + { + Color colour = Color.decode(hexString); + return colour; + } } diff --git a/src/urChatBasic/tests/backend/MessageHandlerTests.java b/src/urChatBasic/tests/backend/MessageHandlerTests.java deleted file mode 100644 index 17618f0..0000000 --- a/src/urChatBasic/tests/backend/MessageHandlerTests.java +++ /dev/null @@ -1,213 +0,0 @@ -package urChatBasic.tests.backend; - -import org.junit.Before; -import org.junit.Test; -import urChatBasic.backend.Connection; -import urChatBasic.backend.MessageHandler; -import urChatBasic.backend.MessageHandler.Message; -import urChatBasic.base.IRCRoomBase; -import urChatBasic.frontend.DriverGUI; -import urChatBasic.frontend.IRCServer; -import urChatBasic.frontend.IRCUser; -import urChatBasic.frontend.UserGUI; -import static org.junit.Assert.assertEquals; -import java.io.IOException; -import javax.swing.text.BadLocationException; -import javax.swing.text.StyledDocument; - -public class MessageHandlerTests { - MessageHandler testHandler; - IRCServer testServer; - UserGUI testGUI; - IRCRoomBase testChannel; - IRCUser testUser; - Connection testConnection; - - // helper functions - private String getLatestLine(StyledDocument doc) - { - int length = doc.getLength(); - - if (length == 0) { - return ""; // Return an empty string if the document is empty - } - - try { - int start = doc.getText(0, length).trim().lastIndexOf('\n') + 1; - int end = length; - - if (end == -1) { - end = length; - } - - return doc.getText(start, end - start).trim(); - } catch (BadLocationException e) { - e.printStackTrace(); - return ""; - } - } - - - @Before - public void setUp() throws Exception { - DriverGUI.createGUI(); - testGUI = DriverGUI.gui; - testServer = new IRCServer("testServer", "testUser", "testUser", "testPassword", "1337", true, "testProxy", "1234", true); - testUser = new IRCUser(testServer, "testUser"); - testServer.addToPrivateRooms(testUser); - testChannel = testServer.getCreatedPrivateRoom(testUser.toString()); - testHandler = new MessageHandler(testServer); - testConnection = new Connection(testServer); - } - - @Test - public void noticeMessageParseTest() - { - String rawMessage = ":ChanServ!ChanServ@services.libera.chat NOTICE userName :[#somechannel] Welcome to #someChannel."; - Message testMessage = testHandler.new Message(rawMessage); - - assertEquals("#somechannel", testMessage.getChannel()); - // assertEquals("Welcome to #somechannel.", testMessage.getBody()); - } - - @Test - public void recvActionMessage() - { - String rawMessage = ":"+testUser+"!~"+testUser+"@userHost PRIVMSG "+testUser+" :ACTION claps hands"; - Message testMessage = testHandler.new Message(rawMessage); - testHandler.parseMessage(testMessage); - assertEquals("> claps hands", testMessage.getBody()); - } - - @Test - public void nickIsHighStyleTest() throws BadLocationException - { - String rawMessage = ":someuser!~someuser@urchatclient PRIVMSG testUser :hello testUser!"; - Message testMessage = testHandler.new Message(rawMessage); - testHandler.parseMessage(testMessage); - StyledDocument testDoc = testChannel.getChannelTextPane().getStyledDocument(); - String testLine = testChannel.getLineFormatter().getLatestLine(testDoc); // "[0629] hello testUser!" - - // Should be highStyle because someuser mentioned my nick, testUser - assertEquals("highStyle", testChannel.getLineFormatter().getStyleAtPosition(testDoc, 11, testLine).getAttribute("name")); - } - - @Test - public void nickIsDefaultStyleTest() throws BadLocationException - { - String rawMessage = ":someuser!~someuser@urchatclient PRIVMSG #somechannel :Welcome to somechannel!"; - Message testMessage = testHandler.new Message(rawMessage); - testHandler.parseMessage(testMessage); - StyledDocument testDoc = testChannel.getChannelTextPane().getStyledDocument(); - String testLine = testChannel.getLineFormatter().getLatestLine(testDoc); // "[0629] hello world!" - - // Should be defaultStyle because the user didn't mention testUser and is just a normal message - assertEquals("defaultStyle", testChannel.getLineFormatter().getStyleAtPosition(testDoc, 11, testLine).getAttribute("name")); - } - - @Test - public void sendActionMessageChannel() - { - String rawMessage = "/me claps hands"; - try - { - testConnection.sendClientText(rawMessage, "#channelname"); - - } catch (IOException e) - { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - @Test - public void sendActionMessageUser() - { - String rawMessage = "/me claps hands"; - try - { - testConnection.sendClientText(rawMessage, "otheruser"); - } catch (IOException e) - { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - @Test - public void sendPrivateMessageMessageUser() - { - String rawMessage = "/msg otheruser hello, did you get this message?"; - try - { - testConnection.sendClientText(rawMessage, "otheruser"); - } catch (IOException e) - { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - @Test - public void noticeMessage2() - { - String rawMessage = ":channeluser!channelname@channelname/bot/primary NOTICE myUsername :this is just some notice message directed to this user from the "; - Message testMessage = testHandler.new Message(rawMessage); - - assertEquals("#channelname", testMessage.getChannel()); - // TODO create this test - } - - @Test - public void handleChannelUrl() - { - String rawMessage = ":services. 328 userName #somechannel :https://somechannel.com/url"; - Message testMessage = testHandler.new Message(rawMessage); - - assertEquals(MessageHandler.NoticeMessage.class, testMessage.getMessageBase().getClass()); - } - - @Test - public void testQuitChannel1() - { - String rawMessage = ":userName!~userName@user/userName QUIT :Read error: Connection reset by peer"; - Message testMessage = testHandler.new Message(rawMessage); - - assertEquals(MessageHandler.DisconnectMessage.class, testMessage.getMessageBase().getClass()); - } - - @Test - public void testQuitChannel2() - { - String rawMessage = ":userName!~userName@user/userName QUIT :Remote host closed the connection"; - Message testMessage = testHandler.new Message(rawMessage); - - assertEquals(MessageHandler.DisconnectMessage.class, testMessage.getMessageBase().getClass()); - } - - @Test - public void testQuitServer() - { - String rawMessage = "ERROR :\"Goodbye cruel world\""; - Message testMessage = testHandler.new Message(rawMessage); - - assertEquals(MessageHandler.DisconnectMessage.class, testMessage.getMessageBase().getClass()); - } - - @Test - public void emojiMessage() - { - // test display of emojis in text - String rawMessage = ":sd!~discord@user/sd PRIVMSG #somechannel :02 this should show a flag"; - // TODO create this test - } - - @Test - public void urlMessage() - { - // test displaying urls - String rawMessage = "https://i.imgur.com/somepicture.png"; - String rawMessage2 = "https://duckduckgo.com/?q=irc+urchat&kp=1&t=h_&ia=web"; - // TODO create this test - } -} diff --git a/tests/URTestRunner.java b/tests/URTestRunner.java new file mode 100644 index 0000000..49bf4e4 --- /dev/null +++ b/tests/URTestRunner.java @@ -0,0 +1,31 @@ +import org.testng.TestNG; +import backend.DialogTests; +import backend.MessageHandlerTests; +import backend.UserGUITests; +import java.util.ArrayList; +import java.util.List; +import org.testng.TestListenerAdapter; + +public class URTestRunner { + + public static void main(String[] args) { + TestNG testNG = new TestNG(); + + // Define your test classes + List classes = new ArrayList<>(); + classes.add(DialogTests.class); + classes.add(MessageHandlerTests.class); + classes.add(UserGUITests.class); + + // Set the test classes to TestNG + testNG.setTestClasses(classes.toArray(new Class[0])); + + // Run TestNG + TestListenerAdapter listener = new TestListenerAdapter(); + testNG.addListener(listener); + + testNG.run(); + + System.exit(0); + } +} \ No newline at end of file diff --git a/tests/backend/DialogTests.java b/tests/backend/DialogTests.java new file mode 100644 index 0000000..00275ce --- /dev/null +++ b/tests/backend/DialogTests.java @@ -0,0 +1,99 @@ +package backend; + +import urChatBasic.frontend.dialogs.MessageDialog; +import urChatBasic.frontend.dialogs.YesNoDialog; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.swing.JButton; +import javax.swing.JOptionPane; +import org.testng.Reporter; +import org.testng.annotations.Test; +import static org.testng.AssertJUnit.*; + +public class DialogTests +{ + protected boolean automateTests = true; + + protected void utilFunction() + { + // to test calling another function + } + + @Test + public void testMessageDialog() + { + // effectively final + AtomicBoolean passedTest = new AtomicBoolean(false); + + MessageDialog testDialog = + new MessageDialog("Test dialog message here", "Test dialog title", JOptionPane.ERROR_MESSAGE, + e -> { + passedTest.set(true); + }); + + if (automateTests) + { + JButton closeButton = testDialog.getCloseButton(); + closeButton.doClick(); + } else + { + testDialog.setVisible(true); + } + + + assertTrue("Didn't press the cancel button", passedTest.get()); + } + + @Test(groups = {"Test #001"}) + public void testYesNoDialogWithBoolean() + { + AtomicBoolean cancelClicked = new AtomicBoolean(false); + + // Create an instance of ChoiceDialog with an anonymous function for cancel action + YesNoDialog testDialog = new YesNoDialog("Click 'No' in this dialog", "Test dialog title", + JOptionPane.QUESTION_MESSAGE, e -> cancelClicked.set(e.getActionCommand().equalsIgnoreCase("No"))); + + if (automateTests) + { + // Simulate clicking the No button + JButton noButton = testDialog.getNoButton(); + noButton.doClick(); + } else + { + testDialog.setVisible(true); + } + + // Assert that the cancel action was executed + assertTrue("No button wasn't pressed.", cancelClicked.get()); + + Reporter.log("Other reporting information for this test"); + } + + @Test + public void testYesNoDialogWithActionListener() + { + + Reporter.log("Custom reporting log information here, defined in the method"); + + // effectively final + AtomicBoolean passedTest = new AtomicBoolean(false); + + // Create an instance of ChoiceDialog with an anonymous function for cancel action + YesNoDialog testDialog = new YesNoDialog("Click any button to execute the function", "Test dialog title", + JOptionPane.QUESTION_MESSAGE, e -> { + passedTest.set(true); + assertTrue(passedTest.get()); + }); + + if (automateTests) + { + // Simulate clicking the cancel button + JButton cancelButton = testDialog.getNoButton(); + cancelButton.doClick(); + } else + { + testDialog.setVisible(true); + } + + assertTrue("Failed because Yes or No wasn't pressed", passedTest.get()); + } +} diff --git a/tests/backend/MessageHandlerTests.java b/tests/backend/MessageHandlerTests.java new file mode 100644 index 0000000..2599743 --- /dev/null +++ b/tests/backend/MessageHandlerTests.java @@ -0,0 +1,366 @@ +package backend; + +import urChatBasic.backend.Connection; +import urChatBasic.backend.MessageHandler; +import urChatBasic.backend.MessageHandler.Message; +import urChatBasic.base.Constants; +import urChatBasic.base.IRCRoomBase; +import urChatBasic.frontend.DriverGUI; +import urChatBasic.frontend.IRCServer; +import urChatBasic.frontend.IRCUser; +import urChatBasic.frontend.UserGUI; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.text.BadLocationException; +import javax.swing.text.StyledDocument; +import org.testng.Reporter; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Optional; +import org.testng.annotations.Parameters; +import org.testng.annotations.Test; +import static org.testng.AssertJUnit.*; + + +public class MessageHandlerTests +{ + MessageHandler testHandler; + IRCServer testServer; + UserGUI testGUI; + IRCRoomBase testChannel; + IRCUser testUser; + Connection testConnection; + + @BeforeMethod(alwaysRun = true) + public void setUp() throws Exception + { + DriverGUI.createGUI(); + testGUI = DriverGUI.gui; + UserGUI.setTimeLineString("[HHmm]"); + testServer = new IRCServer("testServer", "testUser", "testUser", "testPassword", "1337", true, "testProxy", + "1234", true); + testUser = new IRCUser(testServer, "testUser"); + testServer.addToPrivateRooms(testUser); + testChannel = testServer.getCreatedPrivateRoom(testUser.toString()); + testHandler = new MessageHandler(testServer); + testConnection = new Connection(testServer); + } + + @Test(groups = {"Test #001"}) + public void nickIsHighStyleTest() throws BadLocationException, InterruptedException + { + String rawMessage = ":someuser!~someuser@urchatclient PRIVMSG testUser :hello testUser!"; + Message testMessage = testHandler.new Message(rawMessage); + testHandler.parseMessage(testMessage); + String testLine = testChannel.getLineFormatter().getLatestLine(); // "[0629] hello testUser!" + + while (testChannel.messageQueueWorking()) + { + TimeUnit.SECONDS.sleep(1); + } + + // Should be highStyle because someuser mentioned my nick, testUser + assertEquals("highStyle", + testChannel.getLineFormatter().getStyleAtPosition(11, testLine).getAttribute("name")); + } + + @Test(groups = {"Test #001"}) + public void recvActionMessage() + { + String rawMessage = + ":" + testUser + "!~" + testUser + "@userHost PRIVMSG " + testUser + " :ACTION claps hands"; + Message testMessage = testHandler.new Message(rawMessage); + testHandler.parseMessage(testMessage); + assertEquals("> claps hands", testMessage.getBody()); + } + + @Test(groups = {"Test #002"}) + @Parameters({"channelName"}) + public void noticeMessageParseTest(@Optional("someChannel") String channelName) + { + Reporter.log("Using @Optional variable channelName: " + channelName); + + String rawMessage = ":ChanServ!ChanServ@services.libera.chat NOTICE userName :[#" + channelName + + "] Welcome to #" + channelName + "."; + Message testMessage = testHandler.new Message(rawMessage); + + assertEquals("#" + channelName, testMessage.getChannel()); + // assertEquals("Welcome to #somechannel.", testMessage.getBody()); + } + + + @Test(groups = {"Test #003"}, timeOut = 5000) + public void nickIsNickStyleTest() throws BadLocationException, InterruptedException + { + String rawMessage = ":someuser!~someuser@urchatclient PRIVMSG #somechannel :Welcome to somechannel!"; + Message testMessage = testHandler.new Message(rawMessage); + testHandler.parseMessage(testMessage); + StyledDocument testDoc = testChannel.getChannelTextPane().getStyledDocument(); + String testLine = testChannel.getLineFormatter().getLatestLine(); // "[0629] hello world!" + + while (testChannel.messageQueueWorking()) + { + TimeUnit.SECONDS.sleep(1); + } + + // Should be nickStyle because the user didn't mention testUser and is just a normal message + assertEquals("nickStyle", + testChannel.getLineFormatter().getStyleAtPosition(11, testLine).getAttribute("name")); + } + + @Test(groups = {"Test #003"}, dependsOnMethods = {"backend.MessageHandlerTests.nickIsNickStyleTest"}) + public void sendActionMessageChannel() + { + String rawMessage = "/me claps hands"; + try + { + testConnection.sendClientText(rawMessage, "#channelname"); + + } catch (IOException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + Reporter.log("This test won't run unless the dependant method is included in the test and passed."); + } + + @Test + public void sendPrivateMessageMessageUser() + { + String rawMessage = "/msg otheruser hello, did you get this message?"; + try + { + testConnection.sendClientText(rawMessage, "otheruser"); + } catch (IOException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Test(groups = {"Test #004"}, dependsOnMethods = {"backend.MessageHandlerTests.sendPrivateMessageMessageUser"}) + public void sendActionMessageUser() + { + Reporter.log("Depends on sendPrivateMessageMessageUser which will be implicitly included in this test group."); + String rawMessage = "/me claps hands"; + try + { + testConnection.sendClientText(rawMessage, "otheruser"); + } catch (IOException e) + { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + @Test + public void noticeMessage2() + { + String rawMessage = + ":channeluser!channelname@channelname/bot/primary NOTICE myUsername :this is just some notice message directed to this user from the "; + Message testMessage = testHandler.new Message(rawMessage); + + assertEquals("#channelname", testMessage.getChannel()); + } + + @Test + public void handleChannelNoticeUrl() + { + String rawMessage = ":services. 328 userName #somechannel :https://somechannel.com/url"; + Message testMessage = testHandler.new Message(rawMessage); + + assertEquals(MessageHandler.NoticeMessage.class, testMessage.getMessageBase().getClass()); + } + + @Test + public void testQuitChannel1() + { + String rawMessage = ":userName!~userName@user/userName QUIT :Read error: Connection reset by peer"; + Message testMessage = testHandler.new Message(rawMessage); + + assertEquals(MessageHandler.DisconnectMessage.class, testMessage.getMessageBase().getClass()); + } + + @Test + public void testQuitChannel2() + { + String rawMessage = ":userName!~userName@user/userName QUIT :Remote host closed the connection"; + Message testMessage = testHandler.new Message(rawMessage); + + assertEquals(MessageHandler.DisconnectMessage.class, testMessage.getMessageBase().getClass()); + } + + @Test + public void testQuitServer() + { + String rawMessage = "ERROR :\"Goodbye cruel world\""; + Message testMessage = testHandler.new Message(rawMessage); + + assertEquals(MessageHandler.DisconnectMessage.class, testMessage.getMessageBase().getClass()); + } + + @Test(groups = {"Test #005"}) + public void testChannelLineLimit() throws BadLocationException, InterruptedException + { + testGUI.setLimitChannelLines(10); + testGUI.setJoinsQuitsMain(false); + int channelLinesLimit = testGUI.getLimitChannelLinesCount(); + + String channelMessage = ":" + testUser + "!~" + testUser + "@urchatclient PRIVMSG #somechannel :line # "; + + for (int i = 0; i < channelLinesLimit + 10; i++) + { + Message testMessage = testHandler.new Message(channelMessage + i); + testHandler.parseMessage(testMessage); + } + + while (testChannel.messageQueueWorking()) + { + TimeUnit.SECONDS.sleep(1); + } + + // for (int i = 0; i < serverLinesLimit+10; i++) { + // Message testMessage = testHandler.new Message(serverMessage + i); + // testHandler.parseMessage(testMessage); + // } + + // int serverLinesCount = + // testServer.getChannelTextPane().getStyledDocument().getDefaultRootElement().getElementCount(); + int channelLinesCount = + testChannel.getLineFormatter().getDocument().getDefaultRootElement().getElementCount(); + + + + String firstLine = testChannel.getLineFormatter().getFirstLine(); + String lastLine = testChannel.getLineFormatter().getLatestLine(); // " line # 509" + + assertTrue("Last line should line # 19 but it was" + lastLine, lastLine.endsWith("line # 19")); + + assertTrue( + "First line should be line # 10 but it was " + + firstLine, + firstLine.endsWith("line # 10")); + assertSame("Channel line count should equal the line limit", channelLinesLimit, channelLinesCount - 1); + } + + @Test(groups = {"Test #005"}, dependsOnMethods = {"backend.MessageHandlerTests.testChannelLineLimit"} + , description = "Test Description") + public void testServerLineLimit() throws BadLocationException, InterruptedException + { + testGUI.setLimitServerLines(10); + testGUI.setJoinsQuitsMain(false); + int serverLinesLimit = testGUI.getLimitServerLinesCount(); + + String serverMessage = ":" + testServer.getName() + " 001 " + testUser + " :line # "; + + for (int i = 0; i < serverLinesLimit + 10; i++) + { + Message testMessage = testHandler.new Message(serverMessage + i); + testHandler.parseMessage(testMessage); + } + + while (testServer.messageQueueWorking()) + { + TimeUnit.SECONDS.sleep(1); + } + + int serverLinesCount = + testServer.getChannelTextPane().getStyledDocument().getDefaultRootElement().getElementCount(); + + String testLine = testServer.getLineFormatter().getLatestLine(); // " line # 19" + + assertTrue("Last line should line # 19 but it was" + testLine, testLine.endsWith("line # 19")); + + assertTrue( + "First line should be line # 10 but it was " + testServer.getChannelTextPane().getText().split(System.lineSeparator())[0], + testServer.getChannelTextPane().getText().split(System.lineSeparator())[0].trim().endsWith("line # 10")); + assertSame("Channel line count should equal the line limit", serverLinesLimit, serverLinesCount - 1); + } + + @Test + public void _emojiMessage() + { + // test display of emojis in text + String rawMessage = + ":sd!~discord@user/sd PRIVMSG #somechannel :02 this should show a flag"; + // TODO create this test + } + + @Test + public void urlInMessage() throws BadLocationException, InterruptedException + { + // test displaying urls + + String rawMessage = ":someuser!~someuser@urchatclient PRIVMSG #somechannel :https://google.com"; + // String rawMessage2 = "https://duckduckgo.com/?q=irc+urchat&kp=1&t=h_&ia=web"; + + Message testMessage = testHandler.new Message(rawMessage); + testHandler.parseMessage(testMessage); + StyledDocument testDoc = testChannel.getChannelTextPane().getStyledDocument(); + + while (testChannel.messageQueueWorking()) + { + TimeUnit.SECONDS.sleep(1); + } + + String testLine = testChannel.getLineFormatter().getLatestLine(); // "[0629] + // https://google.com" + // Should be urlStyle, i.e a clickable link + assertEquals("urlStyle", + testChannel.getLineFormatter().getStyleAtPosition(19, testLine).getAttribute("name")); + } + + @Test + public void channelRegex() + { + // find and match against any URLs that may be in the text + String line = "join #urchatclient to test the regex"; + + Pattern pattern = Pattern.compile(Constants.CHANNEL_REGEX); + Matcher matcher = pattern.matcher(line); + + + String regexLine = ""; + while (matcher.find()) + { + regexLine = matcher.group(1); + } + + assertTrue(regexLine.equalsIgnoreCase("#urchatclient")); + } + + @Test(groups = {"Test #005"}) + public void channelInMessage() throws BadLocationException, InterruptedException + { + Reporter.log("No dependency, this will pass regardless of the failed testChannelLineLimit method."); + + // test displaying channel + Message testMessage = + testHandler.new Message(":someuser!~someuser@urchatclient PRIVMSG #somechannel :first line"); + testHandler.parseMessage(testMessage); + + String rawMessage = + ":someuser!~someuser@urchatclient PRIVMSG #somechannel :Please join #urchatclient and go to https://github.com/matty-r/urChat then go back to #anotherchannel"; + + testMessage = testHandler.new Message(rawMessage); + testHandler.parseMessage(testMessage); + StyledDocument testDoc = testChannel.getChannelTextPane().getStyledDocument(); + + while (testChannel.messageQueueWorking()) + { + TimeUnit.SECONDS.sleep(1); + } + + String testLine = testChannel.getLineFormatter().getLatestLine(); + // Should be channel, i.e clickable name which allows you to join the channel + assertEquals("channelStyle", + testChannel.getLineFormatter().getStyleAtPosition(33, testLine).getAttribute("name")); + assertEquals("urlStyle", + testChannel.getLineFormatter().getStyleAtPosition(58, testLine).getAttribute("name")); + assertEquals("channelStyle", + testChannel.getLineFormatter().getStyleAtPosition(110, testLine).getAttribute("name")); + } +} diff --git a/src/urChatBasic/tests/backend/UserGUITests.java b/tests/backend/UserGUITests.java similarity index 82% rename from src/urChatBasic/tests/backend/UserGUITests.java rename to tests/backend/UserGUITests.java index e3aa8d9..2e179c0 100644 --- a/src/urChatBasic/tests/backend/UserGUITests.java +++ b/tests/backend/UserGUITests.java @@ -1,22 +1,20 @@ -package urChatBasic.tests.backend; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; +package backend; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; - +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; import urChatBasic.base.Constants; - -import static org.junit.Assert.*; +import static org.junit.Assert.assertNotEquals; +import static org.testng.AssertJUnit.*; public class UserGUITests { Preferences baseTestPreference; Preferences serverTestPreference; Preferences roomTestPreference; - @Before + @BeforeTest public void setUp() throws Exception { baseTestPreference = Constants.BASE_PREFS.parent().node("testing"); serverTestPreference = baseTestPreference.node("servername"); @@ -39,7 +37,7 @@ public void nodeNotEmpty() throws BackingStoreException { assertNotEquals(0, roomTestPreference.keys().length + roomTestPreference.childrenNames().length); } - @After + @AfterTest public void tearDown() throws BackingStoreException { baseTestPreference.removeNode(); }