diff --git a/Applications/PixelPaint/CMakeLists.txt b/Applications/PixelPaint/CMakeLists.txt index 0efc42cfae..1df90c7782 100644 --- a/Applications/PixelPaint/CMakeLists.txt +++ b/Applications/PixelPaint/CMakeLists.txt @@ -6,6 +6,7 @@ set(SOURCES Image.cpp ImageEditor.cpp Layer.cpp + LayerListWidget.cpp LayerModel.cpp LineTool.cpp main.cpp diff --git a/Applications/PixelPaint/Image.cpp b/Applications/PixelPaint/Image.cpp index 1638369f09..e1a906df11 100644 --- a/Applications/PixelPaint/Image.cpp +++ b/Applications/PixelPaint/Image.cpp @@ -67,6 +67,9 @@ void Image::add_layer(NonnullRefPtr layer) ASSERT(&existing_layer != layer.ptr()); } m_layers.append(move(layer)); + + for (auto* client : m_clients) + client->image_did_add_layer(m_layers.size() - 1); } GUI::Model& Image::layer_model() @@ -126,6 +129,21 @@ void Image::remove_layer(Layer& layer) NonnullRefPtr protector(layer); auto index = index_of(layer); m_layers.remove(index); + + for (auto* client : m_clients) + client->image_did_remove_layer(index); +} + +void Image::add_client(ImageClient& client) +{ + ASSERT(!m_clients.contains(&client)); + m_clients.set(&client); +} + +void Image::remove_client(ImageClient& client) +{ + ASSERT(m_clients.contains(&client)); + m_clients.remove(&client); } } diff --git a/Applications/PixelPaint/Image.h b/Applications/PixelPaint/Image.h index c69912978c..d8c0b86a58 100644 --- a/Applications/PixelPaint/Image.h +++ b/Applications/PixelPaint/Image.h @@ -26,6 +26,7 @@ #pragma once +#include #include #include #include @@ -39,6 +40,13 @@ namespace PixelPaint { class Layer; +class ImageClient { +public: + virtual void image_did_add_layer(size_t) { } + virtual void image_did_remove_layer(size_t) { } + virtual void image_did_update_layer(size_t) { } +}; + class Image : public RefCounted { public: static RefPtr create_with_size(const Gfx::Size&); @@ -61,6 +69,9 @@ public: void move_layer_down(Layer&); void remove_layer(Layer&); + void add_client(ImageClient&); + void remove_client(ImageClient&); + private: explicit Image(const Gfx::Size&); @@ -69,6 +80,8 @@ private: Gfx::Size m_size; NonnullRefPtrVector m_layers; RefPtr m_layer_model; + + HashTable m_clients; }; } diff --git a/Applications/PixelPaint/LayerListWidget.cpp b/Applications/PixelPaint/LayerListWidget.cpp new file mode 100644 index 0000000000..a819c911f4 --- /dev/null +++ b/Applications/PixelPaint/LayerListWidget.cpp @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2020, Andreas Kling + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "LayerListWidget.h" +#include "Image.h" +#include "Layer.h" +#include +#include +#include + +namespace PixelPaint { + +LayerListWidget::LayerListWidget() +{ +} + +LayerListWidget::~LayerListWidget() +{ + if (m_image) + m_image->remove_client(*this); +} + +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); + + m_gadgets.clear(); + + if (m_image) { + for (size_t layer_index = 0; layer_index < m_image->layer_count(); ++layer_index) { + m_gadgets.append({ layer_index, {} }); + } + } + + relayout_gadgets(); +} + +void LayerListWidget::resize_event(GUI::ResizeEvent& event) +{ + Widget::resize_event(event); + relayout_gadgets(); +} + +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()); + + for (auto& gadget : m_gadgets) { + painter.draw_rect(gadget.rect, Color::Black); + auto& layer = m_image->layer(gadget.layer_index); + + Gfx::Rect thumbnail_rect { gadget.rect.x(), gadget.rect.y(), gadget.rect.height(), gadget.rect.height() }; + thumbnail_rect.shrink(8, 8); + painter.draw_scaled_bitmap(thumbnail_rect, layer.bitmap(), layer.bitmap().rect()); + + Gfx::Rect text_rect { thumbnail_rect.right() + 10, gadget.rect.y(), gadget.rect.width(), gadget.rect.height() }; + text_rect.intersect(gadget.rect); + + painter.draw_text(text_rect, layer.name(), Gfx::TextAlignment::CenterLeft); + } +} + +void LayerListWidget::mousedown_event(GUI::MouseEvent& event) +{ + (void)event; +} + +void LayerListWidget::mousemove_event(GUI::MouseEvent& event) +{ + (void)event; +} + +void LayerListWidget::mouseup_event(GUI::MouseEvent& event) +{ + (void)event; +} + +void LayerListWidget::image_did_add_layer(size_t layer_index) +{ + Gadget gadget { layer_index, {} }; + m_gadgets.insert(layer_index, move(gadget)); + relayout_gadgets(); +} + +void LayerListWidget::image_did_remove_layer(size_t layer_index) +{ + m_gadgets.remove(layer_index); + relayout_gadgets(); +} + +void LayerListWidget::image_did_update_layer(size_t layer_index) +{ + update(m_gadgets[layer_index].rect); +} + +void LayerListWidget::relayout_gadgets() +{ + constexpr int gadget_height = 30; + constexpr int gadget_spacing = 1; + int y = 0; + + for (auto& gadget : m_gadgets) { + gadget.rect = { 0, y, width(), gadget_height }; + y += gadget_height + gadget_spacing; + } + + update(); +} + +} diff --git a/Applications/PixelPaint/LayerListWidget.h b/Applications/PixelPaint/LayerListWidget.h new file mode 100644 index 0000000000..1f24883526 --- /dev/null +++ b/Applications/PixelPaint/LayerListWidget.h @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2020, Andreas Kling + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#pragma once + +#include "Image.h" +#include + +namespace PixelPaint { + +class LayerListWidget final + : public GUI::Widget + , ImageClient { + C_OBJECT(LayerListWidget); + +public: + virtual ~LayerListWidget() override; + + void set_image(Image*); + +private: + explicit LayerListWidget(); + + virtual void paint_event(GUI::PaintEvent&) override; + virtual void mousedown_event(GUI::MouseEvent&) override; + virtual void mousemove_event(GUI::MouseEvent&) override; + virtual void mouseup_event(GUI::MouseEvent&) override; + virtual void resize_event(GUI::ResizeEvent&) override; + + virtual void image_did_add_layer(size_t) override; + virtual void image_did_remove_layer(size_t) override; + virtual void image_did_update_layer(size_t); + + void relayout_gadgets(); + + struct Gadget { + size_t layer_index { 0 }; + Gfx::Rect rect; + }; + + Vector m_gadgets; + RefPtr m_image; +}; + +} diff --git a/Applications/PixelPaint/main.cpp b/Applications/PixelPaint/main.cpp index 75d9e4e82a..6a5dc1249a 100644 --- a/Applications/PixelPaint/main.cpp +++ b/Applications/PixelPaint/main.cpp @@ -28,6 +28,7 @@ #include "Image.h" #include "ImageEditor.h" #include "Layer.h" +#include "LayerListWidget.h" #include "PaletteWidget.h" #include "Tool.h" #include "ToolboxWidget.h" @@ -84,12 +85,15 @@ int main(int argc, char** argv) vertical_container.add(image_editor); auto& right_panel = horizontal_container.add(); + right_panel.set_fill_with_background_color(true); right_panel.set_size_policy(GUI::SizePolicy::Fixed, GUI::SizePolicy::Fill); right_panel.set_preferred_size(230, 0); right_panel.set_layout(); auto& layer_table_view = right_panel.add(); + auto& layer_list_widget = right_panel.add(); + window->show(); auto menubar = GUI::MenuBar::construct(); @@ -123,58 +127,74 @@ int main(int argc, char** argv) }); auto& layer_menu = menubar->add_menu("Layer"); - layer_menu.add_action(GUI::Action::create("Create new layer...", { Mod_Ctrl | Mod_Shift, Key_N }, [&](auto&) { - auto dialog = PixelPaint::CreateNewLayerDialog::construct(image_editor.image()->size(), window); - if (dialog->exec() == GUI::Dialog::ExecOK) { - auto layer = PixelPaint::Layer::create_with_size(dialog->layer_size(), dialog->layer_name()); - if (!layer) { - GUI::MessageBox::show_error(String::format("Unable to create layer with size %s", dialog->size().to_string().characters())); - return; + layer_menu.add_action(GUI::Action::create( + "Create new layer...", { Mod_Ctrl | Mod_Shift, Key_N }, [&](auto&) { + auto dialog = PixelPaint::CreateNewLayerDialog::construct(image_editor.image()->size(), window); + if (dialog->exec() == GUI::Dialog::ExecOK) { + auto layer = PixelPaint::Layer::create_with_size(dialog->layer_size(), dialog->layer_name()); + if (!layer) { + GUI::MessageBox::show_error(String::format("Unable to create layer with size %s", dialog->size().to_string().characters())); + return; + } + image_editor.image()->add_layer(layer.release_nonnull()); + image_editor.layers_did_change(); } - image_editor.image()->add_layer(layer.release_nonnull()); - image_editor.layers_did_change(); - } - }, window)); + }, + window)); layer_menu.add_separator(); - layer_menu.add_action(GUI::Action::create("Select previous layer", { 0, Key_PageUp }, [&](auto&) { - layer_table_view.move_selection(1); - }, window)); - layer_menu.add_action(GUI::Action::create("Select next layer", { 0, Key_PageDown }, [&](auto&) { - layer_table_view.move_selection(-1); - }, window)); - layer_menu.add_action(GUI::Action::create("Select top layer", { 0, Key_Home }, [&](auto&) { - layer_table_view.selection().set(layer_table_view.model()->index(image_editor.image()->layer_count() - 1)); - }, window)); - layer_menu.add_action(GUI::Action::create("Select bottom layer", { 0, Key_End }, [&](auto&) { - layer_table_view.selection().set(layer_table_view.model()->index(0)); - }, window)); + layer_menu.add_action(GUI::Action::create( + "Select previous layer", { 0, Key_PageUp }, [&](auto&) { + layer_table_view.move_selection(1); + }, + window)); + layer_menu.add_action(GUI::Action::create( + "Select next layer", { 0, Key_PageDown }, [&](auto&) { + layer_table_view.move_selection(-1); + }, + window)); + layer_menu.add_action(GUI::Action::create( + "Select top layer", { 0, Key_Home }, [&](auto&) { + layer_table_view.selection().set(layer_table_view.model()->index(image_editor.image()->layer_count() - 1)); + }, + window)); + layer_menu.add_action(GUI::Action::create( + "Select bottom layer", { 0, Key_End }, [&](auto&) { + layer_table_view.selection().set(layer_table_view.model()->index(0)); + }, + window)); layer_menu.add_separator(); - layer_menu.add_action(GUI::Action::create("Move active layer up", { Mod_Ctrl, Key_PageUp }, [&](auto&) { - auto active_layer = image_editor.active_layer(); - if(!active_layer) - return; - image_editor.image()->move_layer_up(*active_layer); - layer_table_view.move_selection(1); - image_editor.layers_did_change(); - }, window)); - layer_menu.add_action(GUI::Action::create("Move active layer down", { Mod_Ctrl, Key_PageDown }, [&](auto&) { - auto active_layer = image_editor.active_layer(); - if(!active_layer) - return; - image_editor.image()->move_layer_down(*active_layer); - layer_table_view.move_selection(-1); - image_editor.layers_did_change(); - }, window)); + layer_menu.add_action(GUI::Action::create( + "Move active layer up", { Mod_Ctrl, Key_PageUp }, [&](auto&) { + auto active_layer = image_editor.active_layer(); + if (!active_layer) + return; + image_editor.image()->move_layer_up(*active_layer); + layer_table_view.move_selection(1); + image_editor.layers_did_change(); + }, + window)); + layer_menu.add_action(GUI::Action::create( + "Move active layer down", { Mod_Ctrl, Key_PageDown }, [&](auto&) { + auto active_layer = image_editor.active_layer(); + if (!active_layer) + return; + image_editor.image()->move_layer_down(*active_layer); + layer_table_view.move_selection(-1); + image_editor.layers_did_change(); + }, + window)); layer_menu.add_separator(); - layer_menu.add_action(GUI::Action::create("Remove active layer", { Mod_Ctrl , Key_D }, [&](auto&) { - auto active_layer = image_editor.active_layer(); - if(!active_layer) - return; - image_editor.image()->remove_layer(*active_layer); - image_editor.set_active_layer(nullptr); - image_editor.layers_did_change(); - }, window)); + layer_menu.add_action(GUI::Action::create( + "Remove active layer", { Mod_Ctrl, Key_D }, [&](auto&) { + auto active_layer = image_editor.active_layer(); + if (!active_layer) + return; + image_editor.image()->remove_layer(*active_layer); + image_editor.set_active_layer(nullptr); + image_editor.layers_did_change(); + }, + window)); auto& help_menu = menubar->add_menu("Help"); help_menu.add_action(GUI::Action::create("About", [&](auto&) { @@ -196,6 +216,16 @@ int main(int argc, char** argv) image->add_layer(*bg_layer); bg_layer->bitmap().fill(Color::White); + auto fg_layer1 = PixelPaint::Layer::create_with_size({ 200, 200 }, "FG Layer 1"); + fg_layer1->set_location({ 50, 50 }); + image->add_layer(*fg_layer1); + fg_layer1->bitmap().fill(Color::Yellow); + + auto fg_layer2 = PixelPaint::Layer::create_with_size({ 100, 100 }, "FG Layer 2"); + fg_layer2->set_location({ 300, 300 }); + image->add_layer(*fg_layer2); + fg_layer2->bitmap().fill(Color::Blue); + layer_table_view.set_model(image->layer_model()); layer_table_view.on_selection_change = [&] { auto index = layer_table_view.selection().first(); @@ -205,6 +235,8 @@ int main(int argc, char** argv) image_editor.set_active_layer(nullptr); }; + layer_list_widget.set_image(image); + image_editor.set_image(image); image_editor.set_active_layer(bg_layer);