Monday, June 15, 2009

Java: JOptionPane Examples Part 4 - Complex

Break out of the JOptionPane Limitations with JDialog

In the final article in my JOptionPane example series, I will show how to eliminate a JOptionPane limitation that you may encounter. The limitation is size. Sure, the dialog automatically sizes itself to fit your GUI elements. However, there is no way to allow a user to resize the standard JOptionPane dialog.

The way to eliminate this problem is to embed a JOptionPane object within a JDialog. It's very simple and requires very little change to existing code you may have.

Example 1: Resizable and Editable JTextArea with JScrollPane scrollbars

This example expands upon Example 2 (from Part 2). A JTextArea is used, but the dialog is not resizable. By embedding with a JDialog, this limitation can be overcome.

The definition of the JTextArea and Object array remains the same:

    JTextArea area = new JTextArea();
area.setText("line1\nline2\nline3\nline4\nline5\nline6");
area.setRows(5);
area.setColumns(10);
area.setEditable(true);
JScrollPane scrollpane = new JScrollPane(area);

Object[] array = {
new JLabel("Enter some text:"),
scrollpane,
};
 

Instead of using one of the static methods of JOptionPane, a JOptionPane object is created using its constructor. The Object array and the message type are passed as arguments to the constructor.

    JOptionPane pane = new JOptionPane(array, JOptionPane.PLAIN_MESSAGE);
 

Once the JOptionPane object has been created, the createDialog() method is called, which returns a JDialog object. null is passed to createDialog() to center the dialog on the screen, while the string argument will be the title of the dialog.

    JDialog dialog = pane.createDialog(null, "Result Data");
 

Before the setVisible() method is called, setResizable() is called to allow the dialog to be resized. When setVisible() is called, the dialog will exhibit the same general behavior as when using the static methods of JOptionPane.

    dialog.setResizable(true);
dialog.setVisible(true);
 

Once the user has closed the dialog, control returns to the program. As with the original version of the program, the JTextArea object still exists and its value can be obtained using getText().

    String newtext = area.getText();
System.out.println( "newtext: " + newtext );
 

With JDialog, many new options are available.

Wednesday, June 10, 2009

J-Link: Getting Started with Java and Pro/Engineer, Part 2

J-Link Java Code Requirements

J-Link requires that you have static methods with names matching the java_app_start and java_app_stop options from the registry file. Using the example registry file from Part 1, the required signatures for the "start" and "stop" methods would look like this:

    public static void startApp();
public static void stopApp();
 

As you'll see from the code that follows, these static methods don't do much but call other methods where all the important stuff happens. This approach seems to work well, but there are certainly other ways to approach J-Link.

Method Summary:

Here are the other methods used by the program, none of which are static:

Constructor JLinkHelloWorld()

The constructor sets up the needed utility items, such as the program name (based on the class name), a FileWriter object for the log file, and the platform specific newline character.

Method startupApplication()

The startupApplication() method gets the Pro/Engineer session object, defines a UI command, registers the UI command in the GUI (in the 'Tools' menu), and then announces to the user that the program is operational.

Method shutdownApplication()

The shutdownApplication() method closes out the log file's FileWriter object.

Method writeLog(String mesg)

A log file can be used for debugging and reporting purposes. A developer will use it for debugging an application, while an end user will be able to see what the program is doing internally.

Although using the log file is definitely optional, you'll find that there are all sorts of bizarre things in Pro/Engineer models that you will never have anticipated. With the use of proper logging, a developer increases the chances that these anomalies can be reported back by the end user.

Method closeLog()

As you might expect, this closes the log file.

Method DisplayMessage(String mesg)

DisplayMessage() writes an arbitrary message to the Pro/Engineer message area for the user to see. The example uses of DisplayMessage() that I have provided is not really the best approach for internationalization, but it serves the purpose of showing how to write message to the Pro/Engineer GUI. This method also writes the same output to the log file.

UI Command Registration and UI Listeners

A program is not going to be useful to an end user unless they can run the program. The user is not going to be able to run a J-Link program unless it registers itself with the Pro/Engineer GUI via a new menu or new menu option.

