Index: ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_table_view_controller.mm |
diff --git a/ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_table_view_controller.mm b/ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_table_view_controller.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..10a9b92e35568b124494ff681ee06e5747bb5512 |
--- /dev/null |
+++ b/ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_table_view_controller.mm |
@@ -0,0 +1,952 @@ |
+// Copyright 2014 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#import "ios/chrome/browser/ui/ntp/recent_tabs/recent_tabs_table_view_controller.h" |
+ |
+#include <memory> |
+ |
+#import "base/ios/weak_nsobject.h" |
+#include "base/logging.h" |
+#include "base/mac/scoped_nsobject.h" |
+#include "base/metrics/user_metrics.h" |
+#include "base/metrics/user_metrics_action.h" |
+#include "base/strings/sys_string_conversions.h" |
+#include "components/browser_sync/profile_sync_service.h" |
+#include "components/sync/driver/sync_service.h" |
+#include "components/sync_sessions/open_tabs_ui_delegate.h" |
+#include "ios/chrome/browser/browser_state/chrome_browser_state.h" |
+#import "ios/chrome/browser/metrics/new_tab_page_uma.h" |
+#include "ios/chrome/browser/sessions/tab_restore_service_delegate_impl_ios.h" |
+#include "ios/chrome/browser/sessions/tab_restore_service_delegate_impl_ios_factory.h" |
+#include "ios/chrome/browser/sync/ios_chrome_profile_sync_service_factory.h" |
+#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h" |
+#import "ios/chrome/browser/ui/commands/generic_chrome_command.h" |
+#include "ios/chrome/browser/ui/commands/ios_command_ids.h" |
+#import "ios/chrome/browser/ui/context_menu/context_menu_coordinator.h" |
+#include "ios/chrome/browser/ui/ntp/recent_tabs/synced_sessions.h" |
+#import "ios/chrome/browser/ui/ntp/recent_tabs/views/generic_section_header_view.h" |
+#import "ios/chrome/browser/ui/ntp/recent_tabs/views/header_of_collapsable_section_protocol.h" |
+#import "ios/chrome/browser/ui/ntp/recent_tabs/views/session_section_header_view.h" |
+#import "ios/chrome/browser/ui/ntp/recent_tabs/views/session_tab_data_view.h" |
+#import "ios/chrome/browser/ui/ntp/recent_tabs/views/show_full_history_view.h" |
+#import "ios/chrome/browser/ui/ntp/recent_tabs/views/signed_in_sync_in_progress_view.h" |
+#import "ios/chrome/browser/ui/ntp/recent_tabs/views/signed_in_sync_off_view.h" |
+#import "ios/chrome/browser/ui/ntp/recent_tabs/views/signed_in_sync_on_no_sessions_view.h" |
+#import "ios/chrome/browser/ui/ntp/recent_tabs/views/signed_out_view.h" |
+#import "ios/chrome/browser/ui/ntp/recent_tabs/views/spacers_view.h" |
+#include "ios/chrome/browser/ui/ui_util.h" |
+#import "ios/chrome/browser/ui/uikit_ui_util.h" |
+#import "ios/chrome/browser/ui/url_loader.h" |
+#include "ios/chrome/grit/ios_strings.h" |
+#include "ios/web/public/referrer.h" |
+#import "ios/web/public/web_state/context_menu_params.h" |
+#include "ui/base/l10n/l10n_util.h" |
+ |
+namespace { |
+ |
+// Key for saving collapsed session state in the UserDefaults. |
+NSString* const kCollapsedSectionsKey = @"ChromeRecentTabsCollapsedSections"; |
+ |
+// Key for saving whether the Other Device section is collapsed. |
+NSString* const kOtherDeviceCollapsedKey = @"OtherDevicesCollapsed"; |
+ |
+// Key for saving whether the Recently Closed section is collapsed. |
+NSString* const kRecentlyClosedCollapsedKey = @"RecentlyClosedCollapsed"; |
+ |
+// Tag to extract the section headers from the cells. |
+enum { kSectionHeader = 1 }; |
+ |
+// Types of sections. |
+enum SectionType { |
+ SEPARATOR_SECTION, |
+ CLOSED_TAB_SECTION, |
+ OTHER_DEVICES_SECTION, |
+ SESSION_SECTION, |
+}; |
+ |
+// Types of cells. |
+enum CellType { |
+ CELL_CLOSED_TAB_SECTION_HEADER, |
+ CELL_CLOSED_TAB_DATA, |
+ CELL_SHOW_FULL_HISTORY, |
+ CELL_SEPARATOR, |
+ CELL_OTHER_DEVICES_SECTION_HEADER, |
+ CELL_OTHER_DEVICES_SIGNED_OUT, |
+ CELL_OTHER_DEVICES_SIGNED_IN_SYNC_OFF, |
+ CELL_OTHER_DEVICES_SIGNED_IN_SYNC_ON_NO_SESSIONS, |
+ CELL_OTHER_DEVICES_SYNC_IN_PROGRESS, |
+ CELL_SESSION_SECTION_HEADER, |
+ CELL_SESSION_TAB_DATA, |
+}; |
+ |
+} // namespace |
+ |
+@interface RecentTabsTableViewController () { |
+ ios::ChromeBrowserState* _browserState; // weak |
+ // The service that manages the recently closed tabs. |
+ sessions::TabRestoreService* _tabRestoreService; // weak |
+ // Loader used to open new tabs. |
+ id<UrlLoader> _loader; // weak |
+ // The sync state. |
+ SessionsSyncUserState _sessionState; |
+ // The synced sessions. |
+ std::unique_ptr<synced_sessions::SyncedSessions> _syncedSessions; |
+ // Handles displaying the context menu for all form factors. |
+ base::scoped_nsobject<ContextMenuCoordinator> _contextMenuCoordinator; |
+} |
+// Returns the type of the section at index |section|. |
+- (SectionType)sectionType:(NSInteger)section; |
+// Returns the type of the cell at the path |indexPath|. |
+- (CellType)cellType:(NSIndexPath*)indexPath; |
+// Returns the number of sections before the other devices or session sections. |
+- (NSInteger)numberOfSectionsBeforeSessionOrOtherDevicesSections; |
+// Dismisses the modal containing the Recent Tabs panel (iPhone only). |
+- (void)dismissRecentTabsModal; |
+// Dismisses the modal containing the Recent Tabs panel, with completion |
+// handler (iPhone only). |
+- (void)dismissRecentTabsModalWithCompletion:(ProceduralBlock)completion; |
+// Opens a new tab with the content of |distantTab|. |
+- (void)openTabWithContentOfDistantTab: |
+ (synced_sessions::DistantTab const*)distantTab; |
+// Opens a new tab with |url|. |
+- (void)openTabWithURL:(const GURL&)url; |
+// Shows the user's full history. |
+- (void)showFullHistory; |
+// Deletes/inserts cells for section at index |sectionIndex|. |
+- (void)toggleExpansionOfSection:(NSInteger)sectionIndex; |
+// Returns the key used to map |distantSession| to a collapsed status. |
+- (NSString*)keyForDistantSession: |
+ (synced_sessions::DistantSession const*)distantSession; |
+// Sets whether the session addressed with |sectionKey| is collapsed. |
+- (void)setSection:(NSString*)sectionKey collapsed:(BOOL)collapsed; |
+// Returns whether the section addressed with |sectionKey| is collapsed. |
+- (BOOL)sectionIsCollapsed:(NSString*)sectionKey; |
+// Returns the number of session sections. Requires |_sessionState| to be |
+// USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS. |
+- (NSInteger)numberOfSessionSections; |
+// Returns the section indexes of the Session section or the Other Devices |
+// section. |
+- (NSIndexSet*)sessionOrOtherDevicesSectionsIndexes; |
+// Returns the index of the session located at |indexPath|. |
+- (size_t)indexOfSessionAtIndexPath:(NSIndexPath*)indexPath; |
+// Returns the session at |indexPath|. |
+- (synced_sessions::DistantSession const*)sessionAtIndexPath: |
+ (NSIndexPath*)indexPath; |
+// Returns the session tab at the index |indexPath|. |
+- (synced_sessions::DistantTab const*)distantTabAtIndex:(NSIndexPath*)indexPath; |
+// Opens in new tabs all the tabs of the distant session at index |indexPath|. |
+- (void)openTabsFromSessionAtIndexPath:(NSIndexPath*)indexPath; |
+// Removes all the cells of the session section at index |indexPath|. |
+- (void)removeSessionAtIndexPath:(NSIndexPath*)indexPath; |
+// Handles long presses on the UITableView, possibly opening context menus. |
+- (void)handleLongPress:(UILongPressGestureRecognizer*)longPressGesture; |
+@end |
+ |
+@implementation RecentTabsTableViewController |
+ |
+@synthesize delegate = delegate_; |
+ |
+- (instancetype)init { |
+ NOTREACHED(); |
+ return nil; |
+} |
+ |
+- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState |
+ loader:(id<UrlLoader>)loader { |
+ self = [super initWithStyle:UITableViewStylePlain]; |
+ if (self) { |
+ DCHECK(browserState); |
+ DCHECK(loader); |
+ _browserState = browserState; |
+ _loader = loader; |
+ _sessionState = SessionsSyncUserState::USER_SIGNED_OUT; |
+ _syncedSessions.reset(new synced_sessions::SyncedSessions()); |
+ } |
+ return self; |
+} |
+ |
+- (void)dealloc { |
+ [self.tableView removeObserver:self forKeyPath:@"contentSize"]; |
+ [super dealloc]; |
+} |
+ |
+- (void)viewDidLoad { |
+ [super viewDidLoad]; |
+ self.view.accessibilityIdentifier = @"recent_tabs_view_controller"; |
+ [self.tableView setSeparatorColor:[UIColor clearColor]]; |
+ [self.tableView setDataSource:self]; |
+ [self.tableView setDelegate:self]; |
+ base::scoped_nsobject<UILongPressGestureRecognizer> longPress( |
+ [[UILongPressGestureRecognizer alloc] |
+ initWithTarget:self |
+ action:@selector(handleLongPress:)]); |
+ longPress.get().delegate = self; |
+ [self.tableView addGestureRecognizer:longPress]; |
+ |
+ [self.tableView addObserver:self |
+ forKeyPath:@"contentSize" |
+ options:0 |
+ context:NULL]; |
+} |
+ |
+- (void)observeValueForKeyPath:(NSString*)keyPath |
+ ofObject:(id)object |
+ change:(NSDictionary*)change |
+ context:(void*)context { |
+ if ([keyPath isEqualToString:@"contentSize"]) |
+ [delegate_ recentTabsTableViewContentMoved:self.tableView]; |
+} |
+ |
+- (SectionType)sectionType:(NSInteger)section { |
+ if (section == 0) { |
+ return CLOSED_TAB_SECTION; |
+ } |
+ if (section == 1) { |
+ return SEPARATOR_SECTION; |
+ } |
+ if (section < [self numberOfSectionsBeforeSessionOrOtherDevicesSections]) { |
+ return CLOSED_TAB_SECTION; |
+ } |
+ if (_sessionState == |
+ SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS) { |
+ return SESSION_SECTION; |
+ } |
+ // Other cases of recent_tabs::USER_SIGNED_IN_SYNC_OFF, |
+ // recent_tabs::USER_SIGNED_IN_SYNC_ON_NO_SESSIONS, and |
+ // recent_tabs::USER_SIGNED_OUT falls through to here. |
+ return OTHER_DEVICES_SECTION; |
+} |
+ |
+- (CellType)cellType:(NSIndexPath*)indexPath { |
+ SectionType sectionType = [self sectionType:indexPath.section]; |
+ switch (sectionType) { |
+ case CLOSED_TAB_SECTION: |
+ if (indexPath.row == 0) { |
+ return CELL_CLOSED_TAB_SECTION_HEADER; |
+ } |
+ // The last cell of the section is to access the history panel. |
+ if (indexPath.row == |
+ [self numberOfCellsInRecentlyClosedTabsSection] - 1) { |
+ return CELL_SHOW_FULL_HISTORY; |
+ } |
+ return CELL_CLOSED_TAB_DATA; |
+ case SEPARATOR_SECTION: |
+ return CELL_SEPARATOR; |
+ case SESSION_SECTION: |
+ if (indexPath.row == 0) { |
+ return CELL_SESSION_SECTION_HEADER; |
+ } |
+ return CELL_SESSION_TAB_DATA; |
+ case OTHER_DEVICES_SECTION: |
+ if (_sessionState == |
+ SessionsSyncUserState::USER_SIGNED_IN_SYNC_IN_PROGRESS) { |
+ return CELL_OTHER_DEVICES_SYNC_IN_PROGRESS; |
+ } |
+ if (indexPath.row == 0) { |
+ return CELL_OTHER_DEVICES_SECTION_HEADER; |
+ } |
+ switch (_sessionState) { |
+ case SessionsSyncUserState::USER_SIGNED_OUT: |
+ return CELL_OTHER_DEVICES_SIGNED_OUT; |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_OFF: |
+ return CELL_OTHER_DEVICES_SIGNED_IN_SYNC_OFF; |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_NO_SESSIONS: |
+ return CELL_OTHER_DEVICES_SIGNED_IN_SYNC_ON_NO_SESSIONS; |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS: |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_IN_PROGRESS: |
+ NOTREACHED(); |
+ // These cases should never occur. Still, this method needs to |
+ // return _something_, so it's returning the least wrong cell type. |
+ return CELL_OTHER_DEVICES_SIGNED_IN_SYNC_ON_NO_SESSIONS; |
+ } |
+ } |
+} |
+ |
+- (NSInteger)numberOfSectionsBeforeSessionOrOtherDevicesSections { |
+ // The 2 sections are CLOSED_TAB_SECTION and SEPARATOR_SECTION. |
+ return 2; |
+} |
+ |
+- (void)setScrollsToTop:(BOOL)enabled { |
+ [self.tableView setScrollsToTop:enabled]; |
+} |
+ |
+- (void)dismissModals { |
+ [_contextMenuCoordinator stop]; |
+} |
+ |
+#pragma mark - Recently closed tab helpers |
+ |
+- (void)refreshRecentlyClosedTabs { |
+ [self.tableView reloadData]; |
+} |
+ |
+- (void)setTabRestoreService:(sessions::TabRestoreService*)tabRestoreService { |
+ _tabRestoreService = tabRestoreService; |
+} |
+ |
+- (NSInteger)numberOfCellsInRecentlyClosedTabsSection { |
+ // + 2 because of the section header, and the "Show full history" cell. |
+ return [self numberOfRecentlyClosedTabs] + 2; |
+} |
+ |
+- (NSInteger)numberOfRecentlyClosedTabs { |
+ if (!_tabRestoreService) |
+ return 0; |
+ return static_cast<NSInteger>(_tabRestoreService->entries().size()); |
+} |
+ |
+- (const sessions::TabRestoreService::Entry*)tabRestoreEntryAtIndex: |
+ (NSIndexPath*)indexPath { |
+ DCHECK_EQ([self sectionType:indexPath.section], CLOSED_TAB_SECTION); |
+ // "- 1" because of the section header. |
+ NSInteger index = indexPath.row - 1; |
+ DCHECK_LE(index, [self numberOfRecentlyClosedTabs]); |
+ if (!_tabRestoreService) |
+ return nullptr; |
+ |
+ // Advance the entry iterator to the correct index. |
+ // Note that std:list<> can only be accessed sequentially, which is |
+ // suboptimal when using Cocoa table APIs. This list doesn't appear |
+ // to get very long, so it probably won't matter for perf. |
+ sessions::TabRestoreService::Entries::const_iterator iter = |
+ _tabRestoreService->entries().begin(); |
+ std::advance(iter, index); |
+ CHECK(*iter); |
+ return iter->get(); |
+} |
+ |
+#pragma mark - Helpers to open tabs, or show the full history view. |
+ |
+- (void)dismissRecentTabsModal { |
+ [self dismissRecentTabsModalWithCompletion:nil]; |
+} |
+ |
+- (void)dismissRecentTabsModalWithCompletion:(ProceduralBlock)completion { |
+ // Recent Tabs are modally presented only on iPhone. |
+ if (!IsIPadIdiom()) { |
+ // TODO(crbug.com/434683): Use a delegate to dismiss the table view. |
+ [self.tableView.window.rootViewController |
+ dismissViewControllerAnimated:YES |
+ completion:completion]; |
+ } |
+} |
+ |
+- (void)openTabWithContentOfDistantTab: |
+ (synced_sessions::DistantTab const*)distantTab { |
+ sync_sessions::OpenTabsUIDelegate* openTabs = |
+ IOSChromeProfileSyncServiceFactory::GetForBrowserState(_browserState) |
+ ->GetOpenTabsUIDelegate(); |
+ const sessions::SessionTab* toLoad = nullptr; |
+ [self dismissRecentTabsModal]; |
+ if (openTabs->GetForeignTab(distantTab->session_tag, distantTab->tab_id, |
+ &toLoad)) { |
+ base::RecordAction(base::UserMetricsAction("MobileNTPForeignSession")); |
+ new_tab_page_uma::RecordAction( |
+ _browserState, new_tab_page_uma::ACTION_OPENED_FOREIGN_SESSION); |
+ [_loader loadSessionTab:toLoad]; |
+ } |
+} |
+ |
+- (void)openTabWithTabRestoreEntry: |
+ (const sessions::TabRestoreService::Entry*)entry { |
+ DCHECK(entry); |
+ if (!entry) |
+ return; |
+ // We only handle the TAB type. |
+ if (entry->type != sessions::TabRestoreService::TAB) |
+ return; |
+ TabRestoreServiceDelegateImplIOS* delegate = |
+ TabRestoreServiceDelegateImplIOSFactory::GetForBrowserState( |
+ _browserState); |
+ [self dismissRecentTabsModal]; |
+ base::RecordAction(base::UserMetricsAction("MobileNTPRecentlyClosed")); |
+ new_tab_page_uma::RecordAction( |
+ _browserState, new_tab_page_uma::ACTION_OPENED_RECENTLY_CLOSED_ENTRY); |
+ _tabRestoreService->RestoreEntryById(delegate, entry->id, |
+ WindowOpenDisposition::CURRENT_TAB); |
+} |
+ |
+- (void)openTabWithURL:(const GURL&)url { |
+ if (url.is_valid()) { |
+ [self dismissRecentTabsModal]; |
+ [_loader loadURL:url |
+ referrer:web::Referrer() |
+ transition:ui::PAGE_TRANSITION_TYPED |
+ rendererInitiated:NO]; |
+ } |
+} |
+ |
+- (void)showFullHistory { |
+ UIViewController* rootViewController = |
+ self.tableView.window.rootViewController; |
+ ProceduralBlock openHistory = ^{ |
+ base::scoped_nsobject<GenericChromeCommand> openHistory( |
+ [[GenericChromeCommand alloc] initWithTag:IDC_SHOW_HISTORY]); |
+ [rootViewController chromeExecuteCommand:openHistory]; |
+ }; |
+ // Dismiss modal, if shown, and open history. |
+ if (IsIPadIdiom()) { |
+ openHistory(); |
+ } else { |
+ [self dismissRecentTabsModalWithCompletion:openHistory]; |
+ } |
+} |
+ |
+#pragma mark - Handling of the collapsed sections. |
+ |
+- (void)toggleExpansionOfSection:(NSInteger)sectionIndex { |
+ NSString* sectionCollapseKey = nil; |
+ int cellCount = 0; |
+ |
+ SectionType section = [self sectionType:sectionIndex]; |
+ |
+ switch (section) { |
+ case CLOSED_TAB_SECTION: |
+ sectionCollapseKey = kRecentlyClosedCollapsedKey; |
+ // - 1 because the header does not count. |
+ cellCount = [self numberOfCellsInRecentlyClosedTabsSection] - 1; |
+ break; |
+ case SEPARATOR_SECTION: |
+ NOTREACHED(); |
+ return; |
+ case OTHER_DEVICES_SECTION: |
+ cellCount = 1; |
+ sectionCollapseKey = kOtherDeviceCollapsedKey; |
+ break; |
+ case SESSION_SECTION: { |
+ size_t indexOfSession = |
+ sectionIndex - |
+ [self numberOfSectionsBeforeSessionOrOtherDevicesSections]; |
+ DCHECK_LT(indexOfSession, _syncedSessions->GetSessionCount()); |
+ synced_sessions::DistantSession const* distantSession = |
+ _syncedSessions->GetSession(indexOfSession); |
+ cellCount = distantSession->tabs.size(); |
+ sectionCollapseKey = [self keyForDistantSession:distantSession]; |
+ break; |
+ } |
+ } |
+ DCHECK(sectionCollapseKey); |
+ BOOL collapsed = ![self sectionIsCollapsed:sectionCollapseKey]; |
+ [self setSection:sectionCollapseKey collapsed:collapsed]; |
+ |
+ // Builds an array indexing all the cells needing to be removed or inserted to |
+ // collapse/expand the section. |
+ NSMutableArray* cellIndexPathsToDeleteOrInsert = [NSMutableArray array]; |
+ for (int i = 1; i <= cellCount; i++) { |
+ NSIndexPath* tabIndexPath = |
+ [NSIndexPath indexPathForRow:i inSection:sectionIndex]; |
+ [cellIndexPathsToDeleteOrInsert addObject:tabIndexPath]; |
+ } |
+ |
+ // Update the table view. |
+ [self.tableView beginUpdates]; |
+ if (collapsed) { |
+ [self.tableView deleteRowsAtIndexPaths:cellIndexPathsToDeleteOrInsert |
+ withRowAnimation:UITableViewRowAnimationFade]; |
+ } else { |
+ [self.tableView insertRowsAtIndexPaths:cellIndexPathsToDeleteOrInsert |
+ withRowAnimation:UITableViewRowAnimationFade]; |
+ } |
+ [self.tableView endUpdates]; |
+ |
+ // Rotate disclosure icon. |
+ NSIndexPath* sectionCellIndexPath = |
+ [NSIndexPath indexPathForRow:0 inSection:sectionIndex]; |
+ UITableViewCell* sectionCell = |
+ [self.tableView cellForRowAtIndexPath:sectionCellIndexPath]; |
+ UIView* subview = [sectionCell viewWithTag:kSectionHeader]; |
+ DCHECK([subview |
+ conformsToProtocol:@protocol(HeaderOfCollapsableSectionProtocol)]); |
+ id<HeaderOfCollapsableSectionProtocol> headerView = |
+ static_cast<id<HeaderOfCollapsableSectionProtocol>>(subview); |
+ [headerView setSectionIsCollapsed:collapsed animated:YES]; |
+} |
+ |
+- (NSString*)keyForDistantSession: |
+ (synced_sessions::DistantSession const*)distantSession { |
+ return base::SysUTF8ToNSString(distantSession->tag); |
+} |
+ |
+- (void)setSection:(NSString*)sectionKey collapsed:(BOOL)collapsed { |
+ // TODO(jif): Store in the browser state preference instead of NSUserDefaults. |
+ // crbug.com/419346. |
+ NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; |
+ NSDictionary* collapsedSections = |
+ [defaults dictionaryForKey:kCollapsedSectionsKey]; |
+ NSMutableDictionary* newCollapsedSessions = |
+ [NSMutableDictionary dictionaryWithDictionary:collapsedSections]; |
+ NSNumber* value = [NSNumber numberWithBool:collapsed]; |
+ [newCollapsedSessions setValue:value forKey:sectionKey]; |
+ [defaults setObject:newCollapsedSessions forKey:kCollapsedSectionsKey]; |
+} |
+ |
+- (BOOL)sectionIsCollapsed:(NSString*)sectionKey { |
+ // TODO(crbug.com/419346): Store in the profile's preference instead of the |
+ // NSUserDefaults. |
+ DCHECK(sectionKey); |
+ NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults]; |
+ NSDictionary* collapsedSessions = |
+ [defaults dictionaryForKey:kCollapsedSectionsKey]; |
+ NSNumber* value = (NSNumber*)[collapsedSessions valueForKey:sectionKey]; |
+ return [value boolValue]; |
+} |
+ |
+#pragma mark - Distant Sessions helpers |
+ |
+- (void)refreshUserState:(SessionsSyncUserState)newSessionState { |
+ if (newSessionState == _sessionState && |
+ _sessionState != |
+ SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS) { |
+ // No need to refresh the sections. |
+ return; |
+ } |
+ |
+ [self.tableView beginUpdates]; |
+ NSIndexSet* indexesToBeDeleted = [self sessionOrOtherDevicesSectionsIndexes]; |
+ [self.tableView deleteSections:indexesToBeDeleted |
+ withRowAnimation:UITableViewRowAnimationFade]; |
+ syncer::SyncService* syncService = |
+ IOSChromeProfileSyncServiceFactory::GetForBrowserState(_browserState); |
+ _syncedSessions.reset(new synced_sessions::SyncedSessions(syncService)); |
+ _sessionState = newSessionState; |
+ |
+ if (_sessionState == SessionsSyncUserState::USER_SIGNED_IN_SYNC_IN_PROGRESS) { |
+ // Expand the "Other Device" section once sync is finished. |
+ [self setSection:kOtherDeviceCollapsedKey collapsed:NO]; |
+ } |
+ |
+ NSIndexSet* indexesToBeInserted = [self sessionOrOtherDevicesSectionsIndexes]; |
+ [self.tableView insertSections:indexesToBeInserted |
+ withRowAnimation:UITableViewRowAnimationFade]; |
+ [self.tableView endUpdates]; |
+} |
+ |
+- (NSInteger)numberOfSessionSections { |
+ DCHECK(_sessionState == |
+ SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS); |
+ return _syncedSessions->GetSessionCount(); |
+} |
+ |
+- (NSIndexSet*)sessionOrOtherDevicesSectionsIndexes { |
+ NSInteger sectionCount = 0; |
+ switch (_sessionState) { |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS: |
+ sectionCount = [self numberOfSessionSections]; |
+ break; |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_OFF: |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_NO_SESSIONS: |
+ case SessionsSyncUserState::USER_SIGNED_OUT: |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_IN_PROGRESS: |
+ sectionCount = 1; |
+ break; |
+ } |
+ NSRange rangeOfSessionSections = NSMakeRange( |
+ [self numberOfSectionsBeforeSessionOrOtherDevicesSections], sectionCount); |
+ NSIndexSet* sessionSectionsIndexes = |
+ [NSIndexSet indexSetWithIndexesInRange:rangeOfSessionSections]; |
+ return sessionSectionsIndexes; |
+} |
+ |
+- (size_t)indexOfSessionAtIndexPath:(NSIndexPath*)indexPath { |
+ DCHECK_EQ([self sectionType:indexPath.section], SESSION_SECTION); |
+ size_t indexOfSession = |
+ indexPath.section - |
+ [self numberOfSectionsBeforeSessionOrOtherDevicesSections]; |
+ DCHECK_LT(indexOfSession, _syncedSessions->GetSessionCount()); |
+ return indexOfSession; |
+} |
+ |
+- (synced_sessions::DistantSession const*)sessionAtIndexPath: |
+ (NSIndexPath*)indexPath { |
+ return _syncedSessions->GetSession( |
+ [self indexOfSessionAtIndexPath:indexPath]); |
+} |
+ |
+- (synced_sessions::DistantTab const*)distantTabAtIndex: |
+ (NSIndexPath*)indexPath { |
+ DCHECK_EQ([self sectionType:indexPath.section], SESSION_SECTION); |
+ // "- 1" because of the section header. |
+ size_t indexOfDistantTab = indexPath.row - 1; |
+ synced_sessions::DistantSession const* session = |
+ [self sessionAtIndexPath:indexPath]; |
+ DCHECK_LT(indexOfDistantTab, session->tabs.size()); |
+ return session->tabs[indexOfDistantTab].get(); |
+} |
+ |
+#pragma mark - Long press and context menus |
+ |
+- (void)handleLongPress:(UILongPressGestureRecognizer*)longPressGesture { |
+ DCHECK_EQ(self.tableView, longPressGesture.view); |
+ if (longPressGesture.state == UIGestureRecognizerStateBegan) { |
+ CGPoint point = [longPressGesture locationInView:self.tableView]; |
+ NSIndexPath* indexPath = [self.tableView indexPathForRowAtPoint:point]; |
+ if (!indexPath) |
+ return; |
+ DCHECK_LE(indexPath.section, |
+ [self numberOfSectionsInTableView:self.tableView]); |
+ |
+ CellType cellType = [self cellType:indexPath]; |
+ if (cellType != CELL_SESSION_SECTION_HEADER) { |
+ NOTREACHED(); |
+ return; |
+ } |
+ |
+ web::ContextMenuParams params; |
+ // Get view coordinates in local space. |
+ CGPoint viewCoordinate = [longPressGesture locationInView:self.tableView]; |
+ params.location = viewCoordinate; |
+ params.view.reset([self.tableView retain]); |
+ |
+ // Present sheet/popover using controller that is added to view hierarchy. |
+ UIViewController* topController = [params.view window].rootViewController; |
+ while (topController.presentedViewController) |
+ topController = topController.presentedViewController; |
+ |
+ _contextMenuCoordinator.reset([[ContextMenuCoordinator alloc] |
+ initWithBaseViewController:topController |
+ params:params]); |
+ |
+ // Fill the sheet/popover with buttons. |
+ base::WeakNSObject<RecentTabsTableViewController> weakSelf(self); |
+ |
+ // "Open all tabs" button. |
+ NSString* openAllButtonLabel = |
+ l10n_util::GetNSString(IDS_IOS_RECENT_TABS_OPEN_ALL_MENU_OPTION); |
+ [_contextMenuCoordinator |
+ addItemWithTitle:openAllButtonLabel |
+ action:^{ |
+ [weakSelf openTabsFromSessionAtIndexPath:indexPath]; |
+ }]; |
+ |
+ // "Hide for now" button. |
+ NSString* hideButtonLabel = |
+ l10n_util::GetNSString(IDS_IOS_RECENT_TABS_HIDE_MENU_OPTION); |
+ [_contextMenuCoordinator |
+ addItemWithTitle:hideButtonLabel |
+ action:^{ |
+ [weakSelf removeSessionAtIndexPath:indexPath]; |
+ }]; |
+ |
+ [_contextMenuCoordinator start]; |
+ } |
+} |
+ |
+- (void)openTabsFromSessionAtIndexPath:(NSIndexPath*)indexPath { |
+ synced_sessions::DistantSession const* session = |
+ [self sessionAtIndexPath:indexPath]; |
+ [self dismissRecentTabsModal]; |
+ for (auto const& tab : session->tabs) { |
+ [_loader webPageOrderedOpen:tab->virtual_url |
+ referrer:web::Referrer() |
+ windowName:nil |
+ inBackground:YES |
+ appendTo:kLastTab]; |
+ } |
+} |
+ |
+- (void)removeSessionAtIndexPath:(NSIndexPath*)indexPath { |
+ DCHECK_EQ([self cellType:indexPath], CELL_SESSION_SECTION_HEADER); |
+ synced_sessions::DistantSession const* session = |
+ [self sessionAtIndexPath:indexPath]; |
+ std::string sessionTagCopy = session->tag; |
+ syncer::SyncService* syncService = |
+ IOSChromeProfileSyncServiceFactory::GetForBrowserState(_browserState); |
+ sync_sessions::OpenTabsUIDelegate* openTabs = |
+ syncService->GetOpenTabsUIDelegate(); |
+ _syncedSessions->EraseSession([self indexOfSessionAtIndexPath:indexPath]); |
+ [self.tableView |
+ deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] |
+ withRowAnimation:UITableViewRowAnimationLeft]; |
+ // Use dispatch_async to give the action sheet a chance to cleanup before |
+ // replacing its parent view. |
+ dispatch_async(dispatch_get_main_queue(), ^{ |
+ openTabs->DeleteForeignSession(sessionTagCopy); |
+ }); |
+} |
+ |
+#pragma mark - UIGestureRecognizerDelegate |
+ |
+- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer { |
+ CGPoint point = [gestureRecognizer locationInView:self.tableView]; |
+ NSIndexPath* indexPath = [self.tableView indexPathForRowAtPoint:point]; |
+ if (!indexPath) |
+ return NO; |
+ CellType cellType = [self cellType:indexPath]; |
+ // Context menus can be opened on a section header for tabs. |
+ return cellType == CELL_SESSION_SECTION_HEADER; |
+} |
+ |
+#pragma mark - UITableViewDataSource |
+ |
+- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView { |
+ switch (_sessionState) { |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS: |
+ return [self numberOfSectionsBeforeSessionOrOtherDevicesSections] + |
+ [self numberOfSessionSections]; |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_OFF: |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_NO_SESSIONS: |
+ case SessionsSyncUserState::USER_SIGNED_OUT: |
+ case SessionsSyncUserState::USER_SIGNED_IN_SYNC_IN_PROGRESS: |
+ return [self numberOfSectionsBeforeSessionOrOtherDevicesSections] + 1; |
+ } |
+} |
+ |
+- (NSInteger)tableView:(UITableView*)tableView |
+ numberOfRowsInSection:(NSInteger)section { |
+ switch ([self sectionType:section]) { |
+ case CLOSED_TAB_SECTION: |
+ if ([self sectionIsCollapsed:kRecentlyClosedCollapsedKey]) |
+ return 1; |
+ else |
+ return [self numberOfCellsInRecentlyClosedTabsSection]; |
+ case SEPARATOR_SECTION: |
+ return 1; |
+ case OTHER_DEVICES_SECTION: |
+ if (_sessionState == |
+ SessionsSyncUserState::USER_SIGNED_IN_SYNC_IN_PROGRESS) |
+ return 1; |
+ if ([self sectionIsCollapsed:kOtherDeviceCollapsedKey]) |
+ return 1; |
+ else |
+ return 2; |
+ case SESSION_SECTION: { |
+ DCHECK(_sessionState == |
+ SessionsSyncUserState::USER_SIGNED_IN_SYNC_ON_WITH_SESSIONS); |
+ size_t sessionIndex = |
+ section - [self numberOfSectionsBeforeSessionOrOtherDevicesSections]; |
+ DCHECK_LT(sessionIndex, _syncedSessions->GetSessionCount()); |
+ synced_sessions::DistantSession const* distantSession = |
+ _syncedSessions->GetSession(sessionIndex); |
+ NSString* key = [self keyForDistantSession:distantSession]; |
+ if ([self sectionIsCollapsed:key]) |
+ return 1; |
+ else |
+ return distantSession->tabs.size() + 1; |
+ } |
+ } |
+} |
+ |
+- (UITableViewCell*)tableView:(UITableView*)tableView |
+ cellForRowAtIndexPath:(NSIndexPath*)indexPath { |
+ UITableViewCell* cell = |
+ [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault |
+ reuseIdentifier:nil] autorelease]; |
+ UIView* contentView = cell.contentView; |
+ |
+ base::scoped_nsobject<UIView> subview; |
+ CellType cellType = [self cellType:indexPath]; |
+ switch (cellType) { |
+ case CELL_CLOSED_TAB_SECTION_HEADER: { |
+ BOOL collapsed = [self sectionIsCollapsed:kRecentlyClosedCollapsedKey]; |
+ subview.reset([[GenericSectionHeaderView alloc] |
+ initWithType:recent_tabs::RECENTLY_CLOSED_TABS_SECTION_HEADER |
+ sectionIsCollapsed:collapsed]); |
+ [subview setTag:kSectionHeader]; |
+ break; |
+ } |
+ case CELL_CLOSED_TAB_DATA: { |
+ base::scoped_nsobject<SessionTabDataView> genericTabData( |
+ [[SessionTabDataView alloc] initWithFrame:CGRectZero]); |
+ [genericTabData |
+ updateWithTabRestoreEntry:[self tabRestoreEntryAtIndex:indexPath] |
+ browserState:_browserState]; |
+ subview.reset([genericTabData.get() retain]); |
+ break; |
+ } |
+ case CELL_SHOW_FULL_HISTORY: |
+ subview.reset([[ShowFullHistoryView alloc] initWithFrame:CGRectZero]); |
+ break; |
+ case CELL_SEPARATOR: |
+ subview.reset( |
+ [[RecentlyClosedSectionFooter alloc] initWithFrame:CGRectZero]); |
+ [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; |
+ break; |
+ case CELL_OTHER_DEVICES_SECTION_HEADER: { |
+ BOOL collapsed = [self sectionIsCollapsed:kOtherDeviceCollapsedKey]; |
+ subview.reset([[GenericSectionHeaderView alloc] |
+ initWithType:recent_tabs::OTHER_DEVICES_SECTION_HEADER |
+ sectionIsCollapsed:collapsed]); |
+ [subview setTag:kSectionHeader]; |
+ break; |
+ } |
+ case CELL_OTHER_DEVICES_SIGNED_OUT: |
+ subview.reset([[SignedOutView alloc] initWithFrame:CGRectZero]); |
+ [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; |
+ break; |
+ case CELL_OTHER_DEVICES_SIGNED_IN_SYNC_OFF: |
+ subview.reset([[SignedInSyncOffView alloc] initWithFrame:CGRectZero |
+ browserState:_browserState]); |
+ [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; |
+ break; |
+ case CELL_OTHER_DEVICES_SIGNED_IN_SYNC_ON_NO_SESSIONS: |
+ subview.reset( |
+ [[SignedInSyncOnNoSessionsView alloc] initWithFrame:CGRectZero]); |
+ [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; |
+ break; |
+ case CELL_OTHER_DEVICES_SYNC_IN_PROGRESS: |
+ subview.reset( |
+ [[SignedInSyncInProgressView alloc] initWithFrame:CGRectZero]); |
+ [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; |
+ break; |
+ case CELL_SESSION_SECTION_HEADER: { |
+ synced_sessions::DistantSession const* distantSession = |
+ [self sessionAtIndexPath:indexPath]; |
+ NSString* key = [self keyForDistantSession:distantSession]; |
+ BOOL collapsed = [self sectionIsCollapsed:key]; |
+ base::scoped_nsobject<SessionSectionHeaderView> sessionSectionHeader( |
+ [[SessionSectionHeaderView alloc] initWithFrame:CGRectZero |
+ sectionIsCollapsed:collapsed]); |
+ [sessionSectionHeader updateWithSession:distantSession]; |
+ subview.reset(sessionSectionHeader.release()); |
+ [subview setTag:kSectionHeader]; |
+ break; |
+ } |
+ case CELL_SESSION_TAB_DATA: { |
+ base::scoped_nsobject<SessionTabDataView> genericTabData( |
+ [[SessionTabDataView alloc] initWithFrame:CGRectZero]); |
+ [genericTabData updateWithDistantTab:[self distantTabAtIndex:indexPath] |
+ browserState:_browserState]; |
+ subview.reset([genericTabData.get() retain]); |
+ break; |
+ } |
+ } |
+ |
+ DCHECK(subview); |
+ [contentView addSubview:subview]; |
+ |
+ // Sets constraints on the subview. |
+ [subview setTranslatesAutoresizingMaskIntoConstraints:NO]; |
+ |
+ NSDictionary* viewsDictionary = @{ @"view" : subview.get() }; |
+ // This set of constraints should match the constraints set on the |
+ // RecentlyClosedSectionFooter. |
+ // clang-format off |
+ NSArray* constraints = @[ |
+ @"V:|-0-[view]-0-|", |
+ @"H:|-(>=0)-[view(<=548)]-(>=0)-|", |
+ @"H:[view(==548@500)]" |
+ ]; |
+ // clang-format on |
+ [contentView addConstraint:[NSLayoutConstraint |
+ constraintWithItem:subview |
+ attribute:NSLayoutAttributeCenterX |
+ relatedBy:NSLayoutRelationEqual |
+ toItem:contentView |
+ attribute:NSLayoutAttributeCenterX |
+ multiplier:1 |
+ constant:0]]; |
+ ApplyVisualConstraints(constraints, viewsDictionary, contentView); |
+ return cell; |
+} |
+ |
+#pragma mark - UITableViewDelegate |
+ |
+- (NSIndexPath*)tableView:(UITableView*)tableView |
+ willSelectRowAtIndexPath:(NSIndexPath*)indexPath { |
+ DCHECK_EQ(tableView, self.tableView); |
+ CellType cellType = [self cellType:indexPath]; |
+ switch (cellType) { |
+ case CELL_CLOSED_TAB_SECTION_HEADER: |
+ case CELL_OTHER_DEVICES_SECTION_HEADER: |
+ case CELL_SESSION_SECTION_HEADER: |
+ case CELL_CLOSED_TAB_DATA: |
+ case CELL_SESSION_TAB_DATA: |
+ case CELL_SHOW_FULL_HISTORY: |
+ return indexPath; |
+ case CELL_SEPARATOR: |
+ case CELL_OTHER_DEVICES_SIGNED_OUT: |
+ case CELL_OTHER_DEVICES_SIGNED_IN_SYNC_OFF: |
+ case CELL_OTHER_DEVICES_SIGNED_IN_SYNC_ON_NO_SESSIONS: |
+ case CELL_OTHER_DEVICES_SYNC_IN_PROGRESS: |
+ return nil; |
+ } |
+} |
+ |
+- (void)tableView:(UITableView*)tableView |
+ didSelectRowAtIndexPath:(NSIndexPath*)indexPath { |
+ DCHECK_EQ(tableView, self.tableView); |
+ CellType cellType = [self cellType:indexPath]; |
+ switch (cellType) { |
+ case CELL_CLOSED_TAB_SECTION_HEADER: |
+ case CELL_OTHER_DEVICES_SECTION_HEADER: |
+ case CELL_SESSION_SECTION_HEADER: |
+ // Collapse or uncollapse section. |
+ [tableView deselectRowAtIndexPath:indexPath animated:NO]; |
+ [self toggleExpansionOfSection:indexPath.section]; |
+ break; |
+ case CELL_CLOSED_TAB_DATA: |
+ // Open new tab. |
+ [self openTabWithTabRestoreEntry:[self tabRestoreEntryAtIndex:indexPath]]; |
+ break; |
+ case CELL_SESSION_TAB_DATA: |
+ // Open new tab. |
+ [self openTabWithContentOfDistantTab:[self distantTabAtIndex:indexPath]]; |
+ break; |
+ case CELL_SHOW_FULL_HISTORY: |
+ [tableView deselectRowAtIndexPath:indexPath animated:NO]; |
+ [self showFullHistory]; |
+ break; |
+ case CELL_SEPARATOR: |
+ case CELL_OTHER_DEVICES_SIGNED_OUT: |
+ case CELL_OTHER_DEVICES_SIGNED_IN_SYNC_OFF: |
+ case CELL_OTHER_DEVICES_SIGNED_IN_SYNC_ON_NO_SESSIONS: |
+ case CELL_OTHER_DEVICES_SYNC_IN_PROGRESS: |
+ NOTREACHED(); |
+ break; |
+ } |
+} |
+ |
+- (CGFloat)tableView:(UITableView*)tableView |
+ heightForRowAtIndexPath:(NSIndexPath*)indexPath { |
+ DCHECK_EQ(self.tableView, tableView); |
+ CellType cellType = [self cellType:indexPath]; |
+ switch (cellType) { |
+ case CELL_SHOW_FULL_HISTORY: |
+ return [ShowFullHistoryView desiredHeightInUITableViewCell]; |
+ case CELL_SEPARATOR: |
+ return [RecentlyClosedSectionFooter desiredHeightInUITableViewCell]; |
+ case CELL_OTHER_DEVICES_SIGNED_OUT: |
+ return [SignedOutView desiredHeightInUITableViewCell]; |
+ case CELL_OTHER_DEVICES_SIGNED_IN_SYNC_OFF: |
+ return [SignedInSyncOffView desiredHeightInUITableViewCell]; |
+ case CELL_OTHER_DEVICES_SIGNED_IN_SYNC_ON_NO_SESSIONS: |
+ return [SignedInSyncOnNoSessionsView desiredHeightInUITableViewCell]; |
+ case CELL_SESSION_SECTION_HEADER: |
+ return [SessionSectionHeaderView desiredHeightInUITableViewCell]; |
+ case CELL_CLOSED_TAB_DATA: |
+ case CELL_SESSION_TAB_DATA: |
+ return [SessionTabDataView desiredHeightInUITableViewCell]; |
+ case CELL_CLOSED_TAB_SECTION_HEADER: |
+ case CELL_OTHER_DEVICES_SECTION_HEADER: |
+ return [GenericSectionHeaderView desiredHeightInUITableViewCell]; |
+ case CELL_OTHER_DEVICES_SYNC_IN_PROGRESS: |
+ return [SignedInSyncInProgressView desiredHeightInUITableViewCell]; |
+ } |
+} |
+ |
+- (UIView*)tableView:(UITableView*)tableView |
+ viewForHeaderInSection:(NSInteger)section { |
+ if ([self sectionType:section] == CLOSED_TAB_SECTION) { |
+ return [[[RecentlyTabsTopSpacingHeader alloc] initWithFrame:CGRectZero] |
+ autorelease]; |
+ } |
+ return nil; |
+} |
+ |
+- (CGFloat)tableView:(UITableView*)tableView |
+ heightForHeaderInSection:(NSInteger)section { |
+ if ([self sectionType:section] == CLOSED_TAB_SECTION) { |
+ return [RecentlyTabsTopSpacingHeader desiredHeightInUITableViewCell]; |
+ } |
+ return 0; |
+} |
+ |
+#pragma mark - UIScrollViewDelegate |
+ |
+- (void)scrollViewDidScroll:(UIScrollView*)scrollView { |
+ [delegate_ recentTabsTableViewContentMoved:self.tableView]; |
+} |
+ |
+@end |