Skip to content

Commit

Permalink
Allow expanding, shrinking and growing selections
Browse files Browse the repository at this point in the history
Via Selection > Expand/Shrink/Feather Selection. This opens a dialog
where you can input those parameters to manipulate an existing
selection.

Implements #1267 and #1306.
  • Loading branch information
askmeaboutlo0m committed Jul 7, 2024
1 parent ca268b3 commit 41a5256
Show file tree
Hide file tree
Showing 11 changed files with 352 additions and 1 deletion.
1 change: 1 addition & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Unreleased Version 2.2.2-pre
* Fix: Make the fill tool not feather along canvas edge. Thanks Meru for reporting.
* Server Feature: Allow specifying idleOverride and allowWeb in session templates. Thanks MorrowShore for suggesting.
* Feature: Allow shrinking fills and magic wand selections. Thanks Meru for suggesting.
* Feature: New dialog Selection > Expand/Shrink/Feather Selection that alters selections accordingly. Thanks MorrowShore for suggesting.

2024-02-25 Version 2.2.2-beta.1
* Server Feature: Allow adding a message when kicking someone through the admin API.
Expand Down
2 changes: 2 additions & 0 deletions src/desktop/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ target_sources(drawpile PRIVATE
dialogs/resetdialog.h
dialogs/resizedialog.cpp
dialogs/resizedialog.h
dialogs/selectionalterdialog.cpp
dialogs/selectionalterdialog.h
dialogs/serverlogdialog.cpp
dialogs/serverlogdialog.h
dialogs/sessionundodepthlimitdialog.cpp
Expand Down
146 changes: 146 additions & 0 deletions src/desktop/dialogs/selectionalterdialog.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "desktop/dialogs/selectionalterdialog.h"
#include "desktop/utils/widgetutils.h"
#include "desktop/widgets/groupedtoolbutton.h"
#include <QButtonGroup>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QHBoxLayout>
#include <QPushButton>
#include <QSpinBox>
#include <QVBoxLayout>

namespace dialogs {

SelectionAlterDialog::SelectionAlterDialog(QWidget *parent)
: QDialog(parent)
{
setWindowModality(Qt::WindowModal);
QVBoxLayout *layout = new QVBoxLayout(this);

QHBoxLayout *expandLayout = new QHBoxLayout;
expandLayout->setSpacing(0);

m_expandSpinner = new QSpinBox;
m_expandSpinner->setRange(0, 9999);
m_expandSpinner->setValue(getIntProperty(parent, PROP_EXPAND));
m_expandSpinner->setSuffix(tr("px"));
expandLayout->addWidget(m_expandSpinner, 1);
connect(
m_expandSpinner, QOverload<int>::of(&QSpinBox::valueChanged), this,
&SelectionAlterDialog::updateControls);

expandLayout->addSpacing(3);

widgets::GroupedToolButton *expandButton =
new widgets::GroupedToolButton(widgets::GroupedToolButton::GroupLeft);
expandButton->setIcon(QIcon::fromTheme("zoom-in"));
expandButton->setStatusTip(tr("Expand"));
expandButton->setToolTip(expandButton->statusTip());
expandButton->setCheckable(true);
expandButton->setChecked(!getBoolProperty(parent, PROP_SHRINK));
expandLayout->addWidget(expandButton);

widgets::GroupedToolButton *shrinkButton =
new widgets::GroupedToolButton(widgets::GroupedToolButton::GroupRight);
shrinkButton->setIcon(QIcon::fromTheme("zoom-out"));
shrinkButton->setStatusTip(tr("Shrink"));
shrinkButton->setToolTip(shrinkButton->statusTip());
shrinkButton->setCheckable(true);
shrinkButton->setChecked(!expandButton->isChecked());
expandLayout->addWidget(shrinkButton);

m_expandGroup = new QButtonGroup(this);
m_expandGroup->addButton(expandButton, 0);
m_expandGroup->addButton(shrinkButton, 1);
connect(
m_expandGroup,
QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked), this,
&SelectionAlterDialog::updateControls);

layout->addLayout(expandLayout);

m_featherSpinner = new QSpinBox;
m_featherSpinner->setRange(0, 999);
m_featherSpinner->setValue(getIntProperty(parent, PROP_FEATHER));
//: "Feather" is a verb here, referring to blurring the selection.
m_featherSpinner->setPrefix(tr("Feather: "));
m_featherSpinner->setSuffix(tr("px"));
layout->addWidget(m_featherSpinner);
connect(
m_featherSpinner, QOverload<int>::of(&QSpinBox::valueChanged), this,
&SelectionAlterDialog::updateControls);

m_fromEdgeBox = new QCheckBox;
m_fromEdgeBox->setChecked(getBoolProperty(parent, PROP_FROM_EDGE));
layout->addWidget(m_fromEdgeBox);

layout->addStretch();

m_buttons =
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(
m_buttons, &QDialogButtonBox::accepted, this,
&SelectionAlterDialog::accept);
connect(
m_buttons, &QDialogButtonBox::rejected, this,
&SelectionAlterDialog::reject);
layout->addWidget(m_buttons);

connect(
this, &SelectionAlterDialog::accepted, this,
&SelectionAlterDialog::emitAlterSelectionRequested);

updateControls();
resize(300, 150);
}

bool SelectionAlterDialog::getBoolProperty(QWidget *w, const char *name)
{
return w && w->property(name).toBool();
}

int SelectionAlterDialog::getIntProperty(QWidget *w, const char *name)
{
return w ? w->property(name).toInt() : 0;
}

void SelectionAlterDialog::updateControls()
{
bool shrink = m_expandGroup->checkedId() != 0;
bool haveExpand = m_expandSpinner->value() != 0;
bool haveFeather = m_featherSpinner->value() != 0;

if(shrink) {
m_expandSpinner->setPrefix(tr("Shrink: "));
//: "Feather" is a verb here, referring to blurring the selection.
m_fromEdgeBox->setText(tr("Shrink and feather from canvas edge"));
} else {
m_expandSpinner->setPrefix(tr("Expand: "));
//: "Feather" is a verb here, referring to blurring the selection.
m_fromEdgeBox->setText(tr("Feather from canvas edge"));
}

QPushButton *okButton = m_buttons->button(QDialogButtonBox::Ok);
if(okButton) {
okButton->setEnabled(haveExpand || haveFeather);
}
}

void SelectionAlterDialog::emitAlterSelectionRequested()
{
bool shrink = m_expandGroup->checkedId() != 0;
int expand = m_expandSpinner->value();
int feather = m_featherSpinner->value();
bool fromEdge = m_fromEdgeBox->isChecked();
QWidget *pw = parentWidget();
if(pw) {
pw->setProperty(PROP_SHRINK, shrink);
pw->setProperty(PROP_EXPAND, expand);
pw->setProperty(PROP_FEATHER, feather);
pw->setProperty(PROP_FROM_EDGE, fromEdge);
}
emit alterSelectionRequested(expand * (shrink ? -1 : 1), feather, fromEdge);
}

}
42 changes: 42 additions & 0 deletions src/desktop/dialogs/selectionalterdialog.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef DESKTOP_DIALOGS_SELECTIONALTERDIALOG_H
#define DESKTOP_DIALOGS_SELECTIONALTERDIALOG_H
#include <QDialog>

