Click here to Skip to main content
15,310,682 members
Articles / Desktop Programming / MFC
Article
Posted 13 May 2022

Stats

3.5K views
7 bookmarked

IntelliFile

Rate me:
Please Sign up or sign in to vote.
4.36/5 (8 votes)
13 May 2022GPL33 min read
An alternative Windows version to the famous Total Commander!
This article is about the IntelliFile application which is a free alternative Windows version to Total Commander and uses many components that have been published on CodeProject.

IntelliFile

Feature List

The application is free as in free speech/free beer, and the final version will have the following features:

  • File handling
    • Extended copying, moving, renaming and deleting of entire trees
    • Compare files by content, with built-in text editor
    • Encode/Decode files in UUE, XXE and MIME format
    • Show/select files with specific search pattern, size, date or contents
    • Enhanced search function with full text search in any files across multiple drives, even inside archives
    • Supports Drag & Drop with Explorer/the Desktop
  • FTP client
    • Built-in FTP client supports most public FTP servers
    • Secure FTP over SSL/TLS, enter the URL as follows: ftps://ftp.servername.com
    • Download in the background (separate thread)
  • Archive handling
    • Archives are handled like subdirectories. You can easily copy files to and from archives.
    • Built-in ZIP-compatible packer, supports long filenames! This packer is based on ZLIB by Info-Zip.
    • Pack archives in the background (separate thread).

For now, only the basic file operations are implemented: edit, copy, move, rename, and delete!

The Architecture

Each file definition is contained in a CFileData class, with the following interface:

  • DWORD GetFileAttributes(); - returns file's attributes
  • void SetFileAttributes(DWORD dwFileAttributes); - sets file's attributes
  • COleDateTime GetCreationTime(); - return file's creation date/time
  • void SetCreationTime(COleDateTime ftCreationTime); - sets file's creation date/time
  • COleDateTime GetLastAccessTime(); - return file's last read date/time
  • void SetLastAccessTime(COleDateTime ftLastAccessTime); - sets file's last read date/time
  • COleDateTime GetLastWriteTime(); - returns file's last write date/time
  • void SetLastWriteTime(COleDateTime ftLastWriteTime); - sets file's last write date/time
  • ULONGLONG GetFileSize(); - returns file's size
  • void SetFileSize(ULONGLONG nFileSize); - sets file's size
  • CString GetFileName(); - returns file's name
  • void SetFileName(CString strFileName); - sets file's name
  • CString GetAlternate(); - returns file's alternate name
  • void SetAlternate(CString strAlternateFileName); - sets file's alternate name

Then, we define CFileList as typedef CArray<cfiledata*> CFileList;

This list is managed inside the CFileSystem class, with the following interface:

  • BOOL RemoveAll(); - removes all files from list
  • int GetSize(); - returns the count of files in list
  • CFileData* GetAt(int nIndex); - returns a file definition from list
  • int GetSystemType(); - returns file system type (FAT, FTP, ZIP)
  • void SetSystemType(int nSystemType); - sets file system type (FAT, FTP, ZIP)
  • CString GetFolder(); - returns current folder path
  • BOOL SetFolder(CString strFolder); - sets current folder path
  • BOOL Refresh(); - updates the file definitions from list
  • BOOL ViewFile(CString strFilePath); - allows viewing the selected file
  • BOOL EditFile(CString strFilePath); - allows editing the selected file
  • BOOL CopyFile(CFileSystem* pDestination, CFileList* arrSelection); - copies the selected files and folders
  • BOOL MoveFile(CFileSystem* pDestination, CFileList* arrSelection); - moves the selected files and folders
  • BOOL NewFolder(CFileSystem* pDestination, CFileList* arrSelection); - creates a new folder
  • BOOL DeleteFile(CFileSystem* pDestination, CFileList* arrSelection); - deletes the selected files and folders

How to Edit Files in Windows?

