Basic: How to go about implementing such a view?

  • Hi!

    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?
    2. When drawing the vertical lines, should I just use a loop with
    Quartz 2D/Cocoa Drawing calls?
    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.
    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?

    Thanks!

    ~Matt
  • On Dec 30, 2008, at 20: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?
    > 2. When drawing the vertical lines, should I just use a loop with
    > Quartz 2D/Cocoa Drawing calls?

    Yes to both. You can choose to draw a line with a 1 pixel wide
    rectangle (or whatever the line width is) using NSRectFill or one of
    its friends, or you can choose to construct a NSBezierPath (you can
    put some or all of the lines in the same path with move-to/line-to
    combinations) and then stroke the path. There is a subtle difference
    between the two approaches: when using whole-number coordinate values,
    NSRectFill in effect pixel-aligns the edges of the line, while
    NSBezierPath in effect pixel-aligns the center of the line. (Of
    course, you can get the same result with either approach, by suitably
    offsetting your coordinates by half a pixel.)

    > 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.

    With the number of note rects your image shows (and presumably there
    could be a lot more in some music), I'd suggest avoiding subviews. If
    you want to animate the notes (have them slide around smoothly, or
    fade in or out) CA layers would probably be the way to go. Otherwise,
    just draw everything in a single custom 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?

    Something like this:

    NSRect noteFrame = noteView.frame; // or [noteView frame], if you
    prefer that syntax
    noteFrame.size.width = computedViewWidth;
    noteView.frame = noteFrame; // or [noteView setFrame: noteFrame], if
    you prefer that syntax

    and make sure you've disabled horizontal autoresizing for your note
    view (in Interface Builder).

    If you've checked the NSScrollView option to automatically hide/show
    scrollbars (in Interface Builder), changing the note view frame as
    above will automatically hide or show the horizontal scrollbar.
  • On 31 Dec 2008, at 3:35 pm, Matt Rajca wrote:

    > 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?
    > 2. When drawing the vertical lines, should I just use a loop with
    > Quartz 2D/Cocoa Drawing calls?

    I'd say yes, this is probably the most straightforward. There's
    nothing built-in that will do it for you, but this kind of drawing is
    easy enough. For the lines, you might consider caching them into an
    NSBezierPath and drawing it all in one go rather than drawing each
    line separately, but that's a minor detail.

    > 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.

    Draw them directly in the main view. Each as a subview would be a huge
    overkill that would be more complicated than necessary and probably
    suffer from performance problems. Each rectangle would probably be
    most usefully represented by a custom class but it wouldn't need to be
    a view.

    Dragging is not too hard if you design your code appropriately. At
    some point you'll want to translate bidirectionally from your data
    model (note frequency + duration) to the view (rectangles). Hit-
    detecting which rectangle the mouse went down in will depend on you
    being able to find the right rectangle and relate that to the note you
    are affecting.

    > 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?

    Just set the view's bounds to whatever you need. If in a scroller, it
    will work automatically with respect to the scrollbars, both for the
    scroll values and automatic hiding.

    hth,

    Graham
  • On Dec 30, 2008, at 23:35, Matt Rajca <matt.rajca...> wrote:

    > Hi!
    >
    > 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?

    Seems reasonable. The one thingbto be sure of is, for efficiency's
    sake, only draw the rows that intersect the rect passed to drawRect:

    >
    > 2. When drawing the vertical lines, should I just use a loop with
    > Quartz 2D/Cocoa Drawing calls?

    Same answer as #1 :)

    >
    > 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.

    This is up to you. Using a view per note is probably the simplest from
    an implementation standpoint, but it is likely to also be the least
    efficient. Were I to do this, I would probably use A CA layer per note.

    >
    > 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?

    Yes, this is exactly how scroll views work.

    >
    >
    > Thanks!
    >
    > ~Matt
    >
    >
    > <PianoRollWindow.jpg
  • This looks like a good fit for CGLayers.

    http://developer.apple.com/documentation/GraphicsImaging/Conceptual/drawing
    withquartz2d/dq_layers/chapter_13_section_1.html#/

    /apple_ref/doc/uid/TP30001066-CH219-TPXREF101

    The background and color bars could be a plain old bitmap images drawn
    into cglayers. Once loaded into a layer, you can reuse an element as
    many times as you like, manipulating its width and origin with
    transforms. There is an example at the above link that shows how to
    create a U.S. flag using CGLayers that is pretty much what you're
    trying to do with your background.

    I don't know about hit testing an item in a layer, but an alternative
    way to create color bars would be to use NSBezierPath which has a
    "containsPoint:" method you could use for bar selection.

    http://developer.apple.com/documentation/Cocoa/Reference/ApplicationKit/Cla
    sses/NSBezierPath_Class/Reference/Reference.html#/

    /apple_ref/occ/instm/NSBezierPath/containsPoint:

    As to animating the bar movements, someone else will need to chime in
    on that one.

    On Dec 30, 2008, at 11:35 PM, Matt Rajca wrote:

    > Hi!
    >
    > 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?
    > 2. When drawing the vertical lines, should I just use a loop with
    > Quartz 2D/Cocoa Drawing calls?
    > 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.
    > 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?
    >
    > Thanks!
    >
    > ~Matt
    >
    >
    > <PianoRollWindow.jpg
  • 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
  • I'm responding off the top of my head, so take all of this as just my
    first, instinctual approach.

    On 30 Dec 2008, at 10:35 PM, 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?

    I'd think in terms of "cells." Just let your view have a "drawCell"
    method that takes the datum, location, and background, and draws for
    just that datum: It draws a cell within the larger view. Loop through
    the data, subtracting cell.size.height from cell.origin.y at each step.

    See the NSTableDataSource documentation for an example of how a
    controller delegate can supply the data for each row, without your
    view having to know anything about your model*. All you'd have to do
    is keep a reference to the data source.

    ==
    * This is a bit hollow, since your cell-drawing code has to reference
    the properties unique to your model, but you get the idea.
    ==

    So, yes, a loop of Quartz or Cocoa drawing calls.

    > 2. When drawing the vertical lines, should I just use a loop with
    > Quartz 2D/Cocoa Drawing calls?

    I'd have the view draw the lines after it's looped through the cells.
    Again, yes, a loop of +[NSBezierPath strokeLineFromPoint:toPoint:].

    > 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.

    I haven't seen anything that requires Core Animation. A view for each
    row is possible, but I think it's much too heavyweight, and
    complicates the geometry and drawing those lines over everything.

    For dragging, lock drawing focus on an NSImage, draw the cell into it,
    and feed the image to the view's
    dragImage:at:offset:event:pasteboard:source:slideBack:

    > 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?

    As notes are added, change the view's frame to match. The NSScrollView
    mechanism will handle the rest.

    — F

    --
    Fritz Anderson -- Xcode 3 Unleashed: Now in its second printing -- <http://x3u.manoverboard.org/>
  • Hi,

    On 31 dec 2008, at 05:35, Matt Rajca wrote:

    > 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.

    Just an addition.
    I would use everything in one mainview, like the others wrote. But
    when ( left|right ) clicked on a note, I should draw an noteView over
    that note with the same outfit, magnified or not, and use that for
    resizing, movements and other needings like submenu with color
    settings e.t.c ( after clearing the note in the main View ). Then when
    finished with the note, redraw the note in the mainview.

    Not sure for the best way how to do a resize cursor, if wanted.

    Just my 0.01"

    ~RvA