UI command registration is a multistep action requiring a couple of different pieces. The pieces required are a UICommandActionListener and a callback method, while the actions involve creating a command using UICreateCommand() and adding the command to the GUI using UIAddButton(). The text strings used as arguments to the UI methods are either found in the application's text message file (i.e. "JLHW Btn1 Label") or are known by Pro/Engineer (i.e. "Utilities" which represents the "Tools" menu).

The example program implements the listener by defining an inner class that extends the DefaultUICommandActionListener class. The inner class has a single method OnCommand() that is triggered when the user clicks on the command in the GUI. OnCommand() calls Btn1_callback() which is in the main class. Btn1_callback() then gets the current model object, if any, and displays the name in the message window using DisplayMessage().


The J-Link "Hello World" Java Code:

// JLinkHelloWorld.java
// Copyright 2009, MarcMettes@InversionConsulting.com

// imports required
import com.ptc.cipjava.*;
import com.ptc.pfc.pfcCommand.*;
import com.ptc.pfc.pfcGlobal.*;
import com.ptc.pfc.pfcModel.*;
import com.ptc.pfc.pfcSession.*;
import java.io.*;

public class JLinkHelloWorld {

static JLinkHelloWorld App = null;
String programName = null;
Session session = null;
FileWriter log = null;
String msgFile = "msg_jlinkhelloworld.txt";
String newline = null;

// constructor
//
public JLinkHelloWorld () {
programName = this.getClass().getName();
try {
log = new FileWriter(programName + ".log");
newline = System.getProperty("line.separator");
}
catch (Exception e) {
// couldn't create log file, ignore
}
}

// Display message in Pro/Engineer
//
public void DisplayMessage ( String mesg ) throws Exception {
stringseq seq = stringseq.create();
seq.set(0, mesg);
session.UIDisplayMessage(msgFile, "JLHW %s", seq);
seq.clear();
writeLog(mesg);
}

// Write text to log file
//
public void writeLog ( String mesg ) {
try {
if (log == null) { return; }
log.write(mesg + newline);
log.flush();
}
catch (Exception e) {
// ignore
}
}

// Close log file
//
public void closeLog () {
try {
if (log == null) { return; }
log.close();
}
catch (Exception e) {
// ignore
}
}

// Called by Pro/Engineer when starting the application
//
public static void startApp () {
try {
App = new JLinkHelloWorld();
App.startupApplication();
}
catch (Exception e) {
App.writeLog("Problem running startupApplication method" + e.toString());
return;
}
}

// Called by Pro/Engineer when stopping the application
//
public static void stopApp () {
try {
App.shutdownApplication();
}
catch (Exception e) {
App.writeLog("Problem running shutdownApplication method" + e.toString());
return;
}
}

// Perform some steps when shutting down the application
//
public void shutdownApplication () throws Exception {
writeLog("Application '" + programName + "' stopped");
closeLog();
}

// Perform some steps when starting the application
//
public void startupApplication () throws Exception {

try {
writeLog("Application '" + programName + "' started.");
session = pfcGlobal.GetProESession();
}
catch (jxthrowable x) {
writeLog("ERROR: Problem getting session object.");
return;
}

UICommand btn1_cmd = null;

try {
// Define a UI command
btn1_cmd = session.UICreateCommand(
"JLHW Btn1 Cmd", new JLHW_Btn1_CmdListener()
);
}
catch (jxthrowable x) {
writeLog("ERROR: Problem creating uicmd.");
return;
}

try {
// Add UI command to 'Tools' menu
session.UIAddButton(
btn1_cmd, "Utilities", null,
"JLHW Btn1 Label", "JLHW Btn1 Help",
"msg_jlinkhelloworld.txt"
);
}
catch (jxthrowable x) {
writeLog("ERROR: Problem creating menu: " + x.toString());
return;
}

DisplayMessage(programName + " application started.");

}

// Callback for the 'Tools' menu button
//
public void Btn1_callback ( ) throws Exception {

String mesg = null;
Model model = session.GetCurrentModel();

if (model == null) {
mesg = "Hello!";
}
else {
mesg = "Hello! The model is: " + model.GetFileName();
}

DisplayMessage(mesg);

}

// Inner class for UI Command Listener
//
public class JLHW_Btn1_CmdListener extends DefaultUICommandActionListener {

// Handler for button push
//
public void OnCommand () {
try {
Btn1_callback();
}
catch (Exception e) {
writeLog("Exception thrown by Btn1_callback method: " + e.toString());
}
}

}

}
 


