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