15. User Interfaces

Parsing Program Arguments

//----------------------------------------------------------------------------------
// The are several Java options builder packages available. Some popular ones:
//   Apache Jakarta Commons CLI: http://jakarta.apache.org/commons/cli/
//   jopt-simple: http://jopt-simple.sourceforge.net
//   args4j: https://args4j.dev.java.net/ (requires Java 5 with annotations)
//   jargs: http://jargs.sourceforge.net/
//   te-code: http://te-code.sourceforge.net/article-20041121-cli.html
// Most of these can be used from Groovy with some Groovy code benefits.
// Groovy also has the CliBuilder built right in.


// CliBuilder example
def cli = new CliBuilder()
cli.v(longOpt: 'verbose', 'verbose mode')
cli.D(longOpt: 'Debug', 'display debug info')
cli.o(longOpt: 'output', 'use/specify output file')
def options = cli.parse(args)
if (options.v) // ...
if (options.D) println 'Debugging info available'
if (options.o) {
    println 'Output file flag was specified'
    println "Output file is ${options.o}"
}
// ...


// jopt-simple example 1 (short form)
cli = new joptsimple.OptionParser("vDo::")
options = cli.parse(args)
if (options.wasDetected('o')) {
    println 'Output file flag was specified.'
    println "Output file is ${options.argumentsOf('o')}"
}
// ...


// jopt-simple example 2 (declarative form)
op = new joptsimple.OptionParser()
VERBOSE = 'v';  op.accepts( VERBOSE,  "verbose mode" )
DEBUG   = 'D';  op.accepts( DEBUG,    "display debug info" )
OUTPUT  = 'o';  op.accepts( OUTPUT,   "use/specify output file" ).withOptionalArg().
    describedAs( "file" ).ofType( File.class )
options = op.parse(args)
params = options.nonOptionArguments()
if (options.wasDetected( DEBUG )) println 'Debugging info available'
// ...
//----------------------------------------------------------------------------------

Testing Whether a Program Is Running Interactively

//----------------------------------------------------------------------------------
// Groovy like Java can be run in a variety of scenarios, not just interactive vs
// non-interative, e.g. within a servlet container. Sometimes InputStreams and other
// mechanisms are used to hide away differences between the different containers
// in which code is run; other times, code needs to be written purpose-built for
// the container in which it is running. In most situations where the latter applies
// the container will have specific lifecycle mechanisms to allow the code to
// access specific needs, e.g. javax.servlet.ServletRequest.getInputStream()
// rather than System.in
//----------------------------------------------------------------------------------

Clearing the Screen

//----------------------------------------------------------------------------------
// Idiomatically Groovy encourages GUI over text-based applications where a rich
// interface is desirable. Libraries for richer text-based interfaces include:
// jline: http://jline.sourceforge.net
// jcurses: http://sourceforge.net/projects/javacurses/
// java-readline: http://java-readline.sourceforge.net
// enigma console: http://sourceforge.net/projects/enigma-shell/
// Note: Run examples using these libraries from command line not inside an IDE.

// If you are using a terminal/console that understands ANSI codes
// (excludes WinNT derivatives) you can just print the ANSI codes
print ((char)27 + '[2J')

// jline has constants for ANSI codes
import jline.ANSIBuffer
print ANSIBuffer.ANSICodes.clrscr()
// Also available through ConsoleReader.clearScreen()

// Using jcurses
import jcurses.system.*
bg = CharColor.BLACK
fg = CharColor.WHITE
screenColors = new CharColor(bg, fg)
Toolkit.clearScreen(screenColors)
//----------------------------------------------------------------------------------

Determining Terminal or Window Size

//----------------------------------------------------------------------------------
// Not idiomatic for Groovy to use text-based applications here.

// Using jcurses: http://sourceforge.net/projects/javacurses/
// use Toolkit.screenWidth and Toolkit.screenHeight

// 'barchart' example
import jcurses.system.Toolkit
numCols = Toolkit.screenWidth
rand = new Random()
if (numCols < 20) throw new RuntimeException("You must have at least 20 characters")
values = (1..5).collect { rand.nextInt(20) }  // generate rand values
max = values.max()
ratio = (numCols - 12)/max
values.each{ i ->
    printf('%8.1f %s\n', [i as double, "*" * ratio * i])
}

// gives, for example:
//   15.0 *******************************
//   10.0 *********************
//    5.0 **********
//   14.0 *****************************
//   18.0 **************************************
// Run from command line not inside an IDE which may give false width/height values.
//----------------------------------------------------------------------------------

Changing Text Color

