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 }