As you can see, there are a number of non-trivial steps involved in creating J-Link applications. However, creating simple programs is really easy.

If you need a J-Link program written for you, please contact me at MarcMettes@InversionConsulting.com to discuss your requirements.

Monday, June 8, 2009

Java: JOptionPane Examples Part 3 - Advanced

Using Event Listeners and JPanels with JOptionPane Dialogs

In my JOptionPane Examples Part 1 and Part 2, I covered many ways to use the JOptionPane, some ordinary, some surprising. Now you may be thinking that the JOptionPane is great, but that you need a level of interactivity that is cannot support. Well, think again.

I was recently asked about Example 3 (from Part 2), which shows the use of JRadioButtons (tied together with a ButtonGroup) and a JTextField. I was asked if the JTextField could have the keyboard focus by default, so that the user could immediately start typing in the JTextField, accepting the default JRadioButton.

Since the JOptionPane tends to take control of the dialog once it starts, a more subtle approach is required. One way to achieve this is to use an event listener. By adding the appropriate event listener to the JTextField object, an action can be performed after the dialog has initialized.


Example 1: Adding Event Listeners to a JTextField

This is an enhancement to the code example in Part 2, Example 3. In order to force keyboard focus into the JTextField, the use of event listeners is required. Also the array argument to showConfirmDialog() is replaced with a JPanel that will be populated with all needed components.

This example requires the following imports in addition to the imports from Part 2:

    import java.awt.event.*;
import javax.swing.event.*;
 

As in the previous version, a couple of labels, some JRadioButtons, a ButtonGroup, and a JTextField are defined.

    JLabel option_label = new JLabel("Select an option:");

JRadioButton b1 = new JRadioButton("Option 1");
JRadioButton b2 = new JRadioButton("Option 2");
JRadioButton b3 = new JRadioButton("Option 3");
b1.setSelected(true);

ButtonGroup group = new ButtonGroup();
group.add(b1);
group.add(b2);
group.add(b3);

JLabel name_label = new JLabel("Enter a name:");

JTextField name = new JTextField(30);
 

In this version, two listeners are added, a ComponentListener and an AncestorListener. Both of these attempt to perform a requestFocus(), when the event is triggered.

Why are both needed? The ComponentListener approach seems to work great until Java 6.0. Unfortunately, Java 6.0 seems to have broken it, which is why the AncestorListener is used as well. Also unfortunately, the AncestorListener approach doesn't work in JVMs prior to 6.0. Fortunately, both approaches can exist together, if you need a solution for both sets of JVMs.

Adding the ComponentListener to the JTextField component (for JVMs prior to Java 6.0):

    name.addComponentListener(new ComponentListener() {
public void componentHidden(ComponentEvent ce) {}
public void componentMoved(ComponentEvent ce) {
ce.getComponent().requestFocus();
}
public void componentResized(ComponentEvent ce) {
ce.getComponent().requestFocus();
}
public void componentShown(ComponentEvent ce) {}
});
 

Adding the AncestorListener to the JTextField component (for Java 6.0 JVM):

    name.addAncestorListener(new AncestorListener(){   
public void ancestorAdded(AncestorEvent ae){
ae.getComponent().requestFocus();
}
public void ancestorRemoved(AncestorEvent ae){}
public void ancestorMoved(AncestorEvent ae){}
});
 

Now the JPanel is defined and populated. A BoxLayout is used to position the components vertically. The components are placed in the order that they are added to the panel, which is done by using the add() method.

    JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel,BoxLayout.Y_AXIS));
panel.add(option_label);
panel.add(b1);
panel.add(b2);
panel.add(b3);
panel.add(name_label);
panel.add(name);
 

