Skip to content

Commit fd2d0ac

Browse files
committed
feat: add interactive spacer panels between layer groups with add/move functionality
- Add LayerGroupSpacerPanel component with rollover "+" indicator - Support double-click to add layer to group above - Add right-click context menu with Add Layer, Move Group Up/Down options - Update ScoreTopComponent to insert spacer panels between layer group headers - Fix header panel indexing to account for interleaved header/spacer layout - Update layer group push up/down logic to move header+spacer pairs together
1 parent 2f39228 commit fd2d0ac

File tree

2 files changed

+202
-14
lines changed

2 files changed

+202
-14
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* blue - object composition environment for csound
3+
* Copyright (C) 2026
4+
* Steven Yi <stevenyi@gmail.com>
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU General Public License
8+
* as published by the Free Software Foundation; either version 2
9+
* of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program; if not, write to the Free Software
18+
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19+
*/
20+
package blue.ui.core.score;
21+
22+
import blue.score.Score;
23+
import blue.score.layers.LayerGroup;
24+
import java.awt.Color;
25+
import java.awt.Dimension;
26+
import java.awt.Graphics;
27+
import java.awt.Graphics2D;
28+
import java.awt.RenderingHints;
29+
import java.awt.event.MouseAdapter;
30+
import java.awt.event.MouseEvent;
31+
import javax.swing.JMenuItem;
32+
import javax.swing.JPanel;
33+
import javax.swing.JPopupMenu;
34+
import javax.swing.SwingUtilities;
35+
36+
/**
37+
* Interactive spacer panel placed below each LayerGroup header in the Score
38+
* Timeline. Shows a "+" circle on rollover; double-click adds a layer to the
39+
* group above; right-click offers Add Layer, Move Group Up, Move Group Down.
40+
*
41+
* @author stevenyi
42+
*/
43+
public class LayerGroupSpacerPanel extends JPanel {
44+
45+
private static final Color PLUS_COLOR = new Color(120, 120, 120);
46+
47+
private final LayerGroup<?> layerGroup;
48+
private final Score score;
49+
private boolean rollover = false;
50+
51+
public LayerGroupSpacerPanel(LayerGroup<?> layerGroup, Score score) {
52+
this.layerGroup = layerGroup;
53+
this.score = score;
54+
55+
int h = Score.SPACER;
56+
setPreferredSize(new Dimension(0, h));
57+
setMinimumSize(new Dimension(0, h));
58+
setMaximumSize(new Dimension(Integer.MAX_VALUE, h));
59+
setSize(getWidth(), h);
60+
setOpaque(false);
61+
62+
var mouseHandler = new MouseAdapter() {
63+
@Override
64+
public void mouseEntered(MouseEvent e) {
65+
rollover = true;
66+
repaint();
67+
}
68+
69+
@Override
70+
public void mouseExited(MouseEvent e) {
71+
rollover = false;
72+
repaint();
73+
}
74+
75+
@Override
76+
public void mouseClicked(MouseEvent e) {
77+
if (SwingUtilities.isLeftMouseButton(e)
78+
&& e.getClickCount() == 2) {
79+
addLayer();
80+
}
81+
}
82+
83+
@Override
84+
public void mousePressed(MouseEvent e) {
85+
maybeShowPopup(e);
86+
}
87+
88+
@Override
89+
public void mouseReleased(MouseEvent e) {
90+
maybeShowPopup(e);
91+
}
92+
93+
private void maybeShowPopup(MouseEvent e) {
94+
if (e.isPopupTrigger()) {
95+
showPopupMenu(e.getX(), e.getY());
96+
}
97+
}
98+
};
99+
100+
addMouseListener(mouseHandler);
101+
}
102+
103+
public LayerGroup<?> getLayerGroup() {
104+
return layerGroup;
105+
}
106+
107+
private void addLayer() {
108+
layerGroup.newLayerAt(layerGroup.size());
109+
}
110+
111+
private void showPopupMenu(int x, int y) {
112+
JPopupMenu popup = new JPopupMenu();
113+
114+
JMenuItem addLayerItem = new JMenuItem("Add Layer");
115+
addLayerItem.addActionListener(e -> addLayer());
116+
popup.add(addLayerItem);
117+
118+
int groupIndex = score.indexOf(layerGroup);
119+
120+
if (groupIndex > 0) {
121+
JMenuItem moveUpItem = new JMenuItem("Move Layer Group Up");
122+
moveUpItem.addActionListener(e -> {
123+
int idx = score.indexOf(layerGroup);
124+
if (idx > 0) {
125+
score.pushUpItems(idx, idx);
126+
}
127+
});
128+
popup.add(moveUpItem);
129+
}
130+
131+
if (groupIndex >= 0 && groupIndex < score.size() - 1) {
132+
JMenuItem moveDownItem = new JMenuItem("Move Layer Group Down");
133+
moveDownItem.addActionListener(e -> {
134+
int idx = score.indexOf(layerGroup);
135+
if (idx >= 0 && idx < score.size() - 1) {
136+
score.pushDownItems(idx, idx);
137+
}
138+
});
139+
popup.add(moveDownItem);
140+
}
141+
142+
popup.show(this, x, y);
143+
}
144+
145+
@Override
146+
protected void paintComponent(Graphics g) {
147+
super.paintComponent(g);
148+
149+
if (!rollover) {
150+
return;
151+
}
152+
153+
Graphics2D g2 = (Graphics2D) g.create();
154+
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
155+
RenderingHints.VALUE_ANTIALIAS_ON);
156+
157+
int cx = getWidth() / 2;
158+
int cy = getHeight() / 2;
159+
160+
// Draw subtle "+" sign
161+
g2.setColor(PLUS_COLOR);
162+
g2.setStroke(new java.awt.BasicStroke(1.5f));
163+
int armLen = 4;
164+
g2.drawLine(cx - armLen, cy, cx + armLen, cy);
165+
g2.drawLine(cx, cy - armLen, cx, cy + armLen);
166+
167+
g2.dispose();
168+
}
169+
}