//----------------------------------------------------------------------------------
// Idiomatically Groovy encourages GUI over text-based applications where a rich
// interface is desirable. See 15.3 for richer text-based interface libraries.
// Note: Run examples using these libraries from command line not inside an IDE.

// If you are using a terminal/console that understands ANSI codes
// (excludes WinNT derivatives) you can just print the ANSI codes
ESC = "${(char)27}"
redOnBlack = ESC + '[31;40m'
reset = ESC + '[0m'
println (redOnBlack + 'Danger, Will Robinson!' + reset)

// jline has constants for ANSI codes
import jline.ANSIBuffer
redOnBlack = ANSIBuffer.ANSICodes.attrib(31) + ANSIBuffer.ANSICodes.attrib(40)
reset = ANSIBuffer.ANSICodes.attrib(0)
println redOnBlack + 'Danger, Will Robinson!' + reset

// Using JavaCurses
import jcurses.system.*
import jcurses.widgets.*
whiteOnBlack = new CharColor(CharColor.BLACK, CharColor.WHITE)
Toolkit.clearScreen(whiteOnBlack)
redOnBlack = new CharColor(CharColor.BLACK, CharColor.RED)
Toolkit.printString("Danger, Will Robinson!", 0, 0, redOnBlack)
Toolkit.printString("This is just normal text.", 0, 1, whiteOnBlack)
// Blink not supported by JavaCurses

// Using jline constants for Blink
blink = ANSIBuffer.ANSICodes.attrib(5)
reset = ANSIBuffer.ANSICodes.attrib(0)
println (blink + 'Do you hurt yet?' + reset)

// Using jline constants for Coral snake rhyme
def ansi(code) { ANSIBuffer.ANSICodes.attrib(code) }
redOnBlack = ansi(31) + ansi(40)
redOnYellow = ansi(31) + ansi(43)
greenOnCyanBlink = ansi(32) + ansi(46) + ansi(5)
reset = ansi(0)
println redOnBlack + "venom lack"
println redOnYellow + "kill that fellow"
println greenOnCyanBlink + "garish!" + reset
//----------------------------------------------------------------------------------

Reading from the Keyboard

//----------------------------------------------------------------------------------
// Default Java libraries buffer System.in by default.

// Using JavaCurses:
import jcurses.system.Toolkit
print 'Press a key: '
println "\nYou pressed the '${Toolkit.readCharacter().character}' key"

// Also works for special keys:
import jcurses.system.InputChar
print "Press the 'End' key to finish: "
ch = Toolkit.readCharacter()
assert ch.isSpecialCode()
assert ch.code == InputChar.KEY_END

// See also jline Terminal#readCharacter() and Terminal#readVirtualKey()
//----------------------------------------------------------------------------------

Ringing the Terminal Bell

//----------------------------------------------------------------------------------
print "${(char)7}"

// Using jline constant
print "${jline.ConsoleOperations.KEYBOARD_BELL}"
// Also available through ConsoleReader.beep()

// Using JavaCurses (Works only with terminals that support 'beeps')
import jcurses.system.Toolkit
Toolkit.beep()
//----------------------------------------------------------------------------------

Using POSIX termios

//----------------------------------------------------------------------------------
// I think you would need to resort to platform specific calls here,
// E.g. on *nix systems call 'stty' using execute().
// Some things can be set through the packages mentioned in 15.3, e.g.
// echo can be turned on and off, but others like setting the kill character
// didn't appear to be supported (presumably because it doesn't make
// sense for a cross-platform toolkit).
//----------------------------------------------------------------------------------

Checking for Waiting Input

//----------------------------------------------------------------------------------
// Consider using Java's PushbackInputStream or PushbackReader
// Different functionality to original cookbook but can be used
// as an alternative for some scenarios.
//----------------------------------------------------------------------------------

Reading Passwords

//----------------------------------------------------------------------------------
// If using Java 6, use Console.readPassword()
// Otherwise use jline (use 0 instead of mask character '*' for no echo):
password = new jline.ConsoleReader().readLine(new Character('*'))
//----------------------------------------------------------------------------------

Editing Input

//----------------------------------------------------------------------------------
// In Groovy (like Java) normal input is buffered so you can normally make
// edits before hitting 'Enter'. For more control over editing (including completion
// and history etc.) use one of the packages mentioned in 15.3, e.g. jline.
//----------------------------------------------------------------------------------

Managing the Screen

//----------------------------------------------------------------------------------
// Use javacurses or jline (see 15.3) for low level screen management.
// Java/Groovy would normally use a GUI for such functionality.