The only other change to the previous version is that the second argument to showConfirmDialog() is changed from an array to the JPanel, which is called panel.

    int res = JOptionPane.showConfirmDialog(null, panel, "Select", 
JOptionPane.OK_CANCEL_OPTION);
 

The existing result processing code can be used unchanged and therefore will not be duplicated here.


Example 2: Adding an Event Listener to a JPasswordField

Another example that could use an event listener is the JPasswordField. In Example 1 (Part 2), I discussed creating a login/password dialog box using JOptionPane. In this example, the previous code will be enhanced to demonstrate dynamic use of the setEchoChar() method.

Lotus Notes has had a curious password entry feature for a long time. When entering a password, the password masking character changes with each key press. The value of this is debatable, but reproducing this feature presents an interesting challenge. This phenomenon can be achieved in Java by using a KeyListener.

This example requires the following imports in addition to the imports from Part 2:
    import java.awt.event.*;
 

The code for defining the dialog components, is identical.

    JLabel label_loginname = new JLabel("Enter your login name:");
JTextField loginname = new JTextField(15);

JLabel label_password = new JLabel("Enter your password:");
JPasswordField password = new JPasswordField();

JCheckBox rememberCB = new JCheckBox("Remember me");
 

Now a KeyListener is added to the password field. First a list of characters to use for the password masking is defined as a char array. Then each of the KeyListener methods must be defined, even if they are not used.

The keyTyped() method is using for the activity. First the source component is extracted using the getSource() method. Then the setEchoChar() method of the JPasswordField is called. A random char from the char array is passed to setEchoChar() as its only argument.

    password.addKeyListener(new KeyListener() {
char[] mask = { '@', '#', '$', '%', '&', '*' };
public void keyPressed(java.awt.event.KeyEvent ke) {}
public void keyReleased(java.awt.event.KeyEvent ke) {}
public void keyTyped(java.awt.event.KeyEvent ke) {
JPasswordField source = (JPasswordField)ke.getSource();
source.setEchoChar(mask[(int)(mask.length*Math.random())]);
}
});
 

As each character is typed into the password field, that event is fired, which is caught by the listener. The listener then executes and changes the masking character using setEchoChar().

The array definition and call to showConfirmDialog() are repeated here for reference, but are unchanged from the original example.

    Object[] array = { label_loginname, 
loginname,
label_password,
password,
rememberCB };


int res = JOptionPane.showConfirmDialog(null, array, "Login",
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.PLAIN_MESSAGE);
 


As you can see there are many ways to add interactivity to JOptionPane dialogs.

Friday, June 5, 2009

J-Link: Getting Started with Java and Pro/Engineer, Part 1

J-Link can seem intimidating at first, but once you have all the little pieces in hand to get started, it's actually pretty simple. In this article, I'll walk you through all the pieces that you need to create a basic J-Link application.

Even a simplistic J-Link program requires several files, including your Java code compiled to class file(s), a text-based "message file", and an auxiliary application "registry file". I'll explain the required content of each file and how to put them all together.

The Registry File

The registry file is used to instruct Pro/Engineer on how to load an auxiliary application, such as a Pro/Toolkit or J-Link program. Pro/Engineer uses this to find all of the program's pieces. The actual name of the file is not that important, but "protk.dat" will be used as the filename in this example.

An auxiliary application can be started by Pro/Engineer automatically by using the PROTKDAT config.pro option, or by placing the registry file in certain special locations (i.e. the working directory, within the Pro/Engineer install, etc). Auxiliary applications can also be started manually.

Here is an example registry file for the J-Link "Hello World" application:

name               JLinkHelloWorld-App
startup java
java_app_class JLinkHelloWorld
java_app_start startApp
java_app_stop stopApp
allow_stop true
delay_start false
text_dir M:/apps/JLinkHelloWorld/text
java_app_classpath M:/apps/JLinkHelloWorld
end
 

The "name" will appear in the auxiliary application dialog to aid in identifying the program and can be as descriptive (or cryptic) as you need. The "startup java" entry is required for J-Link apps.

