FileDocCategorySizeDatePackage
CityInputMethod.javaAPI DocExample27286Wed Apr 19 11:19:40 BST 2000com.sun.demos.cityim.internal

CityInputMethod.java

/*
 * @(#)CityInputMethod.java	1.2 99/12/03
 * 
 * Copyright 1999 by Sun Microsystems, Inc.,
 * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A.
 * All rights reserved.
 */

package com.sun.demos.cityim.internal;

import java.awt.AWTEvent;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Label;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.InputEvent;
import java.awt.event.InputMethodEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.font.TextAttribute;
import java.awt.font.TextHitInfo;
import java.awt.im.InputMethodHighlight;
import java.awt.im.spi.InputMethod;
import java.awt.im.spi.InputMethodContext;
import java.io.InputStream;
import java.io.IOException;
import java.text.AttributedString;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Properties;
import java.util.Vector;
import java.text.MessageFormat;


public class CityInputMethod implements InputMethod {

    private static Locale YOMI = new Locale("ja", "JP", "YOMI");
    private static Locale[] SUPPORTED_LOCALES = {
        Locale.ENGLISH, Locale.GERMAN,
        Locale.JAPANESE, YOMI,
        Locale.SIMPLIFIED_CHINESE, Locale.TRADITIONAL_CHINESE
    };
    private static Locale[] LOOKUP_LOCALES = {
        Locale.ENGLISH, Locale.GERMAN,
        Locale.JAPANESE, YOMI,
        Locale.CHINESE,
        Locale.SIMPLIFIED_CHINESE, Locale.TRADITIONAL_CHINESE
    };

    // lookup tables - shared by all instances of this input method    
    private static Properties cityNames;
    private static Properties cityAliases;
    private static Properties cityLanguages;
    private static Properties templates;

    // windows - shared by all instances of this input method
    private static Window statusWindow;
    // current or last statusWindow owner instance
    private static CityInputMethod statusWindowOwner;
    // true if Solaris style; false if PC style
    private static boolean attachedStatusWindow = false;
    // remember the statusWindow location per instance
    private Rectangle clientWindowLocation;
    // status window location in PC style
    private static Point globalStatusWindowLocation;
    // keep live city input method instances (synchronized using statusWindow)
    private static HashSet cityInputMethodInstances = new HashSet(5);

    // lookup information - per instance
    private String[] lookupCandidates;
    private Locale[] lookupLocales;
    private int lookupCandidateCount;
    private LookupList lookupList;
    private int lookupSelection;
    
    // per-instance state
    InputMethodContext inputMethodContext;
    private boolean active;
    private boolean disposed;
    private Locale locale;
    private boolean converted;
    private StringBuffer rawText;
    private String convertedText;
    
    private int insertionPoint;
    private String[] rawTextSegs = null;
    private String[] convertedSegs = null;
    private String fmt = null;
    private int fieldPos[][] = null;
    private int segPos[][] = null;
    private int selectedSeg = 0;
    private int numSegs = 0;
    private int committedSeg = -1;
    private int previouslyCommittedCharacterCount = 0;

    public CityInputMethod() throws IOException {
        initializeTables();
        rawText = new StringBuffer();
    }
    
