Sunday, July 10, 2011

Experimenting with ribbons in LiveCode (prelude)

It's a rainy summer afternoon here in Belgium - what better time to pick up on an old experiment, and share the thought process with you, my fellow LiveCode developers? As you may have guessed from this post's subject, we'll be building a cross-platform Ribbon control using only LiveCode controls and behaviors.
Ribbons were introduced by Microsoft in Office 2007, and replace the traditional menubar and toolbar combination with a single tabbed toolbar. The primary goal: provide a better structure for the myriad of available options, allowing the user to discover these options without becoming overwhelmed.

It's something Microsoft developers have been acutely aware of for a long time, but their past forays into this type of user experience had been a mixed success. Take adaptive menus: I'm sure I wasn't the only one to loathe the automatic hiding of unused options that first appeared around 2000.
Sure, there was a chevron widget to bring back the hidden options, but the whole approach was confusing as the user interface became unpredictable. Have you ever had to support an application where the user could completely customize the look-and-feel, including menus and toolbars? Then you know the sort of nightmare this can induce.

Anyway, be sure to check out The Story of the Ribbon by Jensen Harris, and his related series of blog entries on this topic: Why the UI?

Now let's take a look at some screenshots of the ribbon across different Microsoft applications. Here's the original Word 2007 edition:



Followed by the latest Word 2010 edition:



And here's WordPad as it first shipped with Windows 7:



Finally, a screenshot of Word 2011 for Mac:



But before we work on implementing it, what are the different parts of the ribbon?



Hmm, looking at all these screenshots, it's clear some compromises are inevitable to build our ribbon in pure LiveCode:

