Implementing the seat selection screen

The SeatingCanvas class provides the UI that displays the distribution of seats in the theater. It shows the layout of the movie theater from above: the occupied seats are shown as red, the available seats as blue, and the theater screen as a gray rectangle. The user can use the touch screen or the arrow keys to select which seat position to reserve (the currently selected seats are shown in green).

The SeatingCanvas class creates the following UI:

Figure: Seat selection screen

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

To implement the seat selection screen:

  1. Create the SeatingCanvas.java class file.

  2. Import the required classes.

    import com.nokia.example.moviebooking.moviedb.Showing;
    
    import org.eclipse.swt.SWT;
    import org.eclipse.swt.events.ControlEvent;
    import org.eclipse.swt.events.ControlListener;
    import org.eclipse.swt.events.KeyEvent;
    import org.eclipse.swt.events.KeyListener;
    import org.eclipse.swt.events.MouseEvent;
    import org.eclipse.swt.events.MouseListener;
    import org.eclipse.swt.events.PaintEvent;
    import org.eclipse.swt.events.PaintListener;
    import org.eclipse.swt.graphics.Color;
    import org.eclipse.swt.graphics.GC;
    import org.eclipse.swt.graphics.Image;
    import org.eclipse.swt.graphics.Point;
    import org.eclipse.swt.graphics.Rectangle;
    import org.eclipse.swt.widgets.Canvas;
    import org.eclipse.swt.widgets.Shell;
  3. Set SeatingCanvas to extend Canvas and implement PaintListener, ControlListener, KeyListener, and MouseListener, and create the required variables.

    class SeatingCanvas extends Canvas
            implements PaintListener, ControlListener, KeyListener, MouseListener {
    
        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;
        private Point seatSize;
        private Rectangle screenRectangle;
  4. Place the 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 the MIDlet uses the paint listener's paintControl method to paint every pixel.

        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();
        }
  5. Use the init method to define the listeners the class needs.

        void init() {
            // Select the first free seats
            selected = new int[seatCount][2];
            for (int i = 0; i < rows; i++) {
                if (searchSeatsInRow(i, 0)) {
                    selectedRow = i;
                    break;
                }
            }
            // This is necessary to repaint the screen
            addPaintListener(this);
            // Request to listen for keys
            addKeyListener(this);
            //Listen for touch events
            addMouseListener(this);
            // Listen if the size of the parent changes
            shell.addControlListener(this);
    
        }
  6. Implement the key event handlers for key-based interaction.

        public void keyPressed(KeyEvent e) {
            // Nothing to do, we are only interested in key releases
        }
    
        public void keyReleased(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();
                }
            }
        }
  7. Draw the theatre seats and environment.

    Drawing the theatre seats and the environment is an expensive operation, as there are various shapes to draw and the MIDlet implementation redraws all of them every time the SeatingCanvas needs to be painted. This leads to some flickering on the screen and an unpleasant drawing sequence.

    To solve the problem, use double buffering. This means that the content is first drawn into a buffer, which is then drawn to the screen. Double buffering ensures that all the shapes appear on the screen simultaneously, instead of little by little. In eSWT, double buffering is implemented 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.

    In the following code snippet, the paintControl(PaintEvent) method of the PaintListener interface is invoked when a Control needs to be painted. The interface also defines the details of the PaintEvent. When the size of the Control changes, the MIDlet disposes the double buffer Image and calls redraw to make the paint listener paint again. In the paintControl method, the double buffer Image is then recreated with the new Control size.

        public void paintControl(PaintEvent e) {
            //  Dump the offscreen image onto the canvas
            drawSeating();
            e.gc.drawImage(doubleBuffer, 0, 0);
        }
    
        public void controlMoved(ControlEvent e) {
            // We are not interested in this event
        }
    
        public void controlResized(ControlEvent e) {
            // 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;
            }
        }
    
        // Drawing of the offscreen image
        private void drawSeating() {
    
            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);
            }
    
            Rectangle screen = getScreen();
    
            // Clear the image
            /*Point canvasSize = new Point(shell.getClientArea().width,
            shell.getClientArea().height);
             */
            Point canvasSize = getSize();
            gc.setBackground(bgColor);
            gc.fillRectangle(0, 0, canvasSize.x, canvasSize.y);
    
            gc.setForeground(frameColor);
            gc.drawRectangle(1, 1, canvasSize.x - 2, canvasSize.y - 2);
    
            gc.setForeground(screenColor);
            gc.setBackground(screenColor);
            gc.fillRectangle(screen);
    
            boolean[][] seats = showing.getSeating();
            int rows = seats.length;
            int seatsPerRow = seats[0].length;
            int radius = getRadius(rows, seatsPerRow);
            // Space per seat/row
            Point seatSize = getSeatSize(rows, seatsPerRow);
    
            // Empty space to leave for horizontal center alignment
            int xOffset = getOffsetX(screen, seatsPerRow, seatSize);
            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 * seatSize.x, (screen.y
                            + screen.height + radius / 2)
                            + i * seatSize.y, radius, radius);
                }
            }
            for (int i = 0; i < selected.length; i++) {
                gc.setBackground(selectedSeatColor);
                System.out.println("selected[i][1]" + selected[i][1]);
                System.out.println("selected[i][0]" + selected[i][0]);
                gc.fillOval(screen.x + xOffset + selected[i][1] * seatSize.x, (screen.y
                        + screen.height + radius / 2)
                        + selected[i][0] * seatSize.y, radius, radius);
            }
        }
    
        private int getOffsetX(Rectangle screen, int seatsPerRow, Point seatSize) {
            int xOffset = (screen.width - seatSize.x * seatsPerRow) / 2;
            return xOffset;
        }
    
        private Rectangle getScreen() {
    
            Point canvasSize = getSize();
            int width = (95 * canvasSize.x) / 100;
            int height = canvasSize.y / 20;
            screenRectangle = new Rectangle(
                    (canvasSize.x - width) / 2,
                    height,
                    width,
                    height);
    
            return screenRectangle;
        }
    
        /**
         * Calculate the seat per size/row
         *
         * @param canvasSize
         * @param screen
         * @param rows
         * @param seatsPerRow
         * @param radius
         * @return
         */
        private Point getSeatSize(int rows,
                int seatsPerRow) {
    
            int radius = getRadius(rows, seatsPerRow);
            Rectangle screen = getScreen();
            int spacex = screen.width / seatsPerRow;
            int spacey = ((getSize().y - (screen.height + screen.y + radius / 2)) / rows);
            seatSize = new Point(spacex, spacey);
    
            return seatSize;
        }
    
        private int getRadius(int rows, int seatsPerRow) {
            Rectangle screen = getScreen();
            int radius = Math.min((screen.width / seatsPerRow),
                    ((getSize().y - (screen.height + screen.y)) / rows)) - 2;
            return radius;
        }
  8. Implement the mouse event handlers for touch interaction.

        public void mouseDoubleClick(MouseEvent event) {
            // Nothing to do
        }
    
        public void mouseDown(MouseEvent event) {
            handleMouse(event);
    
        }
    
        public void mouseUp(MouseEvent event) {
            handleMouse(event);
        }
    
        private void handleMouse(MouseEvent event) {
            Rectangle screen = getScreen();
            Point seatSize = getSeatSize(seats.length, seats[0].length);
            if (event.y > (screen.y + screen.height)) {
                int col = (event.x) / seatSize.x;
                if (col < 1) {
                    col = 1;
                }
                if (col > seats[0].length) {
                    col = seats[0].length;
                }
                int row = (event.y - screen.y) / seatSize.y;
                if (row < 1) {
                    row = 1;
                }
                if (row > seats.length) {
                    row = seats.length;
                }
    
                for (int seat = 0; seat < seatCount && col < seatsPerRow; col++) {
                    if (!seats[row - 1][col - 1]) {
                        selected[seat][0] = row - 1;
                        selected[seat][1] = col - 1;
                        seat++;
                    }
    
                }
                redraw();
            }
    
        }
    }
  9. Create the SeatingScreen.java class file.

    The SeatingScreen class is used to create a simple, secondary Shell containing the SeatingCanvas.

  10. Import the required classes.

    import com.nokia.example.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.SelectionEvent;
    import org.eclipse.swt.events.SelectionListener;
    import org.eclipse.swt.layout.FillLayout;
    import org.eclipse.swt.widgets.Shell;
  11. Set SeatingScreen to implement SelectionListener and ControlListener, and create the required variables.

    class SeatingScreen implements SelectionListener, ControlListener {
    
        private Command exitCommand, okCommand, backCommand;
        private MovieBooking main;
        private Shell seatingShell;
        private Shell mainShell;
        private SeatingCanvas seatingCanvas;
  12. Create the 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");
    
            // 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());
            updateSize();
  13. Define the commands for the screen and the methods to handle the commands.

            // 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);
    
            seatingShell.open();
        }
    
        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) {
            updateSize();
        }
    
        private void updateSize() {
            // Set the same size as the parent
            seatingShell.setBounds(mainShell.getBounds());
    
        }
    }