    public void setInputMethodContext(InputMethodContext context) {
        inputMethodContext = context;
	if (statusWindow == null) {
            Window sw = context.createInputMethodWindow("City Input Method", false);
	    Label label = new Label();
	    label.setForeground(Color.black);
	    label.setBackground(Color.white);
	    synchronized (this.getClass()) {
		if (statusWindow == null) {
		    statusWindow = sw;
		    statusWindow.addComponentListener(new ComponentListener() {
			public void componentResized(ComponentEvent e) {}
			public void componentMoved(ComponentEvent e) {
			    synchronized (statusWindow) {
				if (!attachedStatusWindow) {
				    Component comp = e.getComponent();
				    if (comp.isVisible()) {
					globalStatusWindowLocation = comp.getLocation();
				    }
				}
			    }
			}
			public void componentShown(ComponentEvent e) {}
			public void componentHidden(ComponentEvent e) {}
		    });
		    label.addMouseListener(new MouseListener() {
			public void mouseClicked(MouseEvent e) {
			    int count = e.getClickCount();
			    if (count >= 2) {
				toggleStatusWindowStyle();
			    }
			}
			public void mousePressed(MouseEvent e) {}
			public void mouseReleased(MouseEvent e) {}
			public void mouseEntered(MouseEvent e) {}
			public void mouseExited(MouseEvent e) {}
		    });
		    statusWindow.add(label);
		    statusWindowOwner = this;
		    updateStatusWindow(locale);
		    label.setSize(200, 50);
		    statusWindow.pack();
		}
	    }
	}
	inputMethodContext.enableClientWindowNotification(this, attachedStatusWindow);
	synchronized (statusWindow) {
	    cityInputMethodInstances.add(this);
	}
    }
    
    public boolean setLocale(Locale locale) {
        for (int i = 0; i < SUPPORTED_LOCALES.length; i++) {
            if (locale.equals(SUPPORTED_LOCALES[i])) {
                this.locale = locale;
		if (statusWindow != null) {
		    updateStatusWindow(locale);
		}
                return true;
            }
        }
        return false;
    }
    
    public Locale getLocale() {
        return locale;
    }
    
    void updateStatusWindow(Locale locale) {
	synchronized (statusWindow) {
	    Label label = (Label) statusWindow.getComponent(0);
	    String localeName = locale == null ? "null" : locale.getDisplayName();
	    String text = "Current locale: " + localeName;
	    if (!label.getText().equals(text)) {
		label.setText(text);
		statusWindow.pack();
	    }
	    if (attachedStatusWindow) {
		if (clientWindowLocation != null) {
		    statusWindow.setLocation(clientWindowLocation.x,
					     clientWindowLocation.y + clientWindowLocation.height);
		}
	    } else {
		setPCStyleStatusWindow();
	    }
	}
    }

    private void setPCStyleStatusWindow() {
	synchronized (statusWindow) {
	    if (globalStatusWindowLocation == null) {
		Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
		globalStatusWindowLocation = new Point(d.width - statusWindow.getWidth(),
						       d.height - statusWindow.getHeight() - 25);
	    }
	    statusWindow.setLocation(globalStatusWindowLocation.x, globalStatusWindowLocation.y);
	}
    }

    private void setStatusWindowForeground(Color fg) {
	synchronized (statusWindow) {
	    if (statusWindowOwner != this) {
		return;
	    }
	    Label label = (Label) statusWindow.getComponent(0);
	    label.setForeground(fg);
	}
    }

    private void toggleStatusWindowStyle() {
	synchronized (statusWindow) {
	    if (attachedStatusWindow) {
		attachedStatusWindow = false;
		setPCStyleStatusWindow();
	    } else {
		attachedStatusWindow = true;
	    }
	    Iterator itr = cityInputMethodInstances.iterator();
	    while (itr.hasNext()) {
		CityInputMethod im = (CityInputMethod)itr.next();
		im.inputMethodContext.enableClientWindowNotification(im, attachedStatusWindow);
	    }
	}
    }	    

    public void setCharacterSubsets(Character.Subset[] subsets) {
        // ignore
    }

    public void reconvert() {
	// not supported yet
	throw new UnsupportedOperationException();
    }

