diff options
author | Julian Eisel <julian@blender.org> | 2022-06-16 12:29:20 +0300 |
---|---|---|
committer | Julian Eisel <julian@blender.org> | 2022-06-16 20:25:50 +0300 |
commit | 23d2e77a54f4f813d7ee38ddb06c14ecc0943e4e (patch) | |
tree | ca697c2b8c3e4a3fc2bbbfb8dd1d70a99e0e610a /source | |
parent | 69d3f41d75ec62e3a7c9658104f438e0756a7e01 (diff) |
UI: Add initial "grid view"
Part of T98560.
See https://wiki.blender.org/wiki/Source/Interface/Views
Adds all the basic functionality needed for grid views. They display
items in a grid of rows and columns, typically with a preview image and
a label underneath. Think of the main region in the Asset Browser.
Current features:
- Active item
- Notifier listening (also added this to the tree view)
- Performance: Skip adding buttons that are not scrolled into view
(solves performance problems for big asset libraries, for example).
- Custom item size
- Preview items (items that draw a preview with a label underneath)
- Margins between items scale so the entire region width is filled with
column, rather than leaving a big empty block at the right if there's
not enough space for another column (like the File and current Asset
Browser does it).
- "Data-View Item" theme colors. Not shown in the UI yet.
No user visible changes expected since the grid views aren't used for
anything yet.
This was developed as part of a rewrite of the Asset Browser UI
(`asset-browser-grid-view` branch), see T95653. There's no reason to
keep this part in a branch, continuing development in master makes
things easier.
Grid and tree views have a lot of very similar code, so I'm planning to
unify them to a degree. I kept things separate for the start to first
find out how much and what exactly makes sense to override.
Diffstat (limited to 'source')
20 files changed, 1127 insertions, 37 deletions
diff --git a/source/blender/blenloader/intern/versioning_userdef.c b/source/blender/blenloader/intern/versioning_userdef.c index b3f3b9cbf7d..40359500d3c 100644 --- a/source/blender/blenloader/intern/versioning_userdef.c +++ b/source/blender/blenloader/intern/versioning_userdef.c @@ -347,6 +347,7 @@ static void do_versions_theme(const UserDef *userdef, bTheme *btheme) */ { /* Keep this block, even when empty. */ + btheme->tui.wcol_view_item = U_theme_default.tui.wcol_view_item; } #undef FROM_DEFAULT_V4_UCHAR diff --git a/source/blender/editors/include/UI_grid_view.hh b/source/blender/editors/include/UI_grid_view.hh new file mode 100644 index 00000000000..6f553f4fad1 --- /dev/null +++ b/source/blender/editors/include/UI_grid_view.hh @@ -0,0 +1,264 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup editorui + * + * API for simple creation of grid UIs, supporting typically needed features. + * https://wiki.blender.org/wiki/Source/Interface/Views/Grid_Views + */ + +#pragma once + +#include "BLI_function_ref.hh" +#include "BLI_map.hh" +#include "BLI_vector.hh" + +#include "UI_resources.h" + +struct bContext; +struct PreviewImage; +struct uiBlock; +struct uiButGridTile; +struct uiLayout; +struct View2D; +struct wmNotifier; + +namespace blender::ui { + +class AbstractGridView; + +/* ---------------------------------------------------------------------- */ +/** \name Grid-View Item Type + * \{ */ + +class AbstractGridViewItem { + friend class AbstractGridView; + friend class GridViewLayoutBuilder; + + const AbstractGridView *view_; + + bool is_active_ = false; + + protected: + /** Reference to a string that uniquely identifies this item in the view. */ + StringRef identifier_{}; + /** Every visible item gets a button of type #UI_BTYPE_GRID_TILE during the layout building. */ + uiButGridTile *grid_tile_but_ = nullptr; + + public: + virtual ~AbstractGridViewItem() = default; + + virtual void build_grid_tile(uiLayout &layout) const = 0; + + /** + * Compare this item's identifier to \a other to check if they represent the same data. + * Used to recognize an item from a previous redraw, to be able to keep its state (e.g. active, + * renaming, etc.). + */ + bool matches(const AbstractGridViewItem &other) const; + + const AbstractGridView &get_view() const; + + /** + * Requires the tree to have completed reconstruction, see #is_reconstructed(). Otherwise we + * can't be sure about the item state. + */ + bool is_active() const; + + protected: + AbstractGridViewItem(StringRef identifier); + + /** Called when the item's state changes from inactive to active. */ + virtual void on_activate(); + /** + * If the result is not empty, it controls whether the item should be active or not, + * usually depending on the data that the view represents. + */ + virtual std::optional<bool> should_be_active() const; + + /** + * Copy persistent state (e.g. active, selection, etc.) from a matching item of + * the last redraw to this item. If sub-classes introduce more advanced state they should + * override this and make it update their state accordingly. + */ + virtual void update_from_old(const AbstractGridViewItem &old); + + /** + * Activates this item, deactivates other items, and calls the + * #AbstractGridViewItem::on_activate() function. + * Requires the tree to have completed reconstruction, see #is_reconstructed(). Otherwise the + * actual item state is unknown, possibly calling state-change update functions incorrectly. + */ + void activate(); + void deactivate(); + + private: + /** See #AbstractTreeView::change_state_delayed() */ + void change_state_delayed(); + static void grid_tile_click_fn(bContext *, void *but_arg1, void *); + void add_grid_tile_button(uiBlock &block); +}; + +/** \} */ + +/* ---------------------------------------------------------------------- */ +/** \name Grid-View Base Class + * \{ */ + +struct GridViewStyle { + GridViewStyle(int width, int height); + int tile_width = 0; + int tile_height = 0; +}; + +class AbstractGridView { + friend class AbstractGridViewItem; + friend class GridViewBuilder; + friend class GridViewLayoutBuilder; + + protected: + Vector<std::unique_ptr<AbstractGridViewItem>> items_; + /** <identifier, item> map to lookup items by identifier, used for efficient lookups in + * #update_from_old(). */ + Map<StringRef, AbstractGridViewItem *> item_map_; + GridViewStyle style_; + bool is_reconstructed_ = false; + + public: + AbstractGridView(); + virtual ~AbstractGridView() = default; + + using ItemIterFn = FunctionRef<void(AbstractGridViewItem &)>; + void foreach_item(ItemIterFn iter_fn) const; + + /** Listen to a notifier, returning true if a redraw is needed. */ + virtual bool listen(const wmNotifier &) const; + + /** + * Convenience wrapper constructing the item by forwarding given arguments to the constructor of + * the type (\a ItemT). + * + * E.g. if your grid-item type has the following constructor: + * \code{.cpp} + * MyGridItem(std::string str, int i); + * \endcode + * You can add an item like this: + * \code + * add_item<MyGridItem>("blabla", 42); + * \endcode + */ + template<class ItemT, typename... Args> inline ItemT &add_item(Args &&...args); + const GridViewStyle &get_style() const; + int get_item_count() const; + + protected: + virtual void build_items() = 0; + + /** + * Check if the view is fully (re-)constructed. That means, both #build_items() and + * #update_from_old() have finished. + */ + bool is_reconstructed() const; + + private: + /** + * Match the grid-view against an earlier version of itself (if any) and copy the old UI state + * (e.g. active, selected, renaming, etc.) to the new one. See + * #AbstractGridViewItem.update_from_old(). + */ + void update_from_old(uiBlock &new_block); + AbstractGridViewItem *find_matching_item(const AbstractGridViewItem &item_to_match, + const AbstractGridView &view_to_search_in) const; + /** + * Items may want to do additional work when state changes. But these state changes can only be + * reliably detected after the view has completed reconstruction (see #is_reconstructed()). So + * the actual state changes are done in a delayed manner through this function. + */ + void change_state_delayed(); + + /** + * Add an already constructed item, moving ownership to the grid-view. + * All items must be added through this, it handles important invariants! + */ + AbstractGridViewItem &add_item(std::unique_ptr<AbstractGridViewItem> item); +}; + +/** \} */ + +/* ---------------------------------------------------------------------- */ +/** \name Grid-View Builder + * + * TODO unify this with `TreeViewBuilder` and call view-specific functions via type erased view? + * \{ */ + +class GridViewBuilder { + uiBlock &block_; + + public: + GridViewBuilder(uiBlock &block); + + /** Build \a grid_view into the previously provided block, clipped by \a view_bounds (view space, + * typically `View2D.cur`). */ + void build_grid_view(AbstractGridView &grid_view, const View2D &v2d); +}; + +/** \} */ + +/* ---------------------------------------------------------------------- */ +/** \name Predefined Grid-View Item Types + * + * Common, Basic Grid-View Item Types. + * \{ */ + +/** + * A grid item that shows preview image icons at a nicely readable size (multiple of the normal UI + * unit size). + */ +class PreviewGridItem : public AbstractGridViewItem { + public: + using IsActiveFn = std::function<bool()>; + using ActivateFn = std::function<void(PreviewGridItem &new_active)>; + + protected: + /** See #set_on_activate_fn() */ + ActivateFn activate_fn_; + /** See #set_is_active_fn() */ + IsActiveFn is_active_fn_; + + public: + std::string label{}; + int preview_icon_id = ICON_NONE; + + PreviewGridItem(StringRef identifier, StringRef label, int preview_icon_id); + + void build_grid_tile(uiLayout &layout) const override; + + /** + * Set a custom callback to execute when activating this view item. This way users don't have to + * sub-class #PreviewGridItem, just to implement custom activation behavior (a common thing to + * do). + */ + void set_on_activate_fn(ActivateFn fn); + /** + * Set a custom callback to check if this item should be active. + */ + void set_is_active_fn(IsActiveFn fn); + + private: + std::optional<bool> should_be_active() const override; + void on_activate() override; +}; + +/** \} */ + +/* ---------------------------------------------------------------------- */ + +template<class ItemT, typename... Args> inline ItemT &AbstractGridView::add_item(Args &&...args) +{ + static_assert(std::is_base_of<AbstractGridViewItem, ItemT>::value, + "Type must derive from and implement the AbstractGridViewItem interface"); + + return dynamic_cast<ItemT &>(add_item(std::make_unique<ItemT>(std::forward<Args>(args)...))); +} + +} // namespace blender::ui diff --git a/source/blender/editors/include/UI_interface.h b/source/blender/editors/include/UI_interface.h index 22f5ecb35b8..b2ec2102ddd 100644 --- a/source/blender/editors/include/UI_interface.h +++ b/source/blender/editors/include/UI_interface.h @@ -64,6 +64,7 @@ struct wmKeyMapItem; struct wmMsgBus; struct wmOperator; struct wmOperatorType; +struct wmRegionListenerParams; struct wmWindow; typedef struct uiBlock uiBlock; @@ -75,6 +76,10 @@ typedef struct uiPopupBlockHandle uiPopupBlockHandle; typedef struct uiTreeViewHandle uiTreeViewHandle; /* C handle for C++ #ui::AbstractTreeViewItem type. */ typedef struct uiTreeViewItemHandle uiTreeViewItemHandle; +/* C handle for C++ #ui::AbstractGridView type. */ +typedef struct uiGridViewHandle uiGridViewHandle; +/* C handle for C++ #ui::AbstractGridViewItem type. */ +typedef struct uiGridViewItemHandle uiGridViewItemHandle; /* Defines */ @@ -390,6 +395,8 @@ typedef enum { UI_BTYPE_DECORATOR = 58 << 9, /* An item in a tree view. Parent items may be collapsible. */ UI_BTYPE_TREEROW = 59 << 9, + /* An item in a grid view. */ + UI_BTYPE_GRID_TILE = 60 << 9, } eButType; #define BUTTYPE (63 << 9) @@ -1740,6 +1747,14 @@ struct PointerRNA *UI_but_extra_operator_icon_add(uiBut *but, struct wmOperatorType *UI_but_extra_operator_icon_optype_get(struct uiButExtraOpIcon *extra_icon); struct PointerRNA *UI_but_extra_operator_icon_opptr_get(struct uiButExtraOpIcon *extra_icon); +/** + * A decent size for a button (typically #UI_BTYPE_PREVIEW_TILE) to display a nicely readable + * preview with label in. + */ +int UI_preview_tile_size_x(void); +int UI_preview_tile_size_y(void); +int UI_preview_tile_size_y_no_label(void); + /* Autocomplete * * Tab complete helper functions, for use in uiButCompleteFunc callbacks. @@ -3185,7 +3200,12 @@ void UI_interface_tag_script_reload(void); /* Support click-drag motion which presses the button and closes a popover (like a menu). */ #define USE_UI_POPOVER_ONCE +void UI_block_views_listen(const uiBlock *block, + const struct wmRegionListenerParams *listener_params); + +bool UI_grid_view_item_is_active(const uiGridViewItemHandle *item_handle); bool UI_tree_view_item_is_active(const uiTreeViewItemHandle *item); +bool UI_grid_view_item_matches(const uiGridViewItemHandle *a, const uiGridViewItemHandle *b); bool UI_tree_view_item_matches(const uiTreeViewItemHandle *a, const uiTreeViewItemHandle *b); /** * Attempt to start dragging the tree-item \a item_. This will not work if the tree item doesn't @@ -3223,6 +3243,15 @@ uiTreeViewItemHandle *UI_block_tree_view_find_item_at(const struct ARegion *regi const int xy[2]) ATTR_NONNULL(1, 2); uiTreeViewItemHandle *UI_block_tree_view_find_active_item(const struct ARegion *region); +/** + * Listen to \a notifier, returning true if the region should redraw. + */ +bool UI_tree_view_listen_should_redraw(const uiTreeViewHandle *view, const wmNotifier *notifier); +/** + * Listen to \a notifier, returning true if the region should redraw. + */ +bool UI_grid_view_listen_should_redraw(const uiGridViewHandle *view, const wmNotifier *notifier); + #ifdef __cplusplus } #endif diff --git a/source/blender/editors/include/UI_interface.hh b/source/blender/editors/include/UI_interface.hh index db43ec54431..3dc56b01993 100644 --- a/source/blender/editors/include/UI_interface.hh +++ b/source/blender/editors/include/UI_interface.hh @@ -23,6 +23,7 @@ struct uiSearchItems; namespace blender::ui { +class AbstractGridView; class AbstractTreeView; /** @@ -55,6 +56,10 @@ void attribute_search_add_items( /** * Override this for all available tree types. */ +blender::ui::AbstractGridView *UI_block_add_view( + uiBlock &block, + blender::StringRef idname, + std::unique_ptr<blender::ui::AbstractGridView> tree_view); blender::ui::AbstractTreeView *UI_block_add_view( uiBlock &block, blender::StringRef idname, diff --git a/source/blender/editors/include/UI_tree_view.hh b/source/blender/editors/include/UI_tree_view.hh index 5504e426f34..1aeb13ca5cc 100644 --- a/source/blender/editors/include/UI_tree_view.hh +++ b/source/blender/editors/include/UI_tree_view.hh @@ -28,6 +28,7 @@ struct uiButTreeRow; struct uiLayout; struct wmDrag; struct wmEvent; +struct wmNotifier; namespace blender::ui { @@ -128,6 +129,9 @@ class AbstractTreeView : public TreeViewItemContainer { void foreach_item(ItemIterFn iter_fn, IterOptions options = IterOptions::None) const; + /** Listen to a notifier, returning true if a redraw is needed. */ + virtual bool listen(const wmNotifier &) const; + /** Only one item can be renamed at a time. */ bool is_renaming() const; @@ -185,7 +189,7 @@ class AbstractTreeViewItem : public TreeViewItemContainer { bool is_renaming_ = false; protected: - /** This label is used for identifying an item within its parent. */ + /** This label is used as the default way to identifying an item within its parent. */ std::string label_{}; /** Every visible item gets a button of type #UI_BTYPE_TREEROW during the layout building. */ uiButTreeRow *tree_row_but_ = nullptr; diff --git a/source/blender/editors/interface/CMakeLists.txt b/source/blender/editors/interface/CMakeLists.txt index 8666745c6c4..2a1852bd6e7 100644 --- a/source/blender/editors/interface/CMakeLists.txt +++ b/source/blender/editors/interface/CMakeLists.txt @@ -25,6 +25,7 @@ set(INC ) set(SRC + grid_view.cc interface.cc interface_align.c interface_anim.c diff --git a/source/blender/editors/interface/grid_view.cc b/source/blender/editors/interface/grid_view.cc new file mode 100644 index 00000000000..a82cb7798fe --- /dev/null +++ b/source/blender/editors/interface/grid_view.cc @@ -0,0 +1,525 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup edinterface + */ + +#include <limits> +#include <stdexcept> + +#include "BLI_index_range.hh" + +#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(); + added_item.view_ = this; + + item_map_.add(added_item.identifier_, &added_item); + + return added_item; +} + +void AbstractGridView::foreach_item(ItemIterFn iter_fn) const +{ + for (auto &item_ptr : items_) { + iter_fn(*item_ptr); + } +} + +bool AbstractGridView::listen(const wmNotifier &) const +{ + /* Nothing by default. */ + return false; +} + +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_from_old(uiBlock &new_block) +{ + uiGridViewHandle *old_view_handle = ui_block_grid_view_find_matching_in_old_block( + &new_block, reinterpret_cast<uiGridViewHandle *>(this)); + if (!old_view_handle) { + /* Initial construction, nothing to update. */ + is_reconstructed_ = true; + return; + } + + AbstractGridView &old_view = reinterpret_cast<AbstractGridView &>(*old_view_handle); + + foreach_item([this, &old_view](AbstractGridViewItem &new_item) { + const AbstractGridViewItem *matching_old_item = find_matching_item(new_item, old_view); + if (!matching_old_item) { + return; + } + + new_item.update_from_old(*matching_old_item); + }); + + /* Finished (re-)constructing the tree. */ + is_reconstructed_ = true; +} + +bool AbstractGridView::is_reconstructed() const +{ + return is_reconstructed_; +} + +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 AbstractGridViewItem &other) const +{ + return identifier_ == other.identifier_; +} + +void AbstractGridViewItem::grid_tile_click_fn(struct bContext * /*C*/, + void *but_arg1, + void * /*arg2*/) +{ + uiButGridTile *grid_tile_but = (uiButGridTile *)but_arg1; + AbstractGridViewItem &grid_item = reinterpret_cast<AbstractGridViewItem &>( + *grid_tile_but->view_item); + + grid_item.activate(); +} + +void AbstractGridViewItem::add_grid_tile_button(uiBlock &block) +{ + const GridViewStyle &style = get_view().get_style(); + grid_tile_but_ = (uiButGridTile *)uiDefBut(&block, + UI_BTYPE_GRID_TILE, + 0, + "", + 0, + 0, + style.tile_width, + style.tile_height, + nullptr, + 0, + 0, + 0, + 0, + ""); + + grid_tile_but_->view_item = reinterpret_cast<uiGridViewItemHandle *>(this); + UI_but_func_set(&grid_tile_but_->but, grid_tile_click_fn, grid_tile_but_, nullptr); +} + +bool AbstractGridViewItem::is_active() const +{ + BLI_assert_msg(get_view().is_reconstructed(), + "State can't be queried until reconstruction is completed"); + return is_active_; +} + +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::update_from_old(const AbstractGridViewItem &old) +{ + is_active_ = old.is_active_; +} + +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 *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 &, + const AbstractGridView &grid_view, + int cols_per_row); + + bool is_item_visible(int item_idx) const; + void fill_layout_before_visible(uiBlock &) const; + void fill_layout_after_visible(uiBlock &) const; + + private: + IndexRange get_visible_range() const; + void add_spacer_button(uiBlock &, 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) +{ +} + +void PreviewGridItem::build_grid_tile(uiLayout &layout) const +{ + const GridViewStyle &style = get_view().get_style(); + uiBlock *block = uiLayoutGetBlock(&layout); + + uiBut *but = uiDefBut(block, + UI_BTYPE_PREVIEW_TILE, + 0, + label.c_str(), + 0, + 0, + style.tile_width, + style.tile_height, + nullptr, + 0, + 0, + 0, + 0, + ""); + ui_def_but_icon(but, + preview_icon_id, + /* NOLINTNEXTLINE: bugprone-suspicious-enum-usage */ + UI_HAS_ICON | UI_BUT_ICON_PREVIEW); +} + +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 + +using namespace blender::ui; + +/* ---------------------------------------------------------------------- */ +/* C-API */ + +using namespace blender::ui; + +bool UI_grid_view_item_is_active(const uiGridViewItemHandle *item_handle) +{ + const AbstractGridViewItem &item = reinterpret_cast<const AbstractGridViewItem &>(*item_handle); + return item.is_active(); +} + +bool UI_grid_view_listen_should_redraw(const uiGridViewHandle *view_handle, + const wmNotifier *notifier) +{ + const AbstractGridView &view = *reinterpret_cast<const AbstractGridView *>(view_handle); + return view.listen(*notifier); +} + +bool UI_grid_view_item_matches(const uiGridViewItemHandle *a_handle, + const uiGridViewItemHandle *b_handle) +{ + const AbstractGridViewItem &a = reinterpret_cast<const AbstractGridViewItem &>(*a_handle); + const AbstractGridViewItem &b = reinterpret_cast<const AbstractGridViewItem &>(*b_handle); + return a.matches(b); +} diff --git a/source/blender/editors/interface/interface.cc b/source/blender/editors/interface/interface.cc index e596bcd2d63..3f623566807 100644 --- a/source/blender/editors/interface/interface.cc +++ b/source/blender/editors/interface/interface.cc @@ -778,6 +778,15 @@ static bool ui_but_equals_old(const uiBut *but, const uiBut *oldbut) } } + if ((but->type == UI_BTYPE_GRID_TILE) && (oldbut->type == UI_BTYPE_GRID_TILE)) { + uiButGridTile *but_gridtile = (uiButGridTile *)but; + uiButGridTile *oldbut_gridtile = (uiButGridTile *)oldbut; + if (!but_gridtile->view_item || !oldbut_gridtile->view_item || + !UI_grid_view_item_matches(but_gridtile->view_item, oldbut_gridtile->view_item)) { + return false; + } + } + return true; } @@ -904,6 +913,12 @@ static void ui_but_update_old_active_from_new(uiBut *oldbut, uiBut *but) SWAP(uiTreeViewItemHandle *, treerow_newbut->tree_item, treerow_oldbut->tree_item); break; } + case UI_BTYPE_GRID_TILE: { + uiButGridTile *gridtile_oldbut = (uiButGridTile *)oldbut; + uiButGridTile *gridtile_newbut = (uiButGridTile *)but; + SWAP(uiGridViewItemHandle *, gridtile_newbut->view_item, gridtile_oldbut->view_item); + break; + } default: break; } @@ -996,9 +1011,9 @@ static bool ui_but_update_from_old_block(const bContext *C, else { int flag_copy = UI_BUT_DRAG_MULTI; - /* Stupid special case: The active button may be inside (as in, overlapped on top) a tree-row + /* Stupid special case: The active button may be inside (as in, overlapped on top) a view-item * button which we also want to keep highlighted then. */ - if (but->type == UI_BTYPE_TREEROW) { + if (ui_but_is_view_item(but)) { flag_copy |= UI_ACTIVE; } @@ -2239,6 +2254,15 @@ int ui_but_is_pushed_ex(uiBut *but, double *value) } break; } + case UI_BTYPE_GRID_TILE: { + uiButGridTile *grid_tile_but = (uiButGridTile *)but; + + is_push = -1; + if (grid_tile_but->view_item) { + is_push = UI_grid_view_item_is_active(grid_tile_but->view_item); + } + break; + } default: is_push = -1; break; @@ -3995,6 +4019,10 @@ static void ui_but_alloc_info(const eButType type, alloc_size = sizeof(uiButHotkeyEvent); alloc_str = "uiButHotkeyEvent"; break; + case UI_BTYPE_GRID_TILE: + alloc_size = sizeof(uiButGridTile); + alloc_str = "uiButGridTile"; + break; default: alloc_size = sizeof(uiBut); alloc_str = "uiBut"; @@ -4969,6 +4997,33 @@ int UI_autocomplete_end(AutoComplete *autocpl, char *autoname) return match; } +#define PREVIEW_TILE_PAD (0.15f * UI_UNIT_X) + +int UI_preview_tile_size_x(void) +{ + const float pad = PREVIEW_TILE_PAD; + return round_fl_to_int((96.0f / 20.0f) * UI_UNIT_X + 2.0f * pad); +} + +int UI_preview_tile_size_y(void) +{ + const uiStyle *style = UI_style_get(); + const float font_height = style->widget.points * UI_DPI_FAC; + const float pad = PREVIEW_TILE_PAD; + + return round_fl_to_int(UI_preview_tile_size_y_no_label() + font_height + + /* Add some extra padding to make things less tight vertically. */ + pad); +} + +int UI_preview_tile_size_y_no_label(void) +{ + const float pad = PREVIEW_TILE_PAD; + return round_fl_to_int((96.0f / 20.0f) * UI_UNIT_Y + 2.0f * pad); +} + +#undef PREVIEW_TILE_PAD + static void ui_but_update_and_icon_set(uiBut *but, int icon) { if (icon) { diff --git a/source/blender/editors/interface/interface_handlers.c b/source/blender/editors/interface/interface_handlers.c index 6d2f45813fe..341d5e78872 100644 --- a/source/blender/editors/interface/interface_handlers.c +++ b/source/blender/editors/interface/interface_handlers.c @@ -2289,6 +2289,9 @@ static void ui_apply_but( case UI_BTYPE_ROW: ui_apply_but_ROW(C, block, but, data); break; + case UI_BTYPE_GRID_TILE: + ui_apply_but_ROW(C, block, but, data); + break; case UI_BTYPE_TREEROW: ui_apply_but_TREEROW(C, block, but, data); break; @@ -4810,6 +4813,47 @@ static int ui_do_but_TREEROW(bContext *C, return WM_UI_HANDLER_CONTINUE; } +static int ui_do_but_GRIDTILE(bContext *C, + uiBut *but, + uiHandleButtonData *data, + const wmEvent *event) +{ + uiButGridTile *grid_tile_but = (uiButGridTile *)but; + BLI_assert(grid_tile_but->but.type == UI_BTYPE_GRID_TILE); + + if (data->state == BUTTON_STATE_HIGHLIGHT) { + if (event->type == LEFTMOUSE) { + switch (event->val) { + case KM_PRESS: + /* Extra icons have priority, don't mess with them. */ + if (ui_but_extra_operator_icon_mouse_over_get(but, data->region, event)) { + return WM_UI_HANDLER_BREAK; + } + button_activate_state(C, but, BUTTON_STATE_WAIT_DRAG); + data->dragstartx = event->xy[0]; + data->dragstarty = event->xy[1]; + return WM_UI_HANDLER_CONTINUE; + + case KM_CLICK: + button_activate_state(C, but, BUTTON_STATE_EXIT); + return WM_UI_HANDLER_BREAK; + + case KM_DBL_CLICK: + data->cancel = true; + // UI_tree_view_item_begin_rename(grid_tile_but->tree_item); + ED_region_tag_redraw(CTX_wm_region(C)); + return WM_UI_HANDLER_BREAK; + } + } + } + else if (data->state == BUTTON_STATE_WAIT_DRAG) { + /* Let "default" button handling take care of the drag logic. */ + return ui_do_but_EXIT(C, but, data, event); + } + + return WM_UI_HANDLER_CONTINUE; +} + static int ui_do_but_EXIT(bContext *C, uiBut *but, uiHandleButtonData *data, const wmEvent *event) { if (data->state == BUTTON_STATE_HIGHLIGHT) { @@ -4851,6 +4895,10 @@ static int ui_do_but_EXIT(bContext *C, uiBut *but, uiHandleButtonData *data, con ret = WM_UI_HANDLER_CONTINUE; } } + const uiBut *view_but = ui_view_item_find_mouse_over(data->region, event->xy); + if (view_but) { + ret = WM_UI_HANDLER_CONTINUE; + } button_activate_state(C, but, BUTTON_STATE_EXIT); return ret; } @@ -8005,6 +8053,9 @@ static int ui_do_button(bContext *C, uiBlock *block, uiBut *but, const wmEvent * case UI_BTYPE_ROW: retval = ui_do_but_TOG(C, but, data, event); break; + case UI_BTYPE_GRID_TILE: + retval = ui_do_but_GRIDTILE(C, but, data, event); + break; case UI_BTYPE_TREEROW: retval = ui_do_but_TREEROW(C, but, data, event); break; @@ -9672,31 +9723,31 @@ static int ui_handle_list_event(bContext *C, const wmEvent *event, ARegion *regi return retval; } -static int ui_handle_tree_hover(const wmEvent *event, const ARegion *region) +static int ui_handle_view_items_hover(const wmEvent *event, const ARegion *region) { - bool has_treerows = false; + bool has_view_item = false; LISTBASE_FOREACH (uiBlock *, block, ®ion->uiblocks) { - /* Avoid unnecessary work: Tree-rows are assumed to be inside tree-views. */ + /* Avoid unnecessary work: view item buttons are assumed to be inside views. */ if (BLI_listbase_is_empty(&block->views)) { continue; } LISTBASE_FOREACH (uiBut *, but, &block->buttons) { - if (but->type == UI_BTYPE_TREEROW) { + if (ui_but_is_view_item(but)) { but->flag &= ~UI_ACTIVE; - has_treerows = true; + has_view_item = true; } } } - if (!has_treerows) { + if (!has_view_item) { /* Avoid unnecessary lookup. */ return WM_UI_HANDLER_CONTINUE; } - /* Always highlight the hovered tree-row, even if the mouse hovers another button inside of it. + /* Always highlight the hovered view item, even if the mouse hovers another button inside of it. */ - uiBut *hovered_row_but = ui_tree_row_find_mouse_over(region, event->xy); + uiBut *hovered_row_but = ui_view_item_find_mouse_over(region, event->xy); if (hovered_row_but) { hovered_row_but->flag |= UI_ACTIVE; } @@ -9704,6 +9755,21 @@ static int ui_handle_tree_hover(const wmEvent *event, const ARegion *region) return WM_UI_HANDLER_CONTINUE; } +static int ui_handle_view_item_event(bContext *C, + const wmEvent *event, + ARegion *region, + uiBut *view_but) +{ + BLI_assert(ui_but_is_view_item(view_but)); + if (event->type == LEFTMOUSE) { + /* Will free active button if there already is one. */ + ui_handle_button_activate(C, region, view_but, BUTTON_ACTIVATE_OVER); + return ui_do_button(C, view_but->block, view_but, event); + } + + return WM_UI_HANDLER_CONTINUE; +} + static void ui_handle_button_return_submenu(bContext *C, const wmEvent *event, uiBut *but) { uiHandleButtonData *data = but->active; @@ -11304,9 +11370,15 @@ static int ui_region_handler(bContext *C, const wmEvent *event, void *UNUSED(use ui_blocks_set_tooltips(region, true); } - /* Always do this, to reliably update tree-row highlighting, even if the mouse hovers a button - * inside the row (it's an overlapping layout). */ - ui_handle_tree_hover(event, region); + /* Always do this, to reliably update view item highlighting, even if the mouse hovers a button + * nested in the item (it's an overlapping layout). */ + ui_handle_view_items_hover(event, region); + if (retval == WM_UI_HANDLER_CONTINUE) { + uiBut *view_item = ui_view_item_find_mouse_over(region, event->xy); + if (view_item) { + retval = ui_handle_view_item_event(C, event, region, view_item); + } + } /* delayed apply callbacks */ ui_apply_but_funcs_after(C); diff --git a/source/blender/editors/interface/interface_intern.h b/source/blender/editors/interface/interface_intern.h index af953cabe8a..791e51b81a6 100644 --- a/source/blender/editors/interface/interface_intern.h +++ b/source/blender/editors/interface/interface_intern.h @@ -351,6 +351,13 @@ typedef struct uiButTreeRow { int indentation; } uiButTreeRow; +/** Derived struct for #UI_BTYPE_GRID_TILE. */ +typedef struct uiButGridTile { + uiBut but; + + uiGridViewItemHandle *view_item; +} uiButGridTile; + /** Derived struct for #UI_BTYPE_HSVCUBE. */ typedef struct uiButHSVCube { uiBut but; @@ -1365,6 +1372,7 @@ void ui_but_anim_decorate_update_from_flag(uiButDecorator *but); bool ui_but_is_editable(const uiBut *but) ATTR_WARN_UNUSED_RESULT; bool ui_but_is_editable_as_text(const uiBut *but) ATTR_WARN_UNUSED_RESULT; bool ui_but_is_toggle(const uiBut *but) ATTR_WARN_UNUSED_RESULT; +bool ui_but_is_view_item(const uiBut *but) ATTR_WARN_UNUSED_RESULT; /** * Can we mouse over the button or is it hidden/disabled/layout. * \note ctrl is kind of a hack currently, @@ -1396,6 +1404,8 @@ uiBut *ui_list_row_find_mouse_over(const struct ARegion *region, const int xy[2] uiBut *ui_list_row_find_from_index(const struct ARegion *region, int index, uiBut *listbox) ATTR_WARN_UNUSED_RESULT; +uiBut *ui_view_item_find_mouse_over(const struct ARegion *region, const int xy[2]) + ATTR_NONNULL(1, 2); uiBut *ui_tree_row_find_mouse_over(const struct ARegion *region, const int xy[2]) ATTR_NONNULL(1, 2); uiBut *ui_tree_row_find_active(const struct ARegion *region); @@ -1533,8 +1543,10 @@ void ui_interface_tag_script_reload_queries(void); /* interface_view.cc */ void ui_block_free_views(struct uiBlock *block); -uiTreeViewHandle *ui_block_view_find_matching_in_old_block(const uiBlock *new_block, - const uiTreeViewHandle *new_view); +uiTreeViewHandle *ui_block_tree_view_find_matching_in_old_block(const uiBlock *new_block, + const uiTreeViewHandle *new_view); +uiGridViewHandle *ui_block_grid_view_find_matching_in_old_block( + const uiBlock *new_block, const uiGridViewHandle *new_view_handle); uiButTreeRow *ui_block_view_find_treerow_in_old_block(const uiBlock *new_block, const uiTreeViewItemHandle *new_item_handle); diff --git a/source/blender/editors/interface/interface_query.cc b/source/blender/editors/interface/interface_query.cc index 4fc8b08218e..71cf60985df 100644 --- a/source/blender/editors/interface/interface_query.cc +++ b/source/blender/editors/interface/interface_query.cc @@ -58,6 +58,11 @@ bool ui_but_is_toggle(const uiBut *but) UI_BTYPE_TREEROW); } +bool ui_but_is_view_item(const uiBut *but) +{ + return ELEM(but->type, UI_BTYPE_TREEROW, UI_BTYPE_GRID_TILE); +} + bool ui_but_is_interactive_ex(const uiBut *but, const bool labeledit, const bool for_tooltip) { /* NOTE: #UI_BTYPE_LABEL is included for highlights, this allows drags. */ @@ -462,6 +467,16 @@ static bool ui_but_is_treerow(const uiBut *but, const void *UNUSED(customdata)) return but->type == UI_BTYPE_TREEROW; } +static bool ui_but_is_view_item_fn(const uiBut *but, const void *UNUSED(customdata)) +{ + return ui_but_is_view_item(but); +} + +uiBut *ui_view_item_find_mouse_over(const ARegion *region, const int xy[2]) +{ + return ui_but_find_mouse_over_ex(region, xy, false, false, ui_but_is_view_item_fn, nullptr); +} + uiBut *ui_tree_row_find_mouse_over(const ARegion *region, const int xy[2]) { return ui_but_find_mouse_over_ex(region, xy, false, false, ui_but_is_treerow, nullptr); diff --git a/source/blender/editors/interface/interface_template_list.cc b/source/blender/editors/interface/interface_template_list.cc index 68a699c652a..e0b6bbb34c4 100644 --- a/source/blender/editors/interface/interface_template_list.cc +++ b/source/blender/editors/interface/interface_template_list.cc @@ -945,13 +945,8 @@ static void ui_template_list_layout_draw(bContext *C, const bool show_names = (flags & UI_TEMPLATE_LIST_NO_NAMES) == 0; - /* TODO ED_fileselect_init_layout(). Share somehow? */ - float size_x = (96.0f / 20.0f) * UI_UNIT_X; - float size_y = (96.0f / 20.0f) * UI_UNIT_Y; - - if (!show_names) { - size_y -= UI_UNIT_Y; - } + const int size_x = UI_preview_tile_size_x(); + const int size_y = show_names ? UI_preview_tile_size_y() : UI_preview_tile_size_y_no_label(); const int cols_per_row = MAX2((uiLayoutGetWidth(box) - V2D_SCROLL_WIDTH) / size_x, 1); uiLayout *grid = uiLayoutGridFlow(row, true, cols_per_row, true, true, true); diff --git a/source/blender/editors/interface/interface_view.cc b/source/blender/editors/interface/interface_view.cc index 85e1dbe73a5..699ac0c2b53 100644 --- a/source/blender/editors/interface/interface_view.cc +++ b/source/blender/editors/interface/interface_view.cc @@ -10,15 +10,22 @@ */ #include <memory> +#include <type_traits> #include <variant> #include "DNA_screen_types.h" +#include "BKE_screen.h" + #include "BLI_listbase.h" +#include "ED_screen.h" + #include "interface_intern.h" #include "UI_interface.hh" + +#include "UI_grid_view.hh" #include "UI_tree_view.hh" using namespace blender; @@ -30,29 +37,51 @@ using namespace blender::ui; */ struct ViewLink : public Link { using TreeViewPtr = std::unique_ptr<AbstractTreeView>; + using GridViewPtr = std::unique_ptr<AbstractGridView>; std::string idname; /* NOTE: Can't use std::get() on this until minimum macOS deployment target is 10.14. */ - std::variant<TreeViewPtr> view; + std::variant<TreeViewPtr, GridViewPtr> view; }; +template<class T> constexpr void check_if_valid_view_type() +{ + static_assert(std::is_same_v<T, AbstractTreeView> || std::is_same_v<T, AbstractGridView>, + "Unsupported view type"); +} + template<class T> T *get_view_from_link(ViewLink &link) { auto *t_uptr = std::get_if<std::unique_ptr<T>>(&link.view); return t_uptr ? t_uptr->get() : nullptr; } -AbstractTreeView *UI_block_add_view(uiBlock &block, - StringRef idname, - std::unique_ptr<AbstractTreeView> tree_view) +template<class T> +static T *ui_block_add_view_impl(uiBlock &block, StringRef idname, std::unique_ptr<T> view) { + check_if_valid_view_type<T>(); + ViewLink *view_link = MEM_new<ViewLink>(__func__); BLI_addtail(&block.views, view_link); - view_link->view = std::move(tree_view); + view_link->view = std::move(view); view_link->idname = idname; - return get_view_from_link<AbstractTreeView>(*view_link); + return get_view_from_link<T>(*view_link); +} + +AbstractGridView *UI_block_add_view(uiBlock &block, + StringRef idname, + std::unique_ptr<AbstractGridView> tree_view) +{ + return ui_block_add_view_impl<AbstractGridView>(block, idname, std::move(tree_view)); +} + +AbstractTreeView *UI_block_add_view(uiBlock &block, + StringRef idname, + std::unique_ptr<AbstractTreeView> tree_view) +{ + return ui_block_add_view_impl<AbstractTreeView>(block, idname, std::move(tree_view)); } void ui_block_free_views(uiBlock *block) @@ -62,6 +91,26 @@ void ui_block_free_views(uiBlock *block) } } +void UI_block_views_listen(const uiBlock *block, const wmRegionListenerParams *listener_params) +{ + ARegion *region = listener_params->region; + + LISTBASE_FOREACH (ViewLink *, view_link, &block->views) { + if (AbstractGridView *grid_view = get_view_from_link<AbstractGridView>(*view_link)) { + if (UI_grid_view_listen_should_redraw(reinterpret_cast<uiGridViewHandle *>(grid_view), + listener_params->notifier)) { + ED_region_tag_redraw(region); + } + } + else if (AbstractTreeView *tree_view = get_view_from_link<AbstractTreeView>(*view_link)) { + if (UI_tree_view_listen_should_redraw(reinterpret_cast<uiTreeViewHandle *>(tree_view), + listener_params->notifier)) { + ED_region_tag_redraw(region); + } + } + } +} + uiTreeViewItemHandle *UI_block_tree_view_find_item_at(const ARegion *region, const int xy[2]) { uiButTreeRow *tree_row_but = (uiButTreeRow *)ui_tree_row_find_mouse_over(region, xy); @@ -82,11 +131,13 @@ uiTreeViewItemHandle *UI_block_tree_view_find_active_item(const ARegion *region) return tree_row_but->tree_item; } -static StringRef ui_block_view_find_idname(const uiBlock &block, const AbstractTreeView &view) +template<class T> static StringRef ui_block_view_find_idname(const uiBlock &block, const T &view) { + check_if_valid_view_type<T>(); + /* First get the idname the of the view we're looking for. */ LISTBASE_FOREACH (ViewLink *, view_link, &block.views) { - if (get_view_from_link<AbstractTreeView>(*view_link) == &view) { + if (get_view_from_link<T>(*view_link) == &view) { return view_link->idname; } } @@ -94,9 +145,11 @@ static StringRef ui_block_view_find_idname(const uiBlock &block, const AbstractT return {}; } -static AbstractTreeView *ui_block_view_find_matching_in_old_block(const uiBlock &new_block, - const AbstractTreeView &new_view) +template<class T> +static T *ui_block_view_find_matching_in_old_block(const uiBlock &new_block, const T &new_view) { + check_if_valid_view_type<T>(); + uiBlock *old_block = new_block.oldblock; if (!old_block) { return nullptr; @@ -109,15 +162,15 @@ static AbstractTreeView *ui_block_view_find_matching_in_old_block(const uiBlock LISTBASE_FOREACH (ViewLink *, old_view_link, &old_block->views) { if (old_view_link->idname == idname) { - return get_view_from_link<AbstractTreeView>(*old_view_link); + return get_view_from_link<T>(*old_view_link); } } return nullptr; } -uiTreeViewHandle *ui_block_view_find_matching_in_old_block(const uiBlock *new_block, - const uiTreeViewHandle *new_view_handle) +uiTreeViewHandle *ui_block_tree_view_find_matching_in_old_block( + const uiBlock *new_block, const uiTreeViewHandle *new_view_handle) { BLI_assert(new_block && new_view_handle); const AbstractTreeView &new_view = reinterpret_cast<const AbstractTreeView &>(*new_view_handle); @@ -126,6 +179,16 @@ uiTreeViewHandle *ui_block_view_find_matching_in_old_block(const uiBlock *new_bl return reinterpret_cast<uiTreeViewHandle *>(old_view); } +uiGridViewHandle *ui_block_grid_view_find_matching_in_old_block( + const uiBlock *new_block, const uiGridViewHandle *new_view_handle) +{ + BLI_assert(new_block && new_view_handle); + const AbstractGridView &new_view = reinterpret_cast<const AbstractGridView &>(*new_view_handle); + + AbstractGridView *old_view = ui_block_view_find_matching_in_old_block(*new_block, new_view); + return reinterpret_cast<uiGridViewHandle *>(old_view); +} + uiButTreeRow *ui_block_view_find_treerow_in_old_block(const uiBlock *new_block, const uiTreeViewItemHandle *new_item_handle) { diff --git a/source/blender/editors/interface/interface_widgets.c b/source/blender/editors/interface/interface_widgets.c index 10096f054b3..e2df2d77817 100644 --- a/source/blender/editors/interface/interface_widgets.c +++ b/source/blender/editors/interface/interface_widgets.c @@ -105,6 +105,7 @@ typedef enum { UI_WTYPE_PROGRESSBAR, UI_WTYPE_NODESOCKET, UI_WTYPE_TREEROW, + UI_WTYPE_GRID_TILE, } uiWidgetTypeEnum; /** @@ -3706,6 +3707,16 @@ static void widget_treerow(uiBut *but, widget_treerow_exec(wcol, rect, state, roundboxalign, tree_row->indentation, zoom); } +static void widget_gridtile(uiWidgetColors *wcol, + rcti *rect, + const uiWidgetStateInfo *state, + int roundboxalign, + const float zoom) +{ + /* TODO Reuse tree-row drawing. */ + widget_treerow_exec(wcol, rect, state, roundboxalign, 0, zoom); +} + static void widget_nodesocket(uiBut *but, uiWidgetColors *wcol, rcti *rect, @@ -4598,9 +4609,15 @@ static uiWidgetType *widget_type(uiWidgetTypeEnum type) break; case UI_WTYPE_TREEROW: + wt.wcol_theme = &btheme->tui.wcol_view_item; wt.custom = widget_treerow; break; + case UI_WTYPE_GRID_TILE: + wt.wcol_theme = &btheme->tui.wcol_view_item; + wt.draw = widget_gridtile; + break; + case UI_WTYPE_NODESOCKET: wt.custom = widget_nodesocket; break; @@ -4937,6 +4954,11 @@ void ui_draw_but(const bContext *C, struct ARegion *region, uiStyle *style, uiBu fstyle = &style->widgetlabel; break; + case UI_BTYPE_GRID_TILE: + wt = widget_type(UI_WTYPE_GRID_TILE); + fstyle = &style->widgetlabel; + break; + case UI_BTYPE_SCROLL: wt = widget_type(UI_WTYPE_SCROLL); break; diff --git a/source/blender/editors/interface/tree_view.cc b/source/blender/editors/interface/tree_view.cc index bf756fb5838..f86d1c4d8bc 100644 --- a/source/blender/editors/interface/tree_view.cc +++ b/source/blender/editors/interface/tree_view.cc @@ -68,6 +68,12 @@ void AbstractTreeView::foreach_item(ItemIterFn iter_fn, IterOptions options) con foreach_item_recursive(iter_fn, options); } +bool AbstractTreeView::listen(const wmNotifier &) const +{ + /* Nothing by default. */ + return false; +} + bool AbstractTreeView::is_renaming() const { return rename_buffer_ != nullptr; @@ -82,7 +88,7 @@ void AbstractTreeView::update_from_old(uiBlock &new_block) return; } - uiTreeViewHandle *old_view_handle = ui_block_view_find_matching_in_old_block( + uiTreeViewHandle *old_view_handle = ui_block_tree_view_find_matching_in_old_block( &new_block, reinterpret_cast<uiTreeViewHandle *>(this)); if (old_view_handle == nullptr) { is_reconstructed_ = true; @@ -805,6 +811,13 @@ class TreeViewItemAPIWrapper { using namespace blender::ui; +bool UI_tree_view_listen_should_redraw(const uiTreeViewHandle *view_handle, + const wmNotifier *notifier) +{ + const AbstractTreeView &view = *reinterpret_cast<const AbstractTreeView *>(view_handle); + return view.listen(*notifier); +} + bool UI_tree_view_item_is_active(const uiTreeViewItemHandle *item_handle) { const AbstractTreeViewItem &item = reinterpret_cast<const AbstractTreeViewItem &>(*item_handle); diff --git a/source/blender/editors/screen/area.c b/source/blender/editors/screen/area.c index ad815f0d998..369ca553a8c 100644 --- a/source/blender/editors/screen/area.c +++ b/source/blender/editors/screen/area.c @@ -145,6 +145,10 @@ void ED_region_do_listen(wmRegionListenerParams *params) region->type->listener(params); } + LISTBASE_FOREACH (uiBlock *, block, ®ion->uiblocks) { + UI_block_views_listen(block, params); + } + LISTBASE_FOREACH (uiList *, list, ®ion->ui_lists) { if (list->type && list->type->listener) { list->type->listener(list, params); diff --git a/source/blender/editors/space_file/filesel.c b/source/blender/editors/space_file/filesel.c index ce36e3e4e4f..e42e1e98660 100644 --- a/source/blender/editors/space_file/filesel.c +++ b/source/blender/editors/space_file/filesel.c @@ -983,6 +983,7 @@ void ED_fileselect_init_layout(struct SpaceFile *sfile, ARegion *region) if (params->display == FILE_IMGDISPLAY) { const float pad_fac = compact ? 0.15f : 0.3f; + /* Matches UI_preview_tile_size_x()/_y() by default. */ layout->prv_w = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_X; layout->prv_h = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_Y; layout->tile_border_x = pad_fac * UI_UNIT_X; @@ -1009,6 +1010,7 @@ void ED_fileselect_init_layout(struct SpaceFile *sfile, ARegion *region) else if (params->display == FILE_VERTICALDISPLAY) { int rowcount; + /* Matches UI_preview_tile_size_x()/_y() by default. */ layout->prv_w = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_X; layout->prv_h = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_Y; layout->tile_border_x = 0.4f * UI_UNIT_X; @@ -1030,6 +1032,7 @@ void ED_fileselect_init_layout(struct SpaceFile *sfile, ARegion *region) layout->flag = FILE_LAYOUT_VER; } else if (params->display == FILE_HORIZONTALDISPLAY) { + /* Matches UI_preview_tile_size_x()/_y() by default. */ layout->prv_w = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_X; layout->prv_h = ((float)params->thumbnail_size / 20.0f) * UI_UNIT_Y; layout->tile_border_x = 0.4f * UI_UNIT_X; diff --git a/source/blender/editors/util/CMakeLists.txt b/source/blender/editors/util/CMakeLists.txt index 89d80d582f8..5c2a3374aa1 100644 --- a/source/blender/editors/util/CMakeLists.txt +++ b/source/blender/editors/util/CMakeLists.txt @@ -91,6 +91,7 @@ set(SRC ../include/ED_uvedit.h ../include/ED_view3d.h ../include/ED_view3d_offscreen.h + ../include/UI_grid_view.hh ../include/UI_icons.h ../include/UI_interface.h ../include/UI_interface.hh diff --git a/source/blender/makesdna/DNA_userdef_types.h b/source/blender/makesdna/DNA_userdef_types.h index e449605ed81..d00826208be 100644 --- a/source/blender/makesdna/DNA_userdef_types.h +++ b/source/blender/makesdna/DNA_userdef_types.h @@ -146,6 +146,7 @@ typedef struct ThemeUI { uiWidgetColors wcol_num, wcol_numslider, wcol_tab; uiWidgetColors wcol_menu, wcol_pulldown, wcol_menu_back, wcol_menu_item, wcol_tooltip; uiWidgetColors wcol_box, wcol_scroll, wcol_progress, wcol_list_item, wcol_pie_menu; + uiWidgetColors wcol_view_item; uiWidgetStateColors wcol_state; diff --git a/source/blender/makesrna/intern/rna_userdef.c b/source/blender/makesrna/intern/rna_userdef.c index 43e8879fc17..40dc1254a7d 100644 --- a/source/blender/makesrna/intern/rna_userdef.c +++ b/source/blender/makesrna/intern/rna_userdef.c @@ -1531,6 +1531,11 @@ static void rna_def_userdef_theme_ui(BlenderRNA *brna) RNA_def_property_ui_text(prop, "List Item Colors", ""); RNA_def_property_update(prop, 0, "rna_userdef_theme_update"); + prop = RNA_def_property(srna, "wcol_view_item", PROP_POINTER, PROP_NONE); + RNA_def_property_flag(prop, PROP_NEVER_NULL); + RNA_def_property_ui_text(prop, "Data-View Item Colors", ""); + RNA_def_property_update(prop, 0, "rna_userdef_theme_update"); + prop = RNA_def_property(srna, "wcol_state", PROP_POINTER, PROP_NONE); RNA_def_property_flag(prop, PROP_NEVER_NULL); RNA_def_property_ui_text(prop, "State Colors", ""); |