C++
BOOL CFileSystem::EditFile(CString strFilePath)
{
    const int nDot = strFilePath.ReverseFind(_T('.'));
    if ((nDot != -1) && !IsApplication(strFilePath))
    {
        CString strExtension = strFilePath.Mid(nDot);

        CString strApplication;
        TCHAR lpszBuffer[0x1000] = { 0 };
        DWORD cbLength = sizeof(lpszBuffer);
        if (SUCCEEDED(AssocQueryString(0, ASSOCSTR_COMMAND, 
                      strExtension, _T("open"), lpszBuffer, &cbLength)))
        {
            lpszBuffer[cbLength] = 0;
            strApplication = lpszBuffer;
        }

        if (strApplication.IsEmpty())
        {
            MessageBox(m_hWndParent, _T("There is no application associated with 
            this type of file."), _T("IntelliFile"), MB_OK | MB_ICONEXCLAMATION);
            return FALSE;
        }

        if (strApplication.Find(_T("%1")) != -1)
        {
            strApplication.Replace(_T("\"%1\""), (_T("\"") + strFilePath + _T("\"")));
            strApplication.Replace(_T("%1"), (_T("\"") + strFilePath + _T("\"")));
        }
        else
        {
            strApplication += _T(" ") + (_T("\"") + strFilePath + _T("\""));
        }

        CString strExe;
        CString strParam;
        if (strApplication.Find(_T("rundll32.exe")) != -1)
        {
            if ((int) ShellExecute(m_hWndParent, _T("open"), 
                      strFilePath, NULL, NULL, SW_SHOWNORMAL) <= 32)
            {
                DisplayErrorBox(m_wndCaptionBar, _T("ShellExecute"), GetLastError());
                return FALSE;
            }
        }
        else
        {
            if (strApplication[0] == _T('"'))
            {
                int nPos = strApplication.Find('"', 1);
                if (nPos != -1)
                {
                    strExe = strApplication.Left(nPos+1);
                    strParam = strApplication.Mid(nPos+1);
                }
                else
                {
                    ASSERT(0);
                }
            }
            else
            {
                int nPos = strApplication.Find(' ', 1);
                if (nPos != -1)
                {
                    strExe = strApplication.Left(nPos+1);
                    strParam = strApplication.Mid(nPos+1);
                }
                else
                {
                    ASSERT(0);
                }
            }

            strExe.Trim(_T(" \r\n\t"));
            strParam.Trim(_T(" \r\n\t"));

            SHELLEXECUTEINFO pShellExecuteInfo;
            pShellExecuteInfo.cbSize = sizeof(SHELLEXECUTEINFO);
            pShellExecuteInfo.fMask = SEE_MASK_FLAG_DDEWAIT | 
            SEE_MASK_NOCLOSEPROCESS | SEE_MASK_DOENVSUBST;
            pShellExecuteInfo.hwnd = m_hWndParent;
            pShellExecuteInfo.lpVerb = NULL;
            pShellExecuteInfo.lpFile = (LPCWSTR)(strExe);
            pShellExecuteInfo.lpParameters = (LPCWSTR)(strParam);
            pShellExecuteInfo.lpDirectory = NULL;
            pShellExecuteInfo.nShow = SW_SHOWNORMAL;

            if (!ShellExecuteEx(&pShellExecuteInfo))
            {
                DisplayErrorBox(m_wndCaptionBar, _T("ShellExecute"), GetLastError());
                return FALSE;
            }
        }
        return TRUE;
    }

    return FALSE;
}

How to Copy Files in Windows?

C++
BOOL CFileSystem::CopyFile(CFileSystem* pDestination, CFileList* arrSelection)
{
    HRESULT hResult = S_OK;
    if ((pDestination != NULL) && (arrSelection != NULL))
    {
        IFileOperation* pFileOperation = NULL;
        if (SUCCEEDED(hResult = CoCreateInstance(CLSID_FileOperation, 
            NULL, CLSCTX_ALL, IID_PPV_ARGS(&pFileOperation))))
        {
            if (SUCCEEDED(hResult = pFileOperation->SetOwnerWindow(m_hWndParent)))
            {
                if (SUCCEEDED(hResult = pFileOperation->SetOperationFlags(
                    FOF_NOCONFIRMMKDIR |
                    FOF_NOERRORUI |
                    FOFX_SHOWELEVATIONPROMPT)))
                {
                    CString strDestination = pDestination->GetFolder();
                    IShellItem* pFolderItem = NULL;
                    if (SUCCEEDED(hResult = SHCreateItemFromParsingName
                       (strDestination, NULL, IID_PPV_ARGS(&pFolderItem))))
                    {
                        if (arrSelection->GetCount() == 1)
                        {
                            CFileData* pFileData = arrSelection->GetAt(0);
                            ASSERT_VALID(pFileData);
                            CString strFileName = pFileData->GetFileName();
                            CString strFolder = GetFolder();
                            CString strFilePath = strFolder + strFileName;

                            IShellItem* pShellItem = NULL;
                            if (SUCCEEDED(hResult = SHCreateItemFromParsingName
                               (strFilePath, NULL, IID_PPV_ARGS(&pShellItem))))
                            {
                                if (SUCCEEDED(hResult = pFileOperation->CopyItem
                                             (pShellItem, pFolderItem, NULL, NULL)))
                                {
                                }
                                else
                                {
                                    DisplayErrorBox(m_wndCaptionBar, 
                                    _T("pFileOperation->CopyItem"), hResult);
                                    pShellItem->Release();
                                    pFolderItem->Release();
                                    pFileOperation->Release();
                                    return FALSE;
                                }

                                pShellItem->Release();
                                pShellItem = NULL;
                            }
                            else
                            {
                                DisplayErrorBox(m_wndCaptionBar, 
                                _T("SHCreateItemFromParsingName"), hResult);
                                pFolderItem->Release();
                                pFileOperation->Release();
                                return FALSE;
                            }
                        }
                        else
                        {
                            const int nCount = (int)arrSelection->GetCount();
                            LPCITEMIDLIST* arrItemIDList = new LPCITEMIDLIST[nCount];
                            for (int nIndex = 0; nIndex < nCount; nIndex++)
                            {
                                CFileData* pFileData = arrSelection->GetAt(nIndex);
                                ASSERT_VALID(pFileData);
                                CString strFileName = pFileData->GetFileName();
                                CString strFolder = GetFolder();
                                CString strFilePath = strFolder + strFileName;

                                arrItemIDList[nIndex] = ILCreateFromPath(strFilePath);
                            }

                            IShellItemArray* pShellItemArray = NULL;
                            if (SUCCEEDED(hResult = SHCreateShellItemArrayFromIDLists
                                         (nCount, arrItemIDList, &pShellItemArray)))
                            {
                                if (SUCCEEDED(hResult = pFileOperation->CopyItems
                                             (pShellItemArray, pFolderItem)))
                                {
                                }
                                else
                                {
                                    DisplayErrorBox(m_wndCaptionBar, 
                                    _T("pFileOperation->CopyItems"), hResult);
                                    pShellItemArray->Release();
                                    pFolderItem->Release();
                                    pFileOperation->Release();
                                    delete arrItemIDList;
                                    return FALSE;
                                }

                                pShellItemArray->Release();
                                pShellItemArray = NULL;
                            }
                            else
                            {
                                DisplayErrorBox(m_wndCaptionBar, 
                                _T("SHCreateShellItemArrayFromIDLists"), hResult);
                                pFileOperation->Release();
                                pFolderItem->Release();
                                delete arrItemIDList;
                                return FALSE;
                            }

                            for (int nIndex = 0; nIndex < nCount; nIndex++)
                            {
                                ILFree((LPITEMIDLIST) arrItemIDList[nIndex]);
                            }
                            delete arrItemIDList;
                            arrItemIDList = NULL;
                        }

                        pFolderItem->Release();
                        pFolderItem = NULL;
                    }
                    else
                    {
                        DisplayErrorBox(m_wndCaptionBar, 
                        _T("SHCreateItemFromParsingName"), hResult);
                        pFileOperation->Release();
                        return FALSE;
                    }

                    if (SUCCEEDED(hResult = pFileOperation->PerformOperations()))
                    {
                    }
                    else
                    {
                        DisplayErrorBox(m_wndCaptionBar, 
                        _T("pFileOperation->PerformOperations"), hResult);
                        pFileOperation->Release();
                        return FALSE;
                    }
                }
                else
                {
                    DisplayErrorBox(m_wndCaptionBar, 
                    _T("pFileOperation->SetOperationFlags"), hResult);
                    pFileOperation->Release();
                    return FALSE;
                }
            }
            else
            {
                DisplayErrorBox(m_wndCaptionBar, 
                _T("pFileOperation->SetOwnerWindow"), hResult);
                pFileOperation->Release();
                return FALSE;
            }

            pFileOperation->Release();
            pFileOperation = NULL;

            return TRUE;
        }
        else
        {
            OutputDebugString(_T("CoCreateInstance(IFileOperation) failed!\n"));
        }
    }
    return FALSE;
}

How to Move/Rename Files in Windows?

C++
BOOL CFileSystem::MoveFile(CFileSystem* pDestination, CFileList* arrSelection)
{
    HRESULT hResult = S_OK;
    if ((pDestination != NULL) && (arrSelection != NULL))
    {
        IFileOperation* pFileOperation = NULL;
        if (SUCCEEDED(hResult = CoCreateInstance
           (CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pFileOperation))))
        {
            if (SUCCEEDED(hResult = pFileOperation->SetOwnerWindow(m_hWndParent)))
            {
                if (SUCCEEDED(hResult = pFileOperation->SetOperationFlags(
                    FOF_NOCONFIRMMKDIR |
                    FOF_NOERRORUI |
                    FOFX_SHOWELEVATIONPROMPT)))
                {
                    CString strDestination = pDestination->GetFolder();
                    IShellItem* pFolderItem = NULL;
                    if (SUCCEEDED(hResult = SHCreateItemFromParsingName
                       (strDestination, NULL, IID_PPV_ARGS(&pFolderItem))))
                    {
                        if (arrSelection->GetCount() == 1)
                        {
                            CFileData* pFileData = arrSelection->GetAt(0);
                            ASSERT_VALID(pFileData);
                            CString strFileName = pFileData->GetFileName();
                            CString strFolder = GetFolder();
                            CString strFilePath = strFolder + strFileName;

                            IShellItem* pShellItem = NULL;
                            if (SUCCEEDED(hResult = SHCreateItemFromParsingName
                               (strFilePath, NULL, IID_PPV_ARGS(&pShellItem))))
                            {
                                if (SUCCEEDED(hResult = pFileOperation->MoveItem
                                             (pShellItem, pFolderItem, NULL, NULL)))
                                {
                                }
                                else
                                {
                                    DisplayErrorBox(m_wndCaptionBar, 
                                    _T("pFileOperation->MoveItem"), hResult);
                                    pShellItem->Release();
                                    pFolderItem->Release();
                                    pFileOperation->Release();
                                    return FALSE;
                                }

                                pShellItem->Release();
                                pShellItem = NULL;
                            }
                            else
                            {
                                DisplayErrorBox(m_wndCaptionBar, 
                                _T("SHCreateItemFromParsingName"), hResult);
                                pFolderItem->Release();
                                pFileOperation->Release();
                                return FALSE;
                            }
                        }
                        else
                        {
                            const int nCount = (int)arrSelection->GetCount();
                            LPCITEMIDLIST* arrItemIDList = new LPCITEMIDLIST[nCount];
                            for (int nIndex = 0; nIndex < nCount; nIndex++)
                            {
                                CFileData* pFileData = arrSelection->GetAt(nIndex);
                                ASSERT_VALID(pFileData);
                                CString strFileName = pFileData->GetFileName();
                                CString strFolder = GetFolder();
                                CString strFilePath = strFolder + strFileName;

                                arrItemIDList[nIndex] = ILCreateFromPath(strFilePath);
                            }

                            IShellItemArray* pShellItemArray = NULL;
                            if (SUCCEEDED(hResult = SHCreateShellItemArrayFromIDLists
                                         (nCount, arrItemIDList, &pShellItemArray)))
                            {
                                if (SUCCEEDED(hResult = pFileOperation->MoveItems
                                             (pShellItemArray, pFolderItem)))
                                {
                                }
                                else
                                {
                                    DisplayErrorBox(m_wndCaptionBar, 
                                    _T("pFileOperation->MoveItems"), hResult);
                                    pShellItemArray->Release();
                                    pFolderItem->Release();
                                    pFileOperation->Release();
                                    delete arrItemIDList;
                                    return FALSE;
                                }

                                pShellItemArray->Release();
                                pShellItemArray = NULL;
                            }
                            else
                            {
                                DisplayErrorBox(m_wndCaptionBar, 
                                _T("SHCreateShellItemArrayFromIDLists"), hResult);
                                pFileOperation->Release();
                                pFolderItem->Release();
                                delete arrItemIDList;
                                return FALSE;
                            }

                            for (int nIndex = 0; nIndex < nCount; nIndex++)
                            {
                                ILFree((LPITEMIDLIST) arrItemIDList[nIndex]);
                            }
                            delete arrItemIDList;
                            arrItemIDList = NULL;
                        }

                        pFolderItem->Release();
                        pFolderItem = NULL;
                    }
                    else
                    {
                        DisplayErrorBox(m_wndCaptionBar, 
                        _T("SHCreateItemFromParsingName"), hResult);
                        pFileOperation->Release();
                        return FALSE;
                    }

                    if (SUCCEEDED(hResult = pFileOperation->PerformOperations()))
                    {
                    }
                    else
                    {
                        DisplayErrorBox(m_wndCaptionBar, 
                        _T("pFileOperation->PerformOperations"), hResult);
                        pFileOperation->Release();
                        return FALSE;
                    }
                }
                else
                {
                    DisplayErrorBox(m_wndCaptionBar, 
                    _T("pFileOperation->SetOperationFlags"), hResult);
                    pFileOperation->Release();
                    return FALSE;
                }
            }
            else
            {
                DisplayErrorBox(m_wndCaptionBar, 
                _T("pFileOperation->SetOwnerWindow"), hResult);
                pFileOperation->Release();
                return FALSE;
            }

            pFileOperation->Release();
            pFileOperation = NULL;

            return TRUE;
        }
        else
        {
            OutputDebugString(_T("CoCreateInstance(IFileOperation) failed!\n"));
        }
    }
    return FALSE;
}

How to Delete Files in Windows?

C++
BOOL CFileSystem::DeleteFile(CFileSystem* pDestination, CFileList* arrSelection)
{
    HRESULT hResult = S_OK;
    if ((pDestination != NULL) && (arrSelection != NULL))
    {
        IFileOperation* pFileOperation = NULL;
        if (SUCCEEDED(hResult = CoCreateInstance
        (CLSID_FileOperation, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pFileOperation))))
        {
            if (SUCCEEDED(hResult = pFileOperation->SetOwnerWindow(m_hWndParent)))
            {
                if (SUCCEEDED(hResult = pFileOperation->SetOperationFlags(
                    FOF_NOCONFIRMMKDIR |
                    FOF_NOERRORUI |
                    FOFX_SHOWELEVATIONPROMPT)))
                {
                    if (arrSelection->GetCount() == 1)
                    {
                        CFileData* pFileData = arrSelection->GetAt(0);
                        ASSERT_VALID(pFileData);
                        CString strFileName = pFileData->GetFileName();
                        CString strFolder = GetFolder();
                        CString strFilePath = strFolder + strFileName;

                        IShellItem* pShellItem = NULL;
                        if (SUCCEEDED(hResult = SHCreateItemFromParsingName
                           (strFilePath, NULL, IID_PPV_ARGS(&pShellItem))))
                        {
                            if (SUCCEEDED(hResult = pFileOperation->DeleteItem
                                         (pShellItem, NULL)))
                            {
                            }
                            else
                            {
                                DisplayErrorBox(m_wndCaptionBar, 
                                _T("pFileOperation->DeleteItem"), hResult);
                                pShellItem->Release();
                                pFileOperation->Release();
                                return FALSE;
                            }

                            pShellItem->Release();
                            pShellItem = NULL;
                        }
                        else
                        {
                            DisplayErrorBox(m_wndCaptionBar, 
                            _T("SHCreateItemFromParsingName"), hResult);
                            pFileOperation->Release();
                            return FALSE;
                        }
                    }
                    else
                    {
                        const int nCount = (int)arrSelection->GetCount();
                        LPCITEMIDLIST* arrItemIDList = new LPCITEMIDLIST[nCount];
                        for (int nIndex = 0; nIndex < nCount; nIndex++)
                        {
                            CFileData* pFileData = arrSelection->GetAt(nIndex);
                            ASSERT_VALID(pFileData);
                            CString strFileName = pFileData->GetFileName();
                            CString strFolder = GetFolder();
                            CString strFilePath = strFolder + strFileName;

                            arrItemIDList[nIndex] = ILCreateFromPath(strFilePath);
                        }

                        IShellItemArray* pShellItemArray = NULL;
                        if (SUCCEEDED(hResult = SHCreateShellItemArrayFromIDLists
                                     (nCount, arrItemIDList, &pShellItemArray)))
                        {
                            if (SUCCEEDED(hResult = pFileOperation->DeleteItems
                                                    (pShellItemArray)))
                            {
                            }
                            else
                            {
                                DisplayErrorBox(m_wndCaptionBar, 
                                _T("pFileOperation->DeleteItems"), hResult);
                                pShellItemArray->Release();
                                pFileOperation->Release();
                                delete arrItemIDList;
                                return FALSE;
                            }

                            pShellItemArray->Release();
                            pShellItemArray = NULL;
                        }
                        else
                        {
                            DisplayErrorBox(m_wndCaptionBar, 
                            _T("SHCreateShellItemArrayFromIDLists"), hResult);
                            pFileOperation->Release();
                            delete arrItemIDList;
                            return FALSE;
                        }

                        for (int nIndex = 0; nIndex < nCount; nIndex++)
                        {
                            ILFree((LPITEMIDLIST) arrItemIDList[nIndex]);
                        }
                        delete arrItemIDList;
                        arrItemIDList = NULL;
                    }

                    if (SUCCEEDED(hResult = pFileOperation->PerformOperations()))
                    {
                    }
                    else
                    {
                        DisplayErrorBox(m_wndCaptionBar, 
                        _T("pFileOperation->PerformOperations"), hResult);
                        pFileOperation->Release();
                        return FALSE;
                    }
                }
                else
                {
                    DisplayErrorBox(m_wndCaptionBar, 
                    _T("pFileOperation->SetOperationFlags"), hResult);
                    pFileOperation->Release();
                    return FALSE;
                }
            }
            else
            {
                DisplayErrorBox(m_wndCaptionBar, 
                       _T("pFileOperation->SetOwnerWindow"), hResult);
                pFileOperation->Release();
                return FALSE;
            }

            pFileOperation->Release();
            pFileOperation = NULL;

            return TRUE;
        }
        else
        {
            OutputDebugString(_T("CoCreateInstance(IFileOperation) failed!\n"));
        }
    }
    return FALSE;
}

