1 // Written in the D programming language.
2 
3 /**
4 This module contains definition for main widget for usual application - with menu and status bar.
5 
6 When you need MainMenu, StatusBar, Toolbars in your app, reuse this class.
7 
8 Synopsis:
9 
10 ----
11 import dlangui.widgets.appframe;
12 
13 ----
14 
15 Copyright: Vadim Lopatin, 2015
16 License:   Boost License 1.0
17 Authors:   Vadim Lopatin, coolreader.org@gmail.com
18 */
19 module dlangui.widgets.appframe;
20 
21 import dlangui.widgets.widget;
22 import dlangui.widgets.menu;
23 import dlangui.widgets.layouts;
24 import dlangui.widgets.statusline;
25 import dlangui.widgets.toolbars;
26 import dlangui.core.files;
27 import dlangui.core.settings;
28 import std.path;
29 
30 /// to update status for background operation in AppFrame
31 class BackgroundOperationWatcher {
32 
33     protected AppFrame _frame;
34     protected bool _cancelRequested;
35     protected bool _finished;
36 
37     this(AppFrame frame) {
38         _frame = frame;
39     }
40 
41     /// returns cancel status
42     @property bool cancelRequested() { return _cancelRequested; }
43     /// returns description of background operation to show in status line
44     @property dstring description() { return null; }
45     /// returns icon of background operation to show in status line
46     @property string icon() { return null; }
47     /// returns desired update interval
48     @property long updateInterval() { return 100; }
49     /// update background operation status
50     void update() {
51         // do some work here
52         // when task is done or cancelled, finished should return true
53         // either simple update of status or some real work can be done here
54         if (_frame.statusLine)
55             _frame.statusLine.setBackgroundOperationStatus(icon, description);
56     }
57     /// request cancel - once cancelled, finished should return true
58     void cancel() {
59         _cancelRequested = true;
60     }
61     /// return true when task is done - to remove it from AppFrame
62     @property bool finished() {
63         return _finished;
64     }
65     /// will be called by app frame when BackgroundOperationWatcher is to be removed
66     void removing() {
67         // in this handler, you can post new background operation to AppFrame
68         if (_frame.statusLine)
69             _frame.statusLine.setBackgroundOperationStatus(null, null);
70     }
71 }
72 
73 /// base class for application frame with main menu, status line, toolbars
74 class AppFrame : VerticalLayout, MenuItemClickHandler, MenuItemActionHandler {
75     protected MainMenu _mainMenu;
76     protected StatusLine _statusLine;
77     protected ToolBarHost _toolbarHost;
78     protected Widget _body;
79     protected BackgroundOperationWatcher _currentBackgroundOperation;
80     protected ulong _currentBackgroundOperationTimer;
81 
82 
83     this() {
84         super("APP_FRAME");
85         layoutWidth = FILL_PARENT;
86         layoutHeight = FILL_PARENT;
87         _appName = "dlangui";
88         initialize();
89     }
90 
91     protected string _appName;
92     /// override to return some identifier for app, e.g. to use as settings directory name
93     @property string appCodeName() {
94         return _appName;
95     }
96     /// override to return some identifier for app, e.g. to use as settings directory name
97     @property AppFrame appCodeName(string name) {
98         _appName = name;
99         return this;
100     }
101 
102     protected string _settingsDir;
103     /// Application settings directory; by default, returns .appcodename directory in user's home directory (e.g. /home/user/.appcodename, C:\Users\User\AppData\Roaming\.appcodename); override to change it
104     @property string settingsDir() {
105         if (!_settingsDir)
106             _settingsDir = appDataPath("." ~ appCodeName);
107         return _settingsDir;
108     }
109 
110     protected SettingsFile _shortcutSettings;
111     /// returns shortcuts settings object
112     @property SettingsFile shortcutSettings() {
113         if (!_shortcutSettings) {
114             _shortcutSettings = new SettingsFile(buildNormalizedPath(settingsDir, "shortcuts.json"));
115         }
116         return _shortcutSettings;
117     }
118 
119     bool applyShortcutsSettings() {
120         if (shortcutSettings.loaded) {
121             foreach(key, value; _shortcutSettings.map) {
122                 int actionId = actionNameToId(key);
123                 if (actionId == 0) {
124                     Log.e("applyShortcutsSettings: Unknown action name: ", key);
125                 } else {
126                     Accelerator[] accelerators = [];
127                     if (value.isArray) {
128                         for (int i = 0; i < value.length; i++) {
129                             string v = value[i].str;
130                             Accelerator a;
131                             if (a.parse(v)) {
132                                 //Log.d("Read accelerator for action ", key, " : ", a.toString);
133                                 accelerators ~= a;
134                             } else
135                                 Log.e("applyShortcutsSettings: cannot parse accelerator: ", v);
136                         }
137                     } else {
138                         string v = value.str;
139                         Accelerator a;
140                         if (a.parse(v)) {
141                             //Log.d("Read accelerator for action ", key, " : ", a.toString);
142                             accelerators ~= a;
143                         } else
144                             Log.e("applyShortcutsSettings: cannot parse accelerator: ", v);
145                     }
146                     setActionAccelerators(actionId, accelerators);
147                 }
148             }
149             return true;
150         }
151         return false;
152     }
153 
154     /// set shortcut settings from actions and save to file - useful for initial settings file version creation
155     bool saveShortcutsSettings(const(Action)[] actions) {
156         shortcutSettings.clear();
157         foreach(a; actions) {
158             string name = actionIdToName(a.id);
159             if (name) {
160                 const(Accelerator)[] acc = a.accelerators;
161                 if (acc.length > 0) {
162                     if (acc.length == 1) {
163                         _shortcutSettings[name] = acc[0].toString;
164                     } else {
165                         string[] array;
166                         foreach(accel; acc) {
167                             array ~= accel.toString;
168                         }
169                         _shortcutSettings[name] = array;
170                     }
171                 }
172             }
173         }
174         return shortcutSettings.save();
175     }
176 
177     /// timer handler
178     override bool onTimer(ulong timerId) {
179         if (timerId == _currentBackgroundOperationTimer) {
180             if (_currentBackgroundOperation) {
181                 _currentBackgroundOperation.update();
182                 if (_currentBackgroundOperation.finished) {
183                     _currentBackgroundOperation.removing();
184                     destroy(_currentBackgroundOperation);
185                     _currentBackgroundOperation = null;
186                     _currentBackgroundOperationTimer = 0;
187                     requestActionsUpdate();
188                     return false;
189                 }
190                 return true;
191             } else {
192                 _currentBackgroundOperationTimer = 0;
193             }
194         }
195         return false; // stop timer
196     }
197 
198     /// set background operation to show in status
199     void setBackgroundOperation(BackgroundOperationWatcher op) {
200         if (_currentBackgroundOperation) {
201             _currentBackgroundOperation.removing();
202             destroy(_currentBackgroundOperation);
203             _currentBackgroundOperation = null;
204         }
205         _currentBackgroundOperation = op;
206         if (op)
207             _currentBackgroundOperationTimer = setTimer(op.updateInterval);
208         requestActionsUpdate();
209     }
210 
211     /// main menu widget
212     @property MainMenu mainMenu() { return _mainMenu; }
213     /// status line widget
214     @property StatusLine statusLine() { return _statusLine; }
215     /// tool bar host
216     @property ToolBarHost toolbars() { return _toolbarHost; }
217     /// body widget
218     @property Widget frameBody() { return _body; }
219 
220     /// map key to action
221     override Action findKeyAction(uint keyCode, uint flags) {
222         if (_mainMenu) {
223             Action action = _mainMenu.findKeyAction(keyCode, flags);
224             if (action)
225                 return action;
226         }
227         return super.findKeyAction(keyCode, flags);
228     }
229 
230     protected void initialize() {
231         _mainMenu = createMainMenu();
232         _toolbarHost = createToolbars();
233         _statusLine = createStatusLine();
234         _body = createBody();
235         _body.focusGroup = true;
236         if (_mainMenu) {
237             _mainMenu.menuItemClick = &onMenuItemClick;
238             addChild(_mainMenu);
239         }
240         if (_toolbarHost)
241             addChild(_toolbarHost);
242         addChild(_body);
243         if (_statusLine)
244             addChild(_statusLine);
245         updateShortcuts();
246     }
247 
248     /// override it
249     protected void updateShortcuts() {
250     }
251 
252     /// override to handle main menu commands
253     override bool onMenuItemClick(MenuItem item) {
254         // default handling: call Action handler
255         return onMenuItemAction(item.action);
256     }
257 
258     /// override to handle main menu actions
259     bool onMenuItemAction(const Action action) {
260         // default handling: dispatch action using window (first offered to focused control, then to main widget)
261         return window.dispatchAction(action);
262     }
263 
264     /// create main menu
265     protected MainMenu createMainMenu() {
266         return new MainMenu(new MenuItem());
267     }
268 
269 
270     /// create app toolbars
271     protected ToolBarHost createToolbars() {
272         ToolBarHost res = new ToolBarHost();
273         return res;
274     }
275 
276 
277     /// create app status line widget
278     protected StatusLine createStatusLine() {
279         return new StatusLine();
280     }
281 
282     /// create app body widget
283     protected Widget createBody() {
284         Widget res = new Widget("APP_FRAME_BODY");
285         res.layoutWidth(FILL_PARENT).layoutHeight(FILL_PARENT);
286         return res;
287     }
288 
289     /// override to handle specific actions
290     override bool handleAction(const Action a) {
291         return false;
292     }
293 
294 }