/*
* Copyright(c) 2002-2010, Rob Eden
* All rights reserved.
*/
package com.starlight.ui;
import com.starlight.locale.ResourceKey;
import com.starlight.thread.SharedThreadPool;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.applet.Applet;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.lang.reflect.InvocationTargetException;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicReference;
/**
* Class used to keep the GUI responsive while performing (possibly) long tasks.
*/
public class LongCallDialog {
public static final long DEFAULT_SHOW_DIALOG_DELAY = 500L;
private static final int DEFAULT_WIDTH = 300;
/** Timer used for all calls. */
private static Timer timer = new Timer( false );
/**
* Do a Swing-based call in which some action is done outside the EDT and then action
* is done inside the EDT with the result.
* <p>
* This method may be called either from inside or outside the EDT.
*/
public static <E> void doSwingWork( final Callable<E> outside_edt_callable,
final ResultHandler<E> edt_result_handler, final ResourceKey<String> dialog_title,
final ResourceKey<String> message, final Component parent,
final boolean allow_cancel ) {
if ( SwingUtilities.isEventDispatchThread() ) {
SharedThreadPool.INSTANCE.execute( new Runnable() {
@Override
public void run() {
doSwingWork( outside_edt_callable, edt_result_handler,
dialog_title, message, parent, allow_cancel );
}
} );
return;
}
try {
doCall( new Callable<Void>() {
@Override
public Void call() throws Exception {
final E result = outside_edt_callable.call();
SwingUtilities.invokeLater( new Runnable() {
@Override
public void run() {
edt_result_handler.run( result );
}
});
return null;
}
}, dialog_title, message, parent, DEFAULT_SHOW_DIALOG_DELAY, allow_cancel );
}
catch( Throwable t ) {
if ( t instanceof ExecutionException ) {
t = t.getCause();
}
final Throwable t_final = t;
SwingUtilities.invokeLater( new Runnable() {
@Override
public void run() {
edt_result_handler.error( t_final );
}
} );
}
}
/**
* Used to present feedback and leave the UI responsive when making a long network or
* other time consuming call. This version does not pass back exceptions or return
* values and so can be called from within the EventDispatchThread (because another
* thread will be spawned in that case). This will return a FutureResult object which
* can be used to get the result of the call at a later time.
*
* @param callable The task that might take a significant amount of time.
* @param dialog_title The title of the dialog.
* @param message The message on the dialog.
* @param parent The parent component, if any.
*/
public static <E> FutureTask<E> doFutureCall( final Callable<E> callable,
final ResourceKey<String> dialog_title, final ResourceKey<String> message,
final Component parent ) {
// Note: CAN be called from the EDT
Callable<E> inner_callable = new Callable<E>() {
public E call() throws Exception {
return doCall( callable, dialog_title, message, parent );
}
};
final FutureTask<E> result = new FutureTask<E>( inner_callable );
SharedThreadPool.INSTANCE.execute( result );
return result;
}
/**
* Used to present feedback and leave the UI responsive when making a long network or
* other time consuming call. This must be called outside of the EventDispatchThread
* and will throw an IllegalStateException if that's not done.
*
* @param callable The task that might take a significant amount of time.
* @param dialog_title The title of the dialog.
* @param message The message on the dialog.
* @param parent The parent component, if any.
*
* @return The value returned from the Callable.
*
* @throws InterruptedException If the user cancels while the task
* is being queued.
* @throws java.util.concurrent.ExecutionException A wrapper exception that's thrown
* if the Callable throws an exception.
* @throws IllegalStateException If called from within the EventDispatchThread.
*/
public static <E> E doCall( Callable<E> callable, ResourceKey<String> dialog_title,
ResourceKey<String> message, Component parent )
throws InterruptedException, ExecutionException {
return doCall( callable, dialog_title, message, parent, DEFAULT_SHOW_DIALOG_DELAY );
}
/**
* Used to present feedback and leave the UI responsive when making a long network or
* other time consuming call. This must be called outside of the EventDispatchThread
* and will throw an IllegalStateException if that's not done.
*
* @param callable The task that might take a significant amount of time.
* @param dialog_title The title of the dialog.
* @param message The message on the dialog.
* @param parent The parent component, if any.
*
* @return The value returned from the Callable.
*
* @throws InterruptedException If the user cancels while the task
* is being queued.
* @throws java.util.concurrent.ExecutionException A wrapper exception that's thrown
* if the Callable throws an exception.
* @throws IllegalStateException If called from within the EventDispatchThread.
*/
public static <E> E doCall( Callable<E> callable, ResourceKey<String> dialog_title,
ResourceKey<String> message, Component parent, long dialog_display_delay )
throws InterruptedException, ExecutionException {
return doCall( callable, dialog_title, message, parent, dialog_display_delay,
true );
}
/**
* Used to present feedback and leave the UI responsive when making a long network or
* other time consuming call. This must be called outside of the EventDispatchThread
* and will throw an IllegalStateException if that's not done.
*
* @param callable The task that might take a significant amount of time.
* @param dialog_title The title of the dialog.
* @param message The message on the dialog.
* @param parent The parent component, if any.
* @param allow_cancel True if the cancel button should be enabled. If pressed,
* the thread running the task will be hidden.
*
* @return The value returned from the Callable.
*
* @throws InterruptedException If the user cancels while the task
* is being queued.
* @throws java.util.concurrent.ExecutionException A wrapper exception that's thrown if the
* Callable throws an exception.
* @throws IllegalStateException If called from within the EventDispatchThread.
*/
public static <E> E doCall( Callable<E> callable,
final ResourceKey<String> dialog_title,
final ResourceKey<String> message, final Component parent,
final long dialog_display_delay, final boolean allow_cancel )
throws InterruptedException, ExecutionException {
if ( SwingUtilities.isEventDispatchThread() ) {
throw new IllegalStateException(
"Call cannot be made from within the EventDispatchThread." );
}
final AtomicReference<ActivityDialog> dialog_slot =
new AtomicReference<ActivityDialog>();
final Window window;
Component root = parent == null ? null : SwingUtilities.getRoot( parent );
if ( root instanceof Applet || root == null ) window = null;
else window = (Window) root;
try {
SwingUtilities.invokeAndWait( new Runnable() {
@Override
public void run() {
ActivityDialog dialog;
if ( window instanceof Dialog ) {
dialog = new ActivityDialog( ( Dialog ) window, dialog_title,
message, allow_cancel );
}
else if ( window instanceof Frame ) {
dialog = new ActivityDialog( ( Frame ) window, dialog_title,
message, allow_cancel );
}
else {
dialog = new ActivityDialog( ( Frame ) null, dialog_title,
message, allow_cancel );
}
dialog_slot.set( dialog );
}
} );
}
catch ( InvocationTargetException e ) {
throw new ExecutionException( e.getCause() );
}
final ActivityDialog dialog = dialog_slot.get();
TimerTask show_dialog_task = new TimerTask() {
public void run() {
dialog.setVisible( true );
}
};
FutureTask<E> future = new FutureTask<E>( callable );
CallThread<E> call_thread =
new CallThread<E>( future, dialog, callable.getClass().getName() );
call_thread.start();
dialog.setThreadsToInterrupt( new Thread[] { Thread.currentThread(), call_thread } );
if ( dialog_display_delay > 0 ) {
timer.schedule( show_dialog_task, dialog_display_delay );
}
else {
// This will block until the dialog is hidden
show_dialog_task.run();
}
try {
return future.get();
}
finally {
show_dialog_task.cancel();
SwingUtilities.invokeLater( new Runnable() {
@Override
public void run() {
dialog.setVisible( false );
dialog.dispose();
}
} );
}
}
private static class ActivityDialog extends JDialog implements ActionListener {
private Thread[] threads_to_interrupt;
public ActivityDialog( Dialog owner, ResourceKey<String> title,
ResourceKey<String> message, boolean allow_cancel ) throws HeadlessException {
super( owner, title.getValue(), ModalityType.DOCUMENT_MODAL );
init( message, allow_cancel );
center( owner );
}
public ActivityDialog( Frame owner, ResourceKey<String> title,
ResourceKey<String> message, boolean allow_cancel ) throws HeadlessException {
super( owner, title.getValue(), ModalityType.DOCUMENT_MODAL );
init( message, allow_cancel );
center( owner );
}
void setThreadsToInterrupt( Thread[] threads_to_interrupt ) {
this.threads_to_interrupt = threads_to_interrupt;
}
private void init( ResourceKey<String> message, boolean allow_cancel ) {
setDefaultCloseOperation( JDialog.DO_NOTHING_ON_CLOSE );
JComponent content_pane = ( JComponent ) getContentPane();
content_pane.setLayout( new BorderLayout() );
JLabel label = new MultiLineLabel( message.getValue() );
label.setBorder( new EmptyBorder( 0, 5, 5, 5 ) );
JProgressBar prog = new JProgressBar();
prog.setIndeterminate( true );
JPanel center_panel = new JPanel( new BorderLayout( 10, 0 ) );
JLabel icon_label = new JLabel( UIManager.getIcon( "OptionPane.informationIcon" ) );
center_panel.add( icon_label, BorderLayout.LINE_START );
JPanel message_panel = new JPanel( new BorderLayout() );
message_panel.add( label, BorderLayout.CENTER );
message_panel.add( prog, BorderLayout.PAGE_END );
center_panel.add( message_panel, BorderLayout.CENTER );
content_pane.add( center_panel, BorderLayout.CENTER );
JPanel button_panel = new JPanel( new FlowLayout( FlowLayout.TRAILING ) );
JButton cancel = new JButton( "Cancel" );
cancel.setVisible( allow_cancel );
if ( allow_cancel ) {
addWindowListener( new WindowAdapter() {
public void windowClosing( WindowEvent e ) {
actionPerformed( null );
}
} );
}
cancel.addActionListener( this );
button_panel.add( cancel );
content_pane.add( button_panel, BorderLayout.PAGE_END );
content_pane.setBorder( new EmptyBorder( 10, 10, 10, 10 ) );
setResizable( false );
pack();
setSize( Math.max( DEFAULT_WIDTH, getWidth() ), getHeight() );
}
private void center( Window parent ) {
if ( parent == null ) {
WindowKit.centerWindow( this );
}
else {
WindowKit.centerWindowOnWindow( this, parent );
}
}
public void actionPerformed( ActionEvent e ) {
for( int i = 0; i < threads_to_interrupt.length; i++ ) {
threads_to_interrupt[ i ].interrupt();
}
}
}
private static class CallThread<E> extends Thread {
private final FutureTask<E> future;
private final JDialog dialog;
CallThread( FutureTask<E> future, JDialog dialog, String name ) {
super( "LongCallDialog CallThread: " + name );
this.future = future;
this.dialog = dialog;
}
public void run() {
try {
future.run();
}
finally {
SwingUtilities.invokeLater( new Runnable() {
public void run() {
dialog.setVisible( false );
}
} );
}
}
}
// public static void main( String[] args ) throws Exception {
// String arg = args.length > 0 ? args[ 0 ] : "30000";
//
// final long time = Long.parseLong( arg );
//
// doCall( new Callable() {
// public Object call() throws Exception {
// Thread.sleep( time );
// return null;
// }
// }, new NxBundleKey( "Test Title" ), new NxBundleKey( "Test Message" ), null, 0,
// true );
//
// System.exit( 0 );
// }
}