    public void dispatchEvent(AWTEvent event) {
        if (!active && (event instanceof KeyEvent)) {
            System.out.println("CityInputMethod.dispatch called with KeyEvent while not active");
        }
        if (disposed) {
            System.out.println("CityInputMethod.dispatch called after being disposed");
        }
        if (!(event instanceof InputEvent)) {
            System.out.println("CityInputMethod.dispatch called with event that's not an InputEvent");
        }
	if (event.getID() == KeyEvent.KEY_RELEASED) {
	    if (lookupList != null) { // if candidate window is active
		KeyEvent e = (KeyEvent) event;
		if (e.isControlDown()) {
		    if (e.getKeyCode() == KeyEvent.VK_DOWN) {
		        // Control + Arrow Down commits chunks
			closeLookupWindow();
			commit(selectedSeg);
			e.consume();
		    }
		} else {
		    // select candidate by Arrow Up/Down
		    if (e.getKeyCode() == KeyEvent.VK_DOWN) {
			if (++lookupSelection == lookupCandidateCount) {
			    lookupSelection = 0;
			}
			selectCandidate(lookupSelection);
			e.consume();
		    } else if (e.getKeyCode() == KeyEvent.VK_UP) {
			if (--lookupSelection < 0) {
			    lookupSelection = lookupCandidateCount;
			}
			selectCandidate(lookupSelection);
			e.consume();
		    }
		}
	    } else {
		if (event.getID() == KeyEvent.KEY_RELEASED) {
		    KeyEvent e = (KeyEvent) event;
		    if (e.isControlDown()) {
			if (e.getKeyCode() == KeyEvent.VK_DOWN) {
			    // Control + Arrow Down commits chunks
			    commit(selectedSeg);
			    e.consume();
			}
		    } else {
		        // move selected segment by Arrow Right/Left
			if ((e.getKeyCode() == KeyEvent.VK_RIGHT) && (converted == true)) {
			    if (selectedSeg < (numSegs - 1)) {
				selectedSeg++;
				sendText(false);
				e.consume();
			    } else {
				Toolkit.getDefaultToolkit().beep();
			    }
			} else if ((e.getKeyCode() == KeyEvent.VK_LEFT) && (converted == true)) {
			    if (selectedSeg > (committedSeg + 1)) {
				selectedSeg--;
				sendText(false);
				e.consume();
			    } else {
				Toolkit.getDefaultToolkit().beep();
			    }
			}
		    }
		}
	    }
	}
	if (event.getID() == MouseEvent.MOUSE_CLICKED) {
	    MouseEvent e = (MouseEvent) event;
	    Component comp = e.getComponent();
	    Point pnt = comp.getLocationOnScreen();
	    int x = (int)pnt.getX() + e.getX();
	    int y = (int)pnt.getY() + e.getY();
	    TextHitInfo hit = inputMethodContext.getLocationOffset(x,y);
	    if (hit != null) {
	        // within composed text
		if (converted) {
		    selectedSeg = findSegment(hit.getInsertionIndex());
		    sendText(false);
		    e.consume();
		} else {
		    insertionPoint = hit.getInsertionIndex();
		}
	    } else {
	        // if hit outside composition, simply commit all.
		commitAll();
	    }
	}
        if (event.getID() == KeyEvent.KEY_TYPED) {
            KeyEvent e = (KeyEvent) event;
            if (handleCharacter(e.getKeyChar())) {
                e.consume();
            }
        }
    }
    
    /**
     * find segment at insertion point
     */
    int findSegment(int insertion) {
	for (int i = committedSeg + 1; i < numSegs; i++) {
	    if ((segPos[i][0] < insertion) && (insertion < segPos[i][1])) {
		return i;
	    }
	}
	return 0;
    }

    public void activate() {
        if (active) {
            System.out.println("CityInputMethod.activate called while active");
        }
        active = true;
	synchronized (statusWindow) {
	    statusWindowOwner = this; 
	    updateStatusWindow(locale);
	    if (!statusWindow.isVisible()) {
		statusWindow.setVisible(true);
	    }
	    setStatusWindowForeground(Color.black);
	}
    }
    
