#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 gridW = juce::roundToInt (cellW * numSteps); const int gridH = juce::roundToInt (cellH * (numRows + 1)); const int x = juce::roundToInt (labelW + (getWidth() - labelW - gridW) / 2.0f); const int y = juce::roundToInt (headerH + (getHeight() - headerH - gridH) / 2.0f); return { x, y, 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(); // grid background g.setColour (colours::bg); g.fillRect (bounds.toFloat()); // selected column highlight g.setColour (colours::selected.withAlpha (0.07f)); g.fillRect (ox + selectedStep * 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 + 1; ++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); if (i == selectedStep) { g.setColour (colours::accent); juce::Path tri; tri.addTriangle (cx - 4.0f, cy - 8.0f, cx + 4.0f, cy - 8.0f, cx, cy); g.fillPath (tri); } } // 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 juce::ColourGradient grad (isSelected ? colours::accent : colours::activeCell, cell.getTopLeft(), isSelected ? colours::accent2 : colours::activeCell.darker (0.6f), cell.getBottomRight(), false); g.setGradientFill (grad); g.fillRoundedRectangle (cell, radius); // slide arrow if (s.slide) { const float ax = cell.getX() + cell.getWidth() * 0.5f; const float ay = cell.getY() + cell.getHeight() - 4.0f; g.setColour (colours::selected); juce::Path tri; tri.addTriangle (ax - 3.5f, ay - 1.5f, ax + 3.5f, ay - 1.5f, ax, ay + 3.5f); g.fillPath (tri); } // 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); g.setColour (colours::accent); 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); } // --- 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()); } // --- play position marker --------------------------------------------------- if (playPos >= 0) { const float px = ox + playPos * cellW + cellW * 0.5f; g.setColour (colours::playMarker.withAlpha (0.65f)); g.drawVerticalLine (juce::roundToInt (px - 1.0f), oy, oy + (float) bounds.getHeight()); g.drawVerticalLine (juce::roundToInt (px), oy, oy + (float) bounds.getHeight()); g.setColour (colours::playMarker); juce::Path tri; tri.addTriangle (px - 3.0f, oy - headerH + 2.0f, px + 3.0f, oy - headerH + 2.0f, px, oy - 1.0f); g.fillPath (tri); } } 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 >= 0 && row < numRows + 1; return { juce::jlimit (0, numSteps - 1, col), juce::jlimit (0, numRows, row), hit }; } void SequencerMatrix::mouseDown (const juce::MouseEvent& e) { const auto cell = cellAt (e); if (! cell.hit) return; selectedStep = cell.step; if (onStepSelected) onStepSelected (cell.step); 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) { if (fineDragging) { const auto cell = cellAt (e); if (! cell.hit) return; // 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) { const auto cell = cellAt (e); if (cell.hit && cell.row == numRows && cell.step != slideLastStep) { slideLastStep = cell.step; if (onSlideChanged) onSlideChanged (cell.step, ! startedSlideActive); repaint(); } return; } if (! dragging) return; const auto cell = cellAt (e); if (cell.hit && cell.row < numRows && cell.row != lastRow) { lastRow = cell.row; moved = true; const int pitch = semitoneForRow (cell.row); 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 (! dragging) return; dragging = false; const auto cell = cellAt (e); if (! cell.hit || cell.step != dragStep || cell.row >= numRows) 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); } }