266 const size_t obj_count = interaction.GetSelectionCount();
267 const bool has_entity = interaction.HasEntitySelection();
269 if (!has_entity && obj_count == 0) {
271 ImGui::TextDisabled(tr(
"Click an object or entity to inspect"));
273 DrawObjectIndexJump(viewer);
279 const auto indices = interaction.GetSelectedObjectIndices();
282 if (indices.size() > 1 && room_id >= 0 && viewer.
rooms()) {
283 auto& room = (*viewer.
rooms())[room_id];
284 auto& objects = room.GetTileObjects();
287 for (
size_t i = 0; i < indices.size() && i < 8; ++i) {
288 size_t idx = indices[i];
289 if (idx < objects.size()) {
290 auto& obj = objects[idx];
292 ImGui::BulletText(tr(
"0x%03X %s"), obj.id_, name.c_str());
295 if (indices.size() > 8) {
296 ImGui::TextDisabled(tr(
" ... and %zu more"), indices.size() - 8);
302 auto& tile_handler = interaction.entity_coordinator().tile_handler();
303 const std::vector<size_t> selection_copy(indices.begin(), indices.end());
304 bool has_room_stream_object =
false;
305 bool has_special_layer_object =
false;
306 for (
const size_t index : selection_copy) {
307 if (index >= objects.size()) {
311 has_room_stream_object =
true;
313 has_special_layer_object =
true;
316 const bool has_editable_size =
322 static int bulk_nudge_dx = 0;
323 static int bulk_nudge_dy = 0;
325 tr(
"Nudge selection (%d px steps)"),
326 interaction.entity_coordinator().SelectionMoveStepPixels());
327 ImGui::PushButtonRepeat(
true);
329 interaction.NudgeSelected(0, -1);
332 interaction.NudgeSelected(0, 1);
335 interaction.NudgeSelected(-1, 0);
338 interaction.NudgeSelected(1, 0);
339 ImGui::PopButtonRepeat();
341 ImGui::SetNextItemWidth(60);
342 ImGui::DragInt(
"##BulkNudgeDx", &bulk_nudge_dx, 0.25f, -63, 63,
"Δx:%d");
344 ImGui::SetNextItemWidth(60);
345 ImGui::DragInt(
"##BulkNudgeDy", &bulk_nudge_dy, 0.25f, -63, 63,
"Δy:%d");
347 if (ImGui::Button(tr(
"Apply##BulkNudgeApply")) &&
348 (bulk_nudge_dx != 0 || bulk_nudge_dy != 0)) {
349 interaction.NudgeSelected(bulk_nudge_dx, bulk_nudge_dy);
355 if (has_room_stream_object && !has_special_layer_object) {
356 ImGui::TextDisabled(tr(
"Object stream (set all)"));
357 if (ImGui::SmallButton(tr(
"Primary##BulkPlacement0")))
358 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
360 if (ImGui::SmallButton(tr(
"BG2 overlay##BulkPlacement1")))
361 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
363 if (ImGui::SmallButton(tr(
"BG1 overlay##BulkPlacement2")))
364 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 2);
365 }
else if (has_special_layer_object && !has_room_stream_object) {
366 ImGui::TextDisabled(tr(
"Special layer (set all)"));
367 if (ImGui::SmallButton(tr(
"Upper (BG1)##BulkPlacement0")))
368 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
370 if (ImGui::SmallButton(tr(
"Lower (BG2)##BulkPlacement1")))
371 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
373 ImGui::TextDisabled(tr(
"Stored placement (mixed selection)"));
374 if (ImGui::SmallButton(tr(
"Primary / Upper (BG1)##BulkPlacement0")))
375 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 0);
377 if (ImGui::SmallButton(tr(
"BG2 overlay / Lower (BG2)##BulkPlacement1")))
378 tile_handler.UpdateObjectsLayer(room_id, selection_copy, 1);
382 ImGui::TextDisabled(tr(
"Stacking"));
384 tile_handler.SendToFront(room_id, selection_copy);
387 tile_handler.SendToBack(room_id, selection_copy);
390 tile_handler.MoveForward(room_id, selection_copy);
393 tile_handler.MoveBackward(room_id, selection_copy);
396 ImGui::TextDisabled(tr(
"Size"));
397 if (!has_editable_size) {
398 ImGui::BeginDisabled();
401 tile_handler.ResizeObjects(room_id, selection_copy, -1);
403 if (ImGui::SmallButton(
ICON_MD_ADD "##BulkSizeInc"))
404 tile_handler.ResizeObjects(room_id, selection_copy, 1);
405 if (!has_editable_size) {
406 ImGui::EndDisabled();
411 (void)interaction.HandleDuplicateSelected();
415 {ImGuiCol_Button, theme.status_error},
416 {ImGuiCol_ButtonHovered,
417 ImVec4(theme.status_error.x + 0.1f, theme.status_error.y + 0.05f,
418 theme.status_error.z + 0.05f, 1.0f)},
421 ImGui::OpenPopup(
"##BulkDeleteConfirm");
424 if (ImGui::BeginPopup(
"##BulkDeleteConfirm")) {
427 selection_copy.size(), selection_copy.size() == 1 ?
"" :
"s");
429 if (ImGui::Button(tr(
"Cancel##BulkDelCancel"))) {
430 ImGui::CloseCurrentPopup();
435 {ImGuiCol_Button, theme.status_error},
438 tile_handler.DeleteObjects(room_id, selection_copy);
439 ImGui::CloseCurrentPopup();
447 if (indices.size() == 1 && room_id >= 0 && viewer.
rooms()) {
448 auto& room = (*viewer.
rooms())[room_id];
449 auto& objects = room.GetTileObjects();
450 const size_t idx = indices.front();
451 if (idx < objects.size()) {
454 const auto obj = objects[idx];
457 int displayed_layer = obj.GetLayerValue();
459 const bool editable =
462 ImGui::TextWrapped(
"%s ($%03X)", obj_name.c_str(), obj.id_);
463 if (ImGui::IsItemHovered()) {
464 ImGui::BeginTooltip();
465 ImGui::Text(
"Object %zu, %s (Type %d)", idx,
466 GetObjectCategory(obj.id_),
468 ImGui::Text(
"Pixel (%d, %d), %s", obj.x_ * 8, obj.y_ * 8,
469 uses_room_stream ? GetObjectStreamName(displayed_layer)
470 : GetSpecialLayerName(displayed_layer));
472 if (!description.wheel_hint.empty())
473 ImGui::TextUnformatted(description.wheel_hint.c_str());
478 const bool has_contents =
479 room.AreChestsLoaded() && *chest_index < room.GetChests().size();
481 const auto chest = room.GetChests()[*chest_index];
485 ImGui::Text(
"Size: %s",
chest.size ?
"Big" :
"Small");
487 ImGui::TextDisabled(
"Contents unavailable");
488 ImGui::Text(
"Size: %s", obj.id_ == 0xFB1 ?
"Big" :
"Small");
490 ImGui::BeginDisabled(!editable || !has_contents);
491 if (ImGui::Button(
"Edit Chest...", ImVec2(-1, 0))) {
494 status.ok() ?
"" : std::string(status.message());
500 ImGui::EndDisabled();
505 const bool show_properties =
507 ImGui::CollapsingHeader(
"Properties##ChestProperties");
512 if (show_properties) {
513 ImGui::BeginDisabled(!editable);
515 constexpr ImGuiTableFlags kPropsFlags =
516 ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg |
517 ImGuiTableFlags_NoPadOuterX;
518 if (ImGui::BeginTable(
"##SelObjProps", 2, kPropsFlags)) {
519 ImGui::TableSetupColumn(
"Prop", ImGuiTableColumnFlags_WidthFixed,
521 ImGui::TableSetupColumn(
"Val", ImGuiTableColumnFlags_WidthStretch);
525 uint16_t obj_id =
static_cast<uint16_t
>(obj.id_ & 0x0FFF);
530 interaction.SetObjectId(idx,
static_cast<int16_t
>(obj_id));
538 ImGui::SetNextItemWidth(60);
540 ImGui::DragInt(
"##SelObjX", &pos_x, 0.1f, 0, 63,
"X:%d");
544 "Selected room object X coordinate");
547 ImGui::SetNextItemWidth(60);
549 ImGui::DragInt(
"##SelObjY", &pos_y, 0.1f, 0, 63,
"Y:%d");
553 "Selected room object Y coordinate");
555 if (x_changed || y_changed) {
556 int delta_x = pos_x - obj.x_;
557 int delta_y = pos_y - obj.y_;
558 interaction.entity_coordinator().tile_handler().MoveObjects(
559 room_id, {idx}, delta_x, delta_y);
563 uint8_t requested_size = obj.size_;
565 interaction.SetObjectSize(idx, requested_size);
569 uses_room_stream ?
"Stream" :
"Layer", [&]() {
570 int layer = displayed_layer;
571 const char* stream_names[] = {
"Primary",
"BG2 overlay",
573 const char* special_layer_names[] = {
"Upper layer (BG1)",
574 "Lower layer (BG2)"};
575 ImGui::SetNextItemWidth(-1);
577 "##SelObjLayer", &layer,
578 uses_room_stream ? stream_names : special_layer_names,
580 ? IM_ARRAYSIZE(stream_names)
581 : IM_ARRAYSIZE(special_layer_names))) {
582 const int max_layer = uses_room_stream ? 2 : 1;
583 layer = std::clamp(layer, 0, max_layer);
584 if (interaction.SetObjectLayer(
587 displayed_layer = layer;
595 const auto current_indices = interaction.GetSelectedObjectIndices();
596 if (current_indices.size() == 1) {
599 ImGui::EndDisabled();
601 ImGui::CollapsingHeader(
"Details##SelectionDetails"))
602 DrawObjectIndexJump(viewer);
610 " Entity Selection");
624 const std::string room_label =
627 : std::string(
"None");
629 bool room_dirty =
false;
630 if (
auto* rooms = viewer.
rooms(); rooms && room_id >= 0) {
631 if (
const auto* room = rooms->GetIfMaterialized(room_id)) {
632 room_dirty = room->HasUnsavedChanges();
638 ImGui::AlignTextToFramePadding();
643 snprintf(buf,
sizeof(buf),
"0x%03X", room_id);
644 ImGui::SetClipboardText(buf);
646 if (ImGui::IsItemHovered()) {
647 ImGui::SetTooltip(tr(
"Copy room ID (0x%03X) to clipboard"), room_id);
656 ImGui::OpenPopup(
"RenameRoomModal");
658 if (ImGui::IsItemHovered()) {
659 ImGui::SetTooltip(tr(
"Rename Room 0x%03X for this project"), room_id);
664 if (ImGui::BeginPopup(
"RenameRoomModal")) {
665 const auto apply = [&](std::optional<std::string> label) {
666 const absl::Status status =
669 : absl::FailedPreconditionError(
"Room names are read-only");
671 ImGui::CloseCurrentPopup();
677 ImGui::Text(
ICON_MD_EDIT " Rename Room 0x%03X", room_id);
681 ImGui::TextDisabled(tr(
"Project Room Name:"));
682 ImGui::SetNextItemWidth(260.0f);
683 const bool enter_pressed = ImGui::InputText(
685 ImGuiInputTextFlags_EnterReturnsTrue);
690 ImGui::BeginDisabled(!has_name);
691 const bool save_clicked = ImGui::Button(
ICON_MD_SAVE " Save");
692 ImGui::EndDisabled();
694 if (has_name && (save_clicked || enter_pressed)) {
705 if (ImGui::IsItemHovered()) {
706 ImGui::SetTooltip(tr(
"Reset to default/registry name"));
712 ImGui::CloseCurrentPopup();
717 ImGui::TextColored(theme.status_error,
"%s",
726 ImGui::TextColored(theme.status_warning,
"Unsaved");
733 std::string project_group_name =
736 const char* group_name =
737 project_group_name.empty() ? nullptr : project_group_name.c_str();
741 group_name = cache_it->second.c_str();
745 auto* rooms = viewer.
rooms();
746 if (rooms && room_id <
static_cast<int>(rooms->size())) {
748 (*rooms)[room_id].blockset());
751 const bool is_custom_name =
753 ImGui::PushTextWrapPos(0.0f);
755 ImGui::TextDisabled(
"%s – %s", group_name, room_label.c_str());
757 ImGui::TextDisabled(
"%s", room_label.c_str());
759 if (is_custom_name) {
761 ImGui::TextColored(theme.text_info,
"(custom)");
763 ImGui::PopTextWrapPos();
765 ImGui::TextUnformatted(tr(
"Room: None"));