1 // Written in the D programming language.
2 
3 /**
4 This module contains common Dialog implementation.
5 
6 
7 Use to create custom dialogs.
8 
9 Synopsis:
10 
11 ----
12 import dlangui.dialogs.dialog;
13 
14 ----
15 
16 Copyright: Vadim Lopatin, 2014
17 License:   Boost License 1.0
18 Authors:   Vadim Lopatin, coolreader.org@gmail.com
19 */
20 module dlangui.dialogs.dialog;
21 
22 import dlangui.core.i18n;
23 import dlangui.core.signals;
24 import dlangui.core.stdaction;
25 import dlangui.widgets.layouts;
26 import dlangui.widgets.controls;
27 import dlangui.widgets.winframe;
28 import dlangui.widgets.popup;
29 import dlangui.platforms.common.platform;
30 
31 import std.conv;
32 
33 /// dialog flag bits
34 enum DialogFlag : uint {
35     /// dialog is modal
36     Modal = 1,
37     /// dialog can be resized
38     Resizable = 2,
39     /// dialog is show in popup widget inside current window instead of separate window
40     Popup = 4,
41 }
42 
43 /// slot to pass dialog result
44 interface DialogResultHandler {
45     public void onDialogResult(Dialog dlg, const Action result);
46 }
47 
48 /// base for all dialogs
49 class Dialog : VerticalLayout {
50     protected Window _window;
51     protected Window _parentWindow;
52     protected PopupWidget _popup;
53     protected UIString _caption;
54     protected uint _flags;
55     protected string _icon;
56     protected int _initialWidth;
57     protected int _initialHeight;
58     protected int _defaultButtonIndex = -1;
59 
60     Signal!DialogResultHandler dialogResult;
61 
62     this(UIString caption, Window parentWindow = null, uint flags = DialogFlag.Modal, int initialWidth = 0, int initialHeight = 0) {
63         super("dialog-main-widget");
64         _initialWidth = initialWidth;
65         _initialHeight = initialHeight;
66         _caption = caption;
67         _parentWindow = parentWindow;
68         _flags = flags;
69         _icon = "";
70     }
71 
72     /**
73         Measure widget according to desired width and height constraints. (Step 1 of two phase layout).
74     */
75     override void measure(int parentWidth, int parentHeight) {
76         super.measure(parentWidth, parentHeight);
77         if ((_flags & DialogFlag.Resizable) && (_flags & DialogFlag.Popup)) {
78             Point sz = Point(_parentWindow.width * 4 / 5, _parentWindow.height * 4 / 5);
79             measuredContent(parentWidth, parentHeight, sz.x, sz.y);
80         }
81     }
82 
83     /// get icon resource id
84     @property string windowIcon() {
85         return _icon;
86     }
87 
88     /// set icon resource id
89     @property Dialog windowIcon(string iconResourceId) {
90         _icon = iconResourceId;
91         static if (BACKEND_GUI) {
92             if (_window) {
93                 if (_icon.length == 0)
94                     _window.windowIcon = drawableCache.getImage(Platform.instance.defaultWindowIcon);
95                 else
96                     _window.windowIcon = drawableCache.getImage(_icon);
97             }
98         }
99         return this;
100     }
101 
102     @property UIString windowCaption() {
103         return _caption;
104     }
105 
106     /// set window caption
107     @property Dialog windowCaption(dstring caption) {
108         _caption = caption;
109         if (_window)
110             _window.windowCaption = caption;
111         return this;
112     }
113 
114     /// get window caption
115     @property Dialog windowCaption(UIString caption) {
116         _caption = caption;
117         if (_window)
118             _window.windowCaption = caption;
119         return this;
120     }
121 
122     protected const(Action) [] _buttonActions;
123 
124     protected ImageTextButton _defaultButton;
125     protected ImageTextButton _cancelButton;
126     /// create panel with buttons based on list of actions
127     Widget createButtonsPanel(const(Action) [] actions, int defaultActionIndex, int splitBeforeIndex) {
128         _defaultButtonIndex = defaultActionIndex;
129         _buttonActions = actions;
130         LinearLayout res = new HorizontalLayout("buttons");
131         res.layoutWidth(FILL_PARENT);
132         res.layoutWeight = 0;
133         for (int i = 0; i < actions.length; i++) {
134             if (splitBeforeIndex == i)
135                 res.addChild(new HSpacer());
136             const Action a = actions[i];
137             string id = "btn" ~ to!string(a.id);
138             ImageTextButton btn = new ImageTextButton(id, a.iconId, a.label);
139             if (defaultActionIndex == i) {
140                 btn.setState(State.Default);
141                 _defaultButton = btn;
142             }
143             if (a.id == StandardAction.Cancel || a.id == StandardAction.No)
144                 _cancelButton = btn;
145             btn.action = a.clone();
146             res.addChild(btn);
147         }
148         return res;
149     }
150 
151     /// map key to action
152     override Action findKeyAction(uint keyCode, uint flags) {
153         foreach(a; _buttonActions) {
154             if (a.checkAccelerator(keyCode, flags))
155                 return a.clone;
156         }
157         return super.findKeyAction(keyCode, flags);
158     }
159 
160     /// Custom handling of actions
161     override bool handleAction(const Action action) {
162         foreach(const Action a; _buttonActions)
163             if (a.id == action.id) {
164                 close(action);
165                 return true;
166             }
167         return false;
168     }
169 
170     /// override to implement creation of dialog controls
171     void initialize() {
172     }
173 
174     /** Notify about dialog result, and then close dialog.
175 
176         If onDialogResult listener is assigned, pass action to it.
177 
178         If no onDialogResult listener, pass to owner window.
179 
180         If action is null, no result dispatching will occur.
181       */
182     void close(const Action action) {
183         if (action) {
184             if (dialogResult.assigned)
185                 dialogResult(this, action);
186             else if (_parentWindow && !_popup)
187                 _parentWindow.dispatchAction(action);
188         }
189         if (_popup)
190             _parentWindow.removePopup(_popup);
191         else
192             window.close();
193     }
194 
195     /// shows dialog
196     void show() {
197         initialize();
198         uint wflags = 0;
199         if (_flags & DialogFlag.Modal)
200             wflags |= WindowFlag.Modal;
201         if (_flags & DialogFlag.Resizable) {
202             wflags |= WindowFlag.Resizable;
203             layoutWidth = FILL_PARENT;
204             layoutHeight = FILL_PARENT;
205         }
206         if (_flags & DialogFlag.Popup) {
207             DialogFrame _frame = new DialogFrame(this, _cancelButton !is null);
208             if (_cancelButton) {
209                 _frame.closeButtonClick = delegate(Widget w) {
210                     close(_cancelButton.action);
211                     return true;
212                 };
213             }
214             _popup = _parentWindow.showPopup(_frame);
215             _popup.flags(PopupFlags.Modal);
216         } else {
217             if (_initialWidth == 0 && _initialHeight == 0)
218                 wflags |= WindowFlag.MeasureSize;
219             _window = Platform.instance.createWindow(_caption, _parentWindow, wflags, _initialWidth, _initialHeight);
220             windowIcon = _icon;
221             _window.backgroundColor = currentTheme.customColor("dialog_background");
222             _window.mainWidget = this;
223             _window.show();
224         }
225         onShow();
226     }
227 
228     /// called after window with dialog is shown
229     void onShow() {
230         // override to do something useful
231         if (_defaultButton)
232             _defaultButton.setFocus();
233     }
234 
235     /// calls close with default action; returns true if default action is found and invoked
236     protected bool closeWithDefaultAction() {
237         if (_defaultButtonIndex >= 0 && _defaultButtonIndex < _buttonActions.length) {
238             close(_buttonActions[_defaultButtonIndex]);
239             return true;
240         }
241         return false;
242     }
243 
244     /// calls close with cancel action (if found); returns true if cancel action is found and invoked
245     protected bool closeWithCancelAction() {
246         for (int i = 0; i < _buttonActions.length; i++) {
247             if (_buttonActions[i].id == StandardAction.Cancel || _buttonActions[i].id == StandardAction.No) {
248                 close(_buttonActions[_defaultButtonIndex]);
249                 return true;
250             }
251         }
252         return false;
253     }
254 
255     /// pass key event here; returns true if search text is updated and you can move selection using it
256     override bool onKeyEvent(KeyEvent event) {
257         if (event.action == KeyAction.KeyUp) {
258             if (event.keyCode == KeyCode.RETURN && event.modifiers == KeyFlag.Control) {
259                 // Ctrl+Enter: default action
260                 return closeWithDefaultAction();
261             }
262             if (event.keyCode == KeyCode.ESCAPE && event.noModifiers) {
263                 // ESC: cancel/no action
264                 return closeWithCancelAction();
265             }
266         }
267         return super.onKeyEvent(event);
268     }
269 }
270 
271 /// frame with caption for dialog
272 class DialogFrame : WindowFrame {
273     protected Dialog _dialog;
274     this(Dialog dialog, bool enableCloseButton) {
275         super(dialog.id ~ "_frame", enableCloseButton);
276         styleId = STYLE_FLOATING_WINDOW;
277         _dialog = dialog;
278         _caption.text = _dialog.windowCaption.value;
279         bodyWidget = _dialog;
280     }
281 }