Tuesday, June 12, 2007

Splash-Screen and Threads

Today I thought it would make my application look much more professional if the login-dialog is part of the splash-screen. The new extension point added in eclipse makes this possible and even provided an template implementation I could use without problem.

The process is straight forward:
  • Create a Product Configuration (or use your existing one)
  • Switch to the Splash-Tab
  • Select in the Customization-Section the Interactive-Template
  • Switch to the Overview and "Launch an Eclipse application"
You are done. That was easy, wasn't it? To make this work you wouldn't need any help. The tricky thing I faced starts now. In my case I'm authentificating using a database-server and to not block the UI im doing this in a seperate thread and showing a ProgressBar in the mean while.

When the application starts up the splash should look like this:

And while Logging into Database:

So the first action is to add the "Check Login"-Label and the Progress bar like this:

private Label progressLabel;
private ProgressBar progressBar;

/** many lines of code */

private void createProgressInfo() {
progressLabel = new Label(fCompositeLogin,SWT.NONE);
progressLabel.setText("Überprüfung läuft");
GridData data = new GridData();
data.horizontalIndent = F_LABEL_HORIZONTAL_INDENT - 50;
progressLabel.setLayoutData(data);
progressLabel.setVisible(false);

progressBar = new ProgressBar(fCompositeLogin,SWT.NONE|SWT.INDETERMINATE);
data = new GridData(SWT.NONE, SWT.NONE, false, false);
data.widthHint = F_TEXT_WIDTH_HINT;
data.horizontalSpan = 2;
progressBar.setLayoutData(data);
progressBar.setVisible(false);
}

private void toggelCheckProgress(boolean state) {
progressLabel.setVisible(state);
progressBar.setVisible(state);
fCompositeLogin.layout();
}


We initially set the those two widgets invisible and show them later when we start the authentification. To make this easy we add helper method to turn the visibility on and off.

The next part is to modify the handleButtonOKWidgetSelected()-method like this:

private volatile int loginStatus = -1;

/** many lines of code */

private void handleButtonOKWidgetSelected() {
final String username = fTextUsername.getText();
final String password = fTextPassword.getText();

toggelCheckProgress(true);

Thread t = new Thread() {

public void run() {
if( login(username,password) ) {
loginStatus = 1;
} else {
loginStatus = 2;
}
}
}
t.start();
}

The content of the method is straight forward. It starts a thread and executes a potentially long running task in our case login(String,String). Our task is now to sync back to the gui-thread and:
  1. Proceed with start up (hiding the login details from the splash-screen)
  2. Display Login-Failure to the user
Normally you do this using Display#(a)syncExec() but that's not available in the splash-screen. The work-around I used as you see above is setting a special variable named loginStatus. The trick is now that you add checks for this variable to the Event-Loop method which looks like this afterwards:

private void doEventLoop() {
Shell splash = getSplash();
while (fAuthenticated == false) {
if (splash.getDisplay().readAndDispatch() == false) {
if( loginStatus == 1 ) {
loginSuccess();
} else if( loginStatus == 2 ) {
loginFailure();
}

splash.getDisplay().sleep();
}
}
}


Your are nearly done now the only two methods missing are:

private void loginSuccess() {
toggelCheckProgress(false);
fCompositeLogin.setVisible(false);
fAuthenticated = true;
loginStatus = -1;
}

private void loginFailure() {
toggelCheckProgress(false);
loginStatus = -1;
MessageDialog.openError(
getSplash(),
"Authentification failed",
"Your username or password was wrong");
}


Well that's all. You are done and have a splash screen who is authenticating without blocking the UI-thread without Display#(a)syncExec. I need to thank Kim for pointing me to this solution and she promised that there will be added API in 3.4 to make this more easier.

12 comments:

Anonymous said...

What look and feeel is that

Tom said...

MacOSX with ShapeShifter and Aluminum Alloy 1.6.1

Anonymous said...

that's awesome!

Tom said...

One more note: The code Thread-code has to be modified slightly to force the event-loop to run:

public void run() {
// As before
getSplash().getDisplay().wake();
}

