Tuesday, July 3, 2012

Metawidget: Collections Support (Nested Metawidgets)

A recent post on the forum started:

"I've written a WidgetBuilder to handle collections... [by using] Metawidget to produce a [nested Metawidget] component for each object in the collection..."

We've covered something like this before, but this time things were complicated by the need to support BeansBindingProcessor with both save and rebind.

Here's what we came up with (needs Metawidget 2.3):

package com.myapp;

import static org.metawidget.inspector.InspectionResultConstants.*;

import java.awt.*;
import java.awt.event.*;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.swing.*;

import org.metawidget.inspector.annotation.UiComesAfter;
import org.metawidget.swing.SwingMetawidget;
import org.metawidget.swing.widgetbuilder.SwingWidgetBuilder;
import org.metawidget.swing.widgetprocessor.binding.beansbinding.BeansBindingProcessor;
import org.metawidget.util.*;
import org.metawidget.widgetbuilder.composite.*;
import org.metawidget.widgetbuilder.iface.WidgetBuilder;

public class Application {

   public static void main( final String[] args ) {

      // Model

      final Person homerSimpson = new Person( "Homer Simpson", "Bart", "Lisa",
            "Maggie" );
      final Person nedFlanders = new Person( "Ned Flanders", "Rod", "Todd" );

      // UI

      final JFrame frame = new JFrame();
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

      // Metawidget

      final SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.setBorder( BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) );
      CompositeWidgetBuilderConfig<JComponent, SwingMetawidget> config = new CompositeWidgetBuilderConfig<JComponent, SwingMetawidget>();
      config.setWidgetBuilders( new CollectionWidgetBuilder(),
            new SwingWidgetBuilder() );
      metawidget
            .setWidgetBuilder( new CompositeWidgetBuilder<JComponent, SwingMetawidget>(
                  config ) );
      metawidget.addWidgetProcessor( new CollectionBeansBindingProcessor() );
      metawidget.setToInspect( homerSimpson );
      frame.add( metawidget, BorderLayout.CENTER );

      // Buttons

      JButton rebindButton = new JButton( "Rebind" );
      rebindButton.addActionListener( new ActionListener() {

         @Override
         public void actionPerformed( ActionEvent e ) {

            try {
               BeansBindingProcessor beansBindingProcessor = metawidget
                     .getWidgetProcessor( BeansBindingProcessor.class );
               beansBindingProcessor.save( metawidget );
               JOptionPane.showMessageDialog(
                     frame,
                     "Saved children as "
                           + ( (Person) metawidget.getToInspect() )
                                 .getChildren() );

               beansBindingProcessor.rebind( nedFlanders, metawidget );
            } catch ( Exception ex ) {
               JOptionPane.showMessageDialog( frame, ex.getMessage(),
                     "Error", JOptionPane.ERROR_MESSAGE );
               return;
            }
         }
      } );

      JPanel buttonPanel = new JPanel();
      buttonPanel.add( rebindButton );
      frame.add( buttonPanel, BorderLayout.SOUTH );

