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 }