Anonymous said...

Great! Only thing:
Is there a more detailed workeround for this solution? For instance which classes the methods have to be implemented in is unclear for me.

Tom said...

1. Screen:
public class InteractiveSplashHandler extends LoginSplashHandler {

}

2. Real implementation:

import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.ProgressBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.splash.AbstractSplashHandler;

import at.bestsolution.swtforms.util.AbstractSwtFormsConfiguration;
import at.bestsolution.swtforms.util.DefaultUser;
import at.bestsolution.swtforms.util.SwtFormsGlobalStorage;

/**
* @since 3.3
*
*/
public class LoginSplashHandler extends AbstractSplashHandler {

private final static int F_LABEL_HORIZONTAL_INDENT = 205;

private final static int F_BUTTON_WIDTH_HINT = 80;

private final static int F_TEXT_WIDTH_HINT = 165;

private final static int F_COLUMN_COUNT = 3;

private Composite fCompositeLogin;

private Text fTextUsername;

private Text fTextPassword;

private Button fButtonOK;

private Button fButtonCancel;

private boolean fAuthenticated;

private Label progressLabel;

private ProgressBar progressBar;

private volatile int loginStatus = -1;

/**
*
*/
public LoginSplashHandler() {
fCompositeLogin = null;
fTextUsername = null;
fTextPassword = null;
fButtonOK = null;
fButtonCancel = null;
fAuthenticated = false;
}

/*
* (non-Javadoc)
*
* @see org.eclipse.ui.splash.AbstractSplashHandler#init(org.eclipse.swt.widgets.Shell)
*/
public void init(final Shell splash) {
// Store the shell
super.init(splash);
// Configure the shell layout
configureUISplash();
// Create UI
createUI();
// Create UI listeners
createUIListeners();
// Force the splash screen to layout
splash.layout(true);
// Keep the splash screen visible and prevent the RCP application from
// loading until the close button is clicked.
doEventLoop();
}

/**
*
*/
private void doEventLoop() {
Shell splash = getSplash();
while (fAuthenticated == false) {
if (splash.getDisplay().readAndDispatch() == false) {
if( loginStatus == 1 ) {
loginSuccess();
} else if( loginStatus == 2 ) {
loginFailure();
}

splash.getDisplay().sleep();
}
}
}

private void loginSuccess() {
toggelCheckProgress(false);
fCompositeLogin.setVisible(false);
fAuthenticated = true;
loginStatus = -1;
}

private void loginFailure() {
toggelCheckProgress(false);
loginStatus = -1;
MessageDialog.openError(
getSplash(),
"Anmeldung fehlgeschlagen", //NON-NLS-1
"Benutzer und/oder Passwort falsch"); //NON-NLS-1
}

/**
*
*/
private void createUIListeners() {
// Create the OK button listeners
createUIListenersButtonOK();
// Create the cancel button listeners
createUIListenersButtonCancel();
// Create listener to press ok on return
createUIListenersReturnOK();
}

private void createUIListenersReturnOK() {
fTextPassword.addTraverseListener(new TraverseListener() {

public void keyTraversed(TraverseEvent e) {
if( e.detail == SWT.TRAVERSE_RETURN ) {
handleButtonOKWidgetSelected();
}
}

});
}

/**
*
*/
private void createUIListenersButtonCancel() {
fButtonCancel.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
handleButtonCancelWidgetSelected();
}
});
}

/**
*
*/
private void handleButtonCancelWidgetSelected() {
// Abort the loading of the RCP application
getSplash().getDisplay().close();
System.exit(0);
}

/**
*
*/
private void createUIListenersButtonOK() {
fButtonOK.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
handleButtonOKWidgetSelected();
}
});
}

