Re: Basic: How to go about implementing such a view?
Re: Basic: How to go about implementing such a view?
- Subject: Re: Basic: How to go about implementing such a view?
- From: Jens Bauer <email@hidden>
- Date: Fri, 2 Jan 2009 15:37:34 +0100
Hi Matt,
Here's my opinion... :)
On Dec 31, 2008, at 05:35, Matt Rajca wrote:
I am trying to figure out how to implement a 'Piano Roll' view in
Cocoa. Attached is a screenshot of what I'm trying to accomplish.
Disregard the keyboard on the left of the window and let's just
focus on the area with the colorful rectangles, symbolizing notes.
1. When drawing the light and dark grey rows which make up a
background, should I just use a loop with Quartz 2D/Cocoa Drawing
calls?
1: Find out what color occupies the most of the view - the dark or the
light color. Erase the whole view using that color.
2: Set the other color [[NSColor
colorWithCalibratedRed:green:blue:alpha:] set]; or the like
3: Set up rectangle: rect.origin = NSZeroPoint; rect.size = [self
frame].size; rect.size.height = barHeight;
4: Draw rectangles for(i = 0; i < positions; i++) { rect.
NSRectFill(rect); rect.origin.y = positions[i++]; }
Note: The fewer NSRectFill you do, the faster your code will be. Don't
use NSBezierpath, as NSRectFill is way faster!
2. When drawing the vertical lines, should I just use a loop with
Quartz 2D/Cocoa Drawing calls?
Use NSRectFill again, because NSBezierPath is way too slow. :)
3. Should I put the notes (colorful rectangles) each in a separate
view (or Core Animation layer), or is it effective to just draw them
directly in the main view (same view in which the rows are drawn)? I
am eventually going to want to resize them and change their position
by dragging them around.
Make one pianoview (eg. MRPianoRollView), which contains only the
pianoroll (all the notes), not the keyboard (put the keyboard in its
own view)
4. How would I go about changing the view's width as more notes
(rectangles) are added to the view? When enclosed in a NSScrollView,
would manipulating the width of the view also hide/show the
horizontal scrollbar appropriately?
You're in luck, just resize the view, and the NSScrollView does the
rest. You may even feel like looking at TextEdit, especially the
"ScalingScrollView", in case you want to add a zoom feature (I don't
know why you'd want that, though). =)
Everything mentioned above is easy.
The most "difficult" thing you will experience about this, is the
notes themselves.
I'd go and make my own note objects (as there will be many),
containing something like a SMPTE for position, some data [eg.
finetune/pitchbend].
I'd then make an array of pointers to this structure, as you usually
don't have more than 127 notes; one could keep a fixed array size here.
In my drawRect, I'd set up a NSRect with Y position, width and height.
The x position will be calculated from the SMPTE position. The color
will be looked up as well.
A prototype implementation could look something like the following:
enum /* flags, currently only 'selected' is used */
{
MRNoteSelected = 0x01,
MRNoteLocked = 0x02 /* not movable */
};
#define MRMinimumDuration (0.1) /* change to what suits you best */
#define MRSideSize (1.0) /* same here */
@class MRNoteData;
typedef struct NoteStruct NoteStruct; /* good old habit*/
struct NoteStruct
{
NoteStruct *prev;
NoteStruct *next;
NoteStruct *master;
long flags;
double smpte;
double duration;
double velocity;
double attack;
/* and whatever else you might want here */
MRNoteData *data; /* other data not related to drawing */
};
@interface MRPianoRollView : NSView
{
NoteStruct notes[128];
NoteStruct *lost;
float barHeight;
float scale;
}
+ (float)defaultBarHeight
+ (void)setDefaultBarHeight:(float)aDefaultBarHeight
+ (float)defaultScale;
+ (void)setDefaultScale:(float)aDefaultScale;
- (void)setScale:(float)aScale;
- (float)scale;
- (void)setBarHeight:(float)aBarHeight;
- (float)barHeight;
- (void)addNote:(NoteStruct *)aNote key:(int)aKey;
@end
@implementation MRPianoRollView
static NSColor *bgColor = NULL; // never release; used across
instances.
static NSColor *fgColor = NULL; // same here.
static float defaultScale = 10.0; // I'd probably make SMPTE in
seconds.
static float defaultBarHeight = 16.0;
#define MASTERNOTE(a) (((a)->master) ? ((a)->master) : (a))
+ (float)defaultBarHeight
{
return(defaultBarHeight);
}
+ (void)setDefaultBarHeight:(float)aDefaultBarHeight
{
defaultBarHeight = aDefaultBarHeight;
}
+ (float)defaultScale
{
return(defaultScale);
}
+ (void)setDefaultScale:(float)aDefaultScale
{
defaultScale = aDefaultScale;
}
- (void)awakeFromNib
{
if(NULL == bgColor)
{
bgColor = [[NSColor colorWithCalibratedRed:0.4 green:0.4 blue:0.4
alpha:1.0] retain];
}
if(NULL == fgColor)
{
fgColor = [[NSColor colorWithCalibratedRed:0.7 green:0.7 blue:0.7
alpha:1.0] retain];
}
for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
{
notes[i] = NULL; // good practice, but not necessary, as alloc is
zeroing the instance data.
}
barHeight = [MRPianoRollView defaultBarHeight];
scale = [MRPianoRollView defaultScale];
}
- (void)setScale:(float)aScale
{
scale = aScale;
}
- (float)scale
{
return(scale);
}
- (void)setBarHeight:(float)aBarHeight
{
barHeight = aBarHeight;
}
- (float)barHeight
{
return(barHeight);
}
- (NoteStruct *)newNote
{
NoteStruct *result;
result = (NoteStruct *)malloc(sizeof(NoteStruct));
if(result)
{
memset(result, 0, sizeof(*result));
result->prev = NULL;
result->next = NULL;
result->master = NULL;
}
return(result);
}
- (void)freeNote:(NoteStruct *)aNote // releases memory. Key must be
unlinked (removed) first
{
free(aNote);
}
- (void)removeNote:(NoteStruct *)aNote // only removes note, keeps it
allocated
{
if(aNote->next)
{
aNote->next->prev = aNote->prev;
}
if(aNote->prev)
{
aNote->prev->next = aNote->next;
}
aNote->prev = NULL;
aNote->next = NULL;
}
- (void)deleteNote:(NoteStruct *)aNote // removes and releases memory
{
[self removeNote:aNote];
[self freeNote:aNote];
}
- (void)addNote:(NoteStruct *)aNote key:(int)aKey
{
NoteStruct *travel;
BOOL lose;
lose = (aKey < 0 || aKey >= (sizeof(notes) / sizeof(*notes)));
aNote->prev = NULL;
aNote->next = NULL;
travel = lose ? lost : notes[aKey];
if(travel)
{
while(travel->next)
{
if(travel->smpte >= aNote->smpte)
{
break;
}
travel = travel->next;
}
if(travel->smpte >= aNote->smpte)
{
aNote->next = travel;
aNote->prev = travel->prev;
if(travel->prev)
{
travel->prev->next = aNote;
}
}
else
{
travel->next = aNote;
}
}
else if(lose)
{
lost = aNote;
}
else
{
notes[key] = aNote;
}
}
- (NoteStruct *)findNoteForKey:(int)aKey time:(double)aTime
{
NoteStruct *travel;
NoteStruct *next;
float start;
if(aKey >= 0 && aKey < (sizeof(notes) / sizeof(*notes)))
{
travel = notes[aKey];
while(travel)
{
next = travel->next;
start = travel->smpte;
if(aTime >= start && aTime < (start + travel->duration))
{
return(travel);
}
travel = next;
}
}
return(NULL);
}
- (double)timeFromPosition:(float)aPosition
{
return(aPosition / [self scale]);
}
- (int)keyFromPosition:(float)aPosition
{
return((int) (aPoint.y / [self barHeight]));
}
- (NoteStruct *)findNoteAtPoint:(NSPoint)aPoint
{
double time;
int key;
float bh;
float y;
time = [self timeFromPosition:aPoint.x];
key = [self keyFromPosition:aPoint.y];
bh = [self barHeight];
y = aPoint - (((float) key) * bh);
if(y >= 1.0 && y < (bh - 2.0))
{
if(key >= 0)
{
return([self findNoteForKey:key time:time]);
}
}
return(NULL);
}
void drawRect:(NSRect)aRect
{
long i;
NSRect rect;
NSRect r;
NSColor *col;
NoteStruct *note;
NoteStruct *next;
NoteStruct *original;
float bh;
float sc;
sc = [self scale]; // Good practice (speed): only use accessor
method once.
bh = [self barHeight];
rect.origin = NSZeroPoint;
rect.size = [self frame].size;
[bgColor set];
NSRectFill(rect);
rect.size.height = bh; // height of each line
[fgColor set];
for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
{
rect.origin.y = ((double) i) * bh + 1.0; // y position of each bar
switch(i % 12) // black keys
{
case 1:
case 3:
case 6:
case 8:
case 10:
NSRectFill(rect);
break;
}
for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
{
rect.origin.y = ((double) i) * bh; // y position of each bar divider
col = NULL; // assume we won't draw a separator line
switch(i % 12)
{
case 0:
rect.size.height = 2.0; // thick line when switching octave
col = [NSColor blackColor]; // (maybe you want the line to be 1
pixel and only change the color)
break;
case 7:
rect.size.height = 1.0; // thin line between E and F.
col = [NSColor grayColor];
break;
}
if(col)
{
[col set];
NSRectFill(rect); // draw the separator line
}
rect.origin.y++; // y position of each note
rect.size.height = bh - 2.0; // height of each note
note = notes[i];
while(note)
{
note = note->next; // point to next note (good practice)
original = note; // inspired by Cubase (it's such a cool
feature, having to change only the master)
if(note->master)
{
original = note->master;
}
// (the above line allows you to unlink the note and delete it in
the loop)
col = [NSColor cyanColor];
if(original->velocity > 120)
{
col = [NSColor yellowColor];
}
else if(original->velocity > 100)
{
col = [NSColor greenColor];
}
rect.origin.x = note->smpte * sc; // calculate x position of note
rect.size.width = original->duration * sc; // calculate width of note
if(note->selected)
{
r = rect;
r.origin.x++;
r.origin.y++;
[[NSColor colorWithCalibratedRed:0.0 green:0.0 blue:0.0 alpha:0.5]
set];
NSRectFillUsingOperation(rect, NSCompositeSourceOver); // draw
note shadow (draw transparant; this is slow)
}
[col set];
NSRectFill(rect); // show note
note = next; // next note.
}
}
}
- (BOOL)toggleNoteSelectionAtPoint:(NSPoint)aPoint
{
NoteStruct *note;
note = [self findNoteAtPoint:aPoint];
if(note)
{
note->flags ^= MRNoteSelected;
return(YES);
}
return(NO);
}
- (BOOL)selectNoteAtPoint:(NSPoint)aPoint
{
NoteStruct *note;
note = [self findNoteAtPoint:aPoint];
if(note && !(note->flags & MRNoteSelected))
{
note->flags |= MRNoteSelected;
return(YES);
}
return(NO);
}
- (BOOL)deselectNoteAtPoint:(NSPoint)aPoint
{
NoteStruct *note;
note = [self findNoteAtPoint:aPoint];
if(note && (note->flags & MRNoteSelected))
{
note->flags &= ~MRNoteSelected;
return(YES);
}
return(NO);
}
- (BOOL)moveSelectedNotes:(NSPoint)aRelativePoint
{
BOOL changed;
NoteStruct *travel;
NoteStruct *next;
int deltaKey;
double deltaTime;
long i;
long newKey;
deltaTime = [self timeFromPosition:aRelativePoint.x];
deltaKey = [self keyFromPosition:aRelativePoint.y];
changed = NO;
if(deltaTime && deltaKey) // if we have a change
{
for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
{
travel = notes[i];
while(travel)
{
next = travel->next;
if(travel->selected)
{
changed = YES;
travel->smpte += deltaTime;
if(travel->smpte < 0.0)
{
travel->smpte = 0.0;
}
newKey = deltaKey + i;
if(newKey < 0 || newKey > (sizeof(notes) / sizeof(*notes)))
{
[self addNote:travel key:-1];
}
else if(deltaKey)
{
[self removeNote:travel];
[self addNote:travel key:newKey];
}
}
travel = next;
}
}
}
return(changed);
}
- (BOOL)changeStartOfSelectedNotes:(double)aDeltaTime
{
BOOL changed;
NoteStruct *travel;
NoteStruct *next;
long i;
double oldValue;
changed = NO;
for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
{
travel = notes[i];
while(travel)
{
next = travel->next;
if(travel->flags & MRNoteSelected)
{
oldValue = travel->smpte;
travel->smpte += aDeltaTime;
if(travel->smpte < 0.0)
{
travel->smpte = 0.0;
}
if(oldValue != travel->smpte)
{
changed = YES;
}
}
travel = next;
}
}
return(changed);
}
- (BOOL)changeDurationOfSelectedNotes:(double)aDeltaTime
{
BOOL changed;
NoteStruct *travel;
NoteStruct *next;
long i;
double oldValue;
changed = NO;
for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
{
travel = notes[i];
while(travel)
{
next = travel->next;
if(travel->flags & MRNoteSelected)
{
oldValue = travel->duration;
travel->duration += aDeltaTime;
if(travel->duration <= MRMinimumDuration)
{
travel->duration = MRMinimumDuration;
}
if(oldValue != travel->duration)
{
changed = YES;
}
}
travel = next;
}
}
return(changed);
}
- (void)clicked:(NSPoint)aPoint withFlags:(NSUInteger)aFlags
{
NoteStruct *note;
double startTime;
double endTime;
double time;
note = [self findNoteAtPoint:pt];
if(note)
{
if(note->flags & MRNoteSelected)
{
time = [self timeFromPosition:aPoint.x];
startTime = note->smpte;
endTime = startTime + note->duration;
if(startTime - MRSideSize < time && startTime + MRSideSize > time)
{
dragKind = MRDragKindChangeStart;
}
else if(endTime - MRSideSize < time && endTime + MRSideSize > time)
{
dragKind = MRDragKindChangeDuration;
}
else
{
dragKind = MRDragKindMove;
}
}
}
if(aFlags & NSShiftKeyMask)
{
if([self toggleNoteSelectionAtPoint:aPoint])
{
[self setNeedsDisplay:YES];
}
}
else
{
if(NULL == note || !(note->flags & MRNoteSelected)) // make sure we
don't deselect when dragging multiple selections
{
if([self selectOneNoteAtPoint:aPoint])
{
[self setNeedsDisplay:YES];
}
}
}
}
- (void)mouseDown:(NSEvent *)aEvent // note: when I use 'a' in
front of the name it's a short for 'a'rgument.
{
NSPoint pt;
NSUInteger modifiers;
modifiers = [aEvent modifierFlags];
pt = [self convertPoint:[aEvent locationInWindow] fromView:NULL];
[self clicked:pt withFlags:modifiers];
}
- (void)mouseDragged:(NSEvent *)aEvent
{
NSPoint pt;
double deltaTime;
BOOL update;
// pt = [self convertPoint:[aEvent locationInWindow] fromView:NULL];
pt.x = [aEvent deltaX];
pt.y = [aEvent deltaY];
deltaTime = [self timeFromPosition:pt.x];
update = NO;
switch(dragKind)
{
case MRDragKindChangeStart:
update = [self changeStartOfSelectedNotes:deltaTime];
break;
case MRDragKindChangeDuration:
update = [self changeDurationOfSelectedNotes:deltaTime];
break;
case MRDragKindMove:
update = [self moveSelectedNotes:pt];
break;
}
if(update)
{
[self setNeedsDisplay:YES];
}
}
@end
The above is just a "what came to mind" example. I don't expect the
code to compile out-of-the-box, but it will probably give you a basic
idea on how you could implement it.
I guess it grew quite large, but it should keep you occupied for a few
minutes. ;)
Hint: Don't do many cocoa invocations ["calls"] in a loop. C is faster.
As the NoteStruct object is a C structure, the preferred way of
allocating/deallocating would be malloc/free as I did above.
I hope this can be of some use to you, and perhaps bring you some new
ideas as well.
As you'll probably notice, I haven't made any 'demo' for the above;
you could add some test-notes in the -awakeFromNib method.
Happy coding! =)
Love,
Jens
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden