diff --git a/src/main/java/io/swtc/SwingIFrame.java b/src/main/java/io/swtc/SwingIFrame.java index bd41aeb..b3f1065 100644 --- a/src/main/java/io/swtc/SwingIFrame.java +++ b/src/main/java/io/swtc/SwingIFrame.java @@ -2,19 +2,25 @@ package io.swtc; import com.github.sarxos.webcam.Webcam; 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.debug.Profiler; import io.swtc.proccessing.ui.desktop.evidence.EvidenceExportFrame; +import io.swtc.proccessing.ui.desktop.recording.MultiRecordingFrame; import io.swtc.proccessing.ui.iframe.*; import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.io.File; +import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import static java.awt.SystemColor.desktop; + public class SwingIFrame { private final JFrame mainFrame; private final DesktopPane desktopPane; @@ -43,6 +49,8 @@ public class SwingIFrame { desktopIconManager = new DIM(desktopPane); setupDesktopExportFrame(); + setupRecordingFrame(); + setupFileEx(); setupProfiler(); 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() { desktopIconManager.addIcon( "Profiler", @@ -72,6 +119,7 @@ public class SwingIFrame { }); } + public void addCameraInternalFrame(Webcam webcam) { CameraInternalFrame cameraFrame = new CameraInternalFrame(webcam, this::handleEffectsRequest); diff --git a/src/main/java/io/swtc/proccessing/ui/IconSetter.java b/src/main/java/io/swtc/proccessing/ui/IconSetter.java index 7347f2a..c985e26 100644 --- a/src/main/java/io/swtc/proccessing/ui/IconSetter.java +++ b/src/main/java/io/swtc/proccessing/ui/IconSetter.java @@ -12,6 +12,9 @@ public class IconSetter { private static ImageIcon ICON_ICON; private static Image effects_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) */ public static Image getIcon() { @@ -73,4 +76,40 @@ public class IconSetter { } 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; + } } diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/DesktopIcon.java b/src/main/java/io/swtc/proccessing/ui/desktop/DesktopIcon.java index 8ac62e8..5a0265c 100644 --- a/src/main/java/io/swtc/proccessing/ui/desktop/DesktopIcon.java +++ b/src/main/java/io/swtc/proccessing/ui/desktop/DesktopIcon.java @@ -1,35 +1,51 @@ package io.swtc.proccessing.ui.desktop; import javax.swing.*; +import javax.swing.border.EmptyBorder; import java.awt.*; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.geom.RoundRectangle2D; 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 final JLabel iconLabel; - private final JLabel textLabel; - public DesktopIcon(String label, Icon icon, Runnable action) { - - setLayout(new BorderLayout(4, 4)); + setLayout(new BorderLayout(0, 2)); 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); - textLabel = new ShadowLabel(label); + Dimension fixedSize = new Dimension(TOTAL_WIDTH, TOTAL_HEIGHT); + 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.setVerticalAlignment(SwingConstants.TOP); add(iconLabel, BorderLayout.CENTER); add(textLabel, BorderLayout.SOUTH); + setupMouseListeners(action); + } + + private void setupMouseListeners(Runnable action) { addMouseListener(new MouseAdapter() { @Override 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 protected void paintComponent(Graphics g) { if (hovered) { - Graphics2D g2 = (Graphics2D) g.create(); - 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(); + paintHoverEffect(g); } - super.paintComponent(g); } - private boolean isBackgroundLight() { - Container p = getParent(); - if (p == null) return true; + private void paintHoverEffect(Graphics g) { + Graphics2D g2 = (Graphics2D) g.create(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - Color bg = p.getBackground(); - int luminance = (int) ( - bg.getRed() * 0.299 + - bg.getGreen() * 0.587 + - bg.getBlue() * 0.114 - ); - return luminance > 180; + boolean lightBg = isBackgroundLight(); + Color fill = lightBg ? new Color(0, 0, 0, 20) : new Color(255, 255, 255, 30); + Color border = lightBg ? new Color(0, 0, 0, 50) : new Color(255, 255, 255, 70); + + g2.setColor(fill); + g2.fill(new RoundRectangle2D.Float(2, 2, getWidth() - 4, getHeight() - 4, 10, 10)); + + 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; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/ShadowLabel.java b/src/main/java/io/swtc/proccessing/ui/desktop/ShadowLabel.java index 6e5075f..ca9f5e0 100644 --- a/src/main/java/io/swtc/proccessing/ui/desktop/ShadowLabel.java +++ b/src/main/java/io/swtc/proccessing/ui/desktop/ShadowLabel.java @@ -5,28 +5,44 @@ import java.awt.*; public class ShadowLabel extends JLabel { public ShadowLabel(String text) { - super(text, SwingConstants.CENTER); + super(text); setForeground(Color.WHITE); - setPreferredSize(new Dimension(15, 20)); } @Override 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(); + if (text == null || text.isEmpty()) return; - int x = (getWidth() - fm.stringWidth(text)) / 2; - int y = ((getHeight() - fm.getHeight()) / 2) + fm.getAscent(); + Graphics2D g2 = (Graphics2D) g; + g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - g2d.setColor(new Color(0, 0, 0, 200)); - g2d.drawString(text, x + 1, y + 1); + FontMetrics fm = g2.getFontMetrics(); + int availableWidth = getWidth(); - g2d.setColor(getForeground()); - g2d.drawString(text, x, y); + String drawText = text; + 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); } } \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/SmoothIcon.java b/src/main/java/io/swtc/proccessing/ui/desktop/SmoothIcon.java new file mode 100644 index 0000000..8ba9188 --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/SmoothIcon.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/recording/CameraCheckItem.java b/src/main/java/io/swtc/proccessing/ui/desktop/recording/CameraCheckItem.java new file mode 100644 index 0000000..647522c --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/recording/CameraCheckItem.java @@ -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(); } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/recording/CheckBoxListRenderer.java b/src/main/java/io/swtc/proccessing/ui/desktop/recording/CheckBoxListRenderer.java new file mode 100644 index 0000000..f661fdf --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/recording/CheckBoxListRenderer.java @@ -0,0 +1,17 @@ +package io.swtc.proccessing.ui.desktop.recording; + +import javax.swing.*; +import java.awt.*; + +public class CheckBoxListRenderer extends JCheckBox implements ListCellRenderer { + public CheckBoxListRenderer() { setOpaque(true); } + @Override + public Component getListCellRendererComponent(JList 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; + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/desktop/recording/MultiRecordingFrame.java b/src/main/java/io/swtc/proccessing/ui/desktop/recording/MultiRecordingFrame.java new file mode 100644 index 0000000..8ce808d --- /dev/null +++ b/src/main/java/io/swtc/proccessing/ui/desktop/recording/MultiRecordingFrame.java @@ -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 listModel = new DefaultListModel<>(); + private final JList cameraList = new JList<>(listModel); + + private final List activeRecorders = new ArrayList<>(); + private boolean isRecording = false; + + private JButton toggleBtn; + private JComboBox 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 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(); + } +} \ No newline at end of file diff --git a/src/main/java/io/swtc/proccessing/ui/iframe/RecordingFrame.java b/src/main/java/io/swtc/proccessing/ui/iframe/RecordingFrame.java index 234ebc6..6e822be 100644 --- a/src/main/java/io/swtc/proccessing/ui/iframe/RecordingFrame.java +++ b/src/main/java/io/swtc/proccessing/ui/iframe/RecordingFrame.java @@ -29,6 +29,7 @@ public class RecordingFrame extends JInternalFrame { private File currentFile; private final Timer statsTimer; private long startTime; + private String camName; private final StringBuilder sb = new StringBuilder(32); @@ -41,6 +42,7 @@ public class RecordingFrame extends JInternalFrame { setFrameIcon(new ImageIcon(ico)); this.cameraPanel = cameraPanel; + this.camName = cameraName; initializeUI(); @@ -131,7 +133,7 @@ public class RecordingFrame extends JInternalFrame { } try { - currentFile = new File(outputDirectory, "vid_" + System.currentTimeMillis() + ".mp4"); + currentFile = new File(outputDirectory, "(" + this.camName + ") " + "vid_" + System.currentTimeMillis() + ".mp4"); Quality selected = (Quality) presetCombo.getSelectedItem(); String preset = (selected != null) ? selected.getFFmpegValue() : "superfast"; diff --git a/src/main/resources/icons/explorer.png b/src/main/resources/icons/explorer.png new file mode 100644 index 0000000..c16f50b Binary files /dev/null and b/src/main/resources/icons/explorer.png differ diff --git a/src/main/resources/icons/rec.png b/src/main/resources/icons/rec.png new file mode 100644 index 0000000..4bafd23 Binary files /dev/null and b/src/main/resources/icons/rec.png differ