blue-ui-core/src/main/java/blue/ui/core/score/ScoreTopComponent.java

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -686,12 +686,18 @@ private void addPanelsForLayerGroup(int index, LayerGroup layerGroup, TimeState
686686

687687
comp.putClientProperty("layerGroup", layerGroup);
688688

689+
final LayerGroupSpacerPanel spacer = new LayerGroupSpacerPanel(
690+
layerGroup, data.getScore());
691+
689692
if (index < 0 || index > layerPanel.getComponentCount() - 1) {
690693
layerPanel.add(comp);
691694
layerHeaderPanel.add(comp2);
695+
layerHeaderPanel.add(spacer);
692696
} else {
697+
int headerIndex = index * 2;
693698
layerPanel.add(comp, index);
694-
layerHeaderPanel.add(comp2, index);
699+
layerHeaderPanel.add(comp2, headerIndex);
700+
layerHeaderPanel.add(spacer, headerIndex + 1);
695701
}
696702

697703
final Dimension d = new Dimension(comp2.getWidth(), comp.getHeight());
@@ -717,10 +723,12 @@ public void componentResized(ComponentEvent e) {
717723
}
718724

719725
private void removePanelsForLayerGroups(int startIndex, int endIndex) {
726+
int headerStart = startIndex * 2;
720727
for (int i = 0; i <= endIndex - startIndex; i++) {
721-
Component comp = layerPanel.getComponent(startIndex);
722728
layerPanel.remove(startIndex);
723-
layerHeaderPanel.remove(startIndex);
729+
// Remove spacer first (at headerStart+1), then header (at headerStart)
730+
layerHeaderPanel.remove(headerStart + 1);
731+
layerHeaderPanel.remove(headerStart);
724732
}
725733
layerPanel.revalidate();
726734
layerPanel.repaint();
@@ -927,7 +935,7 @@ public void componentResized(ComponentEvent e) {
927935

928936
layerPanel.setOpaque(true);
929937
layerPanel.setLayout(new LinearLayout(Score.SPACER));
930-
layerHeaderPanel.setLayout(new LinearLayout(Score.SPACER));
938+
layerHeaderPanel.setLayout(new LinearLayout(0));
931939

932940
scorePanel.add(layerPanel, JLayeredPane.DEFAULT_LAYER);
933941

@@ -1515,31 +1523,42 @@ public void listChanged(ObservableListEvent<LayerGroup<? extends Layer>> evt) {
15151523
LayerGroup lGroup = (LayerGroup) c.getClientProperty("layerGroup");
15161524

15171525
if (layerGroups.get(1) == lGroup) {
1518-
// handle push down
1526+
// handle push down — move end item to start position
15191527
Component comp = layerPanel.getComponent(evt.getEndIndex());
15201528
layerPanel.remove(comp);
15211529
layerPanel.add(comp, evt.getStartIndex());
15221530

1523-
Component comp2 = layerHeaderPanel.getComponent(
1524-
evt.getEndIndex());
1525-
layerHeaderPanel.remove(comp2);
1526-
layerHeaderPanel.add(comp2, evt.getStartIndex());
1531+
// Move header+spacer pair from end to start
1532+
int srcHeader = evt.getEndIndex() * 2;
1533+
int dstHeader = evt.getStartIndex() * 2;
1534+
Component header = layerHeaderPanel.getComponent(srcHeader);
1535+
Component spacer = layerHeaderPanel.getComponent(srcHeader + 1);
1536+
layerHeaderPanel.remove(spacer);
1537+
layerHeaderPanel.remove(header);
1538+
layerHeaderPanel.add(header, dstHeader);
1539+
layerHeaderPanel.add(spacer, dstHeader + 1);
15271540

15281541
layerPanel.revalidate();
15291542
layerHeaderPanel.revalidate();
15301543

15311544
layerPanel.repaint();
15321545
layerHeaderPanel.repaint();
15331546
} else {
1534-
// handle push up
1547+
// handle push up — move start item to end position
15351548
Component comp = layerPanel.getComponent(evt.getStartIndex());
15361549
layerPanel.remove(comp);
15371550
layerPanel.add(comp, evt.getEndIndex());
15381551

1539-
Component comp2 = layerHeaderPanel.getComponent(
1540-
evt.getStartIndex());
1541-
layerHeaderPanel.remove(comp2);
1542-
layerHeaderPanel.add(comp2, evt.getEndIndex());
1552+
// Move header+spacer pair from start to end
1553+
int srcHeader = evt.getStartIndex() * 2;
1554+
int dstHeader = evt.getEndIndex() * 2;
1555+
Component header = layerHeaderPanel.getComponent(srcHeader);
1556+
Component spacer = layerHeaderPanel.getComponent(srcHeader + 1);
1557+
layerHeaderPanel.remove(spacer);
1558+
layerHeaderPanel.remove(header);
1559+
// After removing 2 components, destination shifts down by 2
1560+
layerHeaderPanel.add(header, dstHeader - 2);
1561+
layerHeaderPanel.add(spacer, dstHeader - 1);
15431562

15441563
layerPanel.revalidate();
15451564
layerHeaderPanel.revalidate();

0 commit comments

Comments
 (0)