New Desktop Icons

Some new Functionality to record more than 1 camera using the desktop icon.

open local files using desktop,
open profiler (nothing changed)

Signed-off-by: rattatwinko <seppmutterman@gmail.com>
This commit is contained in:
2026-02-09 12:23:37 +01:00
parent e225d8f0bc
commit 57ee4d9a92
11 changed files with 411 additions and 70 deletions

View File

@@ -2,19 +2,25 @@ package io.swtc;
import com.github.sarxos.webcam.Webcam; import com.github.sarxos.webcam.Webcam;
import io.swtc.proccessing.ui.IconSetter; import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.proccessing.ui.desktop.DIM; import io.swtc.proccessing.ui.desktop.DIM;
import io.swtc.proccessing.ui.desktop.debug.Profiler; import io.swtc.proccessing.ui.desktop.debug.Profiler;
import io.swtc.proccessing.ui.desktop.evidence.EvidenceExportFrame; import io.swtc.proccessing.ui.desktop.evidence.EvidenceExportFrame;
import io.swtc.proccessing.ui.desktop.recording.MultiRecordingFrame;
import io.swtc.proccessing.ui.iframe.*; import io.swtc.proccessing.ui.iframe.*;
import javax.swing.*; import javax.swing.*;
import java.awt.*; import java.awt.*;
import java.awt.event.ActionEvent; import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import static java.awt.SystemColor.desktop;
public class SwingIFrame { public class SwingIFrame {
private final JFrame mainFrame; private final JFrame mainFrame;
private final DesktopPane desktopPane; private final DesktopPane desktopPane;
@@ -43,6 +49,8 @@ public class SwingIFrame {
desktopIconManager = new DIM(desktopPane); desktopIconManager = new DIM(desktopPane);
setupDesktopExportFrame(); setupDesktopExportFrame();
setupRecordingFrame();
setupFileEx();
setupProfiler(); setupProfiler();
setupFullscreenToggle(); setupFullscreenToggle();
@@ -61,6 +69,45 @@ public class SwingIFrame {
); );
} }
private void setupRecordingFrame() {
desktopIconManager.addIcon(
"Record Batch",
IconSetter.getCamerarec_ImageIcon(),
() -> {
MultiRecordingFrame mrf = new MultiRecordingFrame();
desktopPane.add(mrf);
mrf.show();
try {
mrf.setSelected(true);
} catch (java.beans.PropertyVetoException e) {
ShowError.error(null,"Exception", "" + e.getStackTrace());
}
}
);
}
private void setupFileEx() {
desktopIconManager.addIcon(
"Open Recordings",
IconSetter.getExplorerIcon(),
() -> {
String userHome = System.getProperty("user.home");
File folder = new File(userHome,"Videos/swtcctv-rec");
if (Desktop.isDesktopSupported() && folder.exists()) {
try {
Desktop.getDesktop().open(folder);
} catch (IOException e) {
ShowError.error(null,
"Failed to open Folder",
"Failed" + e.getMessage()
);
}
}
}
);
}
private void setupProfiler() { private void setupProfiler() {
desktopIconManager.addIcon( desktopIconManager.addIcon(
"Profiler", "Profiler",
@@ -72,6 +119,7 @@ public class SwingIFrame {
}); });
} }
public void addCameraInternalFrame(Webcam webcam) { public void addCameraInternalFrame(Webcam webcam) {
CameraInternalFrame cameraFrame = new CameraInternalFrame(webcam, this::handleEffectsRequest); CameraInternalFrame cameraFrame = new CameraInternalFrame(webcam, this::handleEffectsRequest);

View File

@@ -12,6 +12,9 @@ public class IconSetter {
private static ImageIcon ICON_ICON; private static ImageIcon ICON_ICON;
private static Image effects_icon; private static Image effects_icon;
private static ImageIcon dbg_icon; private static ImageIcon dbg_icon;
private static Image camerarec;
private static ImageIcon camerarec_imgico;
private static ImageIcon expIcon;
/* this is used for the app icon itself (the one in tb) */ /* this is used for the app icon itself (the one in tb) */
public static Image getIcon() { public static Image getIcon() {
@@ -73,4 +76,40 @@ public class IconSetter {
} }
return dbg_icon; return dbg_icon;
} }
public static Image getCamerarec_img() {
if (Objects.isNull(camerarec)) {
URL url = IconSetter.class.getResource("/icons/rec.png");
if (Objects.isNull(url)) {
ShowError.error(null,"icon","recicon was null Type Image");
throw new RuntimeException("NULL!");
}
camerarec = Toolkit.getDefaultToolkit().getImage(url);
}
return camerarec;
}
public static ImageIcon getCamerarec_ImageIcon() {
if (Objects.isNull(camerarec_imgico)) {
URL url = IconSetter.class.getResource("/icons/rec.png");
if (Objects.isNull(url)) {
ShowError.error(null,"icon","getCamerarec_ImageIcon failed, Type Image");
throw new RuntimeException("NULL!");
}
camerarec_imgico = new ImageIcon(url);
}
return camerarec_imgico;
}
public static ImageIcon getExplorerIcon() {
if (Objects.isNull(expIcon)) {
URL url = IconSetter.class.getResource("/icons/explorer.png");
if (Objects.isNull(url)) {
ShowError.error(null,"icon","getExplorerIcon failed, Type Image");
throw new RuntimeException("NULL!");
}
expIcon = new ImageIcon(url);
}
return expIcon;
}
} }