/**
*
*/
private void handleButtonOKWidgetSelected() {
final String username = fTextUsername.getText();
final String password = fTextPassword.getText();

toggelCheckProgress(true);

Thread t = new Thread() {

@Override
public void run() {
DefaultUser user = new DefaultUser(username,password);
if( user.login() ) {
AbstractSwtFormsConfiguration config = SwtFormsGlobalStorage.getInstance().getAppSpecificDataProvider().getConfiguration();
config.setProperty("swtforms.login.lastUser", username );
try {
config.save();
} catch (Exception e) {
e.printStackTrace();
}
loginStatus = 1;
} else {
loginStatus = 2;
}
getSplash().getDisplay().wake();
}

};
t.start();
}

/**
*
*/
private void createUI() {
// Create the login panel
createUICompositeLogin();
// Create the blank spanner
createUICompositeBlank();
// Create the user name label
createUILabelUserName();
// Create the user name text widget
createUITextUserName();
// Create the password label
createUILabelPassword();
// Create the password text widget
createUITextPassword();
// Create the blank label
createUILabelBlank();
// Create the OK button
createUIButtonOK();
// Create the cancel button
createUIButtonCancel();
// Create progress
createProgressInfo();
}

private void createProgressInfo() {
progressLabel = new Label(fCompositeLogin,SWT.NONE);
progressLabel.setText("Überprüfung läuft");
GridData data = new GridData();
data.horizontalIndent = F_LABEL_HORIZONTAL_INDENT - 50;
progressLabel.setLayoutData(data);
progressLabel.setVisible(false);

progressBar = new ProgressBar(fCompositeLogin,SWT.NONE|SWT.INDETERMINATE);
data = new GridData(SWT.NONE, SWT.NONE, false, false);
data.widthHint = F_TEXT_WIDTH_HINT;
data.horizontalSpan = 2;
progressBar.setLayoutData(data);
progressBar.setVisible(false);
}

private void toggelCheckProgress(boolean state) {
progressLabel.setVisible(state);
progressBar.setVisible(state);
fCompositeLogin.layout();
}

/**
*
*/
private void createUIButtonCancel() {
// Create the button
fButtonCancel = new Button(fCompositeLogin, SWT.PUSH);
fButtonCancel.setText("Cancel"); //NON-NLS-1
// Configure layout data
GridData data = new GridData(SWT.NONE, SWT.NONE, false, false);
data.widthHint = F_BUTTON_WIDTH_HINT;
fButtonCancel.setLayoutData(data);
}

/**
*
*/
private void createUIButtonOK() {
// Create the button
fButtonOK = new Button(fCompositeLogin, SWT.PUSH);
fButtonOK.setText("OK"); //NON-NLS-1
// Configure layout data
GridData data = new GridData(SWT.NONE, SWT.NONE, false, false);
data.widthHint = F_BUTTON_WIDTH_HINT;
fButtonOK.setLayoutData(data);
}

/**
*
*/
private void createUILabelBlank() {
Label label = new Label(fCompositeLogin, SWT.NONE);
label.setVisible(false);
}

/**
*
*/
private void createUITextPassword() {
// Create the text widget
int style = SWT.PASSWORD | SWT.BORDER;
fTextPassword = new Text(fCompositeLogin, style);

String password = getPassword();

if( password != null ) {
fTextPassword.setText(password);
}

// Configure layout data
GridData data = new GridData(SWT.NONE, SWT.NONE, false, false);
data.widthHint = F_TEXT_WIDTH_HINT;
data.horizontalSpan = 2;
fTextPassword.setLayoutData(data);
}

/**
*
*/
private void createUILabelPassword() {
// Create the label
Label label = new Label(fCompositeLogin, SWT.NONE);
label.setText("&Passwort:"); //NON-NLS-1
// Configure layout data
GridData data = new GridData();
data.horizontalIndent = F_LABEL_HORIZONTAL_INDENT;
label.setLayoutData(data);
}

/**
*
*/
private void createUITextUserName() {
// Create the text widget
fTextUsername = new Text(fCompositeLogin, SWT.BORDER);
String username = getUsername();

if( username != null ) {
fTextUsername.setText(username);
}

// Configure layout data
GridData data = new GridData(SWT.NONE, SWT.NONE, false, false);
data.widthHint = F_TEXT_WIDTH_HINT;
data.horizontalSpan = 2;
fTextUsername.setLayoutData(data);
}

