1 module rpui.view_component;
2 
3 public import rpui.view_component.attributes;
4 
5 import core.memory;
6 
7 import std.meta;
8 import std.traits;
9 import std.path;
10 
11 import rpui.events;
12 import rpui.widget_events;
13 import rpui.shortcuts : Shortcuts;
14 import rpui.view;
15 import rpui.widget;
16 import rpui.traits;
17 import rpui.paths;
18 import rpui.rpdl_widget_factory;
19 import rpui.events_observer;
20 
21 /**
22  * ViewComponent is a container for widgets with additional attributes processing
23  * such as `rpui.view.attributes.accessors` for more convinient way to
24  * access widgets, attach shortcuts to view methods and so on.
25  */
26 abstract class ViewComponent {
27     @property Shortcuts shortcuts() { return shortcuts_; }
28     private Shortcuts shortcuts_;
29 
30     private View view;
31     private Widget rootWidget;
32     RpdlWidgetFactory widgetFactory;
33     private Subscriber shortcutsSubscriber;
34 
35     @shortcut("General.focusNext")
36     final void focusNext() {
37         view.focusNext();
38     }
39 
40     @shortcut("General.focusPrev")
41     final void focusPrev() {
42         view.focusPrev();
43     }
44 
45     @shortcut("General.copy")
46     final void copy() {
47         view.events.notify(CopyCommand());
48     }
49 
50     @shortcut("General.paste")
51     final void paste() {
52         view.events.notify(PasteCommand());
53     }
54 
55     @shortcut("General.cut")
56     final void cut() {
57         view.events.notify(CutCommand());
58     }
59 
60     @shortcut("General.unselect")
61     final void unselect() {
62         view.events.notify(UnselectCommand());
63     }
64 
65     @shortcut("General.selectAll")
66     final void selectAll() {
67         view.events.notify(SelectAllCommand());
68     }
69 
70     void onCreate() {
71     }
72 
73     void onDestroy() {
74     }
75 
76     void onProgress(in ProgressEvent event) {
77     }
78 
79     /**
80      * Create viewComponent with `view` from `layoutFileName` and load shortcuts
81      * from `shortcutsFileName`.
82      */
83     this(this T)(View view, in string layoutFileName, in string shortcutsFileName) {
84         assert(view !is null);
85         this.view = view;
86 
87         widgetFactory = new RpdlWidgetFactory(view, layoutFileName);
88         widgetFactory.createWidgets();
89         rootWidget = widgetFactory.rootWidget;
90         assert(rootWidget !is null);
91 
92         shortcuts_ = Shortcuts.createFromFile(shortcutsFileName);
93         readAttributes!T();
94 
95         shortcutsSubscriber = view.events.subscribe!KeyReleasedEvent(
96             event => shortcuts.onKeyReleased(event.key)
97         );
98         GC.collect();
99     }
100 
101     ~this() {
102         view.events.unsubscribe(shortcutsSubscriber);
103         onDestroy();
104         GC.collect();
105     }
106 
107     /**
108      * Create new view instance from file placed in $(I res/ui/layouts).
109      * Instance will be created of `T` type.
110      */
111     static createFromFile(T : ViewComponent)(View view, in string fileName) {
112         const paths = createPathes();
113         const layoutPath = buildPath(paths.resources, "ui", "layouts", fileName);
114         const shortcutsPath = buildPath(paths.resources, "ui", "shortcuts", "general.rdl");
115         auto component = new T(view, layoutPath, shortcutsPath);
116         component.onCreate();
117         return component;
118     }
119 
120     /**
121      * Create new view instance from file placed in $(I res/ui/layouts).
122      * Instance will be created of `T` type.
123      */
124     static createFromFileWithShortcuts(T : ViewComponent)(View view, in string fileName, in string shortcuts = "") {
125         const paths = createPathes();
126         const layoutPath = buildPath(paths.resources, "ui", "layouts", fileName);
127         const shortcutsPath = buildPath(paths.resources, "ui", "shortcuts", shortcuts == "" ? fileName : shortcuts);
128         auto component = new T(view, layoutPath, shortcutsPath);
129         component.onCreate();
130         return component;
131     }
132 
133     /**
134      * Create new view instance from file placed in $(I res/ui/layouts) with custom shorcuts
135      * `shortcutsFilename`. Instance will be created of `T` type.
136      */
137     static createFromFile(T : ViewComponent)(View view, in string layoutFileName,
138                                              in string shortcutsFilename)
139     {
140         const paths = createPathes();
141         const layoutPath = buildPath(paths.resources, "ui", "layouts", layoutFileName);
142         const shortcutsPath = buildPath(paths.resources, "ui", "shortcuts", shortcutsFilename);
143         auto component = new T(view, layoutPath, shortcutsPath);
144         component.onCreate();
145         return component;
146     }
147 
148     private void readAttributes(T : ViewComponent)() {
149         T viewComponent = cast(T) this;
150         readEventsAttributes(viewComponent);
151         readBindWidgetAttributes(viewComponent);
152         readBindGroupWidgets(viewComponent);
153         readShortcutsAttributes(viewComponent);
154     }
155 
156     private void readEventsAttributes(T : ViewComponent)(T viewComponent) {
157         enum events = AliasSeq!(
158             "onClick",
159             "onDblClick",
160             "onFocus",
161             "onBlur",
162             "onMouseWheel",
163             "onKeyReleased",
164             "onTextEntered",
165             "onMouseMove",
166             "onMouseWheel",
167             "onMouseEnter",
168             "onMouseLeave",
169             "onMouseDown",
170             "onMouseUp"
171         );
172 
173         static foreach (eventName; events) {
174             readEventAttribute!(T, eventName)(viewComponent);
175         }
176     }
177 
178     /**
179      * Read event listener attributes and assign this listener to
180      * widget with name uda.widgetName, where uda is attribute
181      */
182     private void readEventAttribute(T : ViewComponent, string eventName)(T viewComponent) {
183         mixin("alias event = " ~ eventName ~ "Listener;");
184 
185         foreach (symbolName; getSymbolsNamesByUDA!(T, event)) {
186             mixin("alias symbol = T." ~ symbolName ~ ";");
187             assert(isFunction!symbol);
188 
189             foreach (uda; getUDAs!(symbol, event)) {
190                 Widget widget = findWidgetByName!(uda.widgetName);
191                 assert(widget !is null, "Widget hasn't found: " ~ uda.widgetName);
192 
193                 enum widgetEventName = eventName[2..$];
194 
195                 // widget.events.subscribe!(clickEvent)(&viewComponent.onOkButtonClick);
196                 mixin("widget.events.subscribe!(" ~ widgetEventName ~ "Event)(&viewComponent." ~ symbolName ~ ");");
197             }
198         }
199     }
200 
201     /// Reading ViewWidget attributes to extract widget by name to variable
202     private void readBindWidgetAttributes(T : ViewComponent)(T viewComponent) {
203         foreach (symbolName; getSymbolsNamesByUDA!(T, bindWidget)) {
204             mixin("alias symbol = T." ~ symbolName ~ ";");
205 
206             foreach (uda; getUDAs!(symbol, bindWidget)) {
207                 enum widgetName = getNameFromAttribute!uda(symbolName);
208 
209                 Widget widget = findWidgetByName!(widgetName);
210                 assert(widget !is null, widgetName ~ " not found");
211 
212                 // alias WidgetType = typeof(view.cancelButton);
213                 // view.cancelButton = cast(WidgetType) widget;
214                 mixin("alias WidgetType = typeof(viewComponent." ~ symbolName ~ ");");
215                 mixin("viewComponent." ~ symbolName ~ " = cast(WidgetType) widget;");
216             }
217         }
218     }
219 
220     /// Find widget in relative view root widget.
221     Widget findWidgetByName(alias name)() {
222         assert(rootWidget !is null);
223 
224         if (rootWidget.name == name) {
225             return rootWidget;
226         } else {
227             return rootWidget.resolver.findWidgetByName(name);
228         }
229     }
230 
231     /**
232      * Get widget name from attribute or set as symbolName
233      * if empty or if it is struct
234      */
235     private static string getNameFromAttribute(alias uda)(in string symbolName) {
236         static if (isType!uda) {
237             return symbolName;
238         } else {
239             static if (uda.widgetName == "") {
240                 return symbolName;
241             } else {
242                 return uda.widgetName;
243             }
244         }
245     }
246 
247     /**
248      * Reading GroupViewWidgets attributes to extract widget children by parent
249      * widget name to variable
250      */
251     private void readBindGroupWidgets(T : ViewComponent)(T viewComponent) {
252         foreach (symbolName; getSymbolsNamesByUDA!(T, bindGroupWidgets)) {
253             mixin("alias symbol = T." ~ symbolName ~ ";");
254 
255             foreach (uda; getUDAs!(symbol, bindGroupWidgets)) {
256                 enum parentWidgetName = getNameFromAttribute!uda(symbolName);
257 
258                 Widget parentWidget = findWidgetByName!(parentWidgetName);
259                 assert(parentWidget !is null);
260 
261                 // alias WidgetType = ForeachType!(typeof(viewComponent.buttons));
262                 // alias symbolType = typeof(viewComponent.buttons);
263                 mixin("alias WidgetType = ForeachType!(typeof(viewComponent." ~ symbolName ~ "));");
264                 mixin("alias symbolType = typeof(viewComponent." ~ symbolName ~ ");");
265 
266                 uint staticArrayIndex = 0;
267 
268                 foreach (Widget childWidget; parentWidget.children) {
269                     // Select correct widget - if associatedWidget is null then get
270                     // child widget. For example row in StackLayout has one single widget
271                     // this widget will be associated because of this widget is our
272                     // content and row is just wrapper
273                     Widget targetWidget = childWidget.associatedWidget;
274 
275                     if (targetWidget is null)
276                         targetWidget = childWidget;
277 
278                     auto typedTargetWidget = cast(WidgetType) targetWidget;
279                     immutable t = "viewComponent." ~ symbolName ~ " ~= typedTargetWidget;";
280 
281                     static if (is(StaticArrayTypeOf!symbolType)) {
282                         // viewComponent.buttons[staticArrayIndex] = typedTargetWidget;
283                         mixin("viewComponent." ~ symbolName ~ "[staticArrayIndex] = typedTargetWidget;");
284                     } else {
285                         // viewComponent.buttons ~= typedTargetWidget;
286                         mixin("viewComponent." ~ symbolName ~ " ~= typedTargetWidget;");
287                     }
288 
289                     ++staticArrayIndex;
290                 }
291             }
292         }
293     }
294 
295     private void readShortcutsAttributes(T : ViewComponent)(T viewComponent) {
296         foreach (symbolName; getSymbolsNamesByUDA!(T, shortcut)) {
297             // alias symbol = T.shortcutAction;
298             mixin("alias symbol = T." ~ symbolName ~ ";");
299 
300             foreach (uda; getUDAs!(symbol, shortcut)) {
301                 enum shortcutPath = uda.shortcutPath;
302                 // shortcuts.attach(shortcutPath, &viewComponent.shortcutAction);
303                 shortcuts.attach(shortcutPath, &mixin("viewComponent." ~ symbolName));
304             }
305         }
306     }
307 }