Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Adding drag-to-reorder rows support to TUITableView, fixes to section header support #36

Closed
wants to merge 29 commits into from

2 participants

@bww
bww commented

Drag-to-reorder rows support is added to TUITableView in a manner similar to UITableView. To use this functionality, a table's data source should implement the new methods:

- (BOOL)tableView:(TUITableView *)tableView canMoveRowAtIndexPath:(TUIFastIndexPath *)indexPath;
- (void)tableView:(TUITableView *)tableView moveRowAtIndexPath:(TUIFastIndexPath *)fromIndexPath toIndexPath:(TUIFastIndexPath *)toIndexPath;

...and optionally, to constrain where rows may be moved, the table's delegate can implement:

- (TUIFastIndexPath *)tableView:(TUITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(TUIFastIndexPath *)fromPath toProposedIndexPath:(TUIFastIndexPath *)proposedPath;

I've added a new category to TUITableView (Cell) which exposes some "internal" methods of the table view that cells need to have access to due to the close relationship between these classes. Specifically, cells can use these methods to forward mouse events to the table so that the table can do something with them (in this case, handle dragging).

In order to allow a table to automatically scroll content into view when a cell is dragged to the top or bottom of the viewport, I've made some small changes to TUIScrollView as well. Specifically, the following methods are added which provide this kind of scrolling:

- (void)beginContinuousScrollForDragAtPoint:(CGPoint)dragLocation animated:(BOOL)animated;
- (void)endContinuousScrollAnimated:(BOOL)animated;

...via the addition of the new animation mode AnimationModeScrollContinuous. Due to the highly specific nature of this kind of scrolling, maybe these methods should be private, but I didn't want to reorganize TUIScrollView too much.

The example project has also been updated to demonstrate drag-to-reorder, however, since there isn't a "real" model and cells are just assigned a label corresponding to their index, when the moved cell is dropped, the table will revert immediately back to its original state. This is because visible cells are laid out again immediately after the operation completes and get re-assigned their index-based label.

Other miscellaneous updates:

  • Adding table will/did reload delegate methods
  • Fixes incorrect geometry in TUITableView -indexesOfSectionsInRect:
  • Adding -rectForSection: method in TUITableView
  • Adding index path enumeration methods to TUITableView
Brian Willia... added some commits
Brian William Wolter Fixing -indexesOfSectionsInRect: and adding -rectForSection: in TUITa…
…bleView
5799afe
Brian William Wolter Adding table will/did reload delegate methods to TUITableView df419f7
Brian William Wolter Adding initial drag-to-reorder support to TUITableView; still needs w…
…ork.
9e1a635
Brian William Wolter Table view cell drag-to-reorder updates c874a52
Brian William Wolter More drag-to-reorder updates 2c0e12f
Brian William Wolter - More drag-to-reorder updates
- Fixing an issue with table header views which could cause views to get out of sync with section info
2ea3115
Brian William Wolter - Adding index path enumeration methods to TUITableView
- Updating drag-to-reorder support to use the above
539d350
Brian William Wolter Trying a new approach; more reliable but slower... still a work in pr…
…ogress.
a75960b
Brian William Wolter More table drag-to-reorder improvements 35670f4
Brian William Wolter More table drag-to-reorder improvements bc1359b
Brian William Wolter More table view drag-to-reorder improvements 4abf3e5
Brian William Wolter Yet more table drag-to-reorder updates 96b764f
Brian William Wolter More table drag-to-reorder updates 6301a7f
Brian William Wolter Yet more table drag-to-reorder updates. In pretty good shape now. c766417
Brian William Wolter Cleaning up some table drag-to-reorder stuff 79e1e44
Brian William Wolter Small adjustments 263901e
Brian William Wolter Merge remote-tracking branch 'upstream/master'
Conflicts:
	lib/UIKit/TUITableView.m
055fc62
Brian William Wolter - Adding "continuous scrolling" support to TUIScrollView, which is us…
…ed to scroll new content into view during a table drag-to-reorder operation when the cell is dragged near the top or bottom of the viewport

- Updating cell reuse to work correctly with drag-to-reorder while scrolling
- Cleaning up the TUITableView+Cell category a bit
1b9cd92
Brian William Wolter More table drag-to-reorder updates b9a6022
Brian William Wolter More table drag-to-reorder fixes caa7577
Brian William Wolter Updating example project to add drag-to-reorder support to the table 2b977c3
Brian William Wolter Reloading on drop now b8e5de5
@atebits

Wow, awesome! Will play for a bit.

@bww
bww commented

One thing you might want to take a look at. Originally, I had the table fully reload after the drop completed to make sure the view and model were consistent. This worked fine in the project I'm working on, but caused problems in the example project: after the first drop the view would clear and scroll back to the bottom for some reason; subsequent reorders would work fine, though.

I changed the full reload to essentially a relayout, but I'm not sure about the correctness of this. I think a full reload is perhaps better—in part because the new table will/did reload delegate methods can handle selection changes after the row order changes and this can be done easily since it's invoked after the drop animation completes.

I'm sure you'll have a better sense of what needs to happen there to make sure everything's in a consistent state, though.

Here's the relevant bit:
https://github.com/bww/twui/blob/master/lib/UIKit/TUITableView+Cell.m#L332

@atebits

I full reload would be cleaner. I recently merged in #31 -- it's off by default, perhaps that (if we change it to be the default behavior) would fix the problem?

@bww
bww commented

I tried merging that in and setting maintainContentOffsetAfterReload, but it didn't seem to make a difference.

Brian Willia... added some commits
@atebits

This is fucking awesome btw. Conflicts a bit with the new sticky section headers, but I'd rather merge and fix as we go.

@atebits

Squash merged in (there were a lot of commits).

@atebits atebits closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jul 26, 2011
  1. Fixing -indexesOfSectionsInRect: and adding -rectForSection: in TUITa…

    Brian William Wolter authored
    …bleView
Commits on Jul 30, 2011
  1. Adding table will/did reload delegate methods to TUITableView

    Brian William Wolter authored
Commits on Aug 1, 2011
  1. Adding initial drag-to-reorder support to TUITableView; still needs w…

    Brian William Wolter authored
    …ork.
  2. Table view cell drag-to-reorder updates

    Brian William Wolter authored
  3. More drag-to-reorder updates

    Brian William Wolter authored
  4. - More drag-to-reorder updates

    Brian William Wolter authored
    - Fixing an issue with table header views which could cause views to get out of sync with section info
Commits on Aug 2, 2011
  1. - Adding index path enumeration methods to TUITableView

    Brian William Wolter authored
    - Updating drag-to-reorder support to use the above
  2. Trying a new approach; more reliable but slower... still a work in pr…

    Brian William Wolter authored
    …ogress.
  3. More table drag-to-reorder improvements

    Brian William Wolter authored
  4. More table drag-to-reorder improvements

    Brian William Wolter authored
  5. More table view drag-to-reorder improvements

    Brian William Wolter authored
Commits on Aug 3, 2011
  1. Yet more table drag-to-reorder updates

    Brian William Wolter authored
  2. More table drag-to-reorder updates

    Brian William Wolter authored
  3. Yet more table drag-to-reorder updates. In pretty good shape now.

    Brian William Wolter authored
  4. Cleaning up some table drag-to-reorder stuff

    Brian William Wolter authored
  5. Small adjustments

    Brian William Wolter authored
  6. Merge remote-tracking branch 'upstream/master'

    Brian William Wolter authored
    Conflicts:
    	lib/UIKit/TUITableView.m
Commits on Aug 4, 2011
  1. - Adding "continuous scrolling" support to TUIScrollView, which is us…

    Brian William Wolter authored
    …ed to scroll new content into view during a table drag-to-reorder operation when the cell is dragged near the top or bottom of the viewport
    
    - Updating cell reuse to work correctly with drag-to-reorder while scrolling
    - Cleaning up the TUITableView+Cell category a bit
  2. More table drag-to-reorder updates

    Brian William Wolter authored
  3. More table drag-to-reorder fixes

    Brian William Wolter authored
Commits on Aug 5, 2011
  1. Reloading on drop now

    Brian William Wolter authored
  2. Merge remote-tracking branch 'upstream/master'

    Brian William Wolter authored
Commits on Aug 11, 2011
  1. Remove/re-add fix from joshaber

    Brian William Wolter authored
  2. Merge remote-tracking branch 'upstream/master'

    Brian William Wolter authored
    Conflicts:
    	lib/UIKit/TUINSView.m
    	lib/UIKit/TUITableViewCell.m
  3. More cell first responder updates

    Brian William Wolter authored
  4. Adding support for "selected" state of TUIControl and cleaning up the…

    Brian William Wolter authored
    … state property
  5. TUIControl now sends touch down/up events

    Brian William Wolter authored
This page is out of date. Refresh to see the latest.
View
13 ExampleProject/ConcordeExample/ExampleSectionHeaderView.m
@@ -30,9 +30,18 @@ -(void)drawRect:(CGRect)rect {
CGContextRef g;
if((g = TUIGraphicsGetCurrentContext()) != nil){
+ [NSGraphicsContext setCurrentContext:[NSGraphicsContext graphicsContextWithGraphicsPort:g flipped:FALSE]];
- CGContextSetRGBFillColor(g, 0.8, 0.8, 0.8, 1);
- CGContextFillRect(g, self.bounds);
+ NSColor *start = [NSColor colorWithCalibratedRed:0.8 green:0.8 blue:0.8 alpha:1];
+ NSColor *end = [NSColor colorWithCalibratedRed:0.9 green:0.9 blue:0.9 alpha:1];
+ NSGradient *gradient = nil;
+
+ gradient = [[NSGradient alloc] initWithStartingColor:start endingColor:end];
+ [gradient drawInRect:self.bounds angle:90];
+ [gradient release];
+
+ [[start shadowWithLevel:0.1] set];
+ NSRectFill(NSMakeRect(0, 0, self.bounds.size.width, 1));
CGFloat labelHeight = 18;
self.labelRenderer.frame = CGRectMake(15, roundf((self.bounds.size.height - labelHeight) / 2.0), self.bounds.size.width - 30, labelHeight);
View
24 ExampleProject/ConcordeExample/ExampleView.m
@@ -44,6 +44,7 @@ Note by default scroll views (and therefore table views) don't
_tableView.autoresizingMask = TUIViewAutoresizingFlexibleSize;
_tableView.dataSource = self;
_tableView.delegate = self;
+ _tableView.maintainContentOffsetAfterReload = TRUE;
[self addSubview:_tableView];
_tabBar = [[ExampleTabBar alloc] initWithNumberOfTabs:5];
@@ -115,6 +116,10 @@ - (void)dealloc
- (void)tabBar:(ExampleTabBar *)tabBar didSelectTab:(NSInteger)index
{
NSLog(@"selected tab %ld", index);
+ if(index == [[tabBar tabViews] count] - 1){
+ NSLog(@"Reload table data...");
+ [_tableView reloadData];
+ }
}
- (NSInteger)numberOfSectionsInTableView:(TUITableView *)tableView
@@ -174,4 +179,23 @@ - (BOOL)tableView:(TUITableView *)tableView shouldSelectRowAtIndexPath:(TUIFastI
return YES;
}
+-(BOOL)tableView:(TUITableView *)tableView canMoveRowAtIndexPath:(TUIFastIndexPath *)indexPath {
+ // return TRUE to enable row reordering by dragging; don't implement this method or return
+ // FALSE to disable
+ return TRUE;
+}
+
+-(void)tableView:(TUITableView *)tableView moveRowAtIndexPath:(TUIFastIndexPath *)fromIndexPath toIndexPath:(TUIFastIndexPath *)toIndexPath {
+ // update the model to reflect the changed index paths; since this example isn't backed by
+ // a "real" model, after dropping a cell the table will revert to it's previous state
+ NSLog(@"Move dragged row: %@ => %@", fromIndexPath, toIndexPath);
+}
+
+-(TUIFastIndexPath *)tableView:(TUITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(TUIFastIndexPath *)fromPath toProposedIndexPath:(TUIFastIndexPath *)proposedPath {
+ // optionally revise the drag-to-reorder drop target index path by returning a different index path
+ // than proposedPath. if proposedPath is suitable, return that. if this method is not implemented,
+ // proposedPath is used by default.
+ return proposedPath;
+}
+
@end
View
6 ExampleProject/Example.xcodeproj/project.pbxproj
@@ -65,6 +65,7 @@
5ED56727139DC35100031CDF /* TUIView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5ED566F2139DC35100031CDF /* TUIView.m */; };
5ED56728139DC35100031CDF /* TUIViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5ED566F4139DC35100031CDF /* TUIViewController.m */; };
5ED56736139DC35800031CDF /* CoreText+Additions.m in Sources */ = {isa = PBXBuildFile; fileRef = 5ED56732139DC35800031CDF /* CoreText+Additions.m */; };
+ D3502AAE13EA0FE4007C5CA7 /* TUITableView+Cell.m in Sources */ = {isa = PBXBuildFile; fileRef = D3502AAD13EA0FE4007C5CA7 /* TUITableView+Cell.m */; };
D3CE671313C6646B00D47B2D /* ExampleSectionHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = D3CE671213C6646B00D47B2D /* ExampleSectionHeaderView.m */; };
/* End PBXBuildFile section */
@@ -178,6 +179,8 @@
5ED566F5139DC35100031CDF /* TUIViewNSViewContainer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TUIViewNSViewContainer.h; sourceTree = "<group>"; };
5ED56731139DC35800031CDF /* CoreText+Additions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "CoreText+Additions.h"; path = "../Support/CoreText+Additions.h"; sourceTree = "<group>"; };
5ED56732139DC35800031CDF /* CoreText+Additions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "CoreText+Additions.m"; path = "../Support/CoreText+Additions.m"; sourceTree = "<group>"; };
+ D3502AAC13EA0FE4007C5CA7 /* TUITableView+Cell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "TUITableView+Cell.h"; sourceTree = "<group>"; };
+ D3502AAD13EA0FE4007C5CA7 /* TUITableView+Cell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "TUITableView+Cell.m"; sourceTree = "<group>"; };
D3CE671113C6646B00D47B2D /* ExampleSectionHeaderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ExampleSectionHeaderView.h; sourceTree = "<group>"; };
D3CE671213C6646B00D47B2D /* ExampleSectionHeaderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ExampleSectionHeaderView.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -333,6 +336,8 @@
5ED566D0139DC35100031CDF /* TUITableView+Additions.m */,
5ED566D1139DC35100031CDF /* TUITableView+Derepeater.h */,
5ED566D2139DC35100031CDF /* TUITableView+Derepeater.m */,
+ D3502AAC13EA0FE4007C5CA7 /* TUITableView+Cell.h */,
+ D3502AAD13EA0FE4007C5CA7 /* TUITableView+Cell.m */,
5ED566D5139DC35100031CDF /* TUITableViewCell.h */,
5ED566D6139DC35100031CDF /* TUITableViewCell.m */,
5ED56698139DC35100031CDF /* TUIActivityIndicatorView.h */,
@@ -483,6 +488,7 @@
5C55D83713A66BD5000ED768 /* ExampleTableViewCell.m in Sources */,
5C90DB9D13A7C08E00ECDD14 /* ExampleTabBar.m in Sources */,
D3CE671313C6646B00D47B2D /* ExampleSectionHeaderView.m in Sources */,
+ D3502AAE13EA0FE4007C5CA7 /* TUITableView+Cell.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
View
40 lib/UIKit/TUIControl.h
@@ -17,36 +17,37 @@
#import "TUIView.h"
enum {
- TUIControlEventTouchDown = 1 << 0,
- TUIControlEventTouchDownRepeat = 1 << 1,
- TUIControlEventTouchUpInside = 1 << 6,
- TUIControlEventTouchUpOutside = 1 << 7,
- TUIControlEventValueChanged = 1 << 12,
- TUIControlEventEditingDidEndOnExit = 1 << 19,
- TUIControlEventAllTouchEvents = 0x00000FFF,
- TUIControlEventAllEditingEvents = 0x000F0000,
- TUIControlEventApplicationReserved = 0x0F000000,
- TUIControlEventSystemReserved = 0xF0000000,
- TUIControlEventAllEvents = 0xFFFFFFFF
+ TUIControlEventTouchDown = 1 << 0,
+ TUIControlEventTouchDownRepeat = 1 << 1,
+ TUIControlEventTouchUpInside = 1 << 6,
+ TUIControlEventTouchUpOutside = 1 << 7,
+ TUIControlEventValueChanged = 1 << 12,
+ TUIControlEventEditingDidEndOnExit = 1 << 19,
+ TUIControlEventAllTouchEvents = 0x00000FFF,
+ TUIControlEventAllEditingEvents = 0x000F0000,
+ TUIControlEventApplicationReserved = 0x0F000000,
+ TUIControlEventSystemReserved = 0xF0000000,
+ TUIControlEventAllEvents = 0xFFFFFFFF
};
typedef NSUInteger TUIControlEvents;
enum {
- TUIControlStateNormal = 0,
- TUIControlStateHighlighted = 1 << 0,
- TUIControlStateDisabled = 1 << 1,
- TUIControlStateSelected = 1 << 2,
- TUIControlStateNotKey = 1 << 11,
- TUIControlStateApplication = 0x00FF0000,
- TUIControlStateReserved = 0xFF000000
+ TUIControlStateNormal = 0,
+ TUIControlStateHighlighted = 1 << 0,
+ TUIControlStateDisabled = 1 << 1,
+ TUIControlStateSelected = 1 << 2,
+ TUIControlStateNotKey = 1 << 11,
+ TUIControlStateApplication = 0x00FF0000,
+ TUIControlStateReserved = 0xFF000000
};
typedef NSUInteger TUIControlState;
@interface TUIControl : TUIView
{
- NSMutableArray* _targetActions;
+ NSMutableArray* _targetActions;
struct {
unsigned int disabled:1;
+ unsigned int selected:1;
unsigned int acceptsFirstMouse:1;
unsigned int tracking:1;
} _controlFlags;
@@ -56,6 +57,7 @@ typedef NSUInteger TUIControlState;
@property(nonatomic,readonly) TUIControlState state;
@property(nonatomic,readonly,getter=isTracking) BOOL tracking;
+@property(nonatomic,assign) BOOL selected;
@property (nonatomic, assign) BOOL acceptsFirstMouse;
View
64 lib/UIKit/TUIControl.m
@@ -57,9 +57,47 @@ - (BOOL)isTracking
- (TUIControlState)state
{
- if(_controlFlags.tracking)
- return TUIControlStateHighlighted;
- return [self.nsWindow isKeyWindow]?TUIControlStateNormal:TUIControlStateNotKey;
+ // start with the normal state, then OR in implicit state that is based on other properties
+ TUIControlState actual = TUIControlStateNormal;
+
+ if(_controlFlags.disabled) actual |= TUIControlStateDisabled;
+ if(_controlFlags.selected) actual |= TUIControlStateSelected;
+ if(_controlFlags.tracking) actual |= TUIControlStateHighlighted;
+ if(![self.nsWindow isKeyWindow]) actual |= TUIControlStateNotKey;
+
+ return actual;
+}
+
+/**
+ * @brief Determine if this control is in a selected state
+ *
+ * Not all controls have a selected state and the meaning of "selected" is left
+ * to individual control implementations to define.
+ *
+ * @return selected or not
+ *
+ * @note This is a convenience interface to the #state property.
+ * @see #state
+ */
+-(BOOL)selected {
+ return _controlFlags.selected;
+}
+
+/**
+ * @brief Specify whether this control is in a selected state
+ *
+ * Not all controls have a selected state and the meaning of "selected" is left
+ * to individual control implementations to define.
+ *
+ * @param selected selected or not
+ *
+ * @see #state
+ */
+-(void)setSelected:(BOOL)selected {
+ [self _stateWillChange];
+ _controlFlags.selected = selected;
+ [self _stateDidChange];
+ [self setNeedsDisplay];
}
- (BOOL)acceptsFirstMouse
@@ -80,19 +118,39 @@ - (BOOL)acceptsFirstMouse:(NSEvent *)event
- (void)mouseDown:(NSEvent *)event
{
[super mouseDown:event];
+
+ // handle state change
[self _stateWillChange];
_controlFlags.tracking = 1;
[self _stateDidChange];
+
+ // handle touch down
+ [self sendActionsForControlEvents:TUIControlEventTouchDown];
+
+ // needs display
[self setNeedsDisplay];
+
}
- (void)mouseUp:(NSEvent *)event
{
[super mouseUp:event];
+
+ // handle state change
[self _stateWillChange];
_controlFlags.tracking = 0;
[self _stateDidChange];
+
+ // handle touch up
+ if([self pointInside:[self localPointForEvent:event] withEvent:event]){
+ [self sendActionsForControlEvents:TUIControlEventTouchUpInside];
+ }else{
+ [self sendActionsForControlEvents:TUIControlEventTouchUpOutside];
+ }
+
+ // needs display
[self setNeedsDisplay];
+
}
@end
View
15 lib/UIKit/TUIGeometry.h
@@ -38,3 +38,18 @@ static inline BOOL TUIEdgeInsetsEqualToEdgeInsets(TUIEdgeInsets insets1, TUIEdge
}
extern const TUIEdgeInsets TUIEdgeInsetsZero;
+
+/**
+ * @brief Constrain a point to a rectangular region
+ *
+ * If the provided @p point lies outside the @p rect, it is adjusted to the
+ * nearest point that lies inside the @p rect.
+ *
+ * @param point a point
+ * @param rect the constraining rect
+ * @return constrained point
+ */
+static inline CGPoint CGPointConstrainToRect(CGPoint point, CGRect rect) {
+ return CGPointMake(MAX(rect.origin.x, MIN((rect.origin.x + rect.size.width), point.x)), MAX(rect.origin.y, MIN((rect.origin.y + rect.size.height), point.y)));
+}
+
View
7 lib/UIKit/TUIScrollView.h
@@ -92,6 +92,8 @@ typedef enum {
BOOL pulling; // horizontal pulling not done yet, this flag should be split
} _pull;
+ CGPoint _dragScrollLocation;
+
BOOL x;
struct {
@@ -103,7 +105,7 @@ typedef enum {
unsigned int scrollDisabled:1;
unsigned int indicatorStyle:2;
unsigned int showsHorizontalScrollIndicator:1;
- unsigned int showsVerticalScrollIndicator:1;
+ unsigned int showsVerticalScrollIndicator:1;
unsigned int delegateScrollViewDidScroll:1;
unsigned int delegateScrollViewWillBeginDragging:1;
unsigned int delegateScrollViewDidEndDragging:1;
@@ -126,6 +128,9 @@ typedef enum {
- (void)scrollToTopAnimated:(BOOL)animated;
- (void)scrollToBottomAnimated:(BOOL)animated;
+- (void)beginContinuousScrollForDragAtPoint:(CGPoint)dragLocation animated:(BOOL)animated;
+- (void)endContinuousScrollAnimated:(BOOL)animated;
+
@property (nonatomic, readonly) CGRect visibleRect;
- (void)flashScrollIndicators;
View
62 lib/UIKit/TUIScrollView.m
@@ -23,6 +23,9 @@
#define FORCE_ENABLE_BOUNCE 1
+#define TUIScrollViewContinuousScrollDragBoundary 25.0
+#define TUIScrollViewContinuousScrollRate 10.0
+
enum {
ScrollPhaseNormal = 0,
ScrollPhaseThrowingBegan = 1,
@@ -33,6 +36,7 @@
enum {
AnimationModeThrow,
AnimationModeScrollTo,
+ AnimationModeScrollContinuous,
};
@interface TUIScrollView (Private)
@@ -407,6 +411,40 @@ - (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
}
}
+/**
+ * @brief Begin scrolling continuously for a drag
+ *
+ * Content is continuously scrolled in the direction of the drag until the end
+ * of the content is reached or the operation is cancelled via
+ * #endContinuousScrollAnimated:.
+ *
+ * @param dragLocation the drag location
+ * @param animated animate the scroll or not (this is currently ignored and the scroll is always animated)
+ */
+- (void)beginContinuousScrollForDragAtPoint:(CGPoint)dragLocation animated:(BOOL)animated {
+ if(dragLocation.y <= TUIScrollViewContinuousScrollDragBoundary || dragLocation.y >= (self.bounds.size.height - TUIScrollViewContinuousScrollDragBoundary)){
+ // note the drag offset
+ _dragScrollLocation = dragLocation;
+ // begin a continuous scroll
+ [self _startTimer:AnimationModeScrollContinuous];
+ }else{
+ [self endContinuousScrollAnimated:animated];
+ }
+}
+
+/**
+ * @brief Stop scrolling continuously for a drag
+ *
+ * This method is the counterpart to #beginContinuousScrollForDragAtPoint:animated:
+ *
+ * @param animated animate the scroll or not (this is currently ignored and the scroll is always animated)
+ */
+- (void)endContinuousScrollAnimated:(BOOL)animated {
+ if(_scrollViewFlags.animationMode == AnimationModeScrollContinuous){
+ [self _stopTimer];
+ }
+}
+
static float clampBounce(float x) {
x *= 0.4;
float m = 60 * 60;
@@ -468,7 +506,7 @@ - (void)_updateBounce
- (void)tick:(NSTimer *)timer
{
[self _updateBounce]; // can't do after _startBounce otherwise dt will be crazy
-
+
if(self.nsWindow == nil) {
NSLog(@"Warning: no window %d (should be 1)", x);
[self _stopTimer];
@@ -522,6 +560,28 @@ - (void)tick:(NSTimer *)timer
break;
}
+ case AnimationModeScrollContinuous: {
+ CGFloat direction;
+ CGFloat distance;
+
+ if(_dragScrollLocation.y <= TUIScrollViewContinuousScrollDragBoundary){
+ distance = MAX(0, MIN(TUIScrollViewContinuousScrollDragBoundary, _dragScrollLocation.y));
+ direction = 1;
+ }else if(_dragScrollLocation.y >= (self.bounds.size.height - TUIScrollViewContinuousScrollDragBoundary)){
+ distance = MAX(0, MIN(TUIScrollViewContinuousScrollDragBoundary, self.bounds.size.height - _dragScrollLocation.y));
+ direction = -1;
+ }else{
+ return; // no scrolling; outside drag boundary
+ }
+
+ CGPoint offset = _unroundedContentOffset;
+ CGFloat step = (1.0 - (distance / TUIScrollViewContinuousScrollDragBoundary)) * TUIScrollViewContinuousScrollRate;
+ CGPoint dest = CGPointMake(offset.x, offset.y + (step * direction));
+
+ [self setContentOffset:dest];
+
+ break;
+ }
}
}
View
34 lib/UIKit/TUITableView+Cell.h
@@ -0,0 +1,34 @@
+/*
+ Copyright 2011 Twitter, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this work except in compliance with the License.
+ You may obtain a copy of the License in the LICENSE file, or at:
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#import "TUITableView.h"
+
+/**
+ * @brief Exposes some internal table view methods to cells (primarily for drag-to-reorder support)
+ */
+@interface TUITableView (Cell)
+
+-(void)__mouseDownInCell:(TUITableViewCell *)cell offset:(CGPoint)offset event:(NSEvent *)event;
+-(void)__mouseUpInCell:(TUITableViewCell *)cell offset:(CGPoint)offset event:(NSEvent *)event;
+-(void)__mouseDraggedCell:(TUITableViewCell *)cell offset:(CGPoint)offset event:(NSEvent *)event;
+
+-(BOOL)__isDraggingCell;
+-(void)__beginDraggingCell:(TUITableViewCell *)cell offset:(CGPoint)offset location:(CGPoint)location;
+-(void)__updateDraggingCell:(TUITableViewCell *)cell offset:(CGPoint)offset location:(CGPoint)location;
+-(void)__endDraggingCell:(TUITableViewCell *)cell offset:(CGPoint)offset location:(CGPoint)location;
+
+@end
+
View
360 lib/UIKit/TUITableView+Cell.m
@@ -0,0 +1,360 @@
+/*
+ Copyright 2011 Twitter, Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this work except in compliance with the License.
+ You may obtain a copy of the License in the LICENSE file, or at:
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+
+#import "TUITableView+Cell.h"
+
+@interface TUITableView (CellPrivate)
+
+- (BOOL)_preLayoutCells;
+- (void)_layoutSectionHeaders:(BOOL)needLayout;
+- (void)_layoutCells:(BOOL)needLayout;
+
+@end
+
+@implementation TUITableView (Cell)
+
+/**
+ * @brief Mouse down in a cell
+ */
+-(void)__mouseDownInCell:(TUITableViewCell *)cell offset:(CGPoint)offset event:(NSEvent *)event {
+ [self __beginDraggingCell:cell offset:offset location:[[cell superview] localPointForEvent:event]];
+}
+
+/**
+ * @brief Mouse up in a cell
+ */
+-(void)__mouseUpInCell:(TUITableViewCell *)cell offset:(CGPoint)offset event:(NSEvent *)event {
+ [self __endDraggingCell:cell offset:offset location:[[cell superview] localPointForEvent:event]];
+}
+
+/**
+ * @brief A cell was dragged
+ *
+ * If reordering is permitted by the table, this will begin a move operation.
+ */
+-(void)__mouseDraggedCell:(TUITableViewCell *)cell offset:(CGPoint)offset event:(NSEvent *)event {
+ [self __updateDraggingCell:cell offset:offset location:[[cell superview] localPointForEvent:event]];
+}
+
+/**
+ * @brief Determine if we're dragging a cell or not
+ */
+-(BOOL)__isDraggingCell {
+ return _dragToReorderCell != nil && _currentDragToReorderIndexPath != nil;
+}
+
+/**
+ * @brief Begin dragging a cell
+ */
+-(void)__beginDraggingCell:(TUITableViewCell *)cell offset:(CGPoint)offset location:(CGPoint)location {
+
+ _currentDragToReorderLocation = location;
+ _currentDragToReorderMouseOffset = offset;
+
+ [_dragToReorderCell release];
+ _dragToReorderCell = [cell retain];
+
+ [_currentDragToReorderIndexPath release];
+ _currentDragToReorderIndexPath = nil;
+ [_previousDragToReorderIndexPath release];
+ _previousDragToReorderIndexPath = nil;
+
+}
+
+/**
+ * @brief Update cell dragging
+ */
+-(void)__updateDraggingCell:(TUITableViewCell *)cell offset:(CGPoint)offset location:(CGPoint)location {
+ BOOL animate = TRUE;
+
+ // note the location in any event
+ _currentDragToReorderLocation = location;
+ _currentDragToReorderMouseOffset = offset;
+
+ // make sure reordering is supported by our data source (this should probably be done only once somewhere)
+ if(self.dataSource == nil || ![self.dataSource respondsToSelector:@selector(tableView:moveRowAtIndexPath:toIndexPath:)]){
+ return; // reordering is not supported by the data source
+ }
+
+ // determine if reordering this cell is permitted or not via our data source (this should probably be done only once somewhere)
+ if(self.dataSource == nil || ![self.dataSource respondsToSelector:@selector(tableView:canMoveRowAtIndexPath:)] || ![self.dataSource tableView:self canMoveRowAtIndexPath:cell.indexPath]){
+ return; // reordering is not permitted
+ }
+
+ // initialize defaults on the first drag
+ if(_currentDragToReorderIndexPath == nil || _previousDragToReorderIndexPath == nil){
+ [_currentDragToReorderIndexPath release];
+ _currentDragToReorderIndexPath = [cell.indexPath retain];
+ [_previousDragToReorderIndexPath release];
+ _previousDragToReorderIndexPath = [cell.indexPath retain];
+ return; // just initialize on the first event
+ }
+
+ CGRect visible = [self visibleRect];
+ // dragged cell destination frame
+ CGRect dest = CGRectMake(0, roundf(MAX(visible.origin.y, MIN(visible.origin.y + visible.size.height - cell.frame.size.height, location.y + visible.origin.y - offset.y))), self.bounds.size.width, cell.frame.size.height);
+ // bring to front
+ [[cell superview] bringSubviewToFront:cell];
+ // move the cell
+ cell.frame = dest;
+
+ // constraint the location to the viewport
+ location = CGPointMake(location.x, MAX(0, MIN(visible.size.height, location.y)));
+ // scroll content if necessary (scroll view figures out whether it's necessary or not)
+ [self beginContinuousScrollForDragAtPoint:location animated:TRUE];
+
+ TUITableViewInsertionMethod insertMethod = TUITableViewInsertionMethodAtIndex;
+ TUIFastIndexPath *currentPath = nil;
+ NSInteger sectionIndex = -1;
+
+ // determine the current index path the cell is occupying
+ if((currentPath = [self indexPathForRowAtPoint:CGPointMake(location.x, location.y + visible.origin.y)]) == nil){
+ if((sectionIndex = [self indexOfSectionWithHeaderAtPoint:CGPointMake(location.x, location.y + visible.origin.y)]) > 0){
+ if(sectionIndex <= cell.indexPath.section){
+ // if we're on a section header (but not the first one, which can't move) which is above the origin
+ // index path we insert after the last index in the section above
+ NSInteger targetSectionIndex = sectionIndex - 1;
+ currentPath = [TUIFastIndexPath indexPathForRow:[self numberOfRowsInSection:targetSectionIndex] - 1 inSection:targetSectionIndex];
+ insertMethod = TUITableViewInsertionMethodAfterIndex;
+ }else{
+ // if we're on a section header below the origin index we insert before the first index in the
+ // section below
+ NSInteger targetSectionIndex = sectionIndex;
+ currentPath = [TUIFastIndexPath indexPathForRow:0 inSection:targetSectionIndex];
+ insertMethod = TUITableViewInsertionMethodBeforeIndex;
+ }
+ }
+ }
+
+ // make sure we have a valid current path before proceeding
+ if(currentPath == nil) return;
+
+ // allow the delegate to revise the proposed index path if it wants to
+ if(self.delegate != nil && [self.delegate respondsToSelector:@selector(tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath:)]){
+ TUIFastIndexPath *proposedPath = currentPath;
+ currentPath = [self.delegate tableView:self targetIndexPathForMoveFromRowAtIndexPath:cell.indexPath toProposedIndexPath:currentPath];
+ // revised index paths always use the "at" insertion method
+ switch([currentPath compare:proposedPath]){
+ case NSOrderedAscending:
+ case NSOrderedDescending:
+ insertMethod = TUITableViewInsertionMethodAtIndex;
+ break;
+ case NSOrderedSame:
+ default:
+ // do nothing
+ break;
+ }
+ }
+
+ // note the previous path
+ [_previousDragToReorderIndexPath release];
+ _previousDragToReorderIndexPath = [_currentDragToReorderIndexPath retain];
+ _previousDragToReorderInsertionMethod = _currentDragToReorderInsertionMethod;
+
+ // note the current path
+ [_currentDragToReorderIndexPath release];
+ _currentDragToReorderIndexPath = [currentPath retain];
+ _currentDragToReorderInsertionMethod = insertMethod;
+
+ // determine the current drag direction
+ NSComparisonResult currentDragDirection = (_previousDragToReorderIndexPath != nil) ? [currentPath compare:_previousDragToReorderIndexPath] : NSOrderedSame;
+
+ // ordered index paths for enumeration
+ TUIFastIndexPath *fromIndexPath = nil;
+ TUIFastIndexPath *toIndexPath = nil;
+
+ if(currentDragDirection == NSOrderedAscending){
+ fromIndexPath = currentPath;
+ toIndexPath = _previousDragToReorderIndexPath;
+ }else if(currentDragDirection == NSOrderedDescending){
+ fromIndexPath = _previousDragToReorderIndexPath;
+ toIndexPath = currentPath;
+ }else if(insertMethod != _previousDragToReorderInsertionMethod){
+ fromIndexPath = currentPath;
+ toIndexPath = currentPath;
+ }
+
+ // we now have the final destination index path. if it's not nil, update surrounding
+ // cells to make room for the dragged cell
+ if(currentPath != nil && fromIndexPath != nil && toIndexPath != nil){
+
+ // begin animations
+ if(animate){
+ [TUIView beginAnimations:NSStringFromSelector(_cmd) context:NULL];
+ }
+
+ // update section headers
+ for(NSInteger i = fromIndexPath.section; i <= toIndexPath.section; i++){
+ TUIView *headerView;
+ if(currentPath.section < i && i <= cell.indexPath.section){
+ // the current index path is above this section and this section is at or
+ // below the origin index path; shift our header down to make room
+ if((headerView = [self headerViewForSection:i]) != nil){
+ CGRect frame = [self rectForHeaderOfSection:i];
+ headerView.frame = CGRectMake(frame.origin.x, frame.origin.y - cell.frame.size.height, frame.size.width, frame.size.height);
+ }
+ }else if(currentPath.section >= i && i > cell.indexPath.section){
+ // the current index path is at or below this section and this section is
+ // below the origin index path; shift our header up to make room
+ if((headerView = [self headerViewForSection:i]) != nil){
+ CGRect frame = [self rectForHeaderOfSection:i];
+ headerView.frame = CGRectMake(frame.origin.x, frame.origin.y + cell.frame.size.height, frame.size.width, frame.size.height);
+ }
+ }else{
+ // restore the header to it's normal position
+ if((headerView = [self headerViewForSection:i]) != nil){
+ headerView.frame = [self rectForHeaderOfSection:i];
+ }
+ }
+ }
+
+ // update rows
+ [self enumerateIndexPathsFromIndexPath:fromIndexPath toIndexPath:toIndexPath withOptions:0 usingBlock:^(TUIFastIndexPath *indexPath, BOOL *stop) {
+ TUITableViewCell *displacedCell;
+ if((displacedCell = [self cellForRowAtIndexPath:indexPath]) != nil && ![displacedCell isEqual:cell]){
+ CGRect frame = [self rectForRowAtIndexPath:indexPath];
+ CGRect target;
+
+ if([indexPath isEqual:currentPath] && insertMethod == TUITableViewInsertionMethodAfterIndex){
+ // the visited index path is the current index path and the insertion method is "after";
+ // leave the cell where it is, the section header should shift out of the way instead
+ target = frame;
+ }else if([indexPath isEqual:currentPath] && insertMethod == TUITableViewInsertionMethodBeforeIndex){
+ // the visited index path is the current index path and the insertion method is "before";
+ // leave the cell where it is, the section header should shift out of the way instead
+ target = frame;
+ }else if([indexPath compare:currentPath] != NSOrderedAscending && [indexPath compare:cell.indexPath] == NSOrderedAscending){
+ // the visited index path is above the origin and below the current index path;
+ // shift the cell down by the height of the dragged cell
+ target = CGRectMake(frame.origin.x, frame.origin.y - cell.frame.size.height, frame.size.width, frame.size.height);
+ }else if([indexPath compare:currentPath] != NSOrderedDescending && [indexPath compare:cell.indexPath] == NSOrderedDescending){
+ // the visited index path is below the origin and above the current index path;
+ // shift the cell up by the height of the dragged cell
+ target = CGRectMake(frame.origin.x, frame.origin.y + cell.frame.size.height, frame.size.width, frame.size.height);
+ }else{
+ // the visited cell is outside the affected range and should be returned to its
+ // normal frame
+ target = frame;
+ }
+
+ // only animate if we actually need to
+ if(!CGRectEqualToRect(target, displacedCell.frame)){
+ displacedCell.frame = target;
+ }
+
+ }
+ }];
+
+ // commit animations
+ if(animate){
+ [TUIView commitAnimations];
+ }
+
+ }
+
+}
+
+/**
+ * @brief Finish dragging a cell
+ */
+-(void)__endDraggingCell:(TUITableViewCell *)cell offset:(CGPoint)offset location:(CGPoint)location {
+ BOOL animate = TRUE;
+
+ // cancel our continuous scroll
+ [self endContinuousScrollAnimated:TRUE];
+
+ // finalize drag to reorder if we have a drag index
+ if(_currentDragToReorderIndexPath != nil){
+ TUIFastIndexPath *targetIndexPath;
+
+ switch(_currentDragToReorderInsertionMethod){
+ case TUITableViewInsertionMethodBeforeIndex:
+ // insert "before" is equivalent to insert "at" as subsequent indexes are shifted down to
+ // accommodate the insert. the distinction is only useful for presentation.
+ targetIndexPath = _currentDragToReorderIndexPath;
+ break;
+ case TUITableViewInsertionMethodAfterIndex:
+ targetIndexPath = [TUIFastIndexPath indexPathForRow:_currentDragToReorderIndexPath.row + 1 inSection:_currentDragToReorderIndexPath.section];
+ break;
+ case TUITableViewInsertionMethodAtIndex:
+ default:
+ targetIndexPath = _currentDragToReorderIndexPath;
+ break;
+ }
+
+ // only update the data source if the drag ended on a different index path
+ // than it started; otherwise just clean up the view
+ if(![targetIndexPath isEqual:cell.indexPath]){
+ // notify our data source that the row will be reordered
+ if(self.dataSource != nil && [self.dataSource respondsToSelector:@selector(tableView:moveRowAtIndexPath:toIndexPath:)]){
+ [self.dataSource tableView:self moveRowAtIndexPath:cell.indexPath toIndexPath:targetIndexPath];
+ }
+ }
+
+ // compute the final cell destination frame
+ CGRect frame = [self rectForRowAtIndexPath:_currentDragToReorderIndexPath];
+ // adjust if necessary based on the insertion method
+ switch(_currentDragToReorderInsertionMethod){
+ case TUITableViewInsertionMethodBeforeIndex:
+ frame = CGRectMake(frame.origin.x, frame.origin.y + cell.frame.size.height, frame.size.width, frame.size.height);
+ break;
+ case TUITableViewInsertionMethodAfterIndex:
+ frame = CGRectMake(frame.origin.x, frame.origin.y - cell.frame.size.height, frame.size.width, frame.size.height);
+ break;
+ case TUITableViewInsertionMethodAtIndex:
+ default:
+ // do nothing. this case is just here to avoid complier complaints...
+ break;
+ }
+
+ // move the cell to its final frame and layout to make sure all the internal caching/geometry
+ // stuff is consistent.
+ if(animate && !CGRectEqualToRect(cell.frame, frame)){
+ // disable user interaction until the animation has completed and the table has reloaded
+ [self setUserInteractionEnabled:FALSE];
+ [TUIView animateWithDuration:0.2
+ animations:^ { cell.frame = frame; }
+ completion:^(BOOL finished) {
+ // reload the table when we're done
+ if(finished) [self reloadData];
+ // restore user interactivity
+ [self setUserInteractionEnabled:TRUE];
+ }
+ ];
+ }else{
+ cell.frame = frame;
+ [self reloadData];
+ }
+
+ // clear state
+ [_currentDragToReorderIndexPath release];
+ _currentDragToReorderIndexPath = nil;
+
+ }
+
+ [_previousDragToReorderIndexPath release];
+ _previousDragToReorderIndexPath = nil;
+
+ [_dragToReorderCell release];
+ _dragToReorderCell = nil;
+
+ _currentDragToReorderLocation = CGPointZero;
+ _currentDragToReorderMouseOffset = CGPointZero;
+
+}
+
+@end
+
View
37 lib/UIKit/TUITableView.h
@@ -29,6 +29,12 @@ typedef enum {
TUITableViewScrollPositionToVisible, // currently the only supported arg
} TUITableViewScrollPosition;
+typedef enum {
+ TUITableViewInsertionMethodBeforeIndex = NSOrderedAscending,
+ TUITableViewInsertionMethodAtIndex = NSOrderedSame,
+ TUITableViewInsertionMethodAfterIndex = NSOrderedDescending
+} TUITableViewInsertionMethod;
+
@class TUITableViewCell;
@protocol TUITableViewDataSource;
@@ -47,6 +53,13 @@ typedef enum {
- (BOOL)tableView:(TUITableView*)tableView shouldSelectRowAtIndexPath:(TUIFastIndexPath*)indexPath forEvent:(NSEvent*)event; // YES, if not implemented
+// the following are good places to update or restore state (such as selection) when the table data reloads
+- (void)tableViewWillReloadData:(TUITableView *)tableView;
+- (void)tableViewDidReloadData:(TUITableView *)tableView;
+
+// the following is optional for row reordering
+- (TUIFastIndexPath *)tableView:(TUITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(TUIFastIndexPath *)fromPath toProposedIndexPath:(TUIFastIndexPath *)proposedPath;
+
@end
@interface TUITableView : TUIScrollView
@@ -56,7 +69,7 @@ typedef enum {
NSArray * _sectionInfo;
TUIView * _pullDownView;
- TUIView *_headerView;
+ TUIView * _headerView;
CGSize _lastSize;
CGFloat _contentHeight;
@@ -71,6 +84,15 @@ typedef enum {
TUIFastIndexPath * _keepVisibleIndexPathForReload;
CGFloat _relativeOffsetForReload;
+ // drag-to-reorder state
+ TUITableViewCell * _dragToReorderCell;
+ CGPoint _currentDragToReorderLocation;
+ CGPoint _currentDragToReorderMouseOffset;
+ TUIFastIndexPath * _currentDragToReorderIndexPath;
+ TUITableViewInsertionMethod _currentDragToReorderInsertionMethod;
+ TUIFastIndexPath * _previousDragToReorderIndexPath;
+ TUITableViewInsertionMethod _previousDragToReorderInsertionMethod;
+
struct {
unsigned int animateSelectionChanges:1;
unsigned int forceSaveScrollPosition:1;
@@ -81,6 +103,7 @@ typedef enum {
unsigned int delegateTableViewWillDisplayCellForRowAtIndexPath:1;
unsigned int maintainContentOffsetAfterReload:1;
} _tableFlags;
+
}
- (id)initWithFrame:(CGRect)frame style:(TUITableViewStyle)style; // must specify style at creation. -initWithFrame: calls this with UITableViewStylePlain
@@ -102,13 +125,21 @@ typedef enum {
- (NSInteger)numberOfRowsInSection:(NSInteger)section;
- (CGRect)rectForHeaderOfSection:(NSInteger)section;
+- (CGRect)rectForSection:(NSInteger)section;
- (CGRect)rectForRowAtIndexPath:(TUIFastIndexPath *)indexPath;
- (NSIndexSet *)indexesOfSectionsInRect:(CGRect)rect;
- (NSIndexSet *)indexesOfSectionHeadersInRect:(CGRect)rect;
- (TUIFastIndexPath *)indexPathForCell:(TUITableViewCell *)cell; // returns nil if cell is not visible
- (NSArray *)indexPathsForRowsInRect:(CGRect)rect; // returns nil if rect not valid
+- (TUIFastIndexPath *)indexPathForRowAtPoint:(CGPoint)point;
+- (NSInteger)indexOfSectionWithHeaderAtPoint:(CGPoint)point;
+
+- (void)enumerateIndexPathsUsingBlock:(void (^)(TUIFastIndexPath *indexPath, BOOL *stop))block;
+- (void)enumerateIndexPathsWithOptions:(NSEnumerationOptions)options usingBlock:(void (^)(TUIFastIndexPath *indexPath, BOOL *stop))block;
+- (void)enumerateIndexPathsFromIndexPath:(TUIFastIndexPath *)fromIndexPath toIndexPath:(TUIFastIndexPath *)toIndexPath withOptions:(NSEnumerationOptions)options usingBlock:(void (^)(TUIFastIndexPath *indexPath, BOOL *stop))block;
+- (TUIView *)headerViewForSection:(NSInteger)section;
- (TUITableViewCell *)cellForRowAtIndexPath:(TUIFastIndexPath *)indexPath; // returns nil if cell is not visible or index path is out of range
- (NSArray *)visibleCells; // no particular order
- (NSArray *)sortedVisibleCells; // top to bottom
@@ -151,6 +182,10 @@ typedef enum {
- (TUITableViewCell *)tableView:(TUITableView *)tableView headerViewForSection:(NSInteger)section;
+// the following are required to support row reordering
+- (BOOL)tableView:(TUITableView *)tableView canMoveRowAtIndexPath:(TUIFastIndexPath *)indexPath;
+- (void)tableView:(TUITableView *)tableView moveRowAtIndexPath:(TUIFastIndexPath *)fromIndexPath toIndexPath:(TUIFastIndexPath *)toIndexPath;
+
/**
Default is 1 if not implemented
*/
View
196 lib/UIKit/TUITableView.m
@@ -15,6 +15,7 @@
*/
#import "TUITableView.h"
+#import "TUITableView+Cell.h"
#import "TUINSView.h"
typedef struct {
@@ -173,6 +174,9 @@ - (void)dealloc
[_indexPathShouldBeFirstResponder release];
[_keepVisibleIndexPathForReload release];
[_pullDownView release];
+ [_dragToReorderCell release];
+ [_currentDragToReorderIndexPath release];
+ [_previousDragToReorderIndexPath release];
[_headerView release];
[super dealloc];
}
@@ -230,6 +234,18 @@ - (CGRect)rectForHeaderOfSection:(NSInteger)section {
return CGRectZero;
}
+- (CGRect)rectForSection:(NSInteger)section
+{
+ if(section >= 0 && section < [_sectionInfo count]){
+ TUITableViewSection *s = [_sectionInfo objectAtIndex:section];
+ CGFloat offset = [s sectionOffset];
+ CGFloat height = [s sectionHeight];
+ CGFloat y = _contentHeight - offset - height;
+ return CGRectMake(0, y, self.bounds.size.width, height);
+ }
+ return CGRectZero;
+}
+
- (CGRect)rectForRowAtIndexPath:(TUIFastIndexPath *)indexPath
{
NSInteger section = indexPath.section;
@@ -304,6 +320,22 @@ - (TUITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier
return nil;
}
+/**
+ * @brief Obtain the header view for the specified section
+ *
+ * If the section has no header, nil is returned.
+ *
+ * @param section the section
+ * @return section header
+ */
+- (TUIView *)headerViewForSection:(NSInteger)section {
+ if(section >= 0 && section < [_sectionInfo count]){
+ return [(TUITableViewSection *)[_sectionInfo objectAtIndex:section] headerView];
+ }else{
+ return nil;
+ }
+}
+
- (TUITableViewCell *)cellForRowAtIndexPath:(TUIFastIndexPath *)indexPath // returns nil if cell is not visible or index path is out of range
{
return [_visibleItems objectForKey:indexPath];
@@ -359,7 +391,7 @@ - (NSIndexSet *)indexesOfSectionsInRect:(CGRect)rect
NSMutableIndexSet *indexes = [[NSMutableIndexSet alloc] init];
for(int i = 0; i < [_sectionInfo count]; i++) {
- if(CGRectIntersectsRect([self rectForHeaderOfSection:i], rect)){
+ if(CGRectIntersectsRect([self rectForSection:i], rect)){
[indexes addIndex:i];
}
}
@@ -406,6 +438,107 @@ - (NSArray *)indexPathsForRowsInRect:(CGRect)rect
return indexPaths;
}
+/**
+ * @brief Obtain the index path of the row at the specified point
+ *
+ * If the point is not valid or no row exists at that point, nil is
+ * returned.
+ *
+ * @param point location in the table view
+ * @return index path of the row at @p point
+ */
+- (TUIFastIndexPath *)indexPathForRowAtPoint:(CGPoint)point {
+
+ NSInteger sectionIndex = 0;
+ for(TUITableViewSection *section in _sectionInfo){
+ for(NSInteger row = 0; row < [section numberOfRows]; row++){
+ TUIFastIndexPath *indexPath = [TUIFastIndexPath indexPathForRow:row inSection:sectionIndex];
+ CGRect cellRect = [self rectForRowAtIndexPath:indexPath];
+ if(CGRectContainsPoint(cellRect, point)){
+ return indexPath;
+ }
+ }
+ ++sectionIndex;
+ }
+
+ return nil;
+}
+
+/**
+ * @brief Obtain the index of a section whose header is at the specified point
+ *
+ * If the point is not valid or no header exists at that point, a negative value
+ * is returned.
+ *
+ * @param point location in the table view
+ * @return index of the section whose header is at @p point
+ */
+- (NSInteger)indexOfSectionWithHeaderAtPoint:(CGPoint)point {
+
+ NSInteger sectionIndex = 0;
+ for(TUITableViewSection *section in _sectionInfo){
+ TUIView *headerView;
+ if((headerView = section.headerView) != nil){
+ CGFloat offset = [section sectionOffset];
+ CGFloat height = [section headerHeight];
+ CGFloat y = _contentHeight - offset - height;
+ CGRect frame = CGRectMake(0, y, self.bounds.size.width, height);
+ if(CGRectContainsPoint(frame, point)){
+ return sectionIndex;
+ }
+ }
+ sectionIndex++;
+ }
+
+ return -1;
+}
+
+/**
+ * @brief Enumerate index paths
+ * @see #enumerateIndexPathsFromIndexPath:toIndexPath:withOptions:usingBlock:
+ */
+- (void)enumerateIndexPathsUsingBlock:(void (^)(TUIFastIndexPath *indexPath, BOOL *stop))block {
+ [self enumerateIndexPathsFromIndexPath:nil toIndexPath:nil withOptions:0 usingBlock:block];
+}
+
+/**
+ * @brief Enumerate index paths
+ * @see #enumerateIndexPathsFromIndexPath:toIndexPath:withOptions:usingBlock:
+ */
+- (void)enumerateIndexPathsWithOptions:(NSEnumerationOptions)options usingBlock:(void (^)(TUIFastIndexPath *indexPath, BOOL *stop))block {
+ [self enumerateIndexPathsFromIndexPath:nil toIndexPath:nil withOptions:options usingBlock:block];
+}
+
+/**
+ * @brief Enumerate index paths
+ *
+ * The provided block is repeatedly invoked with each valid index path between
+ * the specified bounds. Both bounding index paths are inclusive.
+ *
+ * @param fromIndexPath the index path to begin enumerating at or nil to begin at the first index path
+ * @param toIndexPath the index path to stop enumerating at or nil to stop at the last index path
+ * @param options enumeration options (not currently supported; pass 0)
+ * @param block the block to enumerate with
+ */
+- (void)enumerateIndexPathsFromIndexPath:(TUIFastIndexPath *)fromIndexPath toIndexPath:(TUIFastIndexPath *)toIndexPath withOptions:(NSEnumerationOptions)options usingBlock:(void (^)(TUIFastIndexPath *indexPath, BOOL *stop))block {
+ NSInteger sectionLowerBound = (fromIndexPath != nil) ? fromIndexPath.section : 0;
+ NSInteger sectionUpperBound = (toIndexPath != nil) ? toIndexPath.section : [self numberOfSections] - 1;
+ NSInteger rowLowerBound = (fromIndexPath != nil) ? fromIndexPath.row : 0;
+ NSInteger rowUpperBound = (toIndexPath != nil) ? toIndexPath.row : -1;
+
+ NSInteger irow = rowLowerBound; // start at the lower bound row for the first iteration...
+ for(NSInteger i = sectionLowerBound; i < [self numberOfSections] && i <= sectionUpperBound /* inclusive */; i++){
+ NSInteger rowCount = [self numberOfRowsInSection:i];
+ for(NSInteger j = irow; j < rowCount && j <= ((rowUpperBound < 0 || i < sectionUpperBound) ? rowCount - 1 : rowUpperBound) /* inclusive */; j++){
+ BOOL stop = FALSE;
+ block([TUIFastIndexPath indexPathForRow:j inSection:i], &stop);
+ if(stop) return;
+ }
+ irow = 0; // ...then use zero for subsequent iterations
+ }
+
+}
+
- (TUIFastIndexPath *)_topVisibleIndexPath
{
TUIFastIndexPath *topVisibleIndex = nil;
@@ -425,6 +558,13 @@ - (void)setContentOffset:(CGPoint)p
{
_tableFlags.didFirstLayout = 1; // prevent the auto-scroll-to-top during the first layout
[super setContentOffset:p];
+
+ // if we're currently dragging we need to update the drag operation since the content under
+ // the mouse has moved; we just call update again with the last mouse location
+ if([self __isDraggingCell]){
+ [self __updateDraggingCell:_dragToReorderCell offset:_currentDragToReorderMouseOffset location:_currentDragToReorderLocation];
+ }
+
}
- (void)setPullDownView:(TUIView *)p
@@ -603,9 +743,12 @@ - (void)_layoutCells:(BOOL)visibleCellsNeedRelayout
// remove offscreen cells
for(TUIFastIndexPath *i in indexPathsToRemove) {
TUITableViewCell *cell = [self cellForRowAtIndexPath:i];
- [self _enqueueReusableCell:cell];
- [cell removeFromSuperview];
- [_visibleItems removeObjectForKey:i];
+ // don't reuse the dragged cell
+ if(_dragToReorderCell == nil || ![cell isEqual:_dragToReorderCell]){
+ [self _enqueueReusableCell:cell];
+ [cell removeFromSuperview];
+ [_visibleItems removeObjectForKey:i];
+ }
}
// add new cells
@@ -623,16 +766,27 @@ - (void)_layoutCells:(BOOL)visibleCellsNeedRelayout
} else {
[cell setSelected:NO animated:NO];
}
+
[self addSubview:cell];
+
if([_indexPathShouldBeFirstResponder isEqual:i]) {
- [self.nsWindow makeFirstResponderIfNotAlreadyInResponderChain:cell withFutureRequestToken:_futureMakeFirstResponderToken];
+ // only make cells first responder if they accept it
+ if([cell acceptsFirstResponder]){
+ [self.nsWindow makeFirstResponderIfNotAlreadyInResponderChain:cell withFutureRequestToken:_futureMakeFirstResponderToken];
+ }
[_indexPathShouldBeFirstResponder release];
_indexPathShouldBeFirstResponder = nil;
}
+
[_visibleItems setObject:cell forKey:i];
}
}
+ // if we have a dragged cell, make sure it's on top of the newly added cells
+ if([indexPathsToAdd count] > 0 && _dragToReorderCell != nil){
+ [[_dragToReorderCell superview] bringSubviewToFront:_dragToReorderCell];
+ }
+
if(_headerView) {
CGSize s = self.contentSize;
CGRect headerViewRect = CGRectMake(0, s.height - _headerView.frame.size.height, visible.size.width, _headerView.frame.size.height);
@@ -688,6 +842,12 @@ - (void)reloadDataMaintainingVisibleIndexPath:(TUIFastIndexPath *)indexPath rela
- (void)reloadData
{
+
+ // notify our delegate we're about to reload the table
+ if(self.delegate != nil && [self.delegate respondsToSelector:@selector(tableViewWillReloadData:)]){
+ [self.delegate tableViewWillReloadData:self];
+ }
+
// need to recycle all visible cells, have them be regenerated on layoutSubviews
// because the same cells might have different content
for(TUIFastIndexPath *i in _visibleItems) {
@@ -695,12 +855,35 @@ - (void)reloadData
[self _enqueueReusableCell:cell];
[cell removeFromSuperview];
}
+
+ // if we have a dragged cell, clear it
+ [_dragToReorderCell release];
+ _dragToReorderCell = nil;
+
+ // clear visible cells
[_visibleItems removeAllObjects];
+ // remove any visible headers, they should be re-added when the table is laid out
+ for(TUITableViewSection *section in _sectionInfo){
+ TUIView *headerView;
+ if((headerView = [section headerView]) != nil){
+ [headerView removeFromSuperview];
+ }
+ }
+
+ // clear visible section headers
+ [_visibleSectionHeaders removeAllIndexes];
+
[_sectionInfo release];
_sectionInfo = nil; // will be regenerated on next layout
[self layoutSubviews];
+
+ // notify our delegate the table view has been reloaded
+ if(self.delegate != nil && [self.delegate respondsToSelector:@selector(tableViewDidReloadData:)]){
+ [self.delegate tableViewDidReloadData:self];
+ }
+
}
- (void)layoutSubviews
@@ -770,7 +953,8 @@ - (TUIFastIndexPath *)indexPathForLastRow
- (void)_makeRowAtIndexPathFirstResponder:(TUIFastIndexPath *)indexPath
{
TUITableViewCell *cell = [self cellForRowAtIndexPath:indexPath];
- if(cell) {
+ // only cells that accept first responder should be made first responder
+ if(cell && [cell acceptsFirstResponder]) {
[self.nsWindow makeFirstResponderIfNotAlreadyInResponderChain:cell];
} else {
[_indexPathShouldBeFirstResponder release];
View
5 lib/UIKit/TUITableViewCell.h
@@ -25,12 +25,15 @@ typedef enum {
@interface TUITableViewCell : TUIView
{
- NSString *_reuseIdentifier;
+
+ NSString * _reuseIdentifier;
+ CGPoint _mouseOffset;
struct {
unsigned int highlighted:1;
unsigned int selected:1;
} _tableViewCellFlags;
+
}
- (id)initWithStyle:(TUITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier;
View
30 lib/UIKit/TUITableViewCell.m
@@ -16,6 +16,7 @@
#import "TUITableViewCell.h"
#import "TUITableView.h"
+#import "TUITableView+Cell.h"
@implementation TUITableViewCell
@@ -72,21 +73,46 @@ - (BOOL)acceptsFirstMouse:(NSEvent *)event
return NO;
}
+/**
+ * @brief Accept first responder by default
+ */
+-(BOOL)acceptsFirstResponder {
+ return TRUE;
+}
+
- (void)mouseDown:(NSEvent *)event
{
- [super mouseDown:event];
-
+ // note the initial mouse location for dragging
+ _mouseOffset = [self localPointForLocationInWindow:[event locationInWindow]];
+ // notify our table view of the event
+ [self.tableView __mouseDownInCell:self offset:_mouseOffset event:event];
+
TUITableView *tableView = self.tableView;
+ [tableView selectRowAtIndexPath:self.indexPath animated:tableView.animateSelectionChanges scrollPosition:TUITableViewScrollPositionNone];
+ [super mouseDown:event]; // may make the text renderer first responder, so we want to do the selection before this
+
if(![tableView.delegate respondsToSelector:@selector(tableView:shouldSelectRowAtIndexPath:forEvent:)] || [tableView.delegate tableView:tableView shouldSelectRowAtIndexPath:self.indexPath forEvent:event]){
[tableView selectRowAtIndexPath:self.indexPath animated:tableView.animateSelectionChanges scrollPosition:TUITableViewScrollPositionNone];
_tableViewCellFlags.highlighted = 1;
[self setNeedsDisplay];
}
+
+}
+
+/**
+ * @brief The table cell was dragged
+ */
+-(void)mouseDragged:(NSEvent *)event {
+ // notify our table view of the event
+ [self.tableView __mouseDraggedCell:self offset:_mouseOffset event:event];
}
- (void)mouseUp:(NSEvent *)event
{
[super mouseUp:event];
+ // notify our table view of the event
+ [self.tableView __mouseUpInCell:self offset:_mouseOffset event:event];
+
_tableViewCellFlags.highlighted = 0;
[self setNeedsDisplay];
Something went wrong with that request. Please try again.