class QButtonGroup;
class QCheckBox;
class QDialogButtonBox;
class QSpinBox;

namespace dialogs {

class SelectionAlterDialog final : public QDialog {
Q_OBJECT
public:
SelectionAlterDialog(QWidget *parent = nullptr);

signals:
void alterSelectionRequested(int expand, int feather, bool fromEdge);

private:
static constexpr char PROP_SHRINK[] = "SelectionAlterDialog_shrink";
static constexpr char PROP_EXPAND[] = "SelectionAlterDialog_expand";
static constexpr char PROP_FEATHER[] = "SelectionAlterDialog_feather";
static constexpr char PROP_FROM_EDGE[] = "SelectionAlterDialog_fromEdge";

static bool getBoolProperty(QWidget *w, const char *name);
static int getIntProperty(QWidget *w, const char *name);

void updateControls();
void emitAlterSelectionRequested();

QButtonGroup *m_expandGroup;
QSpinBox *m_expandSpinner;
QSpinBox *m_featherSpinner;
QCheckBox *m_fromEdgeBox;
QDialogButtonBox *m_buttons;
};

}

#endif
76 changes: 75 additions & 1 deletion src/desktop/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include "desktop/dialogs/playbackdialog.h"
#include "desktop/dialogs/resetdialog.h"
#include "desktop/dialogs/resizedialog.h"
#include "desktop/dialogs/selectionalterdialog.h"
#include "desktop/dialogs/serverlogdialog.h"
#include "desktop/dialogs/sessionsettings.h"
#include "desktop/dialogs/sessionundodepthlimitdialog.h"
Expand Down Expand Up @@ -75,6 +76,7 @@
#include "libclient/utils/customshortcutmodel.h"
#include "libclient/utils/images.h"
#include "libclient/utils/logging.h"
#include "libclient/utils/selectionalteration.h"
#include "libclient/utils/shortcutdetector.h"
#include "libshared/util/networkaccess.h"
#include "libshared/util/paths.h"
Expand Down Expand Up @@ -3165,6 +3167,7 @@ void MainWindow::updateSelectTransformActions()
getAction("selectinvert")->setEnabled(haveSelection);
getAction("selectlayerbounds")->setEnabled(!haveTransform);
getAction("selectlayercontents")->setEnabled(!haveTransform);
getAction("selectalter")->setEnabled(haveSelection && !haveTransform);
getAction("fillfgarea")->setEnabled(haveSelection);
getAction("recolorarea")->setEnabled(haveSelection);
getAction("colorerasearea")->setEnabled(haveSelection);
Expand All @@ -3176,6 +3179,16 @@ void MainWindow::updateSelectTransformActions()
getAction("transformrotateccw")->setEnabled(haveTransform);
getAction("transformshrinktoview")->setEnabled(haveTransform);
m_dockToolSettings->selectionSettings()->setActionEnabled(haveSelection);

