8
\$\begingroup\$

I was eager to learn the implementation details behind windowing GUI systems. Because I am mostly a high level programmer having not much experience in low-level stuff, I had to simulate it instead of implementing the actual one such as Windows Explorer, KDE, Gnome, X11 and Mac OSX Aqua.

There is the list of things you can do in the simulation:

  • Move the windows by dragging their title bars.
  • Click one to put it on top of other windows.
  • Resize in one direction by dragging a border.
  • Resize in two directions by dragging a corner.
  • Upon resizing, the title will be contracted if does not fit the current width.

MyScreen.java:

package net.coderodde.gui.simulation;

import java.awt.Canvas;
import java.awt.Dimension;
import java.util.List;
import java.awt.Toolkit;
import java.util.ArrayList;
import javax.swing.JFrame;

public class MyScreen {

    private final static int FRAME_WIDTH = 1024;
    private final static int FRAME_HEIGHT = 768;

    private final JFrame frame;
    private final Canvas canvas;
    private final List<MyWindow> wndList;
    private MyWindow activeWindow;
    private MyWindow draggedWindow;
    private int draggedWindowOffsetX;
    private int draggedWindowOffsetY;
    private boolean topBorderLocked;
    private boolean rightBorderLocked;
    private boolean bottomBorderLocked;
    private boolean leftBorderLocked;
    private MyWindow highlightedWindow;
    private boolean disallowDrag;

    public MyScreen() {
        wndList = new ArrayList<>();
        frame = new JFrame("coderodde GUI simulation");
        frame.setSize(FRAME_WIDTH, FRAME_HEIGHT);
        frame.setResizable(false);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        final Dimension realScreenDimension = 
                Toolkit.getDefaultToolkit().getScreenSize();

        frame.setLocation((realScreenDimension.width - frame.getWidth()) / 2, 
                          (realScreenDimension.height - frame.getHeight()) / 2);

        canvas = new MyCanvas(frame.getWidth(), 
                              frame.getHeight(), 
                              wndList, 
                              this);

        frame.add(canvas);
        frame.setVisible(true);
    }

    public void addWindow(final MyWindow wnd) {
        wndList.add(wnd);

        if (activeWindow != null) {
            activeWindow.active(false);
        }

        wnd.active(true);
        activeWindow = wnd;
    }

    public void move(final int x, final int y) {
        MyWindow target = null;

        for (int i = wndList.size() - 1; i >= 0; --i) {
            if (wndList.get(i).windowContainsPoint(x, y)) {
                target = wndList.get(i);
                break;
            }
        }

        if (target == null) {
            if (highlightedWindow != null) {
                highlightedWindow.clearBorderHighlights();
                highlightedWindow = null;
                canvas.repaint();
            }

            return;
        }

        if (highlightedWindow != null) {
            highlightedWindow.clearBorderHighlights();
            highlightedWindow = null;
        }

        if (target.topBorderContainsPoint(x, y)) {
            target.highlightBorder(MyWindow.Border.TOP);
            highlightedWindow = target;
        } else if (target.bottomBorderContainsPoint(x, y)) {
            target.highlightBorder(MyWindow.Border.BOTTOM);
            highlightedWindow = target;
        }

        if (target.rightBorderContainsPoint(x, y)) {
            target.highlightBorder(MyWindow.Border.RIGHT);
            highlightedWindow = target;
        } else if (target.leftBorderContainsPoint(x, y)) {
            target.highlightBorder(MyWindow.Border.LEFT);
            highlightedWindow = target;
        }

        canvas.repaint();
    }

    public void press(final int x, final int y) {
        MyWindow newActiveWindow = null;

        for (int i = wndList.size() - 1; i >= 0; --i) {
            if (wndList.get(i).windowContainsPoint(x, y)) {
                newActiveWindow = wndList.remove(i);
                break;
            }
        }

        if (newActiveWindow == null) {
            return;
        }

        activeWindow.active(false);
        activeWindow = newActiveWindow;
        wndList.add(activeWindow);
        activeWindow.active(true);

        boolean doResize = false;

        if (activeWindow.topBorderContainsPoint(x, y)) {
            topBorderLocked = true;
            doResize = true;
        } else if (activeWindow.bottomBorderContainsPoint(x, y)) {
            bottomBorderLocked = true;
            doResize = true;
        }

        if (activeWindow.leftBorderContainsPoint(x, y)) {
            leftBorderLocked = true;
            doResize = true;
        } else if (activeWindow.rightBorderContainsPoint(x, y)) {
            rightBorderLocked = true;
            doResize = true;
        }

        if (!doResize && activeWindow.titleBarContainsPoint(x, y)) {
            draggedWindow = activeWindow;
            draggedWindowOffsetX = x - draggedWindow.x();
            draggedWindowOffsetY = y - draggedWindow.y();
        } 

        if (doResize) {
            draggedWindow = activeWindow;
        }

        canvas.repaint();
    }