1. Application menu and quick access toolbar
- in Office 2007, the application menu (the Jewel or Orb as it's sometimes referred to) fuses with the quick access toolbar into the window titlebar.
- we could replace the standard window decorations with our own implementations, but this brings its own challenges (such as smooth window resizing)
- in later releases, Microsoft decided the application menu could be toned down a bit, and moved it into a button next to the ribbon tabs, so let's just skip this problem altogether and go for the Windows 7 WordPad approach
- that leaves us with the quick access toolbar in the window titlebar, but as this can also be displayed beneath the ribbon, we'll sidestep the issue and simply move the quick access toolbar to the bottom of the ribbon, shall we?

2. Application menu panel
- before Office 2010, the application menu panel wasn't constrained to the window but in its own layer
- this makes it a bit tricky, but we'll get to it in a future post

3. Windows vs. Mac
- while the Windows version dispensed with the menubar entirely, the Mac version still has a complete menubar, but no application menu button
- we just decided to move the quick access toolbar out of the window titlebar and to the bottom of the ribbon, but on Mac the user has no option to move this toolbar and it's always between the window titlebar and the ribbon tabs
- the ribbon group titles have different locations as well: at the bottom of the ribbon group on Windows, at the top on Mac

4. Out of scope
For various reasons, the following ribbon features won't be in scope for this experiment
- contextual tabs
- galleries
- enhanced tooltips

So we'll compromise, but should still have a pretty good ribbon implementation at the end of the experiment - not just a one-off ribbon implementation, but a generic one which you can modify from script. Here's what we're aiming for:



It won't be pixel-prefect, but we can get pretty close. In our first installment, we'll take care of the groundworks. Hang in there - I'll post it shortly.

In the meantime, you should definitely read up on these MSDN articles:
- Ribbon design process
- UX Guidelines - Ribbons

Monday, June 13, 2011

Quartam PDF Library 1.1.2 Available

This maintenance update to Quartam PDF Library fixes a bug with clipping in combination with automatic page breaks.

The cross-platform .zip archive can be downloaded at: http://downloads.quartam.com/qrtpdflib_112_xplatform.zip
A web page with LiveCode Server / On-Rev demos is available at: http://quartam.on-rev.com/qrtpdfdemos.irev
New examples were added to demonstrate using form data to fill PDF documents and email them.

Quartam PDF Library for LiveCode - version 1.1 introduced support for transformations, transparency and blendmodes, gradients, clipping, text box fitting, inserting pages, compression, experimental support for including EPS files, as well as support for LiveCode Server and On-Rev. It is released as open source under a dual license (GNU Affero General Public License / Commercial License).

Monday, May 30, 2011

Leveraging Java from LiveCode - experiments with multi-threading

For some time now, I've been promoting the idea of extending LiveCode using Java - both are wonderful cross-platform technologies, each with their own strengths and weaknesses. One area where Java has really made an impact is its built-in approach to multi-threading, by offering a way to split work into chunks that can be executed separately and simultaneously. In a world where chip makers turn to multiple cores to speed up processing, this is a very important technology to incorporate into our software designs.

LiveCode is single-threaded - well, mostly single-threaded: script execution happens on a single thread, but some parts, like socket communication, can be used with event callbacks. This means that when new data arrives, an event message is sent to a control and handled on the script execution thread; but the actual reading and writing can happen on other threads. This is a solid middle ground, as coders don't have to worry about data races and the other un-fun things that come with full-blown multi-threaded programming.

Now my earlier postrs on extending LiveCode with Java were based on a strategy of handing off some work to a Java process (via shell calls or process communication via pipes) and waiting for the result to come back. This is all nice and dandy if the work is done quickly, but what if the processing may take a long time to complete? One of the main reasons to employ multi-threading, after all, is to make sure we can update the user interface while some heavy processing continues in the background.

One approach could be to use the 'in time' variant of the 'read from process' command, and poll the other side for content, but I wanted to show a different approach, by wrapping our Java code in an ExecutorService. The basic idea: we can either 'call' a command immedialtely, or 'submit' it as a task to process in the background. Submitting a command returns a pending-result-id, which we can use to 'cancel' (if it hasn't started or finished yet), check on its 'status' (done, cancelled, pending) or even 'get' its result (waiting for the command to complete).

I know that's a lot of things in one paragraph, so let's put it in Java code:


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicInteger;


public class LiveCodeJavaExecutor {

private final static String EXIT_COMMAND = "exit";
private final static String CALL_COMMAND = "call";
private final static String SUBMIT_COMMAND = "submit";
private final static String CANCEL_COMMAND = "cancel";
private final static String STATUS_COMMAND = "status";
private final static String GET_COMMAND = "get";

private final static ExecutorService EXECUTOR_SERVICE;
private final static Map<Integer,Future<String>> PENDING_RESULTS;
private final static AtomicInteger NEXT_PENDING_ID;

static {
EXECUTOR_SERVICE = Executors.newCachedThreadPool();
NEXT_PENDING_ID = new AtomicInteger();
PENDING_RESULTS = new HashMap<Integer,Future<String>>();
}

/**
* @param args
*/
public static void main(String[] args) {
// Make sure to run in head-less mode
System.setProperty("java.awt.headless", "true");

String commandLine = ""; // the last received command line
System.out.println("LiveCodeJavaExecutor started. Type the command you wish to execute (type 'exit' to stop):");
final InputStreamReader isr = new InputStreamReader(System.in);
final BufferedReader bir = new BufferedReader(isr);

InfiniteLoop:
while(!EXIT_COMMAND.equalsIgnoreCase(commandLine)) {
try {
commandLine = bir.readLine();
} catch (IOException e) {
continue InfiniteLoop;
}
final String[] commandParts = commandLine.split("\t");
final String commandName = commandParts[0];
if (EXIT_COMMAND.equalsIgnoreCase(commandName)) {
System.out.println("LiveCodeJavaExecutor is shutting down");
EXECUTOR_SERVICE.shutdown();
System.out.println("LiveCodeJavaExecutor is exiting");
} else if (CALL_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
final CallableCommand callableCommand = new CallableCommand(commandParts);
try {
result = callableCommand.call();
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (SUBMIT_COMMAND.equalsIgnoreCase(commandName)){
String result = "";
try {
final CallableCommand callableCommand = new CallableCommand(commandParts);
final Integer pendingResultId = Integer.valueOf(NEXT_PENDING_ID.incrementAndGet());
final Future pendingResult = EXECUTOR_SERVICE.submit(callableCommand);
PENDING_RESULTS.put(pendingResultId, pendingResult);
result = pendingResultId.toString();
} catch (RejectedExecutionException e) {
result = "error - could not submit command";
}
System.out.println(result);
} else if (CANCEL_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final Future pendingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
result = String.valueOf(pendingResult.cancel(true));
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: cancel<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (STATUS_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final Future pendingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
if (pendingResult.isCancelled()) {
result = "cancelled";
} else if (pendingResult.isDone()) {
result = "done";
} else {
result = "pending";
}
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: status<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else if (GET_COMMAND.equalsIgnoreCase(commandName)) {
String result = "";
try {
final Integer pendingResultId = Integer.valueOf(commandParts[1]);
final Future pendingResult = PENDING_RESULTS.get(pendingResultId);
if (pendingResult != null) {
result = pendingResult.get();
PENDING_RESULTS.remove(pendingResultId);
} else {
result = "unknown pending result id: " + pendingResultId.toString();
}
} catch (ArrayIndexOutOfBoundsException e) {
result = "invalid parameters - expected: get<tab><pending-result-id>";
} catch (Exception e) {
result = e.getMessage().trim();
}
System.out.println(result);
} else {
System.out.println("unrecognized command: " + commandLine);
}
System.out.println('.');
}
}

private static class CallableCommand implements Callable<String> {
private final String[] commandParts;
public CallableCommand(final String[] commandParts) {
super();
this.commandParts = commandParts;
}
public String call() throws Exception {
String result = null;
result = dispatchCommand(commandParts);
return result;
}
}

public static String dispatchCommand(String[] commandParts) throws Exception {
String commandName = commandParts[1];
Thread.sleep(10000);
return commandName;
}

}


Admittedly, the actual command execution part is a bit silly to say the least: sleeping for ten seconds and then just returning the command name is not exactly business logic. But you can take a look at some of my previous examples, to put in code for concatenating PDF files, validating XML data using XSD schemas, image processing or anything else that could take quite a bit of time to finish processing.

Turning our attention to the LiveCode side, we would have a stack script looking something like this:

local sProcess

on JExecutor_StartHelper
if sProcess is empty then
local tDefaultFolder, tStackFolder
put ImageIO_StackFolder() into tStackFolder
put the defaultFolder into tDefaultFolder
set the defaultFolder to tStackFolder
put "java LiveCodeJavaExecutor" into sProcess
open process sProcess for update
set the defaultFolder to tDefaultFolder
--> make sure we're speaking with the right helper
read from process sProcess for 1 line
if it begins with "LiveCodeJavaExecutor started." then
--> good to go
else
close process sProcess
put empty into sProcess
end if
end if
end JExecutor_StartHelper

on JExecutor_StopHelper
write ("exit") & return to process sProcess
close process sProcess
put empty into sProcess
end JExecutor_StopHelper

command JExecutor_CallCommand pCommand
write("call" & tab & pCommand) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_CallCommand

command JExecutor_SubmitCommand pCommand
write("submit" & tab & pCommand) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_SubmitCommand

command JExecutor_CancelRequest pRequestId
write("cancel" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_CancelRequest

function JExecutor_StatusOfRequest pRequestId
write("status" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_StatusOfRequest

function JExecutor_GetResultOfRequest pRequestId
write("get" & tab & pRequestId) & return to process sProcess
read from process sprocess for 1 line
return line 1 of it
end JExecutor_GetResultOfRequest


Now we have the commands and functions we need to start the separate Java process, 'call' commands, 'submit' commands, 'cancel' pending requests, checking the 'status' of pending requests, and fetching the result with a 'get'. Enough to control the lifecycle of such submitted commands a.k.a. pending requests.

If we're sure the command is just a small bit of work, we use the 'call' command and wait for the result to appear; but if it may take a long time to complete, we can 'submit' a command and use the returned 'pending request id' to track its progress - and while the Java side is churning away, we can update our LiveCode user interface with a lovely indeterminate progress display, polling every second or so until the 'status' returns "done" and then use 'get' to fetch the completed command output.

So perhaps the title was a little misleading, as I've only given you a skeleton. Hopefully it will inspire you to combine this technique with code that I posted previously, and actually use it in your own projects. Happy coding!

Monday, May 9, 2011

Quartam PDF Library 1.1.1 Available

This maintenance update to Quartam PDF Library fixes two bugs specific to LiveCode Server environments, and extends the WriteTextTable command to allow more control over border drawing.

The cross-platform .zip archive can be downloaded at: http://downloads.quartam.com/qrtpdflib_111_xplatform.zip
A web page with LiveCode Server / On-Rev demos is available at: http://quartam.on-rev.com/qrtpdfdemos.irev

Quartam PDF Library for LiveCode - version 1.1 introduced support for transformations, transparency and blendmodes, gradients, clipping, text box fitting, inserting pages, compression, experimental support for including EPS files, as well as support for LiveCode Server and On-Rev. It is released as open source under a dual license (GNU Affero General Public License / Commercial License).