From 5e6d30aa0bc1fde88c727c8a55706a36a30aa5e3 Mon Sep 17 00:00:00 2001 From: Vadim Zeitlin Date: Wed, 5 Feb 2014 22:12:53 +0000 Subject: [PATCH] Add support for sorting by more than one column to generic wxDataViewCtrl. Maintain a list of columns used for sorting instead of a single sort column index and allow to add/remove columns to/from it interactively by right clicking them if AllowMultiColumnSort() was used. See https://github.com/wxWidgets/wxWidgets/pull/3 git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@75806 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775 --- docs/changes.txt | 1 + include/wx/dataview.h | 19 ++++ include/wx/generic/dataview.h | 23 ++++- interface/wx/dataview.h | 65 ++++++++++++++ samples/dataview/dataview.cpp | 55 +++++++++++- src/generic/datavgen.cpp | 161 ++++++++++++++++++++++++++++++---- 6 files changed, 299 insertions(+), 25 deletions(-) diff --git a/docs/changes.txt b/docs/changes.txt index 82e7e63731..90d0ae353c 100644 --- a/docs/changes.txt +++ b/docs/changes.txt @@ -26,6 +26,7 @@ All: All (GUI): - XRC handler for wxAuiToolBar added (Kinaou Hervé). +- Add support for sorting wxDataViewCtrl by multiple columns (Trigve). - Add wxHtmlWindow::SetDefaultHTMLCursor() (Jeff A. Marr). - Add default ctor and Create() to wxContextHelpButton (Hanmac). - Send events when toggling wxPropertyGrid nodes from keyboard (Armel Asselin). diff --git a/include/wx/dataview.h b/include/wx/dataview.h index 2aa4a60d7d..8841709527 100644 --- a/include/wx/dataview.h +++ b/include/wx/dataview.h @@ -645,6 +645,25 @@ public: { return m_expander_column; } virtual wxDataViewColumn *GetSortingColumn() const = 0; + virtual wxVector GetSortingColumns() const + { + wxVector columns; + if ( wxDataViewColumn* col = GetSortingColumn() ) + columns.push_back(col); + return columns; + } + + // This must be overridden to return true if the control does allow sorting + // by more than one column, which is not the case by default. + virtual bool AllowMultiColumnSort(bool allow) + { + // We can still return true when disabling multi-column sort. + return !allow; + } + + // This should also be overridden to actually use the specified column for + // sorting if using multiple columns is supported. + virtual void ToggleSortByColumn(int WXUNUSED(column)) { } // items management diff --git a/include/wx/generic/dataview.h b/include/wx/generic/dataview.h index f3ac8ee598..c9797f2151 100644 --- a/include/wx/generic/dataview.h +++ b/include/wx/generic/dataview.h @@ -155,6 +155,7 @@ public: virtual int GetColumnPosition( const wxDataViewColumn *column ) const; virtual wxDataViewColumn *GetSortingColumn() const; + virtual wxVector GetSortingColumns() const; virtual int GetSelectedItemsCount() const; virtual int GetSelections( wxDataViewItemArray & sel ) const; @@ -183,6 +184,10 @@ public: virtual bool SetFont(const wxFont & font); + virtual bool AllowMultiColumnSort(bool allow); + virtual bool IsMultiColumnSortAllowed() { return m_allowMultiColumnSort; } + virtual void ToggleSortByColumn(int column); + #if wxUSE_DRAG_AND_DROP virtual bool EnableDragSource( const wxDataFormat &format ); virtual bool EnableDropTarget( const wxDataFormat &format ); @@ -205,8 +210,15 @@ protected: virtual wxDataViewItem GetItemByRow( unsigned int row ) const; virtual int GetRowByItem( const wxDataViewItem & item ) const; - int GetSortingColumnIndex() const { return m_sortingColumnIdx; } - void SetSortingColumnIndex(int idx) { m_sortingColumnIdx = idx; } + // Mark the column as being used or not for sorting. + void UseColumnForSorting(int idx); + void DontUseColumnForSorting(int idx); + + // Return true if the given column is sorted + bool IsColumnSorted(int idx) const; + + // Reset all columns currently used for sorting. + void ResetAllSortColumns(); public: // utility functions not part of the API @@ -267,8 +279,11 @@ private: // user defined color to draw row lines, may be invalid wxColour m_alternateRowColour; - // the index of the column currently used for sorting or -1 - int m_sortingColumnIdx; + // columns indices used for sorting, empty if nothing is sorted + wxVector m_sortingColumnIdxs; + + // if true, allow sorting by more than one column + bool m_allowMultiColumnSort; private: void OnSize( wxSizeEvent &event ); diff --git a/interface/wx/dataview.h b/interface/wx/dataview.h index 81bcd5adfd..98d93c202e 100644 --- a/interface/wx/dataview.h +++ b/interface/wx/dataview.h @@ -821,6 +821,18 @@ wxEventType wxEVT_DATAVIEW_ITEM_DROP; through wxVariant which can be extended to support more data formats as necessary. Accordingly, all type information uses the strings returned from wxVariant::GetType. + This control supports single column sorting and on some platforms + (currently only those using the generic version, i.e. not wxGTK nor wxOSX) + also sorting by multiple columns at once. The latter must be explicitly + enabled using AllowMultiColumnSort(), which will also indicate whether this + feature is supported, as it changes the default behaviour of right clicking + the column header to add or remove it to the set of columns used for + sorting. If this behaviour is not appropriate, you may handle + @c wxEVT_DATAVIEW_COLUMN_HEADER_RIGHT_CLICK event yourself to prevent it + from happening. In this case you would presumably call ToggleSortByColumn() + from some other event handler to still allow the user to configure sort + order somehow. + @beginStyleTable @style{wxDV_SINGLE} Single selection mode. This is the default. @@ -922,6 +934,24 @@ public: */ virtual ~wxDataViewCtrl(); + /** + Call to allow using multiple columns for sorting. + + When using multiple column for sorting, GetSortingColumns() method + should be used to retrieve all the columns which should be used to + effectively sort the data when processing the sorted event. + + Currently multiple column sort is only implemented in the generic + version, i.e. this functionality is not available when using the native + wxDataViewCtrl implementation in wxGTK nor wxOSX. + + @return @true if sorting by multiple columns could be enabled, @false + otherwise, typically because this feature is not supported. + + @since 3.1.0 + */ + bool AllowMultiColumnSort(bool allow); + /** Create the control. Useful for two step creation. */ @@ -1394,6 +1424,22 @@ public: */ virtual wxDataViewColumn* GetSortingColumn() const; + /** + Returns the columns which should be used for sorting the data in this + control. + + This method is only useful when sorting by multiple columns had been + enabled using AllowMultiColumnSort() previously, otherwise + GetSortingColumn() is more convenient. + + @return A possibly empty vector containing all the columns used + selected by the user for sorting. The sort order can be retrieved + from each column object separately. + + @since 3.1.0 + */ + virtual wxVector GetSortingColumns() const; + /** Returns true if any items are currently selected. @@ -1419,6 +1465,15 @@ public: */ virtual bool IsExpanded(const wxDataViewItem& item) const; + /** + Return @true if using more than one column for sorting is allowed. + + See AllowMultiColumnSort() and GetSortingColumns(). + + @since 3.1.0 + */ + bool IsMultiColumnSortAllowed() const; + /** Return @true if the item is selected. */ @@ -1502,6 +1557,16 @@ public: @since 2.9.2 */ virtual bool SetRowHeight(int rowHeight); + + /** + Toggle sorting by the given column. + + This method should only be used when sorting by multiple columns is + allowed, see AllowMultiColumnSort(), and does nothing otherwise. + + @since 3.1.0 + */ + virtual void ToggleSortByColumn(int column); }; diff --git a/samples/dataview/dataview.cpp b/samples/dataview/dataview.cpp index 5deed638e9..29f1110b82 100644 --- a/samples/dataview/dataview.cpp +++ b/samples/dataview/dataview.cpp @@ -114,7 +114,9 @@ private: void OnHeaderClick( wxDataViewEvent &event ); void OnAttrHeaderClick( wxDataViewEvent &event ); void OnHeaderRightClick( wxDataViewEvent &event ); + void OnHeaderClickList( wxDataViewEvent &event ); void OnSorted( wxDataViewEvent &event ); + void OnSortedList( wxDataViewEvent &event ); void OnContextMenu( wxDataViewEvent &event ); @@ -123,6 +125,8 @@ private: void OnHideAttributes( wxCommandEvent &event); void OnShowAttributes( wxCommandEvent &event); + void OnMultipleSort( wxCommandEvent &event); + #if wxUSE_DRAG_AND_DROP void OnBeginDrag( wxDataViewEvent &event ); void OnDropPossible( wxDataViewEvent &event ); @@ -288,6 +292,7 @@ enum ID_ADD_MANY = 203, ID_HIDE_ATTRIBUTES = 204, ID_SHOW_ATTRIBUTES = 205, + ID_MULTIPLE_SORT = 206, // Fourth page. ID_DELETE_TREE_ITEM = 400, @@ -322,6 +327,8 @@ BEGIN_EVENT_TABLE(MyFrame, wxFrame) EVT_BUTTON( ID_ADD_MANY, MyFrame::OnAddMany) EVT_BUTTON( ID_HIDE_ATTRIBUTES, MyFrame::OnHideAttributes) EVT_BUTTON( ID_SHOW_ATTRIBUTES, MyFrame::OnShowAttributes) + EVT_CHECKBOX( ID_MULTIPLE_SORT, MyFrame::OnMultipleSort) + // Fourth page. EVT_BUTTON( ID_DELETE_TREE_ITEM, MyFrame::OnDeleteTreeItem ) EVT_BUTTON( ID_DELETE_ALL_TREE_ITEMS, MyFrame::OnDeleteAllTreeItems ) @@ -344,6 +351,8 @@ BEGIN_EVENT_TABLE(MyFrame, wxFrame) EVT_DATAVIEW_COLUMN_HEADER_CLICK(ID_MUSIC_CTRL, MyFrame::OnHeaderClick) EVT_DATAVIEW_COLUMN_HEADER_RIGHT_CLICK(ID_MUSIC_CTRL, MyFrame::OnHeaderRightClick) EVT_DATAVIEW_COLUMN_SORTED(ID_MUSIC_CTRL, MyFrame::OnSorted) + EVT_DATAVIEW_COLUMN_SORTED(ID_ATTR_CTRL, MyFrame::OnSortedList) + EVT_DATAVIEW_COLUMN_HEADER_CLICK(ID_ATTR_CTRL, MyFrame::OnHeaderClickList) EVT_DATAVIEW_ITEM_CONTEXT_MENU(ID_MUSIC_CTRL, MyFrame::OnContextMenu) @@ -458,6 +467,8 @@ MyFrame::MyFrame(wxFrame *frame, const wxString &title, int x, int y, int w, int button_sizer2->Add( new wxButton( secondPanel, ID_ADD_MANY, "Add 1000"), 0, wxALL, 10 ); button_sizer2->Add( new wxButton( secondPanel, ID_HIDE_ATTRIBUTES, "Hide attributes"), 0, wxALL, 10 ); button_sizer2->Add( new wxButton( secondPanel, ID_SHOW_ATTRIBUTES, "Show attributes"), 0, wxALL, 10 ); + button_sizer2->Add( new wxCheckBox(secondPanel, ID_MULTIPLE_SORT, "Allow multisort"), + wxSizerFlags().Centre().DoubleBorder() ); wxSizer *secondPanelSz = new wxBoxSizer( wxVERTICAL ); secondPanelSz->Add(m_ctrl[1], 1, wxGROW|wxALL, 5); @@ -617,11 +628,15 @@ void MyFrame::BuildDataViewCtrl(wxPanel* parent, unsigned int nPanel, unsigned l m_ctrl[1]->AppendTextColumn("editable string", MyListModel::Col_EditableText, wxDATAVIEW_CELL_EDITABLE, - wxCOL_WIDTH_AUTOSIZE); + wxCOL_WIDTH_AUTOSIZE, + wxALIGN_NOT, + wxDATAVIEW_COL_SORTABLE); m_ctrl[1]->AppendIconTextColumn("icon", MyListModel::Col_IconText, wxDATAVIEW_CELL_EDITABLE, - wxCOL_WIDTH_AUTOSIZE); + wxCOL_WIDTH_AUTOSIZE, + wxALIGN_NOT, + wxDATAVIEW_COL_REORDERABLE | wxDATAVIEW_COL_SORTABLE); m_attributes = new wxDataViewColumn("attributes", @@ -629,7 +644,7 @@ void MyFrame::BuildDataViewCtrl(wxPanel* parent, unsigned int nPanel, unsigned l MyListModel::Col_TextWithAttr, wxCOL_WIDTH_AUTOSIZE, wxALIGN_RIGHT, - wxDATAVIEW_COL_REORDERABLE | wxDATAVIEW_COL_RESIZABLE ); + wxDATAVIEW_COL_REORDERABLE | wxDATAVIEW_COL_RESIZABLE | wxDATAVIEW_COL_SORTABLE); m_ctrl[1]->AppendColumn( m_attributes ); m_ctrl[1]->AppendColumn( @@ -1090,6 +1105,34 @@ void MyFrame::OnHeaderRightClick( wxDataViewEvent &event ) wxLogMessage( "wxEVT_DATAVIEW_COLUMN_HEADER_RIGHT_CLICK, Column position: %d", pos ); } +void MyFrame::OnSortedList( wxDataViewEvent &/*event*/) +{ + wxVector const columns = m_ctrl[1]->GetSortingColumns(); + wxLogMessage( "wxEVT_DATAVIEW_COLUMN_SORTED using the following columns"); + + for ( wxVector::const_iterator it = columns.begin(), + end = columns.end(); + it != end; + ++it ) + { + wxDataViewColumn* const col = *it; + + wxLogMessage("\t%d. %s %s", + col->GetModelColumn(), + col->GetTitle(), + col->IsSortOrderAscending() ? "ascending" : "descending"); + } +} + +void MyFrame::OnHeaderClickList( wxDataViewEvent &event ) +{ + // Use control+click to toggle sorting by this column. + if ( wxGetKeyState(WXK_CONTROL) ) + m_ctrl[1]->ToggleSortByColumn(event.GetColumn()); + else + event.Skip(); +} + void MyFrame::OnSorted( wxDataViewEvent &event ) { int pos = m_ctrl[0]->GetColumnPosition( event.GetDataViewColumn() ); @@ -1182,3 +1225,9 @@ void MyFrame::OnAddTreeContainerItem(wxCommandEvent& WXUNUSED(event)) ctrl->AppendContainer(selected, "Container", 0 ); } +void MyFrame::OnMultipleSort( wxCommandEvent &event ) +{ + if ( !m_ctrl[1]->AllowMultiColumnSort(event.IsChecked()) ) + wxLogMessage("Sorting by multiple columns not supported"); +} + diff --git a/src/generic/datavgen.cpp b/src/generic/datavgen.cpp index 9f1cf8e7c1..32f781d7c7 100644 --- a/src/generic/datavgen.cpp +++ b/src/generic/datavgen.cpp @@ -193,7 +193,7 @@ void wxDataViewColumn::UnsetAsSortKey() m_sort = false; if ( m_owner ) - m_owner->SetSortingColumnIndex(wxNOT_FOUND); + m_owner->DontUseColumnForSorting(m_owner->GetColumnIndex(this)); UpdateDisplay(); } @@ -203,19 +203,19 @@ void wxDataViewColumn::SetSortOrder(bool ascending) if ( !m_owner ) return; - // First unset the old sort column if any. - int oldSortKey = m_owner->GetSortingColumnIndex(); - if ( oldSortKey != wxNOT_FOUND ) - { - m_owner->GetColumn(oldSortKey)->UnsetAsSortKey(); - } - - // Now set this one as the new sort column. const int idx = m_owner->GetColumnIndex(this); - m_owner->SetSortingColumnIndex(idx); - m_sort = true; - m_sortAscending = ascending; + // If this column isn't sorted already, mark it as sorted + if ( !m_sort ) + { + wxASSERT(!m_owner->IsColumnSorted(idx)); + + // Now set this one as the new sort column. + m_owner->UseColumnForSorting(idx); + m_sort = true; + } + + m_sortAscending = ascending; // Call this directly instead of using UpdateDisplay() as we already have // the column index, no need to look it up again. @@ -237,6 +237,30 @@ public: wxDataViewCtrl *GetOwner() const { return static_cast(GetParent()); } + // Add/Remove additional column to sorting columns + void ToggleSortByColumn(int column) + { + wxDataViewCtrl * const owner = GetOwner(); + + if ( !owner->IsMultiColumnSortAllowed() ) + return; + + wxDataViewColumn * const col = owner->GetColumn(column); + if ( !col->IsSortable() ) + return; + + if ( owner->IsColumnSorted(column) ) + { + col->UnsetAsSortKey(); + SendEvent(wxEVT_DATAVIEW_COLUMN_SORTED, column); + } + else // Do start sortign by it. + { + col->SetSortOrder(true); + SendEvent(wxEVT_DATAVIEW_COLUMN_SORTED, column); + } + } + protected: // implement/override wxHeaderCtrl functions by forwarding them to the main // control @@ -301,6 +325,11 @@ private: } else // not using this column for sorting yet { + // We will sort by this column only now, so reset all the + // previously used ones. + owner->ResetAllSortColumns(); + + // Sort the column col->SetSortOrder(true); } @@ -315,9 +344,13 @@ private: void OnRClick(wxHeaderCtrlEvent& event) { + // Event wasn't processed somewhere, use default behaviour if ( !SendEvent(wxEVT_DATAVIEW_COLUMN_HEADER_RIGHT_CLICK, event.GetColumn()) ) + { event.Skip(); + ToggleSortByColumn(event.GetColumn()); + } } void OnResize(wxHeaderCtrlEvent& event) @@ -4614,13 +4647,12 @@ void wxDataViewCtrl::Init() m_cols.DeleteContents(true); m_notifier = NULL; - // No sorting column at start - m_sortingColumnIdx = wxNOT_FOUND; - m_headerArea = NULL; m_clientArea = NULL; m_colsDirty = false; + + m_allowMultiColumnSort = false; } bool wxDataViewCtrl::Create(wxWindow *parent, @@ -5112,7 +5144,7 @@ bool wxDataViewCtrl::ClearColumns() { SetExpanderColumn(NULL); m_cols.Clear(); - m_sortingColumnIdx = wxNOT_FOUND; + m_sortingColumnIdxs.clear(); m_colsBestWidths.clear(); m_clientArea->ClearCurrentColumn(); @@ -5186,8 +5218,25 @@ int wxDataViewCtrl::GetColumnPosition( const wxDataViewColumn *column ) const wxDataViewColumn *wxDataViewCtrl::GetSortingColumn() const { - return m_sortingColumnIdx == wxNOT_FOUND ? NULL - : GetColumn(m_sortingColumnIdx); + if ( m_sortingColumnIdxs.empty() ) + return NULL; + + return GetColumn(m_sortingColumnIdxs.front()); +} + +wxVector wxDataViewCtrl::GetSortingColumns() const +{ + wxVector out; + + for ( wxVector::const_iterator it = m_sortingColumnIdxs.begin(), + end = m_sortingColumnIdxs.end(); + it != end; + ++it ) + { + out.push_back(GetColumn(*it)); + } + + return out; } wxDataViewItem wxDataViewCtrl::DoGetCurrentItem() const @@ -5410,6 +5459,82 @@ void wxDataViewCtrl::EditItem(const wxDataViewItem& item, const wxDataViewColumn m_clientArea->StartEditing(item, column); } +void wxDataViewCtrl::ResetAllSortColumns() +{ + // Must make copy, because unsorting will remove it from original vector + wxVector const copy(m_sortingColumnIdxs); + for ( wxVector::const_iterator it = copy.begin(), + end = copy.end(); + it != end; + ++it ) + { + GetColumn(*it)->UnsetAsSortKey(); + } + + wxASSERT( m_sortingColumnIdxs.empty() ); +} + +bool wxDataViewCtrl::AllowMultiColumnSort(bool allow) +{ + if ( m_allowMultiColumnSort == allow ) + return true; + + m_allowMultiColumnSort = allow; + + // If disabling, must disable any multiple sort that are active + if ( !allow ) + { + ResetAllSortColumns(); + + if ( wxDataViewModel *model = GetModel() ) + model->Resort(); + } + + return true; +} + + +bool wxDataViewCtrl::IsColumnSorted(int idx) const +{ + for ( wxVector::const_iterator it = m_sortingColumnIdxs.begin(), + end = m_sortingColumnIdxs.end(); + it != end; + ++it ) + { + if ( *it == idx ) + return true; + } + + return false; +} + +void wxDataViewCtrl::UseColumnForSorting(int idx ) +{ + m_sortingColumnIdxs.push_back(idx); +} + +void wxDataViewCtrl::DontUseColumnForSorting(int idx) +{ + for ( wxVector::iterator it = m_sortingColumnIdxs.begin(), + end = m_sortingColumnIdxs.end(); + it != end; + ++it ) + { + if ( *it == idx ) + { + m_sortingColumnIdxs.erase(it); + return; + } + } + + wxFAIL_MSG( "Column is not used for sorting" ); +} + +void wxDataViewCtrl::ToggleSortByColumn(int column) +{ + m_headerArea->ToggleSortByColumn(column); +} + #endif // !wxUSE_GENERICDATAVIEWCTRL #endif // wxUSE_DATAVIEWCTRL