    public void allowDrag() {
        disallowDrag = false;
    }

    public void disallowDrag() {
        disallowDrag = true;
    }

    public void drag(final int x, final int y) {
        if (draggedWindow == null) {
            return;
        }

        if (disallowDrag) {
            return;
        }

        if (!(topBorderLocked 
                || rightBorderLocked 
                || bottomBorderLocked
                || leftBorderLocked)) {
            // Move the entire window.
            draggedWindow.x(x - draggedWindowOffsetX);
            draggedWindow.y(y - draggedWindowOffsetY);
            canvas.repaint();
        }

        // Resize the window.
        final int originalX = draggedWindow.x();
        final int originalY = draggedWindow.y();

        if (topBorderLocked) {
            draggedWindow.resizeAtTop(y - originalY);
        } else if (bottomBorderLocked) {   
            draggedWindow.height(y - originalY);
        }

        if (leftBorderLocked) {
            draggedWindow.resizeAtLeft(x - originalX);
        } else if (rightBorderLocked) {
            draggedWindow.width(x - originalX);
        }

        canvas.repaint();
    }

    public void releaseMouse() {
        draggedWindow = null;
        topBorderLocked = false;
        leftBorderLocked = false;
        bottomBorderLocked = false;
        rightBorderLocked = false;
    }

    List<MyWindow> getWindows() {
        return wndList;
    }
}

MyWindow.java:

package net.coderodde.gui.simulation;

import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;

public class MyWindow {

    public static enum Border {
        TOP,
        RIGHT,
        BOTTOM,
        LEFT
    }

    public final static int MINIMUM_WIDTH = 50;
    public final static int MINIMUM_HEIGHT = 50;

    private final static int TITLE_BAR_HEIGHT = 30;
    private final static Color ACTIVE_TITLE_BAR_BACKGROUND = new Color(255, 150, 100);
    private final static Color TITLE_BAR_TEXT_COLOR = Color.WHITE;
    private final static Color PASSIVE_TITLE_BAR_BACKGROUND = new Color(200, 200, 200);
    private final static Color BORDER_HIGHLIGHT_COLOR = Color.GREEN;
    private final static Color BODY_COLOR = new Color(50, 50, 50);
    private final static Font TITLE_FONT = new Font("Monospaced", Font.BOLD, 12);
    private final static int TITLE_PADDING = 14;

    private String title;
    private int width;
    private int height;
    private int x;
    private int y;
    private boolean active;
    private boolean topBorderHightlighted;
    private boolean rightBorderHighlighted;
    private boolean bottomBorderHightlighted;
    private boolean leftBorderHighlighted;
    private boolean debug;

    public MyWindow(final String title, 
                    final int width, 
                    final int height,
                    final int x,
                    final int y) {
        this.title = title;
        this.width = Math.max(width, MINIMUM_WIDTH);
        this.height = Math.max(height, MINIMUM_HEIGHT) + TITLE_BAR_HEIGHT;
        this.x = x;
        this.y = y;
        this.active = false;
    }

    public void setDebug(final boolean debug) {
        this.debug = debug;
    }

    public boolean active() {
        return active;
    }

    public boolean active(final boolean active) {
        final boolean old = this.active;
        this.active = active;
        return old;
    }

    public void clearBorderHighlights() {
        topBorderHightlighted = false;
        rightBorderHighlighted = false;
        bottomBorderHightlighted = false;
        leftBorderHighlighted = false;
    }

    public void highlightBorder(Border border) {
        switch (border) {
            case TOP:
                topBorderHightlighted = true;
                return;

            case RIGHT:
                rightBorderHighlighted = true;
                return;

            case BOTTOM:
                bottomBorderHightlighted = true;
                return;

            case LEFT:
                leftBorderHighlighted = true;
                return;
        }
    }

    public String title() {
        return title;
    }

    public String title(final String title) {
        final String oldTitle = this.title;
        this.title = title;
        return oldTitle;
    }

    public int width() {
        return width;
    }

    public int width(final int width) {
        final int oldWidth = this.width;
        this.width = Math.max(width, MINIMUM_WIDTH);
        return oldWidth;
    }

    public int height() {
        return height;
    }

    public int height(final int height) {
        if (debug) {
            System.out.println("Height changed from " + height() + " to " +
                               height);
        }
        final int oldHeight = this.height;
        this.height = Math.max(height, MINIMUM_HEIGHT);
        return oldHeight;
    }

    public int x() {
        return x;
    }

    public int y() {
        return y;
    }

    public int x(final int x) {
        final int oldx = this.x;
        this.x = x;
        return oldx;
    }