// Here is a slight variation to cookbook example. This repeatedly calls
// the command feedin on the command line, e.g. "cmd /c dir" on windows
// or 'ps -aux' on Linux. Whenever a line changes, the old line is "faded
// out" using font colors from white through to black. Then the new line
// is faded in using the reverse process.
import jcurses.system.*
color = new CharColor(CharColor.BLACK, CharColor.WHITE)
Toolkit.clearScreen(color)
maxcol = Toolkit.screenWidth
maxrow = Toolkit.screenHeight
colors = [CharColor.WHITE, CharColor.CYAN, CharColor.YELLOW, CharColor.GREEN,
          CharColor.RED, CharColor.BLUE, CharColor.MAGENTA, CharColor.BLACK]
done = false
refresh = false
waittime = 8000
oldlines = []
def fade(line, row, colorList) {
    for (i in 0..<colorList.size()) {
        Toolkit.printString(line, 0, row, new CharColor(CharColor.BLACK, colorList[i]))
        sleep 10
    }
}
while(!done) {
    if (waittime > 9999 || refresh) {
        proc = args[0].execute()
        lines = proc.text.split('\n')
        for (r in 0..<maxrow) {
            if (r >= lines.size() || r > oldlines.size() || lines[r] != oldlines[r]) {
                if (oldlines != [])
                    fade(r < oldlines.size() ? oldlines[r] : ' ' * maxcol, r, colors)
                fade(r < lines.size() ? lines[r] : ' ' * maxcol, r, colors.reverse())
            }
        }
        oldlines = lines
        refresh = false
        waittime = 0
    }
    waittime += 200
    sleep 200
}

// Keyboard handling would be similar to 15.6.
// Something like below but need to synchronize as we are in different threads.
Thread.start{
    while(!done) {
        ch = Toolkit.readCharacter()
        if (ch.isSpecialCode() || ch.character == 'q') done = true
        else refresh = true
    }
}
//----------------------------------------------------------------------------------

Controlling Another Program with Expect

//----------------------------------------------------------------------------------
// These examples uses expectj, a pure Java Expect-like module.
// http://expectj.sourceforge.net/
defaultTimeout = -1 // infinite
expect = new expectj.ExpectJ("logfile.log", defaultTimeout)
command = expect.spawn("program to run")
command.expect('Password', 10)
// expectj doesn't support regular expressions, but see readUntil
// in recipe 18.6 for how to manually code this
command.expect('invalid')
command.send('Hello, world\r')
// kill spawned process
command.stop()

// expecting multiple choices
// expectj doesn't support multiple choices, but see readUntil
// in recipe 18.6 for how to manually code this
//----------------------------------------------------------------------------------

Creating Menus with Tk

//----------------------------------------------------------------------------------
// Methods not shown for the edit menu items, they would be the same as for the
// file menu items.
import groovy.swing.SwingBuilder
def print() {}
def save() {}
frame = new SwingBuilder().frame(title:'Demo') {
    menuBar {
        menu(mnemonic:'F', 'File') {
            menuItem (actionPerformed:this.&print, 'Print')
            separator()
            menuItem (actionPerformed:this.&save, 'Save')
            menuItem (actionPerformed:{System.exit(0)}, 'Quit immediately')
        }
        menu(mnemonic:'O', 'Options') {
            checkBoxMenuItem ('Create Debugging Info', state:true)
        }
        menu(mnemonic:'D', 'Debug') {
            group = buttonGroup()
            radioButtonMenuItem ('Log Level 1', buttonGroup:group, selected:true)
            radioButtonMenuItem ('Log Level 2', buttonGroup:group)
            radioButtonMenuItem ('Log Level 3', buttonGroup:group)
        }
        menu(mnemonic:'F', 'Format') {
            menu('Font') {
                group = buttonGroup()
                radioButtonMenuItem ('Times Roman', buttonGroup:group, selected:true)
                radioButtonMenuItem ('Courier', buttonGroup:group)
            }
        }
        menu(mnemonic:'E', 'Edit') {
            menuItem (actionPerformed:{}, 'Copy')
            menuItem (actionPerformed:{}, 'Cut')
            menuItem (actionPerformed:{}, 'Paste')
            menuItem (actionPerformed:{}, 'Delete')
            separator()
            menu('Object ...') {
                menuItem (actionPerformed:{}, 'Circle')
                menuItem (actionPerformed:{}, 'Square')
                menuItem (actionPerformed:{}, 'Point')
            }
        }
    }
}
frame.pack()
frame.show()
//----------------------------------------------------------------------------------

Creating Dialog Boxes with Tk

