This was meant as an experiment to see how tangible it is to rewrite the File Browser UI code to be based on views, starting with the grid view for thumbnail mode. See T99890. My initial conclusion is that porting to views is quite doable, but we'll need some further UI code features to make certain things possible. Like big "composed" icons, where a file type icon is displayed on top of a big, generic file icon. There is a fair bit of stuff here that I'm not happy with. Plus things like selection, double clicking to open and renaming don't work yet. It's a start, a proof of concept even :)
462 lines
14 KiB
C++
462 lines
14 KiB
C++
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
/** \file
|
|
* \ingroup edinterface
|
|
*/
|
|
|
|
#include <limits>
|
|
#include <stdexcept>
|
|
|
|
#include "BLI_index_range.hh"
|
|
#include "BLI_math_vector.h"
|
|
|
|
#include "WM_types.h"
|
|
|
|
#include "UI_interface.h"
|
|
#include "interface_intern.h"
|
|
|
|
#include "UI_grid_view.hh"
|
|
|
|
namespace blender::ui {
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
AbstractGridView::AbstractGridView() : style_(UI_preview_tile_size_x(), UI_preview_tile_size_y())
|
|
{
|
|
}
|
|
|
|
AbstractGridViewItem &AbstractGridView::add_item(std::unique_ptr<AbstractGridViewItem> item)
|
|
{
|
|
items_.append(std::move(item));
|
|
|
|
AbstractGridViewItem &added_item = *items_.last();
|
|
item_map_.add(added_item.identifier_, &added_item);
|
|
register_item(added_item);
|
|
|
|
return added_item;
|
|
}
|
|
|
|
void AbstractGridView::foreach_item(ItemIterFn iter_fn) const
|
|
{
|
|
for (const auto &item_ptr : items_) {
|
|
iter_fn(*item_ptr);
|
|
}
|
|
}
|
|
|
|
AbstractGridViewItem *AbstractGridView::find_matching_item(
|
|
const AbstractGridViewItem &item_to_match, const AbstractGridView &view_to_search_in) const
|
|
{
|
|
AbstractGridViewItem *const *match = view_to_search_in.item_map_.lookup_ptr(
|
|
item_to_match.identifier_);
|
|
BLI_assert(!match || item_to_match.matches(**match));
|
|
|
|
return match ? *match : nullptr;
|
|
}
|
|
|
|
void AbstractGridView::change_state_delayed()
|
|
{
|
|
BLI_assert_msg(
|
|
is_reconstructed(),
|
|
"These state changes are supposed to be delayed until reconstruction is completed");
|
|
foreach_item([](AbstractGridViewItem &item) { item.change_state_delayed(); });
|
|
}
|
|
|
|
void AbstractGridView::update_children_from_old(const AbstractView &old_view)
|
|
{
|
|
const AbstractGridView &old_grid_view = dynamic_cast<const AbstractGridView &>(old_view);
|
|
|
|
foreach_item([this, &old_grid_view](AbstractGridViewItem &new_item) {
|
|
const AbstractGridViewItem *matching_old_item = find_matching_item(new_item, old_grid_view);
|
|
if (!matching_old_item) {
|
|
return;
|
|
}
|
|
|
|
new_item.update_from_old(*matching_old_item);
|
|
});
|
|
}
|
|
|
|
const GridViewStyle &AbstractGridView::get_style() const
|
|
{
|
|
return style_;
|
|
}
|
|
|
|
int AbstractGridView::get_item_count() const
|
|
{
|
|
return items_.size();
|
|
}
|
|
|
|
GridViewStyle::GridViewStyle(int width, int height) : tile_width(width), tile_height(height)
|
|
{
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
AbstractGridViewItem::AbstractGridViewItem(StringRef identifier) : identifier_(identifier)
|
|
{
|
|
}
|
|
|
|
bool AbstractGridViewItem::matches(const AbstractViewItem &other) const
|
|
{
|
|
const AbstractGridViewItem &other_grid_item = dynamic_cast<const AbstractGridViewItem &>(other);
|
|
return identifier_ == other_grid_item.identifier_;
|
|
}
|
|
|
|
void AbstractGridViewItem::grid_tile_click_fn(struct bContext * /*C*/,
|
|
void *but_arg1,
|
|
void * /*arg2*/)
|
|
{
|
|
uiButViewItem *view_item_but = (uiButViewItem *)but_arg1;
|
|
AbstractGridViewItem &grid_item = reinterpret_cast<AbstractGridViewItem &>(
|
|
*view_item_but->view_item);
|
|
|
|
grid_item.activate();
|
|
}
|
|
|
|
void AbstractGridViewItem::add_grid_tile_button(uiBlock &block)
|
|
{
|
|
const GridViewStyle &style = get_view().get_style();
|
|
view_item_but_ = (uiButViewItem *)uiDefBut(&block,
|
|
UI_BTYPE_VIEW_ITEM,
|
|
0,
|
|
"",
|
|
0,
|
|
0,
|
|
style.tile_width,
|
|
style.tile_height,
|
|
nullptr,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
"");
|
|
|
|
view_item_but_->view_item = reinterpret_cast<uiViewItemHandle *>(this);
|
|
UI_but_func_set(&view_item_but_->but, grid_tile_click_fn, view_item_but_, nullptr);
|
|
}
|
|
|
|
void AbstractGridViewItem::on_activate()
|
|
{
|
|
/* Do nothing by default. */
|
|
}
|
|
|
|
std::optional<bool> AbstractGridViewItem::should_be_active() const
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
void AbstractGridViewItem::change_state_delayed()
|
|
{
|
|
const std::optional<bool> should_be_active = this->should_be_active();
|
|
if (should_be_active.has_value() && *should_be_active) {
|
|
activate();
|
|
}
|
|
}
|
|
|
|
void AbstractGridViewItem::activate()
|
|
{
|
|
BLI_assert_msg(get_view().is_reconstructed(),
|
|
"Item activation can't be done until reconstruction is completed");
|
|
|
|
if (is_active()) {
|
|
return;
|
|
}
|
|
|
|
/* Deactivate other items in the tree. */
|
|
get_view().foreach_item([](auto &item) { item.deactivate(); });
|
|
|
|
on_activate();
|
|
|
|
is_active_ = true;
|
|
}
|
|
|
|
void AbstractGridViewItem::deactivate()
|
|
{
|
|
is_active_ = false;
|
|
}
|
|
|
|
const AbstractGridView &AbstractGridViewItem::get_view() const
|
|
{
|
|
if (UNLIKELY(!view_)) {
|
|
throw std::runtime_error(
|
|
"Invalid state, item must be added through AbstractGridView::add_item()");
|
|
}
|
|
return dynamic_cast<AbstractGridView &>(*view_);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Helper for only adding layout items for grid items that are actually in view. 3 main functions:
|
|
* - #is_item_visible(): Query if an item of a given index is visible in the view (others should be
|
|
* skipped when building the layout).
|
|
* - #fill_layout_before_visible(): Add empty space to the layout before a visible row is drawn, so
|
|
* the layout height is the same as if all items were added (important to get the correct scroll
|
|
* height).
|
|
* - #fill_layout_after_visible(): Same thing, just adds empty space for after the last visible
|
|
* row.
|
|
*
|
|
* Does two assumptions:
|
|
* - Top-to-bottom flow (ymax = 0 and ymin < 0). If that's not good enough, View2D should
|
|
* probably provide queries for the scroll offset.
|
|
* - Only vertical scrolling. For horizontal scrolling, spacers would have to be added on the
|
|
* side(s) as well.
|
|
*/
|
|
class BuildOnlyVisibleButtonsHelper {
|
|
const View2D &v2d_;
|
|
const AbstractGridView &grid_view_;
|
|
const GridViewStyle &style_;
|
|
const int cols_per_row_ = 0;
|
|
/* Indices of items within the view. Calculated by constructor */
|
|
IndexRange visible_items_range_{};
|
|
|
|
public:
|
|
BuildOnlyVisibleButtonsHelper(const View2D &v2d,
|
|
const AbstractGridView &grid_view,
|
|
int cols_per_row);
|
|
|
|
bool is_item_visible(int item_idx) const;
|
|
void fill_layout_before_visible(uiBlock &block) const;
|
|
void fill_layout_after_visible(uiBlock &block) const;
|
|
|
|
private:
|
|
IndexRange get_visible_range() const;
|
|
void add_spacer_button(uiBlock &block, int row_count) const;
|
|
};
|
|
|
|
BuildOnlyVisibleButtonsHelper::BuildOnlyVisibleButtonsHelper(const View2D &v2d,
|
|
const AbstractGridView &grid_view,
|
|
const int cols_per_row)
|
|
: v2d_(v2d), grid_view_(grid_view), style_(grid_view.get_style()), cols_per_row_(cols_per_row)
|
|
{
|
|
visible_items_range_ = get_visible_range();
|
|
}
|
|
|
|
IndexRange BuildOnlyVisibleButtonsHelper::get_visible_range() const
|
|
{
|
|
int first_idx_in_view = 0;
|
|
int max_items_in_view = 0;
|
|
|
|
const float scroll_ofs_y = abs(v2d_.cur.ymax - v2d_.tot.ymax);
|
|
if (!IS_EQF(scroll_ofs_y, 0)) {
|
|
const int scrolled_away_rows = (int)scroll_ofs_y / style_.tile_height;
|
|
|
|
first_idx_in_view = scrolled_away_rows * cols_per_row_;
|
|
}
|
|
|
|
const float view_height = BLI_rctf_size_y(&v2d_.cur);
|
|
const int count_rows_in_view = std::max(round_fl_to_int(view_height / style_.tile_height), 1);
|
|
max_items_in_view = (count_rows_in_view + 1) * cols_per_row_;
|
|
|
|
BLI_assert(max_items_in_view > 0);
|
|
return IndexRange(first_idx_in_view, max_items_in_view);
|
|
}
|
|
|
|
bool BuildOnlyVisibleButtonsHelper::is_item_visible(const int item_idx) const
|
|
{
|
|
return visible_items_range_.contains(item_idx);
|
|
}
|
|
|
|
void BuildOnlyVisibleButtonsHelper::fill_layout_before_visible(uiBlock &block) const
|
|
{
|
|
const float scroll_ofs_y = abs(v2d_.cur.ymax - v2d_.tot.ymax);
|
|
|
|
if (IS_EQF(scroll_ofs_y, 0)) {
|
|
return;
|
|
}
|
|
|
|
const int scrolled_away_rows = (int)scroll_ofs_y / style_.tile_height;
|
|
add_spacer_button(block, scrolled_away_rows);
|
|
}
|
|
|
|
void BuildOnlyVisibleButtonsHelper::fill_layout_after_visible(uiBlock &block) const
|
|
{
|
|
const int last_item_idx = grid_view_.get_item_count() - 1;
|
|
const int last_visible_idx = visible_items_range_.last();
|
|
|
|
if (last_item_idx > last_visible_idx) {
|
|
const int remaining_rows = (cols_per_row_ > 0) ?
|
|
(last_item_idx - last_visible_idx) / cols_per_row_ :
|
|
0;
|
|
BuildOnlyVisibleButtonsHelper::add_spacer_button(block, remaining_rows);
|
|
}
|
|
}
|
|
|
|
void BuildOnlyVisibleButtonsHelper::add_spacer_button(uiBlock &block, const int row_count) const
|
|
{
|
|
/* UI code only supports button dimensions of `signed short` size, the layout height we want to
|
|
* fill may be bigger than that. So add multiple labels of the maximum size if necessary. */
|
|
for (int remaining_rows = row_count; remaining_rows > 0;) {
|
|
const short row_count_this_iter = std::min(
|
|
std::numeric_limits<short>::max() / style_.tile_height, remaining_rows);
|
|
|
|
uiDefBut(&block,
|
|
UI_BTYPE_LABEL,
|
|
0,
|
|
"",
|
|
0,
|
|
0,
|
|
UI_UNIT_X,
|
|
row_count_this_iter * style_.tile_height,
|
|
nullptr,
|
|
0,
|
|
0,
|
|
0,
|
|
0,
|
|
"");
|
|
remaining_rows -= row_count_this_iter;
|
|
}
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
class GridViewLayoutBuilder {
|
|
uiBlock &block_;
|
|
|
|
friend class GridViewBuilder;
|
|
|
|
public:
|
|
GridViewLayoutBuilder(uiBlock &block);
|
|
|
|
void build_from_view(const AbstractGridView &grid_view, const View2D &v2d) const;
|
|
|
|
private:
|
|
void build_grid_tile(uiLayout &grid_layout, AbstractGridViewItem &item) const;
|
|
|
|
uiLayout *current_layout() const;
|
|
};
|
|
|
|
GridViewLayoutBuilder::GridViewLayoutBuilder(uiBlock &block) : block_(block)
|
|
{
|
|
}
|
|
|
|
void GridViewLayoutBuilder::build_grid_tile(uiLayout &grid_layout,
|
|
AbstractGridViewItem &item) const
|
|
{
|
|
uiLayout *overlap = uiLayoutOverlap(&grid_layout);
|
|
|
|
item.add_grid_tile_button(block_);
|
|
item.build_grid_tile(*uiLayoutRow(overlap, false));
|
|
}
|
|
|
|
void GridViewLayoutBuilder::build_from_view(const AbstractGridView &grid_view,
|
|
const View2D &v2d) const
|
|
{
|
|
uiLayout *prev_layout = current_layout();
|
|
|
|
uiLayout &layout = *uiLayoutColumn(current_layout(), false);
|
|
const GridViewStyle &style = grid_view.get_style();
|
|
|
|
const int cols_per_row = std::max(uiLayoutGetWidth(&layout) / style.tile_width, 1);
|
|
|
|
BuildOnlyVisibleButtonsHelper build_visible_helper(v2d, grid_view, cols_per_row);
|
|
|
|
build_visible_helper.fill_layout_before_visible(block_);
|
|
|
|
/* Use `-cols_per_row` because the grid layout uses a multiple of the passed absolute value for
|
|
* the number of columns then, rather than distributing the number of items evenly over rows and
|
|
* stretching the items to fit (see #uiLayoutItemGridFlow.columns_len). */
|
|
uiLayout *grid_layout = uiLayoutGridFlow(&layout, true, -cols_per_row, true, true, true);
|
|
|
|
int item_idx = 0;
|
|
grid_view.foreach_item([&](AbstractGridViewItem &item) {
|
|
/* Skip if item isn't visible. */
|
|
if (!build_visible_helper.is_item_visible(item_idx)) {
|
|
item_idx++;
|
|
return;
|
|
}
|
|
|
|
build_grid_tile(*grid_layout, item);
|
|
item_idx++;
|
|
});
|
|
|
|
/* If there are not enough items to fill the layout, add padding items so the layout doesn't
|
|
* stretch over the entire width. */
|
|
if (grid_view.get_item_count() < cols_per_row) {
|
|
for (int padding_item_idx = 0; padding_item_idx < (cols_per_row - grid_view.get_item_count());
|
|
padding_item_idx++) {
|
|
uiItemS(grid_layout);
|
|
}
|
|
}
|
|
|
|
UI_block_layout_set_current(&block_, prev_layout);
|
|
|
|
build_visible_helper.fill_layout_after_visible(block_);
|
|
}
|
|
|
|
uiLayout *GridViewLayoutBuilder::current_layout() const
|
|
{
|
|
return block_.curlayout;
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
GridViewBuilder::GridViewBuilder(uiBlock &block) : block_(block)
|
|
{
|
|
}
|
|
|
|
void GridViewBuilder::build_grid_view(AbstractGridView &grid_view, const View2D &v2d)
|
|
{
|
|
grid_view.build_items();
|
|
grid_view.update_from_old(block_);
|
|
grid_view.change_state_delayed();
|
|
|
|
GridViewLayoutBuilder builder(block_);
|
|
builder.build_from_view(grid_view, v2d);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------- */
|
|
|
|
PreviewGridItem::PreviewGridItem(StringRef identifier, StringRef label, int preview_icon_id)
|
|
: AbstractGridViewItem(identifier), label(label), preview_icon_id(preview_icon_id)
|
|
{
|
|
}
|
|
|
|
uiBut *PreviewGridItem::add_preview_button(uiLayout &layout,
|
|
const int preview_icon_id,
|
|
const uchar mono_color[4]) const
|
|
{
|
|
const GridViewStyle &style = get_view().get_style();
|
|
uiBlock *block = uiLayoutGetBlock(&layout);
|
|
|
|
return uiDefButPreviewTile(block,
|
|
preview_icon_id,
|
|
label.c_str(),
|
|
0,
|
|
0,
|
|
style.tile_width,
|
|
style.tile_height,
|
|
mono_color);
|
|
}
|
|
|
|
void PreviewGridItem::build_grid_tile(uiLayout &layout) const
|
|
{
|
|
add_preview_button(layout, preview_icon_id);
|
|
}
|
|
|
|
void PreviewGridItem::set_on_activate_fn(ActivateFn fn)
|
|
{
|
|
activate_fn_ = fn;
|
|
}
|
|
|
|
void PreviewGridItem::set_is_active_fn(IsActiveFn fn)
|
|
{
|
|
is_active_fn_ = fn;
|
|
}
|
|
|
|
void PreviewGridItem::on_activate()
|
|
{
|
|
if (activate_fn_) {
|
|
activate_fn_(*this);
|
|
}
|
|
}
|
|
|
|
std::optional<bool> PreviewGridItem::should_be_active() const
|
|
{
|
|
if (is_active_fn_) {
|
|
return is_active_fn_();
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
} // namespace blender::ui
|