Final Words

IntelliFile application uses many components that have been published on CodeProject. Many thanks to:

  • My CMFCListView form view (see source code)
  • Mizan Rahman for his CWndResizer class
  • PJ Naughter for his CInstanceChecker class
  • PJ Naughter for his CVersionInfo class

History

  • Version 1.01 (13th May, 2022) - Initial release.
  • Version 1.02 (20th May, 2022) - Implemented "New folder" functionality.
  • Version 1.03 (23rd May, 2022) - Implemented "Change drive" functionality.
  • Version 1.04 (24th May, 2022) - Implemented "View text file" functionality.

License

This article, along with any associated source code and files, is licensed under The GNU General Public License (GPLv3)

Share

About the Author

Ștefan-Mihai MOGA
Software Developer NXP Semiconductors
Romania Romania
My professional background includes knowledge of analyst programmer for Microsoft Visual C++, Microsoft Visual C#, Microsoft Visual Basic, Sun Java, assembly for Intel 80x86 microprocessors, assembly for PIC microcontrollers (produced by Microchip Inc.), relational databases (MySQL, Oracle, SQL Server), concurrent version systems, bug tracking systems, web design (HTML5, CSS3, XML, PHP/MySQL, JavaScript).

Comments and Discussions

 
GeneralMy vote of 5 Pin
Member 1370414323-May-22 22:38
MemberMember 1370414323-May-22 22:38 
GeneralRe: My vote of 5 Pin
Ștefan-Mihai MOGA23-May-22 23:26
professionalȘtefan-Mihai MOGA23-May-22 23:26 
SuggestionHandling Links Pin
Member 1172312416-May-22 2:38
MemberMember 1172312416-May-22 2:38 
GeneralMy vote of 3 Pin
Rudolf Jan15-May-22 22:39
MemberRudolf Jan15-May-22 22:39 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.