"java_app_class" indentifies the primary class of the application where the "java_app_start" and "java_app_stop" methods are contained. The program is required to have static "stop" and "start" methods matching the names given in the registry file (more on that later).

The "allow_stop" option allows the user to stop the program if it is running. The "delay_start" option (if set to "true") can disable automatic startup allowing the user to do this later, manually, if desired.

The "text_dir" option identifies the folder hierarchy containing your application's message files. Although the inclusion of the "text" folder in the path is not always required, it doesn't cause any problems. Unix style paths work just fine here, as they do with config.pro files. Consult my other articles about cool stuff with registry files.

"java_app_classpath" is generally used to find other class and jar files that you may use in your application. It's like having a private CLASSPATH environment variable for each program. As an alternative, this option can be replaced by using the ADD_JAVA_CLASS_PATH config.pro option, using the same value. However, keeping the option in the registry file is generally the simpler approach.

Not every entry is required, but it's usually a good idea to use the above set for greater flexibility.


The config.pro File

To initiate the program upon startup of Pro/Engineer, you can use a PROTKDAT config.pro option. The value of this option is the full path to the registry file.

Using the above example, the option might look like this:

PROTKDAT M:/apps/JLinkHelloWorld/protk.dat
 

The Text Message File

The text message file is used primarily to support internationalization. However, it also enables "code vs data" separation by relocating static text to standalone files. A text message file is needed for even the most basic J-Link program.

Each "entry" in a text message file is comprised of four lines. Typically only the first two are used, the last two are placeholders. The first line is the "message id" that you use in your code, and the second line is the text that will generally appear for that message id.

In this example, the text message file msg_jlinkhelloworld.txt will contain three entries as follows:

JLHW Btn1 Label
JLink Hello World
#
#
JLHW Btn1 Help
Run the JLink Hello World Application
#
#
JLHW %s
%0s
#
#
 

Be sure to leave an extra blank line at the end of the file to avoid one of the many strange problems with text message files.

The text message file must be placed within the folder structure specified by "text_dir" in the registry file. In this example application, the text message file will be placed in:
    M:/apps/JLinkHelloWorld/text/usascii
 

Proper location of the text message file is very critical. Putting the file in the wrong folder is the source of confusion and frustation to new Pro/Engineer API developers.


Read Part 2 for the Java code

Monday, June 1, 2009

Intralink Scripting: Commonspace Folder Info - Part 3

In Part 1, I discussed how to use the ILFolderTreeModel class to recursively descend through your Commonspace folders to generate a folder list. In Part 2, I discussed how to use CSFolderObjectInfo to perform actions and obtain information on your folders. Now I'm going to discuss yet another data model class to get even more extensive information from your folders.

Intralink classes that extend ILTableModel are the core data presentation classes of the Intralink client. While the ObjectInfo classes represent a chunk of metadata for a specific object, the ILTabelModel presents the data model that is actually displayed in the GUI. Interrogating these classes lets you get data exactly as you would expect to see it in Intralink.

Two folder related data presentation classes that I'll now discuss are ILFolderAuthTableModel and ILPromoteDemoteTableModel. As you might expect from the name, ILFolderAuthTableModel represents the data presentation model of folder authorizations, while ILPromoteDemoteTableModel represents folder release procedures.

To utilize these classes, we need an ILAppObject representation of the folder. The static method getObjectByKey() of the ObjectInfo class allows us to create this object. Here is an example of instantiating an ILFolderAuthTableModel object:

    String folderName = "Root Folder/ProjectX";
ILAppObject fol_ao = ObjectInfo.getObjectByKey(ObjectInfo.tFolder, folderName);
ILFolderAuthTableModel fa_tm = new ILFolderAuthTableModel(false, fol_ao);
 

The table of data contained in the ILTableModel based object can be accessed with generic methods and other methods specific to the data type. Examples of generic methods include getRowCount(), getColumnCount(), getColumnName(), and getValueAt().

Example: Reporting Authorizations and Release Procedures

This example further expands upon the code in Part 2, by adding the output of folder authorizations, release level authorizations, and release procedures associated with each folder.