    public int y(final int y) {
        final int oldy = this.y;
        this.y = y;
        return oldy;
    }

    public boolean titleBarContainsPoint(final int xx, final int yy) {
        return x() <= xx 
                && xx < x() + width
                && y() <= yy
                && yy < y() + TITLE_BAR_HEIGHT;
    }

    public boolean windowContainsPoint(final int xx, final int yy) {
        return x() <= xx 
                && xx < x() + width 
                && y() <= yy
                && yy < y() + height;
    }

    public boolean topBorderContainsPoint(final int xx, final int yy) {
        return x() <= xx && xx < x() + width && (yy == y() || yy == y() + 1);
    }

    public boolean rightBorderContainsPoint(final int xx, final int yy) {
        return (xx == x() + width - 1 || xx == x() + width - 2) 
                && y() <= yy && yy < y() + height;
    }

    public boolean bottomBorderContainsPoint(final int xx, final int yy) {
        return x() <= xx 
                && xx < x() + width 
                && (yy == y() + height - 1 || yy == y() + height - 2);
    }

    public boolean leftBorderContainsPoint(final int xx, final int yy) {
        return (xx == x() || xx == x() + 1) && y() <= yy && yy < y() + height;
    }

    public void draw(final Graphics g) {
        final Color titleBarColor = active() ?
                ACTIVE_TITLE_BAR_BACKGROUND :
                PASSIVE_TITLE_BAR_BACKGROUND;

        // Draw title bar.
        g.setColor(titleBarColor);
        g.fillRect(x, y, width, TITLE_BAR_HEIGHT);
        g.setColor(TITLE_BAR_TEXT_COLOR);
        g.setFont(TITLE_FONT);
        g.drawString(getContractedTitle(g.getFontMetrics(TITLE_FONT)), 
                                        x + TITLE_PADDING,
                                        y + 18);

        // Fill body rectangle.
        g.setColor(BODY_COLOR);
        g.fillRect(x, y + TITLE_BAR_HEIGHT, width, height - TITLE_BAR_HEIGHT);

        // Draw left, right and bottom colors.
        g.setColor(titleBarColor);
            // Draw left border.
        g.drawLine(x, y, x, y + height - 1);
            // Draw right border.
        g.drawLine(x + width - 1, y, x + width - 1, y + height - 1);
            // Draw bottom border
        g.drawLine(x, y + height - 1, x + width - 1, y + height - 1);

        // Try draw border highlights.
        g.setColor(BORDER_HIGHLIGHT_COLOR);

            // Top border.
        if (topBorderHightlighted) {
            g.fillRect(x, y, width, 2);
        }
            // Left border.
        if (leftBorderHighlighted) {
            g.fillRect(x, y, 2, height);
        }
            // Right border.
        if (rightBorderHighlighted) {
            g.fillRect(x + width - 2, y, 2, height);
        }
            // Bottom border.
        if (bottomBorderHightlighted) {
            g.fillRect(x, y + height - 2, width, 2);
        }
    }

    public void resizeAtTop(final int delta) {
        final int oldHeight = height;
        height = Math.max(MINIMUM_HEIGHT, height - delta);
        y += (oldHeight - height);
    }

    public void resizeAtLeft(final int delta) {
        final int oldWidth = width;
        width = Math.max(MINIMUM_WIDTH, width - delta);
        x += (oldWidth - width);
    }

    private String getContractedTitle(final FontMetrics fm) {
        final int allowedWidth = width() - 2 * TITLE_PADDING;

        if (fm.stringWidth(title) <= allowedWidth) {
            // Once here, the window title fits entirely in the title bar.
            return title;
        }

        // Once here, we need to contract the string from right.
        for (int i = title.length() - 1; i > 0; --i) {
            final String contractedTitle = title.substring(0, i);
            final String titleWithDots = contractedTitle + "...";
            if (fm.stringWidth(titleWithDots) <= allowedWidth) {
                return titleWithDots;
            }
        }

        if (fm.stringWidth("...") <= allowedWidth) {
            return "...";
        }

        if (fm.stringWidth("..") <= allowedWidth) {
            return "..";
        }

        return "";
    }
}

MyCanvas.java:

package net.coderodde.gui.simulation;

import java.awt.Canvas;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.List;

class MyCanvas extends Canvas {

    private final int width;
    private final int height;
    private final List<MyWindow> wndList;

    MyCanvas(final int width, 
             final int height, 
             final List<MyWindow> wndList,
             final MyScreen screen) {
        this.width = width;
        this.height = height;
        setSize(width, height);
        this.wndList = wndList;
        final MyMouseListener mml = new MyMouseListener(screen);
        addMouseListener(mml);
        addMouseMotionListener(mml);
    }

    @Override
    public void paint(final Graphics g) {
        g.setColor(Color.BLACK);
        g.fillRect(0, 0, width, height);

        for (final MyWindow wnd : wndList) {
            wnd.draw(g);
        }
    }

