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