| 1 | // Aseprite |
| 2 | // Copyright (C) 2019-2022 Igara Studio S.A. |
| 3 | // Copyright (C) 2018 David Capello |
| 4 | // |
| 5 | // This program is distributed under the terms of |
| 6 | // the End-User License Agreement for Aseprite. |
| 7 | |
| 8 | #ifdef HAVE_CONFIG_H |
| 9 | #include "config.h" |
| 10 | #endif |
| 11 | |
| 12 | #include "app/ui/color_shades.h" |
| 13 | |
| 14 | #include "app/app.h" |
| 15 | #include "app/modules/gfx.h" |
| 16 | #include "app/modules/palettes.h" |
| 17 | #include "app/shade.h" |
| 18 | #include "app/ui/color_bar.h" |
| 19 | #include "app/ui/skin/skin_theme.h" |
| 20 | #include "doc/color_mode.h" |
| 21 | #include "doc/palette.h" |
| 22 | #include "doc/palette_picks.h" |
| 23 | #include "doc/remap.h" |
| 24 | #include "ui/graphics.h" |
| 25 | #include "ui/message.h" |
| 26 | #include "ui/paint_event.h" |
| 27 | #include "ui/size_hint_event.h" |
| 28 | #include "ui/system.h" |
| 29 | |
| 30 | #include <algorithm> |
| 31 | |
| 32 | namespace app { |
| 33 | |
| 34 | ColorShades::ColorShades(const Shade& colors, ClickType click) |
| 35 | : Widget(ui::kGenericWidget) |
| 36 | , m_click(click) |
| 37 | , m_shade(colors) |
| 38 | , m_minColors(1) |
| 39 | , m_hotIndex(-1) |
| 40 | , m_dragIndex(-1) |
| 41 | , m_boxSize(12) |
| 42 | { |
| 43 | setText("No colors" ); |
| 44 | initTheme(); |
| 45 | } |
| 46 | |
| 47 | void ColorShades::setMinColors(int minColors) |
| 48 | { |
| 49 | m_minColors = minColors; |
| 50 | invalidate(); |
| 51 | } |
| 52 | |
| 53 | void ColorShades::reverseShadeColors() |
| 54 | { |
| 55 | std::reverse(m_shade.begin(), m_shade.end()); |
| 56 | invalidate(); |
| 57 | } |
| 58 | |
| 59 | doc::Remap* ColorShades::createShadeRemap(bool left) |
| 60 | { |
| 61 | // We need two or more colors to create a shading remap. In |
| 62 | // other case, the ShadingInkProcessing will use the full |
| 63 | // color palette. |
| 64 | Shade colors = getShade(); |
| 65 | if (colors.size() <= 1) |
| 66 | return nullptr; |
| 67 | |
| 68 | std::unique_ptr<doc::Remap> remap( |
| 69 | new doc::Remap(get_current_palette()->size())); |
| 70 | |
| 71 | for (int i=0; i<remap->size(); ++i) |
| 72 | remap->map(i, i); |
| 73 | |
| 74 | if (left) { |
| 75 | for (int i=1; i<int(colors.size()); ++i) { |
| 76 | int j = colors[i].getIndex(); |
| 77 | if (j >= 0 && j < remap->size()) |
| 78 | remap->map(j, colors[i-1].getIndex()); |
| 79 | } |
| 80 | } |
| 81 | else { |
| 82 | for (int i=0; i<int(colors.size())-1; ++i) { |
| 83 | int j = colors[i].getIndex(); |
| 84 | if (j >= 0 && j < remap->size()) |
| 85 | remap->map(j, colors[i+1].getIndex()); |
| 86 | } |
| 87 | } |
| 88 | return remap.release(); |
| 89 | } |
| 90 | |
| 91 | void ColorShades::setShade(const Shade& shade) |
| 92 | { |
| 93 | m_shade = shade; |
| 94 | invalidate(); |
| 95 | parent()->parent()->layout(); |
| 96 | } |
| 97 | |
| 98 | void ColorShades::onInitTheme(ui::InitThemeEvent& ev) |
| 99 | { |
| 100 | Widget::onInitTheme(ev); |
| 101 | |
| 102 | auto theme = skin::SkinTheme::get(this); |
| 103 | |
| 104 | switch (m_click) { |
| 105 | case ClickEntries: |
| 106 | case DragAndDropEntries: |
| 107 | setStyle(theme->styles.normalShadeView()); |
| 108 | break; |
| 109 | case ClickWholeShade: |
| 110 | setStyle(theme->styles.menuShadeView()); |
| 111 | break; |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | bool ColorShades::onProcessMessage(ui::Message* msg) |
| 116 | { |
| 117 | switch (msg->type()) { |
| 118 | |
| 119 | case ui::kSetCursorMessage: |
| 120 | if (hasCapture()) { |
| 121 | ui::set_mouse_cursor(ui::kMoveCursor); |
| 122 | return true; |
| 123 | } |
| 124 | else if (m_click == ClickEntries && |
| 125 | m_hotIndex >= 0 && |
| 126 | m_hotIndex < int(m_shade.size())) { |
| 127 | ui::set_mouse_cursor(ui::kHandCursor); |
| 128 | return true; |
| 129 | } |
| 130 | break; |
| 131 | |
| 132 | case ui::kMouseEnterMessage: |
| 133 | case ui::kMouseLeaveMessage: |
| 134 | if (!hasCapture()) |
| 135 | m_hotIndex = -1; |
| 136 | |
| 137 | invalidate(); |
| 138 | break; |
| 139 | |
| 140 | case ui::kMouseDownMessage: |
| 141 | if (m_hotIndex >= 0 && |
| 142 | m_hotIndex < int(m_shade.size())) { |
| 143 | switch (m_click) { |
| 144 | case ClickEntries: { |
| 145 | ClickEvent ev(static_cast<ui::MouseMessage*>(msg)->button()); |
| 146 | Click(ev); |
| 147 | |
| 148 | m_hotIndex = -1; |
| 149 | invalidate(); |
| 150 | break; |
| 151 | } |
| 152 | case DragAndDropEntries: |
| 153 | m_dragIndex = m_hotIndex; |
| 154 | m_dropBefore = false; |
| 155 | captureMouse(); |
| 156 | break; |
| 157 | } |
| 158 | } |
| 159 | break; |
| 160 | |
| 161 | case ui::kMouseUpMessage: { |
| 162 | auto button = static_cast<ui::MouseMessage*>(msg)->button(); |
| 163 | |
| 164 | if (m_click == ClickWholeShade) { |
| 165 | setSelected(true); |
| 166 | |
| 167 | ClickEvent ev(button); |
| 168 | Click(ev); |
| 169 | |
| 170 | closeWindow(); |
| 171 | } |
| 172 | |
| 173 | if (m_dragIndex >= 0) { |
| 174 | ASSERT(m_dragIndex < int(m_shade.size())); |
| 175 | |
| 176 | auto color = m_shade[m_dragIndex]; |
| 177 | m_shade.erase(m_shade.begin()+m_dragIndex); |
| 178 | if (m_hotIndex >= 0) |
| 179 | m_shade.insert(m_shade.begin()+m_hotIndex, color); |
| 180 | |
| 181 | m_dragIndex = -1; |
| 182 | invalidate(); |
| 183 | |
| 184 | ClickEvent ev(button); |
| 185 | Click(ev); |
| 186 | |
| 187 | // Relayout the context bar if we have removed an entry. |
| 188 | // |
| 189 | // TODO it looks like this should be handled in some kind of |
| 190 | // Change() event in the ColorBar |
| 191 | if (m_hotIndex < 0 && |
| 192 | parent() && |
| 193 | parent()->parent()) { |
| 194 | parent()->parent()->layout(); |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | if (hasCapture()) |
| 199 | releaseMouse(); |
| 200 | break; |
| 201 | } |
| 202 | |
| 203 | case ui::kMouseMoveMessage: { |
| 204 | ui::MouseMessage* mouseMsg = static_cast<ui::MouseMessage*>(msg); |
| 205 | gfx::Point mousePos = mouseMsg->position() - bounds().origin(); |
| 206 | gfx::Rect bounds = clientBounds(); |
| 207 | int hot = -1; |
| 208 | |
| 209 | bounds.shrink(3*ui::guiscale()); |
| 210 | |
| 211 | if (bounds.contains(mousePos)) { |
| 212 | int count = std::max(1, size()); |
| 213 | int boxWidth = std::max(1, bounds.w / count); |
| 214 | hot = (mousePos.x - bounds.x) / boxWidth; |
| 215 | hot = std::clamp(hot, 0, count-1); |
| 216 | } |
| 217 | |
| 218 | if (m_hotIndex != hot) { |
| 219 | m_hotIndex = hot; |
| 220 | invalidate(); |
| 221 | } |
| 222 | |
| 223 | bool dropBefore = |
| 224 | (hot >= 0 && mousePos.x < (bounds.x+m_boxSize*ui::guiscale()*hot)+m_boxSize*ui::guiscale()/2); |
| 225 | if (m_dropBefore != dropBefore) { |
| 226 | m_dropBefore = dropBefore; |
| 227 | invalidate(); |
| 228 | } |
| 229 | break; |
| 230 | } |
| 231 | } |
| 232 | return Widget::onProcessMessage(msg); |
| 233 | } |
| 234 | |
| 235 | void ColorShades::onSizeHint(ui::SizeHintEvent& ev) |
| 236 | { |
| 237 | int size = this->size(); |
| 238 | if (size < 2) |
| 239 | ev.setSizeHint(gfx::Size((16+m_boxSize)*ui::guiscale()+textWidth(), 18*ui::guiscale())); |
| 240 | else { |
| 241 | if (m_click == ClickWholeShade && size > 16) |
| 242 | size = 16; |
| 243 | ev.setSizeHint(gfx::Size(6+m_boxSize*size, 18)*ui::guiscale()); |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | void ColorShades::onPaint(ui::PaintEvent& ev) |
| 248 | { |
| 249 | auto theme = skin::SkinTheme::get(this); |
| 250 | ui::Graphics* g = ev.graphics(); |
| 251 | gfx::Rect bounds = clientBounds(); |
| 252 | |
| 253 | theme->paintWidget(g, this, style(), bounds); |
| 254 | |
| 255 | bounds.shrink(3*ui::guiscale()); |
| 256 | |
| 257 | Shade colors = getShade(); |
| 258 | if (colors.size() >= m_minColors) { |
| 259 | gfx::Rect box(bounds.x, bounds.y, |
| 260 | bounds.w / std::max(1, int(colors.size())), |
| 261 | bounds.h); |
| 262 | gfx::Rect hotBounds; |
| 263 | |
| 264 | int j = 0; |
| 265 | for (int i=0; box.x<bounds.x2(); ++i, box.x += box.w) { |
| 266 | // Make the last box a little bigger to just use all |
| 267 | // available size |
| 268 | if (i == int(colors.size())-1) |
| 269 | box.w = bounds.x2()-box.x; |
| 270 | |
| 271 | app::Color color; |
| 272 | |
| 273 | if (m_dragIndex >= 0 && |
| 274 | m_hotIndex == i) { |
| 275 | color = colors[m_dragIndex]; |
| 276 | } |
| 277 | else { |
| 278 | if (j == m_dragIndex) { |
| 279 | ++j; |
| 280 | } |
| 281 | if (j < int(colors.size())) |
| 282 | color = colors[j++]; |
| 283 | else |
| 284 | color = app::Color::fromMask(); |
| 285 | } |
| 286 | |
| 287 | draw_color(g, box, color, |
| 288 | (doc::ColorMode)app_get_current_pixel_format()); |
| 289 | |
| 290 | if (m_hotIndex == i) |
| 291 | hotBounds = box; |
| 292 | } |
| 293 | |
| 294 | if (!hotBounds.isEmpty() && |
| 295 | isHotEntryVisible()) { |
| 296 | hotBounds.enlarge(3*ui::guiscale()); |
| 297 | |
| 298 | ui::PaintWidgetPartInfo info; |
| 299 | theme->paintWidgetPart( |
| 300 | g, theme->styles.shadeSelection(), hotBounds, info); |
| 301 | } |
| 302 | } |
| 303 | else { |
| 304 | g->fillRect(theme->colors.editorFace(), bounds); |
| 305 | g->drawAlignedUIText(text(), theme->colors.face(), gfx::ColorNone, bounds, |
| 306 | ui::CENTER | ui::MIDDLE); |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | } // namespace app |
| 311 | |