Comp 110 Chat110 - Chat Client

This part of the problem set will continue from the previous part

In this part of the problem set we will build a "coordinator" layer responsible for sending and receiving Packets as well as coordinating interactions between windows. For example, when the a "who" Packet is received, the coordinator will tell the FriendsWindow to update its list of friends. When a "dm" Packet is received, it will find the ChatWindow for who the message is from (or make a new one!) and send an instruction to it. Conversely, as a message is sent from a ChatWindow, it will tell the coordinator, who will then send a dm Packet to the server.

Part A. Chat Client App Setup

A.1 - client Package - Right click on the src folder and select New > Package. Create a package named comp110.chat.client

A.2 - Chat110App class. This class will house the main method and, just like the ToolsApp from part 1, make a connection to the server. To setup the class, right click on the package created in A.1 and select New > Class. Name the class Chat110App and confirm it is in the package comp110.chat.client

A.3 - To get started, we will provide the code for this class. Notice that it looks very similar to the code for ToolsApp. You will need to plug-in your ONYEN and Key.

package comp110.chat.client;

import comp110.chat.packets.Connection;
import comp110.chat.packets.Packet;
import comp110.chat.tools.PacketToolWindow;
import javafx.application.Application;
import javafx.stage.Stage;

public class Chat110App extends Application {
    public static void main(String[] args) {
        Application.launch();
    }

    public void start(Stage stage) throws Exception {

        Connection connection = new Connection();
        connection.connect("ws://comp110.com/chat110");

        // TODO: Once our app is working, we'll remove this
        PacketToolWindow packets = new PacketToolWindow(connection);

        // TODO: Construct the Coordinator

        connection.send(new Packet("auth:onyen:key"));
    }
}

Go ahead and run the Chat110App and you should see your PacketToolWindow pop-up from part 1 and you should be authorized with your ONYEN and key.

Part B. The Coordinator Class

B.1 - Add a class named Coordinator to the comp110.chat.client package.

B.2 - Like the PacketToolController, the Coordinator class must implement the ConnectionObserver interface. You will need to import ConnectionObserver from the comp110.chat.packets package. For now, make each of the two required methods print the following lines, respectively, for debugging purposes:

// Place in the packetSent method:
System.out.println("Coordinator: packetSent");
// Place in the packetReceived method:
System.out.println("Coordinator: packetReceived");

B.2 Declare a field on the Coordinator class with the following characteristics. This will hold a reference to the Connection to the server so that the Coordinator can send Packets to it:

Visibility:   private
Type:         Connection
Name:         _connection

B.3 Declare a constructor for the Coordinator class with the following characteristics:

Visibility:   public
Parameters:   
    #1:    Type: Connection
           Name: connection

B.4 The constructor should initialize the _connection field by assigning the parameter it is provided to it.

B.5 The constructor should also register the Coordinator being constructed as an observer on the Connection using the code snippet below.

_connection.addObserver(this);

Notice this is the same set of steps we took to make the PacketToolController observe the connection's Packets as they were sent and received.

B.6 Construct a new Coordinator object in the Chat110App's start method. Place this under the TODO comment referencing the Coordinator, before the auth Packet is sent.

Functionality checkpoint: when you run your program, you should now see the PacketTool display packets as you did in Part 1, but you should also see messages being printed to the console from the Coordinator class. 

COMP401 Preview - Notice something cool here, both the Coordinator and the PacketTool are able to respond to Packets as they are sent/received over the Connection. This is possible thanks to multiple objects which implement the ConnectionObserver interface being able to add themselves as "observers" on the Connection. This pattern of "observer/observable" is fully explored in COMP401, however, if you're curious how it works, the code for it is in the Connection class' addObserver and notifyObserversOfSend methods.

Part C. Friends List Window

We need a window to show us who is currently signed on to Chat110 to initiate a chat window with them (coming next!).

C.1 Initial Setup

C.1.0. In the comp110.chat.client package, add a new class named FriendsController. You can leave it empty for the time being.

