1 module dmledit;
2 
3 import dlangui;
4 import dlangui.dialogs.filedlg;
5 import dlangui.dialogs.dialog;
6 import dlangui.dml.dmlhighlight;
7 import dlangui.widgets.metadata;
8 import std.array : replaceFirst;
9 import std.algorithm;
10 import std.stdio;
11 import std.array;
12 import std.file;
13 
14 mixin APP_ENTRY_POINT;
15 
16 // action codes
17 enum IDEActions : int {
18     //ProjectOpen = 1010000,
19     FileNew = 1010000,
20     FileOpen,
21     FileSave,
22     FileSaveAs,
23     FileSaveAll,
24     FileClose,
25     FileExit,
26     EditPreferences,
27     DebugStart,
28     HelpAbout,
29 }
30 
31 // actions
32 const Action ACTION_FILE_NEW = new Action(IDEActions.FileNew, "MENU_FILE_NEW"c, "document-new", KeyCode.KEY_N, KeyFlag.Control);
33 const Action ACTION_FILE_SAVE = (new Action(IDEActions.FileSave, "MENU_FILE_SAVE"c, "document-save", KeyCode.KEY_S, KeyFlag.Control)).disableByDefault();
34 const Action ACTION_FILE_SAVE_AS = (new Action(IDEActions.FileSaveAs, "MENU_FILE_SAVE_AS"c)).disableByDefault();
35 const Action ACTION_FILE_OPEN = new Action(IDEActions.FileOpen, "MENU_FILE_OPEN"c, "document-open", KeyCode.KEY_O, KeyFlag.Control);
36 const Action ACTION_FILE_EXIT = new Action(IDEActions.FileExit, "MENU_FILE_EXIT"c, "document-close"c, KeyCode.KEY_X, KeyFlag.Alt);
37 const Action ACTION_EDIT_COPY = (new Action(EditorActions.Copy, "MENU_EDIT_COPY"c, "edit-copy"c, KeyCode.KEY_C, KeyFlag.Control)).addAccelerator(KeyCode.INS, KeyFlag.Control).disableByDefault();
38 const Action ACTION_EDIT_PASTE = (new Action(EditorActions.Paste, "MENU_EDIT_PASTE"c, "edit-paste"c, KeyCode.KEY_V, KeyFlag.Control)).addAccelerator(KeyCode.INS, KeyFlag.Shift).disableByDefault();
39 const Action ACTION_EDIT_CUT = (new Action(EditorActions.Cut, "MENU_EDIT_CUT"c, "edit-cut"c, KeyCode.KEY_X, KeyFlag.Control)).addAccelerator(KeyCode.DEL, KeyFlag.Shift).disableByDefault();
40 const Action ACTION_EDIT_UNDO = (new Action(EditorActions.Undo, "MENU_EDIT_UNDO"c, "edit-undo"c, KeyCode.KEY_Z, KeyFlag.Control)).disableByDefault();
41 const Action ACTION_EDIT_REDO = (new Action(EditorActions.Redo, "MENU_EDIT_REDO"c, "edit-redo"c, KeyCode.KEY_Y, KeyFlag.Control)).addAccelerator(KeyCode.KEY_Z, KeyFlag.Control|KeyFlag.Shift).disableByDefault();
42 const Action ACTION_EDIT_INDENT = (new Action(EditorActions.Indent, "MENU_EDIT_INDENT"c, "edit-indent"c, KeyCode.TAB, 0)).addAccelerator(KeyCode.KEY_BRACKETCLOSE, KeyFlag.Control).disableByDefault();
43 const Action ACTION_EDIT_UNINDENT = (new Action(EditorActions.Unindent, "MENU_EDIT_UNINDENT"c, "edit-unindent", KeyCode.TAB, KeyFlag.Shift)).addAccelerator(KeyCode.KEY_BRACKETOPEN, KeyFlag.Control).disableByDefault();
44 const Action ACTION_EDIT_TOGGLE_LINE_COMMENT = (new Action(EditorActions.ToggleLineComment, "MENU_EDIT_TOGGLE_LINE_COMMENT"c, null, KeyCode.KEY_DIVIDE, KeyFlag.Control)).disableByDefault();
45 const Action ACTION_EDIT_TOGGLE_BLOCK_COMMENT = (new Action(EditorActions.ToggleBlockComment, "MENU_EDIT_TOGGLE_BLOCK_COMMENT"c, null, KeyCode.KEY_DIVIDE, KeyFlag.Control|KeyFlag.Shift)).disableByDefault();
46 const Action ACTION_EDIT_PREFERENCES = (new Action(IDEActions.EditPreferences, "MENU_EDIT_PREFERENCES"c, null)).disableByDefault();
47 const Action ACTION_DEBUG_START = new Action(IDEActions.DebugStart, "MENU_DEBUG_UPDATE_PREVIEW"c, "debug-run"c, KeyCode.F5, 0);
48 const Action ACTION_HELP_ABOUT = new Action(IDEActions.HelpAbout, "MENU_HELP_ABOUT"c);
49 
50 /// DIDE source file editor
51 class DMLSourceEdit : SourceEdit {
52     this(string ID) {
53         super(ID);
54         MenuItem editPopupItem = new MenuItem(null);
55         editPopupItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, ACTION_EDIT_CUT, ACTION_EDIT_UNDO, ACTION_EDIT_REDO, ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT, ACTION_EDIT_TOGGLE_LINE_COMMENT, ACTION_DEBUG_START);
56         popupMenu = editPopupItem;
57         content.syntaxSupport = new DMLSyntaxSupport("");
58         setTokenHightlightColor(TokenCategory.Comment, 0x008000); // green
59         setTokenHightlightColor(TokenCategory.Keyword, 0x0000FF); // blue
60         setTokenHightlightColor(TokenCategory.String, 0xa31515);  // brown
61         setTokenHightlightColor(TokenCategory.Integer, 0xa315C0);  //
62         setTokenHightlightColor(TokenCategory.Float, 0xa315C0);  //
63         setTokenHightlightColor(TokenCategory.Error, 0xFF0000);  // red
64         setTokenHightlightColor(TokenCategory.Op, 0x503000);
65         setTokenHightlightColor(TokenCategory.Identifier_Class, 0x000080);  // blue
66 
67     }
68     this() {
69         this("DMLEDIT");
70     }
71 }
72 
73 immutable dstring SAMPLE_SOURCE_CODE =
74 q{VerticalLayout {
75     id: vlayout
76     margins: Rect { left: 5; right: 3; top: 2; bottom: 4 }
77     padding: Rect { 5, 4, 3, 2 } // same as Rect { left: 5; top: 4; right: 3; bottom: 2 }
78     TextWidget {
79         /* this widget can be accessed via id myLabel1
80             e.g. w.childById!TextWidget("myLabel1")
81         */
82         id: myLabel1
83         text: "Some text"; padding: 5
84         enabled: false
85     }
86     TextWidget {
87         id: myLabel2
88         text: "More text"; margins: 5
89         enabled: true
90     }
91     CheckBox{ id: cb1; text: "Some checkbox" }
92     HorizontalLayout {
93         RadioButton { id: rb1; text: "Radio Button 1" }
94         RadioButton { id: rb1; text: "Radio Button 2" }
95     }
96 }
97 };
98 
99 // used to generate property lists once, then simply swap
100 StringListAdapter[string] propListsAdapters;
101 
102 class EditFrame : AppFrame {
103 
104     MenuItem mainMenuItems;
105 
106     override protected void initialize() {
107         _appName = "DMLEdit";
108         super.initialize();
109         updatePreview();
110     }
111 
112     /// create main menu
113     override protected MainMenu createMainMenu() {
114         mainMenuItems = new MenuItem();
115         MenuItem fileItem = new MenuItem(new Action(1, "MENU_FILE"));
116         fileItem.add(ACTION_FILE_NEW, ACTION_FILE_OPEN,
117                      ACTION_FILE_EXIT);
118         mainMenuItems.add(fileItem);
119         MenuItem editItem = new MenuItem(new Action(2, "MENU_EDIT"));
120         editItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE,
121                      ACTION_EDIT_CUT, ACTION_EDIT_UNDO, ACTION_EDIT_REDO,
122                      ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT, ACTION_EDIT_TOGGLE_LINE_COMMENT, ACTION_EDIT_TOGGLE_BLOCK_COMMENT, ACTION_DEBUG_START);
123 
124         editItem.add(ACTION_EDIT_PREFERENCES);
125         mainMenuItems.add(editItem);
126         MainMenu mainMenu = new MainMenu(mainMenuItems);
127         return mainMenu;
128     }
129 
130 
131     /// create app toolbars
132     override protected ToolBarHost createToolbars() {
133         ToolBarHost res = new ToolBarHost();
134         ToolBar tb;
135         tb = res.getOrAddToolbar("Standard");
136         tb.addButtons(ACTION_FILE_NEW, ACTION_FILE_OPEN, ACTION_FILE_SAVE, ACTION_SEPARATOR, ACTION_DEBUG_START);
137 
138         tb = res.getOrAddToolbar("Edit");
139         tb.addButtons(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, ACTION_EDIT_CUT, ACTION_SEPARATOR,
140                       ACTION_EDIT_UNDO, ACTION_EDIT_REDO, ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT);
141         return res;
142     }
143 
144     string _filename;
145     void openSourceFile(string filename) {
146         import std.file;
147         // TODO
148         if (exists(filename)) {
149             _filename = filename;
150             window.windowCaption = toUTF32(filename);
151             _editor.load(filename);
152             updatePreview();
153         }
154     }
155 
156     void saveSourceFile(string filename) {
157         if (filename.length == 0)
158             filename = _filename;
159         import std.file;
160         _filename = filename;
161         window.windowCaption = toUTF32(filename);
162         _editor.save(filename);
163     }
164 
165     bool onCanClose() {
166         // todo
167         return true;
168     }
169 
170     FileDialog createFileDialog(UIString caption, bool fileMustExist = true) {
171         uint flags = DialogFlag.Modal | DialogFlag.Resizable;
172         if (fileMustExist)
173             flags |= FileDialogFlag.FileMustExist;
174         FileDialog dlg = new FileDialog(caption, window, null, flags);
175         dlg.filetypeIcons[".d"] = "text-dml";
176         return dlg;
177     }
178 
179     void saveAs() {
180     }
181 
182     /// override to handle specific actions
183     override bool handleAction(const Action a) {
184         if (a) {
185             switch (a.id) {
186                 case IDEActions.FileExit:
187                     if (onCanClose())
188                         window.close();
189                     return true;
190                 case IDEActions.HelpAbout:
191                     window.showMessageBox(UIString.fromRaw("About DlangUI ML Editor"d),
192                                           UIString.fromRaw("DLangIDE\n(C) Vadim Lopatin, 2015\nhttp://github.com/buggins/dlangui\nSimple editor for DML code"d));
193                     return true;
194                 case IDEActions.FileNew:
195                     UIString caption;
196                     caption = "Create new DML file"d;
197                     FileDialog dlg = createFileDialog(caption, false);
198                     dlg.addFilter(FileFilterEntry(UIString.fromRaw("DML files"d), "*.dml"));
199                     dlg.addFilter(FileFilterEntry(UIString.fromRaw("All files"d), "*.*"));
200                     dlg.dialogResult = delegate(Dialog dlg, const Action result) {
201                         if (result.id == ACTION_OPEN.id) {
202                             string filename = result.stringParam;
203                             _editor.text=""d;
204                             saveSourceFile(filename);
205                         }
206                     };
207                     dlg.show();
208                     return true;
209                 case IDEActions.FileSave:
210                     if (_filename.length) {
211                         saveSourceFile(_filename);
212                         return true;
213                     }
214                     UIString caption;
215                     caption = "Save DML File as"d;
216                     FileDialog dlg = createFileDialog(caption, false);
217                     dlg.addFilter(FileFilterEntry(UIString.fromRaw("DML files"d), "*.dml"));
218                     dlg.addFilter(FileFilterEntry(UIString.fromRaw("All files"d), "*.*"));
219                     dlg.dialogResult = delegate(Dialog dlg, const Action result) {
220                         if (result.id == ACTION_OPEN.id) {
221                             string filename = result.stringParam;
222                             saveSourceFile(filename);
223                         }
224                     };
225                     dlg.show();
226                     return true;
227                 case IDEActions.FileOpen:
228                     UIString caption;
229                     caption = "Open DML File"d;
230                     FileDialog dlg = createFileDialog(caption);
231                     dlg.addFilter(FileFilterEntry(UIString.fromRaw("DML files"d), "*.dml"));
232                     dlg.addFilter(FileFilterEntry(UIString.fromRaw("All files"d), "*.*"));
233                     dlg.dialogResult = delegate(Dialog dlg, const Action result) {
234                         if (result.id == ACTION_OPEN.id) {
235                             string filename = result.stringParam;
236                             openSourceFile(filename);
237                         }
238                     };
239                     dlg.show();
240                     return true;
241                 case IDEActions.DebugStart:
242                     updatePreview();
243                     return true;
244                 case IDEActions.EditPreferences:
245                     //showPreferences();
246                     return true;
247                 default:
248                     return super.handleAction(a);
249             }
250         }
251         return false;
252     }
253 
254     /// override to handle specific actions state (e.g. change enabled state for supported actions)
255     override bool handleActionStateRequest(const Action a) {
256         switch (a.id) {
257             case IDEActions.HelpAbout:
258             case IDEActions.FileNew:
259             case IDEActions.FileOpen:
260             case IDEActions.DebugStart:
261             case IDEActions.EditPreferences:
262             case IDEActions.FileSaveAs:
263                 a.state = ACTION_STATE_ENABLED;
264                 return true;
265             case IDEActions.FileSave:
266                 if (_editor.content.modified)
267                     a.state = ACTION_STATE_ENABLED;
268                 else
269                     a.state = ACTION_STATE_DISABLE;
270                 return true;
271             default:
272                 return super.handleActionStateRequest(a);
273         }
274     }
275 
276     void updatePreview() {
277         dstring dsource = _editor.text;
278         string source = toUTF8(dsource);
279         try {
280             Widget w = parseML(source);
281             if (statusLine)
282                 statusLine.setStatusText("No errors"d);
283             if (_fillHorizontal)
284                 w.layoutWidth = FILL_PARENT;
285             if (_fillVertical)
286                 w.layoutHeight = FILL_PARENT;
287             if (_highlightBackground)
288                 w.backgroundColor = 0xC0C0C0C0;
289             _preview.contentWidget = w;
290         } catch (ParserException e) {
291             if (statusLine)
292                 statusLine.setStatusText(toUTF32("ERROR: " ~ e.msg));
293             _editor.setCaretPos(e.line, e.pos);
294             string msg = "\n" ~ e.msg ~ "\n";
295             msg = replaceFirst(msg, " near `", "\nnear `");
296             TextWidget w = new MultilineTextWidget(null, toUTF32(msg));
297             w.padding = 10;
298             w.margins = 10;
299             w.maxLines = 10;
300             w.backgroundColor = 0xC0FF8080;
301             _preview.contentWidget = w;
302         }
303     }
304 
305     protected bool _fillHorizontal;
306     protected bool _fillVertical;
307     protected bool _highlightBackground;
308     protected DMLSourceEdit _editor;
309     protected ScrollWidget _preview;
310     /// create app body widget
311     override protected Widget createBody()
312     {
313 
314         DockHost dockHost = new DockHost();
315 
316         dockHost.layoutWidth = FILL_PARENT;
317         dockHost.layoutHeight = FILL_PARENT;
318 
319         WidgetsList widgetsList = new WidgetsList();
320         StringListWidget propList = new StringListWidget();
321 
322         auto sla = new StringListAdapter();
323 
324         auto registeredWidgetList = getRegisteredWidgetsList();
325         registeredWidgetList.sort!("a < b");
326 
327         foreach(const ref widget; registeredWidgetList)
328         { 
329             auto propertyListAdapter = new StringListAdapter();
330             if ( auto meta = findWidgetMetadata(widget) )
331             {
332                 auto mp = meta.properties;
333                 mp.sort!("a.name < b.name");
334                 foreach(const ref prop; mp)
335                 {
336                 propertyListAdapter.add(UIString.fromRaw(prop.name ~ "   [" ~ to!string(prop.type) ~ "]" ));
337                 propListsAdapters[widget] = propertyListAdapter;
338                 }
339             }
340             sla.add(UIString.fromRaw(widget));
341         }
342 
343         widgetsList.adapter = sla;
344 
345         auto leftPanel = new VerticalLayout();
346         leftPanel.layoutHeight = FILL_PARENT;
347 
348         widgetsList.minHeight=800;
349         propList.minHeight=600;
350 
351         leftPanel.addChild(new TextWidget().text("Widgets").backgroundColor(0xdddddd).minHeight(50) );
352         leftPanel.addChild(widgetsList);
353         leftPanel.addChild(new TextWidget().text("Widget properties").backgroundColor(0xdddddd).minHeight(50));
354         leftPanel.addChild(propList);
355 
356         auto leftDockWin = new DockWindow("left dock");
357 
358         leftDockWin.bodyWidget = leftPanel;
359         leftDockWin.dockAlignment = DockAlignment.Left;
360         leftDockWin.layoutWidth = makePercentSize(25);
361         leftDockWin.layoutHeight = FILL_PARENT;
362         dockHost.addDockedWindow(leftDockWin);
363 
364         _editor = new DMLSourceEdit();
365         _editor.text = SAMPLE_SOURCE_CODE;
366 
367         auto editorDockWin = new DockWindow("editor");
368         editorDockWin.layoutWidth = makePercentSize(50);
369         editorDockWin.bodyWidget = _editor;
370         editorDockWin.dockAlignment = DockAlignment.Left;
371         editorDockWin.layoutHeight = FILL_PARENT;
372         dockHost.addDockedWindow(editorDockWin);
373 
374         VerticalLayout previewLayout = new VerticalLayout();
375         previewLayout.layoutHeight = FILL_PARENT;
376 
377         auto previewControls = new HorizontalLayout();
378         auto cbFillHorizontal = new CheckBox(null, "Fill Horizontal"d);
379         auto cbFillVertical = new CheckBox(null, "Fill Vertical"d);
380         auto cbHighlightBackground = new CheckBox(null, "Background"d);
381         cbFillHorizontal.checkChange = delegate(Widget source, bool checked) {
382             _fillHorizontal = checked;
383             updatePreview();
384             return true;
385         };
386         cbFillVertical.checkChange = delegate(Widget source, bool checked) {
387             _fillVertical = checked;
388             updatePreview();
389             return true;
390         };
391         cbHighlightBackground.checkChange = delegate(Widget source, bool checked) {
392             _highlightBackground = checked;
393             updatePreview();
394             return true;
395         };
396         widgetsList.itemClick = delegate (Widget source, int itemIndex){
397             propList.adapter = propListsAdapters[to!string(widgetsList.selectedItem)];
398             return true;
399         };
400         widgetsList.onItemDoubleClick = delegate (Widget source, int itemIndex) {
401             auto caret = _editor.caretPos;
402             auto widgetClassName = widgetsList.selectedItem;
403             EditOperation op = new EditOperation(EditAction.Replace, caret, widgetClassName);
404             _editor.content.performOperation(op, this);
405         };
406 
407         previewControls.addChild(cbFillHorizontal);
408         previewControls.addChild(cbFillVertical);
409         previewControls.addChild(cbHighlightBackground);
410 
411         _preview = new ScrollWidget();
412         _preview.layoutWidth = FILL_PARENT;
413         _preview.layoutHeight = FILL_PARENT;
414         _preview.backgroundImageId = "tx_fabric.tiled";
415         previewLayout.addChild(previewControls);
416         previewLayout.addChild(_preview);
417 
418         auto previewDockWin = new DockWindow("preview");
419         previewDockWin.layoutWidth = makePercentSize(25);
420         previewDockWin.bodyWidget = previewLayout;
421         previewDockWin.dockAlignment = DockAlignment.Right;
422 
423         previewDockWin.layoutHeight = FILL_PARENT;
424         dockHost.bodyWidget = editorDockWin;
425         dockHost.addDockedWindow(previewDockWin);
426 
427         return dockHost;
428     }
429 }
430 
431 alias onItemDoubleClickHandler = void delegate (Widget source, int itemIndex);
432 
433 class WidgetsList : StringListWidget
434 {
435     onItemDoubleClickHandler onItemDoubleClick;
436 
437     override bool onMouseEvent(MouseEvent event) {
438         bool result = super.onMouseEvent(event);
439         if (event.doubleClick) {
440             if (onItemDoubleClick !is null)
441                 onItemDoubleClick(this, selectedItemIndex);
442         }
443         return result;
444     }
445 }
446 
447 /// entry point for dlangui based application
448 extern (C) int UIAppMain(string[] args) {
449 
450     // embed non-standard resources listed in views/resources.list into executable
451     embeddedResourceList.addResources(embedResourcesFromList!("resources.list")());
452 
453     /// set font gamma (1.0 is neutral, < 1.0 makes glyphs lighter, >1.0 makes glyphs bolder)
454     FontManager.fontGamma = 0.8;
455     FontManager.hintingMode = HintingMode.Normal;
456 
457     // create window
458     Window window = Platform.instance.createWindow("DlangUI ML editor"d, null, WindowFlag.Resizable, 700, 470);
459 
460     // create some widget to show in window
461     window.windowIcon = drawableCache.getImage("dlangui-logo1");
462 
463     // create some widget to show in window
464     window.mainWidget = new EditFrame();
465 
466     // show window
467     window.show();
468 
469     // run message loop
470     return Platform.instance.enterMessageLoop();
471 }