Just as in the GUI, the example shows output only if something was explicitly set for that folder. If a folder authorizations was set only in a parent folder, that authorization will not appear in the output for subfolders. The same applies to release procedures.

    CSFolderObjectInfo fol_oi = null;
ILAppObject fol_ao = null;
ILFolderAuthTableModel fa_tm = null;
ILPromoteDemoteTableModel pd_tm = null;

for (int i=0; i<folders.size(); i++) {

String folderName = folders.get(i).toString();
System.out.println( " " + (i+1) + ": " + folderName );

fol_oi = (CSFolderObjectInfo)ObjectInfo.createByKey( ObjectInfo.tFolder, folderName );
fol_ao = ObjectInfo.getObjectByKey( ObjectInfo.tFolder, folderName);

System.out.println( " " + "Release Scheme: " + fol_oi.getReleaseScheme() );
System.out.println( " " + "File Space: " + fol_oi.getFilespace() );

fa_tm = new ILFolderAuthTableModel(false, fol_ao);
System.out.println( " " + "Authorizations: ");

if (fa_tm.getRowCount() > 0) {

// output column info
for (int col=0; col<fa_tm.getColumnCount(); col++) {
String colName = fa_tm.getColumnName(col);
if (col == 0) {
System.out.print( " " + " " + colName );
}
else {
System.out.print( " / " + colName );
}
}
System.out.println();

// output row info
for (int row=0; row<fa_tm.getRowCount(); row++) {
for (int col=0; col<fa_tm.getColumnCount(); col++) {
String val = fa_tm.getValueAt(row,col).toString();
if (col == 0) {
System.out.print( " " + " " + val );
}
else {
System.out.print( " / " + val );
}
}
System.out.println();
}
}
else {
System.out.println( " " + " None" );
}

pd_tm = new ILPromoteDemoteTableModel(fol_ao);
System.out.println( " " + "Release Procedures: ");

if (pd_tm.getRowCount() > 0) {

// output column info
for (int col=0; col<pd_tm.getColumnCount(); col++) {
String colName = pd_tm.getColumnName(col);
if (col == 0) {
System.out.print( " " + " " + colName );
}
else {
System.out.print( " / " + colName );
}
}
System.out.println();

// output row info
for (int row=0; row<pd_tm.getRowCount(); row++) {
String rellevel = pd_tm.getValueAt(row,pd_tm.RELLEVEL_COLUMN).toString();
String promote_rp = pd_tm.getValueAt(row,pd_tm.PROMOTE_COLUMN).toString();
String demote_rp = pd_tm.getValueAt(row,pd_tm.DEMOTE_COLUMN).toString();
System.out.println( " " + " " + rellevel + " / " + promote_rp + " / " + demote_rp );
}
}
else {
System.out.println( " " + " None" );
}


pd_tm = null;
fa_tm = null;

fol_oi = null;
fol_ao = null;

System.out.println();
System.out.flush();
}
 

The output should look something like this:

  1: Root Folder/Project X
Release Scheme: ReleaseScheme-ProjectX
File Space: Filespace_ProjectX
Authorizations:
Name / Folder Role / WIP / Detail / Prototype / Manufacturing / Obsolete
*Admin / FolAdmin / Admin / Admin / Admin / Admin / Admin
*Engr / FolEngr / Engr / Engr / Engr / Engr / Engr
*Mgr / FolMgr / Mgr / Mgr / Mgr / Mgr / Mgr
*View / FolView / View / View / View / View / View
Release Procedures:
Name / Release Procedure Promote To / Release Procedure Demote From
WIP / /
Detail / /
Prototype / ProjectX_Mgr / ProjectX_Mgr
Manufacturing / ProjectX_Mgr / ProjectX_Mgr
Obsolete / /
 

As you can see, you can extract very specific information from Intralink quite easily with Java. This information would be very difficult, if not impossible, to extract using the GUI itself. A little bit of Intralink Scripting can often save you lots of time.


Need a program for data mining your Intralink metadata? Migrating from Intralink 3.x to PDM.Link or TeamCenter and need to extract metadata quickly? Contact me at MarcMettes@InversionConsulting.com to get an inexpensive application right now.