1 /*
2     Copyright © 2020, Inochi2D Project
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4     
5     Authors: Luna Nielsen
6 */
7 module creator.widgets.mainmenu;
8 import creator.windows;
9 import creator.widgets;
10 import creator.panels;
11 import creator.core;
12 import creator.core.input;
13 import creator.utils.link;
14 import creator;
15 import inochi2d;
16 import inochi2d.core.dbg;
17 import tinyfiledialogs;
18 import i18n;
19 
20 import std.string;
21 import std.stdio;
22 
23 private {
24     bool dbgShowStyleEditor;
25     bool dbgShowDebugger;
26 
27     void fileNew() {
28         incNewProject();
29     }
30 
31     void fileOpen() {
32         const TFD_Filter[] filters = [
33             { ["*.inx"], "Inochi Creator Project (*.inx)" }
34         ];
35 
36         c_str filename = tinyfd_openFileDialog(__("Open..."), "", filters, false);
37         if (filename !is null) {
38             string file = cast(string)filename.fromStringz;
39             incOpenProject(file);
40         }
41     }
42 
43     void fileSave() {
44         // If a projeect path is set then the user has opened or saved
45         // an existing file, we should just override that
46         if (incProjectPath.length > 0) {
47             // TODO: do backups on every save?
48 
49             incSaveProject(incProjectPath);
50         } else {
51             const TFD_Filter[] filters = [
52                 { ["*.inx"], "Inochi Creator Project (*.inx)" }
53             ];
54 
55             c_str filename = tinyfd_saveFileDialog(__("Save..."), "", filters);
56             if (filename !is null) {
57                 string file = cast(string)filename.fromStringz;
58                 incSaveProject(file);
59             }
60         }
61     }
62 
63     void fileSaveAs() {
64         const TFD_Filter[] filters = [
65             { ["*.inx"], "Inochi Creator Project (*.inx)" }
66         ];
67 
68         c_str filename = tinyfd_saveFileDialog(__("Save As..."), "", filters);
69         if (filename !is null) {
70             string file = cast(string)filename.fromStringz;
71             incSaveProject(file);
72         }
73     }
74 }
75 
76 void incMainMenu() {
77     auto io = igGetIO();
78 
79     if (incShortcut("Ctrl+N")) fileNew();
80     if (incShortcut("Ctrl+O")) fileOpen();
81     if (incShortcut("Ctrl+S")) fileSave();
82     if (incShortcut("Ctrl+Shift+S")) fileSaveAs();
83 
84     if(igBeginMainMenuBar()) {
85         ImVec2 avail;
86         igGetContentRegionAvail(&avail);
87         if (incGetUseNativeTitlebar()) {
88             igImage(
89                 cast(void*)incGetLogo(), 
90                 ImVec2(avail.y*2, avail.y*2), 
91                 ImVec2(0, 0), ImVec2(1, 1), 
92                 ImVec4(1, 1, 1, 1), 
93                 ImVec4(0, 0, 0, 0)
94             );
95 
96             igSeparator();
97         }
98 
99         if (igBeginMenu(__("File"), true)) {
100             if(igMenuItem(__("New"), "Ctrl+N", false, true)) {
101                 fileNew();
102             }
103 
104             if (igMenuItem(__("Open"), "Ctrl+O", false, true)) {
105                 fileOpen();
106             }
107 
108             string[] prevProjects = incGetPrevProjects();
109             if (igBeginMenu(__("Recent"), prevProjects.length > 0)) {
110                 foreach(project; incGetPrevProjects) {
111                     import std.path : baseName;
112                     if (igMenuItem(project.baseName.toStringz, "", false, true)) {
113                         incOpenProject(project);
114                     }
115                     incTooltip(project);
116                 }
117                 igEndMenu();
118             }
119             
120             if(igMenuItem(__("Save"), "Ctrl+S", false, true)) {
121                 fileSave();
122             }
123             
124             if(igMenuItem(__("Save As..."), "Ctrl+Shift+S", false, true)) {
125                 fileSaveAs();
126             }
127 
128             if (igBeginMenu(__("Import"), true)) {
129                 if(igMenuItem_Bool(__("Photoshop Document"), "", false, true)) {
130                     const TFD_Filter[] filters = [
131                         { ["*.psd"], "Photoshop Document (*.psd)" }
132                     ];
133 
134                     c_str filename = tinyfd_openFileDialog(__("Import..."), "", filters, false);
135                     if (filename !is null) {
136                         string file = cast(string)filename.fromStringz;
137                         incImportPSD(file);
138                     }
139                 }
140                 incTooltip(_("Import a standard Photoshop PSD file."));
141 
142                 if (igMenuItem_Bool(__("Inochi2D Puppet"), "", false, true)) {
143                     const TFD_Filter[] filters = [
144                         { ["*.inp"], "Inochi2D Puppet (*.inp)" }
145                     ];
146 
147                     c_str filename = tinyfd_openFileDialog(__("Import..."), "", filters, false);
148                     if (filename !is null) {
149                         string file = cast(string)filename.fromStringz;
150                         incImportINP(file);
151                     }
152                 }
153                 incTooltip(_("Import existing puppet file, editing options limited"));
154 
155                 if (igMenuItem_Bool(__("Image Folder"))) {
156                     c_str folder = tinyfd_selectFolderDialog(__("Select a Folder..."), null);
157                     if (folder !is null) {
158                         incImportFolder(cast(string)folder.fromStringz);
159                     }
160                 }
161                 incTooltip(_("Supports PNGs, TGAs and JPEGs."));
162                 igEndMenu();
163             }
164 
165             if (igBeginMenu(__("Export"), true)) {
166                 if(igMenuItem_Bool(__("Inochi Puppet"), "", false, true)) {
167                     const TFD_Filter[] filters = [
168                         { ["*.inp"], "Inochi2D Puppet (*.inp)" }
169                     ];
170 
171                     import std.path : setExtension;
172 
173                     c_str filename = tinyfd_saveFileDialog(__("Export..."), "", filters);
174                     if (filename !is null) {
175                         string file = cast(string)filename.fromStringz;
176 
177                         incExportINP(file);
178                     }
179                 }
180                 igEndMenu();
181             }
182 
183             if(igMenuItem_Bool(__("Quit"), "Alt+F4", false, true)) incExit();
184             igEndMenu();
185         }
186         
187         if (igBeginMenu(__("Edit"), true)) {
188             if(igMenuItem_Bool(__("Undo"), "Ctrl+Z", false, incActionCanUndo())) incActionUndo();
189             if(igMenuItem_Bool(__("Redo"), "Ctrl+Shift+Z", false, incActionCanRedo())) incActionRedo();
190             
191             igSeparator();
192             if(igMenuItem_Bool(__("Cut"), "Ctrl+X", false, false)) {}
193             if(igMenuItem_Bool(__("Copy"), "Ctrl+C", false, false)) {}
194             if(igMenuItem_Bool(__("Paste"), "Ctrl+V", false, false)) {}
195 
196             igSeparator();
197             if(igMenuItem_Bool(__("Settings"), "", false, true)) {
198                 if (!incIsSettingsOpen) incPushWindow(new SettingsWindow);
199             }
200             
201             debug {
202                 igSpacing();
203                 igSpacing();
204 
205                 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("ImGui Debugging"));
206 
207                 igSeparator();
208                 if(igMenuItem_Bool(__("Style Editor"), "", false, true)) dbgShowStyleEditor = !dbgShowStyleEditor;
209                 if(igMenuItem_Bool(__("ImGui Debugger"), "", false, true)) dbgShowDebugger = !dbgShowDebugger;
210             }
211             igEndMenu();
212         }
213 
214         if (igBeginMenu(__("View"), true)) {
215             if (igMenuItem(__("Reset Layout"), null, false, true)) {
216                 incSetDefaultLayout();
217             }
218             igSeparator();
219 
220             // Spacing
221             igSpacing();
222             igSpacing();
223 
224             igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Panels"));
225             igSeparator();
226 
227             foreach(panel; incPanels) {
228 
229                 // Skip panels that'll always be visible
230                 if (panel.alwaysVisible) continue;
231 
232                 // Show menu item for panel
233                 if(igMenuItem_Bool(panel.displayNameC, null, panel.visible, true)) {
234                     panel.visible = !panel.visible;
235                     incSettingsSet(panel.name~".visible", panel.visible);
236                 }
237             }
238 
239             // Spacing
240             igSpacing();
241             igSpacing();
242             
243             igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Extras"));
244 
245             igSeparator();
246             
247             if (igMenuItem(__("Save Screenshot"), "", false, true)) {
248                 const TFD_Filter[] filters = [
249                     { ["*.png"], "PNG Image (*.png)" }
250                 ];
251 
252                 import std.path : setExtension;
253                 c_str filename = tinyfd_saveFileDialog(__("Save Screenshot..."), "", filters);
254                 if (filename !is null) {
255                     string file = (cast(string)filename.fromStringz).setExtension("png");
256 
257                     // Dump viewport to RGBA byte array
258                     int width, height;
259                     inGetViewport(width, height);
260                     Texture outTexture = new Texture(null, width, height);
261 
262                     // Texture data
263                     inSetClearColor(0, 0, 0, 0);
264                     inBeginScene();
265                         incActivePuppet().update();
266                         incActivePuppet().draw();
267                     inEndScene();
268                     ubyte[] textureData = new ubyte[inViewportDataLength()];
269                     inDumpViewport(textureData);
270                     inTexUnPremuliply(textureData);
271                     
272                     // Write to texture
273                     outTexture.setData(textureData);
274 
275                     outTexture.save(file);
276                 }
277             }
278             incTooltip(_("Saves screenshot as PNG of the editor framebuffer."));
279 
280             if (igMenuItem_Bool(__("Show Stats for Nerds"), "", incShowStatsForNerds, true)) {
281                 incShowStatsForNerds = !incShowStatsForNerds;
282                 incSettingsSet("NerdStats", incShowStatsForNerds);
283             }
284 
285             igEndMenu();
286         }
287 
288         if (igBeginMenu(__("Tools"), true)) {
289             import creator.utils.repair : incAttemptRepairPuppet, incRegenerateNodeIDs;
290 
291             igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Puppet Texturing"));
292             igSeparator();
293 
294             // Premultiply textures, causing every pixel value in every texture to
295             // be multiplied by their Alpha (transparency) component
296             if (igMenuItem(__("Premultiply textures"), "", false)) {
297                 import creator.utils.repair : incPremultTextures;
298                 incPremultTextures(incActivePuppet());
299             }
300             incTooltip(_("Premultiplies textures by their alpha component.\n\nOnly use this if your textures look garbled after importing files from an older version of Inochi Creator."));
301             
302             if (igMenuItem(__("Bleed textures..."), "", false)) {
303                 incRebleedTextures();
304             }
305             incTooltip(_("Causes color to bleed out in to fully transparent pixels, this solves outlines on straight alpha compositing.\n\nOnly use this if your game engine can't use premultiplied alpha."));
306 
307             if (igMenuItem(__("Generate Mipmaps..."), "", false)) {
308                 incRegenerateMipmaps();
309             }
310             incTooltip(_("Regenerates the puppet's mipmaps."));
311 
312             // Spacing
313             igSpacing();
314             igSpacing();
315 
316             igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Puppet Recovery"));
317             igSeparator();
318 
319             // FULL REPAIR
320             if (igMenuItem(__("Attempt full repair..."), "", false)) {
321                 incAttemptRepairPuppet(incActivePuppet());
322             }
323             incTooltip(_("Attempts all the recovery and repair methods below on the currently loaded model"));
324 
325             // REGEN NODE IDs
326             if (igMenuItem(__("Regenerate Node IDs"), "", false)) {
327                 import creator.utils.repair : incAttemptRepairPuppet;
328                 incRegenerateNodeIDs(incActivePuppet().root);
329             }
330             incTooltip(_("Regenerates all the unique IDs for the model"));
331 
332             // Spacing
333             igSpacing();
334             igSpacing();
335             igSeparator();
336             if (igMenuItem(__("Verify INP File..."), "", false)) {
337                 incAttemptRepairPuppet(incActivePuppet());
338             }
339             incTooltip(_("Attempts to verify and repair INP files"));
340 
341             igEndMenu();
342         }
343 
344         if (igBeginMenu(__("Help"), true)) {
345 
346             if(igMenuItem_Bool(__("Tutorial"), "(TODO)", false, false)) { }
347             igSeparator();
348             
349             if(igMenuItem_Bool(__("Online Documentation"), "", false, true)) {
350                 incOpenLink("https://github.com/Inochi2D/inochi-creator/wiki");
351             }
352             
353             if(igMenuItem_Bool(__("Inochi2D Documentation"), "", false, true)) {
354                 incOpenLink("https://github.com/Inochi2D/inochi2d/wiki");
355             }
356             igSeparator();
357 
358             if(igMenuItem_Bool(__("About"), "", false, true)) {
359                 incPushWindow(new AboutWindow);
360             }
361             igEndMenu();
362         }
363 
364         // We need to pre-calculate the size of the right adjusted section
365         // This code is very ugly because imgui doesn't really exactly understand this
366         // stuff natively.
367         ImVec2 secondSectionLength = ImVec2(0, 0);
368         secondSectionLength.x += incMeasureString(_("Donate")).x+16; // Add 16 px padding
369         if (incShowStatsForNerds) { // Extra padding I guess
370             secondSectionLength.x += igGetStyle().ItemSpacing.x;
371             secondSectionLength.x += incMeasureString("1000ms").x;
372         }
373         incDummy(ImVec2(-secondSectionLength.x, 0));
374 
375         if (incShowStatsForNerds) {
376             string fpsText = "%.0fms\0".format(1000f/io.Framerate);
377             float textAreaDummyWidth = incMeasureString("1000ms").x-incMeasureString(fpsText).x;
378             incDummy(ImVec2(textAreaDummyWidth, 0));
379             igText(fpsText.ptr);
380         }
381         
382         // Donate button
383         // NOTE: Is this too obstructive in the UI?
384         if(igMenuItem(__("Donate"))) {
385             incOpenLink("https://www.patreon.com/clipsey");
386         }
387         incTooltip(_("Support development via Patreon"));
388 
389         igEndMainMenuBar();
390 
391         if (dbgShowStyleEditor) igShowStyleEditor(igGetStyle());
392         if (dbgShowDebugger) igShowAboutWindow(&dbgShowDebugger);
393     }
394 }