View File

@@ -1,35 +1,51 @@
package io.swtc.proccessing.ui.desktop; package io.swtc.proccessing.ui.desktop;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.awt.*; import java.awt.*;
import java.awt.event.MouseAdapter; import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.awt.geom.RoundRectangle2D;
public class DesktopIcon extends JPanel { public class DesktopIcon extends JPanel {
private static final int ICON_SIZE = 64;
private static final int TOTAL_WIDTH = 100;
private static final int TOTAL_HEIGHT = 85;
private boolean hovered = false; private boolean hovered = false;
private final JLabel iconLabel;
private final JLabel textLabel;
public DesktopIcon(String label, Icon icon, Runnable action) { public DesktopIcon(String label, Icon icon, Runnable action) {
setLayout(new BorderLayout(0, 2));
setLayout(new BorderLayout(4, 4));
setOpaque(false); setOpaque(false);
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
if (icon instanceof ImageIcon) {
Image img = ((ImageIcon) icon).getImage();
Image scaled = img.getScaledInstance(64, 64, Image.SCALE_SMOOTH);
icon = new ImageIcon(scaled);
}
iconLabel = new JLabel(icon, SwingConstants.CENTER); Dimension fixedSize = new Dimension(TOTAL_WIDTH, TOTAL_HEIGHT);
textLabel = new ShadowLabel(label); setPreferredSize(fixedSize);
setMinimumSize(fixedSize);
setMaximumSize(fixedSize);
setBorder(new EmptyBorder(4, 4, 4, 4));
Icon scaledIcon = (icon instanceof ImageIcon)
? new SmoothIcon(((ImageIcon) icon).getImage(), ICON_SIZE, ICON_SIZE)
: icon;
JLabel iconLabel = new JLabel(scaledIcon, SwingConstants.CENTER);
iconLabel.setVerticalAlignment(SwingConstants.BOTTOM);
JLabel textLabel = new ShadowLabel(label);
textLabel.setHorizontalAlignment(SwingConstants.CENTER); textLabel.setHorizontalAlignment(SwingConstants.CENTER);
textLabel.setVerticalAlignment(SwingConstants.TOP);
add(iconLabel, BorderLayout.CENTER); add(iconLabel, BorderLayout.CENTER);
add(textLabel, BorderLayout.SOUTH); add(textLabel, BorderLayout.SOUTH);
setupMouseListeners(action);
}
private void setupMouseListeners(Runnable action) {
addMouseListener(new MouseAdapter() { addMouseListener(new MouseAdapter() {
@Override @Override
public void mouseClicked(MouseEvent e) { public void mouseClicked(MouseEvent e) {
@@ -52,56 +68,35 @@ public class DesktopIcon extends JPanel {
}); });
} }
@Override
public Dimension getPreferredSize() {
Dimension icon = iconLabel.getPreferredSize();
Dimension text = textLabel.getPreferredSize();
int w = Math.max(icon.width, text.width) + 12;
int h = icon.height + text.height + 12;
return new Dimension(w, h);
}
@Override @Override
protected void paintComponent(Graphics g) { protected void paintComponent(Graphics g) {
if (hovered) { if (hovered) {
Graphics2D g2 = (Graphics2D) g.create(); paintHoverEffect(g);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
boolean lightBg = isBackgroundLight();
Color fill = lightBg
? new Color(0, 0, 0, 30)
: new Color(255, 255, 255, 40);
Color border = lightBg
? new Color(0, 0, 0, 80)
: new Color(255, 255, 255, 100);
g2.setColor(fill);
g2.fillRoundRect(2, 2, getWidth() - 4, getHeight() - 4, 10, 10);
g2.setColor(border);
g2.drawRoundRect(2, 2, getWidth() - 5, getHeight() - 5, 10, 10);
g2.dispose();
} }
super.paintComponent(g); super.paintComponent(g);
} }
private boolean isBackgroundLight() { private void paintHoverEffect(Graphics g) {
Container p = getParent(); Graphics2D g2 = (Graphics2D) g.create();
if (p == null) return true; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Color bg = p.getBackground(); boolean lightBg = isBackgroundLight();
int luminance = (int) ( Color fill = lightBg ? new Color(0, 0, 0, 20) : new Color(255, 255, 255, 30);
bg.getRed() * 0.299 + Color border = lightBg ? new Color(0, 0, 0, 50) : new Color(255, 255, 255, 70);
bg.getGreen() * 0.587 +
bg.getBlue() * 0.114 g2.setColor(fill);
); g2.fill(new RoundRectangle2D.Float(2, 2, getWidth() - 4, getHeight() - 4, 10, 10));
return luminance > 180;
g2.setColor(border);
g2.draw(new RoundRectangle2D.Float(2, 2, getWidth() - 4, getHeight() - 4, 10, 10));
g2.dispose();
}
public boolean isBackgroundLight() {
Container p = getParent();
Color bg = (p != null) ? p.getBackground() : Color.WHITE;
double luminance = (0.299 * bg.getRed() + 0.587 * bg.getGreen() + 0.114 * bg.getBlue()) / 255;
return luminance > 0.6;
} }
} }

View File

@@ -5,28 +5,44 @@ import java.awt.*;
public class ShadowLabel extends JLabel { public class ShadowLabel extends JLabel {
public ShadowLabel(String text) { public ShadowLabel(String text) {
super(text, SwingConstants.CENTER); super(text);
setForeground(Color.WHITE); setForeground(Color.WHITE);
setPreferredSize(new Dimension(15, 20));
} }
@Override @Override
protected void paintComponent(Graphics g) { protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g.create();
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
FontMetrics fm = g2d.getFontMetrics();
String text = getText(); String text = getText();
if (text == null || text.isEmpty()) return;
int x = (getWidth() - fm.stringWidth(text)) / 2; Graphics2D g2 = (Graphics2D) g;
int y = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent(); g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setColor(new Color(0, 0, 0, 200)); FontMetrics fm = g2.getFontMetrics();
g2d.drawString(text, x + 1, y + 1); int availableWidth = getWidth();
g2d.setColor(getForeground()); String drawText = text;
g2d.drawString(text, x, y); if (fm.stringWidth(text) > availableWidth) {
for (int i = text.length(); i > 0; i--) {
String temp = text.substring(0, i) + "...";
if (fm.stringWidth(temp) <= availableWidth) {
drawText = temp;
break;
}
}
}
DesktopIcon parent = (DesktopIcon) getParent();
boolean isLightBg = (parent != null) && parent.isBackgroundLight();
g2d.dispose(); Color textColor = isLightBg ? Color.BLACK : Color.WHITE;
Color shadowColor = isLightBg ? new Color(255, 255, 255, 200) : new Color(0, 0, 0, 180);
int x = (availableWidth - fm.stringWidth(drawText)) / 2;
int y = fm.getAscent();
g2.setColor(shadowColor);
g2.drawString(drawText, x + 1, y + 1);
g2.setColor(textColor);
g2.drawString(drawText, x, y);
} }
} }

View File

@@ -0,0 +1,28 @@
package io.swtc.proccessing.ui.desktop;
import javax.swing.*;
import java.awt.*;
public record SmoothIcon(Image image, int width, int height) implements Icon {
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2.drawImage(image, x, y, width, height, null);
g2.dispose();
}
@Override
public int getIconWidth() {
return width;
}
@Override
public int getIconHeight() {
return height;
}
}

View File

@@ -0,0 +1,13 @@
package io.swtc.proccessing.ui.desktop.recording;
import io.swtc.proccessing.ui.iframe.CameraInternalFrame;
public class CameraCheckItem {
private final CameraInternalFrame frame;
private boolean selected = true;
public CameraCheckItem(CameraInternalFrame frame) { this.frame = frame; }
public CameraInternalFrame getFrame() { return frame; }
public boolean isSelected() { return selected; }
public void setSelected(boolean s) { this.selected = s; }
@Override public String toString() { return frame.getTitle(); }
}

View File

@@ -0,0 +1,17 @@
package io.swtc.proccessing.ui.desktop.recording;
import javax.swing.*;
import java.awt.*;
public class CheckBoxListRenderer extends JCheckBox implements ListCellRenderer<CameraCheckItem> {
public CheckBoxListRenderer() { setOpaque(true); }
@Override
public Component getListCellRendererComponent(JList<? extends CameraCheckItem> list, CameraCheckItem value, int index, boolean isSel, boolean cellHasFocus) {
setSelected(value.isSelected());
setText(value.toString());
setBackground(isSel ? list.getSelectionBackground() : list.getBackground());
setForeground(isSel ? list.getSelectionForeground() : list.getForeground());
setEnabled(list.isEnabled());
return this;
}
}

View File

@@ -0,0 +1,183 @@
package io.swtc.proccessing.ui.desktop.recording;
import io.swtc.proccessing.CameraPanel;
import io.swtc.proccessing.ui.IconSetter;
import io.swtc.proccessing.ui.ShowError;
import io.swtc.proccessing.ui.iframe.CameraInternalFrame;
import io.swtc.recording.cv.AVRecorder;
import io.swtc.recording.cv.Quality;
import io.swtc.recording.cv.RecorderConfig;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class MultiRecordingFrame extends JInternalFrame {
private final DefaultListModel<CameraCheckItem> listModel = new DefaultListModel<>();
private final JList<CameraCheckItem> cameraList = new JList<>(listModel);
private final List<AVRecorder> activeRecorders = new ArrayList<>();
private boolean isRecording = false;
private JButton toggleBtn;
private JComboBox<Quality> globalQualityCombo;
private JLabel statusSummaryLabel;
public MultiRecordingFrame() {
super("Record Batch", true, true, false, true);
Image ico = IconSetter.getCamerarec_img();
setFrameIcon(new ImageIcon(ico));
setupUI();
setSize(350, 400);
}
private void setupUI() {
setLayout(new BorderLayout(10, 10));
((JPanel)getContentPane()).setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
statusSummaryLabel = new JLabel("Ready", SwingConstants.CENTER);
JPanel settingsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
settingsPanel.setBorder(BorderFactory.createTitledBorder("Global Encoding"));
globalQualityCombo = new JComboBox<>(Quality.values());
globalQualityCombo.setSelectedItem(Quality.VERYFAST);
settingsPanel.add(new JLabel("CPU Preset:"));
settingsPanel.add(globalQualityCombo);
cameraList.setCellRenderer(new CheckBoxListRenderer());
cameraList.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent e) {
if (isRecording) return;
int index = cameraList.locationToIndex(e.getPoint());
if (index != -1) {
CameraCheckItem item = listModel.getElementAt(index);
item.setSelected(!item.isSelected());
cameraList.repaint(cameraList.getCellBounds(index, index));
}
}
});
toggleBtn = new JButton("Start Batch Recording");
toggleBtn.addActionListener(e -> handleToggleAction());
JButton refreshBtn = new JButton("Refresh List");
refreshBtn.addActionListener(e -> refreshCameraList());
JPanel actionPanel = new JPanel(new GridLayout(2, 1, 5, 5));
actionPanel.add(refreshBtn);
actionPanel.add(toggleBtn);
JPanel southPanel = new JPanel(new BorderLayout(5, 5));
southPanel.add(statusSummaryLabel, BorderLayout.NORTH);
southPanel.add(actionPanel, BorderLayout.SOUTH);
// Add components to frame
add(settingsPanel, BorderLayout.NORTH);
add(new JScrollPane(cameraList), BorderLayout.CENTER);
add(southPanel, BorderLayout.SOUTH);
}
private void startRecordingProcess() {
List<CameraInternalFrame> selectedFrames = new ArrayList<>();
for (int i = 0; i < listModel.size(); i++) {
CameraCheckItem item = listModel.get(i);
if (item.isSelected()) selectedFrames.add(item.getFrame());
}
if (selectedFrames.isEmpty()) {
ShowError.warning(this,"Select 1 Camera at minimum","Selection");
return;
}
Quality quality = (Quality) globalQualityCombo.getSelectedItem();
String preset = (quality != null) ? quality.getFFmpegValue() : "superfast";
File videoDir = new File(System.getProperty("user.home"), "Videos/swtcctv-rec");
if (!videoDir.exists()) videoDir.mkdirs();
for (CameraInternalFrame frame : selectedFrames) {
try {
CameraPanel panel = frame.getCameraPanel();
BufferedImage sample = panel.getCurrentProcessedImage();
if (sample == null) continue;
File outputFile = new File(videoDir, "(" + frame.getTitle() + ") batch " + System.currentTimeMillis() + ".mp4");
RecorderConfig config = new RecorderConfig(outputFile, sample.getWidth(), sample.getHeight(), 20, 18, preset);
AVRecorder recorder = new AVRecorder(config);
recorder.start();
panel.setExternalRecorder(recorder);
activeRecorders.add(recorder);
} catch (Exception e) {
System.err.println("Failed to start recorder for: " + frame.getTitle());
}
}
isRecording = true;
updateUIState(true);
}
private void stopRecordingProcess() {
for (AVRecorder recorder : activeRecorders) {
recorder.stop();
}
activeRecorders.clear();
// Clear references from panels
for (int i = 0; i < listModel.size(); i++) {
listModel.get(i).getFrame().getCameraPanel().setExternalRecorder(null);
}
isRecording = false;
updateUIState(false);
}
private void handleToggleAction() {
if (!isRecording) startRecordingProcess();
else stopRecordingProcess();
}
private void refreshCameraList() {
if (isRecording) return;
listModel.clear();
JDesktopPane desktop = getDesktopPane();
if (desktop == null) return;
for (JInternalFrame f : desktop.getAllFrames()) {
if (f instanceof CameraInternalFrame camFrame) {
listModel.addElement(new CameraCheckItem(camFrame));
}
}
if (statusSummaryLabel != null) {
statusSummaryLabel.setText("Total" + listModel.size() + " Cameras");
}
}
private void updateUIState(boolean recordingActive) {
globalQualityCombo.setEnabled(!recordingActive);
cameraList.setEnabled(!recordingActive);
if (recordingActive) {
toggleBtn.setText("Stop all");
statusSummaryLabel.setText("Active");
} else {
toggleBtn.setText("Start Batch Recording");
toggleBtn.setBackground(null);
toggleBtn.setForeground(null);
statusSummaryLabel.setText("Status: Ready");
statusSummaryLabel.setForeground(null);
}
}
@Override
public void addNotify() {
super.addNotify();
refreshCameraList();
}
}

View File

@@ -29,6 +29,7 @@ public class RecordingFrame extends JInternalFrame {
private File currentFile; private File currentFile;
private final Timer statsTimer; private final Timer statsTimer;
private long startTime; private long startTime;
private String camName;
private final StringBuilder sb = new StringBuilder(32); private final StringBuilder sb = new StringBuilder(32);
@@ -41,6 +42,7 @@ public class RecordingFrame extends JInternalFrame {
setFrameIcon(new ImageIcon(ico)); setFrameIcon(new ImageIcon(ico));
this.cameraPanel = cameraPanel; this.cameraPanel = cameraPanel;
this.camName = cameraName;
initializeUI(); initializeUI();
@@ -131,7 +133,7 @@ public class RecordingFrame extends JInternalFrame {
} }
try { try {
currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4"); currentFile = new File(outputDirectory, "(" + this.camName + ") " + "vid_" + System.currentTimeMillis() + ".mp4");
Quality selected = (Quality) presetCombo.getSelectedItem(); Quality selected = (Quality) presetCombo.getSelectedItem();
String preset = (selected != null) ? selected.getFFmpegValue() : "superfast"; String preset = (selected != null) ? selected.getFFmpegValue() : "superfast";

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB