diff --git a/ChangeLog b/ChangeLog index a28112262f..f7d8d655b2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -43,6 +43,7 @@ Unreleased Version 2.2.2-pre * Fix: Don't unnecessarily scale full-canvas animations by a single pixel. * Feature: Replace GIF export with ffmpeg's libraries, since they are also used for videos. It's way faster, generates much better palettes and . Thanks dAVePAGE and JJ for reporting issues in this regard. * Fix: Constrain aspect ratio of transform scaling properly, it was getting offset by the distance between the clicked point and the actual corner. Thanks Blozzom for reporting. + * Feature: Allow configuring flood fill preview and confirmation behavior, defaulting to the simplest mode similar to single-user software, but still previewing fills locally first. The magic wand always works this way for now, since selections are local only anyway. 2024-08-09 Version 2.2.2-beta.3 * Fix: Use more accurate timers for performance profiles if the platform supports it. diff --git a/src/desktop/cursors/bucketcheck.png b/src/desktop/cursors/bucketcheck.png new file mode 100644 index 0000000000..1427fc6b70 Binary files /dev/null and b/src/desktop/cursors/bucketcheck.png differ diff --git a/src/desktop/cursors/dpcursors.qrc b/src/desktop/cursors/dpcursors.qrc index 99cc393ae9..3cd10cf4dd 100644 --- a/src/desktop/cursors/dpcursors.qrc +++ b/src/desktop/cursors/dpcursors.qrc @@ -2,6 +2,7 @@ arrow.png bucket.png + bucketcheck.png check.png colorpicker.png layerpicker.png diff --git a/src/desktop/dialogs/settingsdialog/userinterface.cpp b/src/desktop/dialogs/settingsdialog/userinterface.cpp index be56b0961c..2f9416a2d1 100644 --- a/src/desktop/dialogs/settingsdialog/userinterface.cpp +++ b/src/desktop/dialogs/settingsdialog/userinterface.cpp @@ -149,11 +149,6 @@ void UserInterface::initMiscellaneous( settings.bindShowTransformNotices(showTransformNotices); form->addRow(tr("On-canvas notices:"), showTransformNotices); - QCheckBox *showFillNotices = - new QCheckBox(tr("Fill and magic wand confirmation")); - settings.bindShowFillNotices(showFillNotices); - form->addRow(nullptr, showFillNotices); - QCheckBox *scrollBars = new QCheckBox(tr("Show scroll bars on canvas")); settings.bindCanvasScrollBars(scrollBars); form->addRow(tr("Miscellaneous:"), scrollBars); diff --git a/src/desktop/scene/canvasview.cpp b/src/desktop/scene/canvasview.cpp index 75a724238f..b2c5795f0f 100644 --- a/src/desktop/scene/canvasview.cpp +++ b/src/desktop/scene/canvasview.cpp @@ -789,9 +789,6 @@ void CanvasView::resetCursor() } else if(m_toolState == int(tools::ToolState::Busy)) { setViewportCursor(Qt::WaitCursor); return; - } else if(m_toolState == int(tools::ToolState::AwaitingConfirmation)) { - setViewportCursor(m_checkCursor); - return; } if(m_toolcursor.shape() == Qt::CrossCursor) { diff --git a/src/desktop/toolwidgets/fillsettings.cpp b/src/desktop/toolwidgets/fillsettings.cpp index b4258958ff..a3ff121cf8 100644 --- a/src/desktop/toolwidgets/fillsettings.cpp +++ b/src/desktop/toolwidgets/fillsettings.cpp @@ -9,15 +9,20 @@ #include "libclient/tools/toolcontroller.h" #include "libclient/tools/toolproperties.h" #include "ui_fillsettings.h" +#include #include #include +#include +#include #include namespace tools { namespace props { static const ToolProperties::Value shrink{ - QStringLiteral("shrink"), false}; + QStringLiteral("shrink"), false}, + editable{QStringLiteral("editable"), false}, + confirm{QStringLiteral("confirm"), false}; static const ToolProperties::RangedValue expand{ QStringLiteral("expand"), 0, 0, 100}, featherRadius{QStringLiteral("featherRadius"), 0, 0, 40}, @@ -48,6 +53,41 @@ FillSettings::~FillSettings() QWidget *FillSettings::createUiWidget(QWidget *parent) { + m_headerWidget = new QWidget(parent); + QHBoxLayout *headerLayout = new QHBoxLayout; + headerLayout->setSpacing(0); + headerLayout->setContentsMargins(0, 0, 0, 0); + + widgets::GroupedToolButton *menuButton = new widgets::GroupedToolButton( + widgets::GroupedToolButton::NotGrouped, m_headerWidget); + menuButton->setIcon(QIcon::fromTheme("application-menu")); + menuButton->setToolButtonStyle(Qt::ToolButtonIconOnly); + menuButton->setPopupMode(QToolButton::InstantPopup); + menuButton->setStatusTip(tr("Fill tool settings")); + menuButton->setToolTip(menuButton->statusTip()); + headerLayout->addWidget(menuButton); + + QMenu *menu = new QMenu(menuButton); + menuButton->setMenu(menu); + + m_editableAction = menu->addAction(tr("&Edit pending fills")); + m_editableAction->setStatusTip(tr( + "Apply changes in settings, color and layer to fills not yet applied")); + m_editableAction->setCheckable(true); + connect( + m_editableAction, &QAction::triggered, this, + &FillSettings::updateSettings); + + m_confirmAction = menu->addAction(tr("&Confirm fills with second click")); + m_confirmAction->setStatusTip(tr( + "Lets you apply fills with a click instead of starting another fill")); + m_confirmAction->setCheckable(true); + connect( + m_confirmAction, &QAction::triggered, this, + &FillSettings::updateSettings); + + headerLayout->addStretch(); + QWidget *uiwidget = new QWidget(parent); m_ui = new Ui_FillSettings; m_ui->setupUi(uiwidget); @@ -85,36 +125,36 @@ QWidget *FillSettings::createUiWidget(QWidget *parent) &FillSettings::updateSize); connect( m_ui->opacity, QOverload::of(&QSpinBox::valueChanged), this, - &FillSettings::pushSettings); + &FillSettings::updateSettings); connect( m_ui->tolerance, QOverload::of(&QSpinBox::valueChanged), this, - &FillSettings::pushSettings); + &FillSettings::updateSettings); connect( m_ui->size, QOverload::of(&QSpinBox::valueChanged), this, - &FillSettings::pushSettings); + &FillSettings::updateSettings); connect( m_ui->expandShrink, &widgets::ExpandShrinkSpinner::spinnerValueChanged, - this, &FillSettings::pushSettings); + this, &FillSettings::updateSettings); connect( m_ui->expandShrink, &widgets::ExpandShrinkSpinner::shrinkChanged, this, - &FillSettings::pushSettings); + &FillSettings::updateSettings); connect( m_ui->expandShrink, &widgets::ExpandShrinkSpinner::kernelChanged, this, - &FillSettings::pushSettings); + &FillSettings::updateSettings); connect( m_ui->feather, QOverload::of(&QSpinBox::valueChanged), this, - &FillSettings::pushSettings); + &FillSettings::updateSettings); connect( m_ui->gap, QOverload::of(&QSpinBox::valueChanged), this, - &FillSettings::pushSettings); + &FillSettings::updateSettings); connect( m_sourceGroup, QOverload::of(&QButtonGroup::buttonClicked), this, - &FillSettings::pushSettings); + &FillSettings::updateSettings); connect( m_areaGroup, QOverload::of(&QButtonGroup::buttonClicked), this, - &FillSettings::pushSettings); + &FillSettings::updateSettings); connect( m_areaGroup, QOverload::of(&QButtonGroup::buttonClicked), this, @@ -122,8 +162,26 @@ QWidget *FillSettings::createUiWidget(QWidget *parent) connect( m_ui->blendModeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, - &FillSettings::pushSettings); + &FillSettings::updateSettings); updateSize(); + + m_ui->applyButton->setIcon( + uiwidget->style()->standardIcon(QStyle::SP_DialogApplyButton)); + connect( + m_ui->applyButton, &QPushButton::clicked, controller(), + &ToolController::finishMultipartDrawing); + + m_ui->cancelButton->setIcon( + uiwidget->style()->standardIcon(QStyle::SP_DialogCancelButton)); + connect( + m_ui->cancelButton, &QPushButton::clicked, controller(), + &ToolController::cancelMultipartDrawing); + + connect( + controller(), &ToolController::floodFillStateChanged, this, + &FillSettings::setButtonState); + setButtonState(false, false); + return uiwidget; } @@ -171,7 +229,8 @@ void FillSettings::pushSettings() m_ui->feather->value(), isSizeUnlimited(size) ? -1 : size, m_ui->opacity->value() / 100.0, m_ui->gap->value(), FloodFill::Source(m_sourceGroup->checkedId()), blendMode, - FloodFill::Area(area)); + FloodFill::Area(area), m_editableAction->isChecked(), + m_confirmAction->isChecked()); if(!m_ui->sourceFillSource->isEnabled() && m_ui->sourceFillSource->isChecked()) { @@ -230,6 +289,8 @@ ToolProperties FillSettings::saveToolSettings() props::tolerance, m_ui->tolerance->value() / qreal(m_ui->tolerance->maximum())); cfg.setValue(props::expand, m_ui->expandShrink->spinnerValue()); + cfg.setValue(props::editable, m_editableAction->isChecked()); + cfg.setValue(props::confirm, m_confirmAction->isChecked()); cfg.setValue(props::shrink, m_ui->expandShrink->isShrink()); cfg.setValue(props::kernel, m_ui->expandShrink->kernel()); cfg.setValue(props::featherRadius, m_ui->feather->value()); @@ -261,6 +322,10 @@ int FillSettings::getSize() const void FillSettings::restoreToolSettings(const ToolProperties &cfg) { + QScopedValueRollback rollback(m_updating); + + m_editableAction->setChecked(cfg.value(props::editable)); + m_confirmAction->setChecked(cfg.value(props::confirm)); m_ui->tolerance->setValue( cfg.value(props::tolerance) * m_ui->tolerance->maximum()); m_ui->expandShrink->setSpinnerValue(cfg.value(props::expand)); @@ -310,6 +375,13 @@ void FillSettings::stepAdjust1(bool increase) } } +void FillSettings::updateSettings() +{ + if(!m_updating) { + pushSettings(); + } +} + void FillSettings::updateSize() { int size = m_ui->size->value(); @@ -368,4 +440,10 @@ void FillSettings::selectBlendMode(int blendMode) } } +void FillSettings::setButtonState(bool running, bool pending) +{ + m_ui->applyButton->setEnabled(pending); + m_ui->cancelButton->setEnabled(running || pending); +} + } diff --git a/src/desktop/toolwidgets/fillsettings.h b/src/desktop/toolwidgets/fillsettings.h index f4dcda0b7f..ff777abd1b 100644 --- a/src/desktop/toolwidgets/fillsettings.h +++ b/src/desktop/toolwidgets/fillsettings.h @@ -3,6 +3,7 @@ #define DESKTOP_TOOLWIDGETS_FILL_H #include "desktop/toolwidgets/toolsettings.h" +class QAction; class QButtonGroup; class Ui_FillSettings; @@ -40,6 +41,8 @@ class FillSettings final : public ToolSettings { void setCompatibilityMode(bool compatibilityMode); + QWidget *getHeaderWidget() override { return m_headerWidget; } + signals: void pixelSizeChanged(int size); @@ -54,6 +57,8 @@ public slots: QWidget *createUiWidget(QWidget *parent) override; private: + void updateSettings(); + void updateSize(); static bool isSizeUnlimited(int size); int calculatePixelSize(int size) const; @@ -61,7 +66,12 @@ public slots: void initBlendModeOptions(); void selectBlendMode(int blendMode); + void setButtonState(bool running, bool pending); + + QWidget *m_headerWidget = nullptr; Ui_FillSettings *m_ui = nullptr; + QAction *m_editableAction = nullptr; + QAction *m_confirmAction = nullptr; QButtonGroup *m_sourceGroup = nullptr; QButtonGroup *m_areaGroup = nullptr; int m_previousMode; @@ -69,6 +79,7 @@ public slots: qreal m_quickAdjust1 = 0.0; bool m_haveSelection = false; bool m_compatibilityMode = false; + bool m_updating = false; }; } diff --git a/src/desktop/ui/fillsettings.ui b/src/desktop/ui/fillsettings.ui index 5fbfd76b7d..a9cad2f962 100644 --- a/src/desktop/ui/fillsettings.ui +++ b/src/desktop/ui/fillsettings.ui @@ -26,6 +26,41 @@ 3 + + + + px + + + Size Limit: + + + 20 + + + 5000 + + + 1 + + + true + + + + + + + Tolerance: + + + 254 + + + true + + + @@ -125,6 +160,28 @@ + + + + % + + + Opacity: + + + 1 + + + 100 + + + 100 + + + true + + + @@ -225,19 +282,29 @@ - - - - Tolerance: + + + + Qt::Vertical - - 254 + + + 0 + 0 + - - true + + + + + + Source: + + + @@ -254,19 +321,6 @@ - - - - Qt::Vertical - - - - 0 - 0 - - - - @@ -283,28 +337,6 @@ - - - - % - - - Opacity: - - - 1 - - - 100 - - - 100 - - - true - - - @@ -312,37 +344,35 @@ - - - - Source: - - - - - - - px - - - Size Limit: - - - 20 - - - 5000 - - - 1 - - - true - - - - - + + + + + + Apply the pending fill, making it visible for everyone + + + Apply the pending fill, making it visible for everyone + + + Apply + + + + + + + Cancel the pending fill, without anyone else seeing its effects + + + Cancel the pending fill, without anyone else seeing its effects + + + Cancel + + + + @@ -363,6 +393,24 @@
desktop/widgets/expandshrinkspinner.h
+ + size + opacity + tolerance + feather + gap + sourceMerged + sourceMergedWithoutBackground + sourceLayer + sourceFillSource + sourceDummyCombo + areaContinuous + areaSimilar + areaSelection + blendModeCombo + applyButton + cancelButton + diff --git a/src/desktop/view/canvascontroller.cpp b/src/desktop/view/canvascontroller.cpp index 8bba5f1361..348ebaf3ce 100644 --- a/src/desktop/view/canvascontroller.cpp +++ b/src/desktop/view/canvascontroller.cpp @@ -1625,9 +1625,6 @@ void CanvasController::resetCursor() } else if(m_toolState == int(tools::ToolState::Busy)) { setViewportCursor(Qt::WaitCursor); return; - } else if(m_toolState == int(tools::ToolState::AwaitingConfirmation)) { - setViewportCursor(m_checkCursor); - return; } if(m_toolCursor.shape() == Qt::CrossCursor) { diff --git a/src/drawdance/libengine/dpengine/canvas_state.h b/src/drawdance/libengine/dpengine/canvas_state.h index 6f3fb88032..3732c0b0f4 100644 --- a/src/drawdance/libengine/dpengine/canvas_state.h +++ b/src/drawdance/libengine/dpengine/canvas_state.h @@ -43,9 +43,9 @@ typedef struct DP_UserCursors DP_UserCursors; typedef struct DP_ViewModeFilter DP_ViewModeFilter; -#define DP_FLAT_IMAGE_INCLUDE_BACKGROUND (1 << 0) -#define DP_FLAT_IMAGE_INCLUDE_SUBLAYERS (1 << 1) -#define DP_FLAT_IMAGE_ONE_BIT_ALPHA (1 << 2) +#define DP_FLAT_IMAGE_INCLUDE_BACKGROUND (1u << 0u) +#define DP_FLAT_IMAGE_INCLUDE_SUBLAYERS (1u << 1u) +#define DP_FLAT_IMAGE_ONE_BIT_ALPHA (1u << 2u) #define DP_FLAT_IMAGE_RENDER_FLAGS \ (DP_FLAT_IMAGE_INCLUDE_BACKGROUND | DP_FLAT_IMAGE_INCLUDE_SUBLAYERS) diff --git a/src/drawdance/libengine/dpengine/flood_fill.c b/src/drawdance/libengine/dpengine/flood_fill.c index 245fbb0daf..66b91bf0bc 100644 --- a/src/drawdance/libengine/dpengine/flood_fill.c +++ b/src/drawdance/libengine/dpengine/flood_fill.c @@ -104,8 +104,20 @@ static void init_selection(DP_FillContext *c, DP_CanvasState *cs, static DP_LayerContent *merge_image(DP_CanvasState *cs, DP_ViewMode view_mode, int active_layer_id, int active_frame_index, - unsigned int flags) + bool include_background, + bool include_sublayers) { + static_assert(DP_FLAT_IMAGE_RENDER_FLAGS & DP_FLAT_IMAGE_INCLUDE_BACKGROUND, + "render flags include background (and we may exclude it)"); + static_assert(DP_FLAT_IMAGE_RENDER_FLAGS & DP_FLAT_IMAGE_INCLUDE_SUBLAYERS, + "render flags include sublayers (and we may exclude them)"); + unsigned int flags = DP_FLAT_IMAGE_RENDER_FLAGS; + if (!include_background) { + flags &= ~DP_FLAT_IMAGE_INCLUDE_BACKGROUND; + } + if (!include_sublayers) { + flags &= ~DP_FLAT_IMAGE_INCLUDE_SUBLAYERS; + } DP_ViewModeBuffer vmb; DP_view_mode_buffer_init(&vmb); DP_ViewModeFilter vmf = DP_view_mode_filter_make( @@ -797,13 +809,15 @@ static DP_FloodFillResult finish_fill(DP_FillContext *c, float *mask, int img_x, return DP_FLOOD_FILL_SUCCESS; } -DP_FloodFillResult DP_flood_fill( - DP_CanvasState *cs, unsigned int context_id, int selection_id, int x, int y, - DP_UPixelFloat fill_color, double tolerance, int layer_id, int size, - int gap, int expand, DP_FloodFillKernel kernel_shape, int feather_radius, - bool from_edge, bool continuous, DP_ViewMode view_mode, int active_layer_id, - int active_frame_index, DP_Image **out_img, int *out_x, int *out_y, - DP_FloodFillShouldCancelFn should_cancel, void *user) +DP_FloodFillResult +DP_flood_fill(DP_CanvasState *cs, unsigned int context_id, int selection_id, + int x, int y, DP_UPixelFloat fill_color, double tolerance, + int layer_id, int size, int gap, int expand, + DP_FloodFillKernel kernel_shape, int feather_radius, + bool from_edge, bool continuous, bool include_sublayers, + DP_ViewMode view_mode, int active_layer_id, + int active_frame_index, DP_Image **out_img, int *out_x, + int *out_y, DP_FloodFillShouldCancelFn should_cancel, void *user) { DP_ASSERT(cs); @@ -857,14 +871,11 @@ DP_FloodFillResult DP_flood_fill( if (layer_id == 0) { c.lc = merge_image(cs, view_mode, active_layer_id, active_frame_index, - DP_FLAT_IMAGE_RENDER_FLAGS - & ~DP_FLAT_IMAGE_INCLUDE_SUBLAYERS); + true, include_sublayers); } else if (layer_id == -1) { c.lc = merge_image(cs, view_mode, active_layer_id, active_frame_index, - DP_FLAT_IMAGE_RENDER_FLAGS - & ~(DP_FLAT_IMAGE_INCLUDE_BACKGROUND - | DP_FLAT_IMAGE_INCLUDE_SUBLAYERS)); + false, include_sublayers); } else { DP_LayerRoutes *lr = DP_canvas_state_layer_routes_noinc(cs); @@ -876,9 +887,14 @@ DP_FloodFillResult DP_flood_fill( else if (DP_layer_routes_entry_is_group(lre)) { DP_LayerGroup *lg = DP_layer_routes_entry_group(lre, cs); DP_LayerProps *lp = DP_layer_routes_entry_props(lre, cs); - DP_TransientLayerContent *tlc = DP_layer_group_merge(lg, lp); + DP_TransientLayerContent *tlc = + DP_layer_group_merge(lg, lp, include_sublayers); c.lc = (DP_LayerContent *)tlc; } + else if (include_sublayers) { + c.lc = DP_layer_content_merge_sublayers( + DP_layer_routes_entry_content(lre, cs)); + } else { c.lc = DP_layer_content_incref(DP_layer_routes_entry_content(lre, cs)); diff --git a/src/drawdance/libengine/dpengine/flood_fill.h b/src/drawdance/libengine/dpengine/flood_fill.h index d5fe29703f..bbbafe9c81 100644 --- a/src/drawdance/libengine/dpengine/flood_fill.h +++ b/src/drawdance/libengine/dpengine/flood_fill.h @@ -43,13 +43,15 @@ typedef enum DP_FloodFillResult { typedef bool (*DP_FloodFillShouldCancelFn)(void *user); -DP_FloodFillResult DP_flood_fill( - DP_CanvasState *cs, unsigned int context_id, int selection_id, int x, int y, - DP_UPixelFloat fill_color, double tolerance, int layer_id, int size, - int gap, int expand, DP_FloodFillKernel kernel_shape, int feather_radius, - bool from_edge, bool continuous, DP_ViewMode view_mode, int active_layer_id, - int active_frame_index, DP_Image **out_img, int *out_x, int *out_y, - DP_FloodFillShouldCancelFn should_cancel, void *user); +DP_FloodFillResult +DP_flood_fill(DP_CanvasState *cs, unsigned int context_id, int selection_id, + int x, int y, DP_UPixelFloat fill_color, double tolerance, + int layer_id, int size, int gap, int expand, + DP_FloodFillKernel kernel_shape, int feather_radius, + bool from_edge, bool continuous, bool include_sublayers, + DP_ViewMode view_mode, int active_layer_id, + int active_frame_index, DP_Image **out_img, int *out_x, + int *out_y, DP_FloodFillShouldCancelFn should_cancel, void *user); DP_FloodFillResult DP_selection_fill(DP_CanvasState *cs, unsigned int context_id, int selection_id, diff --git a/src/drawdance/libengine/dpengine/layer_content.c b/src/drawdance/libengine/dpengine/layer_content.c index 7037eac185..e7f96f66d9 100644 --- a/src/drawdance/libengine/dpengine/layer_content.c +++ b/src/drawdance/libengine/dpengine/layer_content.c @@ -1291,7 +1291,14 @@ DP_LayerContent *DP_layer_content_merge_sublayers(DP_LayerContent *lc) DP_LayerList *ll = lc->sub.contents; int count = DP_layer_list_count(ll); if (count > 0) { - DP_TransientLayerContent *tlc = DP_transient_layer_content_new(lc); + DP_TransientLayerContent *tlc; + if (DP_layer_content_transient(lc)) { + tlc = DP_transient_layer_content_new_transient( + (DP_TransientLayerContent *)lc); + } + else { + tlc = DP_transient_layer_content_new(lc); + } DP_transient_layer_content_merge_all_sublayers(tlc, 0); return DP_transient_layer_content_persist(tlc); } @@ -1318,6 +1325,35 @@ DP_TransientLayerContent *DP_transient_layer_content_new(DP_LayerContent *lc) return tlc; } +DP_TransientLayerContent * +DP_transient_layer_content_new_transient(DP_TransientLayerContent *tlc) +{ + DP_ASSERT(tlc); + DP_ASSERT(DP_atomic_get(&tlc->refcount) > 0); + int width = tlc->width; + int height = tlc->height; + DP_TransientLayerContent *new_tlc = alloc_layer_content(width, height); + int count = DP_tile_total_round(width, height); + for (int i = 0; i < count; ++i) { + DP_Tile *t = tlc->elements[i].tile; + if (t) { + if (DP_tile_transient(t)) { + new_tlc->elements[i].transient_tile = + DP_transient_tile_new_transient((DP_TransientTile *)t, 0); + } + else { + new_tlc->elements[i].tile = DP_tile_incref(t); + } + } + else { + new_tlc->elements[i].tile = NULL; + } + } + new_tlc->sub.contents = DP_layer_list_incref(tlc->sub.contents); + new_tlc->sub.props = DP_layer_props_list_incref(tlc->sub.props); + return new_tlc; +} + DP_TransientLayerContent * DP_transient_layer_content_new_init(int width, int height, DP_Tile *tile) { diff --git a/src/drawdance/libengine/dpengine/layer_content.h b/src/drawdance/libengine/dpengine/layer_content.h index bfc37076a8..ecd88f8038 100644 --- a/src/drawdance/libengine/dpengine/layer_content.h +++ b/src/drawdance/libengine/dpengine/layer_content.h @@ -151,6 +151,9 @@ DP_TransientTile *DP_layer_content_flatten_tile_to( DP_TransientLayerContent *DP_transient_layer_content_new(DP_LayerContent *lc); +DP_TransientLayerContent * +DP_transient_layer_content_new_transient(DP_TransientLayerContent *tlc); + DP_TransientLayerContent * DP_transient_layer_content_new_init(int width, int height, DP_Tile *tile); diff --git a/src/drawdance/libengine/dpengine/layer_group.c b/src/drawdance/libengine/dpengine/layer_group.c index effc2991f7..3821d7876b 100644 --- a/src/drawdance/libengine/dpengine/layer_group.c +++ b/src/drawdance/libengine/dpengine/layer_group.c @@ -335,7 +335,8 @@ DP_Pixel8 *DP_layer_group_to_pixels8(DP_LayerGroup *lg, DP_LayerProps *lp, } DP_TransientLayerContent *DP_layer_group_merge(DP_LayerGroup *lg, - DP_LayerProps *lp) + DP_LayerProps *lp, + bool include_sublayers) { DP_ASSERT(lg); DP_ASSERT(DP_atomic_get(&lg->refcount) > 0); @@ -343,8 +344,8 @@ DP_TransientLayerContent *DP_layer_group_merge(DP_LayerGroup *lg, DP_LayerPropsList *lpl = DP_layer_props_children_noinc(lp); DP_TransientLayerContent *tlc = DP_transient_layer_content_new_init(lg->width, lg->height, NULL); - DP_layer_list_merge_to_flat_image(lg->children, lpl, tlc, DP_BIT15, true, - true, false); + DP_layer_list_merge_to_flat_image(lg->children, lpl, tlc, DP_BIT15, + include_sublayers, true, false); return tlc; } diff --git a/src/drawdance/libengine/dpengine/layer_group.h b/src/drawdance/libengine/dpengine/layer_group.h index 9cb4dcb2b4..598589715e 100644 --- a/src/drawdance/libengine/dpengine/layer_group.h +++ b/src/drawdance/libengine/dpengine/layer_group.h @@ -87,7 +87,8 @@ DP_Pixel8 *DP_layer_group_to_pixels8(DP_LayerGroup *lg, DP_LayerProps *lp, bool reveal_censored); DP_TransientLayerContent *DP_layer_group_merge(DP_LayerGroup *lg, - DP_LayerProps *lp); + DP_LayerProps *lp, + bool include_sublayers); void DP_layer_group_merge_to_flat_image(DP_LayerGroup *lg, DP_LayerProps *lp, DP_TransientLayerContent *tlc, diff --git a/src/drawdance/libengine/dpengine/layer_list.c b/src/drawdance/libengine/dpengine/layer_list.c index 0239106d3c..17f6f1f64d 100644 --- a/src/drawdance/libengine/dpengine/layer_list.c +++ b/src/drawdance/libengine/dpengine/layer_list.c @@ -758,7 +758,7 @@ void DP_transient_layer_list_merge_at(DP_TransientLayerList *tll, DP_ASSERT(index < tll->count); DP_ASSERT(tll->elements[index].is_group); DP_LayerGroup *lg = tll->elements[index].group; - DP_TransientLayerContent *tlc = DP_layer_group_merge(lg, lp); + DP_TransientLayerContent *tlc = DP_layer_group_merge(lg, lp, true); DP_layer_group_decref(lg); tll->elements[index] = (DP_LayerListEntry){.is_group = false, .transient_content = tlc}; diff --git a/src/drawdance/libengine/dpengine/tile.c b/src/drawdance/libengine/dpengine/tile.c index eb2da76fd0..2fd68135d8 100644 --- a/src/drawdance/libengine/dpengine/tile.c +++ b/src/drawdance/libengine/dpengine/tile.c @@ -544,6 +544,12 @@ DP_TransientTile *DP_transient_tile_new(DP_Tile *tile, unsigned int context_id) return tt; } +DP_TransientTile *DP_transient_tile_new_transient(DP_TransientTile *tt, + unsigned int context_id) +{ + return DP_transient_tile_new((DP_Tile *)tt, context_id); +} + DP_TransientTile *DP_transient_tile_new_blank(unsigned int context_id) { DP_TransientTile *tt = alloc_tile(true, true, context_id); diff --git a/src/drawdance/libengine/dpengine/tile.h b/src/drawdance/libengine/dpengine/tile.h index 7abeea1f5c..54292f68c9 100644 --- a/src/drawdance/libengine/dpengine/tile.h +++ b/src/drawdance/libengine/dpengine/tile.h @@ -160,6 +160,9 @@ void DP_tile_sample(DP_Tile *tile_or_null, const uint16_t *mask, int x, int y, DP_TransientTile *DP_transient_tile_new(DP_Tile *tile, unsigned int context_id); +DP_TransientTile *DP_transient_tile_new_transient(DP_TransientTile *tt, + unsigned int context_id); + DP_TransientTile *DP_transient_tile_new_blank(unsigned int context_id); DP_TransientTile *DP_transient_tile_new_nullable(DP_Tile *tile_or_null, diff --git a/src/drawdance/rust/bindings.rs b/src/drawdance/rust/bindings.rs index e075413030..d46471ee97 100644 --- a/src/drawdance/rust/bindings.rs +++ b/src/drawdance/rust/bindings.rs @@ -9,6 +9,7 @@ pub const DP_TILE_SIZE: u32 = 64; pub const DP_TILE_LENGTH: u32 = 4096; pub const DP_FLAT_IMAGE_INCLUDE_BACKGROUND: u32 = 1; pub const DP_FLAT_IMAGE_INCLUDE_SUBLAYERS: u32 = 2; +pub const DP_FLAT_IMAGE_ONE_BIT_ALPHA: u32 = 4; pub const DP_FLAT_IMAGE_RENDER_FLAGS: u32 = 3; pub const DP_DOCUMENT_METADATA_DPIX_DEFAULT: u32 = 72; pub const DP_DOCUMENT_METADATA_DPIY_DEFAULT: u32 = 72; @@ -2813,6 +2814,11 @@ extern "C" { lc: *mut DP_LayerContent, ) -> *mut DP_TransientLayerContent; } +extern "C" { + pub fn DP_transient_layer_content_new_transient( + tlc: *mut DP_TransientLayerContent, + ) -> *mut DP_TransientLayerContent; +} extern "C" { pub fn DP_transient_layer_content_new_init( width: ::std::os::raw::c_int, @@ -3130,6 +3136,7 @@ extern "C" { pub fn DP_layer_group_merge( lg: *mut DP_LayerGroup, lp: *mut DP_LayerProps, + include_sublayers: bool, ) -> *mut DP_TransientLayerContent; } extern "C" { @@ -7282,6 +7289,12 @@ extern "C" { context_id: ::std::os::raw::c_uint, ) -> *mut DP_TransientTile; } +extern "C" { + pub fn DP_transient_tile_new_transient( + tt: *mut DP_TransientTile, + context_id: ::std::os::raw::c_uint, + ) -> *mut DP_TransientTile; +} extern "C" { pub fn DP_transient_tile_new_blank(context_id: ::std::os::raw::c_uint) -> *mut DP_TransientTile; @@ -8032,22 +8045,6 @@ extern "C" { user: *mut ::std::os::raw::c_void, ) -> DP_SaveResult; } -extern "C" { - pub fn DP_save_animation_gif( - cs: *mut DP_CanvasState, - dc: *mut DP_DrawContext, - path: *const ::std::os::raw::c_char, - crop: *mut DP_Rect, - width: ::std::os::raw::c_int, - height: ::std::os::raw::c_int, - interpolation: ::std::os::raw::c_int, - start: ::std::os::raw::c_int, - end_inclusive: ::std::os::raw::c_int, - framerate: ::std::os::raw::c_int, - progress_fn: DP_SaveAnimationProgressFn, - user: *mut ::std::os::raw::c_void, - ) -> DP_SaveResult; -} pub const DP_ACCESS_TIER_OPERATOR: DP_AccessTier = 0; pub const DP_ACCESS_TIER_TRUSTED: DP_AccessTier = 1; pub const DP_ACCESS_TIER_AUTHENTICATED: DP_AccessTier = 2; diff --git a/src/libclient/document.cpp b/src/libclient/document.cpp index bc6a38b311..019f0329e2 100644 --- a/src/libclient/document.cpp +++ b/src/libclient/document.cpp @@ -81,8 +81,6 @@ Document::Document( m_toolctrl, &tools::ToolController::setInterpolateInputs); m_settings.bindMouseSmoothing( m_toolctrl, &tools::ToolController::setMouseSmoothing); - m_settings.bindShowFillNotices( - m_toolctrl, &tools::ToolController::setShowFillNotices); m_banlist = new net::BanlistModel(this); m_authList = new net::AuthListModel(this); m_announcementlist = diff --git a/src/libclient/drawdance/canvasstate.cpp b/src/libclient/drawdance/canvasstate.cpp index b7f8f0d6ad..d7e442ff94 100644 --- a/src/libclient/drawdance/canvasstate.cpp +++ b/src/libclient/drawdance/canvasstate.cpp @@ -324,17 +324,17 @@ DP_FloodFillResult CanvasState::floodFill( unsigned int contextId, int selectionId, int x, int y, const QColor &fillColor, double tolerance, int layerId, int sizeLimit, int gap, int expand, DP_FloodFillKernel kernel, int featherRadius, - bool fromEdge, bool continuous, DP_ViewMode viewMode, int activeLayerId, - int activeFrameIndex, const QAtomicInt &cancel, QImage &outImg, int &outX, - int &outY) const + bool fromEdge, bool continuous, bool includeSublayers, DP_ViewMode viewMode, + int activeLayerId, int activeFrameIndex, const QAtomicInt &cancel, + QImage &outImg, int &outX, int &outY) const { DP_UPixelFloat fillPixel = DP_upixel_float_from_color(fillColor.rgba()); DP_Image *img; DP_FloodFillResult result = DP_flood_fill( m_data, contextId, selectionId, x, y, fillPixel, tolerance, layerId, sizeLimit, gap, expand, kernel, featherRadius, fromEdge, continuous, - viewMode, activeLayerId, activeFrameIndex, &img, &outX, &outY, - shouldCancelFloodFill, const_cast(&cancel)); + includeSublayers, viewMode, activeLayerId, activeFrameIndex, &img, + &outX, &outY, shouldCancelFloodFill, const_cast(&cancel)); if(result == DP_FLOOD_FILL_SUCCESS) { outImg = wrapImage(img); } diff --git a/src/libclient/drawdance/canvasstate.h b/src/libclient/drawdance/canvasstate.h index a3e1c64348..aecb4a73f0 100644 --- a/src/libclient/drawdance/canvasstate.h +++ b/src/libclient/drawdance/canvasstate.h @@ -107,9 +107,9 @@ class CanvasState final { unsigned int contextId, int selectionId, int x, int y, const QColor &fillColor, double tolerance, int layerId, int sizeLimit, int gap, int expand, DP_FloodFillKernel kernel, int featherRadius, - bool fromEdge, bool continuous, DP_ViewMode viewMode, int activeLayerId, - int activeFrameIndex, const QAtomicInt &cancel, QImage &outImg, - int &outX, int &outY) const; + bool fromEdge, bool continuous, bool includeSublayers, + DP_ViewMode viewMode, int activeLayerId, int activeFrameIndex, + const QAtomicInt &cancel, QImage &outImg, int &outX, int &outY) const; DP_FloodFillResult selectionFill( unsigned int contextId, int selectionId, const QColor &fillColor, diff --git a/src/libclient/settings_table.h b/src/libclient/settings_table.h index 15edfd40ee..3332e71d6f 100644 --- a/src/libclient/settings_table.h +++ b/src/libclient/settings_table.h @@ -32,7 +32,6 @@ SETTING(serverAutoReset , ServerAutoReset , "settings/se SETTING(serverPrivateUserList , ServerPrivateUserList , "settings/server/privateUserList" , false) SETTING(serverPort , ServerPort , "settings/server/port" , cmake_config::proto::port()) SETTING(serverTimeout , ServerTimeout , "settings/server/timeout" , 60) -SETTING(showFillNotices , ShowFillNotices , "settings/showfillnotices" , true) SETTING(smoothing , Smoothing , "settings/input/smooth" , defaultSmoothing) #include "libclient/settings_table_macros.h" diff --git a/src/libclient/tools/floodfill.cpp b/src/libclient/tools/floodfill.cpp index b909586aa2..1ef64871bd 100644 --- a/src/libclient/tools/floodfill.cpp +++ b/src/libclient/tools/floodfill.cpp @@ -20,7 +20,8 @@ class FloodFill::Task final : public ToolController::Task { const QPointF &point, const QColor &fillColor, double tolerance, int sourceLayerId, int size, int gap, int expansion, DP_FloodFillKernel kernel, int featherRadius, Area area, - DP_ViewMode viewMode, int activeLayerId, int activeFrameIndex) + DP_ViewMode viewMode, int activeLayerId, int activeFrameIndex, + bool editable) : m_tool(tool) , m_cancel(cancel) , m_canvasState(canvasState) @@ -38,6 +39,7 @@ class FloodFill::Task final : public ToolController::Task { , m_viewMode(viewMode) , m_activeLayerId(activeLayerId) , m_activeFrameIndex(activeFrameIndex) + , m_editable(editable) { } @@ -58,8 +60,9 @@ class FloodFill::Task final : public ToolController::Task { m_contextId, canvas::CanvasModel::MAIN_SELECTION_ID, m_point.x(), m_point.y(), m_fillColor, m_tolerance, m_sourceLayerId, size, continuous ? m_gap : 0, m_expansion, - m_kernel, m_featherRadius, false, continuous, m_viewMode, - m_activeLayerId, m_activeFrameIndex, m_cancel, m_img, m_x, m_y); + m_kernel, m_featherRadius, false, continuous, !m_editable, + m_viewMode, m_activeLayerId, m_activeFrameIndex, m_cancel, + m_img, m_x, m_y); } if(m_result != DP_FLOOD_FILL_SUCCESS && m_result != DP_FLOOD_FILL_CANCELLED) { @@ -75,6 +78,7 @@ class FloodFill::Task final : public ToolController::Task { Area area() const { return m_area; } QColor color() const { return m_fillColor; } const QString &error() const { return m_error; } + bool isEditable() const { return m_editable; } private: FloodFill *m_tool; @@ -99,14 +103,21 @@ class FloodFill::Task final : public ToolController::Task { int m_x; int m_y; QString m_error; + const bool m_editable; }; FloodFill::FloodFill(ToolController &owner) : Tool( - owner, FLOODFILL, QCursor(QPixmap(":cursors/bucket.png"), 2, 29), - true, true, false, false, false) + owner, FLOODFILL, + QCursor(QPixmap(QStringLiteral(":cursors/bucket.png")), 2, 29), true, + true, false, false, false) , m_kernel(int(DP_FLOOD_FILL_KERNEL_ROUND)) , m_blendMode(DP_BLEND_MODE_NORMAL) + , m_originalBlendMode(m_blendMode) + , m_bucketCursor(cursor()) + , m_pendingCursor( + QPixmap(QStringLiteral(":cursors/bucketcheck.png")), 2, 29) + , m_confirmCursor(QPixmap(QStringLiteral(":cursors/check.png"))) { } @@ -115,10 +126,13 @@ void FloodFill::begin(const BeginParams ¶ms) if(params.right) { cancelMultipart(); } else if(!m_running) { + bool shouldFill = true; if(havePending()) { + shouldFill = !m_confirmFills; flushPending(); - } else { - fillAt(params.point, m_owner.activeLayer()); + } + if(shouldFill) { + fillAt(params.point, m_owner.activeLayer(), m_editableFills); } } } @@ -137,6 +151,7 @@ bool FloodFill::isMultipart() const void FloodFill::finishMultipart() { + dispose(); flushPending(); } @@ -171,20 +186,24 @@ void FloodFill::setForegroundColor(const QColor &color) updatePendingPreview(); } -ToolState FloodFill::toolState() const -{ - return havePending() ? ToolState::AwaitingConfirmation : ToolState::Normal; -} - void FloodFill::setParameters( qreal tolerance, int expansion, int kernel, int featherRadius, int size, - qreal opacity, int gap, Source source, int blendMode, Area area) + qreal opacity, int gap, Source source, int blendMode, Area area, + bool editableFills, bool confirmFills) { bool needsUpdate = opacity != m_opacity || blendMode != m_blendMode; bool needsRefill = tolerance != m_tolerance || expansion != m_expansion || kernel != m_kernel || featherRadius != m_featherRadius || size != m_size || gap != m_gap || source != m_source || area != m_area; + m_editableFills = editableFills; + + if(confirmFills != m_confirmFills) { + m_confirmFills = confirmFills; + if(havePending()) { + setCursor(confirmFills ? m_confirmCursor : m_pendingCursor); + } + } if(needsUpdate) { m_opacity = opacity; @@ -213,13 +232,15 @@ int FloodFill::lastActiveLayerId() const : m_owner.activeLayer(); } -void FloodFill::fillAt(const QPointF &point, int activeLayerId) +void FloodFill::fillAt(const QPointF &point, int activeLayerId, bool editable) { m_repeat = false; canvas::CanvasModel *canvas = m_owner.model(); if(canvas && !m_running) { m_lastPoint = point; m_lastActiveLayerId = activeLayerId; + m_originalLayerId = activeLayerId; + m_originalBlendMode = m_blendMode; QColor fillColor = m_blendMode == DP_BLEND_MODE_ERASE ? Qt::black @@ -248,55 +269,65 @@ void FloodFill::fillAt(const QPointF &point, int activeLayerId) m_cancel = false; canvas::PaintEngine *paintEngine = canvas->paintEngine(); setHandlesRightClick(true); - emit m_owner.toolNoticeRequested( + emitFloodFillStateChanged(); + requestToolNotice( QCoreApplication::translate("FillSettings", "Filling…")); m_owner.executeAsync(new Task( this, m_cancel, paintEngine->viewCanvasState(), canvas->localUserId(), point, fillColor, m_tolerance, layerId, m_size, m_gap, m_expansion, DP_FloodFillKernel(m_kernel), m_featherRadius, m_area, paintEngine->viewMode(), - paintEngine->viewLayer(), paintEngine->viewFrame())); + paintEngine->viewLayer(), paintEngine->viewFrame(), editable)); } } void FloodFill::repeatFill() { - if(m_running) { - m_repeat = true; - m_cancel = true; - } else if(havePending()) { - fillAt(m_lastPoint, lastActiveLayerId()); + if(m_pendingEditable) { + if(m_running) { + m_repeat = true; + m_cancel = true; + } else if(havePending()) { + fillAt(m_lastPoint, lastActiveLayerId(), true); + } } } void FloodFill::floodFillFinished(Task *task) { m_running = false; - DP_FloodFillResult result = task->result(); - if(result == DP_FLOOD_FILL_SUCCESS) { - m_pendingImage = task->takeImage(); - if(m_pendingImage.isNull()) { - qWarning("Flood fill failed: image is null"); - } else { - m_pendingPos = task->pos(); - m_pendingArea = task->area(); - m_pendingColor = task->color(); + if(isActiveTool()) { + DP_FloodFillResult result = task->result(); + if(result == DP_FLOOD_FILL_SUCCESS) { + m_pendingImage = task->takeImage(); + if(m_pendingImage.isNull()) { + qWarning("Flood fill failed: image is null"); + } else { + m_pendingPos = task->pos(); + m_pendingArea = task->area(); + m_pendingColor = task->color(); + m_pendingEditable = task->isEditable(); + setCursor(m_confirmFills ? m_confirmCursor : m_pendingCursor); + } + } else if(result != DP_FLOOD_FILL_CANCELLED) { + qWarning("Flood fill failed: %s", qUtf8Printable(task->error())); + m_pendingImage = QImage(); } - } else if(result != DP_FLOOD_FILL_CANCELLED) { - qWarning("Flood fill failed: %s", qUtf8Printable(task->error())); + } else { m_pendingImage = QImage(); + m_repeat = false; } previewPending(); setHandlesRightClick(havePending()); - m_owner.refreshToolState(); + emitFloodFillStateChanged(); if(m_repeat) { - fillAt(m_lastPoint, lastActiveLayerId()); + fillAt(m_lastPoint, lastActiveLayerId(), m_pendingEditable); } } void FloodFill::updatePendingPreview() { - if(m_owner.model() && havePending()) { + if(m_owner.model() && havePending() && m_pendingEditable) { previewPending(); } } @@ -310,8 +341,12 @@ void FloodFill::previewPending() int layerId = m_owner.activeLayer(); if(layerId <= 0) { canvas->paintEngine()->clearFillPreview(); - toolNoticeText = QCoreApplication::translate( - "FillSettings", "No layer selected."); + if(m_pendingEditable) { + toolNoticeText = QCoreApplication::translate( + "FillSettings", "No layer selected."); + } else { + disposePending(); + } } else { QModelIndex layerIndex = canvas->layerlist()->layerIndex(layerId); @@ -321,61 +356,34 @@ void FloodFill::previewPending() if(layerIndex.data(canvas::LayerListModel::IsGroupRole) .toBool()) { canvas->paintEngine()->clearFillPreview(); - toolNoticeText = - QCoreApplication::translate( - "FillSettings", "Can't fill layer group %1.\n" - "Select a regular layer instead.") - .arg(layerTitle); + if(m_pendingEditable) { + toolNoticeText = QCoreApplication::translate( + "FillSettings", + "Can't fill layer group %1.\n" + "Select a regular layer instead.") + .arg(layerTitle); + } else { + disposePending(); + } } else { adjustPendingImage(false); canvas->paintEngine()->previewFill( layerId, m_blendMode, m_opacity, m_pendingPos.x(), m_pendingPos.y(), m_pendingImage); - - if(m_owner.showFillNotices()) { - QString areaText; - switch(m_pendingArea) { - case Area::Continuous: - areaText = QCoreApplication::translate( - "FillSettings", "Continuous fill"); - break; - case Area::Similar: - areaText = QCoreApplication::translate( - "FillSettings", "Similar color fill"); - break; - case Area::Selection: - areaText = QCoreApplication::translate( - "FillSettings", "Selection fill"); - break; - } - toolNoticeText = - QCoreApplication::translate( - "FillSettings", - "%1, %2 by %3 pixels.\n" - "%4 at %5% opacity on %6.\n" - "Click to apply, undo to cancel.") - .arg( - areaText, - QString::number(m_pendingImage.width()), - QString::number(m_pendingImage.height()), - canvas::blendmode::translatedName( - m_blendMode), - QString::number(qRound(m_opacity * 100.0)), - layerTitle); - } } } } else { canvas->paintEngine()->clearFillPreview(); } } - emit m_owner.toolNoticeRequested(toolNoticeText); + requestToolNotice(toolNoticeText); } void FloodFill::flushPending() { if(havePending()) { - int layerId = m_owner.activeLayer(); + int layerId = + m_pendingEditable ? m_owner.activeLayer() : m_originalLayerId; canvas::CanvasModel *canvas = m_owner.model(); bool canFill = layerId > 0 && canvas && !canvas->layerlist() @@ -383,12 +391,15 @@ void FloodFill::flushPending() .data(canvas::LayerListModel::IsGroupRole) .toBool(); if(canFill) { - adjustPendingImage(true); + if(m_pendingEditable) { + adjustPendingImage(true); + } net::Client *client = m_owner.client(); uint8_t contextId = client->myId(); net::MessageList msgs; net::makePutImageMessages( - msgs, contextId, m_owner.activeLayer(), m_blendMode, + msgs, contextId, m_owner.activeLayer(), + m_pendingEditable ? m_blendMode : m_originalBlendMode, m_pendingPos.x(), m_pendingPos.y(), m_pendingImage); if(!msgs.isEmpty()) { msgs.prepend(net::makeUndoPointMessage(contextId)); @@ -405,7 +416,8 @@ void FloodFill::disposePending() m_pendingImage = QImage(); previewPending(); setHandlesRightClick(false); - m_owner.refreshToolState(); + setCursor(m_bucketCursor); + emitFloodFillStateChanged(); } } @@ -431,4 +443,9 @@ void FloodFill::adjustPendingImage(bool adjustOpacity) } } +void FloodFill::emitFloodFillStateChanged() +{ + emit m_owner.floodFillStateChanged(m_running, havePending()); +} + } diff --git a/src/libclient/tools/floodfill.h b/src/libclient/tools/floodfill.h index b8b37423a2..fac5444695 100644 --- a/src/libclient/tools/floodfill.h +++ b/src/libclient/tools/floodfill.h @@ -37,11 +37,11 @@ class FloodFill final : public Tool { bool usesBrushColor() const override { return true; } void setActiveLayer(int layerId) override; void setForegroundColor(const QColor &color) override; - ToolState toolState() const override; void setParameters( qreal tolerance, int expansion, int kernel, int featherRadius, int size, - qreal opacity, int gap, Source source, int blendMode, Area area); + qreal opacity, int gap, Source source, int blendMode, Area area, + bool editableFills, bool confirmFills); private: class Task; @@ -49,7 +49,7 @@ class FloodFill final : public Tool { int lastActiveLayerId() const; - void fillAt(const QPointF &point, int activeLayerId); + void fillAt(const QPointF &point, int activeLayerId, bool editable); void repeatFill(); void floodFillFinished(Task *task); @@ -61,6 +61,8 @@ class FloodFill final : public Tool { void adjustPendingImage(bool adjustOpacity); + void emitFloodFillStateChanged(); + qreal m_tolerance = 0.01; int m_expansion = 0; int m_kernel; @@ -71,8 +73,11 @@ class FloodFill final : public Tool { Source m_source = Source::CurrentLayer; int m_blendMode; Area m_area = Area::Continuous; + bool m_editableFills = false; + bool m_confirmFills = false; bool m_running = false; bool m_repeat = false; + bool m_pendingEditable = false; QAtomicInt m_cancel = false; QPointF m_lastPoint; int m_lastActiveLayerId = 0; @@ -80,6 +85,11 @@ class FloodFill final : public Tool { QPoint m_pendingPos; Area m_pendingArea; QColor m_pendingColor; + int m_originalLayerId = 0; + int m_originalBlendMode; + QCursor m_bucketCursor; + QCursor m_pendingCursor; + QCursor m_confirmCursor; }; } diff --git a/src/libclient/tools/magicwand.cpp b/src/libclient/tools/magicwand.cpp index cc27616e3c..63a7be4621 100644 --- a/src/libclient/tools/magicwand.cpp +++ b/src/libclient/tools/magicwand.cpp @@ -43,7 +43,7 @@ class MagicWandTool::Task final : public ToolController::Task { m_result = m_canvasState.floodFill( 0, 0, m_point.x(), m_point.y(), fillColor, m_tolerance, m_sourceLayerId, m_size, m_continuous ? m_gap : 0, m_expansion, - m_kernel, m_featherRadius, false, m_continuous, m_viewMode, + m_kernel, m_featherRadius, false, m_continuous, false, m_viewMode, m_activeLayerId, m_activeFrameIndex, m_cancel, m_img, m_x, m_y); if(m_result != DP_FLOOD_FILL_SUCCESS && m_result != DP_FLOOD_FILL_CANCELLED) { @@ -98,9 +98,8 @@ void MagicWandTool::begin(const BeginParams ¶ms) } else if(!m_running) { if(havePending()) { flushPending(); - } else { - fillAt(params.point, params.constrain, params.center); } + fillAt(params.point, params.constrain, params.center); } } @@ -140,17 +139,11 @@ void MagicWandTool::dispose() } } -ToolState MagicWandTool::toolState() const -{ - return havePending() ? ToolState::AwaitingConfirmation : ToolState::Normal; -} - void MagicWandTool::updateParameters() { if(havePending()) { const ToolController::SelectionParams &selectionParams = m_owner.selectionParams(); - bool needsUpdate = selectionParams.opacity != m_pendingParams.opacity; bool needsRefill = selectionParams.size != m_pendingParams.size || selectionParams.tolerance != m_pendingParams.tolerance || @@ -165,8 +158,6 @@ void MagicWandTool::updateParameters() m_pendingParams = selectionParams; if(needsRefill) { repeatFill(); - } else if(needsUpdate) { - updateToolNotice(); } } } @@ -202,7 +193,7 @@ void MagicWandTool::fillAt(const QPointF &point, bool constrain, bool center) } setHandlesRightClick(true); - emit m_owner.toolNoticeRequested( + requestToolNotice( QCoreApplication::translate("MagicWandSettings", "Selecting…")); canvas::PaintEngine *paintEngine = canvas->paintEngine(); @@ -231,47 +222,34 @@ void MagicWandTool::floodFillFinished(Task *task) { m_running = false; DP_FloodFillResult result = task->result(); - QString toolNoticeText; if(result == DP_FLOOD_FILL_SUCCESS) { m_pendingImage = task->takeImage(); if(m_pendingImage.isNull()) { qWarning("Magic wand failed: image is null"); } else { m_pendingPos = task->pos(); - emit m_owner.maskPreviewRequested(m_pendingPos, m_pendingImage); + if(!EDITABLE) { + emit m_owner.maskPreviewRequested(m_pendingPos, m_pendingImage); + } } } else if(result != DP_FLOOD_FILL_CANCELLED) { qWarning("Magic wand failed: %s", qUtf8Printable(task->error())); } - updateToolNotice(); + requestToolNotice(QString()); setHandlesRightClick(havePending()); - m_owner.refreshToolState(); if(m_repeat) { fillAt(m_lastPoint, m_lastConstrain, m_lastCenter); + } else if(!EDITABLE) { + flushPending(); } } -void MagicWandTool::updateToolNotice() -{ - QString toolNoticeText; - if(havePending() && m_owner.showFillNotices()) { - toolNoticeText = - QCoreApplication::translate( - "MagicWandSettings", "%1 by %2 pixels, %3% opacity.\n" - "Click to apply, undo to cancel.") - .arg( - QString::number(m_pendingImage.width()), - QString::number(m_pendingImage.height()), - QString::number( - qRound(m_owner.selectionParams().opacity * 100.0))); - } - emit m_owner.toolNoticeRequested(toolNoticeText); -} - void MagicWandTool::flushPending() { if(havePending()) { - adjustPendingImage(); + if(EDITABLE) { + adjustPendingImage(); + } net::Client *client = m_owner.client(); uint8_t contextId = client->myId(); net::MessageList msgs; @@ -291,10 +269,11 @@ void MagicWandTool::disposePending() { if(havePending()) { m_pendingImage = QImage(); - emit m_owner.maskPreviewRequested(QPoint(), QImage()); + if(!EDITABLE) { + emit m_owner.maskPreviewRequested(QPoint(), QImage()); + } setHandlesRightClick(false); - emit m_owner.toolNoticeRequested(QString()); - m_owner.refreshToolState(); + requestToolNotice(QString()); } } diff --git a/src/libclient/tools/magicwand.h b/src/libclient/tools/magicwand.h index 7fbd9d94e0..8bb54f1ca2 100644 --- a/src/libclient/tools/magicwand.h +++ b/src/libclient/tools/magicwand.h @@ -20,11 +20,16 @@ class MagicWandTool final : public Tool { void undoMultipart() override; void cancelMultipart() override; void dispose() override; - ToolState toolState() const override; void updateParameters(); private: + // Dummy constant for potential future editable fill functionality, which + // used to previously exist, similar to how the flood fill tool works + // currently. However, this is less necessary with selections being + // local-only, so currently editable wand selections are always disabled. + static constexpr bool EDITABLE = false; + class Task; friend Task; diff --git a/src/libclient/tools/tool.cpp b/src/libclient/tools/tool.cpp index a3a26cc19c..21547a9c45 100644 --- a/src/libclient/tools/tool.cpp +++ b/src/libclient/tools/tool.cpp @@ -1,17 +1,23 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "libclient/tools/tool.h" -#include "libclient/net/client.h" #include "libclient/tools/toolcontroller.h" namespace tools { +bool Tool::isActiveTool() const +{ + return m_owner.activeTool() == m_type; +} + void Tool::setHandlesRightClick(bool handlesRightClick) { if(handlesRightClick != m_handlesRightClick) { m_handlesRightClick = handlesRightClick; - emit m_owner.toolCapabilitiesChanged( - m_allowColorPick, m_allowToolAdjust, m_handlesRightClick, - m_fractional, m_ignoresSelections); + if(isActiveTool()) { + emit m_owner.toolCapabilitiesChanged( + m_allowColorPick, m_allowToolAdjust, m_handlesRightClick, + m_fractional, m_ignoresSelections); + } } } @@ -19,7 +25,16 @@ void Tool::setCursor(const QCursor &cursor) { if(m_cursor != cursor) { m_cursor = cursor; - emit m_owner.toolCursorChanged(cursor); + if(isActiveTool()) { + emit m_owner.toolCursorChanged(cursor); + } + } +} + +void Tool::requestToolNotice(const QString &text) +{ + if(isActiveTool()) { + emit m_owner.toolNoticeRequested(text); } } diff --git a/src/libclient/tools/tool.h b/src/libclient/tools/tool.h index 829eac9317..ab5c4a9240 100644 --- a/src/libclient/tools/tool.h +++ b/src/libclient/tools/tool.h @@ -148,13 +148,11 @@ class Tool { //! Current foreground color changed virtual void setForegroundColor(const QColor &color) { Q_UNUSED(color); } - //! If this tool is operating or is waiting for a click to confirm - //! something. For example, the fill tool asks for confirmation to fill. - virtual ToolState toolState() const { return ToolState::Normal; } - protected: + bool isActiveTool() const; void setHandlesRightClick(bool handlesRightClick); void setCursor(const QCursor &cursor); + void requestToolNotice(const QString &text); ToolController &m_owner; diff --git a/src/libclient/tools/toolcontroller.cpp b/src/libclient/tools/toolcontroller.cpp index 1cc5ab93ec..efd1f9af1d 100644 --- a/src/libclient/tools/toolcontroller.cpp +++ b/src/libclient/tools/toolcontroller.cpp @@ -32,7 +32,6 @@ ToolController::ToolController(net::Client *client, QObject *parent) , m_drawing(false) , m_applyGlobalSmoothing(true) , m_mouseSmoothing(false) - , m_showFillNotices(true) , m_globalSmoothing(0) , m_interpolateInputs(false) , m_stabilizationMode(brushes::Stabilizer) @@ -135,7 +134,6 @@ void ToolController::setActiveTool(Tool::Type tool) activeToolIgnoresSelections()); emit toolCursorChanged(activeToolCursor()); emit toolNoticeRequested(QString()); - refreshToolState(); } } @@ -390,11 +388,6 @@ void ToolController::setMouseSmoothing(bool mouseSmoothing) m_mouseSmoothing = mouseSmoothing; } -void ToolController::setShowFillNotices(bool showFillNotices) -{ - m_showFillNotices = showFillNotices; -} - void ToolController::updateSmoothing() { int strength = m_globalSmoothing; @@ -580,7 +573,7 @@ void ToolController::executeAsync(Task *task) task->run(); emit asyncExecutionFinished(task); if(--m_taskCount == 0) { - emit toolStateChanged(int(m_activeTool->toolState())); + emit toolStateChanged(int(ToolState::Normal)); } })); } @@ -591,11 +584,4 @@ void ToolController::notifyAsyncExecutionFinished(Task *task) task->deleteLater(); } -void ToolController::refreshToolState() -{ - if(m_taskCount == 0) { - emit toolStateChanged(int(m_activeTool->toolState())); - } -} - } diff --git a/src/libclient/tools/toolcontroller.h b/src/libclient/tools/toolcontroller.h index 3db47cae8a..a4b3db0033 100644 --- a/src/libclient/tools/toolcontroller.h +++ b/src/libclient/tools/toolcontroller.h @@ -124,8 +124,6 @@ class ToolController final : public QObject { int globalSmoothing() const { return m_globalSmoothing; } void setMouseSmoothing(bool mouseSmoothing); - void setShowFillNotices(bool showFillNotices); - bool showFillNotices() const { return m_showFillNotices; } void setTransformParams(bool accurate, int interpolation); int transformInterpolation() const { return m_transformInterpolation; } @@ -167,8 +165,6 @@ class ToolController final : public QObject { */ void executeAsync(Task *task); - void refreshToolState(); - public slots: //! Start a new stroke void startDrawing( @@ -235,7 +231,9 @@ public slots: void showMessageRequested(const QString &message); void toolNoticeRequested(const QString &text); + void floodFillStateChanged(bool running, bool pending); void toolStateChanged(int state); + void asyncExecutionFinished(Task *task); private slots: @@ -264,7 +262,6 @@ private slots: bool m_drawing; bool m_applyGlobalSmoothing; bool m_mouseSmoothing; - bool m_showFillNotices; int m_globalSmoothing; bool m_interpolateInputs; diff --git a/src/libclient/tools/toolstate.h b/src/libclient/tools/toolstate.h index d6e48f17e5..a10532a82b 100644 --- a/src/libclient/tools/toolstate.h +++ b/src/libclient/tools/toolstate.h @@ -4,7 +4,7 @@ namespace tools { -enum class ToolState { Normal, Busy, AwaitingConfirmation }; +enum class ToolState { Normal, Busy }; }