//----------------------------------------------------------------------------------
// Registration Example
import groovy.swing.SwingBuilder
def cancel(event) {
    println 'Sorry you decided not to register.'
    dialog.dispose()
}
def register(event) {
    if (swing.name?.text) {
        println "Welcome to the fold $swing.name.text"
        dialog.dispose()
    } else println "You didn't give me your name!"
}
def dialog(event) {
    dialog = swing.createDialog(title:'Entry')
    def panel = swing.panel {
        vbox {
            hbox {
                label(text:'Name')
                textField(columns:20, id:'name')
            }
            hbox {
                button('Register', actionPerformed:this.&register)
                button('Cancel', actionPerformed:this.&cancel)
            }
        }
    }
    dialog.getContentPane().add(panel)
    dialog.pack()
    dialog.show()
}
swing = new SwingBuilder()
frame = swing.frame(title:'Registration Example') {
    panel {
        button(actionPerformed:this.&dialog, 'Click Here For Registration Form')
        glue()
        button(actionPerformed:{System.exit(0)}, 'Quit')
    }
}
frame.pack()
frame.show()


// Error Example, slight variation to original cookbook
import groovy.swing.SwingBuilder
import javax.swing.WindowConstants as WC
import javax.swing.JOptionPane
def calculate(event) {
    try {
        swing.result.text = evaluate(swing.expr.text)
    } catch (Exception ex) {
        JOptionPane.showMessageDialog(frame, ex.message)
    }
}
swing = new SwingBuilder()
frame = swing.frame(title:'Calculator Example',
    defaultCloseOperation:WC.EXIT_ON_CLOSE) {
    panel {
        vbox {
            hbox {
                label(text:'Expression')
                hstrut()
                textField(columns:12, id:'expr')
            }
            hbox {
                label(text:'Result')
                glue()
                label(id:'result')
            }
            hbox {
                button('Calculate', actionPerformed:this.&calculate)
                button('Quit', actionPerformed:{System.exit(0)})
            }
        }
    }
}
frame.pack()
frame.show()
//----------------------------------------------------------------------------------

Responding to Tk Resize Events

//----------------------------------------------------------------------------------
// Resizing in Groovy follows Java rules, i.e. is dependent on the layout manager.
// You can set preferred, minimum and maximum sizes (may be ignored by some layout managers).
// You can setResizable(false) for some components.
// You can specify a weight value for some layout managers, e.g. GridBagLayout
// which control the degree of scaling which occurs during resizing.
// Some layout managers, e.g. GridLayout, automaticaly resize their contained widgets.
// You can capture resize events and do everything manually yourself.
//----------------------------------------------------------------------------------

Removing the DOS Shell Window with Windows Perl/Tk

//----------------------------------------------------------------------------------
// Removing DOS console on Windows:
// If you are using java.exe to start your Groovy script, use javaw.exe instead.
// If you are using groovy.exe to start your Groovy script, use groovyw.exe instead.
//----------------------------------------------------------------------------------

Program: Small termcap program

//----------------------------------------------------------------------------------
// additions to original cookbook:
// random starting position
// color changes after each bounce
import jcurses.system.*
color = new CharColor(CharColor.BLACK, CharColor.WHITE)
Toolkit.clearScreen(color)
rand = new Random()
maxrow = Toolkit.screenWidth
maxcol = Toolkit.screenHeight
rowinc = 1
colinc = 1
row = rand.nextInt(maxrow)
col = rand.nextInt(maxcol)
chars = '*-/|\\_'
colors = [CharColor.RED, CharColor.BLUE, CharColor.YELLOW,
          CharColor.GREEN, CharColor.CYAN, CharColor.MAGENTA]
delay = 20
ch = null
def nextChar(){
    ch = chars[0]
    chars = chars[1..-1] + chars[0]
    color = new CharColor(CharColor.BLACK, colors[0])
    colors = colors[1..-1] + colors[0]
}
nextChar()
while(true) {
    Toolkit.printString(ch, row, col, color)
    sleep delay
    row = row + rowinc
    col = col + colinc
    if (row in [0, maxrow]) { nextChar(); rowinc = -rowinc }
    if (col in [0, maxcol]) { nextChar(); colinc = -colinc }
}
//----------------------------------------------------------------------------------

Program: tkshufflepod

//----------------------------------------------------------------------------------
// Variation to cookbook. Let's you reshuffle lines in a multi-line string
// by drag-n-drop.
import java.awt.*
import java.awt.datatransfer.*
import java.awt.dnd.*
import javax.swing.*
import javax.swing.ScrollPaneConstants as SPC

