浏览代码

Merge pull request #672 from jMonkeyEngine/copilot/fix-667

Add animation support to material editor for animated materials
Rickard Edén 9 小时之前
父节点
当前提交
9996d34239

+ 1 - 1
.gitignore

@@ -29,4 +29,4 @@ jdks/*.bin
 jdks/*.exe
 jdks/*.zip
 dist/
-/.nb-gradle/
+/.nb-gradle/MATERIAL_EDITOR_CHANGES.md

+ 31 - 0
jme3-materialeditor/src/com/jme3/gde/materialdefinition/editor/MatPanel.java

@@ -52,6 +52,7 @@ import javax.swing.Timer;
 public class MatPanel extends JPanel implements MouseListener, ComponentListener {
     private final MaterialPreviewRenderer renderer;
     private Material mat;
+    private boolean animationEnabled = false;
     /**
      * Creates new form PreviewPanel
      */
@@ -64,6 +65,7 @@ public class MatPanel extends JPanel implements MouseListener, ComponentListener
     }
     
     public void cleanup() {
+        animationTimer.stop();
         renderer.cleanUp();        
     }
     
@@ -366,6 +368,15 @@ public class MatPanel extends JPanel implements MouseListener, ComponentListener
         }
     });
     
+    private final Timer animationTimer = new Timer(20, new ActionListener() {
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            if (animationEnabled && mat != null) {
+                renderer.refreshOnly();
+            }
+        }
+    });
+    
     @Override
     public void mouseExited(MouseEvent e) {
         t.restart();
@@ -399,4 +410,24 @@ public class MatPanel extends JPanel implements MouseListener, ComponentListener
             renderer.showMaterial(mat);
         }
     }
+    
+    /**
+     * Enable or disable animation rendering
+     * @param enabled true to enable continuous rendering, false to disable
+     */
+    public void setAnimationEnabled(boolean enabled) {
+        this.animationEnabled = enabled;
+        if (enabled) {
+            animationTimer.start();
+        } else {
+            animationTimer.stop();
+        }
+    }
+    
+    /**
+     * @return true if animation rendering is enabled
+     */
+    public boolean isAnimationEnabled() {
+        return animationEnabled;
+    }
 }

+ 9 - 13
jme3-materialeditor/src/com/jme3/gde/materials/MaterialPreviewRenderer.java

@@ -1,5 +1,5 @@
 /*
- *  Copyright (c) 2009-2023 jMonkeyEngine
+ *  Copyright (c) 2009-2025 jMonkeyEngine
  *  All rights reserved.
  * 
  *  Redistribution and use in source and binary forms, with or without
@@ -235,17 +235,10 @@ public class MaterialPreviewRenderer implements SceneListener {
 
     public void switchDisplay(DisplayType type) {
         switch (type) {
-            case Box:
-                currentGeom = box;
-                break;
-            case Sphere:
-                currentGeom = sphere;
-                break;
-            case Quad:
-                currentGeom = quad;
-                break;
-            case Teapot:
-                currentGeom = teapot;
+            case Box -> currentGeom = box;
+            case Sphere -> currentGeom = sphere;
+            case Quad -> currentGeom = quad;
+            case Teapot -> currentGeom = teapot;
         }
         showMaterial(currentMaterial);
     }
@@ -283,7 +276,9 @@ public class MaterialPreviewRenderer implements SceneListener {
      * material
      */
     public void refreshOnly() {
-        previewRequested = true;
+        if(previewRequested) {
+            return;
+        }
         SceneApplication.getApplication().enqueue((Callable<Object>) () -> {
             if (currentGeom.getMaterial() != null) {
                 PreviewRequest request = new PreviewRequest(MaterialPreviewRenderer.this, currentGeom, label.getWidth(), label.getHeight());
@@ -293,6 +288,7 @@ public class MaterialPreviewRenderer implements SceneListener {
             }
             return null;
         });
+        previewRequested = true;
     }
 
 }