    @Override
    public void update(final Graphics g) {
        paint(g);
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(width, height);
    }

    static class MyMouseListener implements MouseListener,
                                            MouseMotionListener {

        private final MyScreen screen;

        MyMouseListener(final MyScreen screen) {
            this.screen = screen;
        }

        @Override
        public void mouseClicked(MouseEvent e) {

        }

        @Override
        public void mousePressed(MouseEvent e) {
            screen.press(e.getX(), e.getY());
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            screen.releaseMouse();
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            screen.allowDrag();
        }

        @Override
        public void mouseExited(MouseEvent e) {
            screen.disallowDrag();
        }    

        @Override
        public void mouseDragged(MouseEvent e) {
            screen.drag(e.getX(), e.getY());
        }

        @Override
        public void mouseMoved(MouseEvent e) {
           screen.move(e.getX(), e.getY());
        }
    }
}

Demo.java:

package net.coderodde.gui.simulation;

import javax.swing.SwingUtilities;

public class Demo {

    public static void main(final String... args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                MyScreen screen = new MyScreen();
                screen.addWindow(new MyWindow("Hello", 200, 200, 30, 30));
                screen.addWindow(new MyWindow("World and Funkeeh", 250, 150, 400, 400));
                screen.addWindow(new MyWindow("Yeah!", 400, 300, 10, 100));
            }
        });
    }
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Your project looks nice (not that I read through every line...), but what exactly is your question?? \$\endgroup\$ Commented Jan 24, 2015 at 1:35

1 Answer 1

4
\$\begingroup\$

I have some concerns:

public String title() {
    return title;
}

public String title(final String title) {
    final String oldTitle = this.title;
    this.title = title;
    return oldTitle;
}

This looks confusing to me. title() return the title, which is understandable, but title(final String title) returns the old title and sets the title to the argument. What?

I would rename the first methods to getTitle() and the other to getAndChangeTitle(). Also, consider adding setTitle().

This is the same with:

public boolean active() {
    return active;
}

public boolean active(final boolean active) {
    final boolean old = this.active;
    this.active = active;
    return old;
}

Again, active() should be isActive() and active(final boolean active) should be getAndChangeActive().

This is the same with x and y.

I also see some poor naming. For example:

private final List<MyWindow> wndList;

What is wndList? A list of wnd? But then what is wnd?

Rename it to something like windowsList or just windows.

Also:

public void addWindow(final MyWindow wnd) {

Again, name wnd to something like window.

public boolean titleBarContainsPoint(final int xx, final int yy) {
    return x() <= xx 
            && xx < x() + width
            && y() <= yy
            && yy < y() + TITLE_BAR_HEIGHT;
}

public boolean windowContainsPoint(final int xx, final int yy) {
    return x() <= xx 
            && xx < x() + width 
            && y() <= yy
            && yy < y() + height;
}

public boolean topBorderContainsPoint(final int xx, final int yy) {
    return x() <= xx && xx < x() + width && (yy == y() || yy == y() + 1);
}

public boolean rightBorderContainsPoint(final int xx, final int yy) {
    return (xx == x() + width - 1 || xx == x() + width - 2) 
            && y() <= yy && yy < y() + height;
}

public boolean bottomBorderContainsPoint(final int xx, final int yy) {
    return x() <= xx 
            && xx < x() + width 
            && (yy == y() + height - 1 || yy == y() + height - 2);
}

public boolean leftBorderContainsPoint(final int xx, final int yy) {
    return (xx == x() || xx == x() + 1) && y() <= yy && yy < y() + height;
}

The whole bunch of methods here all have the names xx and yy. Why can't you just do:

public boolean titleBarContainsPoint(final int x, final int y) {
    return this.x <= x 
            && x < this.x + width
            && this.y <= y
            && y < this.y + TITLE_BAR_HEIGHT;
}

public boolean windowContainsPoint(final int x, final int y) {
    return this.x <= x 
            && x < this.x + width
            && this.y <= y
            && y < this.y + height;
}

public boolean topBorderContainsPoint(final int x, final int y) {
    return this.x <= x && x < this.x + width && (y == this.y || y == this.y + 1);
}

public boolean rightBorderContainsPoint(final int x, final int y) {
    return (x == this.x + width - 1 || x == this.x + width - 2) 
            && this.y <= y && y < this.y + height;
}

public boolean bottomBorderContainsPoint(final int x, final int y) {
    return this.x <= x 
            && x < this.x + width 
            && (y == this.y + height - 1 || y == this.y + height - 2);
}

public boolean leftBorderContainsPoint(final int x, final int y) {
    return (x == this.x || x == this.x + 1) && this.y <= y && y < this.y + height;
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.