class DragDropList extends JList implements
        DragSourceListener, DropTargetListener, DragGestureListener {
    def dragSource
    def dropTarget
    def dropTargetCell
    int draggedIndex = -1
    def localDataFlavor = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType)
    def supportedFlavors = [localDataFlavor] as DataFlavor[]

    public DragDropList(model) {
        super()
        setModel(model)
        setCellRenderer(new DragDropCellRenderer(this))
        dragSource = new DragSource()
        dragSource.createDefaultDragGestureRecognizer(this, DnDConstants.ACTION_MOVE, this)
        dropTarget = new DropTarget(this, this)
    }

    public void dragGestureRecognized(DragGestureEvent dge) {
        int index = locationToIndex(dge.dragOrigin)
        if (index == -1 || index == model.size() - 1) return
        def trans = new CustomTransferable(model.getElementAt(index), this)
        draggedIndex = index
        dragSource.startDrag(dge, Cursor.defaultCursor, trans, this)
    }

    public void dragDropEnd(DragSourceDropEvent dsde) {
        dropTargetCell = null
        draggedIndex = -1
        repaint()
    }

    public void dragEnter(DragSourceDragEvent dsde) { }

    public void dragExit(DragSourceEvent dse) { }

    public void dragOver(DragSourceDragEvent dsde) { }

    public void dropActionChanged(DragSourceDragEvent dsde) { }

    public void dropActionChanged(DropTargetDragEvent dtde) { }

    public void dragExit(DropTargetEvent dte) { }

    public void dragEnter(DropTargetDragEvent dtde) {
        if (dtde.source != dropTarget) dtde.rejectDrag()
        else dtde.acceptDrag(DnDConstants.ACTION_COPY_OR_MOVE)
    }

    public void dragOver(DropTargetDragEvent dtde) {
        if (dtde.source != dropTarget) dtde.rejectDrag()
        int index = locationToIndex(dtde.location)
        if (index == -1 || index == draggedIndex + 1) dropTargetCell = null
        else dropTargetCell = model.getElementAt(index)
        repaint()
    }

    public void drop(DropTargetDropEvent dtde) {
        if (dtde.source != dropTarget) {
            dtde.rejectDrop()
            return
        }
        int index = locationToIndex(dtde.location)
        if (index == -1 || index == draggedIndex) {
            dtde.rejectDrop()
            return
        }
        dtde.acceptDrop(DnDConstants.ACTION_MOVE)
        def dragged = dtde.transferable.getTransferData(localDataFlavor)
        boolean sourceBeforeTarget = (draggedIndex < index)
        model.remove(draggedIndex)
        model.add((sourceBeforeTarget ? index - 1 : index), dragged)
        dtde.dropComplete(true)
    }
}

class CustomTransferable implements Transferable {
    def object
    def ddlist

    public CustomTransferable(object, ddlist) {
        this.object = object
        this.ddlist = ddlist
    }

    public Object getTransferData(DataFlavor df) {
        if (isDataFlavorSupported(df)) return object
    }

    public boolean isDataFlavorSupported(DataFlavor df) {
        return df.equals(ddlist.localDataFlavor)
    }

    public DataFlavor[] getTransferDataFlavors() {
        return ddlist.supportedFlavors
    }
}

class DragDropCellRenderer extends DefaultListCellRenderer {
    boolean isTargetCell
    def ddlist

    public DragDropCellRenderer(ddlist) {
        super()
        this.ddlist = ddlist
    }

    public Component getListCellRendererComponent(JList list, Object value,
            int index, boolean isSelected, boolean hasFocus) {
        isTargetCell = (value == ddlist.dropTargetCell)
        boolean showSelected = isSelected && !isTargetCell
        return super.getListCellRendererComponent(list, value, index, showSelected, hasFocus)
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g)
        if (isTargetCell) {
            g.setColor(Color.black)
            g.drawLine(0, 0, size.width.intValue(), 0)
        }
    }
}

lines = '''
This is line 1
This is line 2
This is line 3
This is line 4
'''.trim().split('\n')
def listModel = new DefaultListModel()
lines.each{ listModel.addElement(it) }
listModel.addElement(' ') // dummy
def list = new DragDropList(listModel)
def sp = new JScrollPane(list, SPC.VERTICAL_SCROLLBAR_ALWAYS, SPC.HORIZONTAL_SCROLLBAR_NEVER)
def frame = new JFrame('Line Shuffle Example')
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
frame.contentPane.add(sp)
frame.pack()
frame.setVisible(true)
//----------------------------------------------------------------------------------