diff --git a/src/main/java/cz/kamma/jkeepass/KeepassApp.java b/src/main/java/cz/kamma/jkeepass/KeepassApp.java index 76592a8..096b8b8 100644 --- a/src/main/java/cz/kamma/jkeepass/KeepassApp.java +++ b/src/main/java/cz/kamma/jkeepass/KeepassApp.java @@ -84,6 +84,8 @@ public class KeepassApp extends JFrame { private static final int MAX_RECENT_FILES = 5; private File currentFile; private String currentPassword; + private boolean hasUnsavedChanges; + private final List changedItems = new ArrayList<>(); // Menu items that need state management private JMenuItem menuSave; @@ -117,9 +119,7 @@ public class KeepassApp extends JFrame { addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { - saveWindowState(); - saveComponentState(); - System.exit(0); + requestCloseApplication(); } }); @@ -182,7 +182,7 @@ public class KeepassApp extends JFrame { menuChangeMasterPassword.addActionListener(e -> changeMasterPassword()); minimizeItem.addActionListener(e -> minimizeToCustomTray()); menuLock.addActionListener(e -> lockDatabase()); - exitItem.addActionListener(e -> System.exit(0)); + exitItem.addActionListener(e -> requestCloseApplication()); fileMenu.add(openItem); fileMenu.add(recentMenu); @@ -464,7 +464,7 @@ public class KeepassApp extends JFrame { boolean hasEntrySelected = entryTable != null && entryTable.getSelectedRow() >= 0; // File menu items - menuSave.setEnabled(dbIsLoaded); + menuSave.setEnabled(dbIsLoaded && hasUnsavedChanges); menuChangeMasterPassword.setEnabled(dbIsLoaded); menuLock.setEnabled(dbIsLoaded); @@ -554,6 +554,7 @@ public class KeepassApp extends JFrame { this.database = CustomDatabaseFormat.convertJkpToDatabase(selectedFile, password); this.currentFile = selectedFile; this.currentPassword = password; + setUnsavedChanges(false); updateTree(this.database.getRootGroup()); saveRecentFile(selectedFile.getAbsolutePath()); statusLabel.setText(" Database loaded: " + selectedFile.getName()); @@ -584,21 +585,102 @@ public class KeepassApp extends JFrame { } } - private void saveDatabase() { + private boolean saveDatabase() { if (database == null || currentFile == null || currentPassword == null) { JOptionPane.showMessageDialog(this, "No database open to save.", "Warning", JOptionPane.WARNING_MESSAGE); - return; + return false; } try { // Save as custom JKP format CustomDatabaseFormat.saveDatabase(currentFile, database, currentPassword); + setUnsavedChanges(false); statusLabel.setText(" Database saved successfully."); + return true; } catch (Exception ex) { JOptionPane.showMessageDialog(this, "Error saving database: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); ex.printStackTrace(); + return false; } } + private void requestCloseApplication() { + if (!confirmSaveIfNeeded()) { + return; + } + + saveWindowState(); + saveComponentState(); + System.exit(0); + } + + private boolean confirmSaveIfNeeded() { + if (!hasUnsavedChanges) { + return true; + } + + JTextArea changedItemsArea = new JTextArea(buildChangedItemsText()); + changedItemsArea.setEditable(false); + changedItemsArea.setLineWrap(false); + changedItemsArea.setRows(8); + + JScrollPane changedItemsScroll = new JScrollPane(changedItemsArea); + changedItemsScroll.setPreferredSize(new Dimension(420, 160)); + changedItemsScroll.setBorder(BorderFactory.createTitledBorder("Changed items")); + + JPanel panel = new JPanel(new BorderLayout(0, 8)); + panel.add(new JLabel("Database has unsaved changes. Save before closing?"), BorderLayout.NORTH); + panel.add(changedItemsScroll, BorderLayout.CENTER); + + int choice = JOptionPane.showConfirmDialog( + this, + panel, + "Unsaved Changes", + JOptionPane.YES_NO_CANCEL_OPTION, + JOptionPane.WARNING_MESSAGE + ); + + if (choice == JOptionPane.YES_OPTION) { + return saveDatabase(); + } + + if (choice == JOptionPane.NO_OPTION) { + return true; + } + + return false; + } + + private void setUnsavedChanges(boolean hasUnsavedChanges) { + this.hasUnsavedChanges = hasUnsavedChanges; + if (!hasUnsavedChanges) { + changedItems.clear(); + } + updateMenuState(); + } + + private void markDataChanged(String description) { + this.hasUnsavedChanges = true; + changedItems.add(description); + updateMenuState(); + } + + private String buildChangedItemsText() { + if (changedItems.isEmpty()) { + return "No item-level details available."; + } + + StringBuilder sb = new StringBuilder(); + int maxItems = 25; + int limit = Math.min(changedItems.size(), maxItems); + for (int i = 0; i < limit; i++) { + sb.append(i + 1).append(". ").append(changedItems.get(i)).append('\n'); + } + if (changedItems.size() > maxItems) { + sb.append("... and ").append(changedItems.size() - maxItems).append(" more change(s)"); + } + return sb.toString(); + } + private void changeMasterPassword() { if (database == null || currentFile == null || currentPassword == null) { JOptionPane.showMessageDialog(this, "Database must be unlocked to change master password.", "Warning", JOptionPane.WARNING_MESSAGE); @@ -656,6 +738,7 @@ public class KeepassApp extends JFrame { try { CustomDatabaseFormat.saveDatabase(currentFile, database, newPassword); currentPassword = newPassword; + setUnsavedChanges(false); statusLabel.setText(" Master password changed successfully."); } catch (Exception ex) { JOptionPane.showMessageDialog(this, "Error changing master password: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); @@ -697,6 +780,7 @@ public class KeepassApp extends JFrame { Entry entry = currentGroup.addEntry(database.newEntry()); updateEntryFromDialog(entry, dialog); displayEntriesInGroup(currentGroup); + markDataChanged("Entry added: " + safeItemName(entry.getTitle())); statusLabel.setText(" Entry added. Remember to Save."); updateMenuState(); } @@ -709,8 +793,10 @@ public class KeepassApp extends JFrame { Entry entry = currentEntries.get(modelRow); EntryDialog dialog = new EntryDialog(this, "Edit Entry", entry); if (dialog.showDialog()) { + String oldTitle = entry.getTitle(); updateEntryFromDialog(entry, dialog); displayEntriesInGroup(currentGroup); + markDataChanged("Entry edited: " + safeItemName(oldTitle) + " -> " + safeItemName(entry.getTitle())); statusLabel.setText(" Entry updated. Remember to Save."); updateMenuState(); } @@ -726,8 +812,10 @@ public class KeepassApp extends JFrame { if (confirm == JOptionPane.YES_OPTION) { int modelRow = entryTable.convertRowIndexToModel(selectedRow); Entry entry = currentEntries.get(modelRow); + String entryTitle = entry.getTitle(); currentGroup.removeEntry(entry); displayEntriesInGroup(currentGroup); + markDataChanged("Entry deleted: " + safeItemName(entryTitle)); statusLabel.setText(" Entry deleted. Remember to Save."); updateMenuState(); } @@ -749,6 +837,7 @@ public class KeepassApp extends JFrame { if (name != null && !name.trim().isEmpty()) { parentGroup.addGroup(database.newGroup(name.trim())); updateTree(database.getRootGroup()); + markDataChanged("Category added: " + safeItemName(name.trim())); statusLabel.setText(" Category added. Remember to Save."); updateMenuState(); } @@ -785,11 +874,19 @@ public class KeepassApp extends JFrame { Group parentGroup = ((GroupWrapper) parentNode.getUserObject()).getGroup(); parentGroup.removeGroup(group); updateTree(database.getRootGroup()); + markDataChanged("Category deleted: " + safeItemName(group.getName())); statusLabel.setText(" Category deleted. Remember to Save."); updateMenuState(); } } + private String safeItemName(String value) { + if (value == null || value.trim().isEmpty()) { + return "(unnamed)"; + } + return value.trim(); + } + private void displayEntriesInGroup(Group group) { this.currentGroup = group; tableModel.setRowCount(0);