From 9f4e3813a0a5a5cadcc8ff9a3a432ca2e13495a6 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Wed, 19 Apr 2023 17:31:49 +0200 Subject: [PATCH] More updates. --- .../running_every_day/RecorderApp.java | 16 +++- .../running_every_day/data/DataSource.java | 3 + .../running_every_day/data/RunRecord.java | 7 ++ .../data/RunRecordRepository.java | 2 +- .../view/RunRecordRowSorter.java | 78 +++++++++++++++++++ .../view/RunRecordTableModel.java | 74 ++++++++++++++++-- .../view/RunRecordsPanel.java | 78 +++++++++---------- .../view/WindowDataSourceCloser.java | 27 +++++++ 8 files changed, 233 insertions(+), 52 deletions(-) create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordRowSorter.java create mode 100644 recorder/src/main/java/com/github/andrewlalis/running_every_day/view/WindowDataSourceCloser.java diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/RecorderApp.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/RecorderApp.java index 4ec630b..ff68649 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/RecorderApp.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/RecorderApp.java @@ -3,17 +3,25 @@ package com.github.andrewlalis.running_every_day; import com.formdev.flatlaf.FlatLightLaf; import com.github.andrewlalis.running_every_day.data.DataSource; import com.github.andrewlalis.running_every_day.view.RecorderAppWindow; +import com.github.andrewlalis.running_every_day.view.WindowDataSourceCloser; +import javax.swing.*; import java.sql.SQLException; /** * The main application entrypoint. */ public class RecorderApp { - public static void main(String[] args) throws SQLException { - DataSource dataSource = new DataSource("jdbc:sqlite:runs.db"); + public static void main(String[] args) { FlatLightLaf.setup(); - var window = new RecorderAppWindow(dataSource); - window.setVisible(true); + try { + DataSource dataSource = new DataSource("jdbc:sqlite:runs.db"); + var window = new RecorderAppWindow(dataSource); + window.addWindowListener(new WindowDataSourceCloser(dataSource)); + window.setVisible(true); + } catch (SQLException e) { + JOptionPane.showMessageDialog(null, "Failed to open database: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); + e.printStackTrace(); + } } } diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DataSource.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DataSource.java index a060b7a..fbc111c 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DataSource.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/DataSource.java @@ -9,6 +9,9 @@ import java.sql.SQLException; import java.util.ArrayList; import java.util.List; +/** + * A single object that serves as the application's data source. + */ public class DataSource { private final Connection conn; diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/RunRecord.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/RunRecord.java index d3422e8..0cecfed 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/RunRecord.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/RunRecord.java @@ -35,6 +35,13 @@ public record RunRecord( return (int) duration.getSeconds(); } + public String durationFormatted() { + if (duration.toHoursPart() > 0) { + return String.format("%d:%02d:%02d", duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart()); + } + return String.format("%02d:%02d", duration.toMinutesPart(), duration.toSecondsPart()); + } + public BigDecimal weightGrams() { return weightKg.multiply(BigDecimal.valueOf(1000)); } diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/RunRecordRepository.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/RunRecordRepository.java index d74d2df..8c6e94a 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/RunRecordRepository.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/data/RunRecordRepository.java @@ -8,7 +8,7 @@ import java.util.List; public record RunRecordRepository(Connection conn) { public Page findAll(Pagination pagination) throws SQLException { - String query = "SELECT id, date, start_time, distance, duration, weight, comment FROM run"; + String query = "SELECT * FROM run ORDER BY date ASC"; return pagination.execute(conn, query, new RunRecord.Mapper()); } diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordRowSorter.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordRowSorter.java new file mode 100644 index 0000000..7b925b5 --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordRowSorter.java @@ -0,0 +1,78 @@ +package com.github.andrewlalis.running_every_day.view; + +import com.github.andrewlalis.running_every_day.data.RunRecord; + +import javax.swing.*; +import java.util.List; + +public class RunRecordRowSorter extends RowSorter { + @Override + public RunRecord getModel() { + return null; + } + + @Override + public void toggleSortOrder(int column) { + + } + + @Override + public int convertRowIndexToModel(int index) { + return 0; + } + + @Override + public int convertRowIndexToView(int index) { + return 0; + } + + @Override + public void setSortKeys(List keys) { + + } + + @Override + public List getSortKeys() { + return null; + } + + @Override + public int getViewRowCount() { + return 0; + } + + @Override + public int getModelRowCount() { + return 0; + } + + @Override + public void modelStructureChanged() { + + } + + @Override + public void allRowsChanged() { + + } + + @Override + public void rowsInserted(int firstRow, int endRow) { + + } + + @Override + public void rowsDeleted(int firstRow, int endRow) { + + } + + @Override + public void rowsUpdated(int firstRow, int endRow) { + + } + + @Override + public void rowsUpdated(int firstRow, int endRow, int column) { + + } +} diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordTableModel.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordTableModel.java index c851a3f..078be60 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordTableModel.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordTableModel.java @@ -1,22 +1,84 @@ package com.github.andrewlalis.running_every_day.view; -import com.github.andrewlalis.running_every_day.data.Page; +import com.github.andrewlalis.running_every_day.data.DataSource; +import com.github.andrewlalis.running_every_day.data.Pagination; import com.github.andrewlalis.running_every_day.data.RunRecord; import javax.swing.table.AbstractTableModel; +import java.sql.SQLException; import java.util.ArrayList; import java.util.List; public class RunRecordTableModel extends AbstractTableModel { + private final DataSource dataSource; private List records; + private int cachedPageCount; + private Pagination currentPage; - public RunRecordTableModel() { + public RunRecordTableModel(DataSource dataSource) { + this.dataSource = dataSource; + this.cachedPageCount = 0; this.records = new ArrayList<>(0); + this.currentPage = new Pagination(0, 20); } - public void setPage(Page page) { - this.records = page.items(); - this.fireTableDataChanged(); + public void setPageSize(int size) { + this.currentPage = new Pagination(0, size, currentPage.sorts()); + loadPage(); + } + + public void firstPage() { + this.currentPage = new Pagination(0, currentPage.size(), currentPage.sorts()); + loadPage(); + } + + public boolean canGoToFirstPage() { + return currentPage.page() != 0; + } + + public void lastPage() { + int pageIdx = cachedPageCount > 0 ? cachedPageCount - 1 : 0; + this.currentPage = new Pagination(pageIdx, currentPage.size(), currentPage.sorts()); + loadPage(); + } + + public boolean canGoToLastPage() { + return currentPage.page() < cachedPageCount - 1; + } + + public void nextPage() { + this.currentPage = currentPage.nextPage(); + loadPage(); + } + + public boolean canGoToNextPage() { + return currentPage.page() < cachedPageCount - 1; + } + + public void previousPage() { + this.currentPage = currentPage.previousPage(); + loadPage(); + } + + public boolean canGoToPreviousPage() { + return currentPage.page() > 0; + } + + public Pagination getPagination() { + return currentPage; + } + + private void loadPage() { + try { + var page = dataSource.runRecords().findAll(currentPage); + cachedPageCount = (int) dataSource.runRecords().pageCount(currentPage.size()); + records = page.items(); + this.fireTableDataChanged(); + } catch (SQLException e) { + e.printStackTrace(); + records = new ArrayList<>(); + currentPage = new Pagination(0, 20); + } } @Override @@ -38,7 +100,7 @@ public class RunRecordTableModel extends AbstractTableModel { case 1 -> r.date().toString(); case 2 -> r.startTime().toString(); case 3 -> r.distanceKm().toPlainString(); - case 4 -> r.duration().toString(); + case 4 -> r.durationFormatted(); case 5 -> r.weightKg().toPlainString(); case 6 -> r.comment(); default -> null; diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java index 9c79284..07297ca 100644 --- a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/RunRecordsPanel.java @@ -1,81 +1,76 @@ package com.github.andrewlalis.running_every_day.view; import com.github.andrewlalis.running_every_day.data.DataSource; -import com.github.andrewlalis.running_every_day.data.Pagination; import javax.swing.*; import java.awt.*; -import java.sql.SQLException; /** * A panel for displaying a table view of run records, and various controls for * navigating the data. */ public class RunRecordsPanel extends JPanel { - private final DataSource dataSource; - private final RunRecordTableModel tableModel = new RunRecordTableModel(); + private final RunRecordTableModel tableModel; private final JTextField currentPageField; - private final JComboBox pageSizeSelector; private final JButton firstPageButton; private final JButton previousPageButton; private final JButton nextPageButton; private final JButton lastPageButton; - private Pagination currentPage; public RunRecordsPanel(DataSource dataSource) { super(new BorderLayout()); - this.dataSource = dataSource; - this.currentPage = new Pagination(0, 20); + this.tableModel = new RunRecordTableModel(dataSource); var table = new JTable(tableModel); + table.getTableHeader().setReorderingAllowed(false); + table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); + table.getColumnModel().getColumn(0).setMaxWidth(40); + table.getColumnModel().getColumn(1).setMaxWidth(80); + table.getColumnModel().getColumn(2).setMaxWidth(80); + table.getColumnModel().getColumn(3).setMaxWidth(100); + table.getColumnModel().getColumn(4).setMaxWidth(80); + table.getColumnModel().getColumn(5).setMaxWidth(80); var scrollPane = new JScrollPane(table, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); this.add(scrollPane, BorderLayout.CENTER); JPanel paginationPanel = new JPanel(new GridLayout(1, 6)); firstPageButton = new JButton("First Page"); firstPageButton.addActionListener(e -> { - currentPage = new Pagination(0, currentPage.size(), currentPage.sorts()); - loadPage(); + tableModel.firstPage(); + updateButtonStates(); }); previousPageButton = new JButton("Previous Page"); previousPageButton.addActionListener(e -> { - currentPage = currentPage.previousPage(); - loadPage(); + tableModel.previousPage(); + updateButtonStates(); }); nextPageButton = new JButton("Next Page"); nextPageButton.addActionListener(e -> { - currentPage = currentPage.nextPage(); - loadPage(); + tableModel.nextPage(); + updateButtonStates(); }); lastPageButton = new JButton("Last Page"); lastPageButton.addActionListener(e -> { - try { - long pageCount = dataSource.runRecords().pageCount(currentPage.size()); - if (pageCount <= 0) pageCount = 1; - currentPage = new Pagination((int) (pageCount - 1), currentPage.size(), currentPage.sorts()); - loadPage(); - } catch (SQLException ex) { - ex.printStackTrace(); - } + tableModel.lastPage(); + updateButtonStates(); }); JPanel currentPagePanel = new JPanel(); currentPagePanel.add(new JLabel("Current Page: ")); this.currentPageField = new JTextField("1", 3); - // TODO: Add document change listener. currentPagePanel.add(this.currentPageField); JPanel pageSizePanel = new JPanel(); pageSizePanel.add(new JLabel("Page Size: ")); - pageSizeSelector = new JComboBox<>(new Integer[]{5, 10, 20, 50, 100, 500}); - pageSizeSelector.setSelectedItem(this.currentPage.size()); + JComboBox pageSizeSelector = new JComboBox<>(new Integer[]{5, 10, 20, 50, 100, 500}); + pageSizeSelector.setSelectedItem(tableModel.getPagination().size()); pageSizeSelector.addItemListener(e -> { - currentPage = new Pagination(0, (Integer) e.getItem(), currentPage.sorts()); - loadPage(); + int size = (int) e.getItem(); + tableModel.setPageSize(size); + updateButtonStates(); }); pageSizePanel.add(pageSizeSelector); - paginationPanel.add(firstPageButton); paginationPanel.add(previousPageButton); paginationPanel.add(currentPagePanel); @@ -84,21 +79,22 @@ public class RunRecordsPanel extends JPanel { paginationPanel.add(lastPageButton); this.add(paginationPanel, BorderLayout.SOUTH); + SwingUtilities.invokeLater(() -> { + tableModel.firstPage(); + updateButtonStates(); + }); - SwingUtilities.invokeLater(this::loadPage); + JPanel actionsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JButton addActionButton = new JButton("Add a Record"); + actionsPanel.add(addActionButton); + this.add(actionsPanel, BorderLayout.NORTH); } - private void loadPage() { - try { - this.tableModel.setPage(dataSource.runRecords().findAll(this.currentPage)); - long pageCount = dataSource.runRecords().pageCount(this.currentPage.size()); - this.firstPageButton.setEnabled(this.currentPage.page() != 0); - this.previousPageButton.setEnabled(this.currentPage.page() > 0); - this.nextPageButton.setEnabled(this.currentPage.page() < pageCount - 1); - this.lastPageButton.setEnabled(this.currentPage.page() != pageCount - 1); - this.currentPageField.setText(String.valueOf(this.currentPage.page() + 1)); - } catch (SQLException e) { - e.printStackTrace(); - } + private void updateButtonStates() { + firstPageButton.setEnabled(tableModel.canGoToFirstPage()); + nextPageButton.setEnabled(tableModel.canGoToNextPage()); + previousPageButton.setEnabled(tableModel.canGoToPreviousPage()); + lastPageButton.setEnabled(tableModel.canGoToLastPage()); + currentPageField.setText(String.valueOf(tableModel.getPagination().page() + 1)); } } diff --git a/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/WindowDataSourceCloser.java b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/WindowDataSourceCloser.java new file mode 100644 index 0000000..002a57f --- /dev/null +++ b/recorder/src/main/java/com/github/andrewlalis/running_every_day/view/WindowDataSourceCloser.java @@ -0,0 +1,27 @@ +package com.github.andrewlalis.running_every_day.view; + +import com.github.andrewlalis.running_every_day.data.DataSource; + +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.sql.SQLException; + +/** + * Simple listener that closes the given datasource when the window closes. + */ +public class WindowDataSourceCloser extends WindowAdapter { + private final DataSource dataSource; + + public WindowDataSourceCloser(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public void windowClosing(WindowEvent e) { + try { + dataSource.close(); + } catch (SQLException ex) { + ex.printStackTrace(); + } + } +}