diff --git a/Sources/VLSheetView.h b/Sources/VLSheetView.h index a551925..bd7c7dd 100644 --- a/Sources/VLSheetView.h +++ b/Sources/VLSheetView.h @@ -13,7 +13,8 @@ @class VLDocument; enum VLMusicElement { - kMusicGClef, + kMusicNothing = 0, + kMusicGClef = 0, kMusicFlat, kMusicSharp, kMusicNatural, @@ -30,7 +31,11 @@ enum VLMusicElement { kMusicSixteenthFlag, kMusicThirtysecondthFlag, kMusicNoteCursor, + kMusicFlatCursor, + kMusicSharpCursor, + kMusicNaturalCursor, kMusicRestCursor, + kMusicKillCursor, kMusicElements }; @@ -64,6 +69,8 @@ enum VLRecalc { int fCursorMeasure; VLFract fCursorAt; int fCursorPitch; + int fCursorActualPitch; + VLMusicElement fCursorAccidental; IBOutlet id fFieldEditor; } @@ -77,9 +84,10 @@ enum VLRecalc { - (VLSong *) song; - (NSImage *) musicElement:(VLMusicElement)elt; +- (int) stepWithPitch:(int)pitch; - (float) systemY:(int)system; -- (float) noteYWithPitch:(int)pitch; -- (float) noteYInMeasure:(int)measure withPitch:(int)pitch; +- (float) noteYWithPitch:(int)pitch accidental:(VLMusicElement*)accidental; +- (float) noteYInMeasure:(int)measure withPitch:(int)pitch accidental:(VLMusicElement*)accidental; - (float) noteXInMeasure:(int)measure at:(VLFraction)at; - (void) scrollMeasureToVisible:(int)measure; diff --git a/Sources/VLSheetView.mm b/Sources/VLSheetView.mm index 268dd29..a45ec63 100644 --- a/Sources/VLSheetView.mm +++ b/Sources/VLSheetView.mm @@ -38,7 +38,11 @@ static NSString * sElementNames[kMusicElements] = { @"sixteenth-flag", @"thirtysecondth-flag", @"notecursor", - @"restcursor" + @"flatcursor", + @"sharpcursor", + @"naturalcursor", + @"restcursor", + @"killcursor" }; static float sSharpPos[] = { @@ -115,47 +119,65 @@ static float sFlatPos[] = { return kSystemY+b.origin.y+b.size.height-(system+1)*kSystemH; } -- (float) noteYWithPitch:(int)pitch +int8_t sSemi2Pitch[2][12] = {{ + // C Db D Eb E F Gb G Ab A Bb B + 0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6, +},{ + // C C# D D# E F F# G G# A A# B + 0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6, +}}; + +#define S kMusicSharp, +#define F kMusicFlat, +#define N kMusicNatural, +#define _ kMusicNothing, + +VLMusicElement sSemi2Accidental[12][12] = { + // C DbD EbE F GbG AbA BbB + {N _ N _ N _ _ N _ N _ N}, // Gb major - 6 flats + {_ _ N _ N _ _ N _ N _ N}, // Db major - 5 flats + {_ _ N _ N _ F _ _ N _ N}, // Ab major - 4 flats + {_ F _ _ N _ F _ _ N _ N}, // Eb major - 3 flats + {_ F _ _ N _ F _ F _ _ N}, // Bb major - 2 flats + {_ F _ F _ _ F _ F _ _ N}, // F major - 1 flat + // C C#D D#E F F#G G#A A#B + {_ S _ S _ _ S _ S _ S _}, // C major + {_ S _ S _ N _ _ S _ S _}, // G major - 1 sharp + {N _ _ S _ N _ _ S _ S _}, // D major - 2 sharps + {N _ _ S _ N _ N _ _ S _}, // A major - 3 sharps + {N _ N _ _ N _ N _ _ S _}, // E major - 4 sharps + {N _ N _ _ N _ N _ N _ _}, // B major - 5 sharps +}; + +#undef S +#undef F +#undef N +#undef _ + +- (int) stepWithPitch:(int)pitch +{ + int semi = pitch % 12; + int key = [self song]->fProperties.front().fKey; + bool useSharps = key >= 0; + + return sSemi2Pitch[useSharps][semi]; +} + +- (float) noteYWithPitch:(int)pitch accidental:(VLMusicElement*)accidental { int semi = pitch % 12; int octave = (pitch / 12) - 5; - bool useSharps = [self song]->fProperties.front().fKey >= 0; + int key = [self song]->fProperties.front().fKey; - float y = octave*3.5f*kLineH; - float sharp = useSharps ? 0.0f : 0.5f*kLineH; + *accidental = sSemi2Accidental[key+6][semi]; - switch (semi) { - case 0: // C - return y-1.0f*kLineH; - case 1: // C# / Db - return y-1.0f*kLineH+sharp; - case 2: // D - return y-0.5f*kLineH; - case 3: // D# / Eb - return y-0.5f*kLineH+sharp; - case 4: // E - return y; - case 5: // F - return y+0.5f*kLineH; - case 6: // F# / Gb - return y+0.5f*kLineH+sharp; - case 7: // G - return y+1.0f*kLineH; - case 8: // G# / Ab - return y+1.0f*kLineH+sharp; - case 9: // A - return y+1.5f*kLineH; - case 10: // A# / Bb - return y+1.5f*kLineH+sharp; - case 11: // B - default: - return y+2.0f*kLineH; - } + return (octave*3.5f+[self stepWithPitch:pitch]*0.5f-1.0f)*kLineH; } -- (float) noteYInMeasure:(int)measure withPitch:(int)pitch +- (float) noteYInMeasure:(int)measure withPitch:(int)pitch accidental:(VLMusicElement*)accidental { - return [self systemY:measure/fMeasPerSystem]+[self noteYWithPitch:pitch]; + return [self systemY:measure/fMeasPerSystem] + + [self noteYWithPitch:pitch accidental:accidental]; } - (float) noteXInMeasure:(int)measure at:(VLFraction)at @@ -420,7 +442,7 @@ static float sFlatPos[] = { } const float kSemiFloor = -3.0f*kLineH; -static int sSemiToPitch[] = { +static int8_t sSemiToPitch[] = { 53, // F 55, 57, // A 59, 60, // Middle C @@ -434,6 +456,97 @@ static int sSemiToPitch[] = { 86, 88 // E }; +static int8_t sFlatAcc[] = { + 6, // Cb + 11, + 4, // Db + 9, + 2, // Eb + 7, // Fb + 12, + 5, // Gb + 10, + 3, // Ab + 8, + 1, // Bb +}; + +static int8_t sSharpAcc[] = { + 2, // C# is the 2nd sharp + 9, + 4, // D# + 11, + 6, // E# + 1, // F# + 8, + 3, // G# + 10, + 5, // A# + 12, + 7, // B# +}; + +- (void) accidentalFromEvent:(NSEvent *)event +{ + const VLProperties & prop = [self song]->fProperties.front(); + + fCursorAccidental = (VLMusicElement)0; + if (prop.fKey >= 0) { + if (prop.fKey >= sSharpAcc[fCursorPitch % 12]) { // Sharp in Key + switch ([event modifierFlags] & (NSAlternateKeyMask|NSCommandKeyMask)) { + case NSAlternateKeyMask: + fCursorAccidental = kMusicFlatCursor; // G# -> Gb + fCursorActualPitch = fCursorPitch-1; + break; + default: + case NSCommandKeyMask: + fCursorActualPitch = fCursorPitch+1; + break; // G# -> G# + case NSAlternateKeyMask|NSCommandKeyMask: + fCursorAccidental = kMusicNaturalCursor; // G# -> G + fCursorActualPitch = fCursorPitch; + break; + } + return; + } + } else { + if (prop.fKey <= -sFlatAcc[fCursorPitch % 12]) { // Flat in Key + switch ([event modifierFlags] & (NSAlternateKeyMask|NSCommandKeyMask)) { + default: + case NSAlternateKeyMask: + fCursorActualPitch = fCursorPitch-1; + break; // Gb -> Gb + case NSCommandKeyMask: + fCursorAccidental = kMusicSharpCursor; // Gb -> G# + fCursorActualPitch = fCursorPitch+1; + break; + case NSAlternateKeyMask|NSCommandKeyMask: + fCursorAccidental = kMusicNaturalCursor; // Gb -> G + fCursorActualPitch = fCursorPitch; + break; + } + return; + } + } + // + // Natural + // + switch ([event modifierFlags] & (NSAlternateKeyMask|NSCommandKeyMask)) { + case NSAlternateKeyMask: + fCursorAccidental = kMusicFlatCursor; // G -> Gb + fCursorActualPitch = fCursorPitch-1; + break; + case NSCommandKeyMask: + fCursorAccidental = kMusicSharpCursor; // G -> G# + fCursorActualPitch = fCursorPitch+1; + break; + default: + case NSAlternateKeyMask|NSCommandKeyMask: + fCursorActualPitch = fCursorPitch; + break; // G -> G + } +} + - (VLRegion) findRegionForEvent:(NSEvent *) event { fCursorPitch = VLNote::kNoPitch; @@ -475,9 +588,11 @@ static int sSemiToPitch[] = { return fCursorRegion = kRegionLyrics; } - loc.y -= kSystemY+kSemiFloor; - int semi = static_cast(roundf(loc.y / (0.5f*kLineH))); - fCursorPitch = sSemiToPitch[semi]; + loc.y -= kSystemY+kSemiFloor; + int semi = static_cast(roundf(loc.y / (0.5f*kLineH))); + fCursorPitch = sSemiToPitch[semi]; + + [self accidentalFromEvent:event]; return fCursorRegion = kRegionNote; } @@ -494,6 +609,14 @@ static int sSemiToPitch[] = { [self setNeedsDisplay:(hadCursor || hasCursor)]; } +- (void)flagsChanged:(NSEvent *)event +{ + if (fCursorPitch != VLNote::kNoPitch) { + [self accidentalFromEvent:event]; + [self setNeedsDisplay:YES]; + } +} + - (void) mouseEntered:(NSEvent *)event { [[self window] setAcceptsMouseMovedEvents:YES]; diff --git a/Sources/VLSheetViewNotes.mm b/Sources/VLSheetViewNotes.mm index 2181a66..15ea6ad 100644 --- a/Sources/VLSheetViewNotes.mm +++ b/Sources/VLSheetViewNotes.mm @@ -18,7 +18,7 @@ - (void) addNoteAtCursor { if (fCursorMeasure > -1) { - VLNote newNote(1, fClickMode==' ' ? fCursorPitch : VLNote::kNoPitch); + VLNote newNote(1, fClickMode==' ' ? fCursorActualPitch : VLNote::kNoPitch); if (fClickMode == 'k') [self song]->DelNote(fCursorMeasure, fCursorAt); @@ -38,9 +38,10 @@ - (void) startKeyboardCursor { if (fCursorMeasure < 0) { - fCursorMeasure = 0; - fCursorPitch = VLNote::kMiddleC; - fCursorAt = VLFraction(0); + fCursorMeasure = 0; + fCursorPitch = VLNote::kMiddleC; + fCursorActualPitch = fCursorPitch; + fCursorAt = VLFraction(0); } } @@ -48,24 +49,61 @@ { int cursorX; int cursorY; + VLMusicElement accidental; VLMusicElement cursorElt; cursorX = [self noteXInMeasure:fCursorMeasure at:fCursorAt]-kNoteX; - if (fClickMode == ' ') { + switch (fClickMode) { + default: cursorY = - [self noteYInMeasure:fCursorMeasure withPitch:fCursorPitch]-kNoteY; + [self noteYInMeasure:fCursorMeasure + withPitch:fCursorPitch + accidental:&accidental] + -kNoteY; cursorElt = kMusicNoteCursor; - } else { - cursorY = [self noteYInMeasure:fCursorMeasure withPitch:65]; + break; + case 'r': + cursorY = [self noteYInMeasure:fCursorMeasure + withPitch:65 + accidental:&accidental]; cursorElt = kMusicRestCursor; + break; + case 'k': + cursorY = [self noteYInMeasure:fCursorMeasure + withPitch:fCursorPitch + accidental:&accidental]; + cursorElt = kMusicKillCursor; + break; } + NSPoint at = NSMakePoint(cursorX, cursorY); [[self musicElement:cursorElt] - compositeToPoint:NSMakePoint(cursorX, cursorY) + compositeToPoint:at operation: NSCompositeSourceOver]; + if (fCursorAccidental) { + at.y += kNoteY; + switch (cursorElt= fCursorAccidental) { + case kMusicFlatCursor: + at.x += kFlatW; + at.y += kFlatY; + break; + case kMusicSharpCursor: + at.x += kSharpW; + at.y += kSharpY; + break; + default: + at.x += kNaturalW; + at.y += kNaturalY; + break; + } + [[self musicElement:cursorElt] + compositeToPoint:at + operation: NSCompositeSourceOver]; + } } -- (void) drawNote:(VLFraction)dur at:(NSPoint)p tied:(BOOL)tied +- (void) drawNote:(VLFraction)dur at:(NSPoint)p + accidental:(VLMusicElement)accidental tied:(BOOL)tied { NSPoint s = p; NSPoint c = p; @@ -91,7 +129,30 @@ break; } [head compositeToPoint:p - operation: NSCompositePlusDarker]; + operation: NSCompositePlusDarker]; + // + // Draw accidental + // + if (accidental) { + NSPoint at = p; + at.y += kNoteY; + switch (accidental) { + case kMusicSharp: + at.x += kSharpW; + at.y += kSharpY; + break; + case kMusicFlat: + at.x += kFlatW; + at.y += kFlatY; + break; + case kMusicNatural: + at.x += kNaturalW; + at.y += kNaturalY; + break; + } + [[self musicElement:accidental] + compositeToPoint:at operation: NSCompositeSourceOver]; + } // // Draw stem // @@ -192,6 +253,8 @@ float kSystemY = [self systemY:system]; for (int m = 0; m= song->CountMeasures()) break; @@ -229,17 +292,34 @@ } else { noteDur = partialDur; } - if (pitch != VLNote::kNoPitch) + if (pitch != VLNote::kNoPitch) { + VLMusicElement accidental; + NSPoint pos = + NSMakePoint([self noteXInMeasure:m at:at], + kSystemY+[self noteYWithPitch:pitch + accidental:&accidental]); + VLMusicElement acc = accidental; + int step= [self stepWithPitch:pitch]; + if (acc == accidentals[step]) + acc = kMusicNothing; // Don't repeat accidentals + else if (acc == kMusicNothing) + if (accidentals[step] == kMusicNatural) // Resume signature + acc = prop.fKey < 0 ? kMusicFlat : kMusicSharp; + else + acc = kMusicNatural; [self drawNote:noteDur - at: NSMakePoint( - [self noteXInMeasure:m at:at], - kSystemY+[self noteYWithPitch:pitch]) + at: pos + accidental: acc tied:!first]; - else - [self drawRest:noteDur - at: NSMakePoint( - [self noteXInMeasure:m at:at], - kSystemY+[self noteYWithPitch:65])]; + accidentals[step] = accidental; + } else { + VLMusicElement accidental; + NSPoint pos = + NSMakePoint([self noteXInMeasure:m at:at], + kSystemY+[self noteYWithPitch:65 + accidental:&accidental]); + [self drawRest:noteDur at: pos]; + } dur -= partialDur; at += partialDur; first = NO;