    public void deactivate(boolean isTemporary) {
        closeLookupWindow();
        if (!active) {
            System.out.println("CityInputMethod.deactivate called while not active");
        }
	setStatusWindowForeground(Color.lightGray);
        active = false;
    }
    
    public void hideWindows() {
        if (active) {
            System.out.println("CityInputMethod.hideWindows called while active");
        }
        closeLookupWindow();
	synchronized (statusWindow) {
	    if (statusWindowOwner == this) {
		statusWindow.setVisible(false);
	    }
	}
    }
    
    public void removeNotify() {
    }
    
    public void endComposition() {
        if (rawText.length() != 0) {
            commitAll();
        }
        closeLookupWindow();
    }

    public void notifyClientWindowChange(Rectangle location) {
	clientWindowLocation = location;
	synchronized (statusWindow) {
	    if (!attachedStatusWindow || statusWindowOwner != this) {
		return;
	    }
	    if (location != null) {
		statusWindow.setLocation(location.x, location.y+location.height);
		if (!statusWindow.isVisible()) {
		    if (active) {
			setStatusWindowForeground(Color.black);
		    } else {
			setStatusWindowForeground(Color.lightGray);
		    }
		    statusWindow.setVisible(true);
		}
	    } else {
		statusWindow.setVisible(false);
	    }
	}
    }

    public void dispose() {
        if (active) {
            System.out.println("CityInputMethod.dispose called while active");
        }
        if (disposed) {
            System.out.println("CityInputMethod.disposed called repeatedly");
        }
        closeLookupWindow();
	synchronized (statusWindow) {
	    cityInputMethodInstances.remove(this);
	}
        disposed = true;
    }
    
    public Object getControlObject() {
        return null;
    }
    
    public void setCompositionEnabled(boolean enable) {
	// not supported yet
	throw new UnsupportedOperationException();
    }

    public boolean isCompositionEnabled() {
        // always enabled
	return true;
    }

    private void initializeTables() throws IOException {
        synchronized (this.getClass()) {
            if (templates == null) {
                cityNames = loadProperties("names.properties");
                cityAliases = loadProperties("aliases.properties");
                cityLanguages = loadProperties("languages.properties");
                templates = loadProperties("templates.properties");
            }
        }
    }
        
    private Properties loadProperties(String fileName) throws IOException {
        Properties props = new Properties();
        InputStream stream = this.getClass().getResourceAsStream(fileName);
        props.load(stream);
        stream.close();
        return props;
    }
    
    /**
     * Attempts to handle a typed character.
     * @return whether the character was handled
     */
    private boolean handleCharacter(char ch) {
        if (lookupList != null) {
            if (ch == ' ') {
                if (++lookupSelection == lookupCandidateCount) {
                    lookupSelection = 0;
                }
                selectCandidate(lookupSelection);
                return true;
            } else if (ch == '\n') {
                commitAll();
                closeLookupWindow();
                return true;
            } else if ('1' <= ch && ch <= '0' + lookupCandidateCount) {
                selectCandidate(ch - '1');
                closeLookupWindow();
                return true;
            } else {
                Toolkit.getDefaultToolkit().beep();
                return true;
            }
        } else if (converted) {
            if (ch == ' ') {
                convertAgain();
                return true;
            } else if (ch == '\n') {
                commitAll();
                return true;
            } else {
                Toolkit.getDefaultToolkit().beep();
                return true;
            }
        } else {
            if (ch == ' ') {
                int length = rawText.length();
                if (length == 3 || length == 6 || length == 9) {
                    convertFirstTime();
                    return true;
                }
            } else if (ch == '\n') {
                if (rawText.length() != 0) {
                    commitAll();
                    return true;
                }
            } else if (ch == '\b') {
                if (insertionPoint > 0) {
		    rawText.deleteCharAt(insertionPoint - 1);
		    --insertionPoint;
                    sendText(false);
                    return true;
                }
            } else if ('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z') {
		rawText.insert(insertionPoint++, ch);
                sendText(false);
                return true;
            }
            if (rawText.length() != 0) {
                Toolkit.getDefaultToolkit().beep();
                return true;
            }
        }
        return false;
    }
    
