diff --git a/pom.xml b/pom.xml index 2063fca..7b5d0cf 100644 --- a/pom.xml +++ b/pom.xml @@ -58,6 +58,21 @@ flatlaf 2.2 + + com.weblookandfeel + weblaf-core + 1.2.13 + + + com.weblookandfeel + weblaf-ui + 1.2.13 + + + com.thoughtworks.xstream + xstream + 1.4.19 + com.google.code.gson gson diff --git a/src/main/java/net/mightypork/utils/DesktopApi.java b/src/main/java/net/mightypork/utils/DesktopApi.java new file mode 100644 index 0000000..0e63d1f --- /dev/null +++ b/src/main/java/net/mightypork/utils/DesktopApi.java @@ -0,0 +1,181 @@ +package net.mightypork.utils; + +import java.awt.Desktop; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +// https://stackoverflow.com/a/18004334/10719432 +public class DesktopApi { + public static boolean browse(URI uri) { + if (openSystemSpecific(uri.toString())) return true; + return browseDESKTOP(uri); + } + + public static boolean open(File file) { + if (openSystemSpecific(file.getPath())) return true; + return openDESKTOP(file); + } + + public static boolean edit(File file) { + if (openSystemSpecific(file.getPath())) return true; + return editDESKTOP(file); + } + + private static boolean openSystemSpecific(String what) { + EnumOS os = getOs(); + + if (os.isLinux()) { + if (runCommand("kde-open", what)) return true; + if (runCommand("gnome-open", what)) return true; + if (runCommand("xdg-open", what)) return true; + } + + if (os.isMac()) { + if (runCommand("open", what)) return true; + } + + if (os.isWindows()) { + return runCommand("explorer", what); + } + + return false; + } + + private static boolean browseDESKTOP(URI uri) { + logOut("Trying to use Desktop.getDesktop().browse() with " + uri.toString()); + try { + if (!Desktop.isDesktopSupported()) { + logErr("Platform is not supported."); + return false; + } + + if (!Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + logErr("BROWSE is not supported."); + return false; + } + + Desktop.getDesktop().browse(uri); + return true; + } catch (Throwable t) { + logErr("Error using desktop browse.", t); + return false; + } + } + + + private static boolean openDESKTOP(File file) { + logOut("Trying to use Desktop.getDesktop().open() with " + file.toString()); + try { + if (!Desktop.isDesktopSupported()) { + logErr("Platform is not supported."); + return false; + } + + if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) { + logErr("OPEN is not supported."); + return false; + } + + Desktop.getDesktop().open(file); + return true; + } catch (Throwable t) { + logErr("Error using desktop open.", t); + return false; + } + } + + + private static boolean editDESKTOP(File file) { + logOut("Trying to use Desktop.getDesktop().edit() with " + file); + try { + if (!Desktop.isDesktopSupported()) { + logErr("Platform is not supported."); + return false; + } + + if (!Desktop.getDesktop().isSupported(Desktop.Action.EDIT)) { + logErr("EDIT is not supported."); + return false; + } + + Desktop.getDesktop().edit(file); + return true; + } catch (Throwable t) { + logErr("Error using desktop edit.", t); + return false; + } + } + + + private static boolean runCommand(String command, String file) { + String[] parts = prepareCommand(command, file); + try { + Process p = Runtime.getRuntime().exec(parts); + if (p == null) return false; + + try { + int result = p.exitValue(); + if (result == 0) logErr("Process ended immediately."); + else logErr("Process crashed."); + return false; + } catch (IllegalThreadStateException ignored) { + return true; + } + } catch (IOException e) { + return false; + } + } + + private static String[] prepareCommand(String command, String file) { + List parts = new ArrayList<>(); + parts.add(command); + for (String s : "%s".split(" ")) { + s = String.format(s, file); + parts.add(s.trim()); + } + return parts.toArray(new String[0]); + } + + private static void logErr(String msg, Throwable t) { + System.err.println(msg); + t.printStackTrace(); + } + + private static void logErr(String msg) { + System.err.println(msg); + } + + private static void logOut(String msg) { + System.out.println(msg); + } + + public enum EnumOS { + Linux, MacOS, Solaris, Unknown, Windows; + + public boolean isLinux() { + return this == Linux || this == Solaris; + } + + public boolean isMac() { + return this == MacOS; + } + + public boolean isWindows() { + return this == Windows; + } + } + + public static EnumOS getOs() { + String s = System.getProperty("os.name").toLowerCase(); + if (s.contains("win")) return EnumOS.Windows; + if (s.contains("mac")) return EnumOS.MacOS; + if (s.contains("solaris")) return EnumOS.Solaris; + if (s.contains("sunos")) return EnumOS.Solaris; + if (s.contains("linux")) return EnumOS.Linux; + if (s.contains("unix")) return EnumOS.Linux; + return EnumOS.Unknown; + } +} diff --git a/src/main/java/xyz/mrmelon54/codesize/CodeSize.java b/src/main/java/xyz/mrmelon54/codesize/CodeSize.java index 658ee74..ff38013 100644 --- a/src/main/java/xyz/mrmelon54/codesize/CodeSize.java +++ b/src/main/java/xyz/mrmelon54/codesize/CodeSize.java @@ -16,6 +16,7 @@ import java.awt.event.KeyEvent; import java.awt.event.WindowEvent; import java.io.File; import java.io.IOException; +import java.util.Objects; import java.util.regex.Pattern; public class CodeSize extends JFrame { @@ -23,14 +24,15 @@ public class CodeSize extends JFrame { private JTextField pathField; private JTextField regexField; private InfoPanel infoPanel; + private JButton searchButton; public CodeSize(Settings settings) { super("Code Size"); this.settings = settings; setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - setPreferredSize(new Dimension(600, 600)); - setMinimumSize(new Dimension(600, 600)); + setPreferredSize(new Dimension(800, 600)); + setMinimumSize(new Dimension(800, 600)); setContentPane(createMainPanel()); setJMenuBar(createMenuBar()); } @@ -38,13 +40,13 @@ public class CodeSize extends JFrame { private JPanel createMainPanel() { pathField = ComponentUtils.fixedTextFieldHeight(new JTextField()); regexField = ComponentUtils.fixedTextFieldHeight(new JTextField()); - infoPanel = new InfoPanel(); + infoPanel = new InfoPanel(this); JButton browseButton = new JButton("Browse"); browseButton.addActionListener(e -> browseForFolder()); JButton presetButton = new JButton("Preset"); presetButton.addActionListener(e -> presetForRegex()); - JButton searchButton = new JButton("Search"); + searchButton = new JButton("Search"); searchButton.addActionListener(e -> searchCodeFiles()); Box pathRow = Box.createHorizontalBox(); @@ -157,12 +159,20 @@ public class CodeSize extends JFrame { return; } Pattern pattern; + String rawPattern = regexField.getText(); + if (Objects.equals(rawPattern, "")) rawPattern = "\\..+$"; try { - pattern = Pattern.compile(regexField.getText()); + pattern = Pattern.compile(rawPattern); } catch (Exception ex) { JOptionPane.showMessageDialog(this, "Failed to parse regex pattern", "Code Size", JOptionPane.ERROR_MESSAGE); return; } - new Thread(() -> FileFinder.search(mainFile, pattern, infoPanel)).start(); + searchButton.setEnabled(false); + infoPanel.reset(); + new Thread(() -> { + FileFinder.search(mainFile, pattern, infoPanel); + infoPanel.finishLoading(); + SwingUtilities.invokeLater(() -> searchButton.setEnabled(true)); + }).start(); } } diff --git a/src/main/java/xyz/mrmelon54/codesize/components/InfoPanel.java b/src/main/java/xyz/mrmelon54/codesize/components/InfoPanel.java index 2ee16d9..0d191da 100644 --- a/src/main/java/xyz/mrmelon54/codesize/components/InfoPanel.java +++ b/src/main/java/xyz/mrmelon54/codesize/components/InfoPanel.java @@ -1,13 +1,22 @@ package xyz.mrmelon54.codesize.components; +import net.mightypork.utils.DesktopApi; +import xyz.mrmelon54.codesize.CodeSize; +import xyz.mrmelon54.codesize.loading.LoadingAnimation; import xyz.mrmelon54.codesize.utils.ByteUtils; import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; import java.util.ArrayList; import java.util.List; -public class InfoPanel extends Box { - private final DefaultListModel fileListModel = new DefaultListModel<>(); +public class InfoPanel extends JPanel { + private final DefaultListModel fileListModel = new DefaultListModel<>(); + private final List fileListObjects = new ArrayList<>(); public InfoPanelItem totalFiles = new InfoPanelItem("Total Files", false); public InfoPanelItem totalSize = new InfoPanelItem("Total Size", true); public InfoPanelItem averageSize = new InfoPanelItem("Average Size", true); @@ -18,14 +27,35 @@ public class InfoPanel extends Box { public InfoPanelItem maxLines = new InfoPanelItem("Max Lines", false); public InfoPanelItem minLines = new InfoPanelItem("Min Lines", false); private final List values = new ArrayList<>(); + private final LoadingAnimation loadingAnimation = new LoadingAnimation(); + private final Timer loadingTimer; + private long loadingStartTime; - public InfoPanel() { - super(BoxLayout.Y_AXIS); + public InfoPanel(CodeSize codeSize) { + super(); + setLayout(new BorderLayout()); - JList fileListInfo = new JList<>(); + JList fileListInfo = new JList<>(); fileListInfo.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); fileListInfo.setModel(fileListModel); - add(fileListInfo); + fileListInfo.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + InfoPanelFile selectedValue = fileListInfo.getSelectedValue(); + if (selectedValue == null) return; + File f = selectedValue.file(); + if (DesktopApi.edit(f)) { + System.out.println("Opened file: '" + f.getAbsolutePath() + "'"); + } else { + String s = "Failed to open file '%s' in default text editor".formatted(f.getAbsolutePath()); + System.out.println(s); + JOptionPane.showMessageDialog(codeSize, s, "Code Size", JOptionPane.ERROR_MESSAGE); + } + } else super.mouseClicked(e); + } + }); + add(new JScrollPane(fileListInfo), BorderLayout.CENTER); values.add(totalFiles); values.add(totalSize); @@ -36,15 +66,59 @@ public class InfoPanel extends Box { values.add(averageLines); values.add(maxLines); values.add(minLines); - values.forEach(infoPanelItem -> add(infoPanelItem.component)); + + Box box = Box.createVerticalBox(); + box.setBorder(new EmptyBorder(0, 8, 0, 0)); + values.forEach(infoPanelItem -> box.add(infoPanelItem.component)); + add(box, BorderLayout.EAST); + + loadingTimer = new Timer(1, e -> repaint()); + } + + @Override + public void paint(Graphics g) { + super.paint(g); + if (loadingTimer.isRunning()) { + int w = getWidth(); + int h = getHeight(); + g.setColor(new Color(0f, 0f, 0f, .5f)); + g.fillRect(0, 0, w, h); + loadingAnimation.paint(g.create(w / 2 - 100, h / 2 - 100, 200, 200)); + } + } + + public void reset() { + fileListObjects.clear(); + fileListModel.clear(); + values.forEach(infoPanelItem -> infoPanelItem.setValue(0)); + loadingStartTime = System.currentTimeMillis(); + loadingAnimation.reset(); + loadingTimer.start(); + } + + public void finishLoading() { + long l = System.currentTimeMillis() - loadingStartTime; + if (l < 2000) { + try { + Thread.sleep(2000 - l); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + loadingTimer.stop(); + repaint(); } public void updateValues() { - values.forEach(InfoPanelItem::updateValue); + SwingUtilities.invokeLater(() -> { + fileListModel.clear(); + fileListObjects.forEach(fileListModel::addElement); + values.forEach(InfoPanelItem::updateValue); + }); } - public void addFileOutput(String s) { - fileListModel.addElement(s); + public void addFileOutput(InfoPanelFile infoPanelFile) { + fileListObjects.add(infoPanelFile); } public static class InfoPanelItem { @@ -61,14 +135,21 @@ public class InfoPanel extends Box { updateValue(); } + // only use inside invokeLater() call private void updateValue() { String v = byteUnits ? ByteUtils.ToBytesCount((long) value) : String.valueOf(value); - SwingUtilities.invokeLater(() -> component.setText(label + ": " + v)); + component.setText(label + ": " + v); } public void setValue(float v) { value = v; - updateValue(); + } + } + + public record InfoPanelFile(File file, long lines, long size) { + @Override + public String toString() { + return ByteUtils.ToBytesCount(size) + " -- " + lines + " -- " + file.getName(); } } } diff --git a/src/main/java/xyz/mrmelon54/codesize/loading/LoadingAnimation.java b/src/main/java/xyz/mrmelon54/codesize/loading/LoadingAnimation.java new file mode 100644 index 0000000..5be2cfb --- /dev/null +++ b/src/main/java/xyz/mrmelon54/codesize/loading/LoadingAnimation.java @@ -0,0 +1,37 @@ +package xyz.mrmelon54.codesize.loading; + +import java.awt.*; + +public class LoadingAnimation { + private long lastTime = 0; + private float tick; + + private Color[] ovalColors = new Color[]{ + new Color(0x0099e5), + new Color(0xff4c4c), + new Color(0x34bf49), + new Color(0xedea42), + }; + + public void reset() { + lastTime = System.nanoTime(); + tick = 0; + } + + public void paint(Graphics g) { + double deltaTime = (lastTime - (lastTime = System.nanoTime())) / -150000000f; + tick += deltaTime; + for (int i = 0; i < 8; i++) { + g.setColor(getOvalColor(i)); + g.fillOval(25 * i + 2, getOvalY(tick + i * 0.7f), 21, 21); + } + } + + private Color getOvalColor(int i) { + return ovalColors[i % ovalColors.length]; + } + + private int getOvalY(float y) { + return (int) (Math.sin(y) * 50 + 100 - 21 / 2f); + } +} diff --git a/src/main/java/xyz/mrmelon54/codesize/process/FileFinder.java b/src/main/java/xyz/mrmelon54/codesize/process/FileFinder.java index 6ea26c1..67539f7 100644 --- a/src/main/java/xyz/mrmelon54/codesize/process/FileFinder.java +++ b/src/main/java/xyz/mrmelon54/codesize/process/FileFinder.java @@ -8,8 +8,6 @@ import java.util.Collections; import java.util.List; import java.util.regex.Pattern; -import static xyz.mrmelon54.codesize.utils.ByteUtils.ToBytesCount; - public class FileFinder { public static void search(File mainFile, Pattern pattern, InfoPanel infoPanel) { List searchable = new ArrayList<>(); @@ -29,11 +27,10 @@ public class FileFinder { if (files1 == null) continue; Collections.addAll(searchable, files1); - File[] files2 = path1.listFiles((dir, name) -> { - return pattern.matcher(name).find(); - }); + File[] files2 = path1.listFiles((dir, name) -> pattern.matcher(name).find()); if (files2 == null) return; for (File file : files2) { + if (!file.isFile()) continue; long len = file.length(); totalFiles++; totalSize += len; @@ -43,7 +40,7 @@ public class FileFinder { totalLines += lineCount; if (lineCount > maxLines) maxLines = lineCount; if (lineCount < minLines || minLines == 0) minLines = lineCount; - infoPanel.addFileOutput(ToBytesCount(len) + " -- " + lineCount + " -- " + file.getName()); + infoPanel.addFileOutput(new InfoPanel.InfoPanelFile(file, lineCount, len)); } infoPanel.totalFiles.setValue(totalFiles); diff --git a/src/main/java/xyz/mrmelon54/codesize/ui/PresetSelector.java b/src/main/java/xyz/mrmelon54/codesize/ui/PresetSelector.java index 0e8af7e..4e69e92 100644 --- a/src/main/java/xyz/mrmelon54/codesize/ui/PresetSelector.java +++ b/src/main/java/xyz/mrmelon54/codesize/ui/PresetSelector.java @@ -45,6 +45,10 @@ public class PresetSelector extends JDialog { copyButton.addActionListener(this::copyAction); JIconButton editButton = new JIconButton(GoogleMaterialDesignIcons.EDIT, 16, 24); editButton.addActionListener(this::editAction); + JIconButton upButton = new JIconButton(GoogleMaterialDesignIcons.KEYBOARD_ARROW_UP, 16, 24); + upButton.addActionListener(this::upAction); + JIconButton downButton = new JIconButton(GoogleMaterialDesignIcons.KEYBOARD_ARROW_DOWN, 16, 24); + downButton.addActionListener(this::downAction); JButton chooseButton = new JButton("Choose"); chooseButton.addActionListener(this::selectAction); @@ -56,6 +60,8 @@ public class PresetSelector extends JDialog { hBox.add(removeButton); hBox.add(editButton); hBox.add(copyButton); + hBox.add(upButton); + hBox.add(downButton); hBox.add(Box.createHorizontalGlue()); hBox.add(chooseButton); hBox.add(cancelButton); @@ -147,4 +153,26 @@ public class PresetSelector extends JDialog { private void cancelAction(ActionEvent actionEvent) { dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING)); } + + private void upAction(ActionEvent actionEvent) { + Pair selectedPreset = getSelectedPreset(); + int idx = selectedPreset.t(); + int prev = idx - 1; + if (prev < 0) return; + codeSize.settings.presets.setElementAt(codeSize.settings.presets.elementAt(prev), idx); + codeSize.settings.presets.setElementAt(selectedPreset.u(), prev); + reload(); + presetList.setSelectedIndex(prev); + } + + private void downAction(ActionEvent actionEvent) { + Pair selectedPreset = getSelectedPreset(); + int idx = selectedPreset.t(); + int next = idx + 1; + if (next >= codeSize.settings.presets.size()) return; + codeSize.settings.presets.setElementAt(codeSize.settings.presets.elementAt(next), idx); + codeSize.settings.presets.setElementAt(selectedPreset.u(), next); + reload(); + presetList.setSelectedIndex(next); + } } diff --git a/src/main/java/xyz/mrmelon54/codesize/utils/ArrayUtils.java b/src/main/java/xyz/mrmelon54/codesize/utils/ArrayUtils.java deleted file mode 100644 index 4ed0036..0000000 --- a/src/main/java/xyz/mrmelon54/codesize/utils/ArrayUtils.java +++ /dev/null @@ -1,36 +0,0 @@ -package xyz.mrmelon54.codesize.utils; - - -import java.lang.reflect.Array; -import java.util.Collections; -import java.util.function.IntFunction; - -public record ArrayUtils(IntFunction generator) { - public T[] concat(T[] a, T[] b) { - if (a == null && b == null) return null; - if (a == null) return b; - if (b == null) return a; - final int aLen = Array.getLength(a); - final int bLen = Array.getLength(b); - if (aLen == 0) return b; - if (bLen == 0) return a; - - T[] result = generator.apply(aLen + bLen); - System.arraycopy(a, 0, result, 0, aLen); - System.arraycopy(b, 0, result, aLen, bLen); - return result; - } - - public T[] append(T[] a, T b) { - if (a == null) { - T[] result = generator.apply(1); - result[0] = b; - return result; - } - final int aLen = Array.getLength(a); - T[] result = generator.apply(aLen + 1); - System.arraycopy(a, 0, result, 0, aLen); - result[aLen] = b; - return result; - } -} diff --git a/src/main/java/xyz/mrmelon54/codesize/utils/ByteUtils.java b/src/main/java/xyz/mrmelon54/codesize/utils/ByteUtils.java index 23fce51..f157158 100644 --- a/src/main/java/xyz/mrmelon54/codesize/utils/ByteUtils.java +++ b/src/main/java/xyz/mrmelon54/codesize/utils/ByteUtils.java @@ -7,8 +7,8 @@ public class ByteUtils { if ((len < unit)) return String.format("%d %s", len, unitStr); else unitStr = unitStr.toUpperCase(); - byte[] a = "KMGTPEZY".getBytes(); + String a = "KMGTPEZY"; int exp = ((int) ((Math.log(len) / Math.log(unit)))); - return String.format("%.2f %s%s", (len / Math.pow(unit, exp)), a[(exp - 1)], unitStr); + return String.format("%.2f %s%s", (len / Math.pow(unit, exp)), a.charAt(exp - 1), unitStr); } } diff --git a/src/main/java/xyz/mrmelon54/codesize/utils/ComponentUtils.java b/src/main/java/xyz/mrmelon54/codesize/utils/ComponentUtils.java index 50ade79..b79227c 100644 --- a/src/main/java/xyz/mrmelon54/codesize/utils/ComponentUtils.java +++ b/src/main/java/xyz/mrmelon54/codesize/utils/ComponentUtils.java @@ -5,7 +5,7 @@ import java.awt.*; public class ComponentUtils { public static JTextField fixedTextFieldHeight(JTextField textField) { - textField.setMaximumSize(new Dimension(textField.getMaximumSize().width, textField.getPreferredSize().height)); + textField.setMaximumSize(new Dimension(textField.getMaximumSize().width, textField.getPreferredSize().height + 10)); return textField; } } diff --git a/src/main/java/xyz/mrmelon54/codesize/utils/Preset.java b/src/main/java/xyz/mrmelon54/codesize/utils/Preset.java index e9ba204..fbd24c4 100644 --- a/src/main/java/xyz/mrmelon54/codesize/utils/Preset.java +++ b/src/main/java/xyz/mrmelon54/codesize/utils/Preset.java @@ -12,7 +12,7 @@ public class Preset { public String regex; public boolean builtin; - private static final Pattern rawNamePattern = Pattern.compile("(.+) \\(Copy \\d+\\)"); // TODO + private static final Pattern rawNamePattern = Pattern.compile("(.+) \\(Copy \\d+\\)"); @Override public String toString() { diff --git a/src/main/resources/presets.json b/src/main/resources/presets.json index a3aaeaf..c33b395 100644 --- a/src/main/resources/presets.json +++ b/src/main/resources/presets.json @@ -11,6 +11,12 @@ "regex": "\\.(java)$", "builtin": true }, + { + "uuid": "2ca15e4f-dff8-4da9-8408-52d884f729ec", + "name": "Go Project", + "regex": "\\.(go)$", + "builtin": true + }, { "uuid": "1b71b0a9-365d-45ff-8159-4a1acd869be0", "name": "Node Project", @@ -28,5 +34,11 @@ "name": "Website Project", "regex": "\\.(html|js|css|php)$", "builtin": true + }, + { + "uuid": "0a721596-7ae9-435b-9ad2-3c6fce84853d", + "name": "C/C++ Project", + "regex": "\\.(c|cpp|c\\+\\+|h)$", + "builtin": true } ]