      frame.setSize( 400, 200 );
      frame.setVisible( true );
   }

   //
   // Model
   //

   public static class Person {

      private String         mName;
      private List<Person>   mChildren   = CollectionUtils.newArrayList();

      public Person( String name, String... children ) {

         mName = name;
         for ( String child : children ) {
            mChildren.add( new Person( child ) );
         }
      }

      public String getName() {

         return mName;
      }

      public void setName( String name ) {

         mName = name;
      }

      @UiComesAfter( "name" )
      public List<Person> getChildren() {

         return mChildren;
      }

      @Override
      public String toString() {

         return mName;
      }
   }

   //
   // Widget Builder
   //

   public static class CollectionWidgetBuilder
      implements WidgetBuilder<JComponent, SwingMetawidget> {

      @Override
      public JComponent buildWidget( String elementName,
            Map<String, String> attributes, SwingMetawidget metawidget ) {

         // Not for us?

         Class<?> clazz = WidgetBuilderUtils.getActualClassOrType(
               attributes, null );
         if ( clazz == null ) {
            return null;
         }
         if ( !Collection.class.isAssignableFrom( clazz ) ) {
            return null;
         }

         // Create nested Metawidgets

         List<?> list = (List<?>) ClassUtils.getProperty(
               metawidget.getToInspect(), attributes.get( NAME ) );

         if ( list == null || list.isEmpty() ) {
            return null;
         }

         JPanel panel = new JPanel();
         panel.setLayout( new BoxLayout( panel, javax.swing.BoxLayout.Y_AXIS ) );

         for ( Object child : list ) {
            SwingMetawidget nestedMetawidget = new SwingMetawidget();
            Map<String, String> emptyAttributes = Collections.emptyMap();
            metawidget.initNestedMetawidget( nestedMetawidget,
                  emptyAttributes );
            nestedMetawidget.setToInspect( child );
            nestedMetawidget.setPath( child.getClass().getName() );
            panel.add( nestedMetawidget );
         }
         return panel;
      }
   }

   //
   // Widget Processor
   //

   public static class CollectionBeansBindingProcessor
      extends BeansBindingProcessor {

      @Override
      public JComponent processWidget( JComponent component,
            String elementName, Map<String, String> attributes,
            SwingMetawidget metawidget ) {

         // Support nested widgets from the CollectionWidgetBuilder

         if ( component instanceof JPanel ) {
            getNestedPanels( metawidget ).put( attributes.get( NAME ),
                  (JPanel) component );
            return component;
         }

         return super.processWidget( component, elementName, attributes,
               metawidget );
      }

      @Override
      public void save( SwingMetawidget metawidget ) {

         super.save( metawidget );

         // Nested save

         for ( JPanel nestedPanel : getNestedPanels( metawidget ).values() ) {
            for ( Component nestedComponent : nestedPanel.getComponents() ) {
               if ( nestedComponent instanceof SwingMetawidget ) {
                  SwingMetawidget nestedMetawidget = (SwingMetawidget) nestedComponent;
                  save( nestedMetawidget );
               }
            }
         }
      }

      @Override
      public void rebind( Object toRebind, SwingMetawidget metawidget ) {

         super.rebind( toRebind, metawidget );

         // Cannot expect to rebind nestedPanels, so rebuild them instead

         for ( Map.Entry<String, JPanel> entry : getNestedPanels( metawidget )
               .entrySet() ) {

            // Clear the panel

            JPanel panel = entry.getValue();
            panel.removeAll();

            // Use a dummy Metawidget to create just the child Metawidgets
            // of the Collection

            SwingMetawidget dummyMetawidget = new SwingMetawidget();
            Map<String, String> attributes = CollectionUtils.newHashMap();
            attributes.put( NAME, entry.getKey() );
            metawidget.initNestedMetawidget( dummyMetawidget, attributes );

            // Copy them into our real Metawidget

            for ( Component child : ( (JPanel) dummyMetawidget
                  .getComponent( 1 ) ).getComponents() ) {
               panel.add( child );
            }
         }

         metawidget.validate();
      }

      private Map<String, JPanel> getNestedPanels( SwingMetawidget metawidget ) {

         Map<String, JPanel> nestedPanels = (Map<String, JPanel>) metawidget
               .getClientProperty( CollectionBeansBindingProcessor.class );

         if ( nestedPanels == null ) {
            nestedPanels = CollectionUtils.newHashMap();
            metawidget.putClientProperty(
                  CollectionBeansBindingProcessor.class, nestedPanels );
         }

         return nestedPanels;
      }
   }
}

2 comments:

Unknown said...

What is the way to render the nested widgets with Javascript in AngularJS ?

Richard said...

See this blog entry: http://blog.kennardconsulting.com/2014/05/angularjs-create-editable-tables-with.html

Download the example. Inside the example, inside directives.js, edit the line that says 'var input = $( '<input type="text"' to instead say 'var input = $( '<metawidget ng-model="' and go from there.