319 lines
11 KiB
Java
319 lines
11 KiB
Java
package view;
|
|
|
|
import controller.PhotoAlbumController;
|
|
import model.Photo;
|
|
import model.PhotoAlbumModel;
|
|
|
|
import javax.swing.*;
|
|
import java.awt.*;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* A view that displays a photo album user interface.
|
|
* <p>
|
|
* This class is responsible for displaying the photo album's graphical interface,
|
|
* including the list of photos, current photo display, and control buttons.
|
|
* The view implements the {@link PhotoAlbumModel.ModelChangeListener} interface
|
|
* to receive notifications of model changes.
|
|
* <p>
|
|
* The view provides user interface components for:
|
|
* <ul>
|
|
* <li>Displaying a list of photos with thumbnails and details
|
|
* <li>Showing the currently selected photo
|
|
* <li>Navigation controls (previous/next)
|
|
* <li>Photo management (add/delete)
|
|
* <li>Sorting options
|
|
* </ul>
|
|
*
|
|
* @author Yuri Tatishchev
|
|
* @version 0.1 2025-03-26
|
|
* @see PhotoAlbumModel
|
|
* @see PhotoAlbumController
|
|
* @see Photo
|
|
*/
|
|
public class PhotoAlbumView extends JFrame implements PhotoAlbumModel.ModelChangeListener {
|
|
private PhotoAlbumModel model;
|
|
|
|
private JList<String> photoList;
|
|
private DefaultListModel<String> listModel;
|
|
private JLabel currentPhotoLabel;
|
|
private JButton addButton;
|
|
private JButton deleteButton;
|
|
private JButton previousButton;
|
|
private JButton nextButton;
|
|
private JComboBox<String> sortingCombo;
|
|
|
|
public PhotoAlbumView() {
|
|
initializeComponents();
|
|
setupLayout();
|
|
}
|
|
|
|
/**
|
|
* Initializes all UI components of the photo album view.
|
|
* <p>
|
|
* Sets up the frame properties, creates buttons, list components,
|
|
* and initializes them with default states.
|
|
*/
|
|
private void initializeComponents() {
|
|
setTitle("Photo Album Manager");
|
|
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
|
|
setSize(800, 600);
|
|
|
|
listModel = new DefaultListModel<>();
|
|
photoList = new JList<>(listModel);
|
|
currentPhotoLabel = new JLabel("No photo selected", SwingConstants.CENTER);
|
|
|
|
addButton = new JButton("Add Photo");
|
|
deleteButton = new JButton("Delete Photo");
|
|
previousButton = new JButton("Previous");
|
|
nextButton = new JButton("Next");
|
|
// Disable navigation buttons by default
|
|
previousButton.setEnabled(false);
|
|
nextButton.setEnabled(false);
|
|
deleteButton.setEnabled(false);
|
|
|
|
String[] sortOptions = {"Sort by Name", "Sort by Date", "Sort by Size"};
|
|
sortingCombo = new JComboBox<>(sortOptions);
|
|
|
|
listModel = new DefaultListModel<>();
|
|
photoList = new JList<>(listModel);
|
|
photoList.setEnabled(false);
|
|
photoList.setCellRenderer(new PhotoListCellRenderer());
|
|
photoList.setFixedCellHeight(60); // Accommodate thumbnails
|
|
}
|
|
|
|
|
|
/**
|
|
* Sets up the layout of the photo album view.
|
|
* <p>
|
|
* Arranges the UI components using BorderLayout with:
|
|
* <ul>
|
|
* <li>Photo list in the WEST
|
|
* <li>Current photo display in the CENTER
|
|
* <li>Control buttons in the SOUTH
|
|
* </ul>
|
|
*/
|
|
private void setupLayout() {
|
|
setLayout(new BorderLayout());
|
|
|
|
// Left panel with photo list
|
|
JScrollPane listScrollPane = new JScrollPane(photoList);
|
|
listScrollPane.setPreferredSize(new Dimension(200, 0));
|
|
add(listScrollPane, BorderLayout.WEST);
|
|
|
|
// Center panel with current photo
|
|
JPanel centerPanel = new JPanel(new BorderLayout());
|
|
centerPanel.add(currentPhotoLabel, BorderLayout.CENTER);
|
|
add(centerPanel, BorderLayout.CENTER);
|
|
|
|
// Bottom panel with controls
|
|
JPanel controlPanel = new JPanel();
|
|
controlPanel.add(previousButton);
|
|
controlPanel.add(nextButton);
|
|
controlPanel.add(addButton);
|
|
controlPanel.add(deleteButton);
|
|
controlPanel.add(sortingCombo);
|
|
add(controlPanel, BorderLayout.SOUTH);
|
|
}
|
|
|
|
/**
|
|
* Sets the controller for the photo album view.
|
|
* <p>
|
|
* Attaches the controller to the view and sets up event listeners
|
|
* for the control buttons and sorting combo box.
|
|
*
|
|
* @param controller the controller to set
|
|
*/
|
|
public void setController(PhotoAlbumController controller) {
|
|
this.model = controller.getModel();
|
|
model.addListener(this);
|
|
|
|
addButton.addActionListener(e -> controller.handleAddPhoto());
|
|
deleteButton.addActionListener(e -> controller.handleDeletePhoto());
|
|
nextButton.addActionListener(e -> controller.handleNext());
|
|
previousButton.addActionListener(e -> controller.handlePrevious());
|
|
sortingCombo.addActionListener(e -> controller.handleSort(sortingCombo.getSelectedIndex()));
|
|
|
|
// Select default sorting option
|
|
sortingCombo.setSelectedIndex(1);
|
|
}
|
|
|
|
@Override
|
|
public void onModelChanged() {
|
|
updatePhotoList();
|
|
updateCurrentPhoto();
|
|
updateNavigationButtons();
|
|
}
|
|
|
|
/**
|
|
* Updates the list of photos displayed in the sidebar.
|
|
* <p>
|
|
* Clears the current list model and populates it with
|
|
* names of photos from the model.
|
|
*/
|
|
private void updatePhotoList() {
|
|
listModel.clear();
|
|
List<Photo> photos = model.getPhotos();
|
|
for (Photo photo : photos) {
|
|
listModel.addElement(photo.name());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the display of the current photo in the main view area.
|
|
* <p>
|
|
* If a photo is selected, loads and displays its image.
|
|
* If loading fails, displays an error message.
|
|
* If no photo is selected, displays a default message.
|
|
*/
|
|
private void updateCurrentPhoto() {
|
|
Photo current = model.getCurrentPhoto();
|
|
if (current != null) {
|
|
ImageIcon icon = loadImage(current.filePath());
|
|
if (icon != null) {
|
|
currentPhotoLabel.setIcon(icon);
|
|
currentPhotoLabel.setText("");
|
|
} else {
|
|
currentPhotoLabel.setIcon(null);
|
|
currentPhotoLabel.setText("Unable to load image");
|
|
}
|
|
} else {
|
|
currentPhotoLabel.setIcon(null);
|
|
currentPhotoLabel.setText("No photo selected");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the enabled state of navigation buttons.
|
|
* <p>
|
|
* Enables or disables the previous/next buttons based on
|
|
* the current position in the photo album.
|
|
* Enables the delete button only when the album is not empty.
|
|
*/
|
|
private void updateNavigationButtons() {
|
|
previousButton.setEnabled(model.hasPrevious());
|
|
nextButton.setEnabled(model.hasNext());
|
|
deleteButton.setEnabled(model.getCurrentPhoto() != null);
|
|
}
|
|
|
|
/**
|
|
* Loads and scales an image from the given path.
|
|
* <p>
|
|
* Creates a scaled version of the image suitable for the main display area.
|
|
*
|
|
* @param path the file path of the image to load
|
|
* @return a scaled ImageIcon, or null if loading fails
|
|
*/
|
|
private ImageIcon loadImage(String path) {
|
|
try {
|
|
ImageIcon icon = new ImageIcon(path);
|
|
Image img = icon.getImage();
|
|
Image scaledImg = img.getScaledInstance(400, 300, Image.SCALE_SMOOTH);
|
|
return new ImageIcon(scaledImg);
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* A custom list cell renderer for displaying photos with thumbnails and details.
|
|
* <p>
|
|
* This inner class renders each photo in the list with:
|
|
* <ul>
|
|
* <li>A thumbnail image
|
|
* <li>The photo name
|
|
* <li>Additional details (date and file size)
|
|
* </ul>
|
|
* <p>
|
|
* It also implements thumbnail caching to improve performance.
|
|
*/
|
|
private class PhotoListCellRenderer extends JPanel implements ListCellRenderer<String> {
|
|
private final JLabel imageLabel = new JLabel();
|
|
private final JLabel textLabel = new JLabel();
|
|
private final JLabel detailsLabel = new JLabel();
|
|
|
|
private final Map<String, ImageIcon> thumbnailCache = new HashMap<>();
|
|
|
|
public PhotoListCellRenderer() {
|
|
setLayout(new BorderLayout(5, 0));
|
|
|
|
// Left side - image
|
|
imageLabel.setPreferredSize(new Dimension(50, 50));
|
|
add(imageLabel, BorderLayout.WEST);
|
|
|
|
// Center - name and details
|
|
JPanel textPanel = new JPanel(new GridLayout(2, 1));
|
|
textPanel.setOpaque(false);
|
|
textLabel.setFont(textLabel.getFont().deriveFont(Font.BOLD));
|
|
detailsLabel.setForeground(Color.GRAY);
|
|
detailsLabel.setFont(detailsLabel.getFont().deriveFont(10.0f));
|
|
textPanel.add(textLabel);
|
|
textPanel.add(detailsLabel);
|
|
add(textPanel, BorderLayout.CENTER);
|
|
|
|
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
|
|
setOpaque(true);
|
|
}
|
|
|
|
@Override
|
|
public Component getListCellRendererComponent(JList<? extends String> list,
|
|
String name,
|
|
int index,
|
|
boolean isSelected,
|
|
boolean cellHasFocus
|
|
) {
|
|
textLabel.setText(name);
|
|
|
|
Photo photo = model.getPhotos().get(index);
|
|
ImageIcon thumbnail = thumbnailCache.computeIfAbsent(photo.filePath(), this::loadThumbnail);
|
|
// ImageIcon thumbnail = loadThumbnail(photo.filePath());
|
|
imageLabel.setIcon(thumbnail);
|
|
|
|
// Format date
|
|
String date = String.format("%tF", photo.dateAdded());
|
|
// Format file size
|
|
String size = formatFileSize(photo.fileSize());
|
|
detailsLabel.setText(date + " • " + size);
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Formats a file size in bytes to a human-readable string.
|
|
* <p>
|
|
* Converts the size to kilobytes or megabytes with one decimal place.
|
|
*
|
|
* @param size the file size in bytes
|
|
* @return a formatted string with the size in KB or MB
|
|
*/
|
|
private String formatFileSize(long size) {
|
|
if (size < 1024 * 1024) {
|
|
return String.format("%.1f KB", size / 1024.0);
|
|
} else {
|
|
return String.format("%.1f MB", size / (1024.0 * 1024));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads and scales a thumbnail image from the given path.
|
|
* <p>
|
|
* Creates a scaled version of the image suitable for the list display.
|
|
*
|
|
* @param path the file path of the image to load
|
|
* @return a scaled ImageIcon, or null if loading fails
|
|
*/
|
|
private ImageIcon loadThumbnail(String path) {
|
|
try {
|
|
ImageIcon icon = new ImageIcon(path);
|
|
Image img = icon.getImage();
|
|
Image scaledImg = img.getScaledInstance(50, 50, Image.SCALE_AREA_AVERAGING);
|
|
return new ImageIcon(scaledImg);
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
}
|