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         version (InBranding) {
88             if (incGetUseNativeTitlebar()) {
89                 igImage(
90                     cast(void*)incGetLogo(), 
91                     ImVec2(avail.y*2, avail.y*2), 
92                     ImVec2(0, 0), ImVec2(1, 1), 
93                     ImVec4(1, 1, 1, 1), 
94                     ImVec4(0, 0, 0, 0)
95                 );
96 
97                 igSeparator();
98             }
99         }
100 
101         if (igBeginMenu(__("File"), true)) {
102             if(igMenuItem(__("New"), "Ctrl+N", false, true)) {
103                 fileNew();
104             }
105 
106             if (igMenuItem(__("Open"), "Ctrl+O", false, true)) {
107                 fileOpen();
108             }
109 
110             string[] prevProjects = incGetPrevProjects();
111             if (igBeginMenu(__("Recent"), prevProjects.length > 0)) {
112                 foreach(project; incGetPrevProjects) {
113                     import std.path : baseName;
114                     if (igMenuItem(project.baseName.toStringz, "", false, true)) {
115                         incOpenProject(project);
116                     }
117                     incTooltip(project);
118                 }
119                 igEndMenu();
120             }
121             
122             if(igMenuItem(__("Save"), "Ctrl+S", false, true)) {
123                 fileSave();
124             }
125             
126             if(igMenuItem(__("Save As..."), "Ctrl+Shift+S", false, true)) {
127                 fileSaveAs();
128             }
129 
130             if (igBeginMenu(__("Import"), true)) {
131                 if(igMenuItem_Bool(__("Photoshop Document"), "", false, true)) {
132                     const TFD_Filter[] filters = [
133                         { ["*.psd"], "Photoshop Document (*.psd)" }
134                     ];
135 
136                     c_str filename = tinyfd_openFileDialog(__("Import..."), "", filters, false);
137                     if (filename !is null) {
138                         string file = cast(string)filename.fromStringz;
139                         incImportPSD(file);
140                     }
141                 }
142                 incTooltip(_("Import a standard Photoshop PSD file."));
143 
144                 if (igMenuItem_Bool(__("Inochi2D Puppet"), "", false, true)) {
145                     const TFD_Filter[] filters = [
146                         { ["*.inp"], "Inochi2D Puppet (*.inp)" }
147                     ];
148 
149                     c_str filename = tinyfd_openFileDialog(__("Import..."), "", filters, false);
150                     if (filename !is null) {
151                         string file = cast(string)filename.fromStringz;
152                         incImportINP(file);
153                     }
154                 }
155                 incTooltip(_("Import existing puppet file, editing options limited"));
156 
157                 if (igMenuItem_Bool(__("Image Folder"))) {
158                     c_str folder = tinyfd_selectFolderDialog(__("Select a Folder..."), null);
159                     if (folder !is null) {
160                         incImportFolder(cast(string)folder.fromStringz);
161                     }
162                 }
163                 incTooltip(_("Supports PNGs, TGAs and JPEGs."));
164                 igEndMenu();
165             }
166 
167             if (igBeginMenu(__("Export"), true)) {
168                 if(igMenuItem_Bool(__("Inochi Puppet"), "", false, true)) {
169                     const TFD_Filter[] filters = [
170                         { ["*.inp"], "Inochi2D Puppet (*.inp)" }
171                     ];
172 
173                     import std.path : setExtension;
174 
175                     c_str filename = tinyfd_saveFileDialog(__("Export..."), "", filters);
176                     if (filename !is null) {
177                         string file = cast(string)filename.fromStringz;
178 
179                         incExportINP(file);
180                     }
181                 }
182                 igEndMenu();
183             }
184 
185             if(igMenuItem_Bool(__("Quit"), "Alt+F4", false, true)) incExit();
186             igEndMenu();
187         }
188         
189         if (igBeginMenu(__("Edit"), true)) {
190             if(igMenuItem_Bool(__("Undo"), "Ctrl+Z", false, incActionCanUndo())) incActionUndo();
191             if(igMenuItem_Bool(__("Redo"), "Ctrl+Shift+Z", false, incActionCanRedo())) incActionRedo();
192             
193             igSeparator();
194             if(igMenuItem_Bool(__("Cut"), "Ctrl+X", false, false)) {}
195             if(igMenuItem_Bool(__("Copy"), "Ctrl+C", false, false)) {}
196             if(igMenuItem_Bool(__("Paste"), "Ctrl+V", false, false)) {}
197 
198             igSeparator();
199             if(igMenuItem_Bool(__("Settings"), "", false, true)) {
200                 if (!incIsSettingsOpen) incPushWindow(new SettingsWindow);
201             }
202             
203             debug {
204                 igSpacing();
205                 igSpacing();
206 
207                 igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("ImGui Debugging"));
208 
209                 igSeparator();
210                 if(igMenuItem_Bool(__("Style Editor"), "", false, true)) dbgShowStyleEditor = !dbgShowStyleEditor;
211                 if(igMenuItem_Bool(__("ImGui Debugger"), "", false, true)) dbgShowDebugger = !dbgShowDebugger;
212             }
213             igEndMenu();
214         }
215 
216         if (igBeginMenu(__("View"), true)) {
217             if (igMenuItem(__("Reset Layout"), null, false, true)) {
218                 incSetDefaultLayout();
219             }
220             igSeparator();
221 
222             // Spacing
223             igSpacing();
224             igSpacing();
225 
226             igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Panels"));
227             igSeparator();
228 
229             foreach(panel; incPanels) {
230 
231                 // Skip panels that'll always be visible
232                 if (panel.alwaysVisible) continue;
233 
234                 // Show menu item for panel
235                 if(igMenuItem_Bool(panel.displayNameC, null, panel.visible, true)) {
236                     panel.visible = !panel.visible;
237                     incSettingsSet(panel.name~".visible", panel.visible);
238                 }
239             }
240 
241             // Spacing
242             igSpacing();
243             igSpacing();
244             
245             igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Extras"));
246 
247             igSeparator();
248             
249             if (igMenuItem(__("Save Screenshot"), "", false, true)) {
250                 const TFD_Filter[] filters = [
251                     { ["*.png"], "PNG Image (*.png)" }
252                 ];
253 
254                 import std.path : setExtension;
255                 c_str filename = tinyfd_saveFileDialog(__("Save Screenshot..."), "", filters);
256                 if (filename !is null) {
257                     string file = (cast(string)filename.fromStringz).setExtension("png");
258 
259                     // Dump viewport to RGBA byte array
260                     int width, height;
261                     inGetViewport(width, height);
262                     Texture outTexture = new Texture(null, width, height);
263 
264                     // Texture data
265                     inSetClearColor(0, 0, 0, 0);
266                     inBeginScene();
267                         incActivePuppet().update();
268                         incActivePuppet().draw();
269                     inEndScene();
270                     ubyte[] textureData = new ubyte[inViewportDataLength()];
271                     inDumpViewport(textureData);
272                     inTexUnPremuliply(textureData);
273                     
274                     // Write to texture
275                     outTexture.setData(textureData);
276 
277                     outTexture.save(file);
278                 }
279             }
280             incTooltip(_("Saves screenshot as PNG of the editor framebuffer."));
281 
282             if (igMenuItem_Bool(__("Show Stats for Nerds"), "", incShowStatsForNerds, true)) {
283                 incShowStatsForNerds = !incShowStatsForNerds;
284                 incSettingsSet("NerdStats", incShowStatsForNerds);
285             }
286 
287             igEndMenu();
288         }
289 
290         if (igBeginMenu(__("Tools"), true)) {
291             import creator.utils.repair : incAttemptRepairPuppet, incRegenerateNodeIDs;
292 
293             igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Puppet Texturing"));
294             igSeparator();
295 
296             // Premultiply textures, causing every pixel value in every texture to
297             // be multiplied by their Alpha (transparency) component
298             if (igMenuItem(__("Premultiply textures"), "", false)) {
299                 import creator.utils.repair : incPremultTextures;
300                 incPremultTextures(incActivePuppet());
301             }
302             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."));
303             
304             if (igMenuItem(__("Bleed textures..."), "", false)) {
305                 incRebleedTextures();
306             }
307             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."));
308 
309             if (igMenuItem(__("Generate Mipmaps..."), "", false)) {
310                 incRegenerateMipmaps();
311             }
312             incTooltip(_("Regenerates the puppet's mipmaps."));
313 
314             // Spacing
315             igSpacing();
316             igSpacing();
317 
318             igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Puppet Recovery"));
319             igSeparator();
320 
321             // FULL REPAIR
322             if (igMenuItem(__("Attempt full repair..."), "", false)) {
323                 incAttemptRepairPuppet(incActivePuppet());
324             }
325             incTooltip(_("Attempts all the recovery and repair methods below on the currently loaded model"));
326 
327             // REGEN NODE IDs
328             if (igMenuItem(__("Regenerate Node IDs"), "", false)) {
329                 import creator.utils.repair : incAttemptRepairPuppet;
330                 incRegenerateNodeIDs(incActivePuppet().root);
331             }
332             incTooltip(_("Regenerates all the unique IDs for the model"));
333 
334             // Spacing
335             igSpacing();
336             igSpacing();
337             igSeparator();
338             if (igMenuItem(__("Verify INP File..."), "", false)) {
339                 incAttemptRepairPuppet(incActivePuppet());
340             }
341             incTooltip(_("Attempts to verify and repair INP files"));
342 
343             igEndMenu();
344         }
345 
346         if (igBeginMenu(__("Help"), true)) {
347 
348             if(igMenuItem_Bool(__("Tutorial"), "(TODO)", false, false)) { }
349             igSeparator();
350             
351             if(igMenuItem_Bool(__("Online Documentation"), "", false, true)) {
352                 incOpenLink("https://github.com/Inochi2D/inochi-creator/wiki");
353             }
354             
355             if(igMenuItem_Bool(__("Inochi2D Documentation"), "", false, true)) {
356                 incOpenLink("https://github.com/Inochi2D/inochi2d/wiki");
357             }
358             igSeparator();
359 
360             if(igMenuItem_Bool(__("About"), "", false, true)) {
361                 incPushWindow(new AboutWindow);
362             }
363             igEndMenu();
364         }
365 
366         // We need to pre-calculate the size of the right adjusted section
367         // This code is very ugly because imgui doesn't really exactly understand this
368         // stuff natively.
369         ImVec2 secondSectionLength = ImVec2(0, 0);
370         secondSectionLength.x += incMeasureString(_("Donate")).x+16; // Add 16 px padding
371         if (incShowStatsForNerds) { // Extra padding I guess
372             secondSectionLength.x += igGetStyle().ItemSpacing.x;
373             secondSectionLength.x += incMeasureString("1000ms").x;
374         }
375         incDummy(ImVec2(-secondSectionLength.x, 0));
376 
377         if (incShowStatsForNerds) {
378             string fpsText = "%.0fms\0".format(1000f/io.Framerate);
379             float textAreaDummyWidth = incMeasureString("1000ms").x-incMeasureString(fpsText).x;
380             incDummy(ImVec2(textAreaDummyWidth, 0));
381             igText(fpsText.ptr);
382         }
383         
384         // Donate button
385         // NOTE: Is this too obstructive in the UI?
386         if(igMenuItem(__("Donate"))) {
387             incOpenLink("https://www.patreon.com/clipsey");
388         }
389         incTooltip(_("Support development via Patreon"));
390 
391         igEndMainMenuBar();
392 
393         if (dbgShowStyleEditor) igShowStyleEditor(igGetStyle());
394         if (dbgShowDebugger) igShowAboutWindow(&dbgShowDebugger);
395     }
396 }