diff --git a/README.md b/README.md index 92be333..c82a7fc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > **The Ultimate Fuzzy Matching Engine for Data Cleaning & Deduplication** ✨ ![Java](https://img.shields.io/badge/Java-17+-orange?style=for-the-badge&logo=openjdk) -![Swing](https://img.shields.io/badge/UI-Swing_FlatLaf-blue?style=for-the-badge) +![Swing](https://img.shields.io/badge/UI-Swing-blue?style=for-the-badge) ![Status](https://img.shields.io/badge/Status-Production_Ready-green?style=for-the-badge) ![MakFuzz Screenshot](assets/screenshot.png) @@ -60,7 +60,7 @@ It goes beyond simple string matching by combining **Spelling Similarity** (how Built with robust, industry-standard libraries: - **Java 17+**: The solid LTS foundation. -- **Swing + FlatLaf**: For a crisp, modern, high-DPI aware user interface. +- **Swing**: For a robust and responsive desktop user interface. - **Apache Commons Text/Codec**: The heavy lifters for string algorithms. - **Jakarta XML Binding (JAXB)**: For clean, standard configuration management. - **Apache POI**: For native Excel (.xlsx) generation. diff --git a/pom.xml b/pom.xml index 39feda7..904380b 100644 --- a/pom.xml +++ b/pom.xml @@ -92,10 +92,10 @@ 5.3.0 - - com.formdev - flatlaf - 3.5.2 + + org.pushing-pixels + radiance-theming + 7.0.0 diff --git a/src/main/java/com/makfuzz/UI.java b/src/main/java/com/makfuzz/UI.java index 58bc3df..b9db47b 100644 --- a/src/main/java/com/makfuzz/UI.java +++ b/src/main/java/com/makfuzz/UI.java @@ -55,8 +55,8 @@ import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import com.formdev.flatlaf.FlatClientProperties; -import com.formdev.flatlaf.FlatIntelliJLaf; +import org.pushingpixels.radiance.theming.api.RadianceThemingCortex; +import org.pushingpixels.radiance.theming.api.RadianceThemingSlices; import com.makfuzz.core.Criteria; import com.makfuzz.core.Fuzz; import com.makfuzz.core.SearchResult; @@ -107,6 +107,7 @@ public class UI extends JFrame { private JLabel loadingIcon; private boolean searchPending = false; + private boolean isInitializing = false; // Card Layout for Center Panel private CardLayout centerCardLayout; @@ -115,18 +116,25 @@ public class UI extends JFrame { private static final String CARD_LOADING = "LOADING"; public UI() { - // Apply Modern Theme + // Apply Radiance Mariner Theme for a trending, high-fidelity modern UI try { - UIManager.setLookAndFeel(new FlatIntelliJLaf()); - UIManager.put("Button.arc", 8); - UIManager.put("Component.arc", 8); - UIManager.put("ProgressBar.arc", 8); - UIManager.put("TextComponent.arc", 8); - UIManager.put("Component.focusWidth", 1); - UIManager.put("ScrollBar.trackArc", 999); - UIManager.put("ScrollBar.thumbArc", 999); - UIManager.put("ScrollBar.track", new Color(0xf5f5f5)); - } catch (Exception ignored) {} + UIManager.setLookAndFeel("org.pushingpixels.radiance.theming.api.skin.RadianceMarinerLookAndFeel"); + + // Global overrides for consistent UI + UIManager.put("Button.font", new Font("SansSerif", Font.BOLD, 12)); + UIManager.put("TextField.font", new Font("SansSerif", Font.PLAIN, 12)); + UIManager.put("ComboBox.font", new Font("SansSerif", Font.PLAIN, 12)); + UIManager.put("Table.rowHeight", 28); + + // Optimization for Windows JFileChooser performance + if (System.getProperty("os.name").toLowerCase().contains("win")) { + UIManager.put("FileChooser.useShellFolder", Boolean.FALSE); + } + } catch (Exception ignored) { + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) {} + } setTitle("MakFuzz - Fuzzy Search ✨"); setSize(1400, 850); @@ -168,7 +176,11 @@ private void setupUI() { // 1. Data Source Card JPanel sourcePanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 15, 15)); - sourcePanel.putClientProperty(FlatClientProperties.STYLE, "arc: 15; background: #ffffff; "); + sourcePanel.setBackground(Color.WHITE); + sourcePanel.setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(new Color(230, 230, 230)), + BorderFactory.createEmptyBorder(10, 10, 10, 10) + )); srcLabel = new JLabel("Data Source:"); srcLabel.setFont(new Font("SansSerif", Font.BOLD, 13)); @@ -176,10 +188,8 @@ private void setupUI() { sourcePathField = new JTextField("./names.csv", 60); sourcePathField.setEditable(false); - sourcePathField.putClientProperty(FlatClientProperties.STYLE, "showClearButton: true; arc: 8"); browseBtn = new JButton("Browse"); - browseBtn.putClientProperty(FlatClientProperties.STYLE, "buttonType: toolBarButton; focusWidth: 0"); browseBtn.addActionListener(e -> chooseFileAndColumns()); sourcePanel.add(sourcePathField); @@ -199,8 +209,9 @@ private void setupUI() { // 4. Status Bar JPanel statusBar = new JPanel(new BorderLayout()); - statusBar.setBackground(new Color(63, 81, 181)); - statusBar.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5)); + statusBar.setBackground(new Color(15, 30, 60)); // Solid Dark Nautical Blue for high contrast + // Margin/Padding/Border removed from statusBar + statusBar.setBorder(null); statusLabel = new JLabel("Engine ready..."); statusLabel.setForeground(Color.WHITE); @@ -301,46 +312,52 @@ private void loadSettings() { if (!configFile.exists()) { return; } - ConfigManager.AppConfig config = ConfigManager.loadConfig(configFile); - if (config != null) { - sourcePathField.setText(config.sourcePath); - try { globalThresholdField.setValue(config.globalThreshold); } catch (Exception e) {} - topNField.setText(String.valueOf(config.topN)); + isInitializing = true; + try { + ConfigManager.AppConfig config = ConfigManager.loadConfig(configFile); - if (config.language != null) { - if (config.language.equals("fr")) { - currentLocale = Locale.FRENCH; - langCombo.setSelectedItem("FR"); - } else { - currentLocale = Locale.ENGLISH; - langCombo.setSelectedItem("EN"); + if (config != null) { + sourcePathField.setText(config.sourcePath); + try { globalThresholdField.setValue(config.globalThreshold); } catch (Exception e) {} + topNField.setText(String.valueOf(config.topN)); + + if (config.language != null) { + if (config.language.equals("fr")) { + currentLocale = Locale.FRENCH; + langCombo.setSelectedItem("FR"); + } else { + currentLocale = Locale.ENGLISH; + langCombo.setSelectedItem("EN"); + } + bundle = ResourceBundle.getBundle("messages", currentLocale); + updateTexts(); } - bundle = ResourceBundle.getBundle("messages", currentLocale); - updateTexts(); - } - if (config.criteriaList != null && !config.criteriaList.isEmpty()) { - criteriaContainer.removeAll(); - criteriaLines.clear(); - for (ConfigManager.CriteriaConfig cc : config.criteriaList) { - CriteriaLine line = new CriteriaLine(cc.columnName, cc.value, () -> performSearch(), this::removeCriteriaLine); - line.setColumnIndex(cc.columnIndex); - line.setConfig(cc); - criteriaLines.add(line); - criteriaContainer.add(line); - } - criteriaContainer.revalidate(); - criteriaContainer.repaint(); - - // Update table columns to match loaded criteria - updateTexts(); - - // If we have a path, load data now - if (!config.sourcePath.isEmpty()) { - loadData(config.sourcePath); + if (config.criteriaList != null && !config.criteriaList.isEmpty()) { + criteriaContainer.removeAll(); + criteriaLines.clear(); + for (ConfigManager.CriteriaConfig cc : config.criteriaList) { + CriteriaLine line = new CriteriaLine(cc.columnName, cc.value, () -> performSearch(), this::removeCriteriaLine); + line.setColumnIndex(cc.columnIndex); + line.setConfig(cc); + criteriaLines.add(line); + criteriaContainer.add(line); + } + criteriaContainer.revalidate(); + criteriaContainer.repaint(); + + // Update table columns to match loaded criteria + updateTexts(); } } + } finally { + isInitializing = false; + } + + // Trigger a single search/load after initialization is complete + if (!sourcePathField.getText().isEmpty() && !criteriaLines.isEmpty()) { + performSearch(); } } @@ -350,7 +367,6 @@ private void updateTexts() { appSubtitle.setText(bundle.getString("app.header.subtitle")); srcLabel.setText(bundle.getString("source.label")); browseBtn.setText(bundle.getString("source.button.browse")); - sourcePathField.putClientProperty(FlatClientProperties.PLACEHOLDER_TEXT, bundle.getString("source.placeholder")); criteriaLabel.setText(bundle.getString("search.config.label")); thresholdLabel.setText(bundle.getString("search.label.threshold")); limitLabel.setText(bundle.getString("search.label.topn")); @@ -569,8 +585,7 @@ private void setupTopPanel(JPanel parent) { bottomBar.add(searchLangLabel); langCombo = new JComboBox<>(new String[]{"EN", "FR"}); - langCombo.putClientProperty(FlatClientProperties.STYLE, "arc: 8; background: #3f51b5; foreground: #ffffff; focusWidth: 0;"); - langCombo.setPreferredSize(new Dimension(60, 30)); + langCombo.setPreferredSize(new Dimension(80, 32)); langCombo.addActionListener(e -> { String selected = (String) langCombo.getSelectedItem(); if ("EN".equals(selected)) { @@ -589,8 +604,7 @@ private void setupTopPanel(JPanel parent) { thresholdLabel = new JLabel("Global Threshold:"); bottomBar.add(thresholdLabel); globalThresholdField = new JSpinner(new SpinnerNumberModel(0.3, 0.0, 1.0, 0.05)); - globalThresholdField.putClientProperty(FlatClientProperties.STYLE, "arc: 8; "); - globalThresholdField.setPreferredSize(new Dimension(80, 30)); + globalThresholdField.setPreferredSize(new Dimension(100, 32)); ((JSpinner.DefaultEditor)globalThresholdField.getEditor()).getTextField().addActionListener(e -> performSearch()); globalThresholdField.addChangeListener(e -> performSearch()); bottomBar.add(globalThresholdField); @@ -599,7 +613,7 @@ private void setupTopPanel(JPanel parent) { limitLabel = new JLabel("Top N Limit:"); bottomBar.add(limitLabel); topNField = new JTextField("1000", 5); - topNField.putClientProperty(FlatClientProperties.STYLE, "arc: 8; "); + topNField.setPreferredSize(new Dimension(80, 32)); topNField.addActionListener(e -> { topNField.selectAll(); performSearch(); }); bottomBar.add(topNField); bottomBar.add(Box.createHorizontalStrut(15)); @@ -607,21 +621,25 @@ private void setupTopPanel(JPanel parent) { bottomBar.add(Box.createHorizontalStrut(15)); executeBtn = new JButton("Run Search"); - executeBtn.setBackground(new Color(63, 81, 181)); - executeBtn.setForeground(Color.WHITE); - executeBtn.putClientProperty(FlatClientProperties.STYLE, "hoverBackground: #303F9F; pressedBackground: #1a237e; arc: 10"); + executeBtn.setPreferredSize(new Dimension(210, 32)); // 1.5x original width + executeBtn.setBackground(new Color(15, 30, 60)); // Matches footer background + executeBtn.setForeground(Color.WHITE); + + // Radiance specific: Force white text by preventing theme-based color changes + RadianceThemingCortex.ComponentOrParentChainScope.setColorizationFactor(executeBtn, 1.0); + executeBtn.setFocusPainted(false); executeBtn.setFont(new Font("SansSerif", Font.BOLD, 14)); executeBtn.addActionListener(e -> performSearch()); bottomBar.add(executeBtn); csvBtn = new JButton("Export CSV"); - csvBtn.putClientProperty(FlatClientProperties.STYLE, "buttonType: toolBarButton"); + csvBtn.setPreferredSize(new Dimension(120, 32)); csvBtn.addActionListener(e -> exportToCSV()); bottomBar.add(csvBtn); excelBtn = new JButton("Export Excel"); - excelBtn.putClientProperty(FlatClientProperties.STYLE, "buttonType: toolBarButton"); + excelBtn.setPreferredSize(new Dimension(120, 32)); excelBtn.addActionListener(e -> exportToExcel()); bottomBar.add(excelBtn); @@ -656,8 +674,7 @@ public boolean isCellEditable(int row, int column) { resultTable.getTableHeader().setForeground(new Color(63, 81, 181)); resultTable.getTableHeader().setFont(new Font("SansSerif", Font.BOLD, 12)); - // Modern alternating colors - resultTable.putClientProperty(FlatClientProperties.STYLE, "showHorizontalLines: true; showVerticalLines: true; rowHeight: 28;"); + // Add double-click listener to table header for sorting resultTable.getTableHeader().addMouseListener(new java.awt.event.MouseAdapter() { @@ -778,6 +795,8 @@ private void applyTableColumnStyles() { private void performSearch() { + if (isInitializing) return; + // Prevent concurrent searches if (executeBtn != null && !executeBtn.isEnabled()) { searchPending = true; @@ -954,6 +973,7 @@ private void populateMetricsPanel(SearchResult searchResult, List crit } // Data Rows + gbc.insets = new Insets(0, 0, 0, 0); // No vertical margins between rows addRowToMetrics(bundle.getString("search.metrics.max_under"), searchResult.getMaxUnderCandidate(), searchResult.getMaxUnderThreshold(), 1, gbc, criteriaList); addRowToMetrics(bundle.getString("search.metrics.min_above"), searchResult.getMinAboveCandidate(), searchResult.getMinAboveThreshold(), 2, gbc, criteriaList); addRowToMetrics(bundle.getString("search.metrics.max_above"), searchResult.getMaxAboveCandidate(), searchResult.getMaxAboveThreshold(), 3, gbc, criteriaList); @@ -970,20 +990,18 @@ private void addMetricHeader(String text, int x, int y, GridBagConstraints gbc, private void styleMetricLabel(JLabel lbl, int x, int y, GridBagConstraints gbc, boolean left) { lbl.setForeground(Color.WHITE); - lbl.setFont(new Font("SansSerif", Font.PLAIN, 10)); + lbl.setFont(new Font("SansSerif", Font.BOLD, 10)); lbl.setHorizontalAlignment(left ? JLabel.LEFT : JLabel.RIGHT); - lbl.setBorder(BorderFactory.createEmptyBorder(2, 8, 2, 8)); + lbl.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10)); // No vertical padding - // Soft alternate styling for column groups - // Block 0: Metric Label + Total (col 0, 1) - // Block 1..N: Criteria Groups (3 columns each) + // Segmented background for clarity with SOLID colors int blockIdx = (x < 2) ? 0 : ((x - 2) / 3) + 1; if (blockIdx % 2 == 1) { - lbl.setBackground(new Color(255, 255, 255, 20)); // Soft white overlay - lbl.setOpaque(true); + lbl.setBackground(new Color(45, 65, 120)); // Lighter solid nautical blue } else { - lbl.setOpaque(false); + lbl.setBackground(new Color(25, 35, 75)); // Darker solid nautical blue } + lbl.setOpaque(true); gbc.gridx = x; gbc.gridy = y; @@ -1036,7 +1054,7 @@ private void addRowToMetrics(String label, SimResult result, double threshold, i private static final boolean[] leftCols = {true, false, false, true, false, false}; private void addMetricValue(String text, int x, int y, GridBagConstraints gbc, boolean left, boolean clickable, java.util.function.Consumer onUpdate, double value) { - JLabel lbl = new JLabel(text); + JLabel lbl = new JLabel("" + text + ""); styleMetricLabel(lbl, x, y, gbc, left); if (clickable) { @@ -1052,7 +1070,7 @@ public void mouseClicked(java.awt.event.MouseEvent e) { @Override public void mouseEntered(java.awt.event.MouseEvent e) { lbl.setText("" + text + ""); - lbl.setForeground(new Color(187, 222, 251)); // Soft light blue on hover + lbl.setForeground(Color.YELLOW); // Yellow on hover as requested } @Override public void mouseExited(java.awt.event.MouseEvent e) { @@ -1542,7 +1560,7 @@ public CriteriaLine(String colName, String defaultValue, Runnable onEnter, java. valueLabel = new JLabel("Value:"); add(valueLabel); valueField = new JTextField(defaultValue, 12); - valueField.putClientProperty(FlatClientProperties.STYLE, "arc: 8"); + valueField.setPreferredSize(new Dimension(150, 32)); valueField.addActionListener(e -> { valueField.selectAll(); onEnter.run(); }); add(valueField); @@ -1550,20 +1568,19 @@ public CriteriaLine(String colName, String defaultValue, Runnable onEnter, java. add(typeLabel); typeCombo = new JComboBox<>(Criteria.MatchingType.values()); typeCombo.setSelectedItem(Criteria.MatchingType.SIMILARITY); - typeCombo.putClientProperty(FlatClientProperties.STYLE, "arc: 8"); + typeCombo.setPreferredSize(new Dimension(140, 32)); add(typeCombo); weightLabel = new JLabel("Weight:"); add(weightLabel); weightSpinner = new JSpinner(new SpinnerNumberModel(1, 0, 100, 1)); - weightSpinner.putClientProperty(FlatClientProperties.STYLE, "arc: 8; "); + weightSpinner.setPreferredSize(new Dimension(70, 32)); weightSpinner.addChangeListener(e -> onEnter.run()); add(weightSpinner); minSpellingLabel = new JLabel("Min Spell:"); minSpellingField = new JSpinner(new SpinnerNumberModel(0.8, 0.0, 1.0, 0.05)); - minSpellingField.putClientProperty(FlatClientProperties.STYLE, "arc: 8; "); - minSpellingField.setPreferredSize(new Dimension(80, 30)); + minSpellingField.setPreferredSize(new Dimension(80, 32)); ((JSpinner.DefaultEditor)minSpellingField.getEditor()).getTextField().addActionListener(e -> onEnter.run()); minSpellingField.addChangeListener(e -> onEnter.run()); add(minSpellingLabel); @@ -1571,8 +1588,7 @@ public CriteriaLine(String colName, String defaultValue, Runnable onEnter, java. minPhoneticLabel = new JLabel("Min Phon:"); minPhoneticField = new JSpinner(new SpinnerNumberModel(0.8, 0.0, 1.0, 0.05)); - minPhoneticField.putClientProperty(FlatClientProperties.STYLE, "arc: 8; "); - minPhoneticField.setPreferredSize(new Dimension(80, 30)); + minPhoneticField.setPreferredSize(new Dimension(80, 32)); ((JSpinner.DefaultEditor)minPhoneticField.getEditor()).getTextField().addActionListener(e -> onEnter.run()); minPhoneticField.addChangeListener(e -> onEnter.run()); add(minPhoneticLabel);