From 2e2f4bd4f5998c944b93bd1561f887e7328eb002 Mon Sep 17 00:00:00 2001 From: rdavidek Date: Wed, 14 Jan 2026 21:42:56 +0100 Subject: [PATCH] file attributes added --- .../java/com/kfmanager/ui/FilePanelTab.java | 11 +- .../com/kfmanager/ui/PropertiesDialog.java | 350 ++++++++++++++++++ .../java/com/kfmanager/ui/SettingsDialog.java | 76 +++- 3 files changed, 417 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/kfmanager/ui/PropertiesDialog.java diff --git a/src/main/java/com/kfmanager/ui/FilePanelTab.java b/src/main/java/com/kfmanager/ui/FilePanelTab.java index fbca050..7932c2a 100644 --- a/src/main/java/com/kfmanager/ui/FilePanelTab.java +++ b/src/main/java/com/kfmanager/ui/FilePanelTab.java @@ -972,11 +972,14 @@ public class FilePanelTab extends JPanel { JMenuItem props = new JMenuItem("Properties"); props.addActionListener(ae -> { try { + Window parent = SwingUtilities.getWindowAncestor(FilePanelTab.this); File f = item.getFile(); - String info = String.format("Name: %s\nPath: %s\nSize: %s\nModified: %s\nReadable: %b, Writable: %b, Executable: %b", - f.getName(), f.getAbsolutePath(), item.isDirectory() ? "" : formatSize(f.length()), - new java.util.Date(f.lastModified()).toString(), f.canRead(), f.canWrite(), f.canExecute()); - JOptionPane.showMessageDialog(FilePanelTab.this, info, "Properties", JOptionPane.INFORMATION_MESSAGE); + if (f != null) { + PropertiesDialog dialog = new PropertiesDialog(parent, f); + dialog.setVisible(true); + // Refresh current directory after potential attribute changes + loadDirectory(getCurrentDirectory()); + } } catch (Exception ex) { try { JOptionPane.showMessageDialog(FilePanelTab.this, "Cannot show properties: " + ex.getMessage()); } catch (Exception ignore) {} } diff --git a/src/main/java/com/kfmanager/ui/PropertiesDialog.java b/src/main/java/com/kfmanager/ui/PropertiesDialog.java new file mode 100644 index 0000000..738cb7b --- /dev/null +++ b/src/main/java/com/kfmanager/ui/PropertiesDialog.java @@ -0,0 +1,350 @@ +package com.kfmanager.ui; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.io.File; +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.*; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +public class PropertiesDialog extends JDialog { + private final File file; + private final Path path; + + // Attributes Tab components + private JCheckBox[][] permChecks; // [Owner, Group, Other][Read, Write, Execute] + private JTextField octalField; + private JTextField symbolicField; + private JCheckBox suidCheck, sgidCheck, stickyCheck; + private JCheckBox recursiveCheck; + private JTextField ownerField, groupField; + + public PropertiesDialog(Window owner, File file) { + super(owner, "Properties", ModalityType.APPLICATION_MODAL); + this.file = file; + this.path = file.toPath(); + + initComponents(); + + pack(); + setMinimumSize(new Dimension(500, 550)); + setLocationRelativeTo(owner); + } + + private void initComponents() { + setLayout(new BorderLayout()); + + JTabbedPane tabbedPane = new JTabbedPane(); + tabbedPane.addTab("Properties", buildPropertiesPanel()); + tabbedPane.addTab("Attributes", buildAttributesPanel()); + + add(tabbedPane, BorderLayout.CENTER); + + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + JButton okButton = new JButton("OK"); + JButton cancelButton = new JButton("Cancel"); + + okButton.addActionListener(e -> { + if (applyChanges()) { + dispose(); + } + }); + cancelButton.addActionListener(e -> dispose()); + + buttonPanel.add(okButton); + buttonPanel.add(cancelButton); + add(buttonPanel, BorderLayout.SOUTH); + } + + private JPanel buildPropertiesPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(new EmptyBorder(20, 20, 20, 20)); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(5, 5, 5, 5); + + int row = 0; + addInfoRow(panel, gbc, row++, "Name:", file.getName()); + addInfoRow(panel, gbc, row++, "Path:", file.getAbsolutePath()); + + String sizeStr = file.isDirectory() ? "Directory" : formatSize(file.length()); + addInfoRow(panel, gbc, row++, "Size:", sizeStr); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + addInfoRow(panel, gbc, row++, "Modified:", sdf.format(new Date(file.lastModified()))); + + return panel; + } + + private JPanel buildAttributesPanel() { + JPanel panel = new JPanel(new GridBagLayout()); + panel.setBorder(new EmptyBorder(15, 15, 15, 15)); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(5, 5, 5, 5); + + int row = 0; + + // File name display + JLabel nameLabel = new JLabel("File name"); + gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0; + panel.add(nameLabel, gbc); + + JLabel fileNameValue = new JLabel(file.getName()); + gbc.gridx = 1; gbc.gridy = row; gbc.weightx = 1; gbc.gridwidth = 3; + panel.add(fileNameValue, gbc); + gbc.gridwidth = 1; + row++; + + panel.add(new JSeparator(), new GridBagConstraints(0, row++, 4, 1, 1.0, 0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(10, 0, 10, 0), 0, 0)); + + // Header: Read, Write, Execute + gbc.gridy = row; + gbc.weightx = 0; + String[] titles = {"", "Read", "Write", "Execute"}; + for (int i = 1; i < titles.length; i++) { + gbc.gridx = i; + panel.add(new JLabel(titles[i], SwingConstants.CENTER), gbc); + } + row++; + + // Rows: Owner, Group, Other + String[] labels = {"Owner", "Group", "Other"}; + permChecks = new JCheckBox[3][3]; + + Set perms = new HashSet<>(); + String ownerName = ""; + String groupName = ""; + try { + PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class); + perms = attrs.permissions(); + ownerName = attrs.owner().getName(); + groupName = attrs.group().getName(); + } catch (Exception ignore) { + // Fallback for non-posix systems or permission issues + if (file.canRead()) perms.add(PosixFilePermission.OWNER_READ); + if (file.canWrite()) perms.add(PosixFilePermission.OWNER_WRITE); + if (file.canExecute()) perms.add(PosixFilePermission.OWNER_EXECUTE); + } + + for (int i = 0; i < 3; i++) { + gbc.gridx = 0; gbc.gridy = row; + panel.add(new JLabel(labels[i]), gbc); + + for (int j = 0; j < 3; j++) { + permChecks[i][j] = new JCheckBox(); + permChecks[i][j].setHorizontalAlignment(SwingConstants.CENTER); + gbc.gridx = j + 1; + panel.add(permChecks[i][j], gbc); + + // Set initial state + permChecks[i][j].setSelected(hasPermission(perms, i, j)); + permChecks[i][j].addActionListener(e -> updateOctalAndSymbolic()); + } + row++; + } + + panel.add(new JSeparator(), new GridBagConstraints(0, row++, 4, 1, 1.0, 0, GridBagConstraints.CENTER, GridBagConstraints.HORIZONTAL, new Insets(10, 0, 10, 0), 0, 0)); + + // Special Bits + JPanel bitsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 0)); + bitsPanel.add(new JLabel("Bits:")); + suidCheck = new JCheckBox("SUID"); + sgidCheck = new JCheckBox("SGID"); + stickyCheck = new JCheckBox("Sticky"); + suidCheck.setEnabled(false); // Advanced bits usually require more than just Java basic API or are rare + sgidCheck.setEnabled(false); + stickyCheck.setEnabled(false); + bitsPanel.add(suidCheck); + bitsPanel.add(sgidCheck); + bitsPanel.add(stickyCheck); + + gbc.gridx = 0; gbc.gridy = row++; gbc.gridwidth = 4; + panel.add(bitsPanel, gbc); + gbc.gridwidth = 1; + + // Octal and Symbolic + gbc.gridy = row; + gbc.gridx = 0; + panel.add(new JLabel("Octal:"), gbc); + + octalField = new JTextField(5); + gbc.gridx = 1; + panel.add(octalField, gbc); + + gbc.gridx = 2; + panel.add(new JLabel("Text:"), gbc); + + symbolicField = new JTextField(12); + symbolicField.setEditable(false); + gbc.gridx = 3; + panel.add(symbolicField, gbc); + row++; + + // Owner/Group Section + JPanel ownerGroupPanel = new JPanel(new GridBagLayout()); + ownerGroupPanel.setBorder(BorderFactory.createTitledBorder("Owner")); + GridBagConstraints ogbc = new GridBagConstraints(); + ogbc.fill = GridBagConstraints.HORIZONTAL; + ogbc.insets = new Insets(2, 5, 2, 5); + + ogbc.gridx = 0; ogbc.gridy = 0; + ownerGroupPanel.add(new JLabel("Owner"), ogbc); + ownerField = new JTextField(ownerName); + ownerField.setEditable(false); + ogbc.gridx = 1; ogbc.weightx = 1.0; + ownerGroupPanel.add(ownerField, ogbc); + + ogbc.gridx = 0; ogbc.gridy = 1; + ownerGroupPanel.add(new JLabel("Group"), ogbc); + groupField = new JTextField(groupName); + groupField.setEditable(false); + ogbc.gridx = 1; + ownerGroupPanel.add(groupField, ogbc); + + gbc.gridx = 0; gbc.gridy = row++; gbc.gridwidth = 4; + panel.add(ownerGroupPanel, gbc); + gbc.gridwidth = 1; + + // Recursive + recursiveCheck = new JCheckBox("Recursive"); + recursiveCheck.setEnabled(file.isDirectory()); + gbc.gridx = 0; gbc.gridy = row++; gbc.gridwidth = 4; + panel.add(recursiveCheck, gbc); + + updateOctalAndSymbolic(); + + // Listener for octal field to update checkboxes + octalField.addActionListener(e -> updateFromOctal()); + + return panel; + } + + private boolean hasPermission(Set perms, int userIdx, int permIdx) { + PosixFilePermission p = getPermission(userIdx, permIdx); + return p != null && perms.contains(p); + } + + private PosixFilePermission getPermission(int userIdx, int permIdx) { + if (userIdx == 0) { // Owner + if (permIdx == 0) return PosixFilePermission.OWNER_READ; + if (permIdx == 1) return PosixFilePermission.OWNER_WRITE; + if (permIdx == 2) return PosixFilePermission.OWNER_EXECUTE; + } else if (userIdx == 1) { // Group + if (permIdx == 0) return PosixFilePermission.GROUP_READ; + if (permIdx == 1) return PosixFilePermission.GROUP_WRITE; + if (permIdx == 2) return PosixFilePermission.GROUP_EXECUTE; + } else if (userIdx == 2) { // Other + if (permIdx == 0) return PosixFilePermission.OTHERS_READ; + if (permIdx == 1) return PosixFilePermission.OTHERS_WRITE; + if (permIdx == 2) return PosixFilePermission.OTHERS_EXECUTE; + } + return null; + } + + private void updateOctalAndSymbolic() { + int octal = 0; + if (permChecks[0][0].isSelected()) octal += 0400; + if (permChecks[0][1].isSelected()) octal += 0200; + if (permChecks[0][2].isSelected()) octal += 0100; + if (permChecks[1][0].isSelected()) octal += 0040; + if (permChecks[1][1].isSelected()) octal += 0020; + if (permChecks[1][2].isSelected()) octal += 0010; + if (permChecks[2][0].isSelected()) octal += 0004; + if (permChecks[2][1].isSelected()) octal += 0002; + if (permChecks[2][2].isSelected()) octal += 0001; + + octalField.setText(String.format("%03o", octal)); + + StringBuilder sb = new StringBuilder(); + sb.append(file.isDirectory() ? 'd' : '-'); + sb.append(permChecks[0][0].isSelected() ? 'r' : '-'); + sb.append(permChecks[0][1].isSelected() ? 'w' : '-'); + sb.append(permChecks[0][2].isSelected() ? 'x' : '-'); + sb.append(permChecks[1][0].isSelected() ? 'r' : '-'); + sb.append(permChecks[1][1].isSelected() ? 'w' : '-'); + sb.append(permChecks[1][2].isSelected() ? 'x' : '-'); + sb.append(permChecks[2][0].isSelected() ? 'r' : '-'); + sb.append(permChecks[2][1].isSelected() ? 'w' : '-'); + sb.append(permChecks[2][2].isSelected() ? 'x' : '-'); + symbolicField.setText(sb.toString()); + } + + private void updateFromOctal() { + try { + int val = Integer.parseInt(octalField.getText(), 8); + permChecks[0][0].setSelected((val & 0400) != 0); + permChecks[0][1].setSelected((val & 0200) != 0); + permChecks[0][2].setSelected((val & 0100) != 0); + permChecks[1][0].setSelected((val & 0040) != 0); + permChecks[1][1].setSelected((val & 0020) != 0); + permChecks[1][2].setSelected((val & 0010) != 0); + permChecks[2][0].setSelected((val & 0004) != 0); + permChecks[2][1].setSelected((val & 0002) != 0); + permChecks[2][2].setSelected((val & 0001) != 0); + updateOctalAndSymbolic(); + } catch (Exception ignore) {} + } + + private boolean applyChanges() { + Set perms = new HashSet<>(); + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + if (permChecks[i][j].isSelected()) { + perms.add(getPermission(i, j)); + } + } + } + + try { + applyPermissions(path, perms, recursiveCheck.isSelected()); + return true; + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, "Failed to apply permissions: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); + return false; + } + } + + private void applyPermissions(Path p, Set perms, boolean recursive) throws IOException { + try { + Files.setPosixFilePermissions(p, perms); + } catch (UnsupportedOperationException e) { + // Fallback for non-posix filesystems + File f = p.toFile(); + f.setReadable(perms.contains(PosixFilePermission.OWNER_READ)); + f.setWritable(perms.contains(PosixFilePermission.OWNER_WRITE)); + f.setExecutable(perms.contains(PosixFilePermission.OWNER_EXECUTE)); + } + + if (recursive && Files.isDirectory(p)) { + try (DirectoryStream stream = Files.newDirectoryStream(p)) { + for (Path entry : stream) { + applyPermissions(entry, perms, true); + } + } + } + } + + private void addInfoRow(JPanel panel, GridBagConstraints gbc, int row, String label, String value) { + gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0; + panel.add(new JLabel(label), gbc); + gbc.gridx = 1; gbc.weightx = 1.0; + JTextField textField = new JTextField(value); + textField.setEditable(false); + textField.setBorder(null); + textField.setOpaque(false); + panel.add(textField, gbc); + } + + private String formatSize(long bytes) { + if (bytes < 1024) return bytes + " B"; + int exp = (int) (Math.log(bytes) / Math.log(1024)); + char pre = "KMGTPE".charAt(exp - 1); + return String.format("%.1f %cB (%d bytes)", bytes / Math.pow(1024, exp), pre, bytes); + } +} diff --git a/src/main/java/com/kfmanager/ui/SettingsDialog.java b/src/main/java/com/kfmanager/ui/SettingsDialog.java index 1a40e2b..762e6de 100644 --- a/src/main/java/com/kfmanager/ui/SettingsDialog.java +++ b/src/main/java/com/kfmanager/ui/SettingsDialog.java @@ -178,10 +178,20 @@ public class SettingsDialog extends JDialog { private JPanel buildAppearancePanel() { JPanel p = new JPanel(new BorderLayout(8, 8)); - JPanel grid = new JPanel(new GridLayout(4, 2, 8, 8)); + JPanel grid = new JPanel(new GridBagLayout()); grid.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); + + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(4, 4, 4, 4); + gbc.weightx = 1.0; + + int row = 0; - grid.add(new JLabel("Application font:")); + // Application font + gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0; + grid.add(new JLabel("Application font:"), gbc); + appearanceFontBtn = new JButton(getFontDescription(config.getGlobalFont())); appearanceFontBtn.addActionListener(e -> { Font nf = FontChooserDialog.showDialog(this, config.getGlobalFont()); @@ -191,37 +201,54 @@ public class SettingsDialog extends JDialog { if (onChange != null) onChange.run(); } }); - grid.add(appearanceFontBtn); + gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0; + grid.add(appearanceFontBtn, gbc); - grid.add(new JLabel("Background color:")); + // Background color + gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0; + grid.add(new JLabel("Background color:"), gbc); + appearanceBgBtn = createColorButton(config.getBackgroundColor(), "Choose background color", c -> { config.setBackgroundColor(c); if (onChange != null) onChange.run(); }); - grid.add(appearanceBgBtn); + gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0; + grid.add(appearanceBgBtn, gbc); - grid.add(new JLabel("Selection color:")); + // Selection color + gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0; + grid.add(new JLabel("Selection color:"), gbc); + appearanceSelBtn = createColorButton(config.getSelectionColor() != null ? config.getSelectionColor() : new Color(184, 207, 229), "Choose selection color", c -> { config.setSelectionColor(c); if (onChange != null) onChange.run(); }); - grid.add(appearanceSelBtn); + gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0; + grid.add(appearanceSelBtn, gbc); - grid.add(new JLabel("Marked item color:")); + // Marked item color + gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0; + grid.add(new JLabel("Marked item color:"), gbc); + appearanceMarkBtn = createColorButton(config.getMarkedColor() != null ? config.getMarkedColor() : new Color(204, 153, 0), "Choose marked item color", c -> { config.setMarkedColor(c); if (onChange != null) onChange.run(); }); - grid.add(appearanceMarkBtn); + gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0; + grid.add(appearanceMarkBtn, gbc); - grid.add(new JLabel("Folder icon color:")); + // Folder icon color + gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0; + grid.add(new JLabel("Folder icon color:"), gbc); + appearanceFolderBtn = createColorButton(config.getFolderColor(), "Choose folder icon color", c -> { config.setFolderColor(c); if (onChange != null) onChange.run(); }); - grid.add(appearanceFolderBtn); + gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0; + grid.add(appearanceFolderBtn, gbc); p.add(grid, BorderLayout.NORTH); panels.put("Appearance", p); @@ -262,10 +289,20 @@ public class SettingsDialog extends JDialog { private JPanel buildEditorPanel() { JPanel p = new JPanel(new BorderLayout(8, 8)); - JPanel grid = new JPanel(new GridLayout(2, 2, 8, 8)); + JPanel grid = new JPanel(new GridBagLayout()); grid.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); - grid.add(new JLabel("Editor font:")); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(4, 4, 4, 4); + gbc.weightx = 1.0; + + int row = 0; + + // Editor font + gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0; + grid.add(new JLabel("Editor font:"), gbc); + editorFontBtn = new JButton(getFontDescription(config.getEditorFont())); editorFontBtn.addActionListener(e -> { Font nf = FontChooserDialog.showDialog(this, config.getEditorFont()); @@ -275,9 +312,13 @@ public class SettingsDialog extends JDialog { if (onChange != null) onChange.run(); } }); - grid.add(editorFontBtn); + gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0; + grid.add(editorFontBtn, gbc); + // External editor path - grid.add(new JLabel("External editor:")); + gbc.gridx = 0; gbc.gridy = row; gbc.weightx = 0.0; + grid.add(new JLabel("External editor:"), gbc); + JPanel extPanel = new JPanel(new BorderLayout(6, 0)); externalEditorField = new JTextField(config.getExternalEditorPath()); JButton browse = new JButton("Browse..."); @@ -295,11 +336,14 @@ public class SettingsDialog extends JDialog { externalEditorField.setText(sel.getAbsolutePath()); config.setExternalEditorPath(sel.getAbsolutePath()); // config.saveConfig() will be called when OK is pressed + if (onChange != null) onChange.run(); } }); extPanel.add(externalEditorField, BorderLayout.CENTER); extPanel.add(browse, BorderLayout.EAST); - grid.add(extPanel); + + gbc.gridx = 1; gbc.gridy = row++; gbc.weightx = 1.0; + grid.add(extPanel, gbc); p.add(grid, BorderLayout.NORTH); panels.put("Editor", p);