9d38f87a2d
Payload v12 appends the new velocity->pitch curve and folds the retired filter velAmount into its now-bipolar curve, so pre-v12 projects reopen sounding identical. Preview button takes a drawn play triangle.
175 lines
7.3 KiB
C++
175 lines
7.3 KiB
C++
// editor_input_browse.cpp — the Browse modal's input: the click branch (tabs, cards,
|
|
// select-then-confirm, scroll-thumb grab, search focus), the thumb drag, the wheel scroll,
|
|
// the type-to-filter keystrokes, and the modal's hover. Also carries the degraded
|
|
// drop affordance (an OS drop is never ingested). Windows-only.
|
|
|
|
#include "shell/instrument/reasampler_editor.h"
|
|
|
|
#ifdef _WIN32
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "core/instrument/ui/browser_scroll.h" // BrowseModal + scroll/search geometry
|
|
#include "shell/instrument/editor_internal.h"
|
|
#include "shell/instrument/reasampler_processor.h"
|
|
|
|
namespace reasampler::vst {
|
|
|
|
using namespace reasampler::ui;
|
|
using namespace reasampler::instrument::ui;
|
|
using namespace reasampler::instrument::map;
|
|
|
|
void ReaSamplerEditor::mouseDownBrowse(int w, int h, int x, int y) {
|
|
const BrowseModal bm = computeBrowseModal(w, h);
|
|
if (contains(bm.back, x, y) || contains(bm.cancel, x, y)) {
|
|
// Cancel/Back: discard the pending pick, return to Sample unchanged.
|
|
browsePendingId_.clear();
|
|
searchFocused_ = false;
|
|
view_ = View::kSample;
|
|
invalidate();
|
|
return;
|
|
}
|
|
if (contains(bm.confirm, x, y)) {
|
|
// Load: commit the pending pick (if any) into the loaded selection + reload, then Sample.
|
|
if (!browsePendingId_.empty()) loadSelection(browsePendingId_);
|
|
browsePendingId_.clear();
|
|
searchFocused_ = false;
|
|
view_ = View::kSample;
|
|
invalidate();
|
|
return;
|
|
}
|
|
if (contains(bm.search, x, y)) { searchFocused_ = true; invalidate(); return; }
|
|
searchFocused_ = false;
|
|
|
|
const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height);
|
|
const int bx = x - bm.content.x;
|
|
const int by = y - bm.content.y;
|
|
const int tabCount = static_cast<int>(banks_.size()) + 1;
|
|
const int tab = filterTabHitTest(bl, tabCount, bx, by);
|
|
if (tab >= 0) {
|
|
activeFilterBankId_ = (tab == 0) ? std::string()
|
|
: banks_[static_cast<std::size_t>(tab - 1)].id;
|
|
rebuildVisible();
|
|
invalidate();
|
|
return;
|
|
}
|
|
const Rect thumb = scrollThumbRect(bl, static_cast<int>(visible_.size()), scrollOffset_);
|
|
if (thumb.height > 0 &&
|
|
contains(Rect::ltrb(thumb.x + bm.content.x, thumb.y + bm.content.y,
|
|
thumb.right() + bm.content.x, thumb.bottom() + bm.content.y), x, y)) {
|
|
drag_ = DragKind::kScrollThumb;
|
|
dragStartY_ = y;
|
|
dragStartScrollOffset_ = scrollOffset_;
|
|
return;
|
|
}
|
|
const int card = cardHitTest(bl, static_cast<int>(visible_.size()), bx, by + scrollOffset_);
|
|
if (card >= 0) {
|
|
// Select-then-confirm: a click marks the pending pick; a DOUBLE-click on the same card
|
|
// is the load accelerator (commit + dismiss). Browse never loads on a single click.
|
|
const std::string id = visible_[static_cast<std::size_t>(card)].id;
|
|
if (lastBrowseClickCard_ == card && browsePendingId_ == id) {
|
|
loadSelection(id);
|
|
browsePendingId_.clear();
|
|
lastBrowseClickCard_ = -1;
|
|
searchFocused_ = false;
|
|
view_ = View::kSample;
|
|
invalidate();
|
|
} else {
|
|
browsePendingId_ = id;
|
|
lastBrowseClickCard_ = card;
|
|
invalidate();
|
|
}
|
|
return;
|
|
}
|
|
lastBrowseClickCard_ = -1;
|
|
}
|
|
|
|
void ReaSamplerEditor::dragBrowse(int x, int y) {
|
|
// Map the thumb-drag pixel delta to a new (clamped) scroll offset. The visible-card
|
|
// window recomputes at paint from scrollOffset_.
|
|
(void)x;
|
|
RECT rc{};
|
|
GetClientRect(childHwnd_, &rc);
|
|
const BrowseModal bm = computeBrowseModal(rc.right - rc.left, rc.bottom - rc.top);
|
|
const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height);
|
|
scrollOffset_ = thumbDragToOffset(bl, static_cast<int>(visible_.size()),
|
|
dragStartScrollOffset_, y - dragStartY_);
|
|
invalidate();
|
|
}
|
|
|
|
ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverBrowse(int w, int h, int x,
|
|
int y) const {
|
|
const BrowseModal bm = computeBrowseModal(w, h);
|
|
if (contains(bm.back, x, y)) return {HoverKind::kBack, -1};
|
|
if (contains(bm.cancel, x, y)) return {HoverKind::kBrowseCancel, -1};
|
|
if (contains(bm.confirm, x, y)) return {HoverKind::kBrowseConfirm, -1};
|
|
if (contains(bm.search, x, y)) return {HoverKind::kSearchBox, -1};
|
|
|
|
const BrowserLayout bl = layoutBrowser(bm.content.width, bm.content.height);
|
|
const int bx = x - bm.content.x;
|
|
const int by = y - bm.content.y;
|
|
const int tabCount = static_cast<int>(banks_.size()) + 1;
|
|
const int tab = filterTabHitTest(bl, tabCount, bx, by);
|
|
if (tab >= 0) return {HoverKind::kFilterTab, tab};
|
|
const int card = cardHitTest(bl, static_cast<int>(visible_.size()), bx, by + scrollOffset_);
|
|
if (card >= 0) return {HoverKind::kCard, card};
|
|
return {};
|
|
}
|
|
|
|
void ReaSamplerEditor::onMouseWheel(int delta) {
|
|
// Browser scroll (only in the Browse modal — the sole card grid). One wheel notch
|
|
// (WHEEL_DELTA==120) scrolls roughly one card row; the offset is clamped at paint. A
|
|
// positive delta (wheel up) scrolls toward the top (smaller offset).
|
|
if (view_ != View::kBrowse) return;
|
|
const int rows = delta / 120;
|
|
if (rows == 0) return;
|
|
scrollOffset_ -= rows * kBrowserCardHeight;
|
|
if (scrollOffset_ < 0) scrollOffset_ = 0; // paint clamps the upper bound to the content
|
|
invalidate();
|
|
}
|
|
|
|
void ReaSamplerEditor::onSearchChar(unsigned int ch) {
|
|
// The curve popup: Esc dismisses (checked first — the popup is modal over the face, and
|
|
// the Browse search cannot hold focus under it).
|
|
if (curvePopup_ != CurveTarget::kNone && ch == 27) {
|
|
curvePopup_ = CurveTarget::kNone;
|
|
invalidate();
|
|
return;
|
|
}
|
|
|
|
// Type-to-filter search. Only when the search box has focus (a click focuses it).
|
|
// Backspace deletes; a printable ASCII char appends; the visible list recomposes (bank
|
|
// filter, then search).
|
|
if (view_ != View::kBrowse || !searchFocused_) return;
|
|
if (ch == 8) { // backspace
|
|
if (!searchQuery_.empty()) searchQuery_.pop_back();
|
|
} else if (ch == 27) { // escape clears + defocuses
|
|
searchQuery_.clear();
|
|
searchFocused_ = false;
|
|
} else if (ch >= 32 && ch < 127) {
|
|
searchQuery_.push_back(static_cast<char>(ch));
|
|
} else {
|
|
return; // ignore other control chars
|
|
}
|
|
scrollOffset_ = 0; // a new filter resets the scroll to the top of the narrowed list
|
|
rebuildVisible();
|
|
invalidate();
|
|
}
|
|
|
|
void ReaSamplerEditor::onFilesDropped(int droppedCount) {
|
|
// The instrument is a read-only bank consumer and the cross-artifact ingest relay (editor
|
|
// drop -> extension) is not shipped, so we do not ingest the dropped files and — load-
|
|
// bearing — never insert a timeline item. Instead of silently swallowing the drop, flash a
|
|
// clear affordance pointing at the shipped ingest gesture. dropHintTicks_ counts sync ticks
|
|
// (kSyncTimerIntervalMs each); ~6 ticks keeps the banner up a few seconds, then onSyncTimer
|
|
// decays it to 0.
|
|
(void)droppedCount; // count is informational; the banner text is drop-count-agnostic
|
|
dropHintTicks_ = 6;
|
|
invalidate();
|
|
}
|
|
|
|
} // namespace reasampler::vst
|
|
|
|
#endif // _WIN32
|