#include "SequencerMatrix.h" #include "../PluginProcessor.h" #include "../Theme.h" using namespace monostep; SequencerMatrix::SequencerMatrix (MonostepAudioProcessor& processorRef) : processor (processorRef) { } int SequencerMatrix::rowForSemitone (int semitone) const { return maxSemitone - semitone; } int SequencerMatrix::semitoneForRow (int row) const { return maxSemitone - row; } juce::Rectangle SequencerMatrix::matrixBounds() const { const int w = getWidth(); const int h = getHeight(); if (w > labelW && h > headerH) { cellW = (float) (w - labelW) / (float) numSteps; cellH = (float) (h - headerH) / (float) (numRows + 2); } const int gridW = juce::roundToInt (cellW * numSteps); const int gridH = juce::roundToInt (cellH * (numRows + 2)); return { juce::roundToInt (labelW), juce::roundToInt (headerH), gridW, gridH }; } void SequencerMatrix::setSelectedStep (int stepIdx) { if (stepIdx < 0 || stepIdx >= numSteps) return; selectedStep = stepIdx; repaint(); } void SequencerMatrix::setPlayPosition (int stepIdx) { if (stepIdx < -1 || stepIdx >= numSteps) return; if (stepIdx == playPos) return; playPos = stepIdx; repaint(); } void SequencerMatrix::paint (juce::Graphics& g) { g.fillAll (colours::panel); const auto bounds = matrixBounds(); const float ox = (float) bounds.getX(); const float oy = (float) bounds.getY(); // Slightly-3D header band above the grid. juce::ColourGradient headerGrad (colours::gridLight, 0.0f, 0.0f, colours::panel.darker (0.10f), 0.0f, headerH, false); g.setGradientFill (headerGrad); g.fillRect (0.0f, 0.0f, (float) getWidth(), headerH); g.setColour (colours::gridLight.brighter (0.3f).withAlpha (0.4f)); g.fillRect (0.0f, 0.0f, (float) getWidth(), 1.0f); g.setColour (colours::grid); g.drawHorizontalLine (juce::roundToInt (headerH) - 1, 0, (float) getWidth()); g.setColour (colours::bg); g.drawHorizontalLine (juce::roundToInt (headerH), 0, (float) getWidth()); // grid background g.setColour (colours::bg); g.fillRect (bounds.toFloat()); // selected column highlight g.setColour (colours::selected.withAlpha (0.02f)); g.fillRect (ox + selectedStep * cellW, oy, cellW, (float) bounds.getHeight()); // play position column highlight (drawn before cells so notes stay vivid) if (playPos >= 0) { g.setColour (colours::accent.withAlpha (0.04f)); g.fillRect (ox + playPos * cellW, oy, cellW, (float) bounds.getHeight()); } // grid lines g.setColour (colours::grid); for (int i = 0; i <= numSteps; ++i) { const float x = ox + i * cellW; g.drawVerticalLine (juce::roundToInt (x), oy, oy + (float) bounds.getHeight()); } for (int i = 0; i <= numRows + 2; ++i) { const float y = oy + i * cellH; g.drawHorizontalLine (juce::roundToInt (y), ox, ox + (float) bounds.getWidth()); } // accent C rows (semitone multiple of 12) for (int r = 0; r < numRows; ++r) { const int st = semitoneForRow (r); if (st % 12 == 0) { g.setColour (colours::gridLight); const float y = oy + r * cellH; g.drawHorizontalLine (juce::roundToInt (y), ox, ox + (float) bounds.getWidth()); } } // step header numbers + selected marker const int seqLen = processor.getPatternLength(); for (int i = 0; i < numSteps; ++i) { const float cx = ox + i * cellW + cellW * 0.5f; const float cy = oy - headerH * 0.5f; g.setColour ((i % 4 == 0 && i < seqLen) ? colours::text : colours::textDim); g.setFont (font (10.0f)); g.drawText (juce::String (i + 1), juce::roundToInt (cx - 8.0f), juce::roundToInt (cy - 6.0f), 16, 12, juce::Justification::centred); } // pitch labels for (int r = 0; r < numRows; ++r) { const int st = semitoneForRow (r); const float ly = oy + r * cellH + cellH * 0.5f; g.setFont (font (8.5f)); g.setColour (colours::textDim); g.drawText (juce::String (st > 0 ? "+" : "") + juce::String (st), juce::roundToInt (labelW * 0.1f), juce::roundToInt (ly - 5.0f), juce::roundToInt (labelW * 0.6f), 10, juce::Justification::centredRight); g.setColour (st % 12 == 0 ? colours::accent : colours::text); g.drawText (noteNameForOffset (st), juce::roundToInt (labelW * 0.62f), juce::roundToInt (ly - 5.0f), juce::roundToInt (labelW * 0.4f), 10, juce::Justification::centredRight); } // active cells for (int i = 0; i < numSteps; ++i) { const auto& s = processor.getSequencer().step (i); if (! s.gate) continue; const int r = rowForSemitone (s.semitone); const auto cell = juce::Rectangle (ox + i * cellW + 1.5f, oy + r * cellH + 1.5f, cellW - 3.0f, cellH - 3.0f); const bool isSelected = (i == selectedStep); const float radius = 3.0f; // cell body: keep a visible top->bottom gradient in both states. The selected // note is brighter overall, but still holds its gradient (a flat bright fill // would read as "no gradient"). const juce::Colour noteTop = isSelected ? colours::accent.brighter (0.2f) : colours::accent; const juce::Colour noteBottom = colours::accent.darker (0.3f); juce::ColourGradient grad (noteTop, cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f), noteBottom, cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f), false); g.setGradientFill (grad); g.fillRoundedRectangle (cell, radius); // fine-tune indicator if (std::fabs (s.cents) > 0.5f) { g.setColour (colours::bg.withAlpha (0.85f)); g.setFont (font (7.5f)); g.drawText (formatCents (s.cents), cell.toNearestInt().reduced (1, 1), juce::Justification::topLeft); } } // --- slide row (bottom row) ---------------------------------------------- const float slideY = oy + numRows * cellH; g.setColour (colours::accentDim); g.drawHorizontalLine (juce::roundToInt (slideY - 1.0f), ox, ox + (float) bounds.getWidth()); g.setFont (font (9.0f)); g.setColour (colours::textDim); g.drawText ("SLIDE", juce::roundToInt (labelW * 0.1f), juce::roundToInt (slideY + cellH * 0.5f - 6.0f), juce::roundToInt (labelW * 0.8f), 12, juce::Justification::centredRight); for (int i = 0; i < numSteps; ++i) { if (! processor.getSequencer().step (i).slide) continue; const auto cell = juce::Rectangle (ox + i * cellW + 1.5f, slideY + 1.5f, cellW - 3.0f, cellH - 3.0f); // match the gradient style of the note cells juce::ColourGradient grad (colours::accent, cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f), colours::accent.darker (0.3f), cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f), false); g.setGradientFill (grad); g.fillRoundedRectangle (cell, 3.0f); const float ax = cell.getX() + cell.getWidth() * 0.5f; const float ay = cell.getY() + cell.getHeight() * 0.5f; g.setColour (colours::bg); juce::Path tri; tri.addTriangle (ax - 3.0f, ay - 2.0f, ax - 3.0f, ay + 2.0f, ax + 3.5f, ay); g.fillPath (tri); } // --- accent row (bottom row) ---------------------------------------------- const float accentY = oy + (numRows + 1) * cellH; g.setColour (colours::accentDim); g.drawHorizontalLine (juce::roundToInt (accentY - 1.0f), ox, ox + (float) bounds.getWidth()); g.setFont (font (9.0f)); g.setColour (colours::textDim); g.drawText ("ACCENT", juce::roundToInt (labelW * 0.1f), juce::roundToInt (accentY + cellH * 0.5f - 6.0f), juce::roundToInt (labelW * 0.8f), 12, juce::Justification::centredRight); for (int i = 0; i < numSteps; ++i) { if (! processor.getSequencer().step (i).accent) continue; const auto cell = juce::Rectangle (ox + i * cellW + 1.5f, accentY + 1.5f, cellW - 3.0f, cellH - 3.0f); const bool isSelected = (i == selectedStep); juce::ColourGradient agrad (isSelected ? colours::accent2.brighter (0.2f) : colours::accent2, cell.getTopLeft().translated (cell.getWidth() * 0.5f, 0.0f), colours::accent2.darker (0.3f), cell.getBottomLeft().translated (cell.getWidth() * 0.5f, 0.0f), false); g.setGradientFill (agrad); g.fillRoundedRectangle (cell, 3.0f); const float ax = cell.getX() + cell.getWidth() * 0.5f; const float ay = cell.getY() + cell.getHeight() * 0.5f; g.setColour (colours::bg); juce::Path tri; tri.addTriangle (ax - 2.0f, ay - 2.5f, ax + 2.0f, ay - 2.5f, ax, ay + 2.5f); g.fillPath (tri); } // --- dim columns beyond the pattern length --------------------------------- if (seqLen < numSteps) { g.setColour (colours::bg.withAlpha (0.55f)); g.fillRect (ox + seqLen * cellW, oy, (numSteps - seqLen) * cellW, (float) bounds.getHeight()); } } SequencerMatrix::CellRect SequencerMatrix::cellAt (const juce::MouseEvent& e) const { const auto bounds = matrixBounds(); const int col = (int) std::floor ((e.getPosition().getX() - bounds.getX()) / cellW); const int row = (int) std::floor ((e.getPosition().getY() - bounds.getY()) / cellH); const bool hit = bounds.contains (e.getPosition()) && col >= 0 && col < numSteps && row >= -1 && row < numRows + 2; return { juce::jlimit (0, numSteps - 1, col), juce::jlimit (0, numRows + 1, row), hit }; } void SequencerMatrix::mouseDown (const juce::MouseEvent& e) { const auto cell = cellAt (e); if (! cell.hit) return; if (cell.row == -1) { selectedStep = cell.step; if (onStepSelected) onStepSelected (cell.step); return; } selectedStep = cell.step; if (onStepSelected) onStepSelected (cell.step); if (cell.row == numRows + 1) { accentDragging = true; accentLastStep = cell.step; startedAccentActive = processor.getSequencer().step (cell.step).accent; if (onAccentChanged) onAccentChanged (cell.step, ! startedAccentActive); repaint(); return; } if (cell.row == numRows) { slideDragging = true; slideLastStep = cell.step; startedSlideActive = processor.getSequencer().step (cell.step).slide; if (onSlideChanged) onSlideChanged (cell.step, ! startedSlideActive); repaint(); return; } if (e.mods.isAltDown()) { fineDragging = true; fineRefRow = cell.row; fineStartCents = processor.getSequencer().step (cell.step).cents; fineLastCents = -1000.0f; repaint(); return; } dragging = true; moved = false; dragStep = cell.step; lastRow = cell.row; const auto& s = processor.getSequencer().step (cell.step); startedActive = (s.gate && s.semitone == semitoneForRow (cell.row)); } void SequencerMatrix::mouseDrag (const juce::MouseEvent& e) { const auto cell = cellAt (e); if (! cell.hit || cell.row == -1) return; if (fineDragging) { // one matrix row (one semitone) maps to 4 cents of fine tuning const float cents = juce::jlimit (-50.0f, 50.0f, fineStartCents + (fineRefRow - cell.row) * 4.0f); if (std::fabs (cents - fineLastCents) >= 0.5f) { fineLastCents = cents; if (onFineChanged) onFineChanged (selectedStep, cents); repaint(); } return; } if (slideDragging) { if (cell.hit && cell.row == numRows && cell.step != slideLastStep) { slideLastStep = cell.step; if (onSlideChanged) onSlideChanged (cell.step, ! startedSlideActive); repaint(); } return; } if (accentDragging) { if (cell.hit && cell.row == numRows + 1 && cell.step != accentLastStep) { accentLastStep = cell.step; if (onAccentChanged) onAccentChanged (cell.step, ! startedAccentActive); repaint(); } return; } if (! dragging) return; if (cell.row >= numRows) return; if (cell.hit && cell.row < numRows && cell.row != lastRow) { lastRow = cell.row; moved = true; const int pitch = semitoneForRow (cell.row); const auto& s = processor.getSequencer().step (cell.step); if (startedActive) { if (onNoteChanged) onNoteChanged (dragStep, pitch); } else { if (onGateChanged) onGateChanged (dragStep, true); if (onNoteChanged) onNoteChanged (dragStep, pitch); } repaint(); } } void SequencerMatrix::mouseUp (const juce::MouseEvent& e) { if (fineDragging) { fineDragging = false; return; } if (slideDragging) { slideDragging = false; return; } if (accentDragging) { accentDragging = false; return; } if (! dragging) return; dragging = false; const auto cell = cellAt (e); if (! cell.hit || cell.step != dragStep || cell.row >= numRows || cell.row == -1) return; const int pitch = semitoneForRow (cell.row); if (startedActive && ! moved) { if (onGateChanged) onGateChanged (cell.step, false); } else if (! startedActive && ! moved) { if (onGateChanged) onGateChanged (cell.step, true); if (onNoteChanged) onNoteChanged (cell.step, pitch); } }