SquirrelJME/SquirrelJME

View on GitHub
emulators/emulator-base/src/main/java/cc/squirreljme/emulator/uiform/SwingForm.java

Summary

Maintainability
A
0 mins
Test Coverage
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// SquirrelJME
//     Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the Mozilla Public License Version 2.0.
// See license.mkd for licensing and copyright information.
// ---------------------------------------------------------------------------

package cc.squirreljme.emulator.uiform;

import cc.squirreljme.emulator.NativeBinding;
import cc.squirreljme.jvm.mle.brackets.UIFormBracket;
import cc.squirreljme.jvm.mle.brackets.UIItemBracket;
import cc.squirreljme.jvm.mle.callbacks.UIFormCallback;
import cc.squirreljme.jvm.mle.constants.UIItemPosition;
import cc.squirreljme.jvm.mle.constants.UIWidgetProperty;
import cc.squirreljme.jvm.mle.exceptions.MLECallError;
import cc.squirreljme.runtime.cldc.debug.Debugging;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

/**
 * Represents a single Swing form, which is built-on top of a panel that
 * is shown in a window.
 *
 * @since 2020/07/01
 */
public final class SwingForm
    implements UIFormBracket, SwingWidget
{
    /** The panel which makes up the form. */
    protected final JPanel formPanel;
    
    /** Items on this form, shifted for special items. */
    private final List<SwingItem> _items =
        new ArrayList<>();
    
    /** The command bar. */
    protected final JPanel commandBar =
        new JPanel();
    
    /** Left command. */
    protected final JPanel leftCommand =
        new JPanel();
    
    /** Right command. */
    protected final JPanel rightCommand =
        new JPanel();
    
    /** Title. */
    protected final JPanel title =
        new JPanel();
    
    /** Standard body. */
    protected final JPanel body =
        new JPanel();
    
    /** The ticker. */
    protected final JPanel ticker =
        new JPanel();
    
    /** The top panel (title and ticker). */
    protected final JPanel topBar =
        new JPanel();
    
    /** The primary form list contents. */
    protected final JPanel adjacent =
        new JPanel();
    
    /** The display owning this. */
    SwingDisplay _display;
    
    /** The callback for the form. */
    private UIFormCallback _callback;
    
    /** Does the canvas need to be focused automatically? */
    private volatile boolean _focusBody;
    
    /** The next title to choose. */
    volatile String _nextTitle; 
    
    static
    {
        // We need to poke native binding, so it loads our emulation backend
        NativeBinding.loadedLibraryPath();
        
        try
        {
            // Greatly optimizes speed
            JFrame.setDefaultLookAndFeelDecorated(true);
        }
        catch (Throwable ignored)
        {
        }
    }
    
    /**
     * Initializes the form.
     * 
     * @since 2020/07/01
     */
    @SuppressWarnings("UnnecessaryLocalVariable")
    public SwingForm()
    {
        // Add starting blank items
        List<SwingItem> items = this._items;
        for (int i = 0; i < UIItemPosition.SPECIAL_SHIFT; i++)
            items.add(null);
        
        JPanel panel = new JPanel();
        
        // Make sure the panel is not so tiny
        Dimension minDim = new Dimension(SwingHardMetrics.DISPLAY_MIN_WIDTH,
            SwingHardMetrics.DISPLAY_MIN_HEIGHT);
        panel.setMinimumSize(minDim);
        panel.setPreferredSize(minDim);
        panel.setSize(minDim);
        
        // Use a border layout because we can set tops and bottoms
        // accordingly
        panel.setLayout(new BorderLayout());
        
        this.formPanel = panel;
    }
    
    /**
     * {@inheritDoc}
     * @since 2020/09/13
     */
    @Override
    public UIFormCallback callback()
    {
        synchronized (this)
        {
            return this._callback;
        }
    }
    
    /**
     * Deletes the form.
     * 
     * @since 2020/07/01
     */
    public void delete()
    {
        // Prevent contention
        synchronized (this)
        {
            // Hide the current form
            if (this._display != null)
                this._display.show(null);
            
            // Remove all items from the form
            List<SwingItem> items = this._items;
            for (SwingItem item : items.toArray(new SwingItem[items.size()]))
                if (item != null)
                    this.itemRemove(item);
        }
    }
    
    /**
     * {@inheritDoc}
     * @since 2020/10/17
     */
    @Override
    public UIFormBracket form()
    {
        return this;
    }
    
    /**
     * {@inheritDoc}
     * @since 2020/10/17
     */
    @Override
    public UIItemBracket item()
    {
        return null;
    }
    
    /**
     * Returns the item at the position.
     * 
     * @param __pos The position.
     * @return The item at the position or {@code null} if there is nothing.
     * @throws MLECallError If the position is not valid.
     * @since 2020/07/18
     */
    public final SwingItem itemAtPosition(int __pos)
        throws MLECallError
    {
        if (__pos < UIItemPosition.MIN_VALUE)
            throw new MLECallError("Bad position: " + __pos);
        
        // Normalize
        int normalPos = __pos + UIItemPosition.SPECIAL_SHIFT;
        
        synchronized (this)
        {
            List<SwingItem> items = this._items;
            if (normalPos >= items.size())
                throw new MLECallError("Invalid index: " + __pos);
            
            return items.get(normalPos);
        }
    }
    
    /**
     * Returns the number of normal items on this form.
     * 
     * @return The number of normal items.
     * @since 2020/07/18
     */
    public final int itemCount()
    {
        synchronized (this)
        {
            // Items are shifted
            return Math.max(0,
                this._items.size() - UIItemPosition.SPECIAL_SHIFT);
        }
    }
    
    /**
     * Returns the position of the given item.
     * 
     * @param __item The item to query.
     * @return The position of the item.
     * @throws MLECallError On null arguments.
     * @since 2020/07/18
     */
    public final int itemPosition(SwingItem __item)
        throws MLECallError
    {
        if (__item == null)
            throw new MLECallError("NARG");
        
        if (__item._isDeleted)
            throw new MLECallError("Item was deleted.");
        
        synchronized (this)
        {
            int n = this.itemCount();
            for (int i = UIItemPosition.MIN_VALUE; i < n; i++)
                if (__item == this.itemAtPosition(i))
                    return i;
            
            return UIItemPosition.NOT_ON_FORM;
        }
    }
    
    /**
     * Sets the position of the given item.
     * 
     * @param __item The item.
     * @param __pos The position.
     * @throws MLECallError If the position is not valid, or on null arguments.
     * @since 2020/07/18
     */
    @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
    public final void itemPosition(SwingItem __item, int __pos)
        throws MLECallError
    {
        if (__item == null)
            throw new MLECallError("Null arguments");
        
        if (__pos < UIItemPosition.MIN_VALUE)
            throw new MLECallError("Invalid position: " + __pos);
        
        if (__item._isDeleted)
            throw new MLECallError("Item was deleted.");
        
        // Normalize position
        int normalPos = __pos + UIItemPosition.SPECIAL_SHIFT;
        
        // Prevent thread contention
        synchronized (this)
        {
            // Check if out of range
            List<SwingItem> items = this._items;
            if (normalPos > items.size())
                throw new MLECallError("Invalid position: " + __pos);
            
            // Get the item that was here, since it will be removed at a later
            // step
            SwingItem old = (normalPos < items.size() ? items.get(normalPos) :
                null);
            
            // Do not do anything if the item is staying in the same spot
            if (__item == old)
                return;
            
            // The item being added may be on another form
            SwingForm itemForm;
            synchronized (__item)
            {
                itemForm = __item._form;
            }
            
            // We need to know the old item's index if it is on this form
            // as after we adjust things we need to clear the link
            int oldIndex = (old == null ? UIItemPosition.NOT_ON_FORM :
                this.itemPosition(old));
            if (oldIndex != UIItemPosition.NOT_ON_FORM)
                oldIndex += UIItemPosition.SPECIAL_SHIFT;
            
            // If the position is the size of all the elements then we are
            // adding to the end, make space for it now so that the later
            // parts of the algorithm are normalized
            if (normalPos == items.size())
                items.add(null);
                
            // The old item's form will no longer be valid, we had this item
            // here so we know it is safe to do this
            if (old != null)
                synchronized (old)
                {
                    old._form = null;
                }
            
            // The item was on a form that was not our own, so clear that
            // association from it
            if (itemForm != null && itemForm != this)
                itemForm.itemRemove(itemForm.itemPosition(__item));
            
            // Just overwrite the item here
            items.set(normalPos, __item);
            
            // Take claim over this item
            __item._form = this;
            
            // The old item is being moved/replaced on the form
            if (oldIndex != UIItemPosition.NOT_ON_FORM)
            {
                // It was a special item, so just clear it
                if (oldIndex < UIItemPosition.SPECIAL_SHIFT)
                {
                    // Only clear it if it was not in the same spot
                    if (oldIndex != normalPos)
                        items.set(oldIndex, null);
                }
                
                // Remove the item at the old position, shift over
                else
                    items.remove(oldIndex);
            }
            
            // Debug
            Debugging.debugNote("add(%s, %d): items: %s",
                __item, __pos, items);
            
            // There may be widgets that need to be adjusted accordingly when
            // this is displayed on the form
            __item.addedOnForm(this, __pos);
            
            // Request that the body be focused since the form changed on us
            this._focusBody = true;
            
            // Refresh the form
            this.refresh();
        }
    }
    
    /**
     * Removes the item at the given index.
     * 
     * @param __pos The position to remove from.
     * @return The item that was here, this may be {@code null}.
     * @throws MLECallError If the position is not valid.
     * @since 2020/07/18
     */
    public final SwingItem itemRemove(int __pos)
        throws MLECallError
    {
        if (__pos < UIItemPosition.MIN_VALUE)
            throw new MLECallError("Invalid position: " + __pos);
        
        // Normalize position
        int normalPos = __pos + UIItemPosition.SPECIAL_SHIFT;
        if (normalPos < 0)
            throw new MLECallError("Invalid position: " + __pos);
        
        // Prevent contention
        synchronized (this)
        {
            // Check if out of range
            List<SwingItem> items = this._items;
            if (normalPos >= items.size())
                throw new MLECallError("Invalid position: " + __pos);
            
            // Get item here, which could be null if special
            SwingItem item = items.get(normalPos);
            if (item == null)
                throw new MLECallError("No item at: " + __pos);
            
            // Removing special items just clears the index
            if (normalPos < UIItemPosition.SPECIAL_SHIFT)
                items.set(normalPos, null);
            
            // But otherwise it gets the index removed
            else
                items.remove(normalPos);
            
            // Remove form association
            item._form = null;
            
            // Refresh the form
            this.refresh();
            
            // Return the item that was removed
            return item;
        }
    }
    
    /**
     * Removes the given item from this form.
     * 
     * @param __item The item to remove.
     * @throws MLECallError On null arguments.
     * @since 2020/07/18
     */
    public final void itemRemove(SwingItem __item)
        throws MLECallError
    {
        if (__item == null)
            throw new MLECallError("NARG");
        
        if (__item._isDeleted)
            throw new MLECallError("Item was deleted.");
        
        synchronized (this)
        {
            int foundPos = this.itemPosition(__item);
            if (foundPos == UIItemPosition.NOT_ON_FORM)
                throw new MLECallError("Item not on form.");
            
            this.itemRemove(foundPos);
            
            // Refresh the form
            this.refresh();
        }
    }
    
    /**
     * Tells the form that it was made current.
     * 
     * @since 2020/11/17
     */
    public void madeCurrent()
    {
        // Determine items to be updated
        Map<Integer, SwingItem> items = new LinkedHashMap<>();
        synchronized (this)
        {
            int n = this.itemCount();
            for (int i = UIItemPosition.MIN_VALUE; i < n; i++)
            {
                SwingItem item = this.itemAtPosition(i);
                
                if (item != null) 
                    items.put(i, item);
            }
        }
        
        // Tell the item that it has been "added" to the form
        for (Map.Entry<Integer, SwingItem> entry : items.entrySet())
            entry.getValue().addedOnForm(this, entry.getKey());
        
        // Force the form to refresh
        this.refresh();
    }
    
    /**
     * Refreshes this form.
     * 
     * @since 2020/10/09
     */
    public final void refresh()
    {
        SwingUtilities.invokeLater(new __Refresher__(this));
    }
    
    /**
     * Sets the callback to use.
     * 
     * @param __callback The callback to set.
     * @since 2020/07/19
     */
    public final void setCallback(UIFormCallback __callback)
    {
        synchronized (this)
        {
            this._callback = __callback;
        }
    }
    
    /**
     * {@inheritDoc}
     * @since 2020/09/21
     */
    @Override
    public final void property(int __intProp, int __sub, int __newValue)
        throws MLECallError
    {
        throw new MLECallError("Unknown property: " + __intProp);
    }
    
    /**
     * {@inheritDoc}
     * @since 2020/09/21
     */
    @Override
    public final void property(int __strProp, int __sub, String __newValue)
        throws MLECallError
    {
        SwingDisplay display = this._display;
        switch (__strProp)
        {
            case UIWidgetProperty.STRING_FORM_TITLE:
                this._nextTitle = __newValue;
                if (display != null)
                    display.frame.setTitle(__newValue);
                break;
            
            default:
                throw new MLECallError("Unknown property: " + __strProp);
        }
    }
    
    /**
     * {@inheritDoc}
     * @since 2020/09/21
     */
    @Override
    public int propertyInt(int __intProp, int __sub)
        throws MLECallError
    {
        SwingDisplay display = this._display;
        switch (__intProp)
        {
            case UIWidgetProperty.INT_X_POSITION:
                return (display != null ? display.frame.getX() : 0);
            
            case UIWidgetProperty.INT_Y_POSITION:
                return (display != null ? display.frame.getY() : 0);
            
            case UIWidgetProperty.INT_WIDTH:
                return this.formPanel.getWidth();
            
            case UIWidgetProperty.INT_HEIGHT:
                return this.formPanel.getHeight();
            
            case UIWidgetProperty.INT_IS_SHOWN:
                return (this.formPanel.isVisible() ? 1 : 0);
            
            default:
                throw new MLECallError("Unknown property: " + __intProp);
        }
    }
    
    /**
     * {@inheritDoc}
     * @since 2020/09/21
     */
    @SuppressWarnings("SwitchStatementWithTooFewBranches")
    @Override
    public String propertyStr(int __strProp, int __sub)
        throws MLECallError
    {
        SwingDisplay display = this._display;
        switch (__strProp)
        {
            case UIWidgetProperty.STRING_FORM_TITLE:
                if (display != null)
                    return display.frame.getTitle();
                return this._nextTitle;
            
            default:
                throw new MLECallError("Unknown property: " + __strProp);
        }
    }
    
    /**
     * Refreshes this form.
     * 
     * @since 2020/07/18
     */
    final void __refresh()
    {
        synchronized (this)
        {
            // Debug
            Debugging.debugNote("Refreshing form (locked)...");
            
            JPanel formPanel = this.formPanel;
            
            // The border layout is the simplest for this and makes sense
            formPanel.removeAll();
            formPanel.setLayout(new BorderLayout());
            
            // If a body item is set, only use this one and care about nothing
            // else (is the full-screen desired item)
            SwingItem bodyItem = this.itemAtPosition(UIItemPosition.BODY);
            if (bodyItem != null)
            {
                // Center on this
                formPanel.add(bodyItem.component(), BorderLayout.CENTER);
                
                // Focus on the body if we should do so
                if (this._focusBody)
                {
                    this._focusBody = false;
                    bodyItem.component().requestFocus();
                }
                
                return;
            }
            
            // Adding a title and a ticker to the form?
            SwingItem titleItem = this.itemAtPosition(UIItemPosition.TITLE);
            SwingItem tickerItem = this.itemAtPosition(UIItemPosition.TICKER);
            if (titleItem != null || tickerItem != null)
            {
                JPanel topBar = this.topBar;
                
                // If we are using the grid layout, we have to add one by one
                // so just remove everything to refresh it
                topBar.removeAll();
                topBar.setLayout(new GridLayout(0, 1));
                
                // Add the title component
                if (titleItem != null)
                    topBar.add(titleItem.component());
                
                // Then the ticker component (if any)
                if (tickerItem != null)
                    topBar.add(tickerItem.component());
                
                // Now use the top bar on the form
                formPanel.add(topBar, BorderLayout.PAGE_START);
            }
            
            // Adding commands to the frame?
            SwingItem leftItem = this.itemAtPosition(
                UIItemPosition.LEFT_COMMAND);
            SwingItem rightItem = this.itemAtPosition(
                UIItemPosition.RIGHT_COMMAND);
            if (leftItem != null || rightItem != null)
            {
                JPanel commandBar = this.commandBar;
                
                // Setup command bar for two items
                commandBar.removeAll();
                commandBar.setLayout(new GridLayout(1, 2));
                
                if (leftItem != null)
                    commandBar.add(leftItem.component());
                if (rightItem != null)
                    commandBar.add(rightItem.component());
                
                // Use as the bottom of the form
                formPanel.add(commandBar, BorderLayout.PAGE_END);
            }
            
            // Setup normal adjacent items
            JPanel adjacent = this.adjacent;
            int n = this.itemCount();
            
            // Use a growing layout but one that is even
            adjacent.removeAll();
            adjacent.setLayout(new GridBagLayout());
            
            // Call and inform that a refresh is happening on the form
            UIFormCallback callback = this.callback();
            if (callback != null)
                callback.formRefresh(this,
                    adjacent.getX(), adjacent.getY(),
                    adjacent.getWidth(), adjacent.getHeight());
            
            // Setup constraints for all the items
            GridBagConstraints cons = new GridBagConstraints();
            cons.gridwidth = 1;
            cons.gridheight = n;
            cons.fill = (n > 1 ? GridBagConstraints.HORIZONTAL :
                GridBagConstraints.BOTH);
            cons.weightx = 1.0;
            cons.weighty = 1.0;
            cons.anchor = GridBagConstraints.PAGE_START;
            
            // Now add all the various form bits
            Debugging.debugNote("Placing %d items...", n);
            for (int i = 0; i < n; i++)
            {
                cons.gridx = 0;
                cons.gridy = i;
                
                adjacent.add(this.itemAtPosition(i).component(), cons);
            }
            
            // Add the final form
            formPanel.add(adjacent, BorderLayout.CENTER);
            
            // Request everything be redrawn
            formPanel.validate();
            formPanel.repaint();
        }
    }
}