1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 
26 #include "submiteditorwidget.h"
27 #include "submitfieldwidget.h"
28 #include "submitfilemodel.h"
29 #include "ui_submiteditorwidget.h"
30 
31 #include <utils/algorithm.h>
32 #include <utils/theme/theme.h>
33 
34 #include <QDebug>
35 #include <QPointer>
36 #include <QTextBlock>
37 #include <QTimer>
38 #include <QScopedPointer>
39 
40 #include <QMenu>
41 #include <QHBoxLayout>
42 #include <QToolButton>
43 #include <QSpacerItem>
44 #include <QShortcut>
45 
46 using namespace Utils;
47 
48 enum { debug = 0 };
49 enum { defaultLineWidth = 72 };
50 
51 /*!
52     \class VcsBase::SubmitEditorWidget
53 
54     \brief The SubmitEditorWidget class presents a VCS commit message in a text
55     editor and a
56      checkable list of modified files in a list window.
57 
58     The user can delete files from the list by unchecking them or diff the selection
59     by doubleclicking. A list model which contains state and file columns should be
60     set using setFileModel().
61 
62     Additionally, standard creator actions  can be registered:
63     Undo/redo will be set up to work with the description editor.
64     Submit will be set up to be enabled according to checkstate.
65     Diff will be set up to trigger diffSelected().
66 
67     Note that the actions are connected by signals; in the rare event that there
68     are several instances of the SubmitEditorWidget belonging to the same
69     context active, the actions must be registered/unregistered in the editor
70     change event.
71     Care should be taken to ensure the widget is deleted properly when the
72     editor closes.
73 */
74 
75 namespace VcsBase {
76 
77 // QActionPushButton: A push button tied to an action
78 // (similar to a QToolButton)
79 class QActionPushButton : public QToolButton
80 {
81     Q_OBJECT
82 public:
83     explicit QActionPushButton(QAction *a);
84 
85 private slots:
86     void actionChanged();
87 };
88 
QActionPushButton(QAction * a)89 QActionPushButton::QActionPushButton(QAction *a) :
90      QToolButton()
91 {
92     setIcon(a->icon());
93     setText(a->text());
94     setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
95     connect(a, &QAction::changed, this, &QActionPushButton::actionChanged);
96     connect(this, &QAbstractButton::clicked, a, &QAction::trigger);
97     setEnabled(a->isEnabled());
98 }
99 
actionChanged()100 void QActionPushButton::actionChanged()
101 {
102     if (const QAction *a = qobject_cast<QAction*>(sender())) {
103         setEnabled(a->isEnabled());
104         setText(a->text());
105     }
106 }
107 
108 
109 // Helpers to retrieve model data
110 // Convenience to extract a list of selected indexes
111 // -----------  SubmitEditorWidgetPrivate
112 
113 struct SubmitEditorWidgetPrivate
114 {
115     // A pair of position/action to extend context menus
116     typedef QPair<int, QPointer<QAction> > AdditionalContextMenuAction;
117 
118     Ui::SubmitEditorWidget m_ui;
119 
120     QList<AdditionalContextMenuAction> descriptionEditContextMenuActions;
121     QVBoxLayout *m_fieldLayout = nullptr;
122     QList<SubmitFieldWidget *> m_fieldWidgets;
123     QShortcut *m_submitShortcut = nullptr;
124     QActionPushButton *m_submitButton = nullptr;
125     QString m_description;
126 
127     int m_lineWidth = defaultLineWidth;
128     int m_activatedRow = -1;
129 
130     bool m_filesSelected = false;
131     bool m_emptyFileListEnabled = false;
132     bool m_commitEnabled = false;
133     bool m_ignoreChange = false;
134     bool m_descriptionMandatory = true;
135     bool m_updateInProgress = false;
136 };
137 
SubmitEditorWidget()138 SubmitEditorWidget::SubmitEditorWidget() :
139     d(new SubmitEditorWidgetPrivate)
140 {
141     d->m_ui.setupUi(this);
142     d->m_ui.description->setContextMenuPolicy(Qt::CustomContextMenu);
143     d->m_ui.description->setLineWrapMode(QTextEdit::NoWrap);
144     d->m_ui.description->setWordWrapMode(QTextOption::WordWrap);
145     connect(d->m_ui.description, &QWidget::customContextMenuRequested,
146             this, &SubmitEditorWidget::editorCustomContextMenuRequested);
147     connect(d->m_ui.description, &QTextEdit::textChanged,
148             this, &SubmitEditorWidget::descriptionTextChanged);
149 
150     // File List
151     d->m_ui.fileView->setContextMenuPolicy(Qt::CustomContextMenu);
152     connect(d->m_ui.fileView, &QWidget::customContextMenuRequested,
153             this, &SubmitEditorWidget::fileListCustomContextMenuRequested);
154     d->m_ui.fileView->setSelectionMode(QAbstractItemView::ExtendedSelection);
155     d->m_ui.fileView->setRootIsDecorated(false);
156     connect(d->m_ui.fileView, &QAbstractItemView::doubleClicked,
157             this, &SubmitEditorWidget::diffActivated);
158 
159     connect(d->m_ui.checkAllCheckBox, &QCheckBox::stateChanged,
160             this, &SubmitEditorWidget::checkAllToggled);
161 
162     setFocusPolicy(Qt::StrongFocus);
163     setFocusProxy(d->m_ui.description);
164 }
165 
~SubmitEditorWidget()166 SubmitEditorWidget::~SubmitEditorWidget()
167 {
168     delete d;
169 }
170 
registerActions(QAction * editorUndoAction,QAction * editorRedoAction,QAction * submitAction,QAction * diffAction)171 void SubmitEditorWidget::registerActions(QAction *editorUndoAction, QAction *editorRedoAction,
172                          QAction *submitAction, QAction *diffAction)
173 {
174     if (editorUndoAction) {
175         editorUndoAction->setEnabled(d->m_ui.description->document()->isUndoAvailable());
176         connect(d->m_ui.description, &QTextEdit::undoAvailable,
177                 editorUndoAction, &QAction::setEnabled);
178         connect(editorUndoAction, &QAction::triggered, d->m_ui.description, &QTextEdit::undo);
179     }
180     if (editorRedoAction) {
181         editorRedoAction->setEnabled(d->m_ui.description->document()->isRedoAvailable());
182         connect(d->m_ui.description, &QTextEdit::redoAvailable,
183                 editorRedoAction, &QAction::setEnabled);
184         connect(editorRedoAction, &QAction::triggered, d->m_ui.description, &QTextEdit::redo);
185     }
186 
187     if (submitAction) {
188         if (debug) {
189             const SubmitFileModel *model = fileModel();
190             int count = model ? model->rowCount() : 0;
191             qDebug() << Q_FUNC_INFO << submitAction << count << "items";
192         }
193         d->m_commitEnabled = !canSubmit();
194         connect(this, &SubmitEditorWidget::submitActionEnabledChanged,
195                 submitAction, &QAction::setEnabled);
196         connect(this, &SubmitEditorWidget::submitActionTextChanged,
197                 submitAction, &QAction::setText);
198         d->m_submitButton = new QActionPushButton(submitAction);
199         d->m_ui.buttonLayout->addWidget(d->m_submitButton);
200         if (!d->m_submitShortcut)
201             d->m_submitShortcut = new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return), this);
202         connect(d->m_submitShortcut, &QShortcut::activated,
203                 submitAction, [submitAction] {
204             if (submitAction->isEnabled())
205                 submitAction->trigger();
206         });
207     }
208     if (diffAction) {
209         if (debug)
210             qDebug() << diffAction << d->m_filesSelected;
211         diffAction->setEnabled(d->m_filesSelected);
212         connect(this, &SubmitEditorWidget::fileSelectionChanged, diffAction, &QAction::setEnabled);
213         connect(diffAction, &QAction::triggered, this, &SubmitEditorWidget::triggerDiffSelected);
214         d->m_ui.buttonLayout->addWidget(new QActionPushButton(diffAction));
215     }
216 }
217 
218 // Make sure we have one terminating NL. Do not trim front as leading space might be
219 // required for some formattings.
trimDescription()220 void SubmitEditorWidget::trimDescription()
221 {
222     if (d->m_description.isEmpty())
223         return;
224     // Trim back of string.
225     const int last = d->m_description.size() - 1;
226     int lastWordCharacter = last;
227     for ( ; lastWordCharacter >= 0 && d->m_description.at(lastWordCharacter).isSpace() ;
228           lastWordCharacter--)
229     { }
230     if (lastWordCharacter != last)
231         d->m_description.truncate(lastWordCharacter + 1);
232     d->m_description += QLatin1Char('\n');
233 }
234 
235 // Extract the wrapped text from a text edit, which performs
236 // the wrapping only optically.
wrapDescription()237 void SubmitEditorWidget::wrapDescription()
238 {
239     if (!lineWrap())
240         return;
241     const QChar newLine = QLatin1Char('\n');
242     QTextEdit e;
243     e.setVisible(false);
244     e.setMinimumWidth(1000);
245     e.setFontPointSize(1.0);
246     e.setLineWrapColumnOrWidth(d->m_ui.description->lineWrapColumnOrWidth());
247     e.setLineWrapMode(d->m_ui.description->lineWrapMode());
248     e.setWordWrapMode(d->m_ui.description->wordWrapMode());
249     e.setPlainText(d->m_description);
250     d->m_description.clear();
251     QTextCursor cursor(e.document());
252     cursor.movePosition(QTextCursor::Start);
253     while (!cursor.atEnd()) {
254         const QString block = cursor.block().text();
255         if (block.startsWith(QLatin1Char('\t'))) { // Don't wrap
256             d->m_description += block + newLine;
257             cursor.movePosition(QTextCursor::EndOfBlock);
258         } else {
259             forever {
260                 cursor.select(QTextCursor::LineUnderCursor);
261                 d->m_description += cursor.selectedText();
262                 d->m_description += newLine;
263                 cursor.clearSelection();
264                 if (cursor.atBlockEnd())
265                     break;
266                 cursor.movePosition(QTextCursor::NextCharacter);
267             }
268         }
269         cursor.movePosition(QTextCursor::NextBlock);
270     }
271 }
272 
descriptionText() const273 QString SubmitEditorWidget::descriptionText() const
274 {
275     return d->m_description;
276 }
277 
setDescriptionText(const QString & text)278 void SubmitEditorWidget::setDescriptionText(const QString &text)
279 {
280     d->m_ui.description->setPlainText(text);
281 }
282 
lineWrap() const283 bool SubmitEditorWidget::lineWrap() const
284 {
285     return d->m_ui.description->lineWrapMode() != QTextEdit::NoWrap;
286 }
287 
setLineWrap(bool v)288 void SubmitEditorWidget::setLineWrap(bool v)
289 {
290     if (debug)
291         qDebug() << Q_FUNC_INFO << v;
292     if (v) {
293         d->m_ui.description->setLineWrapColumnOrWidth(d->m_lineWidth);
294         d->m_ui.description->setLineWrapMode(QTextEdit::FixedColumnWidth);
295     } else {
296         d->m_ui.description->setLineWrapMode(QTextEdit::NoWrap);
297     }
298     descriptionTextChanged();
299 }
300 
lineWrapWidth() const301 int SubmitEditorWidget::lineWrapWidth() const
302 {
303     return d->m_lineWidth;
304 }
305 
setLineWrapWidth(int v)306 void SubmitEditorWidget::setLineWrapWidth(int v)
307 {
308     if (debug)
309         qDebug() << Q_FUNC_INFO << v << lineWrap();
310     if (d->m_lineWidth == v)
311         return;
312     d->m_lineWidth = v;
313     if (lineWrap())
314         d->m_ui.description->setLineWrapColumnOrWidth(v);
315     descriptionTextChanged();
316 }
317 
isDescriptionMandatory() const318 bool SubmitEditorWidget::isDescriptionMandatory() const
319 {
320     return d->m_descriptionMandatory;
321 }
322 
setDescriptionMandatory(bool v)323 void SubmitEditorWidget::setDescriptionMandatory(bool v)
324 {
325     d->m_descriptionMandatory = v;
326 }
327 
fileListSelectionMode() const328 QAbstractItemView::SelectionMode SubmitEditorWidget::fileListSelectionMode() const
329 {
330     return d->m_ui.fileView->selectionMode();
331 }
332 
setFileListSelectionMode(QAbstractItemView::SelectionMode sm)333 void SubmitEditorWidget::setFileListSelectionMode(QAbstractItemView::SelectionMode sm)
334 {
335     d->m_ui.fileView->setSelectionMode(sm);
336 }
337 
setFileModel(SubmitFileModel * model)338 void SubmitEditorWidget::setFileModel(SubmitFileModel *model)
339 {
340     d->m_ui.fileView->clearSelection(); // trigger the change signals
341 
342     d->m_ui.fileView->setModel(model);
343 
344     if (model->rowCount()) {
345         const int columnCount = model->columnCount();
346         for (int c = 0;  c < columnCount; c++)
347             d->m_ui.fileView->resizeColumnToContents(c);
348     }
349 
350     connect(model, &QAbstractItemModel::dataChanged,
351             this, &SubmitEditorWidget::updateSubmitAction);
352     connect(model, &QAbstractItemModel::modelReset,
353             this, &SubmitEditorWidget::updateSubmitAction);
354     connect(model, &QAbstractItemModel::dataChanged,
355             this, &SubmitEditorWidget::updateCheckAllComboBox);
356     connect(model, &QAbstractItemModel::modelReset,
357             this, &SubmitEditorWidget::updateCheckAllComboBox);
358     connect(model, &QAbstractItemModel::rowsInserted,
359             this, &SubmitEditorWidget::updateSubmitAction);
360     connect(model, &QAbstractItemModel::rowsRemoved,
361             this, &SubmitEditorWidget::updateSubmitAction);
362     connect(d->m_ui.fileView->selectionModel(), &QItemSelectionModel::selectionChanged,
363             this, &SubmitEditorWidget::updateDiffAction);
364     updateActions();
365 }
366 
fileModel() const367 SubmitFileModel *SubmitEditorWidget::fileModel() const
368 {
369     return static_cast<SubmitFileModel *>(d->m_ui.fileView->model());
370 }
371 
checkedFiles() const372 QStringList SubmitEditorWidget::checkedFiles() const
373 {
374     QStringList rc;
375     const SubmitFileModel *model = fileModel();
376     if (!model)
377         return rc;
378     const int count = model->rowCount();
379     for (int i = 0; i < count; i++)
380         if (model->checked(i))
381             rc.push_back(model->file(i));
382     return rc;
383 }
384 
descriptionEdit() const385 CompletingTextEdit *SubmitEditorWidget::descriptionEdit() const
386 {
387     return d->m_ui.description;
388 }
389 
triggerDiffSelected()390 void SubmitEditorWidget::triggerDiffSelected()
391 {
392     const QList<int> sel = selectedRows();
393     if (!sel.empty())
394         emit diffSelected(sel);
395 }
396 
diffActivatedDelayed()397 void SubmitEditorWidget::diffActivatedDelayed()
398 {
399     emit diffSelected(QList<int>() << d->m_activatedRow);
400 }
401 
diffActivated(const QModelIndex & index)402 void SubmitEditorWidget::diffActivated(const QModelIndex &index)
403 {
404     // We need to delay the signal, otherwise, the diff editor will not
405     // be in the foreground.
406     d->m_activatedRow = index.row();
407     QTimer::singleShot(0, this, &SubmitEditorWidget::diffActivatedDelayed);
408 }
409 
updateActions()410 void SubmitEditorWidget::updateActions()
411 {
412     updateSubmitAction();
413     updateDiffAction();
414     updateCheckAllComboBox();
415 }
416 
417 // Enable submit depending on having checked files
updateSubmitAction()418 void SubmitEditorWidget::updateSubmitAction()
419 {
420     const unsigned checkedCount = checkedFilesCount();
421     const bool newCommitState = canSubmit();
422     // Emit signal to update action
423     if (d->m_commitEnabled != newCommitState) {
424         d->m_commitEnabled = newCommitState;
425         emit submitActionEnabledChanged(d->m_commitEnabled);
426     }
427     if (d->m_ui.fileView && d->m_ui.fileView->model()) {
428         // Update button text.
429         const int fileCount = d->m_ui.fileView->model()->rowCount();
430         const QString msg = checkedCount ?
431                             tr("%1 %2/%n File(s)", nullptr, fileCount)
432                             .arg(commitName()).arg(checkedCount) :
433                             commitName();
434         emit submitActionTextChanged(msg);
435     }
436 }
437 
438 // Enable diff depending on selected files
updateDiffAction()439 void SubmitEditorWidget::updateDiffAction()
440 {
441     const bool filesSelected = hasSelection();
442     if (d->m_filesSelected != filesSelected) {
443         d->m_filesSelected = filesSelected;
444         emit fileSelectionChanged(d->m_filesSelected);
445     }
446 }
447 
updateCheckAllComboBox()448 void SubmitEditorWidget::updateCheckAllComboBox()
449 {
450     d->m_ignoreChange = true;
451     int checkedCount = checkedFilesCount();
452     if (checkedCount == 0)
453         d->m_ui.checkAllCheckBox->setCheckState(Qt::Unchecked);
454     else if (checkedCount == d->m_ui.fileView->model()->rowCount())
455         d->m_ui.checkAllCheckBox->setCheckState(Qt::Checked);
456     else
457         d->m_ui.checkAllCheckBox->setCheckState(Qt::PartiallyChecked);
458     d->m_ignoreChange = false;
459 }
460 
hasSelection() const461 bool SubmitEditorWidget::hasSelection() const
462 {
463     // Not present until model is set
464     if (const QItemSelectionModel *sm = d->m_ui.fileView->selectionModel())
465         return sm->hasSelection();
466     return false;
467 }
468 
checkedFilesCount() const469 int SubmitEditorWidget::checkedFilesCount() const
470 {
471     int checkedCount = 0;
472     if (const SubmitFileModel *model = fileModel()) {
473         const int count = model->rowCount();
474         for (int i = 0; i < count; ++i)
475             if (model->checked(i))
476                 ++checkedCount;
477     }
478     return checkedCount;
479 }
480 
cleanupDescription(const QString & input) const481 QString SubmitEditorWidget::cleanupDescription(const QString &input) const
482 {
483     return input;
484 }
485 
insertTopWidget(QWidget * w)486 void SubmitEditorWidget::insertTopWidget(QWidget *w)
487 {
488     d->m_ui.vboxLayout->insertWidget(0, w);
489 }
490 
insertLeftWidget(QWidget * w)491 void SubmitEditorWidget::insertLeftWidget(QWidget *w)
492 {
493     d->m_ui.splitter->insertWidget(0, w);
494 }
495 
addSubmitButtonMenu(QMenu * menu)496 void SubmitEditorWidget::addSubmitButtonMenu(QMenu *menu)
497 {
498     d->m_submitButton->setMenu(menu);
499 }
500 
hideDescription()501 void SubmitEditorWidget::hideDescription()
502 {
503     d->m_ui.descriptionBox->hide();
504     setDescriptionMandatory(false);
505 }
506 
verifyDescription()507 void SubmitEditorWidget::verifyDescription()
508 {
509     auto fontColor = [](Utils::Theme::Color color) {
510         return QString("<font color=\"%1\">")
511                 .arg(Utils::creatorTheme()->color(color).name());
512     };
513     const QString hint = fontColor(Utils::Theme::OutputPanes_TestWarnTextColor);
514     const QString warning = fontColor(Utils::Theme::TextColorError);
515 
516     const QChar newLine = '\n';
517     const int descriptionLength = d->m_description.length();
518     int subjectLength = d->m_description.indexOf(newLine);
519     int secondLineLength = 0;
520     if (subjectLength >= 0) {
521         const int secondLineStart = subjectLength + 1;
522         int secondLineEnd = d->m_description.indexOf(newLine, secondLineStart);
523         if (secondLineEnd == -1)
524             secondLineEnd = descriptionLength;
525         secondLineLength = secondLineEnd - secondLineStart;
526     } else {
527         subjectLength = descriptionLength;
528     }
529 
530     enum { MaxSubjectLength = 72, WarningSubjectLength = 55 };
531     QStringList hints;
532     QStringList toolTips;
533     if (descriptionLength < 20)
534         hints.append(warning + tr("Warning: The commit message is very short."));
535 
536     if (subjectLength > MaxSubjectLength)
537         hints.append(warning + tr("Warning: The commit subject is too long."));
538     else if (subjectLength > WarningSubjectLength)
539         hints.append(hint + tr("Hint: Aim for a shorter commit subject."));
540 
541     if (secondLineLength > 0)
542         hints.append(hint + tr("Hint: The second line of a commit message should be empty."));
543 
544     d->m_ui.descriptionHint->setText(hints.join("<br>"));
545     if (!d->m_ui.descriptionHint->text().isEmpty()) {
546         d->m_ui.descriptionHint->setToolTip(
547                     tr("<p>Writing good commit messages</p>"
548                        "<ul>"
549                        "<li>Avoid very short commit messages.</li>"
550                        "<li>Consider the first line as subject (like in email) "
551                        "and keep it shorter than %n characters.</li>"
552                        "<li>After an empty second line, a longer description can be added.</li>"
553                        "<li>Describe why the change was done, not how it was done.</li>"
554                        "</ul>", nullptr, MaxSubjectLength));
555         }
556 }
557 
descriptionTextChanged()558 void SubmitEditorWidget::descriptionTextChanged()
559 {
560     d->m_description = cleanupDescription(d->m_ui.description->toPlainText());
561     verifyDescription();
562     wrapDescription();
563     trimDescription();
564     // append field entries
565     foreach (const SubmitFieldWidget *fw, d->m_fieldWidgets)
566         d->m_description += fw->fieldValues();
567     updateSubmitAction();
568 }
569 
canSubmit(QString * whyNot) const570 bool SubmitEditorWidget::canSubmit(QString *whyNot) const
571 {
572     if (d->m_updateInProgress) {
573         if (whyNot)
574             *whyNot = tr("Update in progress");
575         return false;
576     }
577     if (isDescriptionMandatory() && d->m_description.trimmed().isEmpty()) {
578         if (whyNot)
579             *whyNot = tr("Description is empty");
580         return false;
581     }
582     const unsigned checkedCount = checkedFilesCount();
583     const bool res = d->m_emptyFileListEnabled || checkedCount > 0;
584     if (!res && whyNot)
585         *whyNot = tr("No files checked");
586     return res;
587 }
588 
setUpdateInProgress(bool value)589 void SubmitEditorWidget::setUpdateInProgress(bool value)
590 {
591     d->m_updateInProgress = value;
592     updateSubmitAction();
593 }
594 
updateInProgress() const595 bool SubmitEditorWidget::updateInProgress() const
596 {
597     return d->m_updateInProgress;
598 }
599 
selectedRows() const600 QList<int> SubmitEditorWidget::selectedRows() const
601 {
602     return Utils::transform(d->m_ui.fileView->selectionModel()->selectedRows(0), &QModelIndex::row);
603 }
604 
setSelectedRows(const QList<int> & rows)605 void SubmitEditorWidget::setSelectedRows(const QList<int> &rows)
606 {
607     if (const SubmitFileModel *model = fileModel()) {
608         QItemSelectionModel *selectionModel = d->m_ui.fileView->selectionModel();
609         for (int row : rows) {
610             selectionModel->select(model->index(row, 0),
611                                    QItemSelectionModel::Select | QItemSelectionModel::Rows);
612         }
613     }
614 }
615 
commitName() const616 QString SubmitEditorWidget::commitName() const
617 {
618     return tr("&Commit");
619 }
620 
addSubmitFieldWidget(SubmitFieldWidget * f)621 void SubmitEditorWidget::addSubmitFieldWidget(SubmitFieldWidget *f)
622 {
623     if (!d->m_fieldLayout) {
624         // VBox with horizontal, expanding spacer
625         d->m_fieldLayout = new QVBoxLayout;
626         auto outerLayout = new QHBoxLayout;
627         outerLayout->addLayout(d->m_fieldLayout);
628         outerLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Ignored));
629         d->m_ui.descriptionLayout->addLayout(outerLayout);
630     }
631     d->m_fieldLayout->addWidget(f);
632     d->m_fieldWidgets.push_back(f);
633 }
634 
submitFieldWidgets() const635 QList<SubmitFieldWidget *> SubmitEditorWidget::submitFieldWidgets() const
636 {
637     return d->m_fieldWidgets;
638 }
639 
addDescriptionEditContextMenuAction(QAction * a)640 void SubmitEditorWidget::addDescriptionEditContextMenuAction(QAction *a)
641 {
642     d->descriptionEditContextMenuActions
643             .push_back(SubmitEditorWidgetPrivate::AdditionalContextMenuAction(-1, a));
644 }
645 
insertDescriptionEditContextMenuAction(int pos,QAction * a)646 void SubmitEditorWidget::insertDescriptionEditContextMenuAction(int pos, QAction *a)
647 {
648     d->descriptionEditContextMenuActions
649             .push_back(SubmitEditorWidgetPrivate::AdditionalContextMenuAction(pos, a));
650 }
651 
editorCustomContextMenuRequested(const QPoint & pos)652 void SubmitEditorWidget::editorCustomContextMenuRequested(const QPoint &pos)
653 {
654     QScopedPointer<QMenu> menu(d->m_ui.description->createStandardContextMenu());
655     // Extend
656     foreach (const SubmitEditorWidgetPrivate::AdditionalContextMenuAction &a,
657              d->descriptionEditContextMenuActions) {
658         if (a.second) {
659             if (a.first >= 0)
660                 menu->insertAction(menu->actions().at(a.first), a.second);
661             else
662                 menu->addAction(a.second);
663         }
664     }
665     menu->exec(d->m_ui.description->mapToGlobal(pos));
666 }
667 
checkAllToggled()668 void SubmitEditorWidget::checkAllToggled()
669 {
670     if (d->m_ignoreChange)
671         return;
672     Qt::CheckState checkState = d->m_ui.checkAllCheckBox->checkState();
673     fileModel()->setAllChecked(checkState == Qt::Checked || checkState == Qt::PartiallyChecked);
674     // Reset that again, so that the user can't do it
675     d->m_ui.checkAllCheckBox->setTristate(false);
676 }
677 
fileListCustomContextMenuRequested(const QPoint & pos)678 void SubmitEditorWidget::fileListCustomContextMenuRequested(const QPoint & pos)
679 {
680     // Execute menu offering to check/uncheck all
681     QMenu menu;
682     //: Check all for submit
683     QAction *checkAllAction = menu.addAction(tr("Select All"));
684     //: Uncheck all for submit
685     QAction *uncheckAllAction = menu.addAction(tr("Unselect All"));
686     QAction *action = menu.exec(d->m_ui.fileView->mapToGlobal(pos));
687     if (action == checkAllAction) {
688         fileModel()->setAllChecked(true);;
689         return;
690     }
691     if (action == uncheckAllAction) {
692         fileModel()->setAllChecked(false);
693         return;
694     }
695 }
696 
isEmptyFileListEnabled() const697 bool SubmitEditorWidget::isEmptyFileListEnabled() const
698 {
699     return d->m_emptyFileListEnabled;
700 }
701 
setEmptyFileListEnabled(bool e)702 void SubmitEditorWidget::setEmptyFileListEnabled(bool e)
703 {
704     if (e != d->m_emptyFileListEnabled) {
705         d->m_emptyFileListEnabled = e;
706         updateSubmitAction();
707     }
708 }
709 
710 } // namespace VcsBase
711 
712 #include "submiteditorwidget.moc"
713