    /*
     * Looks up the entry for key in the given table, taken the
     * input method's locale into consideration.
     */
    String lookup(String lookupName, Properties table) {
	String result = null;
        String localeLookupName = lookupName + "_" + locale;
        while (true) {
            result = (String) table.get(localeLookupName);
            if (result != null) {
                break;
            }
            int index = localeLookupName.lastIndexOf("_");
            if (index == -1) {
                break;
            }
            localeLookupName = localeLookupName.substring(0, index);
        }
	return result;
    }

    String findAlias(String lookupName) {
        lookupName = lookupName.toUpperCase();
	return cityAliases.getProperty(lookupName, lookupName);
    }

    void convertFirstTime() {
	numSegs = rawText.length() / 3;
	rawTextSegs = new String[numSegs];
	convertedSegs = new String[numSegs];
	for (int i = 0; i < numSegs; ++i) {
	    rawTextSegs[i] = rawText.substring(i * 3, (i + 1) *3);
	    String alias = findAlias(rawTextSegs[i]);
	    String result = lookup(alias, cityNames);
	    if (result != null) {
		convertedSegs[i] = result;
	    } else {
		convertedSegs[i] = rawText.substring(i * 3, (i + 1) * 3);
	    }
	}
	converted = true;
	sendText(false);
    }
	
    void convertAgain() {
        String lookupName;
	lookupName = rawTextSegs[selectedSeg];
	// if converted string is same as original, it's not in word list. We skip
	// further conversion.
	if (!lookupName.equals(convertedSegs[selectedSeg])) {
	    lookupName = findAlias(lookupName);
	    lookupCandidates = new String[LOOKUP_LOCALES.length];
	    lookupLocales = new Locale[LOOKUP_LOCALES.length];
	    lookupCandidateCount = 0;
	    lookupSelection = 0;
	    for (int i = 0; i < LOOKUP_LOCALES.length; i++) {
		Locale iLocale = LOOKUP_LOCALES[i];
		String localeLookupName = lookupName + '_' + iLocale;
		String localeConvertedText = (String) cityNames.get(localeLookupName);
		if (localeConvertedText != null) {
		    lookupCandidates[lookupCandidateCount] = localeConvertedText;
		    lookupLocales[lookupCandidateCount] = iLocale;
		    lookupCandidateCount++;
		} else if (iLocale.equals(Locale.ENGLISH)) {
		    localeConvertedText = (String) cityNames.get(lookupName);
		    if (localeConvertedText != null) {
			lookupCandidates[lookupCandidateCount] = localeConvertedText;
			lookupLocales[lookupCandidateCount] = iLocale;
			lookupCandidateCount++;
		    }
		}
		if (convertedSegs[selectedSeg].equals(localeConvertedText)) {
		    lookupSelection = lookupCandidateCount - 1;
		}
	    }
	    openLookupWindow();
	} else {
	    Toolkit.getDefaultToolkit().beep();
	}
    }

    /* commits all chunks up to the specified index */  
    private void commit(int index) {
	if (index >= (numSegs - 1)) {
	    // if this is the last segment, commit all
	    commitAll();
	} else {
	    committedSeg = index;
	    selectedSeg = committedSeg + 1;
	    sendText(true);
	}
    }
    
    /* commits all chunks */
    void commitAll() {
	committedSeg = numSegs - 1;
        sendText(true);
	// once composed text is committed, reinitialize all variables
	rawText.setLength(0);
        convertedText = null;
        converted = false;

	rawTextSegs = null;
	convertedSegs = null;
	fmt = null;
	fieldPos = null;
	segPos = null;
	selectedSeg = 0;
	insertionPoint = rawText.length();
	numSegs = 0;
	committedSeg = -1;
	previouslyCommittedCharacterCount = 0;
    }
    
