1 /*
2 Scan Tailor - Interactive post-processing tool for scanned pages.
3 Copyright (C) Joseph Artsimovich <joseph.artsimovich@gmail.com>
4
5 This program is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "ZoneContextMenuInteraction.h"
20 #include <QDebug>
21 #include <QMenu>
22 #include <QMessageBox>
23 #include <QPainter>
24 #include <QPainterPath>
25 #include <QSignalMapper>
26 #include <boost/bind.hpp>
27 #include "ImageViewBase.h"
28 #include "QtSignalForwarder.h"
29 #include "ZoneInteractionContext.h"
30
31 class ZoneContextMenuInteraction::OrderByArea {
32 public:
operator ()(const EditableZoneSet::Zone & lhs,const EditableZoneSet::Zone & rhs) const33 bool operator()(const EditableZoneSet::Zone& lhs, const EditableZoneSet::Zone& rhs) const {
34 const QRectF lhs_bbox(lhs.spline()->toPolygon().boundingRect());
35 const QRectF rhs_bbox(rhs.spline()->toPolygon().boundingRect());
36 const qreal lhs_area = lhs_bbox.width() * lhs_bbox.height();
37 const qreal rhs_area = rhs_bbox.width() * rhs_bbox.height();
38
39 return lhs_area < rhs_area;
40 }
41 };
42
43
create(ZoneInteractionContext & context,InteractionState & interaction)44 ZoneContextMenuInteraction* ZoneContextMenuInteraction::create(ZoneInteractionContext& context,
45 InteractionState& interaction) {
46 return create(context, interaction, boost::bind(&ZoneContextMenuInteraction::defaultMenuCustomizer, _1, _2));
47 }
48
create(ZoneInteractionContext & context,InteractionState & interaction,const MenuCustomizer & menu_customizer)49 ZoneContextMenuInteraction* ZoneContextMenuInteraction::create(ZoneInteractionContext& context,
50 InteractionState& interaction,
51 const MenuCustomizer& menu_customizer) {
52 std::vector<Zone> selectable_zones(zonesUnderMouse(context));
53
54 if (selectable_zones.empty()) {
55 return nullptr;
56 } else {
57 return new ZoneContextMenuInteraction(context, interaction, menu_customizer, selectable_zones);
58 }
59 }
60
zonesUnderMouse(ZoneInteractionContext & context)61 std::vector<ZoneContextMenuInteraction::Zone> ZoneContextMenuInteraction::zonesUnderMouse(
62 ZoneInteractionContext& context) {
63 const QTransform from_screen(context.imageView().widgetToImage());
64 const QPointF image_mouse_pos(from_screen.map(context.imageView().mapFromGlobal(QCursor::pos()) + QPointF(0.5, 0.5)));
65
66 // Find zones containing the mouse position.
67 std::vector<Zone> selectable_zones;
68 for (const EditableZoneSet::Zone& zone : context.zones()) {
69 QPainterPath path;
70 path.setFillRule(Qt::WindingFill);
71 path.addPolygon(zone.spline()->toPolygon());
72 if (path.contains(image_mouse_pos)) {
73 selectable_zones.emplace_back(zone);
74 }
75 }
76
77 return selectable_zones;
78 }
79
ZoneContextMenuInteraction(ZoneInteractionContext & context,InteractionState & interaction,const MenuCustomizer & menu_customizer,std::vector<Zone> & selectable_zones)80 ZoneContextMenuInteraction::ZoneContextMenuInteraction(ZoneInteractionContext& context,
81 InteractionState& interaction,
82 const MenuCustomizer& menu_customizer,
83 std::vector<Zone>& selectable_zones)
84 : m_context(context),
85 m_menu(new QMenu(&context.imageView())),
86 m_highlightedZoneIdx(-1),
87 m_menuItemTriggered(false) {
88 #ifdef Q_OS_MAC
89 m_extraDelaysDone = 0;
90 #endif
91
92 m_selectableZones.swap(selectable_zones);
93 std::sort(m_selectableZones.begin(), m_selectableZones.end(), OrderByArea());
94
95 interaction.capture(m_interaction);
96
97 int h = 20;
98 const int h_step = 65;
99 const int s = 255 * 64 / 100;
100 const int v = 255 * 96 / 100;
101 const int alpha = 150;
102 QColor color;
103
104 auto* hover_map = new QSignalMapper(this);
105 connect(hover_map, SIGNAL(mapped(int)), SLOT(highlightItem(int)));
106
107 QPixmap pixmap;
108
109 auto it(m_selectableZones.begin());
110 const auto end(m_selectableZones.end());
111 for (int i = 0; it != end; ++it, ++i, h = (h + h_step) % 360) {
112 color.setHsv(h, s, v, alpha);
113 it->color = color.toRgb();
114
115 if (m_selectableZones.size() > 1) {
116 pixmap = QPixmap(16, 16);
117 color.setAlpha(255);
118 pixmap.fill(color);
119 }
120
121 const StandardMenuItems std_items(propertiesMenuItemFor(*it), deleteMenuItemFor(*it));
122
123 for (const ZoneContextMenuItem& item : menu_customizer(*it, std_items)) {
124 QAction* action = m_menu->addAction(pixmap, item.label());
125 new QtSignalForwarder(
126 action, SIGNAL(triggered()),
127 boost::bind(&ZoneContextMenuInteraction::menuItemTriggered, this, boost::ref(interaction), item.callback()));
128
129 hover_map->setMapping(action, i);
130 connect(action, SIGNAL(hovered()), hover_map, SLOT(map()));
131 }
132
133 m_menu->addSeparator();
134 }
135 // The queued connection is used to ensure it gets called *after*
136 // QAction::triggered().
137 connect(m_menu.get(), SIGNAL(aboutToHide()), SLOT(menuAboutToHide()), Qt::QueuedConnection);
138
139 highlightItem(0);
140 m_menu->popup(QCursor::pos());
141 }
142
143 ZoneContextMenuInteraction::~ZoneContextMenuInteraction() = default;
144
onPaint(QPainter & painter,const InteractionState &)145 void ZoneContextMenuInteraction::onPaint(QPainter& painter, const InteractionState&) {
146 painter.setWorldMatrixEnabled(false);
147 painter.setRenderHint(QPainter::Antialiasing);
148
149 if (m_highlightedZoneIdx >= 0) {
150 const QTransform to_screen(m_context.imageView().imageToWidget());
151 const Zone& zone = m_selectableZones[m_highlightedZoneIdx];
152 m_visualizer.drawSpline(painter, to_screen, zone.spline());
153 }
154 }
155
menuAboutToHide()156 void ZoneContextMenuInteraction::menuAboutToHide() {
157 if (m_menuItemTriggered) {
158 return;
159 }
160
161 #ifdef Q_OS_MAC
162 // On OSX, QAction::triggered() is emitted significantly (like 150ms)
163 // later than QMenu::aboutToHide(). This makes it generally not possible
164 // to tell whether the menu was just dismissed or a menu item was clicked.
165 // The only way to tell is to check back later, which we do here.
166 if (m_extraDelaysDone++ < 1) {
167 QTimer::singleShot(200, this, SLOT(menuAboutToHide()));
168
169 return;
170 }
171 #endif
172
173 InteractionHandler* next_handler = m_context.createDefaultInteraction();
174 if (next_handler) {
175 makePeerPreceeder(*next_handler);
176 }
177
178 unlink();
179 m_context.imageView().update();
180 deleteLater();
181 }
182
menuItemTriggered(InteractionState & interaction,const ZoneContextMenuItem::Callback & callback)183 void ZoneContextMenuInteraction::menuItemTriggered(InteractionState& interaction,
184 const ZoneContextMenuItem::Callback& callback) {
185 m_menuItemTriggered = true;
186 m_visualizer.switchToStrokeMode();
187
188 InteractionHandler* next_handler = callback(interaction);
189 if (next_handler) {
190 makePeerPreceeder(*next_handler);
191 }
192
193 unlink();
194 m_context.imageView().update();
195 deleteLater();
196 }
197
propertiesRequest(const EditableZoneSet::Zone & zone)198 InteractionHandler* ZoneContextMenuInteraction::propertiesRequest(const EditableZoneSet::Zone& zone) {
199 m_context.showPropertiesCommand(zone);
200
201 return m_context.createDefaultInteraction();
202 }
203
deleteRequest(const EditableZoneSet::Zone & zone)204 InteractionHandler* ZoneContextMenuInteraction::deleteRequest(const EditableZoneSet::Zone& zone) {
205 m_context.zones().removeZone(zone.spline());
206 m_context.zones().commit();
207
208 return m_context.createDefaultInteraction();
209 }
210
deleteMenuItemFor(const EditableZoneSet::Zone & zone)211 ZoneContextMenuItem ZoneContextMenuInteraction::deleteMenuItemFor(const EditableZoneSet::Zone& zone) {
212 return ZoneContextMenuItem(tr("Delete"), boost::bind(&ZoneContextMenuInteraction::deleteRequest, this, zone));
213 }
214
propertiesMenuItemFor(const EditableZoneSet::Zone & zone)215 ZoneContextMenuItem ZoneContextMenuInteraction::propertiesMenuItemFor(const EditableZoneSet::Zone& zone) {
216 return ZoneContextMenuItem(tr("Properties"), boost::bind(&ZoneContextMenuInteraction::propertiesRequest, this, zone));
217 }
218
highlightItem(const int zone_idx)219 void ZoneContextMenuInteraction::highlightItem(const int zone_idx) {
220 if (m_selectableZones.size() > 1) {
221 m_visualizer.switchToFillMode(m_selectableZones[zone_idx].color);
222 } else {
223 m_visualizer.switchToStrokeMode();
224 }
225 m_highlightedZoneIdx = zone_idx;
226 m_context.imageView().update();
227 }
228
defaultMenuCustomizer(const EditableZoneSet::Zone & zone,const StandardMenuItems & std_items)229 std::vector<ZoneContextMenuItem> ZoneContextMenuInteraction::defaultMenuCustomizer(const EditableZoneSet::Zone& zone,
230 const StandardMenuItems& std_items) {
231 std::vector<ZoneContextMenuItem> items;
232 items.reserve(2);
233 items.push_back(std_items.propertiesItem);
234 items.push_back(std_items.deleteItem);
235
236 return items;
237 }
238
239 /*========================== StandardMenuItem =========================*/
240
StandardMenuItems(const ZoneContextMenuItem & properties_item,const ZoneContextMenuItem & delete_item)241 ZoneContextMenuInteraction::StandardMenuItems::StandardMenuItems(const ZoneContextMenuItem& properties_item,
242 const ZoneContextMenuItem& delete_item)
243 : propertiesItem(properties_item), deleteItem(delete_item) {}
244
245 /*============================= Visualizer ============================*/
246
switchToFillMode(const QColor & color)247 void ZoneContextMenuInteraction::Visualizer::switchToFillMode(const QColor& color) {
248 m_color = color;
249 }
250
switchToStrokeMode()251 void ZoneContextMenuInteraction::Visualizer::switchToStrokeMode() {
252 m_color = QColor();
253 }
254
prepareForSpline(QPainter & painter,const EditableSpline::Ptr & spline)255 void ZoneContextMenuInteraction::Visualizer::prepareForSpline(QPainter& painter, const EditableSpline::Ptr& spline) {
256 BasicSplineVisualizer::prepareForSpline(painter, spline);
257 if (m_color.isValid()) {
258 painter.setBrush(m_color);
259 }
260 }
261