if(!haveSelection || haveTransform) {
dialogs::SelectionAlterDialog *dlg =
findChild<dialogs::SelectionAlterDialog *>(
QStringLiteral("selectionalterdialog"),
Qt::FindDirectChildrenOnly);
if(dlg) {
dlg->deleteLater();
}
}
}

// clang-format off
Expand Down Expand Up @@ -3487,6 +3500,57 @@ void MainWindow::showUserInfoDialog(int userId)
dlg->show();
}

// clang-format on
void MainWindow::showAlterSelectionDialog()
{
QString name = QStringLiteral("selectionalterdialog");
dialogs::SelectionAlterDialog *dlg =
findChild<dialogs::SelectionAlterDialog *>(
name, Qt::FindDirectChildrenOnly);
if(dlg) {
dlg->activateWindow();
dlg->raise();
} else {
dlg = new dialogs::SelectionAlterDialog(this);
dlg->setAttribute(Qt::WA_DeleteOnClose);
dlg->setWindowTitle(getAction("selectalter")->text());
connect(
dlg, &dialogs::SelectionAlterDialog::alterSelectionRequested, this,
&MainWindow::alterSelection, Qt::QueuedConnection);
utils::showWindow(dlg);
}
}

void MainWindow::alterSelection(int expand, int feather, bool fromEdge)
{
canvas::CanvasModel *canvas = m_doc->canvas();
if(canvas) {
SelectionAlteration *sa = new SelectionAlteration(
canvas->paintEngine()->viewCanvasState(), m_doc->client()->myId(),
canvas::CanvasModel::MAIN_SELECTION_ID, expand, feather, fromEdge);
sa->setAutoDelete(true);
connect(
sa, &SelectionAlteration::success, m_doc, &Document::selectMask);

QProgressDialog *progressDialog = new QProgressDialog(this);
progressDialog->setWindowModality(Qt::WindowModal);
progressDialog->setRange(0, 0);
progressDialog->setMinimumDuration(0);
progressDialog->setLabelText(tr("Altering selection…"));
connect(
sa, &SelectionAlteration::success, progressDialog,
&QProgressDialog::deleteLater);
connect(
sa, &SelectionAlteration::failure, progressDialog,
&QProgressDialog::deleteLater);
connect(
progressDialog, &QProgressDialog::canceled, sa,
&SelectionAlteration::cancel);
progressDialog->show();

QThreadPool::globalInstance()->start(sa);
}
}

