mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-11-23 08:00:20 +00:00
3dccafb2a0
As pointed out by Andreas, drawing them from the bottom feels odd since no other list in the UI is bottom-justified. The commit draws the layers from the top of the list again.
360 lines
11 KiB
C++
360 lines
11 KiB
C++
/*
|
|
* Copyright (c) 2020, Andreas Kling <kling@serenityos.org>
|
|
* Copyright (c) 2021, Mustafa Quraish <mustafa@cs.toronto.edu>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "LayerListWidget.h"
|
|
#include "Image.h"
|
|
#include "ImageEditor.h"
|
|
#include "Layer.h"
|
|
#include <LibGUI/Painter.h>
|
|
#include <LibGfx/Palette.h>
|
|
|
|
REGISTER_WIDGET(PixelPaint, LayerListWidget);
|
|
|
|
namespace PixelPaint {
|
|
|
|
LayerListWidget::LayerListWidget()
|
|
{
|
|
set_should_hide_unnecessary_scrollbars(false);
|
|
horizontal_scrollbar().set_visible(false);
|
|
}
|
|
|
|
LayerListWidget::~LayerListWidget()
|
|
{
|
|
if (m_image)
|
|
m_image->remove_client(*this);
|
|
}
|
|
|
|
size_t LayerListWidget::to_gadget_index(size_t layer_index) const
|
|
{
|
|
return m_image->layer_count() - layer_index - 1;
|
|
}
|
|
|
|
size_t LayerListWidget::to_layer_index(size_t gadget_index) const
|
|
{
|
|
return m_image->layer_count() - gadget_index - 1;
|
|
}
|
|
|
|
void LayerListWidget::set_image(Image* image)
|
|
{
|
|
if (m_image == image)
|
|
return;
|
|
if (m_image)
|
|
m_image->remove_client(*this);
|
|
m_image = image;
|
|
if (m_image)
|
|
m_image->add_client(*this);
|
|
|
|
rebuild_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::rebuild_gadgets()
|
|
{
|
|
m_gadgets.clear();
|
|
if (m_image) {
|
|
for (int layer_index = m_image->layer_count() - 1; layer_index >= 0; --layer_index) {
|
|
m_gadgets.append({ static_cast<size_t>(layer_index), {}, false, {} });
|
|
}
|
|
}
|
|
relayout_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::resize_event(GUI::ResizeEvent& event)
|
|
{
|
|
AbstractScrollableWidget::resize_event(event);
|
|
relayout_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::get_gadget_rects(Gadget const& gadget, Gfx::IntRect& outer_rect, Gfx::IntRect& thumbnail_rect, Gfx::IntRect& text_rect)
|
|
{
|
|
outer_rect = gadget.rect;
|
|
outer_rect.translate_by(0, -vertical_scrollbar().value());
|
|
outer_rect.translate_by(frame_thickness(), frame_thickness());
|
|
if (gadget.is_moving) {
|
|
outer_rect.translate_by(0, gadget.movement_delta.y());
|
|
}
|
|
|
|
thumbnail_rect = { outer_rect.x(), outer_rect.y(), outer_rect.height(), outer_rect.height() };
|
|
thumbnail_rect.shrink(8, 8);
|
|
|
|
text_rect = { thumbnail_rect.right() + 10, outer_rect.y(), outer_rect.width(), outer_rect.height() };
|
|
text_rect.intersect(outer_rect);
|
|
}
|
|
|
|
void LayerListWidget::paint_event(GUI::PaintEvent& event)
|
|
{
|
|
GUI::Painter painter(*this);
|
|
painter.add_clip_rect(event.rect());
|
|
|
|
painter.fill_rect(event.rect(), palette().button());
|
|
|
|
if (!m_image)
|
|
return;
|
|
|
|
painter.fill_rect(event.rect(), palette().button());
|
|
|
|
auto paint_gadget = [&](auto& gadget) {
|
|
auto& layer = m_image->layer(gadget.layer_index);
|
|
|
|
Gfx::IntRect adjusted_rect;
|
|
Gfx::IntRect thumbnail_rect;
|
|
Gfx::IntRect text_rect;
|
|
get_gadget_rects(gadget, adjusted_rect, thumbnail_rect, text_rect);
|
|
|
|
if (gadget.is_moving) {
|
|
painter.fill_rect(adjusted_rect, palette().selection().lightened(1.5f));
|
|
} else if (layer.is_selected()) {
|
|
painter.fill_rect(adjusted_rect, palette().selection());
|
|
}
|
|
|
|
painter.draw_rect(adjusted_rect, palette().color(ColorRole::BaseText));
|
|
painter.draw_scaled_bitmap(thumbnail_rect, layer.bitmap(), layer.bitmap().rect());
|
|
|
|
if (layer.is_visible()) {
|
|
painter.draw_text(text_rect, layer.name(), Gfx::TextAlignment::CenterLeft, layer.is_selected() ? palette().selection_text() : palette().button_text());
|
|
painter.draw_rect(thumbnail_rect, palette().color(ColorRole::BaseText));
|
|
} else {
|
|
painter.draw_text(text_rect, layer.name(), Gfx::TextAlignment::CenterLeft, palette().color(ColorRole::DisabledText));
|
|
painter.draw_rect(thumbnail_rect, palette().color(ColorRole::DisabledText));
|
|
}
|
|
};
|
|
|
|
for (auto& gadget : m_gadgets) {
|
|
if (!gadget.is_moving)
|
|
paint_gadget(gadget);
|
|
}
|
|
|
|
if (m_moving_gadget_index.has_value())
|
|
paint_gadget(m_gadgets[m_moving_gadget_index.value()]);
|
|
|
|
Gfx::StylePainter::paint_frame(painter, rect(), palette(), Gfx::FrameShape::Box, Gfx::FrameShadow::Sunken, 2);
|
|
}
|
|
|
|
Optional<size_t> LayerListWidget::gadget_at(Gfx::IntPoint const& position)
|
|
{
|
|
for (size_t i = 0; i < m_gadgets.size(); ++i) {
|
|
if (m_gadgets[i].rect.contains(position))
|
|
return i;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void LayerListWidget::mousedown_event(GUI::MouseEvent& event)
|
|
{
|
|
if (!m_image)
|
|
return;
|
|
if (event.button() != GUI::MouseButton::Left)
|
|
return;
|
|
|
|
Gfx::IntPoint translated_event_point = { 0, vertical_scrollbar().value() + event.y() };
|
|
|
|
auto maybe_gadget_index = gadget_at(translated_event_point);
|
|
if (!maybe_gadget_index.has_value())
|
|
return;
|
|
auto gadget_index = maybe_gadget_index.value();
|
|
|
|
m_moving_gadget_index = gadget_index;
|
|
m_selected_gadget_index = gadget_index;
|
|
m_moving_event_origin = translated_event_point;
|
|
auto& gadget = m_gadgets[m_moving_gadget_index.value()];
|
|
auto& layer = m_image->layer(to_layer_index(gadget_index));
|
|
set_selected_layer(&layer);
|
|
gadget.is_moving = true;
|
|
gadget.movement_delta = {};
|
|
update();
|
|
}
|
|
|
|
void LayerListWidget::mousemove_event(GUI::MouseEvent& event)
|
|
{
|
|
if (!m_image)
|
|
return;
|
|
if (!m_moving_gadget_index.has_value())
|
|
return;
|
|
|
|
Gfx::IntPoint translated_event_point = { 0, vertical_scrollbar().value() + event.y() };
|
|
|
|
auto delta = translated_event_point - m_moving_event_origin;
|
|
auto& gadget = m_gadgets[m_moving_gadget_index.value()];
|
|
VERIFY(gadget.is_moving);
|
|
gadget.movement_delta = delta;
|
|
|
|
auto adjusted_rect = gadget.rect;
|
|
adjusted_rect.translate_by(gadget.movement_delta);
|
|
scroll_into_view(adjusted_rect, false, true);
|
|
|
|
relayout_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::mouseup_event(GUI::MouseEvent& event)
|
|
{
|
|
if (!m_image)
|
|
return;
|
|
if (event.button() != GUI::MouseButton::Left)
|
|
return;
|
|
if (!m_moving_gadget_index.has_value())
|
|
return;
|
|
|
|
size_t old_index = m_moving_gadget_index.value();
|
|
size_t new_index = hole_index_during_move();
|
|
if (new_index >= m_image->layer_count())
|
|
new_index = m_image->layer_count() - 1;
|
|
|
|
m_moving_gadget_index = {};
|
|
|
|
auto old_layer_index = to_layer_index(old_index);
|
|
auto new_layer_index = to_layer_index(new_index);
|
|
m_image->change_layer_index(old_layer_index, new_layer_index);
|
|
}
|
|
|
|
void LayerListWidget::context_menu_event(GUI::ContextMenuEvent& event)
|
|
{
|
|
Gfx::IntPoint translated_event_point = { 0, vertical_scrollbar().value() + event.position().y() };
|
|
|
|
auto gadget_index = gadget_at(translated_event_point);
|
|
if (gadget_index.has_value()) {
|
|
m_selected_gadget_index = gadget_index.value();
|
|
auto& layer = m_image->layer(to_layer_index(m_selected_gadget_index));
|
|
set_selected_layer(&layer);
|
|
}
|
|
|
|
if (on_context_menu_request)
|
|
on_context_menu_request(event);
|
|
}
|
|
|
|
void LayerListWidget::image_did_add_layer(size_t layer_index)
|
|
{
|
|
if (m_moving_gadget_index.has_value()) {
|
|
m_gadgets[m_moving_gadget_index.value()].is_moving = false;
|
|
m_moving_gadget_index = {};
|
|
}
|
|
auto gadget_index = to_gadget_index(layer_index);
|
|
Gadget gadget { gadget_index, {}, false, {} };
|
|
m_gadgets.insert(gadget_index, gadget);
|
|
relayout_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::image_did_remove_layer(size_t layer_index)
|
|
{
|
|
if (m_moving_gadget_index.has_value()) {
|
|
m_gadgets[m_moving_gadget_index.value()].is_moving = false;
|
|
m_moving_gadget_index = {};
|
|
}
|
|
// No -1 here since a layer has already been removed.
|
|
auto gadget_index = m_image->layer_count() - layer_index;
|
|
m_gadgets.remove(gadget_index);
|
|
m_selected_gadget_index = to_gadget_index(0);
|
|
relayout_gadgets();
|
|
}
|
|
|
|
void LayerListWidget::image_did_modify_layer_properties(size_t layer_index)
|
|
{
|
|
update(m_gadgets[to_gadget_index(layer_index)].rect);
|
|
}
|
|
|
|
void LayerListWidget::image_did_modify_layer_bitmap(size_t layer_index)
|
|
{
|
|
Gfx::IntRect adjusted_rect;
|
|
Gfx::IntRect thumbnail_rect;
|
|
Gfx::IntRect text_rect;
|
|
get_gadget_rects(m_gadgets[to_gadget_index(layer_index)], adjusted_rect, thumbnail_rect, text_rect);
|
|
update(thumbnail_rect);
|
|
}
|
|
|
|
void LayerListWidget::image_did_modify_layer_stack()
|
|
{
|
|
rebuild_gadgets();
|
|
}
|
|
|
|
static constexpr int gadget_height = 40;
|
|
static constexpr int gadget_spacing = -1;
|
|
static constexpr int vertical_step = gadget_height + gadget_spacing;
|
|
|
|
size_t LayerListWidget::hole_index_during_move() const
|
|
{
|
|
VERIFY(is_moving_gadget());
|
|
auto& moving_gadget = m_gadgets[m_moving_gadget_index.value()];
|
|
int center_y_of_moving_gadget = moving_gadget.rect.translated(0, moving_gadget.movement_delta.y()).center().y();
|
|
return center_y_of_moving_gadget / vertical_step;
|
|
}
|
|
|
|
void LayerListWidget::select_bottom_layer()
|
|
{
|
|
if (!m_image || !m_image->layer_count())
|
|
return;
|
|
m_selected_gadget_index = to_gadget_index(0);
|
|
set_selected_layer(&m_image->layer(0));
|
|
}
|
|
|
|
void LayerListWidget::select_top_layer()
|
|
{
|
|
if (!m_image || !m_image->layer_count())
|
|
return;
|
|
m_selected_gadget_index = 0;
|
|
set_selected_layer(&m_image->layer(to_layer_index(0)));
|
|
}
|
|
|
|
void LayerListWidget::cycle_through_selection(int delta)
|
|
{
|
|
if (!m_image || !m_image->layer_count())
|
|
return;
|
|
|
|
auto current_index = static_cast<int>(m_selected_gadget_index);
|
|
current_index += delta;
|
|
|
|
if (current_index < 0)
|
|
current_index = m_gadgets.size() - 1;
|
|
if (current_index > static_cast<int>(m_gadgets.size()) - 1)
|
|
current_index = 0;
|
|
|
|
m_selected_gadget_index = current_index;
|
|
auto selected_layer_index = to_layer_index(m_selected_gadget_index);
|
|
set_selected_layer(&m_image->layer(selected_layer_index));
|
|
}
|
|
|
|
void LayerListWidget::relayout_gadgets()
|
|
{
|
|
int y = 0;
|
|
Optional<size_t> hole_index;
|
|
if (is_moving_gadget())
|
|
hole_index = hole_index_during_move();
|
|
|
|
size_t index = 0;
|
|
for (auto& gadget : m_gadgets) {
|
|
if (gadget.is_moving)
|
|
continue;
|
|
if (index == hole_index)
|
|
y += vertical_step;
|
|
gadget.rect = { 0, y, widget_inner_rect().width(), gadget_height };
|
|
y += vertical_step;
|
|
++index;
|
|
}
|
|
|
|
auto total_gadget_height = static_cast<int>(m_gadgets.size()) * vertical_step + 6;
|
|
set_content_size({ widget_inner_rect().width(), total_gadget_height });
|
|
vertical_scrollbar().set_range(0, max(total_gadget_height - height(), 0));
|
|
update();
|
|
}
|
|
|
|
void LayerListWidget::set_selected_layer(Layer* layer)
|
|
{
|
|
if (!m_image)
|
|
return;
|
|
for (size_t i = 0; i < m_image->layer_count(); ++i) {
|
|
if (layer == &m_image->layer(i)) {
|
|
m_image->layer(i).set_selected(true);
|
|
m_selected_gadget_index = to_gadget_index(i);
|
|
scroll_into_view(m_gadgets[m_selected_gadget_index].rect, false, true);
|
|
} else {
|
|
m_image->layer(i).set_selected(false);
|
|
}
|
|
}
|
|
if (on_layer_select)
|
|
on_layer_select(layer);
|
|
|
|
update();
|
|
}
|
|
|
|
}
|