+ 13 - 0
jme3-materialeditor/src/com/jme3/gde/materials/multiview/MaterialEditorTopComponent.form

@@ -99,6 +99,8 @@
                                   </Group>
                                   <EmptySpace max="-2" attributes="0"/>
                                   <Component id="jCheckBox1" min="-2" pref="148" max="-2" attributes="0"/>
+                                  <EmptySpace max="-2" attributes="0"/>
+                                  <Component id="animationCheckBox" min="-2" pref="148" max="-2" attributes="0"/>
                                   <EmptySpace min="-2" pref="39" max="-2" attributes="0"/>
                               </Group>
                           </Group>
@@ -114,6 +116,7 @@
                                   <Group type="103" groupAlignment="1" attributes="0">
                                       <Component id="jToolBar3" min="-2" pref="25" max="-2" attributes="0"/>
                                       <Component id="jCheckBox1" min="-2" max="-2" attributes="0"/>
+                                      <Component id="animationCheckBox" min="-2" max="-2" attributes="0"/>
                                   </Group>
                                   <EmptySpace max="-2" attributes="0"/>
                                   <Component id="jToolBar2" min="-2" pref="25" max="-2" attributes="0"/>
@@ -363,6 +366,16 @@
                     <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="jCheckBox1ActionPerformed"/>
                   </Events>
                 </Component>
+                <Component class="javax.swing.JCheckBox" name="animationCheckBox">
+                  <Properties>
+                    <Property name="selected" type="boolean" value="false"/>
+                    <Property name="text" type="java.lang.String" value="Enable Animation"/>
+                    <Property name="toolTipText" type="java.lang.String" value="Enable continuous rendering for animated materials"/>
+                  </Properties>
+                  <Events>
+                    <EventHandler event="actionPerformed" listener="java.awt.event.ActionListener" parameters="java.awt.event.ActionEvent" handler="animationCheckBoxActionPerformed"/>
+                  </Events>
+                </Component>
                 <Component class="com.jme3.gde.materials.multiview.widgets.MaterialPreviewWidget" name="materialPreviewWidget1">
                 </Component>
                 <Container class="javax.swing.JTabbedPane" name="additionalRenderStatePane">

+ 20 - 1
jme3-materialeditor/src/com/jme3/gde/materials/multiview/MaterialEditorTopComponent.java

@@ -176,6 +176,7 @@ public final class MaterialEditorTopComponent extends CloneableTopComponent impl
         jLabel3 = new javax.swing.JLabel();
         jTextField1 = new javax.swing.JTextField();
         jCheckBox1 = new javax.swing.JCheckBox();
+        animationCheckBox = new javax.swing.JCheckBox();
         materialPreviewWidget1 = new com.jme3.gde.materials.multiview.widgets.MaterialPreviewWidget();
         additionalRenderStatePane = new javax.swing.JTabbedPane();
         jScrollPane10 = new javax.swing.JScrollPane();
@@ -294,6 +295,15 @@ public final class MaterialEditorTopComponent extends CloneableTopComponent impl
             }
         });
 
+        animationCheckBox.setSelected(false);
+        animationCheckBox.setText("Enable Animation"); // NOI18N
+        animationCheckBox.setToolTipText("Enable continuous rendering for animated materials");
+        animationCheckBox.addActionListener(new java.awt.event.ActionListener() {
+            public void actionPerformed(java.awt.event.ActionEvent evt) {
+                animationCheckBoxActionPerformed(evt);
+            }
+        });
+
         additionalRenderStatePane.setTabLayoutPolicy(javax.swing.JTabbedPane.SCROLL_TAB_LAYOUT);
         additionalRenderStatePane.setMinimumSize(new java.awt.Dimension(100, 100));
         additionalRenderStatePane.setPreferredSize(new java.awt.Dimension(380, 355));
