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 }