OLD | NEW |
(Empty) | |
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/ui/bookmarks/bookmark_menu_view.h" |
| 6 |
| 7 #include <memory> |
| 8 |
| 9 #include "base/mac/foundation_util.h" |
| 10 #include "base/mac/objc_property_releaser.h" |
| 11 #include "base/mac/scoped_nsobject.h" |
| 12 #include "components/bookmarks/browser/bookmark_model.h" |
| 13 #include "components/bookmarks/browser/bookmark_model_observer.h" |
| 14 #include "ios/chrome/browser/bookmarks/bookmark_model_factory.h" |
| 15 #include "ios/chrome/browser/bookmarks/bookmarks_utils.h" |
| 16 #include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
| 17 #include "ios/chrome/browser/experimental_flags.h" |
| 18 #import "ios/chrome/browser/ui/bookmarks/bookmark_menu_cell.h" |
| 19 #import "ios/chrome/browser/ui/bookmarks/bookmark_menu_item.h" |
| 20 #include "ios/chrome/browser/ui/bookmarks/bookmark_model_bridge_observer.h" |
| 21 #import "ios/chrome/browser/ui/bookmarks/bookmark_utils_ios.h" |
| 22 #include "ios/chrome/browser/ui/ui_util.h" |
| 23 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 24 #include "ios/chrome/grit/ios_strings.h" |
| 25 #import "ios/third_party/material_components_ios/src/components/Ink/src/Material
Ink.h" |
| 26 #include "ui/base/l10n/l10n_util.h" |
| 27 #include "ui/base/models/tree_node_iterator.h" |
| 28 |
| 29 using bookmarks::BookmarkNode; |
| 30 |
| 31 @interface BookmarkMenuView ()<BookmarkModelBridgeObserver, |
| 32 MDCInkTouchControllerDelegate, |
| 33 UITableViewDataSource, |
| 34 UITableViewDelegate> { |
| 35 // A bridge to receive bookmark model observer callbacks. |
| 36 std::unique_ptr<bookmarks::BookmarkModelBridge> _modelBridge; |
| 37 |
| 38 base::mac::ObjCPropertyReleaser _propertyReleaser_BookmarkMenuView; |
| 39 } |
| 40 @property(nonatomic, assign) bookmarks::BookmarkModel* bookmarkModel; |
| 41 // This array directly represents the rows that show up in the table. |
| 42 @property(nonatomic, retain) NSMutableArray* menuItems; |
| 43 // The primary menu item is blue instead of gray. |
| 44 @property(nonatomic, retain) BookmarkMenuItem* primaryMenuItem; |
| 45 @property(nonatomic, assign) ios::ChromeBrowserState* browserState; |
| 46 @property(nonatomic, retain) UITableView* tableView; |
| 47 @property(nonatomic, retain) MDCInkTouchController* inkTouchController; |
| 48 |
| 49 // Updates the data model, and the UI. |
| 50 - (void)reloadData; |
| 51 |
| 52 // Creates the views for this class. |
| 53 - (void)createViews; |
| 54 |
| 55 @end |
| 56 |
| 57 @implementation BookmarkMenuView |
| 58 @synthesize bookmarkModel = _bookmarkModel; |
| 59 @synthesize delegate = _delegate; |
| 60 @synthesize menuItems = _menuItems; |
| 61 @synthesize primaryMenuItem = _primaryMenuItem; |
| 62 @synthesize browserState = _browserState; |
| 63 @synthesize tableView = _tableView; |
| 64 @synthesize inkTouchController = _inkTouchController; |
| 65 |
| 66 - (id)initWithFrame:(CGRect)frame { |
| 67 NOTREACHED(); |
| 68 return nil; |
| 69 } |
| 70 |
| 71 - (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState |
| 72 frame:(CGRect)frame { |
| 73 self = [super initWithFrame:frame]; |
| 74 if (self) { |
| 75 _propertyReleaser_BookmarkMenuView.Init(self, [BookmarkMenuView class]); |
| 76 |
| 77 _browserState = browserState; |
| 78 |
| 79 // Set up connection to the BookmarkModel. |
| 80 _bookmarkModel = |
| 81 ios::BookmarkModelFactory::GetForBrowserState(_browserState); |
| 82 // Set up observers. |
| 83 _modelBridge.reset( |
| 84 new bookmarks::BookmarkModelBridge(self, _bookmarkModel)); |
| 85 |
| 86 self.menuItems = [NSMutableArray array]; |
| 87 |
| 88 [self createViews]; |
| 89 } |
| 90 return self; |
| 91 } |
| 92 |
| 93 - (void)dealloc { |
| 94 self.tableView.delegate = nil; |
| 95 self.tableView.dataSource = nil; |
| 96 [super dealloc]; |
| 97 } |
| 98 |
| 99 - (void)createViews { |
| 100 // Make the table view. |
| 101 self.tableView = base::scoped_nsobject<UITableView>( |
| 102 [[UITableView alloc] initWithFrame:self.bounds]); |
| 103 [self addSubview:self.tableView]; |
| 104 self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; |
| 105 self.tableView.delegate = self; |
| 106 self.tableView.dataSource = self; |
| 107 self.tableView.scrollsToTop = NO; |
| 108 [self reloadData]; |
| 109 |
| 110 // Set up ink touch controller. |
| 111 base::scoped_nsobject<MDCInkTouchController> inkTouchController( |
| 112 [[MDCInkTouchController alloc] initWithView:self.tableView]); |
| 113 self.inkTouchController = inkTouchController; |
| 114 self.inkTouchController.delegate = self; |
| 115 self.inkTouchController.delaysInkSpread = YES; |
| 116 } |
| 117 |
| 118 - (void)layoutSubviews { |
| 119 [super layoutSubviews]; |
| 120 self.tableView.frame = self.bounds; |
| 121 } |
| 122 |
| 123 - (void)reloadData { |
| 124 if (!self.bookmarkModel->loaded()) |
| 125 return; |
| 126 |
| 127 BookmarkMenuItem* primaryItem = [self.primaryMenuItem parentItem]; |
| 128 |
| 129 [self.menuItems removeAllObjects]; |
| 130 |
| 131 const BookmarkNode* mobileBookmarks = self.bookmarkModel->mobile_node(); |
| 132 const BookmarkNode* bookmarkBar = self.bookmarkModel->bookmark_bar_node(); |
| 133 const BookmarkNode* otherBookmarks = self.bookmarkModel->other_node(); |
| 134 |
| 135 // The first section is always visible. |
| 136 base::scoped_nsobject<NSMutableArray> topSection( |
| 137 [[NSMutableArray alloc] init]); |
| 138 [self.menuItems addObject:topSection]; |
| 139 |
| 140 if (experimental_flags::IsAllBookmarksEnabled()) { |
| 141 // All Items is always visible. |
| 142 [topSection addObject:[BookmarkMenuItem allMenuItem]]; |
| 143 } |
| 144 // Bookmarks Bar, Mobile Bookmarks and Other Bookmarks are special folders and |
| 145 // are shown at the top if they contain anything. |
| 146 if (!mobileBookmarks->empty() || |
| 147 !experimental_flags::IsAllBookmarksEnabled()) { |
| 148 [topSection |
| 149 addObject:[BookmarkMenuItem folderMenuItemForNode:mobileBookmarks |
| 150 rootAncestor:mobileBookmarks]]; |
| 151 } |
| 152 if (!bookmarkBar->empty()) { |
| 153 [topSection addObject:[BookmarkMenuItem folderMenuItemForNode:bookmarkBar |
| 154 rootAncestor:bookmarkBar]]; |
| 155 } |
| 156 if (!otherBookmarks->empty()) { |
| 157 [topSection |
| 158 addObject:[BookmarkMenuItem folderMenuItemForNode:otherBookmarks |
| 159 rootAncestor:otherBookmarks]]; |
| 160 } |
| 161 |
| 162 // The second section contains all the top level folders (except for the |
| 163 // permanent nodes). |
| 164 base::scoped_nsobject<NSMutableArray> folderSection( |
| 165 [[NSMutableArray alloc] init]); |
| 166 std::vector<const BookmarkNode*> rootLevelFolders = |
| 167 RootLevelFolders(self.bookmarkModel); |
| 168 bookmark_utils_ios::SortFolders(&rootLevelFolders); |
| 169 for (auto node : rootLevelFolders) { |
| 170 [folderSection addObject:[BookmarkMenuItem folderMenuItemForNode:node |
| 171 rootAncestor:node]]; |
| 172 } |
| 173 if ([folderSection count]) { |
| 174 // Add the title and the divider at the top of the section. |
| 175 [folderSection |
| 176 insertObject:[BookmarkMenuItem sectionMenuItemWithTitle: |
| 177 l10n_util::GetNSString( |
| 178 IDS_IOS_BOOKMARK_FOLDERS_LABEL)] |
| 179 atIndex:0]; |
| 180 [folderSection insertObject:[BookmarkMenuItem dividerMenuItem] atIndex:0]; |
| 181 [self.menuItems addObject:folderSection]; |
| 182 } |
| 183 |
| 184 // If the currently selected menuitem is no longer present in the menu, then |
| 185 // select the first item in the top section instead. |
| 186 if (![topSection containsObject:primaryItem] && |
| 187 ![folderSection containsObject:primaryItem]) { |
| 188 self.primaryMenuItem = [topSection firstObject]; |
| 189 [self.delegate bookmarkMenuView:self selectedMenuItem:self.primaryMenuItem]; |
| 190 } |
| 191 |
| 192 [self.tableView reloadData]; |
| 193 } |
| 194 |
| 195 - (BookmarkMenuItem*)defaultMenuItem { |
| 196 // The first item in the first section. |
| 197 DCHECK([[self.menuItems firstObject] firstObject]); |
| 198 return [[self.menuItems firstObject] firstObject]; |
| 199 } |
| 200 |
| 201 - (BookmarkMenuItem*)menuItemAtIndexPath:(NSIndexPath*)indexPath { |
| 202 return self.menuItems[indexPath.section][indexPath.row]; |
| 203 } |
| 204 |
| 205 #pragma mark UIView method |
| 206 |
| 207 - (void)didMoveToSuperview { |
| 208 [super didMoveToSuperview]; |
| 209 // The background color depends on where in the view hierachy the menu is. |
| 210 // For example, the menu may be moved to a slide over panel if the |
| 211 // horizontal size class changes from regular to compact. |
| 212 self.tableView.backgroundColor = bookmark_utils_ios::menuBackgroundColor(); |
| 213 } |
| 214 |
| 215 #pragma mark BookmarkModelBridgeObserver |
| 216 |
| 217 - (void)bookmarkModelLoaded { |
| 218 [self reloadData]; |
| 219 } |
| 220 |
| 221 - (void)bookmarkNodeChanged:(const BookmarkNode*)bookmarkNode { |
| 222 [self reloadData]; |
| 223 } |
| 224 |
| 225 - (void)bookmarkNodeChildrenChanged:(const BookmarkNode*)bookmarkNode { |
| 226 [self reloadData]; |
| 227 } |
| 228 |
| 229 - (void)bookmarkNode:(const BookmarkNode*)bookmarkNode |
| 230 movedFromParent:(const BookmarkNode*)oldParent |
| 231 toParent:(const BookmarkNode*)newParent { |
| 232 if (self.primaryMenuItem.type == bookmarks::MenuItemFolder && |
| 233 bookmarkNode->is_folder()) { |
| 234 // Checking which folder moved and if the current folder was implicated is |
| 235 // complicated and not worth the effort. Just rebuild a new primaryMenu item |
| 236 // unconditionally, this is simpler. |
| 237 const BookmarkNode* currentFolder = self.primaryMenuItem.folder; |
| 238 BookmarkMenuItem* menuItem = [BookmarkMenuItem |
| 239 folderMenuItemForNode:currentFolder |
| 240 rootAncestor:RootLevelFolderForNode(currentFolder, |
| 241 self.bookmarkModel)]; |
| 242 if (menuItem != self.primaryMenuItem) { |
| 243 self.primaryMenuItem = menuItem; |
| 244 [self.delegate bookmarkMenuView:self |
| 245 selectedMenuItem:self.primaryMenuItem]; |
| 246 } |
| 247 } |
| 248 [self reloadData]; |
| 249 } |
| 250 |
| 251 - (void)bookmarkNodeDeleted:(const BookmarkNode*)node |
| 252 fromFolder:(const BookmarkNode*)parentFolder { |
| 253 // If the current folder or one of its ancestor has been deleted, the |
| 254 // selection needs to move up to a non deleted ancestor. This check is made |
| 255 // more complex as by the time this method is called |node| is no longer in |
| 256 // the hierarchy : its parent is already set to null. |
| 257 |
| 258 if (self.primaryMenuItem.type != bookmarks::MenuItemFolder) { |
| 259 // If the object currently selected is not a folder, just reload. |
| 260 [self reloadData]; |
| 261 return; |
| 262 } |
| 263 |
| 264 if (parentFolder == self.primaryMenuItem.folder || !node->is_folder()) { |
| 265 // A child of the selected folder has been deleted or a url not visible in |
| 266 // the UI right now has been deleted. Nothing to do as the menu itself needs |
| 267 // no change. |
| 268 return; |
| 269 } |
| 270 |
| 271 if (node == self.primaryMenuItem.rootAncestor) { |
| 272 // The deleted node is the root node of the current selected folder. Move to |
| 273 // all items. |
| 274 self.primaryMenuItem = [BookmarkMenuItem allMenuItem]; |
| 275 [self.delegate bookmarkMenuView:self selectedMenuItem:self.primaryMenuItem]; |
| 276 [self reloadData]; |
| 277 return; |
| 278 } |
| 279 |
| 280 const BookmarkNode* root = |
| 281 RootLevelFolderForNode(parentFolder, self.bookmarkModel); |
| 282 |
| 283 if (root != self.primaryMenuItem.rootAncestor) { |
| 284 // The deleted folder is not in the same hierarchy as the current selected |
| 285 // folder, there is nothing to reload unless the deleted folder is a root |
| 286 // node. |
| 287 if (!root) |
| 288 [self reloadData]; |
| 289 return; |
| 290 } |
| 291 |
| 292 if (node == self.primaryMenuItem.folder) { |
| 293 // The simple case where the deleted folder is the one currently in the UI. |
| 294 // At this point the deleted folder is known to not be a root node: |
| 295 DCHECK_NE(self.primaryMenuItem.folder, self.primaryMenuItem.rootAncestor); |
| 296 // Simply move to the parent. |
| 297 self.primaryMenuItem = |
| 298 [BookmarkMenuItem folderMenuItemForNode:parentFolder rootAncestor:root]; |
| 299 [self.delegate bookmarkMenuView:self selectedMenuItem:self.primaryMenuItem]; |
| 300 [self reloadData]; |
| 301 } |
| 302 |
| 303 // The only case left is when the deleted folder used to be an ancestor of the |
| 304 // selected folder. This is easy to infer, if the selected folder is no longer |
| 305 // present in the common root hierarchy, this means it was deleted as well. |
| 306 ui::TreeNodeIterator<const BookmarkNode> iterator(root); |
| 307 while (iterator.has_next()) { |
| 308 if (self.primaryMenuItem.folder == iterator.Next()) |
| 309 return; // Nothing to do. |
| 310 } |
| 311 // The current folder was not found, relocate to the first non deleted |
| 312 // ancestor. |
| 313 self.primaryMenuItem = |
| 314 [BookmarkMenuItem folderMenuItemForNode:parentFolder rootAncestor:root]; |
| 315 [self.delegate bookmarkMenuView:self selectedMenuItem:self.primaryMenuItem]; |
| 316 [self reloadData]; |
| 317 } |
| 318 |
| 319 - (void)bookmarkModelRemovedAllNodes { |
| 320 [self reloadData]; |
| 321 } |
| 322 |
| 323 #pragma mark UITableViewDataSource |
| 324 |
| 325 - (UITableViewCell*)tableView:(UITableView*)tableView |
| 326 cellForRowAtIndexPath:(NSIndexPath*)indexPath { |
| 327 BookmarkMenuCell* cell = [tableView |
| 328 dequeueReusableCellWithIdentifier:[BookmarkMenuCell reuseIdentifier]]; |
| 329 if (!cell) { |
| 330 cell = [[[BookmarkMenuCell alloc] |
| 331 initWithStyle:UITableViewCellStyleDefault |
| 332 reuseIdentifier:[BookmarkMenuCell reuseIdentifier]] autorelease]; |
| 333 } |
| 334 cell.selectionStyle = UITableViewCellSelectionStyleNone; |
| 335 BookmarkMenuItem* menuItem = [self menuItemAtIndexPath:indexPath]; |
| 336 BOOL primary = |
| 337 [[self.primaryMenuItem parentItem] isEqual:[menuItem parentItem]]; |
| 338 [cell updateWithBookmarkMenuItem:menuItem primary:primary]; |
| 339 if (primary && bookmark_utils_ios::bookmarkMenuIsInSlideInPanel()) { |
| 340 [tableView selectRowAtIndexPath:indexPath |
| 341 animated:NO |
| 342 scrollPosition:UITableViewScrollPositionNone]; |
| 343 } |
| 344 return cell; |
| 345 } |
| 346 |
| 347 - (NSInteger)tableView:(UITableView*)tableView |
| 348 numberOfRowsInSection:(NSInteger)section { |
| 349 return [self.menuItems[section] count]; |
| 350 } |
| 351 |
| 352 - (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView { |
| 353 return [self.menuItems count]; |
| 354 } |
| 355 |
| 356 #pragma mark UITableViewDelegate |
| 357 |
| 358 - (CGFloat)tableView:(UITableView*)tableView |
| 359 heightForRowAtIndexPath:(NSIndexPath*)indexPath { |
| 360 BookmarkMenuItem* menuItem = [self menuItemAtIndexPath:indexPath]; |
| 361 return [menuItem height]; |
| 362 } |
| 363 |
| 364 - (NSIndexPath*)tableView:(UITableView*)tableView |
| 365 willSelectRowAtIndexPath:(NSIndexPath*)indexPath { |
| 366 BookmarkMenuItem* menuItem = [self menuItemAtIndexPath:indexPath]; |
| 367 return [menuItem canBeSelected] ? indexPath : nil; |
| 368 } |
| 369 |
| 370 - (void)tableView:(UITableView*)tableView |
| 371 didSelectRowAtIndexPath:(NSIndexPath*)indexPath { |
| 372 BookmarkMenuItem* menuItem = [self menuItemAtIndexPath:indexPath]; |
| 373 [self.delegate bookmarkMenuView:self selectedMenuItem:menuItem]; |
| 374 } |
| 375 |
| 376 - (CGFloat)tableView:(UITableView*)tableView |
| 377 heightForHeaderInSection:(NSInteger)section { |
| 378 return 8.0; |
| 379 } |
| 380 |
| 381 - (CGFloat)tableView:(UITableView*)tableView |
| 382 heightForFooterInSection:(NSInteger)section { |
| 383 BOOL isLastSection = [tableView numberOfSections] == (section + 1); |
| 384 return isLastSection ? 8.0 : 0.0; |
| 385 } |
| 386 |
| 387 - (UIView*)tableView:(UITableView*)tableView |
| 388 viewForHeaderInSection:(NSInteger)section { |
| 389 return [[[UIView alloc] initWithFrame:CGRectZero] autorelease]; |
| 390 } |
| 391 |
| 392 - (UIView*)tableView:(UITableView*)tableView |
| 393 viewForFooterInSection:(NSInteger)section { |
| 394 return [[[UIView alloc] initWithFrame:CGRectZero] autorelease]; |
| 395 } |
| 396 |
| 397 #pragma mark MDCInkTouchControllerDelegate |
| 398 |
| 399 - (BOOL)inkTouchController:(MDCInkTouchController*)inkTouchController |
| 400 shouldProcessInkTouchesAtTouchLocation:(CGPoint)location { |
| 401 NSIndexPath* indexPath = [self.tableView indexPathForRowAtPoint:location]; |
| 402 BookmarkMenuItem* menuItem = [self menuItemAtIndexPath:indexPath]; |
| 403 return menuItem.type == bookmarks::MenuItemAll || |
| 404 menuItem.type == bookmarks::MenuItemFolder; |
| 405 } |
| 406 |
| 407 - (MDCInkView*)inkTouchController:(MDCInkTouchController*)inkTouchController |
| 408 inkViewAtTouchLocation:(CGPoint)location { |
| 409 NSIndexPath* indexPath = [self.tableView indexPathForRowAtPoint:location]; |
| 410 BookmarkMenuCell* cell = base::mac::ObjCCastStrict<BookmarkMenuCell>( |
| 411 [self.tableView cellForRowAtIndexPath:indexPath]); |
| 412 return cell.inkView; |
| 413 } |
| 414 |
| 415 #pragma mark Public Methods |
| 416 |
| 417 - (void)updatePrimaryMenuItem:(BookmarkMenuItem*)menuItem { |
| 418 if ([self.primaryMenuItem isEqual:menuItem]) |
| 419 return; |
| 420 |
| 421 self.primaryMenuItem = menuItem; |
| 422 [self.tableView reloadData]; |
| 423 } |
| 424 |
| 425 - (void)setScrollsToTop:(BOOL)scrollsToTop { |
| 426 self.tableView.scrollsToTop = scrollsToTop; |
| 427 } |
| 428 |
| 429 @end |
OLD | NEW |