C.1.1. Open SceneBuilder to begin working on a View. From the Containers tab, drag an AnchorPane into the designer pane. From the Controls tab, drag the following two required controls onto the AnchorPane: a ListView and a Button. You can optionally add a Label, as pictured below.


C.1.1.OPTIONAL - Want to make your FriendsView look nice even after the window has been resized? It turns out this is what the AnchorPane is designed for. If you select a control like the ListView and go to its Layout tab (pictured above right) you will see AnchorPaneConstraints. If you plug in specific values for any of the 4 given sides, the control will be "anchored" to that side. You can "Preview" this by going to the Preview menu in SceneBuilder and selecting "Show Preview in Window" and resizing the Window. I would encourage playing around with various permutations of this. If you would like a Label to be centered, anchor its left and right sides, say 8 units each, and then in the Properties tab select Node > Alignment > Center. You'll want to anchor the Button to the bottom only.

C.1.2 - Save your FXML file in the comp110.chat.client package as FriendsView.fxml - refresh your project in Eclipse and FriendsView.fxml should show up beneath FriendsController.java

C.1.3 - In the comp110.chat.client package, add a new class named FriendsWindow. Use the following boilerplate Window code below:

package comp110.chat.client;

import java.io.IOException;

import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class FriendsWindow {

    // Notice we will hold a reference to the FriendsController
    private FriendsController _controller;

    // Boilerplate for loading FXML bound to a Controller
    // TODO: You will need to tweak this so that the Controller is given a reference
    // to the Coordinator.
    public FriendsWindow() {
        try {

            _controller = new FriendsController();
            String view = "FriendsView.fxml";
            String title = "Chat110";

            // Construct the FXML Loader
            FXMLLoader loader = new FXMLLoader(getClass().getResource(view));
            loader.setController(_controller);

            // Load the Scene
            Scene scene = new Scene(loader.load());

            // Setup the window
            Stage stage = new Stage();
            stage.setScene(scene);
            stage.setTitle(title);
            stage.show();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

C.1.4 - In Coordinator class, declare a field of type FriendsWindow named _friends.

C.1.5 - In Coordinator's constructor, initialize the _friends field to a new instance of the FriendsWindow class.

Functionality checkpoint: Running the program should result in the FriendsWindow being visible, though it has no functionality yet! If you have any errors, or you do not see the FriendsWindow, resolve those first before moving further along.

C.2 - Updating the Friends View

Each time a "who" Packet is received, we will want the Friends list to update to show everyone currently connected to Chat110. At a high-level, the Coordinator will receive this Packet, it will then process the Packet and turn it into a List of onyens. Finally tell the FriendsWindow, "hey, update your List to this newly updated List of onyens".

C.2.1 - First, we need to give FriendsController the ability to interact with the ListView control. Recall that doing this requires giving the ListView an fx:id in SceneBuilder and declaring a matching @FXML field in the Controller.

In SceneBuilder, click on the ListView control, go to its code tab, and give it an fx:id of _friends. Save! Refresh in Eclipse!

In Eclipse, refresh the project! Then open FriendsController and import the following files: javafx.fxml.FXML and javafx.scene.control.ListView. Declare the following field to hook it up to the same control in the view.

 @FXML
 private ListView<String> _friends;

C.2.2 - Add a method that will allow us to tell the controller "hey, update the friends list to this new list of ONYENS". You will need to import the java.util.List interface. Using the code snippet below, implement the TODO note below.

   public void update(List<String> friends) {
        List<String> viewList = _friends.getItems();
        viewList.clear();
        // TODO: Loop through each element in the friends parameter and add each to viewList
    }

C.2.3 - The updated friends List will need to make its way from the Coordinator receiving a "who" Packet, to the FriendsController. Unfortunately, the Coordinator only has access to a FriendsWindow, not to a FriendsController. Large, object-oriented apps often run into this kind of an issue because the Window "contains" the Controller. The workaround we'll use is called a "proxy" because we'll add a method to the FriendsWindow that serves no other purpose than to relay, or proxy, the same method call along to the FriendsController.

In FriendsWindow, import java.util.List, and declare the following method:

Name:       update
Visibility: public
Parameters: 
  #1) Name: friends
      Type: List<String>
Return Type: void

The definition of this method is a single line of code which calls the update method on the Window's _controller field and passes the friends parameter to it as an argument.

C.2.4 - Updating the Friends List from the Coordinator

Whenever the Coordinator receives a "who" Packet, we will want it to tell the FriendsWindow to update its list of onyens. First, declare a constant (see Lecture 19) in the Coordinator class with the following characteristics:

Type:       String
Name:       WHO
Value:      "who"
Visibility: public

C.2.5 - Declare a helper method (see Lecture 20) with the following characteristics:

Name:        handleWho
Return Type: void
Visibility: private
Parameters:
  #1 Type: Packet
     Name: packet

Import java.util.List and java.util.ArrayList. Use the following stand-in code for the handleWho method's definition. You should be able to trace and understand what it is doing!

List<String> onyens = new ArrayList<String>();
onyens.add("carol");
onyens.add("krisj");
_friends.update(onyens);

C.2.6 - Finally, let's change Coordinator's packetReceived method to handle "who" Packets specially. Go ahead and delete the println statement.

Add an if-then conditional check to test to see if packet has a type of WHO. Note the Packet class has a method with the following declaration:

public String getType()

Remember, when comparing the equality of two String objects you must use the following method declared on String:

public boolean equals(String other)

In this conditional check, you should make use of the WHO constant you setup.

In the then-block, call the handleWho method and pass it the packet.

Functionality checkpoint: You should now be able to run the app, and in the Packet Tool window try sending a "who" message. As soon as the server responds, you should see the onyens "carol" and "krisj" appear in the list. Do not continue forward until this is working!

C.2.7 - Implement correct logic for the handleWho method in Coordinator. The Packet class has the following methods declared on it:

public int size() // # of parameters
public String getParameter(int i) // Return parameter at i

Remember, String separated by a colon in a Packet after the Packet's type is considered a parameter of the Packet. Using these two methods, write a for loop in the handleWho method that adds all of the parameters of the packet to the onyens list.

Functionality checkpoint: Try running your code and sending a who request through the Packet Tool again. You should see yours and all other connected onyens show up on the Friend ListView.

C.3 The Refresh Button

Having to type "who" into the Packet Tool isn't a very good user experience. Let's hook up the Refresh button! When clicked, it will trigger a method call in the FriendsController which, in turn, needs to tell the Coordinator "hey, refresh!" and, finally, the Coordinator will send a "who" Packet to the server.

Unfortunately, the FriendsController currently has no way of calling a method on the Coordinator to say "hey, refresh!" because the FriendsController has no reference to the Coordinator. We first need to tweak our existing code to pass a reference of the Coordinator to the FriendsController. We'll do this via adding parameters to the constructors of FriendsWindow and FriendsController and a field to hold the reference in FriendsController.

C.3.1 - Add a parameter of type Coordinator to the constructor of FriendsWindow. Name it anything you'd like.

C.3.2 - This will cause an error in the Coordinator class where a new FriendsWindow object is being constructed. Use 'this' as the argument to the FriendsWindow constructor to resolve the error.

C.3.3 - In the FriendsController class, declare a field to hold the reference to the Coordinator with the following characteristics:

Type:       Coordinator
Visibility: private
Name:       _coordinator

C.3.4 - In the FriendsController class, declare a constructor with a single parameter of type Coordinator. In the definition, assign that parameter to the field you added in C.3.3. You should be comfortable enough with the terminology of this instruction to turn it into code on the next midterm (constructor, paramater, field, assignment).

C.3.5 - Adding a 1-parameter constructor to FriendsController creates an error in the FriendsWindow class where the old 0-parameter constructor was used. Let's fix it! In FriendsWindow, pass the Coordinator parameter as an argument to the FriendsController.

That's quite a few steps just to "introduce" one object to another. Now that the FriendsController has a reference to the Coordinator, though, we can finally hook-up the refresh button!

C.3.6 - In the Coordinator, add the following method declaration and definition:

    public void refresh() {
        _connection.send(new Packet("who"));
    }

Notice that this refresh method is simply sending a "who" Packet to the server. When the server responds, the packetReceived method will be called and the list of onyens will be updated by the work you've already done in the handleWho method.

C.3.7 - In the FriendsController class, we need to add a method that will run when you click on the "Refresh" button. It will simply relay the method call to the Coordinator's refresh method you just declared.

    public void refresh() {
        _coordinator.refresh();
    }

C.3.8 - Finally, in SceneBuilder, select the Refresh button. Go to its Code tab. In the "On Action" field, enter "refresh". Save and refresh the project in Eclipse.

Progress Checkpoint: You should now be able to press the Refresh button and see the Friends View's List update. In the Packet Tool, you should also see the Packet being sent and received each time you click "Refresh".

Guided Commentary: Notice that the Friends Window and Controller know nothing about a Connection nor about a Packet. All of the logic for working with the Connection and Packets is contained in the Coordinator class. The Coordinator serves as a "layer" that rests between the Connection and the Windows. Its responsibility is to send instructions to Windows (like "update the friend list to this new list of onyens") and to receive commands from windows and send packets to the server (i.e. "refresh the friends list" becomes a "who" packet). This is an example of a design principle called separation of concerns. The Coordinator and the FriendWindow/FriendController classes each have their own distinct purposes and they rely upon communicating with one another via method calls for the other's capabilities. You will learn more about program design principles like separation of concerns in COMP401.

Part D. The Chat Window

Now that we have a Friend List, we need a window for an individual chat dialog.

D.1 Initial Setup

D.1.0 - In the comp110.chat.client package, add a new class named ChatController. Since we learned in Part C that this Controller will likely need a reference to the Coordinator (i.e. so the ChatController can tell the Coordinator "send this message to this particular onyen") we'll go ahead and add one and a field to hold the reference. Each chat will also be with a specific person, so we'll add a field for the onyen of the person we're chatting with, too.

Fields
  #1  Type: String
      Name: _onyen
  #2  Type: Coordinator
      Name: _coordinator
Constructor
  Parameters:
    #1 Type: String
       Name: onyen
    #2 Type: Coordinator
       Name: coordinator

The constructor should initialize each of the fields using its parameters.

D.1.1 - Open SceneBuilder to begin working on a View. From the Containers tab, drag an AnchorPane into the designer pane. From the Controls tab, drag the following two required controls onto the AnchorPane: a ListView and a TextField. Our simple design is shown below.


D.1.2 - Save your FXML file in the comp110.chat.client package as ChatView.fxml. Can't remember where your project is located? Right click on your project in Eclipse and select "Show In" > "System Explorer" and it will open the directory your project is stored in. Refresh your project in Eclipse and it should show up!

D.1.3 - In the comp110.chat.client package, add a new class named ChatWindow. Use the following modified boilerplate Window code below:

package comp110.chat.client;

import java.io.IOException;

import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class ChatWindow {

    private Stage _stage;

    private ChatController _controller;

    public ChatWindow(String onyen, Coordinator dispatcher) {
        try {

            // Notice we're constructing the ChatController with the correct parameters
            // We're also making the title of the window be based on the onyen
            _controller = new ChatController(onyen, dispatcher);
            String view = "ChatView.fxml";
            String title = onyen + " - Chat110";

            // Construct the FXML Loader
            FXMLLoader loader = new FXMLLoader(getClass().getResource(view));
            loader.setController(_controller);

            // Load the Scene
            Scene scene = new Scene(loader.load());

            // Setup the window
            _stage = new Stage();
            _stage.setScene(scene);
            _stage.setTitle(title);
            this.show();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /*
     * Cause the Stage to appear (even after it's been closed!) and force it to be brought as the front window open.
     */
    public void show() {
        _stage.show();
        _stage.toFront();
    }

}

D.1.4 - In the Coordinator's constructor, try adding the following snippet of code, after the existing initialization statements, to declare a local ChatWindow variable and initialize it to a newly constructed ChatWindow object. Substitute your ONYEN in the String.

ChatWindow chat = new ChatWindow("onyen", this);

Functionality checkpoint: Running the program should result in a blank ChatWindow appearing. It will have no functionality, but you should see your ONYEN in the window's title bar. This is a ChatWindow to your own ONYEN we'll test with as we hook everything up.

D.2 - Sending Messages from a ChatWindow

When you enter a message in a ChatWindow and press enter it should send the message to the person you are chatting with. We will need to hook up the following chain of events to occur when the user enters a message and presses enter:

1. The ChatView calls a method in the ChatController when enter is pressed
2. The ChatController calls a method on the Coordinator telling it to send a message to a specific ONYEN with a specific text
3. The Coordinator then calls the send method on the Connection with a properly formatted "dm" Packet.

We will work our way from bottom to top, starting from the Coordinator's send method and working our way backwards toward the View.

D.2.3 - In the Coordinator class, add a method with the following characteristics:

Name: send
Return Type: void
Visibility: public
Parameters:
  #1 Type: String
     Name: onyen
  #2 Type: String
     Name: text

The method definition should simply send a properly formatted "dm" Packet object using the _connection field's send method. You will need to use String concatenation to setup the Packet. Refer to the Coordinator's refresh method for how to send a new Packet. Remember, the format of a "dm" packet is:

dm:onyen:text

D.2.2 - Prepare the ChatController class for bindings to the ChatView. Import the following three classes: javafx.fxml.FXML, javafx.scene.control.ListView, javafx.scene.control.TextField. Then add the following two fields:

	@FXML
	private TextField _text;

	@FXML
	private ListView<String> _history;

Next, add the following method to ChatController, which we will next hook-up as the event handler for when enter is pressed in the _text field:

	public void send() {
		_coordinator.send(_onyen, _text.getText());
                _text.clear();
	}

Notice this method simply uses the _onyen field the ChatController was constructed with, as well as the current text written in the TextField to instruct the Coordinator to send a message.

Finally, we need to setup the bindings in the View FXML to each of the controls and the send event handler. Make the following connections in SceneBuilder:

ListView
  fx:id:     _history
TextField
  fx:id:     _text
  On Action: send

Save in SceneBuilder and refresh your project in Eclipse!

Functionality Checkpoint: You should be able to run your project and when you enter text in the ChatWindow and press enter it should disappear. The message will not yet show up in the History List View, but if you look at your Packet Tool you should see a correctly formatted DM Packet was sent to you and then received by you.

D.3 - Organizing ChatWindows in the Coordinator

We are now ready to face a new problem we have not yet addressed: how do we organize multiple Window objects? We will potentially have *many* ChatWindow objects open at once, one per each person we are chatting with, so the Coordinator must have a way to keep each organized and know how to relay a dm from, say, "cfolt" to the correct ChatWindow.

For this, we will use the Map abstract data type, which will keep track of the "mapping" between an ONYEN String and its corresponding ChatWindow. Maps are first introduced and covered in Lecture 21 and on this Maps topic page.

D.3.1 - In Coordinator.java, import the java.util.Map interface and declare the following field:

Type: Map<String, ChatWindow>
Name: _chats

D.3.2 - In Coordinator, import the class java.util.HashMap, and initialize the _chats field using the HashMap class in Coordinator's constructor. For an example, see the Maps topic page.

D.3.3 - When a message is received from someone for the first time, their onyen will not be a key in the _chats Map. When we get the ONYEN, the resulting value will be null. In this case, we will need to construct a new ChatWindow for their onyen and put it in the _chats Map. On subsequent messages, there will be a ChatWindow associated with their ONYEN.

Declare the following helper method:

Name:        getChatWindow
Visibility:  private
Return Type: ChatWindow
Parameters:
  #1 Type: String
     Name: onyen

The purpose of this method is to return a ChatWindow for a given ONYEN and show the ChatWindow if it were previously closed. The pseudo-code you should use for the logic of this method is as follows:

1. Declare and initialize a local variable to get the 
   ChatWindow for the key onyen from _chats.
2. If the variable is equal to null, then:
   2.a Construct and assign to the local variable a new ChatWindow object
   2.b Properly put this object in the _chats map
3. Call the show method on the ChatWindow
4. Return the ChatWindow to the method's caller

In Coordinator's constructor, replace the following line:

ChatWindow chat = new ChatWindow("onyen", this);

With (using your ONYEN in the String):

ChatWindow chat = this.getChatWindow("onyen");

Note! Be sure that you have already initialized the _chats Map *before* calling getChatWindow. Otherwise you will encounter a NullPointerException.

Functionality checkpoint: You should be able to run your program again and still see a ChatWindow opened for you. If you have any errors at this point you should stop, figure out where they are coming from, and resolve them before continuing on.

Guided commentary: Take notice that every time you call the getChatWindow helper method, you will always be returned a ChatWindow. If it is the first time you call the method with an ONYEN, a new ChatWindow will be constructed for that ONYEN. On subsequent calls, you will get back a reference to the exact same ChatWindow object for that ONYEN every single time. This is a common use of Maps and is a simple example of a technique we call "caching" that has many other uses, as well.

D.4 - Posting Messages to a ChatWindow's History

Every time a dm Packet is sent or received, the Coordinator should instruct the correct ChatWindow to add the message to its history. Once again, the ChatWindow will relay or "proxy" that message along to its ChatController which is responsible for updating the View.

D.4.1 - Add the following method to ChatController which will add a message to the _history ListView's items List.

	public void addHistory(String message) {
		_history.getItems().add(message);
		_history.scrollTo(_history.getItems().size() - 1);
	}

D.4.2 - Add the following method to ChatWindow, which simply relays the message along to the window's ChatController object.

	public void addHistory(String message) {
		_controller.addHistory(message);
	}

Functionality Checkpoint: For testing purposes, try calling the method above in the Coordinator's constructor...

ChatWindow chat = new ChatWindow("krisj", this);
chat.addHistory("You: hello, world");

You should see the String "You: hello, world" appear in your ChatWindow before continuing on.

D.5 - Sneaking dm Packets into ChatWindows

Now that we have a mechanism for looking up a ChatWindow by ONYEN and adding messages to its history, we need to specially handle DM packets as they are sent and received.

D.5.1 - In Coordinator, declare a constant with the following characteristics:

Type:       String
Name:       DM
Value:      "dm"
Visibility: public

D.5.2 - In Coordinator, declare a helper method with the following characteristics. This method will be called when a DM packet is received.

Name:        handleDMReceived
Return Type: void
Visibility:  private
Parameters:
  #1 Type: Packet
     Name: packet

You can use the following definition of this method. You should understand each line's purpose. For the "join" method, read through the implementation of this method in the Packet class.

		String onyen = packet.getParameter(0);
		String text = packet.join(1);
		this.getChatWindow(onyen).addHistory(onyen + ": " + text);

D.5.3 - In Coordinator, we will also declare a similar method for when a DM packet is sent. You can use the snippet below. Notice the subtle difference between it and the handleDMReceived method above.

	private void handleDMSent(Packet packet) {
		String onyen = packet.getParameter(0);
		String text = packet.join(1);
		this.getChatWindow(onyen).addHistory("You: " + text);
	}

D.5.4 - Now that we have helper methods to handle when a DM packet is sent or received, we need to modify the packetSent and packetReceived methods to calthese helper methods when a DM packet is sent or received.

In the packetReceived method, add an else-if check to see if the Packet's type is a DM (use the constant you setup in D.5.1 as the comparison). If so, call the handleDMReceived helper method and pass it the Packet.

In the packetSent method, add an if-then check to see if the Packet's type is a DM. If so, call the handleDMSent helper method and pass it the Packet.

Functionality checkpoint: you should now be able to send yourself a message and it should appear twice in your ChatWindow. Once, from you, the second time from your onyen. Try using the PacketTool to send a message to another connected student like you did in part 1. You should see a new ChatWindow pop-up that you can begin using!

Conceptual checkpoint: Take a peek at the Packet Tool to convince yourself that what you've done in the last series of steps is wire up the user interface to specially handle DM messages that are sent or received. Every time a DM message is sent or received a specific series of steps is taken by the Coordinator to find/construct the correct DM window, show it, and add the message to its history.

Now all that's left is hooking up the FriendsController to allow us to double click on an ONYEN and it open a new ChatWindow!

Part E - Finishing Touches

E.1 - Cleaning up

E.1.1 - Remove the following lines that setup a dummy ChatWindow from Coordinator's constructor:

ChatWindow chat = this.getChatWindow("krisj");
chat.addHistory("You: hello, world");

E.1.2 - In the Chat110App class' start method, after sending the auth packet, go ahead and send a "who" Packet as well. This way the FriendWindow is loaded with connected ONYENs automatically when the program starts.

E.2 - Starting ChatWindows from the Friends List

E.2.1 - If you want to initiate a chat with someone from the FriendsWindow, it will need to be able to tell the Coordinator "hey, show the ChatWindow for <onyen>". First, let's add this simple method to the Coordinator class:

	public void showChatWindow(String onyen) {
		this.getChatWindow(onyen);
	}

Notice this method is making use of the helper method we setup for finding a particular ChatWindow by someone's onyen. The logic for showing the window is contained within the getChatWindow logic. This helper method is now called (and reused!) from three different places in the Coordinator class.

E.2.2 - In the FriendsController, import the class javafx.scene.input.MouseEvent. Add the following method, below:


    public void handleFriendClick(MouseEvent event) {
    	if (event.getClickCount() == 2) {
    		String onyen = _friends.getSelectionModel().getSelectedItem();
    		_coordinator.showChatWindow(onyen);
    	}
    }

You should understand each of the lines of this method. The if-then test is checking to see if this click event is a double click. If so, we are asking the _friends ListView for its selected item which is the onyen being clicked on. Then we are telling the Coordinator to show the correct chat window for the given ONYEN.

E.2.3 - In SceneBuilder, open FreindsView.fxml, and select the ListView. In its code tab, find the "On Mouse Clicked" event and enter the name of the method we just declared above: handleFriendClick. Save in SceneBuilder, refresh the project in Eclipse.

Functionality Checkpoint: You should now be able to start the program, see the friends list populated with connected ONYENs, double click an ONYEN in the list, and begin sending messages to them. If someone sends you a message and you do not have a chat window open, it should pop open automatically!

Conceptual checkpoint: As you use the program, keep your eye on the Packet Tool to see that as you press refresh, send, and receive messages the Coordinator is receiving the same messages as the Packet Tool, however it has different logic for how it handles each of the Packets and it coordinates where it sends commands to: the FriendsWindow or a specific ChatWindow. Neither the FriendsWindow/Controller nor the ChatWindow/Controller know anything about packets or the connection to the server. Each simply sends instructions, via method calls, to the Coordinator when the user takes an action on the view. Additionally, the Coordinator sends instructions to the windows, when new information is received. 

The design of classes and methods in a larger app like this is the focus of COMP401 and upper level courses. The goal of this assignment was to give you exposure to a full-sized app and to see the fundamental concepts we've learned in isolation in 110 come together to form a single coherent app. While you likely could not start from scratch and come up with these exact classes and methods directly after finishing COMP110, hopefully you are starting to get a sense of what it feels like to build an app and how object-oriented pieces and user interfaces can be fit together.

E.2.4 - Remove the Packet Tool Window - The last required step in this part of the problem set is to comment out the line in the Chat110App's start method that constructs the Packet Tool Window. We only made that in Part 1 so that it would be easier to debug in Part 2 and so that you could see exactly what Packets were being sent to and received from the server.

Hacker Edition Extensions

Coming soon! Ideas include: adding everyone's emoji as their avatar to the FriendList and ChatWindows, read receipts, auto-refreshing the friends list on a 10 second interval, and more!