void MainWindow::changeUndoDepthLimit()
{
Expand All @@ -3504,7 +3568,7 @@ void MainWindow::changeUndoDepthLimit()
}
}
}

// clang-format off

void MainWindow::updateDevToolsActions()
{
Expand Down Expand Up @@ -4387,6 +4451,11 @@ void MainWindow::setupActions()
makeAction("selectlayercontents", tr("&Layer to Selection"))
.shortcut(Qt::SHIFT | Qt::Key_L)
.icon("edit-image");
QAction *selectalter =
//: "Feather" is a verb here, referring to blurring the selection.
makeAction("selectalter", tr("&Expand/Shrink/Feather Selection"))
.noDefaultShortcut()
.icon("zoom-select");
QAction *fillfgarea = makeAction("fillfgarea", tr("Fill Selection"))
.shortcut(CTRL_KEY | Qt::Key_Comma);
QAction *recolorarea = makeAction("recolorarea", tr("Recolor Selection"))
Expand Down Expand Up @@ -4434,6 +4503,7 @@ void MainWindow::setupActions()
m_currentdoctools->addAction(selectinvert);
m_currentdoctools->addAction(selectlayerbounds);
m_currentdoctools->addAction(selectlayercontents);
m_currentdoctools->addAction(selectalter);

m_putimagetools->addAction(fillfgarea);
m_putimagetools->addAction(recolorarea);
Expand All @@ -4449,6 +4519,9 @@ void MainWindow::setupActions()
connect(
selectlayercontents, &QAction::triggered, m_doc,
&Document::selectLayerContents);
connect(
selectalter, &QAction::triggered, this,
&MainWindow::showAlterSelectionDialog);
connect(
fillfgarea, &QAction::triggered, this,
std::bind(
Expand All @@ -4475,6 +4548,7 @@ void MainWindow::setupActions()
selectMenu->addAction(selectinvert);
selectMenu->addAction(selectlayerbounds);
selectMenu->addAction(selectlayercontents);
selectMenu->addAction(selectalter);
selectMenu->addSeparator();
selectMenu->addAction(cleararea);
selectMenu->addAction(fillfgarea);
Expand Down
2 changes: 2 additions & 0 deletions src/desktop/mainwindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ private slots:
void showLayoutsDialog();
void showUserInfoDialog(int userId);

void showAlterSelectionDialog();
void alterSelection(int expand, int feather, bool fromEdge);
void changeUndoDepthLimit();

void updateDevToolsActions();
Expand Down
2 changes: 2 additions & 0 deletions src/libclient/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ target_sources(dpclient PRIVATE
utils/listservermodel.h
utils/logging.cpp
utils/logging.h
utils/selectionalteration.cpp
utils/selectionalteration.h
utils/selectionoutlinegenerator.cpp
utils/selectionoutlinegenerator.h
utils/sessionfilterproxymodel.cpp
Expand Down
11 changes: 11 additions & 0 deletions src/libclient/document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,17 @@ void Document::selectLayerContents()
selectLayer(true);
}

void Document::selectMask(const QImage &img, int x, int y)
{
if(img.isNull()) {
selectNone();
} else {
selectOp(
DP_MSG_SELECTION_PUT_OP_REPLACE,
QRect(x, y, img.width(), img.height()), img);
}
}

void Document::selectLayer(bool includeMask)
{
if(m_canvas) {
Expand Down
1 change: 1 addition & 0 deletions src/libclient/document.h
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ public slots:
void selectInvert();
void selectLayerBounds();
void selectLayerContents();
void selectMask(const QImage &img, int x, int y);

void copyVisible();
void copyMerged();
Expand Down
Loading

0 comments on commit 41a5256

Please sign in to comment.