/**
*
*/
private void createUILabelUserName() {
// Create the label
Label label = new Label(fCompositeLogin, SWT.NONE);
label.setText("&Benutzer:"); //NON-NLS-1
// Configure layout data
GridData data = new GridData();
data.horizontalIndent = F_LABEL_HORIZONTAL_INDENT;
label.setLayoutData(data);
}

/**
*
*/
private void createUICompositeBlank() {
Composite spanner = new Composite(fCompositeLogin, SWT.NONE);
GridData data = new GridData(SWT.FILL, SWT.FILL, true, false);
data.horizontalSpan = F_COLUMN_COUNT;
data.heightHint=150;
spanner.setLayoutData(data);
}

/**
*
*/
private void createUICompositeLogin() {
// Create the composite
fCompositeLogin = new Composite(getSplash(), SWT.BORDER);
GridLayout layout = new GridLayout(F_COLUMN_COUNT, false);
fCompositeLogin.setLayout(layout);
}

/**
*
*/
private void configureUISplash() {
// Configure layout
FillLayout layout = new FillLayout();
getSplash().setLayout(layout);
// Force shell to inherit the splash background
getSplash().setBackgroundMode(SWT.INHERIT_DEFAULT);
}

private static String getUsername() {
AbstractSwtFormsConfiguration config = SwtFormsGlobalStorage.getInstance().getAppSpecificDataProvider().getConfiguration();
if( config.getLoadLastUsername() ) {
return config.getProperty("swtforms.login.lastUser");
} else {
return null;
}
}

private static String getPassword() {
AbstractSwtFormsConfiguration config = SwtFormsGlobalStorage.getInstance().getAppSpecificDataProvider().getConfiguration();
if( config.getLoadPassword() ) {
return config.getProperty("swtforms.login.lastPwd");
} else {
return null;
}
}
}

Banse said...

First of all: Thanks for this very nice Feature!!!

I switched from a Login Dialog to the Splash Login with your Improvement and it works great!

The only (little) thing that's missing to make the Splash Login perfect is to see the splash in the (Windows) Task Bar. Without that you can easily "loose" the Splash-Screen among the other open Windows...

Do you (or anybody else) also have any Idea how to solve this Problem?

Tom said...

There's a resent thread on RCP-Platform asking for this kind of feature and the answer was from Andrew Niefer was:

"
The splash screen window is created natively with the extended window style WS_EX_TOOLWINDOW which makes it not appear in the task bar. This corresponds to the SWT constant SWT.TOOL.

I don't know if it is possible to change this afterwards. That native window is wrapped in the SWT.Shell that contains your interactive splash, so you may be able to change it there.

You may also be able to create a new shell that doesn't have this style (Nest it inside the splash shell maybe?) to force it to show in the task bar.
"

Banse said...

thanks for your fast reply!

I also read this thread and tried the second idea of Andrew but that didn't work (maybe I haven't done it all correct) Now i'm trying to apply the first alternative but that looks to me like I had to change some internal eclipse Code what I don't want. I thaught there could be an easy way but I think that's not so trivial.

I'll give it another try today (and reply here and in the RCP-Platform Thread if I find a solution)

Maralc said...

Hi Tom,
Thanks for the excelent post.

As I am new to SWT, would it be possible for you to post the given util classes somewhere?

Thanks anyway.

Marcelo

Niddhi said...

Hi,

I am new to Eclipse RCP. please excuse if my questions are stupid.

I am trying to have a log-in screen on my eclipse application.
I selected the interactive template.
if i make no further changes, then what should i get on the splash when i run the application.

I was expecting the log-in fields should be there and i can write the required code for the backend processing as required.

But what i got was a plain splash screen.

I tried using ur code even, but no luck. Nothing appeared on the splash.

Please guide me as where i might be going wrong.

This is really urgent and i hv alredy spend 3 says over it with no fruitful output.

Thanks!!

Tom said...

Please ask once more on: http://tomsondev.bestsolution.at/2007/06/12/splash-screen-and-threads/ I'm not answering questions on this blog anymore