1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.sdkuilib.ui;
18
19import com.android.SdkConstants;
20
21import org.eclipse.swt.SWT;
22import org.eclipse.swt.events.DisposeEvent;
23import org.eclipse.swt.events.DisposeListener;
24import org.eclipse.swt.graphics.Point;
25import org.eclipse.swt.graphics.Rectangle;
26import org.eclipse.swt.widgets.Dialog;
27import org.eclipse.swt.widgets.Display;
28import org.eclipse.swt.widgets.Shell;
29
30import java.util.HashMap;
31import java.util.Map;
32
33/**
34 * A base class for an SWT Dialog.
35 * <p/>
36 * The base class offers the following goodies: <br/>
37 * - Dialog is automatically centered on its parent. <br/>
38 * - Dialog size is reused during the session. <br/>
39 * - A simple API with an {@link #open()} method that returns a boolean. <br/>
40 * <p/>
41 * A typical usage is:
42 * <pre>
43 *   MyDialog extends SwtBaseDialog { ... }
44 *   MyDialog d = new MyDialog(parentShell, "My Dialog Title");
45 *   if (d.open()) {
46 *      ...do something like refresh parent list view
47 *   }
48 * </pre>
49 * We also have a JFace-base {@link GridDialog}.
50 * The JFace dialog is good when you just want a typical OK/Cancel layout with the
51 * buttons all managed for you.
52 * This SWT base dialog has little decoration.
53 * It's up to you to manage whatever buttons you want, if any.
54 */
55public abstract class SwtBaseDialog extends Dialog {
56
57    /**
58     * Min Y location for dialog. Need to deal with the menu bar on mac os.
59     */
60    private final static int MIN_Y =
61        SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN ? 20 : 0;
62
63    /** Last dialog size for this session, different for each dialog class. */
64    private static Map<Class<?>, Point> sLastSizeMap = new HashMap<Class<?>, Point>();
65
66    private volatile boolean mQuitRequested = false;
67    private boolean mReturnValue;
68    private Shell mShell;
69
70    /**
71     * Create the dialog.
72     *
73     * @param parent The parent's shell
74     * @param title The dialog title. Can be null.
75     */
76    public SwtBaseDialog(Shell parent, int swtStyle, String title) {
77        super(parent, swtStyle);
78        if (title != null) {
79            setText(title);
80        }
81    }
82
83    /**
84     * Open the dialog.
85     *
86     * @return The last value set using {@link #setReturnValue(boolean)} or false by default.
87     */
88    public boolean open() {
89        if (!mQuitRequested) {
90            createShell();
91        }
92        if (!mQuitRequested) {
93            createContents();
94        }
95        if (!mQuitRequested) {
96            positionShell();
97        }
98        if (!mQuitRequested) {
99            postCreate();
100        }
101        if (!mQuitRequested) {
102            mShell.open();
103            mShell.layout();
104            eventLoop();
105        }
106
107        return mReturnValue;
108    }
109
110    /**
111     * Creates the shell for this dialog.
112     * The default shell has a size of 450x300, which is also its minimum size.
113     * You might want to override these values.
114     * <p/>
115     * Called before {@link #createContents()}.
116     */
117    protected void createShell() {
118        mShell = new Shell(getParent(), SWT.DIALOG_TRIM | SWT.RESIZE | SWT.APPLICATION_MODAL);
119        mShell.setMinimumSize(new Point(450, 300));
120        mShell.setSize(450, 300);
121        if (getText() != null) {
122            mShell.setText(getText());
123        }
124        mShell.addDisposeListener(new DisposeListener() {
125            @Override
126            public void widgetDisposed(DisposeEvent e) {
127                saveSize();
128            }
129        });
130    }
131
132    /**
133     * Creates the content and attaches it to the current shell (cf. {@link #getShell()}).
134     * <p/>
135     * Derived classes should consider creating the UI here and initializing their
136     * state in {@link #postCreate()}.
137     */
138    protected abstract void createContents();
139
140    /**
141     * Called after {@link #createContents()} and after {@link #positionShell()}
142     * just before the dialog is actually shown on screen.
143     * <p/>
144     * Derived classes should consider creating the UI in {@link #createContents()} and
145     * initialize it here.
146     */
147    protected abstract void postCreate();
148
149    /**
150     * Run the event loop.
151     * This is called from {@link #open()} after {@link #postCreate()} and
152     * after the window has been shown on screen.
153     * Derived classes might want to use this as a place to start automated
154     * tasks that will update the UI.
155     */
156    protected void eventLoop() {
157        Display display = getParent().getDisplay();
158        while (!mQuitRequested && !mShell.isDisposed()) {
159            if (!display.readAndDispatch()) {
160                display.sleep();
161            }
162        }
163    }
164
165    /**
166     * Returns the current value that {@link #open()} will return to the caller.
167     * Default is false.
168     */
169    protected boolean getReturnValue() {
170        return mReturnValue;
171    }
172
173    /**
174     * Sets the value that {@link #open()} will return to the caller.
175     * @param returnValue The new value to be returned by {@link #open()}.
176     */
177    protected void setReturnValue(boolean returnValue) {
178        mReturnValue = returnValue;
179    }
180
181    /**
182     * Returns the shell created by {@link #createShell()}.
183     * @return The current {@link Shell}.
184     */
185    protected Shell getShell() {
186        return mShell;
187    }
188
189    /**
190     * Saves the dialog size and close the dialog.
191     * The {@link #open()} method will given return value (see {@link #setReturnValue(boolean)}.
192     * <p/>
193     * It's safe to call this method before the shell is initialized,
194     * in which case the dialog will close as soon as possible.
195     */
196    protected void close() {
197        if (mShell != null && !mShell.isDisposed()) {
198            saveSize();
199            getShell().close();
200        }
201        mQuitRequested = true;
202    }
203
204    //-------
205
206    /**
207     * Centers the dialog in its parent shell.
208     */
209    private void positionShell() {
210        // Centers the dialog in its parent shell
211        Shell child = mShell;
212        Shell parent = getParent();
213        if (child != null && parent != null) {
214            // get the parent client area with a location relative to the display
215            Rectangle parentArea = parent.getClientArea();
216            Point parentLoc = parent.getLocation();
217            int px = parentLoc.x;
218            int py = parentLoc.y;
219            int pw = parentArea.width;
220            int ph = parentArea.height;
221
222            // Reuse the last size if there's one, otherwise use the default
223            Point childSize = sLastSizeMap.get(this.getClass());
224            if (childSize == null) {
225                childSize = child.getSize();
226            }
227            int cw = childSize.x;
228            int ch = childSize.y;
229
230            int x = px + (pw - cw) / 2;
231            if (x < 0) x = 0;
232
233            int y = py + (ph - ch) / 2;
234            if (y < MIN_Y) y = MIN_Y;
235
236            child.setLocation(x, y);
237            child.setSize(cw, ch);
238        }
239    }
240
241    private void saveSize() {
242        if (mShell != null && !mShell.isDisposed()) {
243            sLastSizeMap.put(this.getClass(), mShell.getSize());
244        }
245    }
246
247}
248