// ================================================================================ // == This file is a part of Turbo Badger. (C) 2011-2014, Emil Segerås == // == See tb_core.h for more information. == // ================================================================================ #include "tb_select.h" #include "tb_menu_window.h" #include "tb_widgets_listener.h" #include "tb_language.h" #include "tb_tempbuffer.h" #include "tb_sort.h" namespace tb { // == Sort callback for sorting items =================================================== static int select_list_sort_cb(TBSelectItemSource *source, const int *a, const int *b) { int value = strcmp(source->GetItemString(*a), source->GetItemString(*b)); return source->GetSort() == TB_SORT_DESCENDING ? -value : value; } // == TBSelectList ============================================== TBSelectList::TBSelectList() : m_value(-1) , m_list_is_invalid(false) , m_scroll_to_current(false) , m_header_lng_string_id(TBIDC("TBList.header")) , m_sort_callback(select_list_sort_cb) , m_ui_list_view(false) { SetSource(&m_default_source); SetIsFocusable(true); SetSkinBg(TBIDC("TBSelectList"), WIDGET_INVOKE_INFO_NO_CALLBACKS); m_container.SetGravity(WIDGET_GRAVITY_ALL); m_container.SetRect(GetPaddingRect()); AddChild(&m_container); m_layout.SetGravity(WIDGET_GRAVITY_ALL); m_layout.SetAxis(AXIS_Y); m_layout.SetSpacing(0); m_layout.SetLayoutPosition(LAYOUT_POSITION_LEFT_TOP); m_layout.SetLayoutDistributionPosition(LAYOUT_DISTRIBUTION_POSITION_LEFT_TOP); m_layout.SetLayoutSize(LAYOUT_SIZE_AVAILABLE); m_container.GetContentRoot()->AddChild(&m_layout); m_container.SetScrollMode(SCROLL_MODE_Y_AUTO); m_container.SetAdaptContentSize(true); } TBSelectList::~TBSelectList() { m_container.GetContentRoot()->RemoveChild(&m_layout); RemoveChild(&m_container); SetSource(nullptr); } void TBSelectList::OnSourceChanged() { InvalidateList(); } void TBSelectList::OnItemChanged(int index) { if (m_list_is_invalid) // We're updating all widgets soon. return; TBWidget *old_widget = GetItemWidget(index); if (!old_widget) // We don't have this widget so we have nothing to update. return; // Replace the old widget representing the item, with a new one. Preserve its state. WIDGET_STATE old_state = old_widget->GetStateRaw(); if (TBWidget *widget = CreateAndAddItemAfter(index, old_widget)) widget->SetStateRaw(old_state); old_widget->GetParent()->RemoveChild(old_widget); delete old_widget; } void TBSelectList::OnItemAdded(int index) { if (m_list_is_invalid) // We're updating all widgets soon. return; // Sorting, filtering etc. makes it messy to handle dynamic addition of items. // Resort to invalidate the entire list (may even be faster anyway) InvalidateList(); } void TBSelectList::OnItemRemoved(int index) { if (m_list_is_invalid) // We're updating all widgets soon. return; // Sorting, filtering etc. makes it messy to handle dynamic addition of items. // Resort to invalidate the entire list (may even be faster anyway) InvalidateList(); } void TBSelectList::OnAllItemsRemoved() { InvalidateList(); m_value = -1; } void TBSelectList::SetFilter(const char *filter) { TBStr new_filter; if (filter && *filter) new_filter.Set(filter); if (m_filter.Equals(new_filter)) return; m_filter.Set(new_filter); InvalidateList(); } void TBSelectList::SetHeaderString(const TBID& id) { if (m_header_lng_string_id == id) return; m_header_lng_string_id = id; InvalidateList(); } void TBSelectList::InvalidateList() { if (m_list_is_invalid) return; m_list_is_invalid = true; Invalidate(); } void TBSelectList::ValidateList() { if (!m_list_is_invalid) return; m_list_is_invalid = false; // FIX: Could delete and create only the changed items (faster filter change) // Remove old items while (TBWidget *child = m_layout.GetContentRoot()->GetFirstChild()) { child->GetParent()->RemoveChild(child); delete child; } if (!m_source || !m_source->GetNumItems()) return; // Create a sorted list of the items we should include using the current filter. TBTempBuffer sort_buf; if (!sort_buf.Reserve(m_source->GetNumItems() * sizeof(int))) return; // Out of memory int *sorted_index = (int *) sort_buf.GetData(); // Populate the sorted index list int num_sorted_items = 0; for (int i = 0; i < m_source->GetNumItems(); i++) if (m_filter.IsEmpty() || m_source->Filter(i, m_filter)) sorted_index[num_sorted_items++] = i; // Sort if (m_source->GetSort() != TB_SORT_NONE) insertion_sort(sorted_index, num_sorted_items, m_source, m_sort_callback); // Show header if we only show a subset of all items. // a single space filter is used as a dummy filter if (!m_filter.IsEmpty() && (!(m_filter.Length() == 1 && m_filter[0] == ' '))) { if (TBWidget *widget = new TBTextField()) { TBStr str; str.SetFormatted(g_tb_lng->GetString(m_header_lng_string_id), num_sorted_items, m_source->GetNumItems()); widget->SetText(str); widget->SetSkinBg(TBIDC("TBList.header")); widget->SetState(WIDGET_STATE_DISABLED, true); widget->SetGravity(WIDGET_GRAVITY_ALL); widget->data.SetInt(-1); m_layout.GetContentRoot()->AddChild(widget); } } // Create new items for (int i = 0; i < num_sorted_items; i++) CreateAndAddItemAfter(sorted_index[i], nullptr); SelectItem(m_value, true); // FIX: Should not scroll just because we update the list. Only automatically first time! m_scroll_to_current = true; TBWidgetEvent ev(EVENT_TYPE_CUSTOM); // TBIDC does not register the string with the UI system TBID refid("select_list_validation_end"); ev.ref_id = refid; InvokeEvent(ev); } TBWidget *TBSelectList::CreateAndAddItemAfter(int index, TBWidget *reference) { if (TBWidget *widget = m_source->CreateItemWidget(index, this)) { // Use item data as widget to index lookup widget->data.SetInt(index); m_layout.GetContentRoot()->AddChildRelative(widget, WIDGET_Z_REL_AFTER, reference); return widget; } return nullptr; } void TBSelectList::SetValue(int value) { if (value == m_value) return; if (!m_ui_list_view) SelectItem(m_value, false); m_value = value; SelectItem(m_value, true); ScrollToSelectedItem(); TBWidgetEvent ev(EVENT_TYPE_CHANGED); if (TBWidget *widget = GetItemWidget(m_value)) ev.ref_id = widget->GetID(); InvokeEvent(ev); } TBID TBSelectList::GetItemID(int index) const { if (!m_source) return TBID(); return m_source->GetItemID(index); } int TBSelectList::GetNumItems() const { if (!m_source) return 0; return m_source->GetNumItems(); } TBID TBSelectList::GetSelectedItemID() { if (m_source && m_value >= 0 && m_value < m_source->GetNumItems()) return m_source->GetItemID(m_value); return TBID(); } void TBSelectList::SelectItem(int index, bool selected) { if (!m_ui_list_view) { if (TBWidget *widget = GetItemWidget(index)) widget->SetState(WIDGET_STATE_SELECTED, selected); } } TBWidget *TBSelectList::GetItemWidget(int index) { if (index == -1) return nullptr; for (TBWidget *tmp = m_layout.GetContentRoot()->GetFirstChild(); tmp; tmp = tmp->GetNext()) if (tmp->data.GetInt() == index) return tmp; return nullptr; } void TBSelectList::ScrollToSelectedItem() { if (m_list_is_invalid) { m_scroll_to_current = true; return; } m_scroll_to_current = false; if (TBWidget *widget = GetItemWidget(m_value)) m_container.ScrollIntoView(widget->GetRect()); else m_container.ScrollTo(0, 0); } void TBSelectList::OnSkinChanged() { m_container.SetRect(GetPaddingRect()); } void TBSelectList::OnProcess() { ValidateList(); } void TBSelectList::OnProcessAfterChildren() { if (m_scroll_to_current) ScrollToSelectedItem(); } bool TBSelectList::OnEvent(const TBWidgetEvent &ev) { if (ev.type == EVENT_TYPE_CLICK && ev.target->GetParent() == m_layout.GetContentRoot()) { // SetValue (EVENT_TYPE_CHANGED) might cause something to delete this (f.ex closing // the dropdown menu. We want to sent another event, so ensure we're still around. TBWidgetSafePointer this_widget(this); int index = ev.target->data.GetInt(); if (!m_ui_list_view) SetValue(index); else { if (TBWidget *widget = GetItemWidget(index)) { TBWidgetEvent change_ev(EVENT_TYPE_CUSTOM); // TBIDC does not register the string with the UI system TBID refid("select_list_selection_changed"); change_ev.ref_id = refid; change_ev.modifierkeys = ev.modifierkeys; // forward to delegate widget->InvokeEvent(change_ev); } } // If we're still around, invoke the click event too. if (this_widget.Get()) { TBSelectList *target_list = this; // If the parent window is a TBMenuWindow, we will iterate up the event destination // chain to find the top TBMenuWindow and invoke the event there. // That way events in submenus will reach the caller properly, and seem like it was // invoked on the top menu. TBWindow *window = GetParentWindow(); while (TBMenuWindow *menu_win = TBSafeCast(window)) { target_list = menu_win->GetList(); window = menu_win->GetEventDestination()->GetParentWindow(); } // Invoke the click event on the target list TBWidgetEvent ev(EVENT_TYPE_CLICK); if (TBWidget *widget = GetItemWidget(m_value)) ev.ref_id = widget->GetID(); target_list->InvokeEvent(ev); } return true; } else if (ev.type == EVENT_TYPE_KEY_DOWN) { if (ChangeValue(ev.special_key)) return true; // Give the scroll container a chance to handle the key so it may // scroll. This matters if the list itself is focused instead of // some child view of any select item (since that would have passed // the container already) if (GetScrollContainer()->OnEvent(ev)) return true; } return TBWidget::OnEvent(ev); } bool TBSelectList::ChangeValue(SPECIAL_KEY key) { if (m_ui_list_view || !m_source || !m_layout.GetContentRoot()->GetFirstChild()) return false; bool forward; if (key == TB_KEY_HOME || key == TB_KEY_DOWN) forward = true; else if (key == TB_KEY_END || key == TB_KEY_UP) forward = false; else return false; TBWidget *item_root = m_layout.GetContentRoot(); TBWidget *current = GetItemWidget(m_value); TBWidget *origin = nullptr; if (key == TB_KEY_HOME || (!current && key == TB_KEY_DOWN)) current = item_root->GetFirstChild(); else if (key == TB_KEY_END || (!current && key == TB_KEY_UP)) current = item_root->GetLastChild(); else origin = current; while (current) { if (current != origin && !current->GetDisabled()) break; current = forward ? current->GetNext() : current->GetPrev(); } // Select and focus what we found if (current) { SetValue(current->data.GetInt()); return true; } return false; } bool TBSelectList::GetItemSelected(int index) { if (TBWidget *widget = GetItemWidget(index)) return widget->GetState(WIDGET_STATE_SELECTED); return false; } void TBSelectList::SelectAllItems(bool select) { if (!m_source) return; for (int i = 0; i < m_source->GetNumItems(); i++) { SelectItem(i, select); } if (!select) m_value = -1; } // == TBSelectDropdown ========================================== TBSelectDropdown::TBSelectDropdown() : m_value(-1) { SetSource(&m_default_source); SetSkinBg(TBIDC("TBSelectDropdown"), WIDGET_INVOKE_INFO_NO_CALLBACKS); m_arrow.SetSkinBg(TBIDC("TBSelectDropdown.arrow"), WIDGET_INVOKE_INFO_NO_CALLBACKS); GetContentRoot()->AddChild(&m_arrow); } TBSelectDropdown::~TBSelectDropdown() { GetContentRoot()->RemoveChild(&m_arrow); SetSource(nullptr); CloseWindow(); } void TBSelectDropdown::OnSourceChanged() { m_value = -1; if (m_source && m_source->GetNumItems()) SetValue(0); } void TBSelectDropdown::OnItemChanged(int index) { } void TBSelectDropdown::SetValue(int value) { if (value == m_value || !m_source) return; m_value = value; if (m_value < 0) SetText(""); else if (m_value < m_source->GetNumItems()) SetText(m_source->GetItemString(m_value)); TBWidgetEvent ev(EVENT_TYPE_CHANGED); InvokeEvent(ev); } TBID TBSelectDropdown::GetSelectedItemID() { if (m_source && m_value >= 0 && m_value < m_source->GetNumItems()) return m_source->GetItemID(m_value); return TBID(); } void TBSelectDropdown::OpenWindow() { if (!m_source || !m_source->GetNumItems() || m_window_pointer.Get()) return; if (TBMenuWindow *window = new TBMenuWindow(this, TBIDC("TBSelectDropdown.window"))) { m_window_pointer.Set(window); window->SetSkinBg(TBIDC("TBSelectDropdown.window")); window->Show(m_source, TBPopupAlignment(), GetValue()); } } void TBSelectDropdown::CloseWindow() { if (TBMenuWindow *window = GetMenuIfOpen()) window->Close(); } TBMenuWindow *TBSelectDropdown::GetMenuIfOpen() const { return TBSafeCast(m_window_pointer.Get()); } bool TBSelectDropdown::OnEvent(const TBWidgetEvent &ev) { if (ev.target == this && ev.type == EVENT_TYPE_CLICK) { // Open the menu, or set the value and close it if already open (this will // happen when clicking by keyboard since that will call click on this button) if (TBMenuWindow *menu_window = GetMenuIfOpen()) { TBWidgetSafePointer tmp(this); int value = menu_window->GetList()->GetValue(); menu_window->Die(); if (tmp.Get()) SetValue(value); } else OpenWindow(); return true; } else if (ev.target->GetID() == TBIDC("TBSelectDropdown.window") && ev.type == EVENT_TYPE_CLICK) { // Set the value of the clicked item if (TBMenuWindow *menu_window = GetMenuIfOpen()) SetValue(menu_window->GetList()->GetValue()); return true; } else if (ev.target == this && m_source && ev.IsKeyEvent()) { if (TBMenuWindow *menu_window = GetMenuIfOpen()) { // Redirect the key strokes to the list TBWidgetEvent redirected_ev(ev); return menu_window->GetList()->InvokeEvent(redirected_ev); } } return false; } } // namespace tb