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.panels.parameters; 8 import creator.viewport.model.deform; 9 import creator.panels; 10 import creator.widgets; 11 import creator.windows; 12 import creator.core; 13 import creator; 14 import std.string; 15 import inochi2d; 16 import i18n; 17 import std.uni : toLower; 18 import std.stdio; 19 import std.algorithm; 20 import creator.utils; 21 22 private { 23 ParameterBinding[][Node] cParamBindingEntries; 24 ParameterBinding[][Node] cParamBindingEntriesAll; 25 Node[] cAllBoundNodes; 26 ParameterBinding[BindTarget] cSelectedBindings; 27 Node[] cCompatibleNodes; 28 vec2u cParamPoint; 29 vec2u cClipboardPoint; 30 ParameterBinding[BindTarget] cClipboardBindings; 31 32 void refreshBindingList(Parameter param) { 33 // Filter selection to remove anything that went away 34 ParameterBinding[BindTarget] newSelectedBindings; 35 36 cParamBindingEntriesAll.clear(); 37 foreach(ParameterBinding binding; param.bindings) { 38 BindTarget target = binding.getTarget(); 39 if (target in cSelectedBindings) newSelectedBindings[target] = binding; 40 cParamBindingEntriesAll[binding.getNode()] ~= binding; 41 } 42 cAllBoundNodes = cParamBindingEntriesAll.keys.dup; 43 cAllBoundNodes.sort!((x, y) => x.name < y.name); 44 cSelectedBindings = newSelectedBindings; 45 paramPointChanged(param); 46 } 47 48 void paramPointChanged(Parameter param) { 49 cParamBindingEntries.clear(); 50 51 cParamPoint = param.findClosestKeypoint(); 52 foreach(ParameterBinding binding; param.bindings) { 53 if (binding.isSet(cParamPoint)) { 54 cParamBindingEntries[binding.getNode()] ~= binding; 55 } 56 } 57 } 58 59 void mirrorAll(Parameter param, uint axis) { 60 foreach(ParameterBinding binding; param.bindings) { 61 uint xCount = param.axisPointCount(0); 62 uint yCount = param.axisPointCount(1); 63 foreach(x; 0..xCount) { 64 foreach(y; 0..yCount) { 65 vec2u index = vec2u(x, y); 66 if (binding.isSet(index)) { 67 binding.scaleValueAt(index, axis, -1); 68 } 69 } 70 } 71 } 72 } 73 74 void mirroredAutofill(Parameter param, uint axis, float min, float max) { 75 foreach(ParameterBinding binding; param.bindings) { 76 uint xCount = param.axisPointCount(0); 77 uint yCount = param.axisPointCount(1); 78 foreach(x; 0..xCount) { 79 float offX = param.axisPoints[0][x]; 80 if (axis == 0 && (offX < min || offX > max)) continue; 81 foreach(y; 0..yCount) { 82 float offY = param.axisPoints[1][x]; 83 if (axis == 1 && (offY < min || offY > max)) continue; 84 85 vec2u index = vec2u(x, y); 86 if (!binding.isSet(index)) binding.extrapolateValueAt(index, axis); 87 } 88 } 89 } 90 } 91 92 void fixScales(Parameter param) { 93 foreach(ParameterBinding binding; param.bindings) { 94 switch(binding.getName()) { 95 case "transform.s.x": 96 case "transform.s.y": 97 if (ValueParameterBinding b = cast(ValueParameterBinding)binding) { 98 uint xCount = param.axisPointCount(0); 99 uint yCount = param.axisPointCount(1); 100 foreach(x; 0..xCount) { 101 foreach(y; 0..yCount) { 102 vec2u index = vec2u(x, y); 103 if (b.isSet(index)) { 104 b.values[x][y] += 1; 105 } 106 } 107 } 108 b.reInterpolate(); 109 } 110 break; 111 default: break; 112 } 113 } 114 } 115 116 Node[] getCompatibleNodes() { 117 Node thisNode = null; 118 119 foreach(binding; cSelectedBindings.byValue()) { 120 if (thisNode is null) thisNode = binding.getNode(); 121 else if (!(binding.getNode() is thisNode)) return null; 122 } 123 if (thisNode is null) return null; 124 125 Node[] compatible; 126 nodeLoop: foreach(otherNode; cParamBindingEntriesAll.byKey()) { 127 if (otherNode is thisNode) continue; 128 129 foreach(binding; cSelectedBindings.byValue()) { 130 if (!binding.isCompatibleWithNode(otherNode)) 131 continue nodeLoop; 132 } 133 compatible ~= otherNode; 134 } 135 136 return compatible; 137 } 138 139 void copySelectionToNode(Parameter param, Node target) { 140 Node src = cSelectedBindings.keys[0].node; 141 142 foreach(binding; cSelectedBindings.byValue()) { 143 assert(binding.getNode() is src, "selection mismatch"); 144 145 ParameterBinding b = param.getOrAddBinding(target, binding.getName()); 146 binding.copyKeypointToBinding(cParamPoint, b, cParamPoint); 147 } 148 149 refreshBindingList(param); 150 } 151 152 void swapSelectionWithNode(Parameter param, Node target) { 153 Node src = cSelectedBindings.keys[0].node; 154 155 foreach(binding; cSelectedBindings.byValue()) { 156 assert(binding.getNode() is src, "selection mismatch"); 157 158 ParameterBinding b = param.getOrAddBinding(target, binding.getName()); 159 binding.swapKeypointWithBinding(cParamPoint, b, cParamPoint); 160 } 161 162 refreshBindingList(param); 163 } 164 165 void keypointActions(Parameter param, ParameterBinding[] bindings) { 166 if (igMenuItem(__("Unset"), "", false, true)) { 167 foreach(binding; bindings) { 168 binding.unset(cParamPoint); 169 } 170 incViewportNodeDeformNotifyParamValueChanged(); 171 } 172 if (igMenuItem(__("Set to current"), "", false, true)) { 173 foreach(binding; bindings) { 174 binding.setCurrent(cParamPoint); 175 } 176 incViewportNodeDeformNotifyParamValueChanged(); 177 } 178 if (igMenuItem(__("Reset"), "", false, true)) { 179 foreach(binding; bindings) { 180 binding.reset(cParamPoint); 181 } 182 incViewportNodeDeformNotifyParamValueChanged(); 183 } 184 if (igMenuItem(__("Invert"), "", false, true)) { 185 foreach(binding; bindings) { 186 binding.scaleValueAt(cParamPoint, -1, -1); 187 } 188 incViewportNodeDeformNotifyParamValueChanged(); 189 } 190 if (igBeginMenu(__("Mirror"), true)) { 191 if (igMenuItem(__("Horizontally"), "", false, true)) { 192 foreach(binding; bindings) { 193 binding.scaleValueAt(cParamPoint, 0, -1); 194 } 195 incViewportNodeDeformNotifyParamValueChanged(); 196 } 197 if (igMenuItem(__("Vertically"), "", false, true)) { 198 foreach(binding; bindings) { 199 binding.scaleValueAt(cParamPoint, 1, -1); 200 } 201 incViewportNodeDeformNotifyParamValueChanged(); 202 } 203 igEndMenu(); 204 } 205 if (param.isVec2) { 206 if (igBeginMenu(__("Set from mirror"), true)) { 207 if (igMenuItem(__("Horizontally"), "", false, true)) { 208 foreach(binding; bindings) { 209 binding.extrapolateValueAt(cParamPoint, 0); 210 } 211 incViewportNodeDeformNotifyParamValueChanged(); 212 } 213 if (igMenuItem(__("Vertically"), "", false, true)) { 214 foreach(binding; bindings) { 215 binding.extrapolateValueAt(cParamPoint, 1); 216 } 217 incViewportNodeDeformNotifyParamValueChanged(); 218 } 219 if (igMenuItem(__("Diagonally"), "", false, true)) { 220 foreach(binding; bindings) { 221 binding.extrapolateValueAt(cParamPoint, -1); 222 } 223 incViewportNodeDeformNotifyParamValueChanged(); 224 } 225 igEndMenu(); 226 } 227 } else { 228 if (igMenuItem(__("Set from mirror"), "", false, true)) { 229 foreach(binding; bindings) { 230 binding.extrapolateValueAt(cParamPoint, 0); 231 } 232 incViewportNodeDeformNotifyParamValueChanged(); 233 } 234 } 235 236 if (igMenuItem(__("Copy"), "", false, true)) { 237 cClipboardPoint = cParamPoint; 238 cClipboardBindings.clear(); 239 foreach(binding; bindings) { 240 cClipboardBindings[binding.getTarget()] = binding; 241 } 242 } 243 244 if (igMenuItem(__("Paste"), "", false, true)) { 245 if (bindings.length == 1 && cClipboardBindings.length == 1 && 246 bindings[0].isCompatibleWithNode(cClipboardBindings.values[0].getNode())) { 247 cClipboardBindings.values[0].copyKeypointToBinding(cClipboardPoint, bindings[0], cParamPoint); 248 } else { 249 foreach(binding; bindings) { 250 if (binding.getTarget() in cClipboardBindings) { 251 ParameterBinding origBinding = cClipboardBindings[binding.getTarget()]; 252 origBinding.copyKeypointToBinding(cClipboardPoint, binding, cParamPoint); 253 } 254 } 255 } 256 } 257 258 } 259 260 void bindingList(Parameter param) { 261 if (!igCollapsingHeader(__("Bindings"), ImGuiTreeNodeFlags.DefaultOpen)) return; 262 263 refreshBindingList(param); 264 265 auto io = igGetIO(); 266 auto style = igGetStyle(); 267 ImS32 inactiveColor = igGetColorU32(style.Colors[ImGuiCol.TextDisabled]); 268 269 igBeginChild("BindingList", ImVec2(0, 256), false); 270 igPushStyleVar(ImGuiStyleVar.CellPadding, ImVec2(4, 1)); 271 igPushStyleVar(ImGuiStyleVar.IndentSpacing, 14); 272 273 foreach(node; cAllBoundNodes) { 274 ParameterBinding[] allBindings = cParamBindingEntriesAll[node]; 275 ParameterBinding[] *bindings = (node in cParamBindingEntries); 276 277 // Figure out if node is selected ( == all bindings selected) 278 bool nodeSelected = true; 279 bool someSelected = false; 280 foreach(binding; allBindings) { 281 if ((binding.getTarget() in cSelectedBindings) is null) 282 nodeSelected = false; 283 else 284 someSelected = true; 285 } 286 287 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.OpenOnArrow; 288 if (nodeSelected) 289 flags |= ImGuiTreeNodeFlags.Selected; 290 291 if (bindings is null) igPushStyleColor(ImGuiCol.Text, inactiveColor); 292 string nodeName = incTypeIdToIconConcat(node.typeId) ~ " " ~ node.name; 293 if (igTreeNodeEx(cast(void*)node.uuid, flags, nodeName.toStringz)) { 294 if (bindings is null) igPopStyleColor(); 295 if (igBeginPopup("###BindingPopup")) { 296 if (igMenuItem(__("Remove"), "", false, true)) { 297 foreach(binding; cSelectedBindings.byValue()) { 298 param.removeBinding(binding); 299 } 300 incViewportNodeDeformNotifyParamValueChanged(); 301 } 302 303 keypointActions(param, cSelectedBindings.values); 304 305 if (igBeginMenu(__("Interpolation Mode"), true)) { 306 if (igMenuItem(__("Nearest"), "", false, true)) { 307 foreach(binding; cSelectedBindings.values) { 308 binding.interpolateMode = InterpolateMode.Nearest; 309 } 310 incViewportNodeDeformNotifyParamValueChanged(); 311 } 312 if (igMenuItem(__("Linear"), "", false, true)) { 313 foreach(binding; cSelectedBindings.values) { 314 binding.interpolateMode = InterpolateMode.Linear; 315 } 316 incViewportNodeDeformNotifyParamValueChanged(); 317 } 318 igEndMenu(); 319 } 320 321 bool haveCompatible = cCompatibleNodes.length > 0; 322 if (igBeginMenu(__("Copy to"), haveCompatible)) { 323 foreach(cNode; cCompatibleNodes) { 324 if (igMenuItem(cNode.name.toStringz, "", false, true)) { 325 copySelectionToNode(param, cNode); 326 } 327 } 328 igEndMenu(); 329 } 330 if (igBeginMenu(__("Swap with"), haveCompatible)) { 331 foreach(cNode; cCompatibleNodes) { 332 if (igMenuItem(cNode.name.toStringz, "", false, true)) { 333 swapSelectionWithNode(param, cNode); 334 } 335 } 336 igEndMenu(); 337 } 338 339 igEndPopup(); 340 } 341 if (igIsItemClicked(ImGuiMouseButton.Right)) { 342 if (!someSelected) { 343 cSelectedBindings.clear(); 344 foreach(binding; allBindings) { 345 cSelectedBindings[binding.getTarget()] = binding; 346 } 347 } 348 cCompatibleNodes = getCompatibleNodes(); 349 igOpenPopup("###BindingPopup"); 350 } 351 352 // Node selection logic 353 if (igIsItemClicked(ImGuiMouseButton.Left) && !igIsItemToggledOpen()) { 354 355 // Select the node you've clicked in the bindings list 356 if (incNodeInSelection(node)) { 357 incFocusCamera(node); 358 } else incSelectNode(node); 359 360 if (!io.KeyCtrl) { 361 cSelectedBindings.clear(); 362 nodeSelected = false; 363 } 364 foreach(binding; allBindings) { 365 if (nodeSelected) cSelectedBindings.remove(binding.getTarget()); 366 else cSelectedBindings[binding.getTarget()] = binding; 367 } 368 } 369 foreach(binding; allBindings) { 370 ImGuiTreeNodeFlags flags2 = 371 ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.OpenOnArrow | 372 ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen; 373 374 bool selected = cast(bool)(binding.getTarget() in cSelectedBindings); 375 if (selected) flags2 |= ImGuiTreeNodeFlags.Selected; 376 377 // Style as inactive if not set at this keypoint 378 if (!binding.isSet(cParamPoint)) 379 igPushStyleColor(ImGuiCol.Text, inactiveColor); 380 381 // Binding entry 382 auto value = cast(ValueParameterBinding)binding; 383 string label; 384 if (value && binding.isSet(cParamPoint)) { 385 label = format("%s (%.02f)", binding.getName(), value.getValue(cParamPoint)); 386 } else { 387 label = binding.getName(); 388 } 389 igTreeNodeEx("binding", flags2, label.toStringz); 390 if (!binding.isSet(cParamPoint)) igPopStyleColor(); 391 392 // Binding selection logic 393 if (igIsItemClicked(ImGuiMouseButton.Right)) { 394 if (!selected) { 395 cSelectedBindings.clear(); 396 cSelectedBindings[binding.getTarget()] = binding; 397 } 398 cCompatibleNodes = getCompatibleNodes(); 399 igOpenPopup("###BindingPopup"); 400 } 401 if (igIsItemClicked(ImGuiMouseButton.Left)) { 402 if (!io.KeyCtrl) { 403 cSelectedBindings.clear(); 404 selected = false; 405 } 406 if (selected) cSelectedBindings.remove(binding.getTarget()); 407 else cSelectedBindings[binding.getTarget()] = binding; 408 } 409 } 410 igTreePop(); 411 } else if (bindings is null) igPopStyleColor(); 412 } 413 igPopStyleVar(); 414 igPopStyleVar(); 415 igEndChild(); 416 } 417 418 } 419 420 /** 421 Generates a parameter view 422 */ 423 void incParameterView(Parameter param, string* grabParam) { 424 bool open = igCollapsingHeader(param.name.toStringz, ImGuiTreeNodeFlags.DefaultOpen); 425 if(igBeginDragDropSource(ImGuiDragDropFlags.SourceAllowNullID)) { 426 igSetDragDropPayload("_PARAMETER", cast(void*)¶m, (¶m).sizeof, ImGuiCond.Always); 427 igText(param.name.toStringz); 428 igEndDragDropSource(); 429 } 430 431 if (!open) return; 432 igIndent(); 433 igPushID(cast(void*)param); 434 435 float reqSpace = param.isVec2 ? 144 : 52; 436 437 // Parameter Control 438 ImVec2 avail = incAvailableSpace(); 439 440 if (igBeginChild("###PARAM", ImVec2(avail.x-24, reqSpace))) { 441 // Popup for rightclicking the controller 442 if (igBeginPopup("###ControlPopup")) { 443 if (incArmedParameter() == param) { 444 keypointActions(param, param.bindings); 445 } 446 igEndPopup(); 447 } 448 449 if (param.isVec2) igText("%.2f %.2f", param.value.x, param.value.y); 450 else igText("%.2f", param.value.x); 451 452 if (incController("###CONTROLLER", param, ImVec2(avail.x-18, reqSpace-24), incArmedParameter() == param, *grabParam)) { 453 if (incArmedParameter() == param) { 454 incViewportNodeDeformNotifyParamValueChanged(); 455 paramPointChanged(param); 456 } 457 if (igIsMouseDown(ImGuiMouseButton.Left)) { 458 if (*grabParam == null) 459 *grabParam = param.name; 460 } else { 461 *grabParam = ""; 462 } 463 } 464 if (igIsItemClicked(ImGuiMouseButton.Right)) { 465 if (incArmedParameter() == param) incViewportNodeDeformNotifyParamValueChanged(); 466 refreshBindingList(param); 467 igOpenPopup("###ControlPopup"); 468 } 469 } 470 igEndChild(); 471 472 473 if (incEditMode == EditMode.ModelEdit) { 474 igSameLine(0, 0); 475 // Parameter Setting Buttons 476 if(igBeginChild("###SETTING", ImVec2(avail.x-24, reqSpace))) { 477 if (igBeginPopup("###EditParam")) { 478 if (igMenuItem(__("Edit Properties"), "", false, true)) { 479 incPushWindowList(new ParamPropWindow(param)); 480 } 481 482 if (igMenuItem(__("Edit Axes Points"), "", false, true)) { 483 incPushWindowList(new ParamAxesWindow(param)); 484 } 485 486 if (param.isVec2) { 487 if (igMenuItem(__("Flip X"), "", false, true)) { 488 param.reverseAxis(0); 489 } 490 if (igMenuItem(__("Flip Y"), "", false, true)) { 491 param.reverseAxis(1); 492 } 493 } else { 494 if (igMenuItem(__("Flip"), "", false, true)) { 495 param.reverseAxis(0); 496 } 497 } 498 if (igBeginMenu(__("Mirror"), true)) { 499 if (igMenuItem(__("Horizontally"), "", false, true)) { 500 mirrorAll(param, 0); 501 incViewportNodeDeformNotifyParamValueChanged(); 502 } 503 if (igMenuItem(__("Vertically"), "", false, true)) { 504 mirrorAll(param, 1); 505 incViewportNodeDeformNotifyParamValueChanged(); 506 } 507 igEndMenu(); 508 } 509 if (igBeginMenu(__("Mirrored Autofill"), true)) { 510 if (igMenuItem(__(""), "", false, true)) { 511 mirroredAutofill(param, 0, 0, 0.4999); 512 incViewportNodeDeformNotifyParamValueChanged(); 513 } 514 if (igMenuItem(__(""), "", false, true)) { 515 mirroredAutofill(param, 0, 0.5001, 1); 516 incViewportNodeDeformNotifyParamValueChanged(); 517 } 518 if (param.isVec2) { 519 if (igMenuItem(__(""), "", false, true)) { 520 mirroredAutofill(param, 1, 0, 0.4999); 521 incViewportNodeDeformNotifyParamValueChanged(); 522 } 523 if (igMenuItem(__(""), "", false, true)) { 524 mirroredAutofill(param, 1, 0.5001, 1); 525 incViewportNodeDeformNotifyParamValueChanged(); 526 } 527 } 528 igEndMenu(); 529 } 530 531 igNewLine(); 532 igSeparator(); 533 534 if (igMenuItem(__("Duplicate"), "", false, true)) { 535 Parameter newParam = param.dup; 536 incActivePuppet().parameters ~= newParam; 537 } 538 539 if (igMenuItem(__("Delete"), "", false, true)) { 540 if (incArmedParameter() == param) { 541 incDisarmParameter(); 542 } 543 incActivePuppet().removeParameter(param); 544 } 545 546 igNewLine(); 547 igSeparator(); 548 549 // Sets the default value of the param 550 if (igMenuItem(__("Set Starting Position"), "", false, true)) { 551 param.defaults = param.value; 552 } 553 igEndPopup(); 554 } 555 556 if (igButton("", ImVec2(24, 24))) { 557 igOpenPopup("###EditParam"); 558 } 559 560 561 if (incButtonColored("", ImVec2(24, 24), incArmedParameter() == param ? ImVec4(1f, 0f, 0f, 1f) : *igGetStyleColorVec4(ImGuiCol.Text))) { 562 if (incArmedParameter() == param) { 563 incDisarmParameter(); 564 } else { 565 param.value = param.getClosestKeypointValue(); 566 paramPointChanged(param); 567 incArmParameter(param); 568 } 569 } 570 571 // Arms the parameter for recording values. 572 incTooltip(_("Arm Parameter")); 573 } 574 igEndChild(); 575 } 576 if (incArmedParameter() == param) { 577 bindingList(param); 578 } 579 igPopID(); 580 igUnindent(); 581 } 582 583 /** 584 The logger frame 585 */ 586 class ParametersPanel : Panel { 587 private: 588 string filter; 589 string grabParam = ""; 590 protected: 591 override 592 void onUpdate() { 593 auto parameters = incActivePuppet().parameters; 594 595 if (igBeginPopup("###AddParameter")) { 596 if (igMenuItem(__("Add 1D Parameter (0..1)"), "", false, true)) { 597 Parameter param = new Parameter( 598 "Param #%d\0".format(parameters.length), 599 false 600 ); 601 incActivePuppet().parameters ~= param; 602 } 603 if (igMenuItem(__("Add 1D Parameter (-1..1)"), "", false, true)) { 604 Parameter param = new Parameter( 605 "Param #%d\0".format(parameters.length), 606 false 607 ); 608 param.min.x = -1; 609 param.max.x = 1; 610 param.insertAxisPoint(0, 0.5); 611 incActivePuppet().parameters ~= param; 612 } 613 if (igMenuItem(__("Add 2D Parameter (0..1)"), "", false, true)) { 614 Parameter param = new Parameter( 615 "Param #%d\0".format(parameters.length), 616 true 617 ); 618 incActivePuppet().parameters ~= param; 619 } 620 if (igMenuItem(__("Add 2D Parameter (-1..+1)"), "", false, true)) { 621 Parameter param = new Parameter( 622 "Param #%d\0".format(parameters.length), 623 true 624 ); 625 param.min = vec2(-1, -1); 626 param.max = vec2(1, 1); 627 param.insertAxisPoint(0, 0.5); 628 param.insertAxisPoint(1, 0.5); 629 incActivePuppet().parameters ~= param; 630 } 631 if (igMenuItem(__("Add Mouth Shape"), "", false, true)) { 632 Parameter param = new Parameter( 633 "Mouth #%d\0".format(parameters.length), 634 true 635 ); 636 param.min = vec2(-1, 0); 637 param.max = vec2(1, 1); 638 param.insertAxisPoint(0, 0.25); 639 param.insertAxisPoint(0, 0.5); 640 param.insertAxisPoint(0, 0.6); 641 param.insertAxisPoint(1, 0.3); 642 param.insertAxisPoint(1, 0.5); 643 param.insertAxisPoint(1, 0.6); 644 incActivePuppet().parameters ~= param; 645 } 646 igEndPopup(); 647 } 648 if (igBeginChild("###FILTER", ImVec2(0, 32))) { 649 if (incInputText("Filter", filter)) { 650 filter = filter.toLower; 651 } 652 incTooltip(_("Filter, search for specific parameters")); 653 } 654 igEndChild(); 655 656 if (igBeginChild("ParametersList", ImVec2(0, -36))) { 657 658 // Always render the currently armed parameter on top 659 if (incArmedParameter()) { 660 incParameterView(incArmedParameter(), &grabParam); 661 } 662 663 // Render other parameters 664 foreach(ref param; parameters) { 665 if (incArmedParameter() == param) continue; 666 import std.algorithm.searching : canFind; 667 if (filter.length == 0 || param.indexableName.canFind(filter)) { 668 incParameterView(param, &grabParam); 669 } 670 } 671 } 672 igEndChild(); 673 674 // Right align add button 675 ImVec2 avail = incAvailableSpace(); 676 incDummy(ImVec2(avail.x-32, 32)); 677 igSameLine(0, 0); 678 679 // Add button 680 if (igButton("", ImVec2(32, 32))) { 681 igOpenPopup("###AddParameter"); 682 } 683 incTooltip(_("Add Parameter")); 684 } 685 686 public: 687 this() { 688 super("Parameters", _("Parameters"), false); 689 } 690 } 691 692 /** 693 Generate logger frame 694 */ 695 mixin incPanel!ParametersPanel; 696 697