// // This file is part of the aMule Project. // // Copyright (c) 2003-2011 aMule Team ( admin@amule.org / http://www.amule.org ) // Copyright (c) 2002 Merkur ( devs@emule-project.net / http://www.emule-project.net ) // // Any parts of this program derived from the xMule, lMule or eMule project, // or contributed by third-party developers are copyrighted by their // respective authors. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA // #include // Needed for wxMenu #include // Needed for wxConfig #include // Needed for wxStringTokenizer #include // Needed for wxImageList #include // Needed for MULE_VALIDATE_ #include // Needed for StrToLong #include #include "MuleListCtrl.h" // Interface declarations #include "GetTickCount.h" // Needed for GetTickCount() #include "OtherFunctions.h" // For arrow-pixmaps #include "pixmaps/sort_dn.xpm" #include "pixmaps/sort_up.xpm" #include "pixmaps/sort_dnx2.xpm" #include "pixmaps/sort_upx2.xpm" // Global constants #ifdef __WXGTK__ const int COL_SIZE_MIN = 10; #elif defined(__WXMSW__) || defined(__WXMAC__) || defined(__WXCOCOA__) const int COL_SIZE_MIN = 0; #else #error Need to define COL_SIZE_MIN for your OS #endif BEGIN_EVENT_TABLE(CMuleListCtrl, MuleExtern::wxGenericListCtrl) EVT_LIST_COL_CLICK( -1, CMuleListCtrl::OnColumnLClick) EVT_LIST_COL_RIGHT_CLICK( -1, CMuleListCtrl::OnColumnRClick) EVT_LIST_ITEM_SELECTED(-1, CMuleListCtrl::OnItemSelected) EVT_LIST_ITEM_DESELECTED(-1, CMuleListCtrl::OnItemSelected) EVT_LIST_DELETE_ITEM(-1, CMuleListCtrl::OnItemDeleted) EVT_LIST_DELETE_ALL_ITEMS(-1, CMuleListCtrl::OnAllItemsDeleted) EVT_CHAR( CMuleListCtrl::OnChar) EVT_MENU_RANGE(MP_LISTCOL_1, MP_LISTCOL_15, CMuleListCtrl::OnMenuSelected) EVT_MOUSEWHEEL(CMuleListCtrl::OnMouseWheel) END_EVENT_TABLE() //! Shared list of arrow-pixmaps static wxImageList imgList(16, 16, true, 0); CMuleListCtrl::CMuleListCtrl(wxWindow *parent, wxWindowID winid, const wxPoint& pos, const wxSize& size, long style, const wxValidator& validator, const wxString& name) : MuleExtern::wxGenericListCtrl(parent, winid, pos, size, style, validator, name) { m_sort_func = NULL; m_tts_time = 0; m_tts_item = -1; m_isSorting = false; if (imgList.GetImageCount() == 0) { imgList.Add(wxBitmap(sort_dn_xpm)); imgList.Add(wxBitmap(sort_up_xpm)); imgList.Add(wxBitmap(sort_dnx2_xpm)); imgList.Add(wxBitmap(sort_upx2_xpm)); } // Default sort-order is to sort by the first column (asc). m_sort_orders.push_back(CColPair(0, 0)); SetImageList(&imgList, wxIMAGE_LIST_SMALL); } CMuleListCtrl::~CMuleListCtrl() { if (!m_name.IsEmpty()) { SaveSettings(); } } long CMuleListCtrl::InsertColumn(long col, const wxString& heading, int format, int width, const wxString& name) { if (!name.IsEmpty()) { #ifdef __DEBUG__ // Check for valid names wxASSERT_MSG(name.Find(wxT(':')) == wxNOT_FOUND, wxT("Column name \"") + name + wxT("\" contains invalid characters!")); wxASSERT_MSG(name.Find(wxT(',')) == wxNOT_FOUND, wxT("Column name \"") + name + wxT("\" contains invalid characters!")); // Check for uniqueness of names. for (ColNameList::const_iterator it = m_column_names.begin(); it != m_column_names.end(); ++it) { if (name == it->name) { wxFAIL_MSG(wxT("Column name \"") + name + wxT("\" is not unique!")); } } #endif // Insert name at position col. ColNameList::iterator it = m_column_names.begin(); while (it != m_column_names.end() && it->index < col) { ++it; } m_column_names.insert(it, ColNameEntry(col, width, name)); while (it != m_column_names.end()) { ++it; ++(it->index); } } return MuleExtern::wxGenericListCtrl::InsertColumn(col, heading, format, width); } void CMuleListCtrl::SaveSettings() { wxCHECK_RET(!m_name.IsEmpty(), wxT("Cannot save settings for unnamed list")); wxConfigBase* cfg = wxConfigBase::Get(); // Save sorting, column and order wxString sortOrder; for (CSortingList::iterator it = m_sort_orders.begin(); it != m_sort_orders.end();) { wxString columnName = GetColumnName(it->first); if (!columnName.IsEmpty()) { sortOrder += columnName; sortOrder += wxT(":"); sortOrder += it->second & SORT_DES ? wxT("1") : wxT("0"); sortOrder += wxT(":"); sortOrder += it->second & SORT_ALT ? wxT("1") : wxT("0"); if (++it != m_sort_orders.end()) { sortOrder += wxT(","); } } else { ++it; } } cfg->Write(wxT("/eMule/TableOrdering") + m_name, sortOrder); // Save column widths. ATM this is also used to signify hidden columns. wxString buffer; for (int i = 0; i < GetColumnCount(); ++i) { wxString columnName = GetColumnName(i); if (!columnName.IsEmpty()) { if (!buffer.IsEmpty()) { buffer << wxT(","); } int currentwidth = GetColumnWidth(i); int savedsize = (m_column_sizes.size() && (i < (int) m_column_sizes.size())) ? m_column_sizes[i] : 0; buffer << columnName << wxT(":") << ((currentwidth > 0) ? currentwidth : (-1 * savedsize)); } } cfg->Write(wxT("/eMule/TableWidths") + m_name, buffer); } void CMuleListCtrl::ParseOldConfigEntries(const wxString& sortOrders, const wxString& columnWidths) { // Set sort order (including sort column) wxStringTokenizer tokens(sortOrders, wxT(",")); while (tokens.HasMoreTokens()) { wxString token = tokens.GetNextToken(); long column = 0; unsigned long order = 0; if (token.BeforeFirst(wxT(' ')).Strip(wxString::both).ToLong(&column)) { if (token.AfterFirst(wxT(' ')).Strip(wxString::both).ToULong(&order)) { column = GetNewColumnIndex(column); // Sanity checking, to avoid asserting if column count changes. if (column >= 0 && column < GetColumnCount()) { // Sanity checking, to avoid asserting if data-format changes. if ((order & ~SORTING_MASK) == 0) { // SetSorting will take care of duplicate entries SetSorting(column, order); } } } } } // Set column widths int counter = 0; wxStringTokenizer tokenizer(columnWidths, wxT(",")); while (tokenizer.HasMoreTokens()) { long idx = GetNewColumnIndex(counter++); long width = StrToLong(tokenizer.GetNextToken()); if (idx >= 0) { SetColumnWidth(idx, width); } } } void CMuleListCtrl::LoadSettings() { wxCHECK_RET(!m_name.IsEmpty(), wxT("Cannot load settings for unnamed list")); wxConfigBase* cfg = wxConfigBase::Get(); // Load sort order (including sort-column) m_sort_orders.clear(); wxString sortOrders = cfg->Read(wxT("/eMule/TableOrdering") + m_name, wxEmptyString); wxString columnWidths = cfg->Read(wxT("/eMule/TableWidths") + m_name, wxEmptyString); // Prevent sorting from occuring when calling SetSorting MuleListCtrlCompare sortFunc = m_sort_func; m_sort_func = NULL; if (columnWidths.Find(wxT(':')) == wxNOT_FOUND) { // Old-style config entries... ParseOldConfigEntries(sortOrders, columnWidths); } else { // Sort orders wxStringTokenizer tokens(sortOrders, wxT(",")); // Sort orders are stored in order primary, secondary, ... // We want to apply them with SetSorting(), so we have to apply them in reverse order, // so that the primary order is applied last and wins. // Read them with tokenizer and store them in a list in reverse order. CStringList tokenList; while (tokens.HasMoreTokens()) { tokenList.push_front(tokens.GetNextToken()); } for (CStringList::iterator it = tokenList.begin(); it != tokenList.end(); it++) { wxString token = *it; wxString name = token.BeforeFirst(wxT(':')); long order = StrToLong(token.AfterFirst(wxT(':')).BeforeLast(wxT(':'))); long alt = StrToLong(token.AfterLast(wxT(':'))); int col = GetColumnIndex(name); if (col >= 0) { SetSorting(col, (order ? SORT_DES : 0) | (alt ? SORT_ALT : 0)); } } // Column widths wxStringTokenizer tkz(columnWidths, wxT(",")); while (tkz.HasMoreTokens()) { wxString token = tkz.GetNextToken(); wxString name = token.BeforeFirst(wxT(':')); long width = StrToLong(token.AfterFirst(wxT(':'))); int col = GetColumnIndex(name); if (col >= 0) { if (col >= (int) m_column_sizes.size()) { m_column_sizes.resize(col + 1, 0); } m_column_sizes[col] = abs(width); SetColumnWidth(col, (width > 0) ? width : 0); } } } // Must have at least one sort-order specified if (m_sort_orders.empty()) { m_sort_orders.push_back(CColPair(0, 0)); } // Re-enable sorting and resort the contents (if any). m_sort_func = sortFunc; SortList(); } const wxString& CMuleListCtrl::GetColumnName(int index) const { for (ColNameList::const_iterator it = m_column_names.begin(); it != m_column_names.end(); ++it) { if (it->index == index) { return it->name; } } return EmptyString; } int CMuleListCtrl::GetColumnDefaultWidth(int index) const { for (ColNameList::const_iterator it = m_column_names.begin(); it != m_column_names.end(); ++it) { if (it->index == index) { return it->defaultWidth; } } return wxLIST_AUTOSIZE; } int CMuleListCtrl::GetColumnIndex(const wxString& name) const { for (ColNameList::const_iterator it = m_column_names.begin(); it != m_column_names.end(); ++it) { if (it->name == name) { return it->index; } } return -1; } int CMuleListCtrl::GetNewColumnIndex(int oldindex) const { wxStringTokenizer oldcolumns(GetOldColumnOrder(), wxT(","), wxTOKEN_RET_EMPTY_ALL); while (oldcolumns.HasMoreTokens()) { wxString name = oldcolumns.GetNextToken(); if (oldindex == 0) { return GetColumnIndex(name); } --oldindex; } return -1; } long CMuleListCtrl::GetInsertPos(wxUIntPtr data) { // Find the best place to position the item through a binary search int Min = 0; int Max = GetItemCount(); // Only do this if there are any items and a sorter function. // Otherwise insert at end. if (Max && m_sort_func) { // This search will find the place to position the new item // so it matches current sorting. // The result will be the position the new item will have // after insertion, which is the format expected by the InsertItem function. // If the item equals another item it will be inserted after it. do { int cur_pos = ( Max - Min ) / 2 + Min; int cmp = CompareItems(data, GetItemData(cur_pos)); // Value is less than the one at the current pos if ( cmp < 0 ) { Max = cur_pos; } else { Min = cur_pos + 1; } } while ((Min != Max)); } return Max; } int CMuleListCtrl::CompareItems(wxUIntPtr item1, wxUIntPtr item2) { CSortingList::const_iterator it = m_sort_orders.begin(); for (; it != m_sort_orders.end(); ++it) { int result = m_sort_func(item1, item2, it->first | it->second); if (result != 0) { return result; } } // Ensure that different items are never considered equal. return CmpAny(item1, item2); } int CMuleListCtrl::SortProc(wxUIntPtr item1, wxUIntPtr item2, long data) { MuleSortData* sortdata = (MuleSortData*) data; const CSortingList& orders = sortdata->m_sort_orders; CSortingList::const_iterator it = orders.begin(); for (; it != orders.end(); ++it) { int result = sortdata->m_sort_func(item1, item2, it->first | it->second); if (result != 0) { return result; } } // Ensure that different items are never considered equal. return CmpAny(item1, item2); } void CMuleListCtrl::SortList() { if (!IsSorting() && (m_sort_func && GetColumnCount())) { m_isSorting = true; MuleSortData sortdata(m_sort_orders, m_sort_func); // In many cases control already has correct order, and sorting causes nasty flickering. // Make one pass through it to check if sorting is necessary at all. int nrItems = GetItemCount(); bool clean = true; long lastItemdata = 0; if (nrItems > 1) { lastItemdata = GetItemData(0); } for (int i = 1; i < nrItems; i++) { long nextItemdata = GetItemData(i); if (SortProc(lastItemdata, nextItemdata, (long int)&sortdata) > 0) { // ok - we need to sort clean = false; break; } lastItemdata = nextItemdata; } if (clean) { // no need to sort m_isSorting = false; return; } // Positions are likely to be invalid after sorting. ResetTTS(); // Store the current selected items ItemDataList selectedItems = GetSelectedItems(); // Store the focused item long pos = GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_FOCUSED ); wxUIntPtr focused = (pos == -1) ? 0 : GetItemData(pos); SortItems(SortProc, (long int)&sortdata); // Re-select the selected items. for (unsigned i = 0; i < selectedItems.size(); ++i) { long it_pos = FindItem(-1, selectedItems[i]); if (it_pos != -1) { SetItemState(it_pos, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); } } // Set focus on item if any was focused if (focused) { long it_pos = FindItem(-1, focused); if (it_pos != -1) { SetItemState(it_pos, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED); } } m_isSorting = false; } } CMuleListCtrl::ItemDataList CMuleListCtrl::GetSelectedItems() const { // Create the initial vector with as many items as are selected ItemDataList list( GetSelectedItemCount() ); // Current item being located unsigned int current = 0; long pos = GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ); while ( pos != -1 ) { wxASSERT( current < list.size() ); list[ current++ ] = GetItemData( pos ); pos = GetNextItem( pos, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ); } return list; } void CMuleListCtrl::OnColumnRClick(wxListEvent& evt) { wxMenu menu; wxListItem item; for ( int i = 0; i < GetColumnCount() && i < 15; ++i) { GetColumn(i, item); menu.AppendCheckItem(i + MP_LISTCOL_1, item.GetText() ); menu.Check( i + MP_LISTCOL_1, GetColumnWidth(i) > COL_SIZE_MIN ); } PopupMenu(&menu, evt.GetPoint()); } void CMuleListCtrl::OnMenuSelected( wxCommandEvent& evt ) { unsigned int col = evt.GetId() - MP_LISTCOL_1; if (col >= m_column_sizes.size()) { m_column_sizes.resize(col + 1, 0); } if (GetColumnWidth(col) > COL_SIZE_MIN) { m_column_sizes[col] = GetColumnWidth(col); SetColumnWidth(col, 0); } else { int oldsize = m_column_sizes[col]; SetColumnWidth(col, (oldsize > 0) ? oldsize : GetColumnDefaultWidth(col)); } } void CMuleListCtrl::OnColumnLClick(wxListEvent& evt) { // Stop if no sorter-function has been defined if (!m_sort_func) { return; } else if (evt.GetColumn() == -1) { // This happens if a user clicks past the last column header. return; } unsigned sort_order = 0; if (m_sort_orders.front().first == (unsigned)evt.GetColumn()) { // Same column as before, flip the sort-order sort_order = m_sort_orders.front().second; if (sort_order & SORT_DES) { if (AltSortAllowed(evt.GetColumn())) { sort_order = (~sort_order) & SORT_ALT; } else { sort_order = 0; } } else { sort_order = SORT_DES | (sort_order & SORT_ALT); } m_sort_orders.pop_front(); } else { // Check if the column has already been set CSortingList::iterator it = m_sort_orders.begin(); for (; it != m_sort_orders.end(); ++it) { if ((unsigned)evt.GetColumn() == it->first) { sort_order = it->second; break; } } } SetSorting(evt.GetColumn(), sort_order); } void CMuleListCtrl::ClearSelection() { if (GetSelectedItemCount()) { long index = GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); while (index != -1) { SetItemState(index, 0, wxLIST_STATE_SELECTED); index = GetNextItem(index, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); } } } bool CMuleListCtrl::AltSortAllowed(unsigned WXUNUSED(column)) const { return false; } void CMuleListCtrl::SetSorting(unsigned column, unsigned order) { MULE_VALIDATE_PARAMS(column < (unsigned)GetColumnCount(), wxT("Invalid column to sort by.")); MULE_VALIDATE_PARAMS(!(order & ~SORTING_MASK), wxT("Sorting order contains invalid data.")); if (!m_sort_orders.empty()) { SetColumnImage(m_sort_orders.front().first, -1); CSortingList::iterator it = m_sort_orders.begin(); for (; it != m_sort_orders.end(); ++it) { if (it->first == column) { m_sort_orders.erase(it); break; } } } m_sort_orders.push_front(CColPair(column, order)); if (order & SORT_DES) { SetColumnImage(column, (order & SORT_ALT) ? 2 : 0); } else { SetColumnImage(column, (order & SORT_ALT) ? 3 : 1); } SortList(); } bool CMuleListCtrl::IsItemSorted(long item) { wxCHECK_MSG(m_sort_func, true, wxT("No sort function specified!")); wxCHECK_MSG((item >= 0) && (item < GetItemCount()), true, wxT("Invalid item")); bool sorted = true; wxUIntPtr data = GetItemData(item); // Check that the item before the current item is smaller (or equal) if (item > 0) { sorted &= (CompareItems(GetItemData(item - 1), data) <= 0); } // Check that the item after the current item is greater (or equal) if (sorted && (item < GetItemCount() - 1)) { sorted &= (CompareItems(GetItemData(item + 1), data) >= 0); } return sorted; } void CMuleListCtrl::OnMouseWheel(wxMouseEvent &event) { // This enables scrolling with the mouse wheel event.Skip(); } void CMuleListCtrl::SetColumnImage(unsigned col, int image) { wxListItem item; item.SetMask(wxLIST_MASK_IMAGE); item.SetImage(image); SetColumn(col, item); } long CMuleListCtrl::CheckSelection(wxMouseEvent& event) { int flags = 0; wxListEvent evt; evt.m_itemIndex = HitTest(event.GetPosition(), flags); return CheckSelection(evt); } long CMuleListCtrl::CheckSelection(wxListEvent& event) { long item = event.GetIndex(); // Check if clicked item is selected. If not, unselect all and select it. if ((item != -1) && !GetItemState(item, wxLIST_STATE_SELECTED)) { ClearSelection(); SetItemState(item, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); } return item; } wxString CMuleListCtrl::GetTTSText(unsigned item) const { MULE_VALIDATE_PARAMS(item < (unsigned)GetItemCount(), wxT("Invalid row.")); MULE_VALIDATE_STATE((GetWindowStyle() & wxLC_OWNERDRAW) == 0, wxT("GetTTSText must be overwritten for owner-drawn lists.")); return GetItemText(item); } void CMuleListCtrl::OnChar(wxKeyEvent& evt) { int key = evt.GetKeyCode(); if (key == 0) { // We prefer GetKeyCode() to GetUnicodeKey(), in order to work // around a bug in the GetUnicodeKey(), that causes values to // be returned untranslated. This means for instance, that if // shift and '1' is pressed, the result is '1' rather than '!' // (as it should be on my keyboard). This has been reported: // http://sourceforge.net/tracker/index.php?func=detail&aid=1864810&group_id=9863&atid=109863 key = evt.GetUnicodeKey(); } else if (key >= WXK_START) { // wxKeycodes are ignored, as they signify events such as the 'home' // button. Unicoded chars are not checked as there is an overlap valid // chars and the wx keycodes. evt.Skip(); return; } // We wish to avoid handling shortcuts, with the exception of 'select-all'. if (evt.AltDown() || evt.ControlDown() || evt.MetaDown()) { if (evt.CmdDown() && (evt.GetKeyCode() == 0x01)) { // Ctrl+a (Command+a on Mac) was pressed, select all items for (int i = 0; i < GetItemCount(); ++i) { SetItemState(i, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); } } evt.Skip(); return; } else if (m_tts_time + 1500u < GetTickCount()) { m_tts_text.Clear(); } m_tts_time = GetTickCount(); m_tts_text.Append(wxTolower(key)); // May happen if the subclass does not forward deletion events. // Or rather when single-char-repeated (see below) wraps around, so don't assert. if (m_tts_item >= GetItemCount()) { m_tts_item = -1; } unsigned next = (m_tts_item == -1) ? 0 : m_tts_item; for (unsigned i = 0, count = GetItemCount(); i < count; ++i) { wxString text = GetTTSText((next + i) % count).MakeLower(); if (text.StartsWith(m_tts_text)) { ClearSelection(); m_tts_item = (next + i) % count; SetItemState(m_tts_item, wxLIST_STATE_FOCUSED | wxLIST_STATE_SELECTED, wxLIST_STATE_FOCUSED | wxLIST_STATE_SELECTED); EnsureVisible(m_tts_item); return; } } if (m_tts_item != -1) { // Crop the string so that it matches the old item (avoid typos). wxString text = GetTTSText(m_tts_item).MakeLower(); // If the last key didn't result in a hit, then we skip the event. if (!text.StartsWith(m_tts_text)) { if ((m_tts_text.Length() == 2) && (m_tts_text[0] == m_tts_text[1])) { // Special case, single-char, repeated. This allows toggeling // between items starting with a specific letter. m_tts_text.Clear(); // Increment, so the next will be selected (or wrap around). m_tts_item++; OnChar(evt); } else { m_tts_text.RemoveLast(); evt.Skip(true); } } } else { evt.Skip(true); } } void CMuleListCtrl::OnItemSelected(wxListEvent& evt) { if (IsSorting()) { // Selection/Deselection that happened while sorting. evt.Veto(); } else { // We reset the current TTS session if the user manually changes the selection if (m_tts_item != evt.GetIndex()) { ResetTTS(); // The item is changed so that the next TTS starts from the selected item. m_tts_item = evt.GetIndex(); } evt.Skip(); } } void CMuleListCtrl::OnItemDeleted(wxListEvent& evt) { if (evt.GetIndex() <= m_tts_item) { m_tts_item--; } evt.Skip(); } void CMuleListCtrl::OnAllItemsDeleted(wxListEvent& evt) { ResetTTS(); evt.Skip(); } void CMuleListCtrl::ResetTTS() { m_tts_item = -1; m_tts_time = 0; } wxString CMuleListCtrl::GetOldColumnOrder() const { return wxEmptyString; } // File_checked_for_headers