Implementing the seat selection screen

SeatingCanvas

This class takes care of displaying the distribution of seats in the theater. It shows the layout of the movie threatre from above. The occupied seats are shown as red and the available seats as blue. The user can use the arrow keys to select the desired seat position to reserve. The currently selected seats are shown in green. The screen of theatre is presented by a gray rectangle. The screenshot below gives an idea of how it all looks like.

Figure 42: SeatingCanvas screenshot

SeatingCanvas is implemented as a simple custom widget which implements its own drawing hand key event handling. This is achieved by utilizing the Canvas widget.

  1. Create the SeatingCanvas class file.

  2. Import the required classes and assign this class to the package moviebooking.ui.

    package moviebooking.ui;
    
    import moviebooking.moviedb.Showing;
    
    import org.eclipse.swt.SWT;
    import org.eclipse.swt.events.PaintListener;
    import org.eclipse.swt.events.ControlListener;
    import org.eclipse.swt.events.KeyListener;
    import org.eclipse.swt.events.KeyEvent;
    import org.eclipse.swt.events.PaintEvent;
    import org.eclipse.swt.events.ControlEvent;
    import org.eclipse.swt.graphics.Color;
    import org.eclipse.swt.graphics.GC;
    import org.eclipse.swt.graphics.Image;
    import org.eclipse.swt.graphics.Rectangle;
    import org.eclipse.swt.widgets.Canvas;
    import org.eclipse.swt.widgets.Composite;
    import org.eclipse.swt.widgets.Shell;
    
    /**
     * Contains the seat distribution and facilities to select it
     */
    class SeatingCanvas extends Canvas
    implements PaintListener, ControlListener, KeyListener {
    
    	private int w, h;
    	private Showing showing;
    	private int[][] selected;
    	private Image doubleBuffer;
    	private GC gc;
    	private boolean[][] seats;
    	private int seatCount;
    	private int selectedRow, firstSelectedSeat;
    	private int rows;
    	private int seatsPerRow;
    	private Shell shell;
    
  3. Place SeatingCanvas inside a Shell whose parent is the top-level Shell. This kind of a second level Shell can be thought of as a dialog or a window on top of the parent Shell. The NO_BACKGROUND flag is used to prevent the Canvas from drawing its background since we are going to paint every pixel ourselves in the paint listener (paintControl method). For more information the Shell class, see the class Shell.

    	SeatingCanvas(Shell shell, Showing showing,
    			int[][] selectedSeating, int seatCount) {
    		super(shell, SWT.NO_BACKGROUND);
    		this.showing = showing;
    		this.shell = shell;
    		this.seatCount = seatCount;
    		this.selected = selectedSeating;
    		seats = showing.getSeating();
    		rows = seats.length;
    		seatsPerRow = seats[0].length;
    
    		init();
    	}
    	
    	void init() {
    		// Obtain the size of the parent
    		Rectangle clientArea = getClientArea();
    		w = clientArea.width;
    		h = clientArea.height;
    		// Select the first free seats
    		selected = new int[seatCount][2];
    		for (int i = 0; i < rows; i++) {
    			if (searchSeatsInRow(i, 0)) {
    				selectedRow = i;
    				break;
    			}
    		}
    
  4. Add listeners. PaintListener() is set to handle events generated when controls need to be painted. When this happens, the paintControl() method is invoked. KeyListener() is set to handle events generated when keys are pressed on the device keyboard. ControlListener() is set to handle events generated when controls are moved or resized. Note that the methods invoked by these listeners are defined in step 5 below

    		// This is necessary to repaint the screen
    		addPaintListener(this);
    		// Request to listen for keys
    		addKeyListener(this);
    		// Listen if the size of the parent changes
    		shell.addControlListener(this);
    	}
  5. Draw the theatre seats and environment.

    This is an expensive operation as there are quite many shapes to draw and our implementation redraws all of them every time that SeatingCanvas needs to be painted. This leads to some flickering on the screen and an unpleasant drawing sequence. A solution to this problem is to use double buffering. This means that the content is first drawn into a buffer which is then drawn to the screen. This way all the shapes drawn appear on the screen simultaneously instead of little by little.

    In eSWT this is done by creating an off-screen image size and a GC object that allows drawing onto the image. After that the normal drawing methods can be called and applied to the image.

    	public void paintControl(PaintEvent e) {
    		//	Dump the offscreen image onto the canvas
    		drawSeating();
    		e.gc.drawImage(doubleBuffer, 0, 0);
    	}
    	
            	// Drawing of the offscreen image
    	private void drawSeating() {	
    		int width = (95 * w) / 100;
    		int height = h / 20;
    
    		// The movie theather's screen
    		Rectangle screen = new Rectangle(
    				(w - width) / 2,
    				height,
    				width,
    				height);
    		
    		Color bgColor = getDisplay().getSystemColor(SWT.COLOR_WHITE);
    		Color occupiedSeatColor = getDisplay().getSystemColor(SWT.COLOR_RED);
    		Color availableSeatColor = getDisplay().getSystemColor(SWT.COLOR_BLUE);
    		Color frameColor = getDisplay().getSystemColor(SWT.COLOR_BLACK);
    		Color selectedSeatColor = getDisplay().getSystemColor(SWT.COLOR_GREEN);
    		Color screenColor = getDisplay().getSystemColor(SWT.COLOR_GRAY);
    
    		if(doubleBuffer == null ) {
    			doubleBuffer = new Image(getDisplay(), getBounds());
    		}
    		if(gc == null) {
    			gc = new GC(doubleBuffer);
    		}
    		
    		// Clear the image
    		gc.setBackground(bgColor);
    		gc.fillRectangle(0, 0, w, h);
    				
    		gc.setForeground(frameColor);
    		gc.drawRectangle(0, 0, w - 1, h - 1);
    
    		gc.setForeground(screenColor);
    		gc.setBackground(screenColor);
    		gc.fillRectangle(screen);
    
    		boolean[][] seats = showing.getSeating();
    		int rows = seats.length;
    		int seatsPerRow = seats[0].length;
    		int radius = Math.min((screen.width / seatsPerRow),
    				((h - (screen.height + screen.y)) / rows)) - 2;
    		// Space per seat/row
    		int spacex = screen.width / seatsPerRow;
    		int spacey = ((h - (screen.height + screen.y + radius / 2)) / rows);
    		// Empty space to leave for horizontal center alignment
    		int xOffset = (screen.width - spacex * seatsPerRow) / 2;
    		for (int i = 0; i < rows; i++) {
    			for (int j = 0; j < seatsPerRow; j++) {
    				if (seats[i][j]) {
    					gc.setBackground(occupiedSeatColor);
    				} else {
    					gc.setBackground(availableSeatColor);
    				}
    				gc.fillOval(screen.x + xOffset + j * spacex, (screen.y
    						+ screen.height + radius / 2)
    						+ i * spacey, radius, radius);
    			}
    		}
    		for (int i = 0; i < selected.length; i++) {
    			gc.setBackground(selectedSeatColor);
    			gc.fillOval(screen.x + xOffset + selected[i][1] * spacex, (screen.y
    					+ screen.height + radius / 2)
    					+ selected[i][0] * spacey, radius, radius);
    		}
    	}

    In the code above, the paintControl(PaintEvent) method of the PaintListener() interface is invoked when a control needs to be painted. This also defines the details of the paintEvent.

  6. Add rules for events.

    	public void keyPressed(KeyEvent e) {
    		// Check if the arrow keys are pressed
    		if (e.keyCode == SWT.ARROW_UP) {
    			if (selectedRow > 0) {
    				searchSeatsInRow(selectedRow - 1, firstSelectedSeat);
    				redraw();
    			}
    		} else if (e.keyCode == SWT.ARROW_DOWN) {
    			if (selectedRow < (rows - 1)) {
    				searchSeatsInRow(selectedRow + 1, firstSelectedSeat);
    				redraw();
    			}
    		} else if (e.keyCode == SWT.ARROW_LEFT) {
    			if (firstSelectedSeat > 0) {
    				searchSeatsInRow(selectedRow, firstSelectedSeat - 1);
    				redraw();
    			}
    		} else if (e.keyCode == SWT.ARROW_RIGHT) {
    			if (firstSelectedSeat < (seatsPerRow - seatCount)) {
    				searchSeatsInRow(selectedRow, firstSelectedSeat + 1);
    				redraw();
    			}
    		}
    	}
    
    	public void keyReleased(KeyEvent e) {
    		// Nothing to do, we are only interested in key releases
    	}
    
  7. Apply the controls and drawing methods. The selection moves when the user presses the keys. When the control's size changes, we dispose the double buffer Image and call redraw() to make the paint listener paint again. In the paintControl method the double buffer Image is then recreated with the new size of the control.

    	public void controlMoved(ControlEvent e) {
    		// We are not interested in this event
    	}
    
    	public void controlResized(ControlEvent e) {
    		// Update the width and height
    		Rectangle clientArea = ((Composite) e.widget).getClientArea();
    		w = clientArea.width;
    		h = clientArea.height;
    		// Recreate the double buffer with the new size
    		if(doubleBuffer != null) {
    			doubleBuffer.dispose();
    		}
    		doubleBuffer = null;
    		if(gc != null){
    			gc.dispose();
    		}
    		gc = null;
    		
    		redraw();
    	}
    
    	// Returns the currently selected seats
    	int[][] getSelectedSeats() {
    		return selected;
    	}
    	
    	public void destroy() {
    		if (doubleBuffer != null) {
    			doubleBuffer.dispose();
    		}
    		if (gc != null) {
    			gc.dispose();
    		}
    	}
    	
    	// Search if seatCount seats are available in row i
    	private boolean searchSeatsInRow(int i, int firstSelectedSeat) {
    		int k = 0;
    		for (int j = firstSelectedSeat; j < seatsPerRow; j++) {
    			if (!seats[i][j]) {
    				k++;
    			}
    		}
    		if (k >= seatCount) {
    			k = 0;
    			for (int j = firstSelectedSeat; j < seatsPerRow
    			&& k < seatCount; j++) {
    				if (!seats[i][j]) {
    					selected[k][0] = i;
    					selected[k++][1] = j;
    				}
    			}
    			selectedRow = i;
    			this.firstSelectedSeat = selected[0][1];
    			return true;
    		} else {
    			return false;
    		}
    	}

SeatingScreen

The SeatingScreen class creates a simple, secondary shell containing the SeatingCanvas.

  1. Create the SeatingScreen class file.

  2. Import the required classes and assign this class to the package moviebooking.ui.

    package moviebooking.ui;
    
    import moviebooking.moviedb.Showing;
    
    import org.eclipse.ercp.swt.mobile.Command;
    import org.eclipse.swt.SWT;
    import org.eclipse.swt.events.ControlEvent;
    import org.eclipse.swt.events.ControlListener;
    import org.eclipse.swt.events.SelectionListener;
    import org.eclipse.swt.events.SelectionEvent;
    import org.eclipse.swt.graphics.Rectangle;
    import org.eclipse.swt.layout.FillLayout;
    import org.eclipse.swt.widgets.Control;
    import org.eclipse.swt.widgets.Shell;
    /**
     * Simple shell that containsthe SeatingCanvas
     */
    class SeatingScreen implements SelectionListener, ControlListener {
    
    	private Command exitCommand, okCommand, backCommand;
    	private MovieBooking main;
    	private Shell seatingShell;
    	private Shell mainShell;
    	private SeatingCanvas seatingCanvas;
    
  3. Create the Shell. For more information the Shell class, see the class Shell.

    	SeatingScreen(MovieBooking main, Shell mainShell, Showing showing,
    			int[][] selectedSeats, int ticketsCount) {
    		this.main = main;
    
    		this.mainShell = mainShell;
    		
    		seatingShell = new Shell(mainShell, SWT.BORDER | SWT.TITLE);
    		seatingShell.setText("Seats");
    		seatingShell.open();
    
    		// Add a ControlListener to watch the size changes of the top-level 
    		// Shell and update the seatingShell's size
    		mainShell.addControlListener(this);
    
    		// Seating canvas displays the seating arrangement
    		seatingCanvas = new SeatingCanvas(
    				seatingShell,
    				showing,
    				selectedSeats,
    				ticketsCount);
    
    		// Size and location of the seatingCanvas are controlled by
    		// The FillLayout of the seatingShell. 
    		seatingShell.setLayout(new FillLayout());
    		seatingShell.layout();
  4. Add commands. For more information on the Command class, see the class Command.

     		// add some commands
    		exitCommand = new Command(seatingShell, Command.EXIT, 0);
    		exitCommand.setText("Exit");
    		exitCommand.addSelectionListener(this);
    
    		okCommand = new Command(seatingShell, Command.OK, 1);
    		okCommand.setText("Select");
    		okCommand.addSelectionListener(this);
    
    		backCommand = new Command(seatingShell, Command.BACK, 0);
    		backCommand.setText("Back");
    		backCommand.addSelectionListener(this);
    	}
    
    	private void destroy() {		
    		mainShell.removeControlListener(this);
    		seatingCanvas.destroy();
    		seatingShell.dispose();
    	}
    
    	public void widgetSelected(SelectionEvent e) {
    		if (e.widget == exitCommand) {
    			destroy();
    			main.exit();
    		} else if (e.widget == okCommand) {
    			destroy();
    			main.setSelectedSeats(seatingCanvas.getSelectedSeats());
    		} else if (e.widget == backCommand) {
    			destroy();
    			main.cancelSeatSelection();
    		}
    
    	}
    
    	public void widgetDefaultSelected(SelectionEvent e) {
    		// Nothing to do
    	}
    
    	public void controlMoved(ControlEvent e) {
    		// Can be left at (0, 0)
    	}
    	
    	public void controlResized(ControlEvent e) {
    		//seatingShell.setBounds(((Control)e.widget).getBounds());
    		Rectangle rect = ((Control)e.widget).getBounds();
    		rect.x += rect.width/4;
    		rect.y += rect.height/4;
    		rect.height /= 2;
    		rect.width /= 2;
    		seatingShell.setBounds(rect);
    	}
    }