diff options
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", ""); |