Chat page UI implemented.

This commit is contained in:
Asal Lotfi
2025-08-23 15:48:12 +03:30
parent f80fd60a48
commit 221f093324
33 changed files with 855 additions and 69 deletions
@@ -0,0 +1,277 @@
package org.to.telegramfinalproject.UI;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.geometry.Side;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Circle;
import javafx.stage.FileChooser;
import java.io.File;
public class ChatPageController {
// ===== messages area =====
@FXML private VBox messageContainer;
@FXML private ScrollPane messageScrollPane;
// ===== input area =====
@FXML private TextArea messageInput;
@FXML private Button sendButton;
@FXML private Button attachmentButton;
@FXML private ImageView attachmentIcon; // <ImageView> inside the attachment button
// ===== header =====
@FXML private ImageView userAvatar; // 36x36 in the FXML
@FXML private Label chatTitle; // contact/group title
@FXML private Label chatStatus; // last seen / online
@FXML private Button searchInChatButton; // magnifier button
@FXML private ImageView searchIcon;
@FXML private Button moreButton; // 3-dots button
@FXML private ImageView moreIcon;
@FXML private ContextMenu moreMenu;
@FXML private MenuItem viewProfileItem;
@FXML private MenuItem deleteChatItem;
// ===== send icon =====
@FXML private ImageView sendIcon;
// ===== state =====
private String chatName;
private final ThemeManager themeManager = ThemeManager.getInstance();
// Where your icons live
private static final String ICON_BASE = "/org/to/telegramfinalproject/Icons/";
@FXML
public void initialize() {
// Send button
if (sendButton != null) {
sendButton.setOnAction(e -> sendMessage());
}
// ENTER = send, SHIFT+ENTER = newline
messageInput.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == javafx.scene.input.KeyCode.ENTER) {
if (e.isShiftDown()) {
// let newline happen
} else {
e.consume(); // block newline
sendMessage();
}
}
});
// Enable/disable send button by content (ignore spaces/newlines)
messageInput.textProperty().addListener((obs, oldV, newV) -> {
boolean hasRealText = newV != null && !newV.trim().isEmpty();
sendButton.setDisable(!hasRealText);
// toggle style class for color state
var sc = sendButton.getStyleClass();
sc.removeAll("send-empty", "send-ready");
sc.add(hasRealText ? "send-ready" : "send-empty");
});
// Auto-resize input box like Telegram
messageInput.textProperty().addListener((obs, oldText, newText) -> {
Platform.runLater(() -> {
var textNode = messageInput.lookup(".text");
if (textNode != null) {
double textHeight = textNode.getBoundsInLocal().getHeight();
double padding = 20; // top + bottom padding
double newHeight = textHeight + padding;
if (newHeight < 40) newHeight = 40; // min (1 row)
if (newHeight > 120) newHeight = 120; // max (~5 rows)
messageInput.setPrefHeight(newHeight);
}
});
});
// Start in "empty" state
sendButton.getStyleClass().add("send-empty");
// attach file
if (attachmentButton != null) {
attachmentButton.setOnAction(e -> openFileChooser());
}
// Hook "More" button → show menu under it
if (moreButton != null) {
moreButton.setOnAction(e -> {
if (!moreMenu.isShowing()) {
moreMenu.show(moreButton, Side.BOTTOM, 0, 0);
} else {
moreMenu.hide();
}
});
}
// Menu item actions
viewProfileItem.setOnAction(e -> {
System.out.println("Viewing profile of " + chatName);
// TODO: open profile UI
});
deleteChatItem.setOnAction(e -> {
System.out.println("Deleting chat with " + chatName);
// TODO: delete logic
});
// Initial icon sync once the Scene is ready (stylesheet applied)
Platform.runLater(this::syncIconsWithTheme);
// Auto-Scroll
messageContainer.heightProperty().addListener((obs, oldVal, newVal) -> {
Platform.runLater(() -> {
var timeline = new javafx.animation.Timeline();
var kv = new javafx.animation.KeyValue(
messageScrollPane.vvalueProperty(), 1.0, javafx.animation.Interpolator.EASE_BOTH
);
var kf = new javafx.animation.KeyFrame(javafx.util.Duration.millis(200), kv);
timeline.getKeyFrames().add(kf);
timeline.play();
});
});
// Smooth scroll feel
messageScrollPane.getStylesheets().add(getClass().getResource("/org/to/telegramfinalproject/CSS/scrollpane.css").toExternalForm());
messageScrollPane.setPannable(true);
messageScrollPane.setFitToWidth(true);
messageScrollPane.setFitToHeight(false);
messageScrollPane.getContent().setOnScroll(event -> {
double deltaY = event.getDeltaY() * 0.00003; // smaller = smoother
messageScrollPane.setVvalue(messageScrollPane.getVvalue() - deltaY);
});
// React to theme changes everywhere
themeManager.darkModeProperty().addListener((o, oldVal, isDark) -> syncIconsWithTheme());
}
@FXML
private void openSearchPanel() {
MainController.getInstance().showSearchPanel();
}
/** Called by main controller when opening a chat. */
public void setChat(String chatName, String avatarPath) {
this.chatName = chatName;
// Header text (if present)
if (chatTitle != null) chatTitle.setText(chatName);
if (chatStatus != null) chatStatus.setText("last seen recently"); // or live status
// Load avatar if provided
if (userAvatar != null && avatarPath != null) {
try {
Image avatarImg = new Image(getClass().getResourceAsStream(avatarPath));
userAvatar.setImage(avatarImg);
Circle clip = new Circle(18, 18, 18); // x, y, radius
userAvatar.setClip(clip);
} catch (Exception e) {
System.err.println("Could not load avatar: " + avatarPath);
}
}
addSystemMessage("Chat with " + chatName + " opened.");
Platform.runLater(() -> messageInput.requestFocus());
}
// ----- actions -----
private void sendMessage() {
String text = messageInput.getText() == null ? "" : messageInput.getText().trim();
if (!text.isEmpty()) {
addMessage("You", text);
messageInput.clear();
// TODO: send to server
}
}
private void openFileChooser() {
FileChooser fc = new FileChooser();
fc.setTitle("Select a file to send");
File file = fc.showOpenDialog(attachmentButton.getScene().getWindow());
if (file != null) {
System.out.println("Selected file: " + file.getAbsolutePath());
// TODO: actually send file
addSystemMessage("Attached file: " + file.getName());
}
}
// ----- UI helpers -----
/** Add a normal message bubble (very simple for now). */
public void addMessage(String sender, String content) {
Label msg = new Label(sender + ": " + content);
msg.setWrapText(true);
boolean dark = themeManager.isDarkMode();
String bubbleColor = dark ? "#20405a" : "#4fa8f0";
String textColor = dark ? "#e8f1f8" : "#0f141a";
msg.setStyle(
"-fx-background-color: " + bubbleColor + ";" +
"-fx-text-fill: " + textColor + ";" +
"-fx-padding: 6 10; -fx-background-radius: 10;"
);
messageContainer.getChildren().add(msg);
}
private void addSystemMessage(String content) {
Label sys = new Label(content);
sys.setStyle("-fx-text-fill: gray; -fx-font-size: 11;");
messageContainer.getChildren().add(sys);
messageScrollPane.layout();
messageScrollPane.setVvalue(1.0);
}
/** Update all header/footer icons according to current theme. */
private void syncIconsWithTheme() {
boolean dark = themeManager.isDarkMode();
// We use “_light” icons on dark backgrounds, and “_dark” on light backgrounds.
String suffix = dark ? "_light.png" : "_dark.png";
// attachment
if (attachmentIcon != null) {
attachmentIcon.setImage(loadIcon("attachment" + suffix));
}
// send
if (sendIcon != null) {
sendIcon.setImage(loadIcon("send_cyan2.png"));
}
// header icons
if (searchIcon != null) {
searchIcon.setImage(loadIcon("search" + suffix));
}
if (moreIcon != null) {
moreIcon.setImage(loadIcon("more" + suffix));
}
// header text tint (if youre not fully relying on CSS)
if (chatTitle != null) chatTitle.setStyle(dark ? "-fx-text-fill:#e8f1f8;" : "-fx-text-fill:#0f141a;");
if (chatStatus != null) chatStatus.setStyle(dark ? "-fx-text-fill:#8ea1b2;" : "-fx-text-fill:#7e8a97;");
// View profile icon in more button
((ImageView) viewProfileItem.getGraphic())
.setImage(loadIcon("view_profile" + suffix));
}
private Image loadIcon(String filename) {
var url = getClass().getResource(ICON_BASE + filename);
if (url == null) {
System.err.println("Icon not found: " + ICON_BASE + filename);
return null;
}
return new Image(url.toExternalForm());
}
}
@@ -6,13 +6,9 @@ import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.SplitPane;
import javafx.scene.control.TextField;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@@ -22,23 +18,46 @@ import java.io.IOException;
public class MainController {
// === LEFT PANE ===
@FXML private VBox chatListContainer; // inside the scrollPane
@FXML private ScrollPane scrollPane; // chat list scroll
@FXML private VBox chatSearchPane; // search results panel
@FXML private ListView<String> chatSearchResults;
@FXML private MenuButton scopeDropdown;
// === TOP BAR ===
@FXML private TextField searchBar; // global search bar
@FXML private Button menuButton;
// === RIGHT PANE ===
@FXML private VBox leftPane;
@FXML private javafx.scene.layout.StackPane chatDisplayArea;
@FXML private Label placeholderLabel;
// === Main root ===
@FXML private StackPane mainRoot;
@FXML private VBox chatListContainer;
@FXML private StackPane chatDisplayArea;
@FXML private SplitPane mainSplitPane;
// === Sidebar ===
@FXML private Pane overlay;
@FXML private ImageView menuIcon;
@FXML private ScrollPane scrollPane;
@FXML private SplitPane mainSplitPane;
@FXML private VBox leftPane;
@FXML private Button menuButton;
@FXML private TextField searchBar;
// === STATE ===
private static MainController instance;
private Node sidebarRoot;
private boolean isSidebarOpen = false;
// Add ThemeManager handle
private final ThemeManager themeManager = ThemeManager.getInstance();
public MainController() {
instance = this;
}
public static MainController getInstance() {
return instance;
}
@FXML
public void initialize() {
addSampleChats();
@@ -79,6 +98,44 @@ public class MainController {
if (!mainSplitPane.getDividers().isEmpty()) {
mainSplitPane.getDividers().get(0).positionProperty().addListener((o, ov, nv) -> clampDivider());
}
// Search in chat field listener
searchBar.textProperty().addListener((obs, oldV, newV) -> {
if (!chatSearchPane.isVisible()) return; // only react if in search mode
if (newV.trim().isEmpty()) {
chatSearchResults.setVisible(false);
chatSearchResults.setManaged(false);
} else {
chatSearchResults.setVisible(true);
chatSearchResults.setManaged(true);
chatSearchResults.getItems().setAll(
"Result 1: " + newV,
"Result 2: " + newV,
"Result 3: " + newV
);
}
});
}
// Called from ChatPageController when user clicks search button
public void showSearchPanel() {
scrollPane.setVisible(false);
scrollPane.setManaged(false);
chatSearchPane.setVisible(true);
chatSearchPane.setManaged(true);
searchBar.requestFocus();
}
@FXML
public void closeSearchPanel() {
chatSearchPane.setVisible(false);
chatSearchPane.setManaged(false);
scrollPane.setVisible(true);
scrollPane.setManaged(true);
}
private void addSampleChats() {
@@ -117,8 +174,20 @@ public class MainController {
}
private void openChat(String chatName) {
chatDisplayArea.getChildren().clear();
chatDisplayArea.getChildren().add(new javafx.scene.control.Label("Chat with " + chatName));
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/to/telegramfinalproject/Fxml/chat_page.fxml"));
Node chatPage = loader.load();
ChatPageController controller = loader.getController();
controller.setChat("Alice", "/org/to/telegramfinalproject/Avatars/profile_test.png");
chatDisplayArea.getChildren().clear();
chatDisplayArea.getChildren().add(chatPage);
} catch (IOException e) {
e.printStackTrace();
}
}
@FXML
@@ -3,7 +3,6 @@ package org.to.telegramfinalproject.UI;
import javafx.animation.TranslateTransition;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.image.Image;
@@ -43,7 +42,7 @@ public class SidebarMenuController {
@FXML
public void initialize() {
// Load default profile image
Image profile = loadImage("/org/to/telegramfinalproject/Images/profile.png");
Image profile = loadImage("/org/to/telegramfinalproject/Avatars/profile.png");
if (profile != null) profileImage.setImage(profile);
setupButtonActions();
@@ -4,6 +4,7 @@ package org.to.telegramfinalproject.UI;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import java.io.IOException;
@@ -18,6 +19,11 @@ public class TelegramApplication extends Application {
stage.setTitle("Telegram");
stage.setScene(scene);
// Add icon to the stage
Image icon = new Image(TelegramApplication.class.getResourceAsStream("/org/to/telegramfinalproject/Images/telegram_icon.png"));
stage.getIcons().add(icon);
stage.show();
}
public static void main(String[] args) {