1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of the Qt Designer of the Qt Toolkit.
7 **
8 ** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9 ** Commercial License Usage
10 ** Licensees holding valid commercial Qt licenses may use this file in
11 ** accordance with the commercial license agreement provided with the
12 ** Software or, alternatively, in accordance with the terms contained in
13 ** a written agreement between you and The Qt Company. For licensing terms
14 ** and conditions see https://www.qt.io/terms-conditions. For further
15 ** information use the contact form at https://www.qt.io/contact-us.
16 **
17 ** GNU General Public License Usage
18 ** Alternatively, this file may be used under the terms of the GNU
19 ** General Public License version 3 as published by the Free Software
20 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21 ** included in the packaging of this file. Please review the following
22 ** information to ensure the GNU General Public License requirements will
23 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24 **
25 ** $QT_END_LICENSE$
26 **
27 ****************************************************************************/
28 
29 #include "buddyeditor.h"
30 
31 #include <QtDesigner/abstractformwindow.h>
32 #include <QtDesigner/propertysheet.h>
33 #include <QtDesigner/abstractformeditor.h>
34 #include <QtDesigner/qextensionmanager.h>
35 
36 #include <qdesigner_command_p.h>
37 #include <qdesigner_propertycommand_p.h>
38 #include <qdesigner_utils_p.h>
39 #include <qlayout_widget_p.h>
40 #include <connectionedit_p.h>
41 #include <metadatabase_p.h>
42 
43 #include <QtCore/qdebug.h>
44 #include <QtCore/qvector.h>
45 #include <QtWidgets/qlabel.h>
46 #include <QtWidgets/qmenu.h>
47 #include <QtWidgets/qaction.h>
48 #include <QtWidgets/qapplication.h>
49 
50 #include <algorithm>
51 
52 QT_BEGIN_NAMESPACE
53 
54 static const char *buddyPropertyC = "buddy";
55 
canBeBuddy(QWidget * w,QDesignerFormWindowInterface * form)56 static bool canBeBuddy(QWidget *w, QDesignerFormWindowInterface *form)
57 {
58     if (qobject_cast<const QLayoutWidget*>(w) || qobject_cast<const QLabel*>(w))
59         return false;
60     if (w == form->mainContainer() || w->isHidden() )
61         return false;
62 
63     QExtensionManager *ext = form->core()->extensionManager();
64     if (QDesignerPropertySheetExtension *sheet = qt_extension<QDesignerPropertySheetExtension*>(ext, w)) {
65         const int index = sheet->indexOf(QStringLiteral("focusPolicy"));
66         if (index != -1) {
67             bool ok = false;
68             const Qt::FocusPolicy q = static_cast<Qt::FocusPolicy>(qdesigner_internal::Utils::valueOf(sheet->property(index), &ok));
69             // Refuse No-focus unless the widget is promoted.
70             return (ok && q != Qt::NoFocus) || qdesigner_internal::isPromoted(form->core(), w);
71         }
72     }
73     return false;
74 }
75 
buddy(QLabel * label,QDesignerFormEditorInterface * core)76 static QString buddy(QLabel *label, QDesignerFormEditorInterface *core)
77 {
78     QDesignerPropertySheetExtension *sheet = qt_extension<QDesignerPropertySheetExtension*>(core->extensionManager(), label);
79     if (sheet == nullptr)
80         return QString();
81     const int prop_idx = sheet->indexOf(QLatin1String(buddyPropertyC));
82     if (prop_idx == -1)
83         return QString();
84     return sheet->property(prop_idx).toString();
85 }
86 
87 namespace qdesigner_internal {
88 
89 /*******************************************************************************
90 ** BuddyEditor
91 */
92 
BuddyEditor(QDesignerFormWindowInterface * form,QWidget * parent)93 BuddyEditor::BuddyEditor(QDesignerFormWindowInterface *form, QWidget *parent) :
94     ConnectionEdit(parent, form),
95     m_formWindow(form),
96     m_updating(false)
97 {
98 }
99 
100 
widgetAt(const QPoint & pos) const101 QWidget *BuddyEditor::widgetAt(const QPoint &pos) const
102 {
103     QWidget *w = ConnectionEdit::widgetAt(pos);
104 
105     while (w != nullptr && !m_formWindow->isManaged(w))
106         w = w->parentWidget();
107     if (!w)
108         return w;
109 
110     if (state() == Editing) {
111         QLabel *label = qobject_cast<QLabel*>(w);
112         if (label == nullptr)
113             return nullptr;
114         const int cnt = connectionCount();
115         for (int i = 0; i < cnt; ++i) {
116             Connection *con = connection(i);
117             if (con->widget(EndPoint::Source) == w)
118                 return nullptr;
119         }
120     } else {
121         if (!canBeBuddy(w, m_formWindow))
122             return nullptr;
123     }
124 
125     return w;
126 }
127 
createConnection(QWidget * source,QWidget * destination)128 Connection *BuddyEditor::createConnection(QWidget *source, QWidget *destination)
129 {
130     return new Connection(this, source, destination);
131 }
132 
formWindow() const133 QDesignerFormWindowInterface *BuddyEditor::formWindow() const
134 {
135     return m_formWindow;
136 }
137 
updateBackground()138 void BuddyEditor::updateBackground()
139 {
140     if (m_updating || background() == nullptr)
141         return;
142     ConnectionEdit::updateBackground();
143 
144     m_updating = true;
145     QVector<Connection *> newList;
146     const auto label_list = background()->findChildren<QLabel*>();
147     for (QLabel *label : label_list) {
148         const QString buddy_name = buddy(label, m_formWindow->core());
149         if (buddy_name.isEmpty())
150             continue;
151 
152         const QWidgetList targets = background()->findChildren<QWidget*>(buddy_name);
153         if (targets.isEmpty())
154             continue;
155 
156         const auto wit = std::find_if(targets.cbegin(), targets.cend(),
157                                       [] (const QWidget *w) { return !w->isHidden(); });
158         if (wit == targets.cend())
159             continue;
160 
161         Connection *con = new Connection(this);
162         con->setEndPoint(EndPoint::Source, label, widgetRect(label).center());
163         con->setEndPoint(EndPoint::Target, *wit, widgetRect(*wit).center());
164         newList.append(con);
165     }
166 
167     QVector<Connection *> toRemove;
168 
169     const int c = connectionCount();
170     for (int i = 0; i < c; i++) {
171         Connection *con = connection(i);
172         QObject *source = con->object(EndPoint::Source);
173         QObject *target = con->object(EndPoint::Target);
174         const bool found =
175             std::any_of(newList.cbegin(), newList.cend(),
176                         [source, target] (const Connection *nc)
177                         { return nc->object(EndPoint::Source) == source && nc->object(EndPoint::Target) == target; });
178         if (!found)
179             toRemove.append(con);
180     }
181     if (!toRemove.isEmpty()) {
182         DeleteConnectionsCommand command(this, toRemove);
183         command.redo();
184         for (Connection *con : qAsConst(toRemove))
185             delete takeConnection(con);
186     }
187 
188     for (Connection *newConn : qAsConst(newList)) {
189         bool found = false;
190         const int c = connectionCount();
191         for (int i = 0; i < c; i++) {
192             Connection *con = connection(i);
193             if (con->object(EndPoint::Source) == newConn->object(EndPoint::Source) &&
194                             con->object(EndPoint::Target) == newConn->object(EndPoint::Target)) {
195                 found = true;
196                 break;
197             }
198         }
199         if (found) {
200             delete newConn;
201         } else {
202             AddConnectionCommand command(this, newConn);
203             command.redo();
204         }
205     }
206     m_updating = false;
207 }
208 
setBackground(QWidget * background)209 void BuddyEditor::setBackground(QWidget *background)
210 {
211     clear();
212     ConnectionEdit::setBackground(background);
213 
214     const auto label_list = background->findChildren<QLabel*>();
215     for (QLabel *label : label_list) {
216         const QString buddy_name = buddy(label, m_formWindow->core());
217         if (buddy_name.isEmpty())
218             continue;
219         QWidget *target = background->findChild<QWidget*>(buddy_name);
220         if (target == nullptr)
221             continue;
222 
223         Connection *con = new Connection(this);
224         con->setEndPoint(EndPoint::Source, label, widgetRect(label).center());
225         con->setEndPoint(EndPoint::Target, target, widgetRect(target).center());
226         addConnection(con);
227     }
228 }
229 
createBuddyCommand(QDesignerFormWindowInterface * fw,QLabel * label,QWidget * buddy)230 static QUndoCommand *createBuddyCommand(QDesignerFormWindowInterface *fw, QLabel *label, QWidget *buddy)
231 {
232     SetPropertyCommand *command = new SetPropertyCommand(fw);
233     command->init(label, QLatin1String(buddyPropertyC), buddy->objectName());
234     command->setText(BuddyEditor::tr("Add buddy"));
235     return command;
236 }
237 
endConnection(QWidget * target,const QPoint & pos)238 void BuddyEditor::endConnection(QWidget *target, const QPoint &pos)
239 {
240     Connection *tmp_con = newlyAddedConnection();
241     Q_ASSERT(tmp_con != nullptr);
242 
243     tmp_con->setEndPoint(EndPoint::Target, target, pos);
244 
245     QWidget *source = tmp_con->widget(EndPoint::Source);
246     Q_ASSERT(source != nullptr);
247     Q_ASSERT(target != nullptr);
248     setEnabled(false);
249     Connection *new_con = createConnection(source, target);
250     setEnabled(true);
251     if (new_con != nullptr) {
252         new_con->setEndPoint(EndPoint::Source, source, tmp_con->endPointPos(EndPoint::Source));
253         new_con->setEndPoint(EndPoint::Target, target, tmp_con->endPointPos(EndPoint::Target));
254 
255         selectNone();
256         addConnection(new_con);
257         QLabel *source = qobject_cast<QLabel*>(new_con->widget(EndPoint::Source));
258         QWidget *target = new_con->widget(EndPoint::Target);
259         if (source) {
260             undoStack()->push(createBuddyCommand(m_formWindow, source, target));
261         } else {
262             qDebug("BuddyEditor::endConnection(): not a label");
263         }
264         setSelected(new_con, true);
265     }
266 
267     clearNewlyAddedConnection();
268     findObjectsUnderMouse(mapFromGlobal(QCursor::pos()));
269 }
270 
widgetRemoved(QWidget * widget)271 void BuddyEditor::widgetRemoved(QWidget *widget)
272 {
273     QWidgetList child_list = widget->findChildren<QWidget*>();
274     child_list.prepend(widget);
275 
276     ConnectionSet remove_set;
277     for (QWidget *w : qAsConst(child_list)) {
278         const ConnectionList &cl = connectionList();
279         for (Connection *con : cl) {
280             if (con->widget(EndPoint::Source) == w || con->widget(EndPoint::Target) == w)
281                 remove_set.insert(con, con);
282         }
283     }
284 
285     if (!remove_set.isEmpty()) {
286         undoStack()->beginMacro(tr("Remove buddies"));
287         for (Connection *con : qAsConst(remove_set)) {
288             setSelected(con, false);
289             con->update();
290             QWidget *source = con->widget(EndPoint::Source);
291             if (qobject_cast<QLabel*>(source) == 0) {
292                 qDebug("BuddyConnection::widgetRemoved(): not a label");
293             } else {
294                 ResetPropertyCommand *command = new ResetPropertyCommand(formWindow());
295                 command->init(source, QLatin1String(buddyPropertyC));
296                 undoStack()->push(command);
297             }
298             delete takeConnection(con);
299         }
300         undoStack()->endMacro();
301     }
302 }
303 
deleteSelected()304 void BuddyEditor::deleteSelected()
305 {
306     const ConnectionSet selectedConnections = selection(); // want copy for unselect
307     if (selectedConnections.isEmpty())
308         return;
309 
310     undoStack()->beginMacro(tr("Remove %n buddies", nullptr, selectedConnections.size()));
311     for (Connection *con : selectedConnections) {
312         setSelected(con, false);
313         con->update();
314         QWidget *source = con->widget(EndPoint::Source);
315         if (qobject_cast<QLabel*>(source) == 0) {
316             qDebug("BuddyConnection::deleteSelected(): not a label");
317         } else {
318             ResetPropertyCommand *command = new ResetPropertyCommand(formWindow());
319             command->init(source, QLatin1String(buddyPropertyC));
320             undoStack()->push(command);
321         }
322         delete takeConnection(con);
323     }
324     undoStack()->endMacro();
325 }
326 
autoBuddy()327 void BuddyEditor::autoBuddy()
328 {
329     // Any labels?
330     auto labelList = background()->findChildren<QLabel*>();
331     if (labelList.isEmpty())
332         return;
333     // Find already used buddies
334     QWidgetList usedBuddies;
335     const ConnectionList &beforeConnections = connectionList();
336     for (const Connection *c : beforeConnections)
337         usedBuddies.push_back(c->widget(EndPoint::Target));
338     // Find potential new buddies, keep lists in sync
339     QWidgetList buddies;
340     for (auto it = labelList.begin(); it != labelList.end(); ) {
341         QLabel *label = *it;
342         QWidget *newBuddy = nullptr;
343         if (m_formWindow->isManaged(label)) {
344             const QString buddy_name = buddy(label, m_formWindow->core());
345             if (buddy_name.isEmpty())
346                 newBuddy = findBuddy(label, usedBuddies);
347         }
348         if (newBuddy) {
349             buddies.push_back(newBuddy);
350             usedBuddies.push_back(newBuddy);
351             ++it;
352         } else {
353             it = labelList.erase(it);
354         }
355     }
356     // Add the list in one go.
357     if (labelList.isEmpty())
358         return;
359     const int count = labelList.size();
360     Q_ASSERT(count == buddies.size());
361     undoStack()->beginMacro(tr("Add %n buddies", nullptr, count));
362     for (int i = 0; i < count; i++)
363         undoStack()->push(createBuddyCommand(m_formWindow, labelList.at(i), buddies.at(i)));
364     undoStack()->endMacro();
365     // Now select all new ones
366     const ConnectionList &connections = connectionList();
367     for (Connection *con : connections)
368         setSelected(con, buddies.contains(con->widget(EndPoint::Target)));
369 }
370 
371 // Geometrically find  a potential buddy for label by checking neighbouring children of parent
findBuddy(QLabel * l,const QWidgetList & existingBuddies) const372 QWidget *BuddyEditor::findBuddy(QLabel *l, const QWidgetList &existingBuddies) const
373 {
374     enum { DeltaX = 5 };
375     const QWidget *parent = l->parentWidget();
376     // Try to find next managed neighbour on horizontal line
377     const QRect geom = l->geometry();
378     const int y = geom.center().y();
379     QWidget *neighbour = nullptr;
380     switch (l->layoutDirection()) {
381     case Qt::LayoutDirectionAuto:
382     case Qt::LeftToRight: { // Walk right to find next managed neighbour
383         const int xEnd = parent->size().width();
384         for (int x = geom.right() + 1; x < xEnd; x += DeltaX)
385             if (QWidget *c = parent->childAt (x, y))
386                 if (m_formWindow->isManaged(c)) {
387                     neighbour = c;
388                     break;
389                 }
390     }
391         break;
392     case Qt::RightToLeft:  // Walk left to find next managed neighbour
393         for (int x = geom.x() - 1; x >= 0; x -= DeltaX)
394             if (QWidget *c = parent->childAt (x, y))
395                 if (m_formWindow->isManaged(c)) {
396                     neighbour = c;
397                     break;
398                 }
399         break;
400     }
401     if (neighbour && !existingBuddies.contains(neighbour) && canBeBuddy(neighbour, m_formWindow))
402         return neighbour;
403 
404     return nullptr;
405 }
406 
createContextMenu(QMenu & menu)407 void BuddyEditor::createContextMenu(QMenu &menu)
408 {
409     QAction *autoAction = menu.addAction(tr("Set automatically"));
410     connect(autoAction, &QAction::triggered, this, &BuddyEditor::autoBuddy);
411     menu.addSeparator();
412     ConnectionEdit::createContextMenu(menu);
413 }
414 
415 }
416 
417 QT_END_NAMESPACE
418