@@ -326,6 +336,8 @@ public final class MaterialEditorTopComponent extends CloneableTopComponent impl
                             .addComponent(jToolBar2, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                         .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                         .addComponent(jCheckBox1, javax.swing.GroupLayout.PREFERRED_SIZE, 148, javax.swing.GroupLayout.PREFERRED_SIZE)
+                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+                        .addComponent(animationCheckBox, javax.swing.GroupLayout.PREFERRED_SIZE, 148, javax.swing.GroupLayout.PREFERRED_SIZE)
                         .addGap(39, 39, 39))))
         );
         editorPanelLayout.setVerticalGroup(
@@ -336,7 +348,8 @@ public final class MaterialEditorTopComponent extends CloneableTopComponent impl
                         .addContainerGap()
                         .addGroup(editorPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                             .addComponent(jToolBar3, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
-                            .addComponent(jCheckBox1))
+                            .addComponent(jCheckBox1)
+                            .addComponent(animationCheckBox))
                         .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                         .addComponent(jToolBar2, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE))
                     .addComponent(materialPreviewWidget1, javax.swing.GroupLayout.PREFERRED_SIZE, 225, javax.swing.GroupLayout.PREFERRED_SIZE))
@@ -382,8 +395,14 @@ public final class MaterialEditorTopComponent extends CloneableTopComponent impl
         }
     }//GEN-LAST:event_jComboBox1ActionPerformed
 
+    private void animationCheckBoxActionPerformed(java.awt.event.ActionEvent evt) {
+        boolean enabled = animationCheckBox.isSelected();
+        materialPreviewWidget1.setAnimationEnabled(enabled);
+    }
+
     // Variables declaration - do not modify//GEN-BEGIN:variables
     private javax.swing.JTabbedPane additionalRenderStatePane;
+    private javax.swing.JCheckBox animationCheckBox;
     private javax.swing.JPanel editorPanel;
     private javax.swing.JCheckBox jCheckBox1;
     private javax.swing.JComboBox jComboBox1;

+ 30 - 0
jme3-materialeditor/src/com/jme3/gde/materials/multiview/widgets/MaterialPreviewWidget.java

@@ -41,6 +41,8 @@ import com.jme3.gde.core.scene.SceneApplication;
 import com.jme3.gde.core.scene.SceneRequest;
 import com.jme3.gde.materials.MaterialPreviewRenderer;
 import com.jme3.gde.materials.multiview.widgets.icons.Icons;
+import java.awt.event.ActionEvent;
+import javax.swing.Timer;
 
 /**
  *
@@ -50,11 +52,18 @@ public class MaterialPreviewWidget extends javax.swing.JPanel {
 
     private boolean init=false;
     private MaterialPreviewRenderer matRenderer;
+    private boolean animationEnabled = false;
     
     /** Creates new form MaterialPreviewWidget */
     public MaterialPreviewWidget() {
         initComponents();        
     }
