| // 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> |
| |
| #include "base/logging.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" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| 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. |
| __weak id<UrlLoader> _loader; |
| // The sync state. |
| SessionsSyncUserState _sessionState; |
| // The synced sessions. |
| std::unique_ptr<synced_sessions::SyncedSessions> _syncedSessions; |
| // Handles displaying the context menu for all form factors. |
| 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"]; |
| } |
| |
| - (void)viewDidLoad { |
| [super viewDidLoad]; |
| self.view.accessibilityIdentifier = @"recent_tabs_view_controller"; |
| [self.tableView setSeparatorColor:[UIColor clearColor]]; |
| [self.tableView setDataSource:self]; |
| [self.tableView setDelegate:self]; |
| UILongPressGestureRecognizer* longPress = |
| [[UILongPressGestureRecognizer alloc] |
| initWithTarget:self |
| action:@selector(handleLongPress:)]; |
| longPress.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( |
| "MobileRecentTabManagerTabFromOtherDeviceOpened")); |
| 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("MobileRecentTabManagerRecentTabOpened")); |
| 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 = ^{ |
| 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); |
| |
| // Present sheet/popover using controller that is added to view hierarchy. |
| UIViewController* topController = [params.view window].rootViewController; |
| while (topController.presentedViewController) |
| topController = topController.presentedViewController; |
| |
| _contextMenuCoordinator = |
| [[ContextMenuCoordinator alloc] initWithBaseViewController:topController |
| params:params]; |
| |
| // Fill the sheet/popover with buttons. |
| __weak 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() |
| 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]; |
| UIView* contentView = cell.contentView; |
| |
| UIView* subview; |
| CellType cellType = [self cellType:indexPath]; |
| switch (cellType) { |
| case CELL_CLOSED_TAB_SECTION_HEADER: { |
| BOOL collapsed = [self sectionIsCollapsed:kRecentlyClosedCollapsedKey]; |
| subview = [[GenericSectionHeaderView alloc] |
| initWithType:recent_tabs::RECENTLY_CLOSED_TABS_SECTION_HEADER |
| sectionIsCollapsed:collapsed]; |
| [subview setTag:kSectionHeader]; |
| break; |
| } |
| case CELL_CLOSED_TAB_DATA: { |
| SessionTabDataView* genericTabData = |
| [[SessionTabDataView alloc] initWithFrame:CGRectZero]; |
| [genericTabData |
| updateWithTabRestoreEntry:[self tabRestoreEntryAtIndex:indexPath] |
| browserState:_browserState]; |
| subview = genericTabData; |
| break; |
| } |
| case CELL_SHOW_FULL_HISTORY: |
| subview = [[ShowFullHistoryView alloc] initWithFrame:CGRectZero]; |
| break; |
| case CELL_SEPARATOR: |
| subview = [[RecentlyClosedSectionFooter alloc] initWithFrame:CGRectZero]; |
| [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; |
| break; |
| case CELL_OTHER_DEVICES_SECTION_HEADER: { |
| BOOL collapsed = [self sectionIsCollapsed:kOtherDeviceCollapsedKey]; |
| subview = [[GenericSectionHeaderView alloc] |
| initWithType:recent_tabs::OTHER_DEVICES_SECTION_HEADER |
| sectionIsCollapsed:collapsed]; |
| [subview setTag:kSectionHeader]; |
| break; |
| } |
| case CELL_OTHER_DEVICES_SIGNED_OUT: |
| subview = [[SignedOutView alloc] initWithFrame:CGRectZero]; |
| [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; |
| break; |
| case CELL_OTHER_DEVICES_SIGNED_IN_SYNC_OFF: |
| subview = [[SignedInSyncOffView alloc] initWithFrame:CGRectZero |
| browserState:_browserState]; |
| [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; |
| break; |
| case CELL_OTHER_DEVICES_SIGNED_IN_SYNC_ON_NO_SESSIONS: |
| subview = [[SignedInSyncOnNoSessionsView alloc] initWithFrame:CGRectZero]; |
| [cell setSelectionStyle:UITableViewCellSelectionStyleNone]; |
| break; |
| case CELL_OTHER_DEVICES_SYNC_IN_PROGRESS: |
| subview = [[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]; |
| SessionSectionHeaderView* sessionSectionHeader = |
| [[SessionSectionHeaderView alloc] initWithFrame:CGRectZero |
| sectionIsCollapsed:collapsed]; |
| [sessionSectionHeader updateWithSession:distantSession]; |
| subview = sessionSectionHeader; |
| [subview setTag:kSectionHeader]; |
| break; |
| } |
| case CELL_SESSION_TAB_DATA: { |
| SessionTabDataView* genericTabData = |
| [[SessionTabDataView alloc] initWithFrame:CGRectZero]; |
| [genericTabData updateWithDistantTab:[self distantTabAtIndex:indexPath] |
| browserState:_browserState]; |
| subview = genericTabData; |
| break; |
| } |
| } |
| |
| DCHECK(subview); |
| [contentView addSubview:subview]; |
| |
| // Sets constraints on the subview. |
| [subview setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| |
| NSDictionary* viewsDictionary = @{ @"view" : subview }; |
| // 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]; |
| } |
| 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 |