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. *

* 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. *

* The view provides user interface components for: *

* * @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 photoList; private DefaultListModel listModel; private JLabel currentPhotoLabel; private JButton addButton; private JButton deleteButton; private JButton previousButton; private JButton nextButton; private JComboBox sortingCombo; public PhotoAlbumView() { initializeComponents(); setupLayout(); } /** * Initializes all UI components of the photo album view. *

* 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. *

* Arranges the UI components using BorderLayout with: *

    *
  • Photo list in the WEST *
  • Current photo display in the CENTER *
  • Control buttons in the SOUTH *
*/ 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. *

* 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. *

* Clears the current list model and populates it with * names of photos from the model. */ private void updatePhotoList() { listModel.clear(); List photos = model.getPhotos(); for (Photo photo : photos) { listModel.addElement(photo.name()); } } /** * Updates the display of the current photo in the main view area. *

* 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. *

* 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. *

* 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. *

* This inner class renders each photo in the list with: *

    *
  • A thumbnail image *
  • The photo name *
  • Additional details (date and file size) *
*

* It also implements thumbnail caching to improve performance. */ private class PhotoListCellRenderer extends JPanel implements ListCellRenderer { private final JLabel imageLabel = new JLabel(); private final JLabel textLabel = new JLabel(); private final JLabel detailsLabel = new JLabel(); private final Map 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 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. *

* 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. *

* 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; } } } }