this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

polish Dock a lot

+876 -118
+4
src/main/java/app/Environment.java
··· 257 257 UIManager.put("Component.arrowType", "chevron"); 258 258 UIManager.put("Component.focusWidth", 1); 259 259 260 + int arc = 10; 261 + UIManager.put("Button.arc", arc); 262 + UIManager.put("TabbedPane.cardTabArc", arc); 263 + 260 264 fixScrollSpeed(); 261 265 262 266 if (fromJar && gitBuildTag != null && mainConfig.getBoolean(Options.CheckForUpdates))
+45 -51
src/main/java/app/StarRodMain.java
··· 1 1 package app; 2 2 3 - import static app.Directories.PROJ_THUMBNAIL; 4 - 3 + import java.awt.Color; 5 4 import java.awt.Desktop; 6 5 import java.awt.Dimension; 7 6 import java.awt.GraphicsEnvironment; 8 7 import java.awt.Image; 9 8 import java.awt.Toolkit; 10 - import java.awt.datatransfer.Clipboard; 11 - import java.awt.datatransfer.StringSelection; 12 9 import java.awt.event.WindowEvent; 13 10 import java.io.File; 14 11 import java.io.IOException; ··· 18 15 19 16 import javax.imageio.ImageIO; 20 17 import javax.swing.AbstractButton; 21 - import javax.swing.BorderFactory; 22 18 import javax.swing.ImageIcon; 23 19 import javax.swing.JButton; 24 20 import javax.swing.JLabel; 25 - import javax.swing.JMenuItem; 21 + import javax.swing.JMenu; 22 + import javax.swing.JMenuBar; 26 23 import javax.swing.JOptionPane; 27 24 import javax.swing.JPanel; 28 - import javax.swing.JPopupMenu; 29 - import javax.swing.JProgressBar; 30 - import javax.swing.JScrollBar; 31 - import javax.swing.JScrollPane; 32 25 import javax.swing.JSplitPane; 33 - import javax.swing.JTextArea; 34 - import javax.swing.ScrollPaneConstants; 35 26 import javax.swing.SwingConstants; 36 27 import javax.swing.SwingUtilities; 37 28 import javax.swing.SwingWorker; 38 29 import javax.swing.UIManager; 39 30 import javax.swing.WindowConstants; 40 31 41 - import org.apache.commons.io.FilenameUtils; 42 - 43 - import project.engine.BuildEnvironment; 44 - import project.engine.BuildOutputListener; 45 - import project.engine.BuildResult; 46 - import app.config.Options; 32 + import app.bar.Bar; 47 33 import app.input.InvalidInputException; 48 34 import app.pane.Dock; 35 + import app.pane.Pane; 49 36 import assets.AssetHandle; 50 37 import assets.AssetManager; 51 38 import assets.ExpectedAsset; 52 39 import common.BaseEditor; 53 - import project.engine.Engine; 54 40 import game.globals.editor.GlobalsEditor; 55 41 import game.map.Map; 56 42 import game.map.compiler.BuildException; ··· 58 44 import game.map.compiler.GeometryCompiler; 59 45 import game.map.editor.MapEditor; 60 46 import game.map.scripts.ScriptGenerator; 61 - import game.map.scripts.extract.Extractor; 62 47 import game.message.editor.MessageEditor; 63 48 import game.sprite.editor.SpriteEditor; 64 49 import game.texture.editor.ImageEditor; 65 50 import game.worldmap.WorldMapEditor; 66 51 import net.miginfocom.swing.MigLayout; 67 - import project.Project; 68 52 import util.Logger; 69 - import util.Priority; 70 53 71 54 public class StarRodMain extends StarRodFrame 72 55 { 73 56 // Layout constants 74 57 private static final int MIN_PANE_WIDTH = 250; 75 - private static final int MIN_WINDOW_WIDTH = MIN_PANE_WIDTH * 3; 58 + private static final int MIN_WINDOW_WIDTH = MIN_PANE_WIDTH * 3 + 300; 76 59 private static final int MIN_WINDOW_HEIGHT = 600; 77 - private static final int MIN_DOCK_HEIGHT = 100; 60 + private static final int MIN_DOCK_HEIGHT = 140; 78 61 79 62 public static void main(String[] args) throws InterruptedException 80 63 { ··· 101 84 setTitle(Environment.decorateTitle("Star Rod")); 102 85 setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 103 86 setMinimumSize(new Dimension(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT)); 87 + 88 + Color bg = UIManager.getColor("Panel.background"); 89 + getContentPane().setBackground(bg.darker()); 90 + 91 + // Menu bar 92 + JMenuBar menuBar = new JMenuBar(); 93 + JMenu fileMenu = new JMenu("File"); 94 + JMenu editMenu = new JMenu("Edit"); 95 + menuBar.add(fileMenu); 96 + menuBar.add(editMenu); 97 + setJMenuBar(menuBar); 104 98 105 99 // TODO: click this to change project 106 100 JLabel projectIdLabel = new JLabel(Environment.getProject().getManifest().getId()); ··· 212 206 }); 213 207 214 208 // Left pane - buttons panel 215 - JPanel leftPane = new JPanel(new MigLayout("fill, ins 8, wrap 1")); 209 + Pane leftPane = new Pane(); 210 + leftPane.setLayout(new MigLayout("fill, ins 8, wrap 1")); 216 211 217 212 JPanel buttonsPanel = new JPanel(new MigLayout("fillx, wrap 1, hidemode 3")); 218 213 buttonsPanel.add(mapEditorButton, "growx"); ··· 230 225 leftPane.setMinimumSize(new Dimension(MIN_PANE_WIDTH, 0)); 231 226 232 227 // Middle pane - placeholder for now 233 - JPanel middlePane = new JPanel(new MigLayout("fill, ins 8")); 228 + Pane middlePane = new Pane(); 229 + middlePane.setLayout(new MigLayout("fill, ins 8")); 234 230 middlePane.add(new JLabel("Middle Pane"), "center"); 235 231 middlePane.setMinimumSize(new Dimension(MIN_PANE_WIDTH, 0)); 236 232 237 233 // Right pane - placeholder for now 238 - JPanel rightPane = new JPanel(new MigLayout("fill, ins 8")); 234 + Pane rightPane = new Pane(); 235 + rightPane.setLayout(new MigLayout("fill, ins 8")); 239 236 rightPane.add(new JLabel("Right Pane"), "center"); 240 237 rightPane.setMinimumSize(new Dimension(MIN_PANE_WIDTH, 0)); 241 238 242 - // Create horizontal split panes (left | middle | right) 243 - JSplitPane leftMiddleSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPane, middlePane); 239 + // Dock (bottom panel in middle column) 240 + Dock dock = new Dock(); 241 + dock.setMinimumSize(new Dimension(0, MIN_DOCK_HEIGHT)); 242 + 243 + // Create vertical split pane (middlePane | dock) for center column 244 + JSplitPane middleDockSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, middlePane, dock); 245 + middleDockSplit.setOneTouchExpandable(false); 246 + middleDockSplit.setDividerSize(4); 247 + middleDockSplit.setResizeWeight(1.0); // Give most space to middle pane 248 + middleDockSplit.setOpaque(false); 249 + 250 + // Create horizontal split panes (left | (middle + dock) | right) 251 + JSplitPane leftMiddleSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPane, middleDockSplit); 244 252 leftMiddleSplit.setOneTouchExpandable(false); 245 - leftMiddleSplit.setDividerSize(1); 253 + leftMiddleSplit.setDividerSize(4); 246 254 leftMiddleSplit.setResizeWeight(0.0); // Left pane stays fixed, middle gets extra space 255 + leftMiddleSplit.setOpaque(false); 247 256 248 257 JSplitPane mainHorizontalSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftMiddleSplit, rightPane); 249 258 mainHorizontalSplit.setOneTouchExpandable(false); 250 - mainHorizontalSplit.setDividerSize(1); 259 + mainHorizontalSplit.setDividerSize(4); 251 260 mainHorizontalSplit.setResizeWeight(1.0); // Middle gets priority, right stays fixed 252 - 253 - // Dock (bottom panel) 254 - Dock dock = new Dock(); 255 - dock.setMinimumSize(new Dimension(0, MIN_DOCK_HEIGHT)); 256 - dock.setPreferredSize(new Dimension(0, MIN_WINDOW_HEIGHT / 3)); 257 - 258 - // Create vertical split pane (middlePane | dock) 259 - JSplitPane verticalSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, mainHorizontalSplit, dock); 260 - verticalSplit.setOneTouchExpandable(false); 261 - verticalSplit.setDividerSize(4); 262 - verticalSplit.setResizeWeight(1.0); // Give most space to top pane 261 + mainHorizontalSplit.setOpaque(false); 263 262 264 263 // Status bar 265 - JLabel statusBarLabel = new JLabel("Status bar"); 266 - statusBarLabel.setBorder(BorderFactory.createCompoundBorder( 267 - BorderFactory.createMatteBorder(1, 0, 0, 0, UIManager.getColor("Separator.foreground")), 268 - BorderFactory.createEmptyBorder(2, 8, 2, 8) 269 - )); 270 - SwingUtils.setFontSize(statusBarLabel, 11); 264 + Bar statusBar = new Bar(); 271 265 272 266 // Layout 273 - setLayout(new MigLayout("fill, ins 0, wrap")); 274 - add(verticalSplit, "grow, push"); 275 - add(statusBarLabel, "growx, h 20!"); 267 + setLayout(new MigLayout("fill, ins 4, gap 4, wrap")); 268 + add(mainHorizontalSplit, "grow, push"); 269 + add(statusBar, "growx, h 24!"); 276 270 277 271 pack(); 278 272 setLocationRelativeTo(null);
+25
src/main/java/app/bar/Bar.java
··· 1 + package app.bar; 2 + 3 + import javax.swing.JPanel; 4 + 5 + import net.miginfocom.swing.MigLayout; 6 + 7 + /** A horizontal bar component for displaying status information. */ 8 + public class Bar extends JPanel 9 + { 10 + private final GitBranch gitBranch; 11 + 12 + public Bar() 13 + { 14 + setOpaque(false); 15 + setLayout(new MigLayout("fill, ins 4, gap 8")); 16 + 17 + gitBranch = new GitBranch(); 18 + add(gitBranch, "alignx left"); 19 + } 20 + 21 + public void dispose() 22 + { 23 + gitBranch.dispose(); 24 + } 25 + }
+153
src/main/java/app/bar/GitBranch.java
··· 1 + package app.bar; 2 + 3 + import java.awt.FlowLayout; 4 + import java.io.BufferedReader; 5 + import java.io.File; 6 + import java.io.IOException; 7 + import java.io.InputStreamReader; 8 + import java.nio.file.FileSystems; 9 + import java.nio.file.Path; 10 + import java.nio.file.StandardWatchEventKinds; 11 + import java.nio.file.WatchEvent; 12 + import java.nio.file.WatchKey; 13 + import java.nio.file.WatchService; 14 + 15 + import javax.swing.JLabel; 16 + import javax.swing.JPanel; 17 + import javax.swing.SwingUtilities; 18 + 19 + import app.Environment; 20 + import util.Logger; 21 + import util.ui.ThemedIcon; 22 + 23 + /** Displays the current git branch and watches for changes. */ 24 + public class GitBranch extends JPanel 25 + { 26 + private final JLabel label; 27 + private WatchService watchService; 28 + private Thread watchThread; 29 + 30 + public GitBranch() 31 + { 32 + setLayout(new FlowLayout(FlowLayout.LEFT, 4, 0)); 33 + setOpaque(false); 34 + 35 + label = new JLabel(); 36 + label.setIcon(ThemedIcon.GIT_BRANCH.derive(15, 15)); 37 + label.setIconTextGap(4); 38 + 39 + add(label); 40 + 41 + updateGitBranch(); 42 + startWatching(); 43 + } 44 + 45 + private void updateGitBranch() 46 + { 47 + File projectDir = Environment.getProject().getDirectory(); 48 + File gitDir = new File(projectDir, ".git"); 49 + 50 + if (!gitDir.exists()) { 51 + setVisible(false); 52 + return; 53 + } 54 + 55 + try { 56 + // Use symbolic-ref which works even without commits 57 + Process process = new ProcessBuilder("git", "symbolic-ref", "--short", "HEAD") 58 + .directory(projectDir) 59 + .start(); 60 + 61 + String branch = null; 62 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 63 + branch = reader.readLine(); 64 + } 65 + 66 + int exitCode = process.waitFor(); 67 + 68 + if (exitCode == 0 && branch != null && !branch.isEmpty()) { 69 + label.setText(branch); 70 + setVisible(true); 71 + } 72 + else { 73 + setVisible(false); 74 + } 75 + } 76 + catch (Exception e) { 77 + Logger.logWarning("Failed to get git branch: " + e.getMessage()); 78 + setVisible(false); 79 + } 80 + } 81 + 82 + /** Refresh the git branch display. */ 83 + public void refresh() 84 + { 85 + updateGitBranch(); 86 + } 87 + 88 + private void startWatching() 89 + { 90 + File projectDir = Environment.getProject().getDirectory(); 91 + File gitDir = new File(projectDir, ".git"); 92 + 93 + if (!gitDir.exists()) 94 + return; 95 + 96 + try { 97 + watchService = FileSystems.getDefault().newWatchService(); 98 + Path gitPath = gitDir.toPath(); 99 + 100 + // Watch .git directory for changes to HEAD file 101 + gitPath.register(watchService, 102 + StandardWatchEventKinds.ENTRY_MODIFY, 103 + StandardWatchEventKinds.ENTRY_CREATE); 104 + 105 + watchThread = new Thread(() -> { 106 + try { 107 + while (!Thread.currentThread().isInterrupted()) { 108 + WatchKey key = watchService.take(); 109 + 110 + for (WatchEvent<?> event : key.pollEvents()) { 111 + Path changed = (Path) event.context(); 112 + if (changed.toString().equals("HEAD")) { 113 + // Update on Swing thread 114 + SwingUtilities.invokeLater(this::updateGitBranch); 115 + } 116 + } 117 + 118 + if (!key.reset()) 119 + break; 120 + } 121 + } 122 + catch (InterruptedException e) { 123 + Thread.currentThread().interrupt(); 124 + } 125 + }, "GitBranchWatcher"); 126 + 127 + watchThread.setDaemon(true); 128 + watchThread.start(); 129 + } 130 + catch (IOException e) { 131 + Logger.logWarning("Failed to start git branch watcher: " + e.getMessage()); 132 + } 133 + } 134 + 135 + /** Stop watching for git changes. Call when disposing. */ 136 + public void dispose() 137 + { 138 + if (watchThread != null) { 139 + watchThread.interrupt(); 140 + watchThread = null; 141 + } 142 + 143 + if (watchService != null) { 144 + try { 145 + watchService.close(); 146 + } 147 + catch (IOException e) { 148 + Logger.logWarning("Error closing watch service: " + e.getMessage()); 149 + } 150 + watchService = null; 151 + } 152 + } 153 + }
+192 -41
src/main/java/app/pane/Dock.java
··· 1 1 package app.pane; 2 2 3 - import java.awt.BorderLayout; 3 + import java.awt.BasicStroke; 4 + import java.awt.CardLayout; 5 + import java.awt.Dimension; 6 + import java.awt.Graphics; 7 + import java.awt.Graphics2D; 8 + import java.awt.RenderingHints; 9 + import java.awt.event.MouseAdapter; 10 + import java.awt.event.MouseEvent; 11 + import java.awt.geom.Area; 12 + import java.awt.geom.Path2D; 13 + import java.util.ArrayList; 14 + import java.util.List; 4 15 5 - import javax.swing.BorderFactory; 16 + import javax.swing.JComponent; 6 17 import javax.swing.JPanel; 7 - import javax.swing.JScrollPane; 8 - import javax.swing.JTabbedPane; 9 - import javax.swing.JTextArea; 18 + import javax.swing.UIManager; 10 19 11 - import app.pane.explorer.Explorer; 12 - import util.Logger; 13 - import util.ui.ThemedIcon; 20 + import net.miginfocom.swing.MigLayout; 21 + import util.ui.Squircle; 14 22 15 - public class Dock extends JPanel 23 + /** Vertical tabs. */ 24 + public class Dock extends Pane 16 25 { 17 - private JTabbedPane tabbedPane; 18 - private JTextArea logTextArea; 26 + private static final int TAB_COLUMN_WIDTH = 30; 27 + private static final int ARC = 10; 28 + 29 + private final List<DockTab> tabs = new ArrayList<>(); 30 + private final List<TabButton> buttons = new ArrayList<>(); 31 + private final JPanel tabColumn; 32 + private final JPanel contentPanel; 33 + private final CardLayout cardLayout; 34 + private int selectedIndex = -1; 19 35 20 36 public Dock() 21 37 { 22 - // Create vertical tabbed pane with dock styling 23 - tabbedPane = new JTabbedPane(JTabbedPane.LEFT); 24 - tabbedPane.putClientProperty("JTabbedPane.tabType", "card"); 25 - tabbedPane.putClientProperty("JTabbedPane.hasFullBorder", true); 38 + super(ARC); 39 + setLayout(new MigLayout("ins 0, fill, gap 2 0", "[" + TAB_COLUMN_WIDTH + "!][grow,fill]", "[grow,fill]")); 40 + 41 + tabColumn = new JPanel(new MigLayout("ins " + ARC + " 0, wrap, gap 0", "[" + TAB_COLUMN_WIDTH + "!]", "")); 42 + tabColumn.setOpaque(false); 43 + add(tabColumn, "growy"); 44 + 45 + cardLayout = new CardLayout(); 46 + contentPanel = new JPanel(cardLayout); 47 + contentPanel.setOpaque(false); 48 + add(contentPanel, "grow"); 49 + 50 + addTab(new app.pane.explorer.Tab()); 51 + addTab(new app.pane.logs.Tab()); 52 + 53 + if (!tabs.isEmpty()) { 54 + selectTab(0); 55 + // Account for tab column width and gap in Dock's layout 56 + Dimension tabSize = tabs.get(0).getPreferredSize(); 57 + setPreferredSize(new Dimension( 58 + tabSize.width + TAB_COLUMN_WIDTH + 2, // +2 for layout gap 59 + tabSize.height 60 + )); 61 + } 62 + } 26 63 27 - // Add File Explorer tab 28 - var explorer = new Explorer(); 29 - tabbedPane.addTab(null, ThemedIcon.FOLDER_OPEN_16, explorer); 64 + public void addTab(DockTab tab) 65 + { 66 + int index = tabs.size(); 67 + tabs.add(tab); 30 68 31 - // Add Logs tab with text area 32 - logTextArea = createLogTextArea(); 33 - JScrollPane logScrollPane = new JScrollPane(logTextArea); 34 - logScrollPane.setBorder(null); 35 - tabbedPane.addTab(null, ThemedIcon.TERMINAL_16, logScrollPane); 36 - Logger.addListener(new LogListener(logTextArea)); 69 + var button = new TabButton(tab, index); 70 + buttons.add(button); 71 + tabColumn.add(button, "growx, h 24!, gapleft 1"); 37 72 38 - // Layout 39 - setLayout(new BorderLayout()); 40 - add(tabbedPane, BorderLayout.CENTER); 73 + contentPanel.add(tab, "tab" + index); 41 74 } 42 75 43 - private JTextArea createLogTextArea() 76 + private void selectTab(int index) 44 77 { 45 - JTextArea textArea = new JTextArea(); 46 - textArea.setEditable(false); 47 - // Add horizontal padding using EmptyBorder 48 - textArea.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8)); 49 - return textArea; 78 + if (index == selectedIndex) 79 + return; 80 + 81 + selectedIndex = index; 82 + cardLayout.show(contentPanel, "tab" + index); 83 + 84 + for (var button : buttons) 85 + button.repaint(); 86 + } 87 + 88 + public void dispose() 89 + { 90 + for (var tab : tabs) 91 + tab.dispose(); 92 + } 93 + 94 + @Override 95 + protected void paintComponent(Graphics g) 96 + { 97 + var g2 = (Graphics2D) g.create(); 98 + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 99 + 100 + int w = getWidth(); 101 + int h = getHeight(); 102 + int tabW = TAB_COLUMN_WIDTH + 2; 103 + 104 + // Create the full squircle for the Dock 105 + var fullShape = Squircle.path(0, 0, w, h, ARC, ARC, ARC, ARC); 106 + 107 + // Create the tabColumn area (rounded only on left) to exclude from background 108 + var tabColumnShape = Squircle.path(0, 0, tabW, h, ARC, 0, 0, ARC); 109 + 110 + // Subtract tabColumn from full shape 111 + var area = new Area(fullShape); 112 + area.subtract(new Area(tabColumnShape)); 113 + 114 + // Draw background only in the content area 115 + g2.setColor(UIManager.getColor("TabbedPane.background")); 116 + g2.fill(area); 117 + 118 + // Draw 1px border for tabColumn (left, top, bottom only, no right) 119 + g2.setColor(UIManager.getColor("Component.borderColor")); 120 + g2.setStroke(new BasicStroke(1)); 121 + 122 + var borderPath = new Path2D.Double(); 123 + double x = 0.5; 124 + double y = 0.5; 125 + double bw = TAB_COLUMN_WIDTH + 1; 126 + double bh = h - 1; 127 + double arc = ARC; 128 + 129 + // Start at top-right 130 + borderPath.moveTo(x + bw, y); 131 + // Top edge to top-left corner 132 + borderPath.lineTo(x + arc, y); 133 + // Top-left corner 134 + borderPath.quadTo(x, y, x, y + arc); 135 + // Left edge 136 + borderPath.lineTo(x, y + bh - arc); 137 + // Bottom-left corner 138 + borderPath.quadTo(x, y + bh, x + arc, y + bh); 139 + // Bottom edge to bottom-right 140 + borderPath.lineTo(x + bw, y + bh); 141 + 142 + g2.draw(borderPath); 143 + 144 + g2.dispose(); 50 145 } 51 146 52 - private static class LogListener implements Logger.Listener 147 + private class TabButton extends JComponent 53 148 { 54 - private final JTextArea textArea; 149 + private static final int INDICATOR_WIDTH = 2; 150 + private static final int HOVER_SIZE = 20; 55 151 56 - public LogListener(JTextArea textArea) 152 + private final DockTab tab; 153 + private final int index; 154 + private boolean hovered = false; 155 + 156 + TabButton(DockTab tab, int index) 57 157 { 58 - this.textArea = textArea; 158 + this.tab = tab; 159 + this.index = index; 160 + setPreferredSize(new Dimension(TAB_COLUMN_WIDTH, 24)); 161 + setToolTipText(tab.getTabName()); 162 + 163 + addMouseListener(new MouseAdapter() { 164 + @Override 165 + public void mouseClicked(MouseEvent e) 166 + { 167 + selectTab(TabButton.this.index); 168 + } 169 + 170 + @Override 171 + public void mouseEntered(MouseEvent e) 172 + { 173 + hovered = true; 174 + repaint(); 175 + } 176 + 177 + @Override 178 + public void mouseExited(MouseEvent e) 179 + { 180 + hovered = false; 181 + repaint(); 182 + } 183 + }); 59 184 } 60 185 61 186 @Override 62 - public void post(Logger.Message msg) 187 + protected void paintComponent(Graphics g) 63 188 { 64 - textArea.append(msg.text + System.lineSeparator()); 65 - // Auto-scroll to bottom 66 - textArea.setCaretPosition(textArea.getDocument().getLength()); 189 + var g2 = (Graphics2D) g.create(); 190 + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 191 + 192 + // Selection indicator 193 + if (index == selectedIndex) { 194 + g2.setColor(UIManager.getColor("Component.focusColor")); 195 + int barH = getHeight(); 196 + int barY = (getHeight() - barH) / 2; 197 + g2.fillRect(0, barY, INDICATOR_WIDTH, barH); 198 + } 199 + 200 + // Hover squircle 201 + if (hovered) { 202 + int hoverX = INDICATOR_WIDTH + (getWidth() - INDICATOR_WIDTH - HOVER_SIZE) / 2; 203 + int hoverY = (getHeight() - HOVER_SIZE) / 2; 204 + var hoverShape = Squircle.path(hoverX, hoverY, HOVER_SIZE, HOVER_SIZE, 4); 205 + g2.setColor(UIManager.getColor("Component.borderColor")); 206 + g2.fill(hoverShape); 207 + } 208 + 209 + // Icon (centered accounting for indicator space) 210 + var icon = tab.getTabIcon(); 211 + if (icon != null) { 212 + int iconX = INDICATOR_WIDTH + (getWidth() - INDICATOR_WIDTH - icon.getIconWidth()) / 2; 213 + int iconY = (getHeight() - icon.getIconHeight()) / 2; 214 + icon.paintIcon(this, g2, iconX, iconY); 215 + } 216 + 217 + g2.dispose(); 67 218 } 68 219 } 69 220 }
+13
src/main/java/app/pane/DockTab.java
··· 1 + package app.pane; 2 + 3 + import javax.swing.Icon; 4 + import javax.swing.JPanel; 5 + 6 + public abstract class DockTab extends JPanel 7 + { 8 + public abstract Icon getTabIcon(); 9 + 10 + public abstract String getTabName(); 11 + 12 + public void dispose() {} 13 + }
+54
src/main/java/app/pane/Pane.java
··· 1 + package app.pane; 2 + 3 + import java.awt.Graphics; 4 + import java.awt.Graphics2D; 5 + import java.awt.RenderingHints; 6 + 7 + import javax.swing.JPanel; 8 + import javax.swing.UIManager; 9 + 10 + import util.ui.Squircle; 11 + 12 + /** 13 + * A JPanel with squircle corners. 14 + */ 15 + public class Pane extends JPanel 16 + { 17 + private final int radius; 18 + 19 + public Pane(int radius) 20 + { 21 + this.radius = radius; 22 + setOpaque(false); 23 + setBackground(UIManager.getColor("TabbedPane.background")); 24 + } 25 + 26 + public Pane() 27 + { 28 + this(10); 29 + } 30 + 31 + @Override 32 + protected void paintComponent(Graphics g) 33 + { 34 + var g2 = (Graphics2D) g.create(); 35 + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 36 + 37 + var shape = Squircle.path(0, 0, getWidth(), getHeight(), radius); 38 + 39 + g2.setColor(getBackground()); 40 + g2.fill(shape); 41 + g2.dispose(); 42 + } 43 + 44 + @Override 45 + protected void paintChildren(Graphics g) 46 + { 47 + var g2 = (Graphics2D) g.create(); 48 + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 49 + 50 + g2.setClip(Squircle.path(0, 0, getWidth(), getHeight(), radius)); 51 + super.paintChildren(g2); 52 + g2.dispose(); 53 + } 54 + }
+2 -2
src/main/java/app/pane/explorer/AssetItem.java
··· 13 13 { 14 14 final AssetHandle asset; 15 15 16 - AssetItem(Explorer explorer, AssetHandle asset) 16 + AssetItem(Tab explorer, AssetHandle asset) 17 17 { 18 18 super(explorer, asset.getAssetName(), ThemedIcon.PACKAGE_24, false); 19 19 this.asset = asset; ··· 67 67 @Override 68 68 Transferable createDragTransferable() 69 69 { 70 - return new Explorer.AssetTransferable(asset); 70 + return new Tab.AssetTransferable(asset); 71 71 } 72 72 }
+1 -1
src/main/java/app/pane/explorer/DirectoryItem.java
··· 19 19 { 20 20 private final String targetPath; 21 21 22 - DirectoryItem(Explorer explorer, String name, String targetPath) 22 + DirectoryItem(Tab explorer, String name, String targetPath) 23 23 { 24 24 super(explorer, name, ThemedIcon.FOLDER_FILLED.derive(36, 36), false); 25 25 this.targetPath = targetPath;
+52 -20
src/main/java/app/pane/explorer/Explorer.java src/main/java/app/pane/explorer/Tab.java
··· 1 1 package app.pane.explorer; 2 2 3 3 import java.awt.Cursor; 4 - import java.awt.FlowLayout; 4 + import java.awt.Dimension; 5 5 import java.awt.Font; 6 6 import java.awt.datatransfer.DataFlavor; 7 7 import java.awt.datatransfer.Transferable; ··· 24 24 import java.util.ArrayList; 25 25 import java.util.List; 26 26 27 + import javax.swing.Icon; 27 28 import javax.swing.JLabel; 28 29 import javax.swing.JPanel; 29 30 import javax.swing.JScrollPane; 30 31 import javax.swing.SwingUtilities; 31 32 import javax.swing.UIManager; 33 + import javax.swing.border.EmptyBorder; 32 34 33 35 import app.Environment; 34 36 import app.SwingUtils; 37 + import app.pane.DockTab; 35 38 import assets.AssetHandle; 36 39 import assets.AssetManager; 37 40 import assets.AssetManager.DirectoryListing; 38 41 import net.miginfocom.swing.MigLayout; 39 42 import util.Logger; 43 + import util.ui.FadingScrollPane; 44 + import util.ui.ThemedIcon; 40 45 import util.ui.UniformGridLayout; 41 46 42 - public class Explorer extends JPanel 47 + public class Tab extends DockTab 43 48 { 44 49 private String currentPath = ""; 45 50 private Item selectedItem; 46 51 47 - private JPanel breadcrumbBar; 52 + private JPanel topBar; 53 + private JPanel breadcrumbsPanel; 54 + private SearchField searchField; 48 55 private JPanel resultsPanel; 49 56 private JScrollPane scrollPane; 50 57 ··· 52 59 private Thread watchThread; 53 60 private final List<WatchKey> watchKeys = new ArrayList<>(); 54 61 55 - public Explorer() 62 + public Tab() 56 63 { 57 - setLayout(new MigLayout("ins 4, fill", "[grow]", "[pref!][grow]")); 64 + setLayout(new MigLayout("ins 0, fill", "[grow]", "[pref!][grow]")); 58 65 59 - breadcrumbBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); 60 - add(breadcrumbBar, "growx, wrap"); 66 + // Set preferred size to fit exactly 2 rows of items 67 + // topBar (36) + border top (7) + 2 rows (160) + 1 vgap (1) + border bottom (12) = 216 68 + setPreferredSize(new Dimension(400, 216)); 69 + 70 + topBar = new JPanel(new MigLayout("h 36!, ins 5, fill, gap 8", "[grow][]", "[fill]")); 61 71 62 - resultsPanel = new JPanel(new UniformGridLayout(Item.SIZE, Item.SIZE, 0, 0)); 72 + breadcrumbsPanel = new JPanel(new MigLayout("ins 5 14 0 14, gap 0", "", "[center]")); 73 + topBar.add(breadcrumbsPanel); 74 + 75 + searchField = new SearchField(); 76 + topBar.add(searchField, "w 200!"); 77 + 78 + add(topBar, "growx, wrap"); 79 + 80 + resultsPanel = new JPanel(new UniformGridLayout(Item.SIZE, Item.SIZE, 1, 1)); 81 + resultsPanel.setBorder(new EmptyBorder(7, 12, 12, 12)); 63 82 resultsPanel.addMouseListener(new MouseAdapter() { 64 83 @Override 65 84 public void mouseClicked(MouseEvent e) ··· 67 86 clearSelection(); 68 87 } 69 88 }); 70 - scrollPane = new JScrollPane(resultsPanel); 71 - scrollPane.setBorder(null); 89 + 90 + scrollPane = new FadingScrollPane(resultsPanel); 72 91 add(scrollPane, "grow, push"); 73 92 74 93 try { ··· 127 146 128 147 private void rebuildBreadcrumb() 129 148 { 130 - breadcrumbBar.removeAll(); 149 + breadcrumbsPanel.removeAll(); 131 150 132 - String projectName = Environment.getProject().getManifest().getName(); 133 - breadcrumbBar.add(createBreadcrumbLabel(projectName, "")); 151 + String projectId = Environment.getProject().getManifest().getId(); 152 + breadcrumbsPanel.add(createBreadcrumbLabel(projectId, ""), "aligny baseline"); 134 153 135 154 if (!currentPath.isEmpty()) { 136 155 String[] parts = currentPath.split("/"); ··· 140 159 continue; 141 160 pathSoFar.append(part).append("/"); 142 161 143 - breadcrumbBar.add(createSeparatorLabel()); 144 - breadcrumbBar.add(createBreadcrumbLabel(part, pathSoFar.toString())); 162 + breadcrumbsPanel.add(createSeparatorLabel(), "aligny baseline"); 163 + breadcrumbsPanel.add(createBreadcrumbLabel(part, pathSoFar.toString()), "aligny baseline"); 145 164 } 146 165 } 147 166 148 167 if (selectedItem != null) { 149 - breadcrumbBar.add(createSeparatorLabel()); 168 + breadcrumbsPanel.add(createSeparatorLabel(), "aligny baseline"); 150 169 151 170 var fileLabel = new JLabel(selectedItem.name); 152 171 fileLabel.setFont(fileLabel.getFont().deriveFont(Font.BOLD)); 153 - breadcrumbBar.add(fileLabel); 172 + breadcrumbsPanel.add(fileLabel, "aligny baseline"); 154 173 } 155 174 156 - breadcrumbBar.revalidate(); 157 - breadcrumbBar.repaint(); 175 + breadcrumbsPanel.revalidate(); 176 + breadcrumbsPanel.repaint(); 158 177 } 159 178 160 179 private JLabel createBreadcrumbLabel(String text, String targetPath) ··· 177 196 { 178 197 if (dtde.isDataFlavorSupported(AssetHandle.FLAVOUR)) { 179 198 dtde.acceptDrag(DnDConstants.ACTION_MOVE); 180 - label.setFont(normalFont.deriveFont(Font.BOLD)); 199 + label.putClientProperty("FlatLaf.style", "font: semibold"); 181 200 } 182 201 else { 183 202 dtde.rejectDrag(); ··· 309 328 watchThread.start(); 310 329 } 311 330 331 + @Override 332 + public Icon getTabIcon() 333 + { 334 + return ThemedIcon.FOLDER_OPEN_24.derive(15, 15); 335 + } 336 + 337 + @Override 338 + public String getTabName() 339 + { 340 + return "Explorer"; 341 + } 342 + 343 + @Override 312 344 public void dispose() 313 345 { 314 346 if (watchThread != null)
+4 -3
src/main/java/app/pane/explorer/Item.java
··· 48 48 static final int PADDING = 3; 49 49 static final int SIZE = AssetHandle.THUMBNAIL_WIDTH + PADDING * 2; 50 50 51 - final Explorer explorer; 51 + final Tab explorer; 52 52 final String name; 53 53 54 54 private Icon icon; ··· 60 60 private static final JPopupMenu contextMenu = buildContextMenu(); 61 61 private static Item popupItem; 62 62 63 - Item(Explorer explorer, String name, Icon defaultIcon, boolean checkerboard) 63 + Item(Tab explorer, String name, Icon defaultIcon, boolean checkerboard) 64 64 { 65 65 this.explorer = explorer; 66 66 this.name = name; ··· 297 297 // Selected background 298 298 if (selected) { 299 299 g2.setColor(UIManager.getColor("Component.borderColor")); 300 - g2.fillRoundRect(0, 0, getWidth(), getHeight(), 8, 8); 300 + int arc = UIManager.getInt("Button.arc"); 301 + g2.fillRoundRect(0, 0, getWidth(), getHeight(), arc, arc); 301 302 } 302 303 303 304 Insets ins = getInsets();
+54
src/main/java/app/pane/explorer/SearchField.java
··· 1 + package app.pane.explorer; 2 + 3 + import java.awt.Graphics; 4 + import java.awt.Graphics2D; 5 + import java.awt.RenderingHints; 6 + 7 + import javax.swing.JTextField; 8 + import javax.swing.border.EmptyBorder; 9 + 10 + import app.pane.Pane; 11 + import net.miginfocom.swing.MigLayout; 12 + import util.ui.ThemedIcon; 13 + 14 + class SearchField extends Pane 15 + { 16 + private static final int ICON_SIZE = 15; 17 + private static final int ICON_PADDING = 8; 18 + 19 + private final JTextField textField; 20 + 21 + SearchField() 22 + { 23 + super(6); 24 + setLayout(new MigLayout("ins 0, fill", "[grow]", "[]")); 25 + 26 + textField = new JTextField(); 27 + textField.setOpaque(false); 28 + textField.setBorder(new EmptyBorder(4, 8, 4, ICON_SIZE + ICON_PADDING * 2)); 29 + textField.putClientProperty("JTextField.placeholderText", "Search..."); 30 + add(textField, "grow"); 31 + 32 + setBackground(textField.getBackground()); 33 + } 34 + 35 + @Override 36 + protected void paintChildren(Graphics g) 37 + { 38 + super.paintChildren(g); 39 + 40 + var g2 = (Graphics2D) g.create(); 41 + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 42 + 43 + int iconX = getWidth() - ICON_SIZE - ICON_PADDING; 44 + int iconY = (getHeight() - ICON_SIZE) / 2; 45 + 46 + ThemedIcon.SEARCH.derive(ICON_SIZE, ICON_SIZE).paintIcon(this, g2, iconX, iconY); 47 + g2.dispose(); 48 + } 49 + 50 + String getText() 51 + { 52 + return textField.getText(); 53 + } 54 + }
+54
src/main/java/app/pane/logs/Tab.java
··· 1 + package app.pane.logs; 2 + 3 + import javax.swing.BorderFactory; 4 + import javax.swing.Icon; 5 + import javax.swing.JScrollPane; 6 + import javax.swing.JTextArea; 7 + 8 + import app.pane.DockTab; 9 + import net.miginfocom.swing.MigLayout; 10 + import util.Logger; 11 + import util.ui.ThemedIcon; 12 + 13 + public class Tab extends DockTab 14 + { 15 + private final JTextArea textArea; 16 + private final Logger.Listener listener; 17 + 18 + public Tab() 19 + { 20 + textArea = new JTextArea(); 21 + textArea.setEditable(false); 22 + textArea.setBorder(BorderFactory.createEmptyBorder(4, 8, 4, 8)); 23 + 24 + var scrollPane = new JScrollPane(textArea); 25 + scrollPane.setBorder(null); 26 + 27 + setLayout(new MigLayout("fill, ins 0")); 28 + add(scrollPane, "grow"); 29 + 30 + listener = msg -> { 31 + textArea.append(msg.text + System.lineSeparator()); 32 + textArea.setCaretPosition(textArea.getDocument().getLength()); 33 + }; 34 + Logger.addListener(listener); 35 + } 36 + 37 + @Override 38 + public Icon getTabIcon() 39 + { 40 + return ThemedIcon.TERMINAL_24.derive(15, 15); 41 + } 42 + 43 + @Override 44 + public String getTabName() 45 + { 46 + return "Logs"; 47 + } 48 + 49 + @Override 50 + public void dispose() 51 + { 52 + Logger.removeListener(listener); 53 + } 54 + }
+122
src/main/java/util/ui/FadingScrollPane.java
··· 1 + package util.ui; 2 + 3 + import java.awt.Color; 4 + import java.awt.Component; 5 + import java.awt.Graphics; 6 + import java.awt.MouseInfo; 7 + import java.awt.Point; 8 + import java.awt.Rectangle; 9 + 10 + import javax.swing.JScrollPane; 11 + import javax.swing.Timer; 12 + import javax.swing.UIManager; 13 + 14 + /** 15 + * A JScrollPane with macOS-style scrollbars that fade in on hover and fade out when not in use. 16 + */ 17 + public class FadingScrollPane extends JScrollPane 18 + { 19 + private static final float FADE_SPEED = 0.1f; // alpha change per repaint 20 + private static final int CHECK_INTERVAL = 50; // milliseconds 21 + 22 + private float currentAlpha = 0.0f; 23 + private boolean lastMouseInside = false; 24 + 25 + public FadingScrollPane(Component view) 26 + { 27 + super(view); 28 + 29 + setBorder(null); 30 + setOpaque(false); 31 + getViewport().setOpaque(false); 32 + getViewport().setBackground(new Color(0, 0, 0, 0)); 33 + 34 + // Configure scrollbars - make everything transparent 35 + var vScrollBar = getVerticalScrollBar(); 36 + var hScrollBar = getHorizontalScrollBar(); 37 + 38 + vScrollBar.setOpaque(false); 39 + hScrollBar.setOpaque(false); 40 + vScrollBar.setBackground(new Color(0, 0, 0, 0)); 41 + hScrollBar.setBackground(new Color(0, 0, 0, 0)); 42 + vScrollBar.setBorder(null); 43 + hScrollBar.setBorder(null); 44 + vScrollBar.putClientProperty("JScrollBar.showButtons", false); 45 + hScrollBar.putClientProperty("JScrollBar.showButtons", false); 46 + 47 + // Set initial style with fully transparent scrollbar 48 + String initialStyle = "track: #00000000; thumb: #00000000"; 49 + vScrollBar.putClientProperty("FlatLaf.style", initialStyle); 50 + hScrollBar.putClientProperty("FlatLaf.style", initialStyle); 51 + 52 + putClientProperty("JScrollPane.smoothScrolling", true); 53 + 54 + // Periodically check mouse position and trigger repaints as needed 55 + new Timer(CHECK_INTERVAL, e -> { 56 + boolean mouseInside = isMouseInside(); 57 + if (mouseInside != lastMouseInside) { 58 + lastMouseInside = mouseInside; 59 + repaint(); 60 + } 61 + }).start(); 62 + } 63 + 64 + private boolean isMouseInside() 65 + { 66 + try { 67 + Point mousePos = MouseInfo.getPointerInfo().getLocation(); 68 + Point componentPos = getLocationOnScreen(); 69 + Rectangle bounds = new Rectangle(componentPos.x, componentPos.y, getWidth(), getHeight()); 70 + return bounds.contains(mousePos); 71 + } 72 + catch (Exception e) { 73 + return false; 74 + } 75 + } 76 + 77 + @Override 78 + public void paint(Graphics g) 79 + { 80 + updateScrollbarColors(); 81 + super.paint(g); 82 + } 83 + 84 + private void updateScrollbarColors() 85 + { 86 + float targetAlpha = isMouseInside() ? 1.0f : 0.0f; 87 + 88 + // Interpolate current alpha toward target 89 + if (currentAlpha < targetAlpha) { 90 + currentAlpha = Math.min(targetAlpha, currentAlpha + FADE_SPEED); 91 + } 92 + else if (currentAlpha > targetAlpha) { 93 + currentAlpha = Math.max(targetAlpha, currentAlpha - FADE_SPEED); 94 + } 95 + 96 + Color baseThumb = UIManager.getColor("ScrollBar.thumb"); 97 + if (baseThumb == null) { 98 + baseThumb = new Color(128, 128, 128); 99 + } 100 + 101 + // Apply alpha to thumb 102 + Color thumbColor = new Color( 103 + baseThumb.getRed(), 104 + baseThumb.getGreen(), 105 + baseThumb.getBlue(), 106 + (int) (currentAlpha * 255) 107 + ); 108 + 109 + String style = String.format( 110 + "track: #00000000; thumb: #%02x%02x%02x%02x", 111 + thumbColor.getRed(), thumbColor.getGreen(), thumbColor.getBlue(), thumbColor.getAlpha() 112 + ); 113 + 114 + getVerticalScrollBar().putClientProperty("FlatLaf.style", style); 115 + getHorizontalScrollBar().putClientProperty("FlatLaf.style", style); 116 + 117 + // Continue animation if not at target 118 + if (currentAlpha != targetAlpha) { 119 + repaint(); 120 + } 121 + } 122 + }
+67
src/main/java/util/ui/Squircle.java
··· 1 + package util.ui; 2 + 3 + import java.awt.geom.Path2D; 4 + 5 + public abstract class Squircle 6 + { 7 + public static Path2D.Double path(double x, double y, double w, double h, double arc) 8 + { 9 + var path = new Path2D.Double(); 10 + 11 + path.moveTo(x + w - arc, y); 12 + path.lineTo(x + arc, y); 13 + path.quadTo(x, y, x, y + arc); 14 + path.lineTo(x, y + h - arc); 15 + path.quadTo(x, y + h, x + arc, y + h); 16 + path.lineTo(x + w - arc, y + h); 17 + path.quadTo(x + w, y + h, x + w, y + h - arc); 18 + path.lineTo(x + w, y + arc); 19 + path.quadTo(x + w, y, x + w - arc, y); 20 + path.closePath(); 21 + 22 + return path; 23 + } 24 + 25 + public static Path2D.Double path(double x, double y, double w, double h, double topLeft, double topRight, double bottomRight, double bottomLeft) 26 + { 27 + var path = new Path2D.Double(); 28 + 29 + // Start at top edge after top-right corner 30 + path.moveTo(x + w - topRight, y); 31 + // Top edge to top-left corner 32 + if (topLeft > 0) { 33 + path.lineTo(x + topLeft, y); 34 + path.quadTo(x, y, x, y + topLeft); 35 + } 36 + else { 37 + path.lineTo(x, y); 38 + } 39 + // Left edge 40 + if (bottomLeft > 0) { 41 + path.lineTo(x, y + h - bottomLeft); 42 + path.quadTo(x, y + h, x + bottomLeft, y + h); 43 + } 44 + else { 45 + path.lineTo(x, y + h); 46 + } 47 + // Bottom edge 48 + if (bottomRight > 0) { 49 + path.lineTo(x + w - bottomRight, y + h); 50 + path.quadTo(x + w, y + h, x + w, y + h - bottomRight); 51 + } 52 + else { 53 + path.lineTo(x + w, y + h); 54 + } 55 + // Right edge 56 + if (topRight > 0) { 57 + path.lineTo(x + w, y + topRight); 58 + path.quadTo(x + w, y, x + w - topRight, y); 59 + } 60 + else { 61 + path.lineTo(x + w, y); 62 + } 63 + path.closePath(); 64 + 65 + return path; 66 + } 67 + }
+4
src/main/java/util/ui/ThemedIcon.java
··· 83 83 84 84 public static final FlatSVGIcon PACKAGE_24 = getIcon("package"); 85 85 public static final FlatSVGIcon PACKAGE_16 = PACKAGE_24.derive(16, 16); 86 + 87 + public static final FlatSVGIcon GIT_BRANCH = getIcon("git_branch"); 88 + 89 + public static final FlatSVGIcon SEARCH = getIcon("search"); 86 90 }
+16
src/main/resources/icon/git_branch.svg
··· 1 + <svg 2 + xmlns="http://www.w3.org/2000/svg" 3 + width="24" 4 + height="24" 5 + viewBox="0 0 24 24" 6 + fill="none" 7 + stroke="currentColor" 8 + stroke-width="2" 9 + stroke-linecap="round" 10 + stroke-linejoin="round" 11 + > 12 + <line x1="6" x2="6" y1="3" y2="15" /> 13 + <circle cx="18" cy="6" r="3" /> 14 + <circle cx="6" cy="18" r="3" /> 15 + <path d="M18 9a9 9 0 0 1-9 9" /> 16 + </svg>
+14
src/main/resources/icon/search.svg
··· 1 + <svg 2 + xmlns="http://www.w3.org/2000/svg" 3 + width="24" 4 + height="24" 5 + viewBox="0 0 24 24" 6 + fill="none" 7 + stroke="currentColor" 8 + stroke-width="2" 9 + stroke-linecap="round" 10 + stroke-linejoin="round" 11 + > 12 + <path d="m21 21-4.34-4.34" /> 13 + <circle cx="11" cy="11" r="8" /> 14 + </svg>