UI fixes - context menu

This commit is contained in:
Radek Davidek 2026-01-22 15:21:11 +01:00
parent c790303438
commit 42ea4fec7a
2 changed files with 119 additions and 13 deletions

View File

@ -20,6 +20,7 @@ public class FilePanel extends JPanel {
private cz.kamma.kfmanager.config.AppConfig appConfig;
private Runnable onDirectoryChangedAll;
private boolean ignoreComboActions = false;
private boolean active = false;
public FilePanel(String initialPath) {
initComponents();
@ -263,6 +264,7 @@ public class FilePanel extends JPanel {
FilePanelTab tab = new FilePanelTab(path, requestFocus);
if (appConfig != null) tab.setAppConfig(appConfig);
tab.setActive(this.active);
// Set callback for updating tab title on directory change
tab.setOnDirectoryChanged(() -> {
@ -309,6 +311,7 @@ public class FilePanel extends JPanel {
public void addNewTabWithMode(String path, ViewMode mode, boolean requestFocus) {
FilePanelTab tab = new FilePanelTab(path, requestFocus);
if (appConfig != null) tab.setAppConfig(appConfig);
tab.setActive(this.active);
tab.setOnDirectoryChanged(() -> {
updateTabTitle(tab);
if (onDirectoryChangedAll != null) onDirectoryChangedAll.run();
@ -531,6 +534,7 @@ public class FilePanel extends JPanel {
* The indicator is placed between the drive dropdown and the tabs.
*/
public void setActive(boolean active, Color activeColor) {
this.active = active;
// Inactive indicator is subtle dark gray
Color inactiveColor = new Color(60, 60, 60);
Color targetColor = active ? activeColor : inactiveColor;
@ -538,6 +542,15 @@ public class FilePanel extends JPanel {
// Apply a matte border to the top of tabbedPane - this places it between
// the topPanel (dropdown) and the tabs themselves.
tabbedPane.setBorder(BorderFactory.createMatteBorder(3, 0, 0, 0, targetColor));
// Propagate active state to all tabs
for (int i = 0; i < tabbedPane.getTabCount(); i++) {
Component c = tabbedPane.getComponentAt(i);
if (c instanceof FilePanelTab) {
((FilePanelTab) c).setActive(active);
}
}
revalidate();
repaint();
}

View File

@ -68,6 +68,7 @@ public class FilePanelTab extends JPanel {
private Path currentArchiveTempDir = null;
private File currentArchiveSourceFile = null;
private boolean inlineRenameActive = false;
private boolean active = false;
public FilePanelTab(String initialPath) {
this(initialPath, true);
@ -241,6 +242,13 @@ public class FilePanelTab extends JPanel {
public void setOnSwitchPanelRequested(Runnable callback) {
this.onSwitchPanelRequested = callback;
}
public void setActive(boolean active) {
this.active = active;
if (fileTable != null) {
fileTable.repaint();
}
}
private void initComponents() {
setLayout(new BorderLayout());
@ -270,6 +278,7 @@ public class FilePanelTab extends JPanel {
// changing default mouse-driven selection behavior.
try {
if (e.isPopupTrigger()) {
fileTable.requestFocusInWindow();
int col = columnAtPoint(e.getPoint());
int row = rowAtPoint(e.getPoint());
FileItem item = null;
@ -282,14 +291,14 @@ public class FilePanelTab extends JPanel {
int selRow = index % tableModel.briefRowsPerColumn;
int selCol = index / tableModel.briefRowsPerColumn;
briefCurrentColumn = selCol;
// Select the logical row so actions apply to this item
fileTable.setRowSelectionInterval(selRow, selRow);
// Focus and Select the item
fileTable.changeSelection(selRow, selCol, false, false);
}
}
} else {
item = tableModel.getItem(row);
if (item != null) {
fileTable.setRowSelectionInterval(row, row);
fileTable.changeSelection(row, col, false, false);
}
}
}
@ -678,6 +687,9 @@ public class FilePanelTab extends JPanel {
if (fileTable.isEditing()) {
fileTable.getCellEditor().cancelCellEditing();
e.consume();
} else {
// Close any active context menu or other popups
MenuSelectionManager.defaultManager().clearSelectedPath();
}
} else if (viewMode == ViewMode.BRIEF) {
handleBriefKeyNavigation(e);
@ -1462,19 +1474,74 @@ public class FilePanelTab extends JPanel {
}
}
/**
* Helper to set menu item state and ensure it's visually greyed out if disabled.
*/
private void setMenuItemEnabled(JMenuItem item, boolean enabled) {
item.setEnabled(enabled);
if (!enabled) {
String text = item.getText();
if (text != null && !text.startsWith("<html>")) {
item.setText("<html><font color='#808080'>" + text + "</font></html>");
}
// Ensure HTML rendering even when disabled
item.putClientProperty("html.disable", Boolean.FALSE);
item.setForeground(Color.GRAY);
} else {
String text = item.getText();
if (text != null && text.startsWith("<html>")) {
String plain = text.replaceAll("<[^>]*>", "");
item.setText(plain);
}
item.setForeground(null);
}
}
/**
* Show a context popup menu for a specific file item at the given point (table coordinates).
*/
private void showContextMenuForItem(FileItem item, Point tablePoint) {
if (item == null) return;
JPopupMenu menu = new JPopupMenu();
// Force ALL possible disabled text properties to grey to override System L&F
Color forceColor = Color.GRAY;
UIManager.put("MenuItem.disabledForeground", forceColor);
UIManager.put("Label.disabledForeground", forceColor);
UIManager.put("textInactiveText", forceColor);
UIManager.put("Button.disabledText", forceColor);
UIManager.put("CheckBoxMenuItem.disabledForeground", forceColor);
UIManager.put("Menu.disabledForeground", forceColor);
boolean isParentDir = item.getName().equals("..");
boolean isDir = item.isDirectory();
boolean hasClipboardFiles = !cz.kamma.kfmanager.service.ClipboardService.getFilesFromClipboard().isEmpty();
JPopupMenu menu = new JPopupMenu() {
@Override
public void setVisible(boolean b) {
super.setVisible(b);
if (b) {
// When the menu becomes visible, ensure it can receive key events
requestFocusInWindow();
}
}
};
// Explicitly handle ESCAPE to close the menu
menu.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancel");
menu.getActionMap().put("cancel", new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
menu.setVisible(false);
}
});
// Open
JMenuItem openItem = new JMenuItem("Open");
setMenuItemEnabled(openItem, !isParentDir);
openItem.addActionListener(ae -> {
if (item.getName().equals("..")) return;
if (item.isDirectory()) {
if (isParentDir) return;
if (isDir) {
loadDirectory(item.getFile());
} else {
openFileNative(item.getFile());
@ -1483,10 +1550,10 @@ public class FilePanelTab extends JPanel {
menu.add(openItem);
// Open with
if (!item.getName().equals("..")) {
if (!isParentDir) {
java.util.List<AppConfig.OpenWithEntry> owEntries = persistedConfig != null ? persistedConfig.getOpenWithEntries() : new java.util.ArrayList<>();
String itemType;
if (item.isDirectory()) {
if (isDir) {
itemType = "directory";
} else {
String name = item.getFile().getName().toLowerCase();
@ -1509,13 +1576,22 @@ public class FilePanelTab extends JPanel {
openWithMenu.add(owItem);
}
menu.add(openWithMenu);
} else {
JMenuItem openWithMenuItem = new JMenuItem("Open with");
setMenuItemEnabled(openWithMenuItem, false);
menu.add(openWithMenuItem);
}
} else {
JMenuItem openWithMenuItem = new JMenuItem("Open with");
setMenuItemEnabled(openWithMenuItem, false);
menu.add(openWithMenuItem);
}
// Edit
JMenuItem editItem = new JMenuItem("Edit");
setMenuItemEnabled(editItem, !isParentDir && !isDir);
editItem.addActionListener(ae -> {
if (item.isDirectory() || item.getName().equals("..")) return;
if (isDir || isParentDir) return;
try {
String ext = persistedConfig != null ? persistedConfig.getExternalEditorPath() : null;
if (ext != null && !ext.trim().isEmpty()) {
@ -1534,6 +1610,7 @@ public class FilePanelTab extends JPanel {
// Show in folder / Reveal
JMenuItem revealItem = new JMenuItem("Show in folder");
setMenuItemEnabled(revealItem, !isParentDir);
revealItem.addActionListener(ae -> {
try {
String os = System.getProperty("os.name").toLowerCase();
@ -1554,6 +1631,7 @@ public class FilePanelTab extends JPanel {
// Copy path
JMenuItem copyPath = new JMenuItem("Copy path");
setMenuItemEnabled(copyPath, !isParentDir);
copyPath.addActionListener(ae -> {
try {
StringSelection sel = new StringSelection(item.getFile().getAbsolutePath());
@ -1569,18 +1647,21 @@ public class FilePanelTab extends JPanel {
// Cut
JMenuItem cutItem = new JMenuItem("Cut");
cutItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK));
setMenuItemEnabled(cutItem, !isParentDir);
cutItem.addActionListener(ae -> copyToClipboard(true));
menu.add(cutItem);
// Copy
JMenuItem copyItem = new JMenuItem("Copy");
copyItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK));
setMenuItemEnabled(copyItem, !isParentDir);
copyItem.addActionListener(ae -> copyToClipboard(false));
menu.add(copyItem);
// Paste
JMenuItem pasteItem = new JMenuItem("Paste");
pasteItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK));
setMenuItemEnabled(pasteItem, hasClipboardFiles);
pasteItem.addActionListener(ae -> pasteFromClipboard());
menu.add(pasteItem);
@ -1588,6 +1669,7 @@ public class FilePanelTab extends JPanel {
// Delete
JMenuItem deleteItem = new JMenuItem("Delete");
setMenuItemEnabled(deleteItem, !isParentDir);
deleteItem.addActionListener(ae -> {
int res = JOptionPane.showConfirmDialog(FilePanelTab.this,
"Really delete '" + item.getName() + "'?",
@ -1680,6 +1762,7 @@ public class FilePanelTab extends JPanel {
// Properties
JMenuItem props = new JMenuItem("Properties");
setMenuItemEnabled(props, !isParentDir);
props.addActionListener(ae -> {
try {
Window parent = SwingUtilities.getWindowAncestor(FilePanelTab.this);
@ -1696,6 +1779,16 @@ public class FilePanelTab extends JPanel {
});
menu.add(props);
// Ensure focus returns to table when menu is cancelled (e.g. via ESC)
menu.addPopupMenuListener(new javax.swing.event.PopupMenuListener() {
@Override public void popupMenuWillBecomeVisible(javax.swing.event.PopupMenuEvent e) {}
@Override public void popupMenuWillBecomeInvisible(javax.swing.event.PopupMenuEvent e) {
// Use invokeLater to ensure the menu is fully closed before requesting focus
SwingUtilities.invokeLater(() -> fileTable.requestFocusInWindow());
}
@Override public void popupMenuCanceled(javax.swing.event.PopupMenuEvent e) {}
});
// Show the popup at the click location
SwingUtilities.invokeLater(() -> {
try {
@ -2296,9 +2389,9 @@ public class FilePanelTab extends JPanel {
isCurrentCell = isSelected;
}
// Only show selection highlight when this table has keyboard focus.
// If the table is not focused, do not highlight any row (show normal background).
if (isCurrentCell && table.hasFocus()) {
// Show selection highlight even when the table doesn't have focus, but only
// if this panel/tab is the active one.
if (isCurrentCell && FilePanelTab.this.active) {
setBackground(selectionColor);
} else {
setBackground(FilePanelTab.this.getBackground());
@ -2322,7 +2415,7 @@ public class FilePanelTab extends JPanel {
// Preserve whatever style the base font has (do not force plain)
setFont(baseFont.deriveFont(baseStyle));
// Automatically adjust foreground contrast
if (isCurrentCell && table.hasFocus()) {
if (isCurrentCell && FilePanelTab.this.active) {
setForeground(isDark(selectionColor) ? Color.WHITE : Color.BLACK);
} else {
setForeground(isDark(FilePanelTab.this.getBackground()) ? Color.WHITE : Color.BLACK);