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