Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@
<version>5.3.0</version>
</dependency>

<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>
<version>3.5.2</version>
<dependency>
<groupId>org.pushing-pixels</groupId>
<artifactId>radiance-theming</artifactId>
<version>7.0.0</version>
</dependency>

<dependency>
Expand Down
180 changes: 98 additions & 82 deletions src/main/java/com/makfuzz/UI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -168,18 +176,20 @@ 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));
sourcePanel.add(srcLabel);

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);
Expand All @@ -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("<html>Engine ready...</html>");
statusLabel.setForeground(Color.WHITE);
Expand Down Expand Up @@ -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();
}
}

Expand All @@ -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"));
Expand Down Expand Up @@ -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)) {
Expand All @@ -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);
Expand All @@ -599,29 +613,33 @@ 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));

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

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -778,6 +795,8 @@ private void applyTableColumnStyles() {


private void performSearch() {
if (isInitializing) return;

// Prevent concurrent searches
if (executeBtn != null && !executeBtn.isEnabled()) {
searchPending = true;
Expand Down Expand Up @@ -954,6 +973,7 @@ private void populateMetricsPanel(SearchResult searchResult, List<Criteria> 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);
Expand All @@ -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;
Expand Down Expand Up @@ -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<Double> onUpdate, double value) {
JLabel lbl = new JLabel(text);
JLabel lbl = new JLabel("<html>" + text + "</html>");
styleMetricLabel(lbl, x, y, gbc, left);

if (clickable) {
Expand All @@ -1052,7 +1070,7 @@ public void mouseClicked(java.awt.event.MouseEvent e) {
@Override
public void mouseEntered(java.awt.event.MouseEvent e) {
lbl.setText("<html><u>" + text + "</u></html>");
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) {
Expand Down Expand Up @@ -1542,37 +1560,35 @@ 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);

typeLabel = new JLabel("Type:");
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);
add(minSpellingField);

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);
Expand Down
Loading