    void parseFormat() {
	Vector vec = new Vector();
	int[] elem = null;

	for(int i = 0; i < fmt.length(); i++) {
	    if (fmt.charAt(i) == '{') {
		elem = new int[2];
		elem[0] = i;
	    } else if (fmt.charAt(i) == '}') {
		elem[1] = i;
		vec.add(elem);
	    }
	}
	if (vec.size() != 0) {
	    fieldPos = new int[vec.size()][];
	    vec.toArray(fieldPos);
	}
    }

    void formatOutput() {
	if (fmt == null) {
	    String key = "Template" + Integer.toString(numSegs);
	    fmt = lookup(key, templates);
	    parseFormat();
	}
	convertedText = MessageFormat.format(fmt, (Object [])convertedSegs);
	// Figure out converted segment position
	int errors = 0;
	segPos = new int[fieldPos.length][2];
	for (int i = 0; i < fieldPos.length; i++) {
	    int optLen = (fieldPos[i][1] - fieldPos[i][0]) + 1;
	    int diffs = convertedSegs[i].length() - optLen;
	    segPos[i][0] = fieldPos[i][0] + errors;
	    segPos[i][1] = segPos[i][0] + convertedSegs[i].length();
	    errors += diffs;
	}
    }

    void sendText(boolean committed) {
	AttributedString as = null;
	TextHitInfo caret = null;
	int committedCharacterCount = 0;
	int newTotalCommittedCharacterCount = previouslyCommittedCharacterCount;

	if (converted) {
	    formatOutput();
	    if (committed) {
		if (committedSeg == (numSegs - 1)) {
		    newTotalCommittedCharacterCount = convertedText.length();
		} else {
		    newTotalCommittedCharacterCount = segPos[committedSeg + 1][0];
		}
		committedCharacterCount = newTotalCommittedCharacterCount - previouslyCommittedCharacterCount;
	    }
            as = new AttributedString(convertedText.substring(previouslyCommittedCharacterCount));
            for(int i = committedSeg + 1; i < numSegs; i++) {
                InputMethodHighlight highlight;
                if (i == selectedSeg) {
                    highlight = InputMethodHighlight.SELECTED_CONVERTED_TEXT_HIGHLIGHT;
                } else {
                    highlight = InputMethodHighlight.UNSELECTED_CONVERTED_TEXT_HIGHLIGHT;
                }
                as.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT, highlight,
                                segPos[i][0] - previouslyCommittedCharacterCount,
                                segPos[i][1] - previouslyCommittedCharacterCount);
            }
            previouslyCommittedCharacterCount = newTotalCommittedCharacterCount;
	} else {
	    as = new AttributedString(rawText.toString());
	    if (committed) {
	        committedCharacterCount = rawText.length();
	    } else if (rawText.length() != 0) {
		as.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT,
				InputMethodHighlight.SELECTED_RAW_TEXT_HIGHLIGHT);
	        caret = TextHitInfo.leading(insertionPoint);
	    }
	}
	inputMethodContext.dispatchInputMethodEvent(
						    InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
						    as.getIterator(),
						    committedCharacterCount,
						    caret,
						    null);
    }
    
    void selectCandidate(int candidate) {
        lookupSelection = candidate;
        lookupList.selectCandidate(lookupSelection);
        convertedSegs[selectedSeg] = lookupCandidates[lookupSelection];
        sendText(false);
    }
    
    int getSelectedSegmentOffset() {
        return segPos[selectedSeg][0] - previouslyCommittedCharacterCount;
    }
    
    void openLookupWindow() {
        lookupList = new LookupList(this, inputMethodContext, 
				    lookupCandidates, lookupLocales, lookupCandidateCount);
        lookupList.selectCandidate(lookupSelection);
    }
    
    void closeLookupWindow() {
        if (lookupList != null) {
            lookupList.setVisible(false);
            lookupList = null;
        }
    }
}

