diff options
author | Daniel Wilhelm <daniel@wili.li> | 2014-04-18 17:15:16 +0200 |
---|---|---|
committer | Daniel Wilhelm <daniel@wili.li> | 2014-04-18 17:15:16 +0200 |
commit | bd6336c629841c6db3a6ca53a936d629d34db53b (patch) | |
tree | 3721ef997864108df175ce677a8a7d4342a6f1d2 /lib/custom_grid.cpp | |
parent | 4.0 (diff) | |
download | FreeFileSync-bd6336c629841c6db3a6ca53a936d629d34db53b.tar.gz FreeFileSync-bd6336c629841c6db3a6ca53a936d629d34db53b.tar.bz2 FreeFileSync-bd6336c629841c6db3a6ca53a936d629d34db53b.zip |
4.1
Diffstat (limited to 'lib/custom_grid.cpp')
-rw-r--r-- | lib/custom_grid.cpp | 2400 |
1 files changed, 2400 insertions, 0 deletions
diff --git a/lib/custom_grid.cpp b/lib/custom_grid.cpp new file mode 100644 index 00000000..6da8b275 --- /dev/null +++ b/lib/custom_grid.cpp @@ -0,0 +1,2400 @@ +// ************************************************************************** +// * This file is part of the FreeFileSync project. It is distributed under * +// * GNU General Public License: http://www.gnu.org/licenses/gpl.html * +// * Copyright (C) 2008-2011 ZenJu (zhnmju123 AT gmx.de) * +// ************************************************************************** + +#include "custom_grid.h" +#include "resources.h" +#include <wx/dc.h> +#include <wx+/format_unit.h> +#include <wx+/string_conv.h> +#include "resources.h" +#include <typeinfo> +#include "../ui/grid_view.h" +#include "../synchronization.h" +#include <wx/dcclient.h> +#include <wx/icon.h> +#include <wx/tooltip.h> +#include <wx/settings.h> + +#ifdef FFS_WIN +#include <wx/timer.h> +#include "status_handler.h" +#include <cmath> + +#elif defined FFS_LINUX +#include <gtk/gtk.h> +#endif + +using namespace zen; + + +const size_t MIN_ROW_COUNT = 15; + +//class containing pure grid data: basically the same as wxGridStringTable, but adds cell formatting + +/* +class hierarchy: + CustomGridTable + /|\ + ________________|________________ + | | + CustomGridTableRim | + /|\ | + __________|__________ | + | | | +CustomGridTableLeft CustomGridTableRight CustomGridTableMiddle +*/ + +class CustomGridTable : public wxGridTableBase +{ +public: + CustomGridTable(int initialRows = 0, int initialCols = 0) : //note: initialRows/initialCols MUST match with GetNumberRows()/GetNumberCols() at initialization!!! + wxGridTableBase(), + gridDataView(NULL), + lastNrRows(initialRows), + lastNrCols(initialCols) {} + + + virtual ~CustomGridTable() {} + + + void setGridDataTable(const GridView* view) + { + this->gridDataView = view; + } + + + //########################################################################### + //grid standard input output methods, redirected directly to gridData to improve performance + + virtual int GetNumberRows() + { + if (gridDataView) + return static_cast<int>(std::max(gridDataView->rowsOnView(), MIN_ROW_COUNT)); + else + return 0; //grid is initialized with zero number of rows + } + + + virtual bool IsEmptyCell(int row, int col) + { + return false; //avoid overlapping cells + + //return (GetValue(row, col) == wxEmptyString); + } + + + virtual void SetValue(int row, int col, const wxString& value) + { + assert (false); //should not be used, since values are retrieved directly from gridDataView + } + + //update dimensions of grid: no need for InsertRows(), AppendRows(), DeleteRows() anymore!!! + void updateGridSizes() + { + const int currentNrRows = GetNumberRows(); + + if (lastNrRows < currentNrRows) + { + if (GetView()) + { + wxGridTableMessage msg(this, + wxGRIDTABLE_NOTIFY_ROWS_APPENDED, + currentNrRows - lastNrRows); + + GetView()->ProcessTableMessage( msg ); + } + } + else if (lastNrRows > currentNrRows) + { + if (GetView()) + { + wxGridTableMessage msg(this, + wxGRIDTABLE_NOTIFY_ROWS_DELETED, + 0, + lastNrRows - currentNrRows); + + GetView()->ProcessTableMessage( msg ); + } + } + lastNrRows = currentNrRows; + + const int currentNrCols = GetNumberCols(); + + if (lastNrCols < currentNrCols) + { + if (GetView()) + { + wxGridTableMessage msg(this, + wxGRIDTABLE_NOTIFY_COLS_APPENDED, + currentNrCols - lastNrCols); + + GetView()->ProcessTableMessage( msg ); + } + } + else if (lastNrCols > currentNrCols) + { + if (GetView()) + { + wxGridTableMessage msg(this, + wxGRIDTABLE_NOTIFY_COLS_DELETED, + 0, + lastNrCols - currentNrCols); + + GetView()->ProcessTableMessage( msg ); + } + } + lastNrCols = currentNrCols; + } + //########################################################################### + + + virtual wxGridCellAttr* GetAttr(int row, int col, wxGridCellAttr::wxAttrKind kind) + { + const std::pair<wxColour, wxColour> color = getRowColor(row); + + //add color to some rows + wxGridCellAttr* result = wxGridTableBase::GetAttr(row, col, kind); + if (result) + { + if (result->GetTextColour() == color.first && + result->GetBackgroundColour() == color.second) + { + return result; + } + else //grid attribute might be referenced by other elements, so clone it! + { + wxGridCellAttr* attr = result->Clone(); //attr has ref-count 1 + result->DecRef(); + result = attr; + } + } + else + result = new wxGridCellAttr; //created with ref-count 1 + + result->SetTextColour (color.first); + result->SetBackgroundColour(color.second); + + return result; + } + + + const FileSystemObject* getRawData(size_t row) const + { + if (gridDataView) + return gridDataView->getObject(row); //returns NULL if request is not valid or not data found + + return NULL; + } + +protected: + static const wxColour COLOR_BLUE; + static const wxColour COLOR_GREY; + static const wxColour COLOR_ORANGE; + static const wxColour COLOR_CMP_RED; + static const wxColour COLOR_CMP_BLUE; + static const wxColour COLOR_CMP_GREEN; + static const wxColour COLOR_SYNC_BLUE; + static const wxColour COLOR_SYNC_BLUE_LIGHT; + static const wxColour COLOR_SYNC_GREEN; + static const wxColour COLOR_SYNC_GREEN_LIGHT; + static const wxColour COLOR_YELLOW; + static const wxColour COLOR_YELLOW_LIGHT; + + const GridView* gridDataView; //(very fast) access to underlying grid data :) + +private: + virtual const std::pair<wxColour, wxColour> getRowColor(int row) = 0; //rows that are filtered out are shown in different color: <foreground, background> + + int lastNrRows; + int lastNrCols; +}; + +//see http://www.latiumsoftware.com/en/articles/00015.php#12 for "safe" colors +const wxColour CustomGridTable::COLOR_ORANGE( 238, 201, 0); +const wxColour CustomGridTable::COLOR_BLUE( 80, 110, 255); +const wxColour CustomGridTable::COLOR_GREY( 212, 208, 200); +const wxColour CustomGridTable::COLOR_CMP_RED( 249, 163, 165); +const wxColour CustomGridTable::COLOR_CMP_BLUE( 144, 232, 246); +const wxColour CustomGridTable::COLOR_CMP_GREEN( 147, 253, 159); +const wxColour CustomGridTable::COLOR_SYNC_BLUE( 201, 203, 247); +const wxColour CustomGridTable::COLOR_SYNC_BLUE_LIGHT(201, 225, 247); +const wxColour CustomGridTable::COLOR_SYNC_GREEN(197, 248, 190); +const wxColour CustomGridTable::COLOR_SYNC_GREEN_LIGHT(226, 248, 190); +const wxColour CustomGridTable::COLOR_YELLOW( 247, 252, 62); +const wxColour CustomGridTable::COLOR_YELLOW_LIGHT(253, 252, 169); + + +class CustomGridTableRim : public CustomGridTable +{ +public: + virtual ~CustomGridTableRim() {} + + virtual int GetNumberCols() + { + return static_cast<int>(columnPositions.size()); + } + + virtual wxString GetColLabelValue( int col ) + { + return CustomGridRim::getTypeName(getTypeAtPos(col)); + } + + + void setupColumns(const std::vector<xmlAccess::ColumnTypes>& positions) + { + columnPositions = positions; + updateGridSizes(); //add or remove columns + } + + + xmlAccess::ColumnTypes getTypeAtPos(size_t pos) const + { + if (pos < columnPositions.size()) + return columnPositions[pos]; + else + return xmlAccess::DIRECTORY; + } + + //get filename in order to retrieve the icon from it + virtual Zstring getIconFile(size_t row) const = 0; //return "folder" if row points to a folder + +protected: + template <SelectedSide side> + wxString GetValueSub(int row, int col) + { + const FileSystemObject* fsObj = getRawData(row); + if (fsObj) + { + struct GetTextValue : public FSObjectVisitor + { + GetTextValue(xmlAccess::ColumnTypes colType, const FileSystemObject& fso) : colType_(colType), fsObj_(fso) {} + virtual void visit(const FileMapping& fileObj) + { + switch (colType_) + { + case xmlAccess::FULL_PATH: + value = toWx(beforeLast(fileObj.getFullName<side>(), FILE_NAME_SEPARATOR)); + break; + case xmlAccess::FILENAME: //filename + value = toWx(fileObj.getShortName<side>()); + break; + case xmlAccess::REL_PATH: //relative path + value = toWx(beforeLast(fileObj.getObjRelativeName(), FILE_NAME_SEPARATOR)); //returns empty string if ch not found + break; + case xmlAccess::DIRECTORY: + value = toWx(fileObj.getBaseDirPf<side>()); + break; + case xmlAccess::SIZE: //file size + if (!fsObj_.isEmpty<side>()) + value = zen::toStringSep(fileObj.getFileSize<side>()); + break; + case xmlAccess::DATE: //date + if (!fsObj_.isEmpty<side>()) + value = zen::utcToLocalTimeString(fileObj.getLastWriteTime<side>()); + break; + case xmlAccess::EXTENSION: //file extension + value = toWx(fileObj.getExtension<side>()); + break; + } + } + + virtual void visit(const SymLinkMapping& linkObj) + { + switch (colType_) + { + case xmlAccess::FULL_PATH: + value = toWx(beforeLast(linkObj.getFullName<side>(), FILE_NAME_SEPARATOR)); + break; + case xmlAccess::FILENAME: //filename + value = toWx(linkObj.getShortName<side>()); + break; + case xmlAccess::REL_PATH: //relative path + value = toWx(beforeLast(linkObj.getObjRelativeName(), FILE_NAME_SEPARATOR)); //returns empty string if ch not found + break; + case xmlAccess::DIRECTORY: + value = toWx(linkObj.getBaseDirPf<side>()); + break; + case xmlAccess::SIZE: //file size + if (!fsObj_.isEmpty<side>()) + value = _("<Symlink>"); + break; + case xmlAccess::DATE: //date + if (!fsObj_.isEmpty<side>()) + value = zen::utcToLocalTimeString(linkObj.getLastWriteTime<side>()); + break; + case xmlAccess::EXTENSION: //file extension + value = wxEmptyString; + break; + } + } + + virtual void visit(const DirMapping& dirObj) + { + switch (colType_) + { + case xmlAccess::FULL_PATH: + value = toWx(dirObj.getFullName<side>()); + break; + case xmlAccess::FILENAME: + value = toWx(dirObj.getShortName<side>()); + break; + case xmlAccess::REL_PATH: + value = toWx(beforeLast(dirObj.getObjRelativeName(), FILE_NAME_SEPARATOR)); //returns empty string if ch not found + break; + case xmlAccess::DIRECTORY: + value = toWx(dirObj.getBaseDirPf<side>()); + break; + case xmlAccess::SIZE: //file size + if (!fsObj_.isEmpty<side>()) + value = _("<Directory>"); + break; + case xmlAccess::DATE: //date + if (!fsObj_.isEmpty<side>()) + value = wxEmptyString; + break; + case xmlAccess::EXTENSION: //file extension + value = wxEmptyString; + break; + } + } + xmlAccess::ColumnTypes colType_; + wxString value; + + const FileSystemObject& fsObj_; + } getVal(getTypeAtPos(col), *fsObj); + fsObj->accept(getVal); + return getVal.value; + } + //if data is not found: + return wxEmptyString; + } + + template <SelectedSide side> + Zstring getIconFileImpl(size_t row) const //return "folder" if row points to a folder + { + const FileSystemObject* fsObj = getRawData(row); + if (fsObj && !fsObj->isEmpty<side>()) + { + struct GetIcon : public FSObjectVisitor + { + virtual void visit(const FileMapping& fileObj) + { + //Optimization: if filename exists on both sides, always use left side's file + //if (!fileObj.isEmpty<LEFT_SIDE>() && !fileObj.isEmpty<RIGHT_SIDE>()) + // iconName = fileObj.getFullName<LEFT_SIDE>(); + //else -> now with thumbnails this isn't viable anymore + iconName = fileObj.getFullName<side>(); + } + virtual void visit(const SymLinkMapping& linkObj) + { + iconName = linkObj.getLinkType<side>() == LinkDescriptor::TYPE_DIR ? + Zstr("folder") : + linkObj.getFullName<side>(); + } + virtual void visit(const DirMapping& dirObj) + { + iconName = Zstr("folder"); + } + + Zstring iconName; + } getIcon; + fsObj->accept(getIcon); + return getIcon.iconName; + } + + return Zstring(); + } + + +private: + virtual const std::pair<wxColour, wxColour> getRowColor(int row) //rows that are filtered out are shown in different color: <foreground, background> + { + std::pair<wxColour, wxColour> result(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + const FileSystemObject* fsObj = getRawData(row); + if (fsObj) + { + //mark filtered rows + if (!fsObj->isActive()) + { + result.first = *wxBLACK; + result.second = COLOR_BLUE; + } + else + { + //mark directories and symlinks + struct GetRowColor : public FSObjectVisitor + { + GetRowColor(wxColour& foreground, wxColour& background) : foreground_(foreground), background_(background) {} + + virtual void visit(const FileMapping& fileObj) {} + virtual void visit(const SymLinkMapping& linkObj) + { + foreground_ = *wxBLACK; + background_ = COLOR_ORANGE; + } + virtual void visit(const DirMapping& dirObj) + { + foreground_ = *wxBLACK; + background_ = COLOR_GREY; + } + + private: + wxColour& foreground_; + wxColour& background_; + } getCol(result.first, result.second); + fsObj->accept(getCol); + } + } + + return result; + } + + std::vector<xmlAccess::ColumnTypes> columnPositions; +}; + + +class CustomGridTableLeft : public CustomGridTableRim +{ +public: + + virtual wxString GetValue(int row, int col) + { + return CustomGridTableRim::GetValueSub<LEFT_SIDE>(row, col); + } + + virtual Zstring getIconFile(size_t row) const //return "folder" if row points to a folder + { + return getIconFileImpl<LEFT_SIDE>(row); + } +}; + + +class CustomGridTableRight : public CustomGridTableRim +{ +public: + virtual wxString GetValue(int row, int col) + { + return CustomGridTableRim::GetValueSub<RIGHT_SIDE>(row, col); + } + + virtual Zstring getIconFile(size_t row) const //return "folder" if row points to a folder + { + return getIconFileImpl<RIGHT_SIDE>(row); + } +}; + + +class CustomGridTableMiddle : public CustomGridTable +{ +public: + //middle grid is created (first wxWidgets internal call to GetNumberCols()) with one column + CustomGridTableMiddle() : + CustomGridTable(0, GetNumberCols()), //attention: static binding to virtual GetNumberCols() in a Constructor! + syncPreviewActive(false) {} + + virtual int GetNumberCols() + { + return 1; + } + + virtual wxString GetColLabelValue( int col ) + { + return wxEmptyString; + } + + virtual wxString GetValue(int row, int col) //method used for exporting .csv file only! + { + const FileSystemObject* fsObj = getRawData(row); + if (fsObj) + { + if (syncPreviewActive) //synchronization preview + return getSymbol(fsObj->getSyncOperation()); + else + return getSymbol(fsObj->getCategory()); + } + return wxEmptyString; + } + + void enableSyncPreview(bool value) + { + syncPreviewActive = value; + } + + bool syncPreviewIsActive() const + { + return syncPreviewActive; + } + +private: + virtual const std::pair<wxColour, wxColour> getRowColor(int row) //rows that are filtered out are shown in different color: <foreground, background> + { + std::pair<wxColour, wxColour> result(wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT), + wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW)); + + const FileSystemObject* fsObj = getRawData(row); + if (fsObj) + { + //mark filtered rows + if (!fsObj->isActive()) + { + result.first = *wxBLACK;; + result.second = COLOR_BLUE; + } + else + { + if (syncPreviewActive) //synchronization preview + { + switch (fsObj->getSyncOperation()) //evaluate comparison result and sync direction + { + case SO_DO_NOTHING: + case SO_EQUAL: + break;//usually white + case SO_CREATE_NEW_LEFT: + case SO_OVERWRITE_LEFT: + case SO_DELETE_LEFT: + result.first = *wxBLACK; + result.second = COLOR_SYNC_BLUE; + break; + case SO_COPY_METADATA_TO_LEFT: + result.first = *wxBLACK; + result.second = COLOR_SYNC_BLUE_LIGHT; + break; + case SO_CREATE_NEW_RIGHT: + case SO_OVERWRITE_RIGHT: + case SO_DELETE_RIGHT: + result.first = *wxBLACK; + result.second = COLOR_SYNC_GREEN; + break; + case SO_COPY_METADATA_TO_RIGHT: + result.first = *wxBLACK; + result.second = COLOR_SYNC_GREEN_LIGHT; + break; + case SO_UNRESOLVED_CONFLICT: + result.first = *wxBLACK; + result.second = COLOR_YELLOW; + break; + } + } + else //comparison results view + { + switch (fsObj->getCategory()) + { + case FILE_LEFT_SIDE_ONLY: + case FILE_LEFT_NEWER: + result.first = *wxBLACK; + result.second = COLOR_SYNC_BLUE; //COLOR_CMP_BLUE; + break; + + case FILE_RIGHT_SIDE_ONLY: + case FILE_RIGHT_NEWER: + result.first = *wxBLACK; + result.second = COLOR_SYNC_GREEN; //COLOR_CMP_GREEN; + break; + case FILE_DIFFERENT: + result.first = *wxBLACK; + result.second = COLOR_CMP_RED; + break; + case FILE_EQUAL: + break;//usually white + case FILE_CONFLICT: + result.first = *wxBLACK; + result.second = COLOR_YELLOW; + break; + case FILE_DIFFERENT_METADATA: + result.first = *wxBLACK; + result.second = COLOR_YELLOW_LIGHT; + break; + } + } + } + } + + return result; + } + + bool syncPreviewActive; //determines wheter grid shall show compare result or sync preview +}; + +//######################################################################################################## + + +CustomGrid::CustomGrid(wxWindow* parent, + wxWindowID id, + const wxPoint& pos, + const wxSize& size, + long style, + const wxString& name) : + wxGrid(parent, id, pos, size, style, name), + m_gridLeft(NULL), + m_gridMiddle(NULL), + m_gridRight(NULL), + isLeading(false), + m_marker(-1, ASCENDING) +{ + //wxColour darkBlue(40, 35, 140); -> user default colors instead! + //SetSelectionBackground(wxSystemSettings::GetColour(wxSYS_COLOUR_HIGHLIGHT)); + //SetSelectionForeground(*wxWHITE); +} + + +void CustomGrid::initSettings(CustomGridLeft* gridLeft, + CustomGridMiddle* gridMiddle, + CustomGridRight* gridRight, + const GridView* gridDataView) +{ + assert(this == gridLeft || this == gridRight || this == gridMiddle); + + //these grids will scroll together + m_gridLeft = gridLeft; + m_gridRight = gridRight; + m_gridMiddle = gridMiddle; + + //enhance grid functionality; identify leading grid by keyboard input or scroll action + Connect(wxEVT_KEY_DOWN, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + Connect(wxEVT_SCROLLWIN_TOP, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + Connect(wxEVT_SCROLLWIN_BOTTOM, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + Connect(wxEVT_SCROLLWIN_LINEUP, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + Connect(wxEVT_SCROLLWIN_LINEDOWN, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + Connect(wxEVT_SCROLLWIN_PAGEUP, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + Connect(wxEVT_SCROLLWIN_PAGEDOWN, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + Connect(wxEVT_SCROLLWIN_THUMBTRACK, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + Connect(wxEVT_SCROLLWIN_THUMBRELEASE, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + Connect(wxEVT_GRID_LABEL_LEFT_CLICK, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + Connect(wxEVT_SET_FOCUS, wxEventHandler(CustomGrid::onGridAccess), NULL, this); //used by grid text-search + GetGridWindow()->Connect(wxEVT_LEFT_DOWN, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + GetGridWindow()->Connect(wxEVT_RIGHT_DOWN, wxEventHandler(CustomGrid::onGridAccess), NULL, this); + + GetGridWindow()->Connect(wxEVT_ENTER_WINDOW, wxEventHandler(CustomGrid::adjustGridHeights), NULL, this); + + //parallel grid scrolling: do NOT use DoPrepareDC() to align grids! GDI resource leak! Use regular paint event instead: + GetGridWindow()->Connect(wxEVT_PAINT, wxEventHandler(CustomGrid::OnPaintGrid), NULL, this); +} + + +void CustomGrid::release() //release connection to zen::GridView +{ + assert(getGridDataTable()); + getGridDataTable()->setGridDataTable(NULL); //kind of "disable" griddatatable; don't delete it with SetTable(NULL)! May be used by wxGridCellStringRenderer +} + + +bool CustomGrid::isLeadGrid() const +{ + return isLeading; +} + + +void CustomGrid::setIconManager(const std::shared_ptr<IconBuffer>& iconBuffer) +{ + if (iconBuffer.get()) + SetDefaultRowSize(iconBuffer->getSize() + 1, true); //+ 1 for line between rows + else + SetDefaultRowSize(IconBuffer(IconBuffer::SIZE_SMALL).getSize() + 1, true); //currently iconBuffer is always bound, but we may use it as a "no icon" status at some time... + + enableFileIcons(iconBuffer); + Refresh(); +} + + +void CustomGrid::RefreshCell(int row, int col) +{ + wxRect rectScrolled(CellToRect(row, col)); + //use: wxRect rect = CellToRect( row, col ); ? + CalcScrolledPosition(rectScrolled.x, rectScrolled.y, &rectScrolled.x, &rectScrolled.y); + + GetGridWindow()->RefreshRect(rectScrolled); //note: CellToRect() and YToRow work on m_gridWindow NOT on the whole grid! +} + + +void CustomGrid::OnPaintGrid(wxEvent& event) +{ + if (isLeadGrid()) //avoid back coupling + alignOtherGrids(m_gridLeft, m_gridMiddle, m_gridRight); //scroll other grids + event.Skip(); +} + + +void moveCursorWhileSelecting(int anchor, int oldPos, int newPos, wxGrid* grid) +{ + //note: all positions are valid in this context! + + grid->SetGridCursor( newPos, grid->GetGridCursorCol()); + grid->MakeCellVisible(newPos, grid->GetGridCursorCol()); + + if (oldPos < newPos) + { + for (int i = oldPos; i < std::min(anchor, newPos); ++i) + grid->DeselectRow(i); //remove selection + + for (int i = std::max(oldPos, anchor); i <= newPos; ++i) + grid->SelectRow(i, true); //add to selection + } + else + { + for (int i = std::max(newPos, anchor) + 1; i <= oldPos; ++i) + grid->DeselectRow(i); //remove selection + + for (int i = newPos; i <= std::min(oldPos, anchor); ++i) + grid->SelectRow(i, true); //add to selection + } +} + + +void execGridCommands(wxEvent& event, wxGrid* grid) +{ + static int anchorRow = 0; + if (grid->GetNumberRows() == 0 || + grid->GetNumberCols() == 0) + return; + + const wxKeyEvent* keyEvent = dynamic_cast<const wxKeyEvent*> (&event); + if (keyEvent) + { + //ensure cursorOldPos is always a valid row! + const int cursorOldPos = std::max(std::min(grid->GetGridCursorRow(), grid->GetNumberRows() - 1), 0); + const int cursorOldColumn = std::max(std::min(grid->GetGridCursorCol(), grid->GetNumberCols() - 1), 0); + + const bool shiftPressed = keyEvent->ShiftDown(); + const bool ctrlPressed = keyEvent->ControlDown(); + const int keyCode = keyEvent->GetKeyCode(); + + //SHIFT + X + if (shiftPressed) + switch (keyCode) + { + case WXK_UP: + case WXK_NUMPAD_UP: + { + const int cursorNewPos = std::max(cursorOldPos - 1, 0); + moveCursorWhileSelecting(anchorRow, cursorOldPos, cursorNewPos, grid); + return; //no event.Skip() + } + case WXK_DOWN: + case WXK_NUMPAD_DOWN: + { + const int cursorNewPos = std::min(cursorOldPos + 1, grid->GetNumberRows() - 1); + moveCursorWhileSelecting(anchorRow, cursorOldPos, cursorNewPos, grid); + return; //no event.Skip() + } + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + { + const int cursorColumn = std::max(cursorOldColumn - 1, 0); + grid->SetGridCursor(cursorOldPos, cursorColumn); + grid->MakeCellVisible(cursorOldPos, cursorColumn); + return; //no event.Skip() + } + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + { + const int cursorColumn = std::min(cursorOldColumn + 1, grid->GetNumberCols() - 1); + grid->SetGridCursor(cursorOldPos, cursorColumn); + grid->MakeCellVisible(cursorOldPos, cursorColumn); + return; //no event.Skip() + } + case WXK_PAGEUP: + case WXK_NUMPAD_PAGEUP: + { + const int rowsPerPage = grid->GetGridWindow()->GetSize().GetHeight() / grid->GetDefaultRowSize(); + const int cursorNewPos = std::max(cursorOldPos - rowsPerPage, 0); + moveCursorWhileSelecting(anchorRow, cursorOldPos, cursorNewPos, grid); + return; //no event.Skip() + } + case WXK_PAGEDOWN: + case WXK_NUMPAD_PAGEDOWN: + { + const int rowsPerPage = grid->GetGridWindow()->GetSize().GetHeight() / grid->GetDefaultRowSize(); + const int cursorNewPos = std::min(cursorOldPos + rowsPerPage, grid->GetNumberRows() - 1); + moveCursorWhileSelecting(anchorRow, cursorOldPos, cursorNewPos, grid); + return; //no event.Skip() + } + case WXK_HOME: + case WXK_NUMPAD_HOME: + { + const int cursorNewPos = 0; + moveCursorWhileSelecting(anchorRow, cursorOldPos, cursorNewPos, grid); + return; //no event.Skip() + } + case WXK_END: + case WXK_NUMPAD_END: + { + const int cursorNewPos = grid->GetNumberRows() - 1; + moveCursorWhileSelecting(anchorRow, cursorOldPos, cursorNewPos, grid); + return; //no event.Skip() + } + } + + //CTRL + X + if (ctrlPressed) + switch (keyCode) + { + case WXK_UP: + case WXK_NUMPAD_UP: + { + grid->SetGridCursor(0, grid->GetGridCursorCol()); + grid->MakeCellVisible(0, grid->GetGridCursorCol()); + return; //no event.Skip() + } + case WXK_DOWN: + case WXK_NUMPAD_DOWN: + { + grid->SetGridCursor(grid->GetNumberRows() - 1, grid->GetGridCursorCol()); + grid->MakeCellVisible(grid->GetNumberRows() - 1, grid->GetGridCursorCol()); + return; //no event.Skip() + } + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + { + grid->SetGridCursor(grid->GetGridCursorRow(), 0); + grid->MakeCellVisible(grid->GetGridCursorRow(), 0); + return; //no event.Skip() + } + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + { + grid->SetGridCursor(grid->GetGridCursorRow(), grid->GetNumberCols() - 1); + grid->MakeCellVisible(grid->GetGridCursorRow(), grid->GetNumberCols() - 1); + return; //no event.Skip() + } + } + + //button with or without control keys pressed + switch (keyCode) + { + case WXK_HOME: + case WXK_NUMPAD_HOME: + { + grid->SetGridCursor(0, grid->GetGridCursorCol()); + grid->MakeCellVisible(0, grid->GetGridCursorCol()); + return; //no event.Skip() + } + case WXK_END: + case WXK_NUMPAD_END: + { + grid->SetGridCursor(grid->GetNumberRows() - 1, grid->GetGridCursorCol()); + grid->MakeCellVisible(grid->GetNumberRows() - 1, grid->GetGridCursorCol()); + return; //no event.Skip() + } + + case WXK_PAGEUP: + case WXK_NUMPAD_PAGEUP: + { + const int rowsPerPage = grid->GetGridWindow()->GetSize().GetHeight() / grid->GetDefaultRowSize(); + const int cursorNewPos = std::max(cursorOldPos - rowsPerPage, 0); + grid->SetGridCursor(cursorNewPos, grid->GetGridCursorCol()); + grid->MakeCellVisible(cursorNewPos, grid->GetGridCursorCol()); + return; //no event.Skip() + } + case WXK_PAGEDOWN: + case WXK_NUMPAD_PAGEDOWN: + { + const int rowsPerPage = grid->GetGridWindow()->GetSize().GetHeight() / grid->GetDefaultRowSize(); + const int cursorNewPos = std::min(cursorOldPos + rowsPerPage, grid->GetNumberRows() - 1); + grid->SetGridCursor(cursorNewPos, grid->GetGridCursorCol()); + grid->MakeCellVisible(cursorNewPos, grid->GetGridCursorCol()); + return; //no event.Skip() + } + } + + //button without additonal control keys pressed + if (keyEvent->GetModifiers() == wxMOD_NONE) + switch (keyCode) + { + case WXK_UP: + case WXK_NUMPAD_UP: + { + const int cursorNewPos = std::max(cursorOldPos - 1, 0); + grid->SetGridCursor(cursorNewPos, grid->GetGridCursorCol()); + grid->MakeCellVisible(cursorNewPos, grid->GetGridCursorCol()); + return; //no event.Skip() + } + case WXK_DOWN: + case WXK_NUMPAD_DOWN: + { + const int cursorNewPos = std::min(cursorOldPos + 1, grid->GetNumberRows() - 1); + grid->SetGridCursor(cursorNewPos, grid->GetGridCursorCol()); + grid->MakeCellVisible(cursorNewPos, grid->GetGridCursorCol()); + return; //no event.Skip() + } + case WXK_LEFT: + case WXK_NUMPAD_LEFT: + { + const int cursorColumn = std::max(cursorOldColumn - 1, 0); + grid->SetGridCursor(cursorOldPos, cursorColumn); + grid->MakeCellVisible(cursorOldPos, cursorColumn); + return; //no event.Skip() + } + case WXK_RIGHT: + case WXK_NUMPAD_RIGHT: + { + const int cursorColumn = std::min(cursorOldColumn + 1, grid->GetNumberCols() - 1); + grid->SetGridCursor(cursorOldPos, cursorColumn); + grid->MakeCellVisible(cursorOldPos, cursorColumn); + return; //no event.Skip() + } + } + } + + anchorRow = grid->GetGridCursorRow(); + event.Skip(); //let event delegate! +} + + +inline +bool gridsShouldBeCleared(const wxEvent& event) +{ + const wxMouseEvent* mouseEvent = dynamic_cast<const wxMouseEvent*>(&event); + if (mouseEvent) + { + if (mouseEvent->ControlDown() || mouseEvent->ShiftDown()) + return false; + + if (mouseEvent->ButtonDown(wxMOUSE_BTN_LEFT)) + return true; + } + else + { + const wxKeyEvent* keyEvent = dynamic_cast<const wxKeyEvent*>(&event); + if (keyEvent) + { + if (keyEvent->ControlDown() || keyEvent->AltDown() || keyEvent->ShiftDown()) + return false; + + switch (keyEvent->GetKeyCode()) + { + //default navigation keys + case WXK_UP: + case WXK_DOWN: + case WXK_LEFT: + case WXK_RIGHT: + case WXK_PAGEUP: + case WXK_PAGEDOWN: + case WXK_HOME: + case WXK_END: + case WXK_NUMPAD_UP: + case WXK_NUMPAD_DOWN: + case WXK_NUMPAD_LEFT: + case WXK_NUMPAD_RIGHT: + case WXK_NUMPAD_PAGEUP: + case WXK_NUMPAD_PAGEDOWN: + case WXK_NUMPAD_HOME: + case WXK_NUMPAD_END: + //other keys + case WXK_TAB: + case WXK_RETURN: + case WXK_NUMPAD_ENTER: + case WXK_ESCAPE: + return true; + } + } + } + + return false; +} + + +void CustomGrid::onGridAccess(wxEvent& event) +{ + if (!isLeading) + { + //notify other grids of new user focus + m_gridLeft ->isLeading = m_gridLeft == this; + m_gridMiddle->isLeading = m_gridMiddle == this; + m_gridRight ->isLeading = m_gridRight == this; + + wxGrid::SetFocus(); + } + + //clear grids + if (gridsShouldBeCleared(event)) + { + m_gridLeft ->ClearSelection(); + m_gridMiddle->ClearSelection(); + m_gridRight ->ClearSelection(); + } + + //update row labels NOW (needed when scrolling if buttons keep being pressed) + m_gridLeft ->GetGridRowLabelWindow()->Update(); + m_gridRight->GetGridRowLabelWindow()->Update(); + + //support for custom short-cuts (overwriting wxWidgets functionality!) + execGridCommands(event, this); //event.Skip is handled here! +} + + +//workaround: ensure that all grids are properly aligned: add some extra window space to grids that have no horizontal scrollbar +void CustomGrid::adjustGridHeights(wxEvent& event) +{ + //m_gridLeft, m_gridRight, m_gridMiddle not NULL because called after initSettings() + + int y1 = 0; + int y2 = 0; + int y3 = 0; + int dummy = 0; + + m_gridLeft ->GetViewStart(&dummy, &y1); + m_gridRight ->GetViewStart(&dummy, &y2); + m_gridMiddle->GetViewStart(&dummy, &y3); + + if (y1 != y2 || y2 != y3) + { + int yMax = std::max(y1, std::max(y2, y3)); + + if (m_gridLeft->isLeadGrid()) //do not handle case (y1 == yMax) here!!! Avoid back coupling! + m_gridLeft->SetMargins(0, 0); + else if (y1 < yMax) + m_gridLeft->SetMargins(0, 30); + + if (m_gridRight->isLeadGrid()) + m_gridRight->SetMargins(0, 0); + else if (y2 < yMax) + m_gridRight->SetMargins(0, 30); + + if (m_gridMiddle->isLeadGrid()) + m_gridMiddle->SetMargins(0, 0); + else if (y3 < yMax) + m_gridMiddle->SetMargins(0, 30); + + m_gridLeft ->ForceRefresh(); + m_gridRight ->ForceRefresh(); + m_gridMiddle->ForceRefresh(); + } +} + + +void CustomGrid::updateGridSizes() +{ + if (getGridDataTable()) + getGridDataTable()->updateGridSizes(); +} + + +void CustomGridRim::updateGridSizes() +{ + CustomGrid::updateGridSizes(); + + //set row label size + + //SetRowLabelSize(wxGRID_AUTOSIZE); -> we can do better + wxClientDC dc(GetGridRowLabelWindow()); + dc.SetFont(GetLabelFont()); + + wxArrayString lines; + lines.push_back(GetRowLabelValue(GetNumberRows())); + + long width = 0; + long dummy = 0; + GetTextBoxSize(dc, lines, &width, &dummy); + + width += 8; + SetRowLabelSize(width); +} + + +void CustomGrid::setSortMarker(SortMarker marker) +{ + m_marker = marker; +} + + +void CustomGrid::DrawColLabel(wxDC& dc, int col) +{ + wxGrid::DrawColLabel(dc, col); + + if (col == m_marker.first) + { + if (m_marker.second == ASCENDING) + dc.DrawBitmap(GlobalResources::getImage(wxT("smallUp")), GetColRight(col) - 16 - 2, 2, true); //respect 2-pixel border + else + dc.DrawBitmap(GlobalResources::getImage(wxT("smallDown")), GetColRight(col) - 16 - 2, 2, true); //respect 2-pixel border + } +} + + +std::pair<int, int> CustomGrid::mousePosToCell(wxPoint pos) +{ + int x = -1; + int y = -1; + CalcUnscrolledPosition(pos.x, pos.y, &x, &y); + + std::pair<int, int> output(-1, -1); + if (x >= 0 && y >= 0) + { + output.first = YToRow(y); + output.second = XToCol(x); + } + return output; +} + + +std::set<size_t> CustomGrid::getAllSelectedRows() const +{ + std::set<size_t> output; + + const wxArrayInt selectedRows = this->GetSelectedRows(); + if (!selectedRows.IsEmpty()) + { + for (size_t i = 0; i < selectedRows.GetCount(); ++i) + output.insert(selectedRows[i]); + } + + if (!this->GetSelectedCols().IsEmpty()) //if a column is selected this is means all rows are marked for deletion + { + for (int k = 0; k < const_cast<CustomGrid*>(this)->GetNumberRows(); ++k) //messy wxGrid implementation... + output.insert(k); + } + + const wxGridCellCoordsArray singlySelected = this->GetSelectedCells(); + if (!singlySelected.IsEmpty()) + { + for (size_t k = 0; k < singlySelected.GetCount(); ++k) + output.insert(singlySelected[k].GetRow()); + } + + const wxGridCellCoordsArray tmpArrayTop = this->GetSelectionBlockTopLeft(); + if (!tmpArrayTop.IsEmpty()) + { + wxGridCellCoordsArray tmpArrayBottom = this->GetSelectionBlockBottomRight(); + + size_t arrayCount = tmpArrayTop.GetCount(); + + if (arrayCount == tmpArrayBottom.GetCount()) + { + for (size_t i = 0; i < arrayCount; ++i) + { + const int rowTop = tmpArrayTop[i].GetRow(); + const int rowBottom = tmpArrayBottom[i].GetRow(); + + for (int k = rowTop; k <= rowBottom; ++k) + output.insert(k); + } + } + } + + //some exception: also add current cursor row to selection if there are no others... hopefully improving usability + if (output.empty() && this->isLeadGrid()) + output.insert(const_cast<CustomGrid*>(this)->GetCursorRow()); //messy wxGrid implementation... + + return output; +} + + +//############################################################################################ +//CustomGrid specializations + +class GridCellRenderer : public wxGridCellStringRenderer +{ +public: + GridCellRenderer(CustomGridRim::FailedIconLoad& failedLoads, + const CustomGridTableRim* gridDataTable, + const std::shared_ptr<zen::IconBuffer>& iconBuffer) : + failedLoads_(failedLoads), + m_gridDataTable(gridDataTable), + iconBuffer_(iconBuffer) {} + + + virtual void Draw(wxGrid& grid, + wxGridCellAttr& attr, + wxDC& dc, + const wxRect& rect, //unscrolled rect + int row, int col, + bool isSelected) + { + //############## show windows explorer file icons ###################### + + if (iconBuffer_.get() && + m_gridDataTable->getTypeAtPos(col) == xmlAccess::FILENAME) + { + const int iconSize = iconBuffer_->getSize(); + if (rect.GetWidth() >= iconSize) + { + // Partitioning: + // ____________________________ + // | 2 pix border | icon | rest | + // ---------------------------- + { + //clear area where icon will be placed (including border) + wxRect rectShrinked(rect); + rectShrinked.SetWidth(LEFT_BORDER + iconSize); //add 2 pixel border + wxGridCellRenderer::Draw(grid, attr, dc, rectShrinked, row, col, isSelected); + } + + { + //draw rest + wxRect rest(rect); //unscrolled + rest.x += LEFT_BORDER + iconSize; + rest.width -= LEFT_BORDER + iconSize; + wxGridCellStringRenderer::Draw(grid, attr, dc, rest, row, col, isSelected); + } + + wxRect rectIcon(rect); + rectIcon.SetWidth(iconSize); //set to icon area only + rectIcon.x += LEFT_BORDER; // + + //try to draw icon + //retrieve grid data + const Zstring fileName = m_gridDataTable->getIconFile(row); + if (!fileName.empty()) + { + wxIcon icon; + + //first check if it is a directory icon: + if (fileName == Zstr("folder")) + icon = iconBuffer_->genericDirIcon(); + else //retrieve file icon + { + if (!iconBuffer_->requestFileIcon(fileName, &icon)) //returns false if icon is not in buffer + { + icon = iconBuffer_->genericFileIcon(); //better than nothing + + failedLoads_.insert(row); //save status of failed icon load -> used for async. icon loading + //falsify only! we want to avoid writing incorrect success values when only partially updating the DC, e.g. when scrolling, + //see repaint behavior of ::ScrollWindow() function! + } + } + + if (icon.IsOk()) + { + int posX = rectIcon.GetX(); + int posY = rectIcon.GetY(); + //center icon if it is too small + if (rectIcon.GetWidth() > icon.GetWidth()) + posX += (rectIcon.GetWidth() - icon.GetWidth()) / 2; + if (rectIcon.GetHeight() > icon.GetHeight()) + posY += (rectIcon.GetHeight() - icon.GetHeight()) / 2; + + dc.DrawIcon(icon, posX, posY); + } + } + return; + } + } + + //default + wxGridCellStringRenderer::Draw(grid, attr, dc, rect, row, col, isSelected); + } + + + virtual wxSize GetBestSize(wxGrid& grid, //adapt reported width if file icons are shown + wxGridCellAttr& attr, + wxDC& dc, + int row, int col) + { + if (iconBuffer_.get() && //evaluate at compile time + m_gridDataTable->getTypeAtPos(col) == xmlAccess::FILENAME) + { + wxSize rv = wxGridCellStringRenderer::GetBestSize(grid, attr, dc, row, col); + rv.SetWidth(rv.GetWidth() + LEFT_BORDER + iconBuffer_->getSize()); + return rv; + } + + //default + return wxGridCellStringRenderer::GetBestSize(grid, attr, dc, row, col); + } + + +private: + CustomGridRim::FailedIconLoad& failedLoads_; + const CustomGridTableRim* const m_gridDataTable; + std::shared_ptr<zen::IconBuffer> iconBuffer_; + + static const int LEFT_BORDER = 2; +}; + +//---------------------------------------------------------------------------------------- + +CustomGridRim::CustomGridRim(wxWindow* parent, + wxWindowID id, + const wxPoint& pos, + const wxSize& size, + long style, + const wxString& name) : + CustomGrid(parent, id, pos, size, style, name), otherGrid(NULL) +{ + Connect(wxEVT_GRID_COL_SIZE, wxGridSizeEventHandler(CustomGridRim::OnResizeColumn), NULL, this); //row-based tooltip +} + + +void CustomGridRim::setOtherGrid(CustomGridRim* other) //call during initialization! +{ + otherGrid = other; +} + + +void CustomGridRim::OnResizeColumn(wxGridSizeEvent& event) +{ + //Resize columns on both sides in parallel + const int thisCol = event.GetRowOrCol(); + + if (!otherGrid || thisCol < 0 || thisCol >= GetNumberCols()) return; + + const xmlAccess::ColumnTypes thisColType = getTypeAtPos(thisCol); + + for (int i = 0; i < otherGrid->GetNumberCols(); ++i) + if (otherGrid->getTypeAtPos(i) == thisColType) + { + otherGrid->SetColSize(i, GetColSize(thisCol)); + otherGrid->ForceRefresh(); + break; + } +} + + +//this method is called when grid view changes: useful for parallel updating of multiple grids +void CustomGridRim::alignOtherGrids(CustomGrid* gridLeft, CustomGrid* gridMiddle, CustomGrid* gridRight) +{ + if (!otherGrid) return; + + int x = 0; + int y = 0; + GetViewStart(&x, &y); + gridMiddle->Scroll(-1, y); + otherGrid->Scroll(x, y); +} + + +template <SelectedSide side> +void CustomGridRim::setTooltip(const wxMouseEvent& event) +{ + const int hoveredRow = mousePosToCell(event.GetPosition()).first; + + wxString toolTip; + if (hoveredRow >= 0 && getGridDataTable() != NULL) + { + const FileSystemObject* const fsObj = getGridDataTable()->getRawData(hoveredRow); + if (fsObj && !fsObj->isEmpty<side>()) + { + struct AssembleTooltip : public FSObjectVisitor + { + AssembleTooltip(wxString& tipMsg) : tipMsg_(tipMsg) {} + + virtual void visit(const FileMapping& fileObj) + { + tipMsg_ = toWx(fileObj.getRelativeName<side>()) + "\n" + + _("Size") + ": " + zen::filesizeToShortString(fileObj.getFileSize<side>()) + "\n" + + _("Date") + ": " + zen::utcToLocalTimeString(fileObj.getLastWriteTime<side>()); + } + + virtual void visit(const SymLinkMapping& linkObj) + { + tipMsg_ = toWx(linkObj.getRelativeName<side>()) + "\n" + + _("Date") + ": " + zen::utcToLocalTimeString(linkObj.getLastWriteTime<side>()); + } + + virtual void visit(const DirMapping& dirObj) + { + tipMsg_ = toWx(dirObj.getRelativeName<side>()); + } + + wxString& tipMsg_; + } assembler(toolTip); + fsObj->accept(assembler); + } + } + + + wxToolTip* tt = GetGridWindow()->GetToolTip(); + + const wxString currentTip = tt ? tt->GetTip() : wxString(); + if (toolTip != currentTip) + { + if (toolTip.IsEmpty()) + GetGridWindow()->SetToolTip(NULL); //wxGTK doesn't allow wxToolTip with empty text! + else + { + //wxWidgets bug: tooltip multiline property is defined by first tooltip text containing newlines or not (same is true for maximum width) + if (!tt) + GetGridWindow()->SetToolTip(new wxToolTip(wxT("a b\n\ + a b"))); //ugly, but is working (on Windows) + tt = GetGridWindow()->GetToolTip(); //should be bound by now + if (tt) + tt->SetTip(toolTip); + } + } +} + + +xmlAccess::ColumnAttributes CustomGridRim::getDefaultColumnAttributes() +{ + xmlAccess::ColumnAttributes defaultColumnSettings; + + xmlAccess::ColumnAttrib newEntry; + newEntry.type = xmlAccess::FULL_PATH; + newEntry.visible = false; + newEntry.position = 0; + newEntry.width = 150; + defaultColumnSettings.push_back(newEntry); + + newEntry.type = xmlAccess::DIRECTORY; + newEntry.position = 1; + newEntry.width = 140; + defaultColumnSettings.push_back(newEntry); + + newEntry.type = xmlAccess::REL_PATH; + newEntry.visible = true; + newEntry.position = 2; + newEntry.width = 118; + defaultColumnSettings.push_back(newEntry); + + newEntry.type = xmlAccess::FILENAME; + newEntry.position = 3; + newEntry.width = 138; + defaultColumnSettings.push_back(newEntry); + + newEntry.type = xmlAccess::SIZE; + newEntry.position = 4; + newEntry.width = 70; + defaultColumnSettings.push_back(newEntry); + + newEntry.type = xmlAccess::DATE; + newEntry.position = 5; + newEntry.width = 113; + defaultColumnSettings.push_back(newEntry); + + newEntry.type = xmlAccess::EXTENSION; + newEntry.visible = false; + newEntry.position = 6; + newEntry.width = 60; + defaultColumnSettings.push_back(newEntry); + + return defaultColumnSettings; +} + + +xmlAccess::ColumnAttributes CustomGridRim::getColumnAttributes() +{ + std::sort(columnSettings.begin(), columnSettings.end(), xmlAccess::sortByPositionAndVisibility); + + xmlAccess::ColumnAttributes output; + xmlAccess::ColumnAttrib newEntry; + for (size_t i = 0; i < columnSettings.size(); ++i) + { + newEntry = columnSettings[i]; + if (newEntry.visible) + newEntry.width = GetColSize(static_cast<int>(i)); //hidden columns are sorted to the end of vector! + output.push_back(newEntry); + } + + return output; +} + + +void CustomGridRim::setColumnAttributes(const xmlAccess::ColumnAttributes& attr) +{ + //remove special alignment for column "size" + if (GetLayoutDirection() != wxLayout_RightToLeft) //don't change for RTL languages + for (int i = 0; i < GetNumberCols(); ++i) + if (getTypeAtPos(i) == xmlAccess::SIZE) + { + wxGridCellAttr* cellAttributes = GetOrCreateCellAttr(0, i); + cellAttributes->SetAlignment(wxALIGN_LEFT, wxALIGN_CENTRE); + SetColAttr(i, cellAttributes); + break; + } + //---------------------------------------------------------------------------------- + + columnSettings.clear(); + if (attr.size() == 0) + { + //default settings: + columnSettings = getDefaultColumnAttributes(); + } + else + { + for (size_t i = 0; i < xmlAccess::COLUMN_TYPE_COUNT; ++i) + { + xmlAccess::ColumnAttrib newEntry; + + if (i < attr.size()) + newEntry = attr[i]; + else //fix corrupted data: + { + newEntry.type = static_cast<xmlAccess::ColumnTypes>(xmlAccess::COLUMN_TYPE_COUNT); //sort additional rows to the end + newEntry.visible = false; + newEntry.position = i; + newEntry.width = 100; + } + columnSettings.push_back(newEntry); + } + + std::sort(columnSettings.begin(), columnSettings.end(), xmlAccess::sortByType); + for (size_t i = 0; i < xmlAccess::COLUMN_TYPE_COUNT; ++i) //just be sure that each type exists only once + columnSettings[i].type = static_cast<xmlAccess::ColumnTypes>(i); + + std::sort(columnSettings.begin(), columnSettings.end(), xmlAccess::sortByPositionOnly); + for (size_t i = 0; i < xmlAccess::COLUMN_TYPE_COUNT; ++i) //just be sure that positions are numbered correctly + columnSettings[i].position = i; + } + + std::sort(columnSettings.begin(), columnSettings.end(), xmlAccess::sortByPositionAndVisibility); + std::vector<xmlAccess::ColumnTypes> newPositions; + for (size_t i = 0; i < columnSettings.size() && columnSettings[i].visible; ++i) //hidden columns are sorted to the end of vector! + newPositions.push_back(columnSettings[i].type); + + //set column positions + if (getGridDataTableRim()) + getGridDataTableRim()->setupColumns(newPositions); + + //set column width (set them after setupColumns!) + for (size_t i = 0; i < newPositions.size(); ++i) + SetColSize(static_cast<int>(i), columnSettings[i].width); + + //-------------------------------------------------------------------------------------------------------- + //set special alignment for column "size" + if (GetLayoutDirection() != wxLayout_RightToLeft) //don't change for RTL languages + for (int i = 0; i < GetNumberCols(); ++i) + if (getTypeAtPos(i) == xmlAccess::SIZE) + { + wxGridCellAttr* cellAttributes = GetOrCreateCellAttr(0, i); + cellAttributes->SetAlignment(wxALIGN_RIGHT, wxALIGN_CENTRE); + SetColAttr(i, cellAttributes); //make filesize right justified on grids + break; + } + + ClearSelection(); + ForceRefresh(); +} + + +xmlAccess::ColumnTypes CustomGridRim::getTypeAtPos(size_t pos) const +{ + if (getGridDataTableRim()) + return getGridDataTableRim()->getTypeAtPos(pos); + else + return xmlAccess::DIRECTORY; +} + + +wxString CustomGridRim::getTypeName(xmlAccess::ColumnTypes colType) +{ + switch (colType) + { + case xmlAccess::FULL_PATH: + return _("Full path"); + case xmlAccess::FILENAME: + return _("Filename"); + case xmlAccess::REL_PATH: + return _("Relative path"); + case xmlAccess::DIRECTORY: + return _("Directory"); + case xmlAccess::SIZE: + return _("Size"); + case xmlAccess::DATE: + return _("Date"); + case xmlAccess::EXTENSION: + return _("Extension"); + } + + return wxEmptyString; //dummy +} + + +void CustomGridRim::autoSizeColumns() //performance optimized column resizer (analog to wxGrid::AutoSizeColumns() +{ + for (int col = 0; col < GetNumberCols(); ++col) + { + if (col < 0) + return; + + int rowMax = -1; + size_t lenMax = 0; + for (int row = 0; row < GetNumberRows(); ++row) + if (GetCellValue(row, col).size() > lenMax) + { + lenMax = GetCellValue(row, col).size(); + rowMax = row; + } + + wxCoord extentMax = 0; + + //calculate width of (most likely) widest cell + wxClientDC dc(GetGridWindow()); + if (rowMax > -1) + { + wxGridCellAttr* attr = GetCellAttr(rowMax, col); + if (attr) + { + wxGridCellRenderer* renderer = attr->GetRenderer(this, rowMax, col); + if (renderer) + { + const wxSize size = renderer->GetBestSize(*this, *attr, dc, rowMax, col); + extentMax = std::max(extentMax, size.x); + renderer->DecRef(); + } + attr->DecRef(); + } + } + + //consider column label + dc.SetFont(GetLabelFont()); + wxCoord w = 0; + wxCoord h = 0; + dc.GetMultiLineTextExtent(GetColLabelValue(col), &w, &h ); + if (GetColLabelTextOrientation() == wxVERTICAL) + w = h; + extentMax = std::max(extentMax, w); + + extentMax += 15; //leave some space around text + + SetColSize(col, extentMax); + + } + Refresh(); +} + + +void CustomGridRim::enableFileIcons(const std::shared_ptr<IconBuffer>& iconBuffer) +{ + iconBuffer_ = iconBuffer; + SetDefaultRenderer(new GridCellRenderer(failedLoads, getGridDataTableRim(), iconBuffer)); //SetDefaultRenderer takes ownership! +} + + +std::pair<CustomGridRim::RowBegin, CustomGridRim::RowEnd> CustomGridRim::getVisibleRows() +{ + int dummy = -1; + int height = -1; + GetGridWindow()->GetClientSize(&dummy, &height); + + if (height >= 0) + { + const int rowTop = mousePosToCell(wxPoint(0, 0)).first; + int rowEnd = mousePosToCell(wxPoint(0, height)).first; + if (rowEnd == -1) //when scrolling to the very end, there are a few border pixels that do not belong to any row + rowEnd = GetNumberRows(); + else + ++rowEnd; + + if (0 <= rowTop && rowTop <= rowEnd) + return std::make_pair(rowTop, rowEnd); //"top" means here top of the screen => smaller value + } + return std::make_pair(0, 0); +} + + +inline +CustomGridTableRim* CustomGridRim::getGridDataTableRim() const +{ + return dynamic_cast<CustomGridTableRim*>(getGridDataTable()); //I'm tempted to use a static cast here... +} + + +void CustomGridRim::getIconsToBeLoaded(std::vector<Zstring>& newLoad) //loads all (not yet) drawn icons +{ + //don't check too often! give worker thread some time to fetch data + + newLoad.clear(); + + if (iconBuffer_.get()) + { + const CustomGridTableRim* gridDataTable = getGridDataTableRim(); + if (!gridDataTable) return; + + const int totalCols = const_cast<CustomGridTableRim*>(gridDataTable)->GetNumberCols(); + const int totalRows = const_cast<CustomGridTableRim*>(gridDataTable)->GetNumberRows(); + + //determine column + const int colFilename = [&]() -> int + { + for (int k = 0; k < totalCols; ++k) + if (gridDataTable->getTypeAtPos(k) == xmlAccess::FILENAME) + return k; + return -1; + }(); + if (colFilename < 0) + return; + + const auto rowsOnScreen = getVisibleRows(); + + //loop over all visible rows + const int firstRow = static_cast<int>(rowsOnScreen.first); + const int rowNo = std::min(static_cast<int>(rowsOnScreen.second), totalRows) - firstRow; + + for (int i = 0; i < rowNo; ++i) + { + //alternate when adding rows: first, last, first + 1, last - 1 ... -> Icon buffer will then load reversely, i.e. from inside out + const int currentRow = firstRow + (i % 2 == 0 ? + i / 2 : + rowNo - 1 - (i - 1) / 2); + + if (failedLoads.find(currentRow) != failedLoads.end()) //find failed attempts to load icon + { + const Zstring fileName = gridDataTable->getIconFile(currentRow); + if (!fileName.empty()) + { + //test if they are already loaded in buffer: + if (iconBuffer_->requestFileIcon(fileName)) + { + //exists in buffer: refresh Row + RefreshCell(currentRow, colFilename); //do a *full* refresh for *every* failed load to update partial DC updates while scrolling + failedLoads.erase(currentRow); // + } + else //not yet in buffer: mark for async. loading + { + newLoad.push_back(fileName); + } + } + } + } + } +} + +//---------------------------------------------------------------------------------------- + + +//update file icons periodically: use SINGLE instance to coordinate left and right grid at once +IconUpdater::IconUpdater(CustomGridLeft* leftGrid, CustomGridRight* rightGrid) : + m_leftGrid(leftGrid), + m_rightGrid(rightGrid), + m_timer(new wxTimer) //connect timer event for async. icon loading +{ + m_timer->Connect(wxEVT_TIMER, wxEventHandler(IconUpdater::loadIconsAsynchronously), NULL, this); + m_timer->Start(50); //timer interval in ms +} + + +IconUpdater::~IconUpdater() {} + + +void IconUpdater::loadIconsAsynchronously(wxEvent& event) //loads all (not yet) drawn icons +{ + std::vector<Zstring> iconsLeft; + m_leftGrid->getIconsToBeLoaded(iconsLeft); + + std::vector<Zstring> newLoad; + m_rightGrid->getIconsToBeLoaded(newLoad); + + //merge vectors + newLoad.insert(newLoad.end(), iconsLeft.begin(), iconsLeft.end()); + + if (m_leftGrid->iconBuffer_.get()) + m_leftGrid->iconBuffer_->setWorkload(newLoad); + + //event.Skip(); +} + +//---------------------------------------------------------------------------------------- + + +CustomGridLeft::CustomGridLeft(wxWindow* parent, + wxWindowID id, + const wxPoint& pos, + const wxSize& size, + long style, + const wxString& name) : + CustomGridRim(parent, id, pos, size, style, name) +{ + GetGridWindow()->Connect(wxEVT_MOTION, wxMouseEventHandler(CustomGridLeft::OnMouseMovement), NULL, this); //row-based tooltip +} + + +bool CustomGridLeft::CreateGrid(int numRows, int numCols, wxGrid::wxGridSelectionModes selmode) +{ + //use custom wxGridTableBase class for management of large sets of formatted data. + //This is done in CreateGrid instead of SetTable method since source code is generated and wxFormbuilder invokes CreatedGrid by default. + SetTable(new CustomGridTableLeft, true, wxGrid::wxGridSelectRows); //give ownership to wxGrid: gridDataTable is deleted automatically in wxGrid destructor + return true; +} + + +void CustomGridLeft::initSettings(CustomGridLeft* gridLeft, //create connection with zen::GridView + CustomGridMiddle* gridMiddle, + CustomGridRight* gridRight, + const zen::GridView* gridDataView) +{ + //set underlying grid data + assert(getGridDataTable()); + getGridDataTable()->setGridDataTable(gridDataView); + + CustomGridRim::setOtherGrid(gridRight); + + CustomGridRim::initSettings(gridLeft, gridMiddle, gridRight, gridDataView); +} + + +void CustomGridLeft::OnMouseMovement(wxMouseEvent& event) +{ + CustomGridRim::setTooltip<LEFT_SIDE>(event); + event.Skip(); +} + + +CustomGridTable* CustomGridLeft::getGridDataTable() const +{ + return static_cast<CustomGridTable*>(GetTable()); //one of the few cases where no dynamic_cast is required! +} + + +//---------------------------------------------------------------------------------------- +CustomGridRight::CustomGridRight(wxWindow* parent, + wxWindowID id, + const wxPoint& pos, + const wxSize& size, + long style, + const wxString& name) : + CustomGridRim(parent, id, pos, size, style, name) +{ + GetGridWindow()->Connect(wxEVT_MOTION, wxMouseEventHandler(CustomGridRight::OnMouseMovement), NULL, this); //row-based tooltip +} + + +bool CustomGridRight::CreateGrid(int numRows, int numCols, wxGrid::wxGridSelectionModes selmode) +{ + SetTable(new CustomGridTableRight, true, wxGrid::wxGridSelectRows); //give ownership to wxGrid: gridDataTable is deleted automatically in wxGrid destructor + return true; +} + + +void CustomGridRight::initSettings(CustomGridLeft* gridLeft, //create connection with zen::GridView + CustomGridMiddle* gridMiddle, + CustomGridRight* gridRight, + const zen::GridView* gridDataView) +{ + //set underlying grid data + assert(getGridDataTable()); + getGridDataTable()->setGridDataTable(gridDataView); + + CustomGridRim::setOtherGrid(gridLeft); + + CustomGridRim::initSettings(gridLeft, gridMiddle, gridRight, gridDataView); +} + + +void CustomGridRight::OnMouseMovement(wxMouseEvent& event) +{ + CustomGridRim::setTooltip<RIGHT_SIDE>(event); + event.Skip(); +} + + +CustomGridTable* CustomGridRight::getGridDataTable() const +{ + return static_cast<CustomGridTable*>(GetTable()); //one of the few cases where no dynamic_cast is required! +} + + +//---------------------------------------------------------------------------------------- +class GridCellRendererMiddle : public wxGridCellStringRenderer +{ +public: + GridCellRendererMiddle(const CustomGridMiddle& middleGrid) : m_gridMiddle(middleGrid) {}; + + virtual void Draw(wxGrid& grid, + wxGridCellAttr& attr, + wxDC& dc, + const wxRect& rect, + int row, int col, + bool isSelected); + +private: + const CustomGridMiddle& m_gridMiddle; +}; + + +//define new event types +const wxEventType FFS_CHECK_ROWS_EVENT = wxNewEventType(); //attention! do NOT place in header to keep (generated) id unique! +const wxEventType FFS_SYNC_DIRECTION_EVENT = wxNewEventType(); + +const int CHECK_BOX_IMAGE = 11; //width of checkbox image +const int CHECK_BOX_WIDTH = CHECK_BOX_IMAGE + 3; //width of first block + +// cell: +// ---------------------------------- +// | checkbox | left | middle | right| +// ---------------------------------- + + +CustomGridMiddle::CustomGridMiddle(wxWindow* parent, + wxWindowID id, + const wxPoint& pos, + const wxSize& size, + long style, + const wxString& name) : + CustomGrid(parent, id, pos, size, style, name), + selectionRowBegin(-1), + selectionPos(BLOCKPOS_CHECK_BOX), + highlightedRow(-1), + highlightedPos(BLOCKPOS_CHECK_BOX) +{ + SetLayoutDirection(wxLayout_LeftToRight); // + GetGridWindow ()->SetLayoutDirection(wxLayout_LeftToRight); //avoid mirroring this dialog in RTL languages like Hebrew or Arabic + GetGridColLabelWindow()->SetLayoutDirection(wxLayout_LeftToRight); // + + //connect events for dynamic selection of sync direction + GetGridWindow()->Connect(wxEVT_MOTION, wxMouseEventHandler(CustomGridMiddle::OnMouseMovement), NULL, this); + GetGridWindow()->Connect(wxEVT_LEAVE_WINDOW, wxMouseEventHandler(CustomGridMiddle::OnLeaveWindow), NULL, this); + GetGridWindow()->Connect(wxEVT_LEFT_UP, wxMouseEventHandler(CustomGridMiddle::OnLeftMouseUp), NULL, this); + GetGridWindow()->Connect(wxEVT_LEFT_DOWN, wxMouseEventHandler(CustomGridMiddle::OnLeftMouseDown), NULL, this); +} + + +CustomGridMiddle::~CustomGridMiddle() {} + + +bool CustomGridMiddle::CreateGrid(int numRows, int numCols, wxGrid::wxGridSelectionModes selmode) +{ + SetTable(new CustomGridTableMiddle, true, wxGrid::wxGridSelectRows); //give ownership to wxGrid: gridDataTable is deleted automatically in wxGrid destructor + + //display checkboxes (representing bool values) if row is enabled for synchronization + SetDefaultRenderer(new GridCellRendererMiddle(*this)); //SetDefaultRenderer takes ownership! + + return true; +} + + +void CustomGridMiddle::initSettings(CustomGridLeft* gridLeft, //create connection with zen::GridView + CustomGridMiddle* gridMiddle, + CustomGridRight* gridRight, + const zen::GridView* gridDataView) +{ + //set underlying grid data + assert(getGridDataTable()); + getGridDataTable()->setGridDataTable(gridDataView); + +#ifdef FFS_LINUX //get rid of scrollbars; Linux: change policy for GtkScrolledWindow + GtkWidget* gridWidget = wxWindow::m_widget; + GtkScrolledWindow* scrolledWindow = GTK_SCROLLED_WINDOW(gridWidget); + gtk_scrolled_window_set_policy(scrolledWindow, GTK_POLICY_NEVER, GTK_POLICY_NEVER); +#endif + + CustomGrid::initSettings(gridLeft, gridMiddle, gridRight, gridDataView); +} + + +CustomGridTable* CustomGridMiddle::getGridDataTable() const +{ + return static_cast<CustomGridTable*>(GetTable()); //one of the few cases where no dynamic_cast is required! +} + + +inline +CustomGridTableMiddle* CustomGridMiddle::getGridDataTableMiddle() const +{ + return static_cast<CustomGridTableMiddle*>(getGridDataTable()); +} + + +#ifdef FFS_WIN //get rid of scrollbars; Windows: overwrite virtual method +void CustomGridMiddle::SetScrollbar(int orientation, int position, int thumbSize, int range, bool refresh) +{ + wxWindow::SetScrollbar(orientation, 0, 0, 0, refresh); +} +#endif + + +void CustomGridMiddle::OnMouseMovement(wxMouseEvent& event) +{ + const int rowOld = highlightedRow; + const BlockPosition posOld = highlightedPos; + + + if (selectionRowBegin == -1) //change highlightning only if currently not dragging mouse + { + highlightedRow = mousePosToRow(event.GetPosition(), &highlightedPos); + + if (rowOld != highlightedRow) + { + RefreshCell(highlightedRow, 0); + RefreshCell(rowOld, 0); + } + else if (posOld != highlightedPos) + RefreshCell(highlightedRow, 0); + + //handle tooltip + showToolTip(highlightedRow, GetGridWindow()->ClientToScreen(event.GetPosition())); + } + + event.Skip(); +} + + +void CustomGridMiddle::OnLeaveWindow(wxMouseEvent& event) +{ + highlightedRow = -1; + highlightedPos = BLOCKPOS_CHECK_BOX; + Refresh(); + + //handle tooltip + toolTip.hide(); +} + + +void CustomGridMiddle::showToolTip(int rowNumber, wxPoint pos) +{ + if (!getGridDataTableMiddle()) + return; + + const FileSystemObject* const fsObj = getGridDataTableMiddle()->getRawData(rowNumber); + if (fsObj == NULL) //if invalid row... + { + toolTip.hide(); + return; + } + + if (getGridDataTableMiddle()->syncPreviewIsActive()) //synchronization preview + { + const SyncOperation syncOp = fsObj->getSyncOperation(); + switch (syncOp) + { + case SO_CREATE_NEW_LEFT: + toolTip.show(getDescription(syncOp), pos, &GlobalResources::getImage(wxT("createLeft"))); + break; + case SO_CREATE_NEW_RIGHT: + toolTip.show(getDescription(syncOp), pos, &GlobalResources::getImage(wxT("createRight"))); + break; + case SO_DELETE_LEFT: + toolTip.show(getDescription(syncOp), pos, &GlobalResources::getImage(wxT("deleteLeft"))); + break; + case SO_DELETE_RIGHT: + toolTip.show(getDescription(syncOp), pos, &GlobalResources::getImage(wxT("deleteRight"))); + break; + case SO_OVERWRITE_LEFT: + case SO_COPY_METADATA_TO_LEFT: + toolTip.show(getDescription(syncOp), pos, &GlobalResources::getImage(wxT("updateLeft"))); + break; + case SO_OVERWRITE_RIGHT: + case SO_COPY_METADATA_TO_RIGHT: + toolTip.show(getDescription(syncOp), pos, &GlobalResources::getImage(wxT("updateRight"))); + break; + case SO_DO_NOTHING: + toolTip.show(getDescription(syncOp), pos, &GlobalResources::getImage(wxT("none"))); + break; + case SO_EQUAL: + toolTip.show(getDescription(syncOp), pos, &GlobalResources::getImage(wxT("equal"))); + break; + case SO_UNRESOLVED_CONFLICT: + toolTip.show(fsObj->getSyncOpConflict(), pos, &GlobalResources::getImage(wxT("conflict"))); + break; + }; + } + else + { + const CompareFilesResult cmpRes = fsObj->getCategory(); + switch (cmpRes) + { + case FILE_LEFT_SIDE_ONLY: + toolTip.show(getDescription(cmpRes), pos, &GlobalResources::getImage(wxT("leftOnly"))); + break; + case FILE_RIGHT_SIDE_ONLY: + toolTip.show(getDescription(cmpRes), pos, &GlobalResources::getImage(wxT("rightOnly"))); + break; + case FILE_LEFT_NEWER: + toolTip.show(getDescription(cmpRes), pos, &GlobalResources::getImage(wxT("leftNewer"))); + break; + case FILE_RIGHT_NEWER: + toolTip.show(getDescription(cmpRes), pos, &GlobalResources::getImage(wxT("rightNewer"))); + break; + case FILE_DIFFERENT: + toolTip.show(getDescription(cmpRes), pos, &GlobalResources::getImage(wxT("different"))); + break; + case FILE_EQUAL: + toolTip.show(getDescription(cmpRes), pos, &GlobalResources::getImage(wxT("equal"))); + break; + case FILE_DIFFERENT_METADATA: + toolTip.show(getDescription(cmpRes), pos, &GlobalResources::getImage(wxT("conflict"))); + break; + case FILE_CONFLICT: + toolTip.show(fsObj->getCatConflict(), pos, &GlobalResources::getImage(wxT("conflict"))); + break; + } + } +} + + +void CustomGridMiddle::OnLeftMouseDown(wxMouseEvent& event) +{ + selectionRowBegin = mousePosToRow(event.GetPosition(), &selectionPos); + event.Skip(); +} + + +void CustomGridMiddle::OnLeftMouseUp(wxMouseEvent& event) +{ + //int selRowEnd = mousePosToCell(event.GetPosition()).first; + //-> use visibly marked rows instead! with wxWidgets 2.8.12 there is no other way than IsInSelection() + int selRowEnd = -1; + if (0 <= selectionRowBegin && selectionRowBegin < GetNumberRows()) + { + for (int i = selectionRowBegin; i < GetNumberRows() && IsInSelection(i, 0); ++i) + selRowEnd = i; + + if (selRowEnd == selectionRowBegin) + for (int i = selectionRowBegin; i >= 0 && IsInSelection(i, 0); --i) + selRowEnd = i; + } + + if (0 <= selectionRowBegin && 0 <= selRowEnd) + { + switch (selectionPos) + { + case BLOCKPOS_CHECK_BOX: + { + //create a custom event + FFSCheckRowsEvent evt(selectionRowBegin, selRowEnd); + AddPendingEvent(evt); + } + break; + case BLOCKPOS_LEFT: + { + //create a custom event + FFSSyncDirectionEvent evt(selectionRowBegin, selRowEnd, SYNC_DIR_LEFT); + AddPendingEvent(evt); + } + break; + case BLOCKPOS_MIDDLE: + { + //create a custom event + FFSSyncDirectionEvent evt(selectionRowBegin, selRowEnd, SYNC_DIR_NONE); + AddPendingEvent(evt); + } + break; + case BLOCKPOS_RIGHT: + { + //create a custom event + FFSSyncDirectionEvent evt(selectionRowBegin, selRowEnd, SYNC_DIR_RIGHT); + AddPendingEvent(evt); + } + break; + } + } + selectionRowBegin = -1; + selectionPos = BLOCKPOS_CHECK_BOX; + + ClearSelection(); + event.Skip(); +} + + +int CustomGridMiddle::mousePosToRow(wxPoint pos, BlockPosition* block) +{ + if (!getGridDataTableMiddle()) + return 0; + + int row = -1; + int x = -1; + int y = -1; + CalcUnscrolledPosition( pos.x, pos.y, &x, &y ); + if (x >= 0 && y >= 0) + { + row = YToRow(y); + + //determine blockposition within cell (optional) + if (block) + { + *block = BLOCKPOS_CHECK_BOX; //default + + if (row >= 0) + { + const FileSystemObject* const fsObj = getGridDataTableMiddle()->getRawData(row); + if (fsObj != NULL && //if valid row... + getGridDataTableMiddle()->syncPreviewIsActive() && + fsObj->getSyncOperation() != SO_EQUAL) //in sync-preview equal files shall be treated as in cmp result, i.e. as full checkbox + { + // cell: + // ---------------------------------- + // | checkbox | left | middle | right| + // ---------------------------------- + + const wxRect rect = CellToRect(row, 0); + if (rect.GetWidth() > CHECK_BOX_WIDTH) + { + const double blockWidth = (rect.GetWidth() - CHECK_BOX_WIDTH) / 3.0; + if (rect.GetX() + CHECK_BOX_WIDTH <= x && x < rect.GetX() + rect.GetWidth()) + { + if (x - (rect.GetX() + CHECK_BOX_WIDTH) < blockWidth) + *block = BLOCKPOS_LEFT; + else if (x - (rect.GetX() + CHECK_BOX_WIDTH) < 2 * blockWidth) + *block = BLOCKPOS_MIDDLE; + else + *block = BLOCKPOS_RIGHT; + } + } + } + } + } + } + return row; +} + + +void CustomGridMiddle::enableSyncPreview(bool value) +{ + assert(getGridDataTableMiddle()); + getGridDataTableMiddle()->enableSyncPreview(value); + + if (value) + GetGridColLabelWindow()->SetToolTip(_("Synchronization Preview")); + else + GetGridColLabelWindow()->SetToolTip(_("Comparison Result")); +} + + +void GridCellRendererMiddle::Draw(wxGrid& grid, + wxGridCellAttr& attr, + wxDC& dc, + const wxRect& rect, + int row, int col, + bool isSelected) +{ + //retrieve grid data + const FileSystemObject* const fsObj = m_gridMiddle.getGridDataTableMiddle() ? m_gridMiddle.getGridDataTableMiddle()->getRawData(row) : NULL; + if (fsObj != NULL) //if valid row... + { + if (rect.GetWidth() > CHECK_BOX_WIDTH) + { + const bool rowIsHighlighted = row == m_gridMiddle.highlightedRow; + + wxRect rectShrinked(rect); + + //clean first block of rect that will receive image of checkbox + rectShrinked.SetWidth(CHECK_BOX_WIDTH); + wxGridCellRenderer::Draw(grid, attr, dc, rectShrinked, row, col, isSelected); + + //print checkbox into first block + rectShrinked.SetX(rect.GetX() + 1); + + //HIGHLIGHTNING (checkbox): + if (rowIsHighlighted && + m_gridMiddle.highlightedPos == CustomGridMiddle::BLOCKPOS_CHECK_BOX) + { + dc.DrawLabel(wxEmptyString, GlobalResources::getImage(fsObj->isActive() ? + wxT("checkboxTrueFocus") : + wxT("checkboxFalseFocus")), rectShrinked, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + } + else //default + dc.DrawLabel(wxEmptyString, GlobalResources::getImage(fsObj->isActive() ? + wxT("checkboxTrue") : + wxT("checkboxFalse")), rectShrinked, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + + //clean remaining block of rect that will receive image of checkbox/directions + rectShrinked.SetWidth(rect.GetWidth() - CHECK_BOX_WIDTH); + rectShrinked.SetX(rect.GetX() + CHECK_BOX_WIDTH); + wxGridCellRenderer::Draw(grid, attr, dc, rectShrinked, row, col, isSelected); + + //print remaining block + if (m_gridMiddle.getGridDataTableMiddle()->syncPreviewIsActive()) //synchronization preview + { + //print sync direction into second block + + //HIGHLIGHTNING (sync direction): + if (rowIsHighlighted && + m_gridMiddle.highlightedPos != CustomGridMiddle::BLOCKPOS_CHECK_BOX) //don't allow changing direction for "=="-files + switch (m_gridMiddle.highlightedPos) + { + case CustomGridMiddle::BLOCKPOS_CHECK_BOX: + break; + case CustomGridMiddle::BLOCKPOS_LEFT: + dc.DrawLabel(wxEmptyString, getSyncOpImage(fsObj->testSyncOperation(SYNC_DIR_LEFT)), rectShrinked, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL); + break; + case CustomGridMiddle::BLOCKPOS_MIDDLE: + dc.DrawLabel(wxEmptyString, getSyncOpImage(fsObj->testSyncOperation(SYNC_DIR_NONE)), rectShrinked, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); + break; + case CustomGridMiddle::BLOCKPOS_RIGHT: + dc.DrawLabel(wxEmptyString, getSyncOpImage(fsObj->testSyncOperation(SYNC_DIR_RIGHT)), rectShrinked, wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL); + break; + } + else //default + { + const wxBitmap& syncOpIcon = getSyncOpImage(fsObj->getSyncOperation()); + dc.DrawLabel(wxEmptyString, syncOpIcon, rectShrinked, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); + } + } + else //comparison results view + { + switch (fsObj->getCategory()) + { + case FILE_LEFT_SIDE_ONLY: + dc.DrawLabel(wxEmptyString, GlobalResources::getImage(wxT("leftOnlySmall")), rectShrinked, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); + break; + case FILE_RIGHT_SIDE_ONLY: + dc.DrawLabel(wxEmptyString, GlobalResources::getImage(wxT("rightOnlySmall")), rectShrinked, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); + break; + case FILE_LEFT_NEWER: + dc.DrawLabel(wxEmptyString, GlobalResources::getImage(wxT("leftNewerSmall")), rectShrinked, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); + break; + case FILE_RIGHT_NEWER: + dc.DrawLabel(wxEmptyString, GlobalResources::getImage(wxT("rightNewerSmall")), rectShrinked, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); + break; + case FILE_DIFFERENT: + dc.DrawLabel(wxEmptyString, GlobalResources::getImage(wxT("differentSmall")), rectShrinked, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); + break; + case FILE_EQUAL: + dc.DrawLabel(wxEmptyString, GlobalResources::getImage(wxT("equalSmall")), rectShrinked, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); + break; + case FILE_CONFLICT: + case FILE_DIFFERENT_METADATA: + dc.DrawLabel(wxEmptyString, GlobalResources::getImage(wxT("conflictSmall")), rectShrinked, wxALIGN_CENTER | wxALIGN_CENTER_VERTICAL); + break; + } + } + + return; + } + } + + //fallback + wxGridCellStringRenderer::Draw(grid, attr, dc, rect, row, col, isSelected); +} + + +//this method is called when grid view changes: useful for parallel updating of multiple grids +void CustomGridMiddle::alignOtherGrids(CustomGrid* gridLeft, CustomGrid* gridMiddle, CustomGrid* gridRight) +{ + int x = 0; + int y = 0; + GetViewStart(&x, &y); + gridLeft->Scroll(-1, y); + gridRight->Scroll(-1, y); +} + + +void CustomGridMiddle::DrawColLabel(wxDC& dc, int col) +{ + CustomGrid::DrawColLabel(dc, col); + + if (!getGridDataTableMiddle()) + return; + + const wxRect rect(GetColLeft(col), 0, GetColWidth(col), GetColLabelSize()); + + if (getGridDataTableMiddle()->syncPreviewIsActive()) + dc.DrawLabel(wxEmptyString, GlobalResources::getImage(wxT("syncViewSmall")), rect, wxALIGN_CENTER_HORIZONTAL | wxALIGN_CENTER_VERTICAL); + else + dc.DrawLabel(wxEmptyString, GlobalResources::getImage(wxT("cmpViewSmall")), rect, wxALIGN_CENTER_HORIZONTAL | wxALIGN_CENTER_VERTICAL); +} + + +const wxBitmap& zen::getSyncOpImage(SyncOperation syncOp) +{ + switch (syncOp) //evaluate comparison result and sync direction + { + case SO_CREATE_NEW_LEFT: + return GlobalResources::getImage(wxT("createLeftSmall")); + case SO_CREATE_NEW_RIGHT: + return GlobalResources::getImage(wxT("createRightSmall")); + case SO_DELETE_LEFT: + return GlobalResources::getImage(wxT("deleteLeftSmall")); + case SO_DELETE_RIGHT: + return GlobalResources::getImage(wxT("deleteRightSmall")); + case SO_OVERWRITE_RIGHT: + case SO_COPY_METADATA_TO_RIGHT: + return GlobalResources::getImage(wxT("updateRightSmall")); + case SO_OVERWRITE_LEFT: + case SO_COPY_METADATA_TO_LEFT: + return GlobalResources::getImage(wxT("updateLeftSmall")); + case SO_DO_NOTHING: + return GlobalResources::getImage(wxT("noneSmall")); + case SO_EQUAL: + return GlobalResources::getImage(wxT("equalSmall")); + case SO_UNRESOLVED_CONFLICT: + return GlobalResources::getImage(wxT("conflictSmall")); + } + + return wxNullBitmap; //dummy +} + |