+    
+    private final Timer animationTimer = new Timer(20, (ActionEvent e) -> {
+            if (animationEnabled && matRenderer != null) {
+                matRenderer.refreshOnly();
+            }
+        });
 
     private  void initWidget() {
         sphereButton.setSelected(true);
@@ -81,9 +90,30 @@ public class MaterialPreviewWidget extends javax.swing.JPanel {
     }
 
     public void cleanUp(){
+         animationTimer.stop();
          matRenderer.cleanUp();
     }
     
+    /**
+     * Enable or disable animation rendering
+     * @param enabled true to enable continuous rendering, false to disable
+     */
+    public void setAnimationEnabled(boolean enabled) {
+        this.animationEnabled = enabled;
+        if (enabled) {
+            animationTimer.start();
+        } else {
+            animationTimer.stop();
+        }
+    }
+    
+    /**
+     * @return true if animation rendering is enabled
+     */
+    public boolean isAnimationEnabled() {
+        return animationEnabled;
+    }
+    
     /** This method is called from within the constructor to
      * initialize the form.
      * WARNING: Do NOT modify this code. The content of this method is

+ 8 - 0
jme3-materialeditor/src/com/jme3/gde/materials/multiview/widgets/icons/Icons.java

@@ -53,6 +53,10 @@ public class Icons {
     public static final String LIGHT_ON = ICONS_PATH + "light-bulb-on.svg";
     public static final String LIGHT_OFF = ICONS_PATH + "light-bulb-off.svg";
     
+    // Using reload icon from core editor icons
+    public static final String ANIMATION_OFF = "com/jme3/gde/core/editor/icons/reload.png";
+    public static final String ANIMATION_ON = "com/jme3/gde/core/editor/icons/reload.png";
+    
     public static final ImageIcon textureRemove =
             ImageUtilities.loadImageIcon(TEXTURE_REMOVE, false);
     public static final ImageIcon cube =
@@ -75,4 +79,8 @@ public class Icons {
             ImageUtilities.loadImageIcon(LIGHT_ON, false);
     public static final ImageIcon lightOff =
             ImageUtilities.loadImageIcon(LIGHT_OFF, false);
+    public static final ImageIcon animationOff =
+            ImageUtilities.loadImageIcon(ANIMATION_OFF, false);
+    public static final ImageIcon animationOn =
+            ImageUtilities.loadImageIcon(ANIMATION_ON, false);
 }

+ 53 - 0
jme3-materialeditor/test/manual/MaterialPreviewWidgetManualTest.java

@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2009-2024 jMonkeyEngine
+ * All rights reserved.
+ */
+package com.jme3.gde.materials.multiview.widgets;
+
+import javax.swing.JFrame;
+import javax.swing.SwingUtilities;
+
+/**
+ * Simple manual test to verify animation toggle functionality
+ * 
+ * @author copilot
+ */
+public class MaterialPreviewWidgetManualTest {
+    
+    public static void main(String[] args) {
+        SwingUtilities.invokeLater(() -> {
+            JFrame frame = new JFrame("Material Preview Widget Test");
+            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+            
+            MaterialPreviewWidget widget = new MaterialPreviewWidget();
+            frame.add(widget);
+            
+            frame.setSize(300, 250);
+            frame.setLocationRelativeTo(null);
+            frame.setVisible(true);
+            
+            // Test animation toggle after 2 seconds
+            new Thread(() -> {
+                try {
+                    Thread.sleep(2000);
+                    System.out.println("Animation enabled: " + widget.isAnimationEnabled());
+                    
+                    Thread.sleep(1000);
+                    SwingUtilities.invokeLater(() -> {
+                        widget.setAnimationEnabled(true);
+                        System.out.println("Animation enabled: " + widget.isAnimationEnabled());
+                    });
+                    
+                    Thread.sleep(3000);
+                    SwingUtilities.invokeLater(() -> {
+                        widget.setAnimationEnabled(false);
+                        System.out.println("Animation enabled: " + widget.isAnimationEnabled());
+                    });
+                    
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }).start();
+        });
+    }
+}

+ 140 - 0
jme3-materialeditor/test/unit/src/com/jme3/gde/materialdefinition/editor/MatPanelTest.java

@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2009-2024 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.materialdefinition.editor;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import org.junit.jupiter.api.Test;
+import java.awt.EventQueue;
+import java.lang.reflect.InvocationTargetException;
+import org.openide.util.Exceptions;
+
+/**
+ * Test class for MatPanel animation functionality
+ * 
+ * @author copilot
+ */
+public class MatPanelTest {
+    
+    public MatPanelTest() {
+    }
+    
+    @Test
+    public void testAnimationEnabled() {
+        MatPanel panel = new MatPanel();
+        
+        // Test initial state - animation should be disabled by default
+        assertFalse(panel.isAnimationEnabled());
+        
+        // Test enabling animation
+        try {
+            EventQueue.invokeAndWait(() -> {
+                panel.setAnimationEnabled(true);
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        assertTrue(panel.isAnimationEnabled());
+        
+        // Test disabling animation
+        try {
+            EventQueue.invokeAndWait(() -> {
+                panel.setAnimationEnabled(false);
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        assertFalse(panel.isAnimationEnabled());
+    }
+    
+    @Test
+    public void testAnimationToggle() {
+        MatPanel panel = new MatPanel();
+        
+        // Initially disabled
+        assertFalse(panel.isAnimationEnabled());
+        
+        // Enable and verify
+        try {
+            EventQueue.invokeAndWait(() -> {
+                panel.setAnimationEnabled(true);
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        assertTrue(panel.isAnimationEnabled());
+        
+        // Toggle off and verify
+        try {
+            EventQueue.invokeAndWait(() -> {
+                panel.setAnimationEnabled(false);
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        assertFalse(panel.isAnimationEnabled());
+    }
+    
+    @Test
+    public void testCleanupStopsAnimation() {
+        MatPanel panel = new MatPanel();
+        
+        // Enable animation
+        try {
+            EventQueue.invokeAndWait(() -> {
+                panel.setAnimationEnabled(true);
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        assertTrue(panel.isAnimationEnabled());
+        
+        // Cleanup should stop animation
+        try {
+            EventQueue.invokeAndWait(() -> {
+                panel.cleanup();
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        // Animation should still be logically enabled but timer stopped
+        // This tests that cleanup properly stops the timer
+        assertTrue(panel.isAnimationEnabled());
+    }
+}

+ 140 - 0
jme3-materialeditor/test/unit/src/com/jme3/gde/materials/multiview/widgets/MaterialPreviewWidgetTest.java

@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2009-2024 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.gde.materials.multiview.widgets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import org.junit.jupiter.api.Test;
+import java.awt.EventQueue;
+import java.lang.reflect.InvocationTargetException;
+import org.openide.util.Exceptions;
+
+/**
+ * Test class for MaterialPreviewWidget animation functionality
+ * 
+ * @author copilot
+ */
+public class MaterialPreviewWidgetTest {
+    
+    public MaterialPreviewWidgetTest() {
+    }
+    
+    @Test
+    public void testAnimationEnabled() {
+        MaterialPreviewWidget widget = new MaterialPreviewWidget();
+        
+        // Test initial state - animation should be disabled by default
+        assertFalse(widget.isAnimationEnabled());
+        
+        // Test enabling animation
+        try {
+            EventQueue.invokeAndWait(() -> {
+                widget.setAnimationEnabled(true);
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        assertTrue(widget.isAnimationEnabled());
+        
+        // Test disabling animation
+        try {
+            EventQueue.invokeAndWait(() -> {
+                widget.setAnimationEnabled(false);
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        assertFalse(widget.isAnimationEnabled());
+    }
+    
+    @Test
+    public void testAnimationToggle() {
+        MaterialPreviewWidget widget = new MaterialPreviewWidget();
+        
+        // Initially disabled
+        assertFalse(widget.isAnimationEnabled());
+        
+        // Enable and verify
+        try {
+            EventQueue.invokeAndWait(() -> {
+                widget.setAnimationEnabled(true);
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        assertTrue(widget.isAnimationEnabled());
+        
+        // Toggle off and verify
+        try {
+            EventQueue.invokeAndWait(() -> {
+                widget.setAnimationEnabled(false);
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        assertFalse(widget.isAnimationEnabled());
+    }
+    
+    @Test
+    public void testCleanupStopsAnimation() {
+        MaterialPreviewWidget widget = new MaterialPreviewWidget();
+        
+        // Enable animation
+        try {
+            EventQueue.invokeAndWait(() -> {
+                widget.setAnimationEnabled(true);
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        assertTrue(widget.isAnimationEnabled());
+        
+        // Cleanup should stop animation
+        try {
+            EventQueue.invokeAndWait(() -> {
+                widget.cleanUp();
+            });
+        } catch (InterruptedException | InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+        
+        // Animation should still be logically enabled but timer stopped
+        // This tests that cleanup properly stops the timer
+        assertTrue(widget.isAnimationEnabled());
+    }
+}