class LookupList extends Canvas {

    CityInputMethod inputMethod;
    InputMethodContext context;
    Window lookupWindow;
    String[] candidates;
    Locale[] locales;
    int candidateCount;
    int selected;
    
    final int INSIDE_INSET = 4;
    final int LINE_SPACING = 18;
    
    LookupList(CityInputMethod inputMethod, InputMethodContext context, 
	       String[] candidates, Locale[] locales, int candidateCount) {
        this.inputMethod = inputMethod;
        this.context = context;
        this.candidates = candidates;
        this.locales = locales;
        this.candidateCount = candidateCount;
        
        lookupWindow = context.createInputMethodWindow("Lookup Window", true);
        setFont(new Font("Dialog", Font.PLAIN, 12));
        setSize(200, candidateCount * LINE_SPACING + 2 * INSIDE_INSET);
        setForeground(Color.black);
        setBackground(Color.white);
        
        enableEvents(AWTEvent.KEY_EVENT_MASK);
        enableEvents(AWTEvent.MOUSE_EVENT_MASK);
        
        lookupWindow.add(this);
        lookupWindow.pack();
        updateWindowLocation();
        lookupWindow.setVisible(true);
    }

    /**
     * Positions the lookup window near (usually below) the
     * insertion point in the component where composition occurs.
     */
    private void updateWindowLocation() {
        Point windowLocation = new Point();
        int textOffset = inputMethod.getSelectedSegmentOffset();
        Rectangle caretRect = context.getTextLocation(TextHitInfo.leading(textOffset));
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension windowSize = lookupWindow.getSize();
        final int SPACING = 2;
    
	if (caretRect.x + windowSize.width > screenSize.width) {
            windowLocation.x = screenSize.width - windowSize.width;
        } else {
            windowLocation.x = caretRect.x;
        }
        
	if (caretRect.y + caretRect.height + SPACING + windowSize.height > screenSize.height) {
            windowLocation.y = caretRect.y - SPACING - windowSize.height;         
        } else {
            windowLocation.y = caretRect.y + caretRect.height + SPACING;
        }

        lookupWindow.setLocation(windowLocation);
    }
    
    void selectCandidate(int candidate) {
        selected = candidate;
        repaint();
    }
    
    public void paint(Graphics g) {
        FontMetrics metrics = g.getFontMetrics();
        int descent = metrics.getDescent();
        int ascent = metrics.getAscent();
        for (int i = 0; i < candidateCount; i++) {
            g.drawString((i + 1) + "   " + candidates[i] + 
                    "  (" + locales[i].getDisplayName() + ")",
                    INSIDE_INSET, LINE_SPACING * (i + 1) + INSIDE_INSET - descent);
        }
        Dimension size = getSize();
        g.drawRect(INSIDE_INSET / 2,
                LINE_SPACING * ( selected + 1) + INSIDE_INSET - (descent + ascent + 1),
                size.width - INSIDE_INSET,
                descent + ascent + 2);
        g.drawRect(0, 0, size.width - 1, size.height - 1);
    }
    
    public void setVisible(boolean visible) {
        if (!visible) {
            lookupWindow.setVisible(false);
            lookupWindow.dispose();
            lookupWindow = null;
        }
        super.setVisible(visible);
    }
    
    protected void processKeyEvent(KeyEvent event) {
        inputMethod.dispatchEvent(event);
    }
    
    protected void processMouseEvent(MouseEvent event) {
        if (event.getID() == MouseEvent.MOUSE_PRESSED) {
            int y = event.getY();
            if (y >= INSIDE_INSET && y < INSIDE_INSET + candidateCount * LINE_SPACING) {
                inputMethod.selectCandidate((y - INSIDE_INSET) / LINE_SPACING);
                inputMethod.closeLookupWindow();
            }
        }
    }
}