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.config;
15 import creator;
16 import inochi2d;
17 import inochi2d.core.dbg;
18 import tinyfiledialogs;
19 import i18n;
20 
21 import std.string;
22 import std.stdio;
23 import std.path : setExtension;
24 
25 private {
26     bool dbgShowStyleEditor;
27     bool dbgShowDebugger;
28     bool dbgShowMetrics;
29     bool dbgShowStackTool;
30 
31     void fileNew() {
32         incPopWelcomeWindow();
33         incNewProject();
34     }
35 
36     void fileOpen() {
37         const TFD_Filter[] filters = [
38             { ["*.inx"], "Inochi Creator Project (*.inx)" }
39         ];
40 
41         incPopWelcomeWindow();
42         string file = incShowOpenDialog(filters, _("Open..."));
43         if (file) incOpenProject(file);
44     }
45 
46     void fileSave() {
47         incPopWelcomeWindow();
48 
49         // If a projeect path is set then the user has opened or saved
50         // an existing file, we should just override that
51         if (incProjectPath.length > 0) {
52             // TODO: do backups on every save?
53 
54             incSaveProject(incProjectPath);
55         } else {
56             const TFD_Filter[] filters = [
57                 { ["*.inx"], "Inochi Creator Project (*.inx)" }
58             ];
59 
60             string file = incShowSaveDialog(filters, "", _("Save..."));
61             if (file) incSaveProject(file);
62         }
63     }
64 
65     void fileSaveAs() {
66         incPopWelcomeWindow();
67         const TFD_Filter[] filters = [
68             { ["*.inx"], "Inochi Creator Project (*.inx)" }
69         ];
70 
71         string fname = incProjectPath().length > 0 ? incProjectPath : "";
72         string file = incShowSaveDialog(filters, fname, _("Save As..."));
73         if (file) incSaveProject(file);
74     }
75 }
76 
77 void incMainMenu() {
78     auto io = igGetIO();
79     
80     // Save these for rendering popups
81     auto border = igGetStyle().Colors[ImGuiCol.Border];
82     auto borderShadow = igGetStyle().Colors[ImGuiCol.BorderShadow];
83     auto seperator = igGetStyle().Colors[ImGuiCol.Separator];
84 
85     // Otherwise, hide borders.
86     igPushStyleColor(ImGuiCol.Border, ImVec4(0, 0, 0, 0));
87     igPushStyleColor(ImGuiCol.BorderShadow, ImVec4(0, 0, 0, 0));
88     igPushStyleColor(ImGuiCol.Separator, ImVec4(0, 0, 0, 0));
89 
90         if (incShortcut("Ctrl+N")) fileNew();
91         if (incShortcut("Ctrl+O")) fileOpen();
92         if (incShortcut("Ctrl+S")) fileSave();
93         if (incShortcut("Ctrl+Shift+S")) fileSaveAs();
94 
95         if (!incSettingsGet("hasDoneQuickSetup", false)) igBeginDisabled();
96 
97         if(igBeginMainMenuBar()) {
98                 
99             ImVec2 pos;
100             igGetCursorPos(&pos);
101             igSetCursorPos(ImVec2(pos.x-(igGetStyle().WindowPadding.x/2), pos.y));
102 
103             ImVec2 avail;
104             igGetContentRegionAvail(&avail);
105             version (InBranding) {
106                 igImage(
107                     cast(void*)incGetLogoI2D().getTextureId(), 
108                     ImVec2(avail.y*2, avail.y*2), 
109                     ImVec2(0, 0), ImVec2(1, 1), 
110                     ImVec4(1, 1, 1, 1), 
111                     ImVec4(0, 0, 0, 0)
112                 );
113                 
114                 import creator.core.egg : incAdaTickOne;
115                 if (igIsItemClicked(ImGuiMouseButton.Left)) {
116                     incAdaTickOne();
117                 }
118                 igSeparator();
119             }
120 
121 
122             // We do want borders on our popup menus.
123             igPushStyleColor(ImGuiCol.Border, border);
124             igPushStyleColor(ImGuiCol.BorderShadow, borderShadow);
125             igPushStyleColor(ImGuiCol.Separator, seperator);
126                 if (igBeginMenu(__("File"), true)) {
127                     if(igMenuItem(__("New"), "Ctrl+N", false, true)) {
128                         fileNew();
129                     }
130 
131                     if (igMenuItem(__("Open"), "Ctrl+O", false, true)) {
132                         fileOpen();
133                     }
134 
135                     string[] prevProjects = incGetPrevProjects();
136                     if (igBeginMenu(__("Recent"), prevProjects.length > 0)) {
137                         foreach(project; incGetPrevProjects) {
138                             import std.path : baseName;
139                             if (igMenuItem(project.baseName.toStringz, "", false, true)) {
140                                 incPopWelcomeWindow();
141                                 incOpenProject(project);
142                             }
143                             incTooltip(project);
144                         }
145                         igEndMenu();
146                     }
147                     
148                     if(igMenuItem(__("Save"), "Ctrl+S", false, true)) {
149                         fileSave();
150                     }
151                     
152                     if(igMenuItem(__("Save As..."), "Ctrl+Shift+S", false, true)) {
153                         fileSaveAs();
154                     }
155 
156                     if (igBeginMenu(__("Import"), true)) {
157                         if(igMenuItem(__("Photoshop Document"), "", false, true)) {
158                             incPopWelcomeWindow();
159                             incImportShowPSDDialog();
160                         }
161                         incTooltip(_("Import a standard Photoshop PSD file."));
162 
163                         if (igMenuItem(__("Inochi2D Puppet"), "", false, true)) {
164                             const TFD_Filter[] filters = [
165                                 { ["*.inp"], "Inochi2D Puppet (*.inp)" }
166                             ];
167 
168                             string file = incShowOpenDialog(filters, _("Import..."));
169                             if (file) {
170                                 incPopWelcomeWindow();
171                                 incImportINP(file);
172                             }
173                         }
174                         incTooltip(_("Import existing puppet file, editing options limited"));
175 
176                         if (igMenuItem(__("Image Folder"))) {
177                             string folder = incShowOpenFolderDialog(_("Select a Folder..."));
178                             if (folder) {
179                                 incPopWelcomeWindow();
180                                 incImportFolder(folder);
181                             }
182                         }
183                         incTooltip(_("Supports PNGs, TGAs and JPEGs."));
184                         igEndMenu();
185                     }
186                     if (igBeginMenu(__("Merge"), true)) {
187                         if(igMenuItem(__("Photoshop Document"), "", false, true)) {
188                             const TFD_Filter[] filters = [
189                                 { ["*.psd"], "Photoshop Document (*.psd)" }
190                             ];
191 
192                             string file = incShowOpenDialog(filters, _("Import..."));
193                             if (file) {
194                                 incPopWelcomeWindow();
195                                 incPushWindow(new PSDMergeWindow(file));
196                             }
197                         }
198                         incTooltip(_("Merge layers from Photoshop document"));
199 
200                         if (igMenuItem(__("Inochi Creator Project"), "", false, true)) {
201                             incPopWelcomeWindow();
202                             // const TFD_Filter[] filters = [
203                             //     { ["*.inp"], "Inochi2D Puppet (*.inp)" }
204                             // ];
205 
206                             // c_str filename = tinyfd_openFileDialog(__("Import..."), "", filters, false);
207                             // if (filename !is null) {
208                             //     string file = cast(string)filename.fromStringz;
209                             // }
210                         }
211                         incTooltip(_("Merge another Inochi Creator project in to this one"));
212                         
213                         igEndMenu();
214                     }
215 
216                     if (igBeginMenu(__("Export"), true)) {
217                         if(igMenuItem(__("Inochi2D Puppet"), "", false, true)) {
218                             const TFD_Filter[] filters = [
219                                 { ["*.inp"], "Inochi2D Puppet (*.inp)" }
220                             ];
221 
222                             string file = incShowSaveDialog(filters, "", _("Export..."));
223                             if (file) incExportINP(file);
224                         }
225                         if (igBeginMenu(__("Image"), true)) {
226                             if(igMenuItem(__("PNG (*.png)"), "", false, true)) {
227                                 const TFD_Filter[] filters = [
228                                     { ["*.png"], "Portable Network Graphics (*.png)" }
229                                 ];
230 
231                                 string file = incShowSaveDialog(filters, "", _("Export..."));
232                                 if (file) incPushWindow(new ImageExportWindow(file.setExtension("png")));
233                             }
234 
235                             if(igMenuItem(__("JPEG (*.jpeg)"), "", false, true)) {
236                                 const TFD_Filter[] filters = [
237                                     { ["*.jpeg"], "JPEG Image (*.jpeg)" }
238                                 ];
239 
240                                 string file = incShowSaveDialog(filters, "", _("Export..."));
241                                 if (file) incPushWindow(new ImageExportWindow(file.setExtension("jpeg")));
242                             }
243 
244                             if(igMenuItem(__("TARGA (*.tga)"), "", false, true)) {
245                                 const TFD_Filter[] filters = [
246                                     { ["*.tga"], "TARGA Graphics (*.tga)" }
247                                 ];
248 
249                                 string file = incShowSaveDialog(filters, "", _("Export..."));
250                                 if (file) incPushWindow(new ImageExportWindow(file.setExtension("tga")));
251                             }
252 
253                             igEndMenu();
254                         }
255                         if(igMenuItem(__("Video"), "", false, incVideoCanExport())) {
256                             const TFD_Filter[] filters = [
257                                 { ["*.mp4"], "H.264 Video (*.mp4)" },
258                                 { ["*.avi"], "AVI Video (*.avi)" },
259                                 { ["*.png"], "PNG Sequence (*.png)" }
260                             ];
261 
262                             // string file = incShowSaveDialog(filters, "", _("Export..."));
263                             // if (file) incPushWindow(new ImageExportWindow(file.setExtension("tga")));
264                         }
265                         igEndMenu();
266                     }
267 
268                     // Close Project option
269                     if (igMenuItem(__("Close Project"))) {
270                         
271                         // Just in case...
272                         incPopWelcomeWindow();
273 
274                         // TODO: Check if changes were done to project and warn before
275                         // creating new project
276                         incNewProject();
277                         incPushWindow(new WelcomeWindow());
278                     }
279 
280                     // Quit option
281                     if (igMenuItem(__("Quit"), "Alt+F4", false, true)) incExit();
282                     igEndMenu();
283                 }
284                 
285                 if (igBeginMenu(__("Edit"), true)) {
286                     if(igMenuItem(__("Undo"), "Ctrl+Z", false, incActionCanUndo())) incActionUndo();
287                     if(igMenuItem(__("Redo"), "Ctrl+Shift+Z", false, incActionCanRedo())) incActionRedo();
288                     
289                     igSeparator();
290                     if(igMenuItem(__("Cut"), "Ctrl+X", false, false)) {}
291                     if(igMenuItem(__("Copy"), "Ctrl+C", false, false)) {}
292                     if(igMenuItem(__("Paste"), "Ctrl+V", false, false)) {}
293 
294                     igSeparator();
295                     if(igMenuItem(__("Settings"), "", false, true)) {
296                         if (!incIsSettingsOpen) incPushWindow(new SettingsWindow);
297                     }
298                     
299                     debug {
300                         igSpacing();
301                         igSpacing();
302 
303                         igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("ImGui Debugging"));
304 
305                         igSeparator();
306                         if(igMenuItem(__("Style Editor"), "", dbgShowStyleEditor, true)) dbgShowStyleEditor = !dbgShowStyleEditor;
307                         if(igMenuItem(__("ImGui Debugger"), "", dbgShowDebugger, true)) dbgShowDebugger = !dbgShowDebugger;
308                         if(igMenuItem(__("ImGui Metrics"), "", dbgShowMetrics, true)) dbgShowMetrics = !dbgShowMetrics;
309                         if(igMenuItem(__("ImGui Stack Tool"), "", dbgShowStackTool, true)) dbgShowStackTool = !dbgShowStackTool;
310                     }
311                     igEndMenu();
312                 }
313 
314                 if (igBeginMenu(__("View"), true)) {
315                     if (igMenuItem(__("Reset Layout"), null, false, true)) {
316                         incSetDefaultLayout();
317                     }
318                     igSeparator();
319 
320                     // Spacing
321                     igSpacing();
322                     igSpacing();
323 
324                     igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Panels"));
325                     igSeparator();
326 
327                     foreach(panel; incPanels) {
328 
329                         // Skip panels that'll always be visible
330                         if (panel.alwaysVisible) continue;
331 
332                         // Show menu item for panel
333                         if(igMenuItem(panel.displayNameC, null, panel.visible, true)) {
334                             panel.visible = !panel.visible;
335                             incSettingsSet(panel.name~".visible", panel.visible);
336                         }
337                     }
338 
339                     // Spacing
340                     igSpacing();
341                     igSpacing();
342 
343                     igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Configuration"));
344 
345                     // Opens the directory where configuration resides in the user's file browser.
346                     if (igMenuItem(__("Open Configuration Folder"), null, false, true)) {
347                         incOpenLink(incGetAppConfigPath());
348                     }
349 
350                     // Spacing
351                     igSpacing();
352                     igSpacing();
353                     
354                     
355                     igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Extras"));
356 
357                     igSeparator();
358                     if (igMenuItem(__("Save Screenshot"), "", false, true)) {
359                         const TFD_Filter[] filters = [
360                             { ["*.png"], "PNG Image (*.png)" }
361                         ];
362                         
363                         string filename = incShowSaveDialog(filters, "", _("Save Screenshot..."));
364                         if (filename) {
365                             string file = filename.setExtension("png");
366 
367                             // Dump viewport to RGBA byte array
368                             int width, height;
369                             inGetViewport(width, height);
370                             Texture outTexture = new Texture(null, width, height);
371 
372                             // Texture data
373                             inSetClearColor(0, 0, 0, 0);
374                             inBeginScene();
375                                 incActivePuppet().update();
376                                 incActivePuppet().draw();
377                             inEndScene();
378                             ubyte[] textureData = new ubyte[inViewportDataLength()];
379                             inDumpViewport(textureData);
380                             inTexUnPremuliply(textureData);
381                             
382                             // Write to texture
383                             outTexture.setData(textureData);
384 
385                             outTexture.save(file);
386                         }
387                     }
388                     incTooltip(_("Saves screenshot as PNG of the editor framebuffer."));
389 
390                     if (igMenuItem(__("Show Stats for Nerds"), "", incShowStatsForNerds, true)) {
391                         incShowStatsForNerds = !incShowStatsForNerds;
392                         incSettingsSet("NerdStats", incShowStatsForNerds);
393                     }
394 
395 
396                     igEndMenu();
397                 }
398 
399                 if (igBeginMenu(__("Tools"), true)) {
400                     import creator.utils.repair : incAttemptRepairPuppet, incRegenerateNodeIDs;
401 
402                     igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Puppet Texturing"));
403                     igSeparator();
404 
405                     // Premultiply textures, causing every pixel value in every texture to
406                     // be multiplied by their Alpha (transparency) component
407                     if (igMenuItem(__("Premultiply textures"), "", false)) {
408                         import creator.utils.repair : incPremultTextures;
409                         incPremultTextures(incActivePuppet());
410                     }
411                     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."));
412                     
413                     if (igMenuItem(__("Bleed textures..."), "", false)) {
414                         incRebleedTextures();
415                     }
416                     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."));
417 
418                     if (igMenuItem(__("Generate Mipmaps..."), "", false)) {
419                         incRegenerateMipmaps();
420                     }
421                     incTooltip(_("Regenerates the puppet's mipmaps."));
422 
423                     if (igMenuItem(__("Generate fake layer name info..."), "", false)) {
424                         import creator.ext;
425                         auto parts = incActivePuppet().getAllParts();
426                         foreach(ref part; parts) {
427                             auto expart = cast(ExPart)part;
428                             if (expart) {
429                                 expart.layerPath = "/"~part.name;
430                             }
431                         }
432                     }
433                     incTooltip(_("Generates fake layer info based on node names"));
434 
435                     // Spacing
436                     igSpacing();
437                     igSpacing();
438 
439                     igTextColored(ImVec4(0.7, 0.5, 0.5, 1), __("Puppet Recovery"));
440                     igSeparator();
441 
442                     // FULL REPAIR
443                     if (igMenuItem(__("Attempt full repair..."), "", false)) {
444                         incAttemptRepairPuppet(incActivePuppet());
445                     }
446                     incTooltip(_("Attempts all the recovery and repair methods below on the currently loaded model"));
447 
448                     // REGEN NODE IDs
449                     if (igMenuItem(__("Regenerate Node IDs"), "", false)) {
450                         import creator.utils.repair : incAttemptRepairPuppet;
451                         incRegenerateNodeIDs(incActivePuppet().root);
452                     }
453                     incTooltip(_("Regenerates all the unique IDs for the model"));
454 
455                     // Spacing
456                     igSpacing();
457                     igSpacing();
458                     igSeparator();
459                     if (igMenuItem(__("Verify INP File..."), "", false)) {
460                         incAttemptRepairPuppet(incActivePuppet());
461                     }
462                     incTooltip(_("Attempts to verify and repair INP files"));
463 
464                     igEndMenu();
465                 }
466 
467                 if (igBeginMenu(__("Help"), true)) {
468 
469                     if(igMenuItem(__("Online Documentation"), "", false, true)) {
470                         incOpenLink("https://github.com/Inochi2D/inochi-creator/wiki");
471                     }
472                     
473                     if(igMenuItem(__("Inochi2D Documentation"), "", false, true)) {
474                         incOpenLink("https://github.com/Inochi2D/inochi2d/wiki");
475                     }
476                     igSpacing();
477                     igSeparator();
478                     igSpacing();
479                     
480 
481                     if (igMenuItem(__("Report a Bug"))) {
482                         incOpenLink(INC_BUG_REPORT_URI);
483                     }
484                     if (igMenuItem(__("Request a Feature"))) {
485                         incOpenLink(INC_FEATURE_REQ_URI);
486                     }
487                     igSpacing();
488                     igSeparator();
489                     igSpacing();
490 
491 
492                     if(igMenuItem(__("About"), "", false, true)) {
493                         incPushWindow(new AboutWindow);
494                     }
495                     igEndMenu();
496                 }
497                 
498             igPopStyleColor();
499             igPopStyleColor();
500             igPopStyleColor();
501 
502             // We need to pre-calculate the size of the right adjusted section
503             // This code is very ugly because imgui doesn't really exactly understand this
504             // stuff natively.
505             ImVec2 secondSectionLength = ImVec2(0, 0);
506             secondSectionLength.x += incMeasureString(_("Donate")).x+16; // Add 16 px padding
507             if (incShowStatsForNerds) { // Extra padding I guess
508                 secondSectionLength.x += igGetStyle().ItemSpacing.x;
509                 secondSectionLength.x += incMeasureString("1000ms").x;
510             }
511             incDummy(ImVec2(-secondSectionLength.x, 0));
512 
513             if (incShowStatsForNerds) {
514                 string fpsText = "%.0fms".format(1000f/io.Framerate);
515                 float textAreaDummyWidth = incMeasureString("1000ms").x-incMeasureString(fpsText).x;
516                 incDummy(ImVec2(textAreaDummyWidth, 0));
517                 incText(fpsText);
518             }
519             
520             // Donate button
521             // NOTE: Is this too obstructive in the UI?
522             if(igMenuItem(__("Donate"))) {
523                 incOpenLink("https://www.patreon.com/clipsey");
524             }
525             incTooltip(_("Support development via Patreon"));
526         }
527         igEndMainMenuBar();
528 
529         // For quick-setup stuff
530         if (!incSettingsGet("hasDoneQuickSetup", false)) igEndDisabled();
531 
532     igPopStyleColor();
533     igPopStyleColor();
534     igPopStyleColor();
535 
536     // ImGui Debug Stuff
537     if (dbgShowStyleEditor) igShowStyleEditor(igGetStyle());
538     if (dbgShowDebugger) igShowAboutWindow(&dbgShowDebugger);
539     if (dbgShowStackTool) igShowStackToolWindow();
540     if (dbgShowMetrics) igShowMetricsWindow();
541 }