added database dirty state monitor

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Radek Davidek 2026-04-29 19:18:15 +02:00
parent 2669fb00f1
commit 2e4e0d3f77

View File

@ -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<String> 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);