blob: 4a9428f376db00711a504e4737a1b79f149de9a1 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/base/models/list_selection_model.h"
#include <algorithm>
#include <optional>
#include <valarray>
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
namespace ui {
namespace {
// Determines the new index for `original_index` after inserting an item at
// `insert_position`. This is called for the selected indices.
size_t GetIndexAfterInsertion(size_t insert_position, size_t original_index) {
return (original_index >= insert_position) ? original_index + 1
: original_index;
}
// Determines the new index for `original_index` after inserting an item at
// `insert_position`. This is called for the active and anchor indexes.
std::optional<size_t> GetIndexAfterInsertion(
size_t insert_position,
std::optional<size_t> original_index) {
return original_index.has_value()
? std::optional<size_t>(GetIndexAfterInsertion(
insert_position, original_index.value()))
: std::nullopt;
}
// Determines the new index for `original_index` after removing an item at
// `remove_position`. This is called for the selected indices. Returns
// std::nullopt if `original_index` should be erased from its container.
std::optional<size_t> GetIndexAfterRemoval(size_t remove_position,
size_t original_index) {
if (original_index == remove_position) {
return std::nullopt;
}
if (original_index > remove_position) {
return original_index - 1;
}
return original_index;
}
// Determines the new index for `original_index` after removing an item at
// `remove_position`. This is called for active and anchor indexes. Returns
// std::nullopt if `original_index` should be erased from its container.
std::optional<size_t> GetIndexAfterRemoval(
size_t remove_position,
std::optional<size_t> original_index) {
return original_index.has_value()
? std::optional<size_t>(GetIndexAfterRemoval(
remove_position, original_index.value()))
: std::nullopt;
}
// Returns the new index for `original_index` when a range of items are moved
// from `source_position` to `destination_position`. This assumes that the items
// are moved to a lower index. This is called for active and anchor indexes.
std::optional<size_t> GetIndexAfterMove(size_t source_position,
size_t destination_position,
size_t range_size,
std::optional<size_t> original_index) {
DCHECK_LE(destination_position, source_position);
if (!original_index.has_value()) {
return std::nullopt;
}
// When a range of items moves to a lower index, the only affected indices
// are those in the interval [destination_position, source_position +
// range_size).
size_t original_index_val = original_index.value();
if (destination_position <= original_index_val &&
original_index_val < source_position + range_size) {
if (original_index_val < source_position) {
// The items originally in the interval [destination_position,
// source_position) see `range_size` many items inserted before them, so
// their indices increase.
return original_index_val + range_size;
} else {
// The items originally in the interval [source_position, source_position
// + range_size) are shifted downward by (source_position -
// destination_position) many spots, so their indices decrease.
return original_index_val - (source_position - destination_position);
}
}
return original_index;
}
} // namespace
ListSelectionModel::ListSelectionModel() = default;
ListSelectionModel::ListSelectionModel(const ListSelectionModel&) = default;
ListSelectionModel::ListSelectionModel(ListSelectionModel&&) noexcept = default;
ListSelectionModel::~ListSelectionModel() = default;
ListSelectionModel& ListSelectionModel::operator=(const ListSelectionModel&) =
default;
ListSelectionModel& ListSelectionModel::operator=(ListSelectionModel&&) =
default;
void ListSelectionModel::IncrementFrom(size_t index) {
// Shift the selection to account for a newly inserted item at |index|.
for (size_t& selected_index : selected_indices_) {
selected_index = GetIndexAfterInsertion(index, selected_index);
}
anchor_ = GetIndexAfterInsertion(index, anchor_);
set_active(GetIndexAfterInsertion(index, active_));
}
void ListSelectionModel::DecrementFrom(size_t index) {
for (auto it = selected_indices_.begin(); it != selected_indices_.end();) {
std::optional<size_t> new_value = GetIndexAfterRemoval(index, *it);
if (new_value == std::nullopt) {
it = selected_indices_.erase(it);
} else {
*it = new_value.value();
++it;
}
}
anchor_ = GetIndexAfterRemoval(index, anchor_);
set_active(GetIndexAfterRemoval(index, active_));
}
void ListSelectionModel::SetSelectedIndex(std::optional<size_t> index) {
anchor_ = index;
set_active(index);
selected_indices_.clear();
if (index.has_value()) {
selected_indices_.insert(index.value());
}
}
bool ListSelectionModel::IsSelected(size_t index) const {
return base::Contains(selected_indices_, index);
}
void ListSelectionModel::AddIndexToSelection(size_t index) {
selected_indices_.insert(index);
}
void ListSelectionModel::AddIndexRangeToSelection(size_t index_start,
size_t index_end) {
DCHECK_LE(index_start, index_end);
if (index_start == index_end)
return AddIndexToSelection(index_start);
for (size_t i = index_start; i <= index_end; ++i) {
selected_indices_.insert(i);
}
}
void ListSelectionModel::RemoveIndexFromSelection(size_t index) {
selected_indices_.erase(index);
}
void ListSelectionModel::SetSelectionFromAnchorTo(size_t index) {
if (!anchor_.has_value()) {
SetSelectedIndex(index);
} else {
SelectedIndices new_selection;
for (size_t min = std::min(index, anchor_.value()),
delta = std::max(index, anchor_.value()) - min, i = min;
i <= min + delta; ++i) {
new_selection.insert(i);
}
selected_indices_.swap(new_selection);
set_active(index);
}
}
void ListSelectionModel::AddSelectionFromAnchorTo(size_t index) {
if (!anchor_.has_value()) {
SetSelectedIndex(index);
} else {
for (size_t i = std::min(index, anchor_.value()),
end = std::max(index, anchor_.value());
i <= end; ++i) {
selected_indices_.insert(i);
}
set_active(index);
}
}
void ListSelectionModel::Move(size_t old_index,
size_t new_index,
size_t length) {
// |length| many items are moving from index |old_index| to index |new_index|.
DCHECK_NE(old_index, new_index);
DCHECK_GT(length, 0u);
// Remap move-to-higher-index operations to the equivalent move-to-lower-index
// operation. As an example, the permutation "ABCDEFG" -> "CDEFABG" can be
// thought of either as shifting 'AB' higher by 4, or by shifting 'CDEF' lower
// by 2.
if (new_index > old_index) {
Move(old_index + length, old_index, new_index - old_index);
return;
}
// We know that |old_index| > |new_index|, so this is a move to a lower index.
// Start by transforming |anchor_| and |active_|.
anchor_ = GetIndexAfterMove(old_index, new_index, length, anchor_);
set_active(GetIndexAfterMove(old_index, new_index, length, active_));
// When a range of items moves to a lower index, the affected items are those
// in the interval [new_index, old_index + length). Search within
// |selected_indices_| for indices that fall into that range.
auto low = std::lower_bound(selected_indices_.begin(),
selected_indices_.end(), new_index);
auto high =
std::lower_bound(low, selected_indices_.end(), old_index + length);
// The items originally in the interval [new_index, old_index) will see
// |length| many items inserted before them, so their indices increase.
auto middle = std::lower_bound(low, high, old_index);
size_t pivot_value = new_index + length;
for (auto it = low; it != middle; ++it) {
(*it) += length;
DCHECK_LE(pivot_value, *it);
DCHECK_LT(*it, old_index + length);
}
// The items originally in the interval [old_index, old_index + length) are
// shifted downward by (old_index - new_index) many spots, so their indices
// decrease.
for (auto it = middle; it != high; ++it) {
(*it) -= (old_index - new_index);
DCHECK_LE(new_index, *it);
DCHECK_LT(*it, pivot_value);
}
// Reorder the ranges [low, middle), and [middle, high) so that the elements
// in [middle, high) appear first, followed by [low, middle). This suffices to
// restore the sort condition on |selected_indices_|, because each range is
// still sorted piecewise, and |pivot_value| is a lower bound for elements in
// [low, middle), and an upper bound for [middle, high).
std::rotate(low, middle, high);
}
void ListSelectionModel::Clear() {
anchor_ = active_ = std::nullopt;
selected_indices_.clear();
}
std::string ListSelectionModel::ToString() const {
const auto optional_to_string = [](const auto& opt) {
return opt.has_value() ? base::NumberToString(opt.value())
: std::string("<none>");
};
std::vector<std::string> index_strings;
std::ranges::transform(
selected_indices_, std::back_inserter(index_strings),
[](const auto& index) { return base::NumberToString(index); });
return "active=" + optional_to_string(active_) +
" anchor=" + optional_to_string(anchor_) +
" selection=" + base::JoinString(index_strings, " ");
}
} // namespace ui