submiteditorwidget.cpp 17.5 KB
Newer Older
1
/**************************************************************************
con's avatar
con committed
2
3
4
**
** This file is part of Qt Creator
**
5
** Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
con's avatar
con committed
6
7
8
**
** Contact:  Qt Software Information (qt-info@nokia.com)
**
9
** Commercial Usage
10
**
11
12
13
14
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
15
**
16
** GNU Lesser General Public License Usage
17
**
18
19
20
21
22
23
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file.  Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24
**
25
26
** If you are unsure which license is appropriate for your use, please
** contact the sales department at qt-sales@nokia.com.
con's avatar
con committed
27
**
28
**************************************************************************/
hjk's avatar
hjk committed
29

con's avatar
con committed
30
31
32
33
34
#include "submiteditorwidget.h"
#include "ui_submiteditorwidget.h"

#include <QtCore/QDebug>
#include <QtCore/QPointer>
35
#include <QtCore/QTimer>
36
#include <QtCore/QSignalMapper>
con's avatar
con committed
37

38
#include <QtGui/QPushButton>
39
40
41
42
43
44
#include <QtGui/QMenu>
#include <QtGui/QLineEdit>
#include <QtGui/QFormLayout>
#include <QtGui/QHBoxLayout>
#include <QtGui/QToolButton>
#include <QtGui/QSpacerItem>
45

hjk's avatar
hjk committed
46
enum { debug = 0 };
47
enum { defaultLineWidth = 72 };
con's avatar
con committed
48
49
50
51

namespace Core {
namespace Utils {

52
53
// QActionPushButton: A push button tied to an action
// (similar to a QToolButton)
hjk's avatar
hjk committed
54
55
class QActionPushButton : public QPushButton
{
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
    Q_OBJECT
public:
    explicit QActionPushButton(QAction *a);

private slots:
    void actionChanged();
};

QActionPushButton::QActionPushButton(QAction *a) :
     QPushButton(a->icon(), a->text())
{
    connect(a, SIGNAL(changed()), this, SLOT(actionChanged()));
    connect(this, SIGNAL(clicked()), a, SLOT(trigger()));
    setEnabled(a->isEnabled());
}

void QActionPushButton::actionChanged()
{
    if (const QAction *a = qobject_cast<QAction*>(sender()))
        setEnabled(a->isEnabled());
}

78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// Helpers to retrieve model data
static inline bool listModelChecked(const QAbstractItemModel *model, int row, int column = 0)
{
    const QModelIndex checkableIndex = model->index(row, column, QModelIndex());
    return model->data(checkableIndex, Qt::CheckStateRole).toInt() == Qt::Checked;
}

static inline QString listModelText(const QAbstractItemModel *model, int row, int column)
{
    const QModelIndex index = model->index(row, column, QModelIndex());
    return model->data(index, Qt::DisplayRole).toString();
}

// Find a check item in a model
static bool listModelContainsCheckedItem(const QAbstractItemModel *model)
{
    const int count = model->rowCount();
    for (int i = 0; i < count; i++)
        if (listModelChecked(model, i, 0))
            return true;
    return false;
}

// Convenience to extract a list of selected indexes
QList<int> selectedRows(const QAbstractItemView *view)
{
    const QModelIndexList indexList = view->selectionModel()->selectedRows(0);
    if (indexList.empty())
        return QList<int>();
    QList<int> rc;
    const QModelIndexList::const_iterator cend = indexList.constEnd();
    for (QModelIndexList::const_iterator it = indexList.constBegin(); it != cend; ++it)
        rc.push_back(it->row());
    return rc;
}

114
// -----------  SubmitEditorWidgetPrivate
115

hjk's avatar
hjk committed
116
117
struct SubmitEditorWidgetPrivate
{
118
119
120
    // A pair of position/action to extend context menus
    typedef QPair<int, QPointer<QAction> > AdditionalContextMenuAction;

con's avatar
con committed
121
122
123
124
125
    SubmitEditorWidgetPrivate();

    Ui::SubmitEditorWidget m_ui;
    bool m_filesSelected;
    bool m_filesChecked;
126
127
    int m_fileNameColumn;
    int m_activatedRow;
128
129
130
131
132
133
134

    QList<AdditionalContextMenuAction> descriptionEditContextMenuActions;
    QFormLayout *m_fieldLayout;
    // Field entries (label, line edits)
    typedef QPair<QString, QLineEdit*> FieldEntry;
    QList<FieldEntry> m_fieldEntries;
    QSignalMapper *m_fieldSignalMapper;
135
    int m_lineWidth;
con's avatar
con committed
136
137
138
139
};

SubmitEditorWidgetPrivate::SubmitEditorWidgetPrivate() :
    m_filesSelected(false),
140
141
    m_filesChecked(false),
    m_fileNameColumn(1),
142
143
    m_activatedRow(-1),
    m_fieldLayout(0),
144
145
    m_fieldSignalMapper(0),
    m_lineWidth(defaultLineWidth)
con's avatar
con committed
146
147
148
149
150
151
152
153
{
}

SubmitEditorWidget::SubmitEditorWidget(QWidget *parent) :
    QWidget(parent),
    m_d(new SubmitEditorWidgetPrivate)
{
    m_d->m_ui.setupUi(this);
154
    m_d->m_ui.description->setContextMenuPolicy(Qt::CustomContextMenu);
155
    m_d->m_ui.description->setLineWrapMode(QTextEdit::NoWrap);
156
157
158
    connect(m_d->m_ui.description, SIGNAL(customContextMenuRequested(QPoint)),
            this, SLOT(editorCustomContextMenuRequested(QPoint)));

con's avatar
con committed
159
    // File List
160
161
162
163
    m_d->m_ui.fileView->setSelectionMode(QAbstractItemView::ExtendedSelection);
    m_d->m_ui.fileView->setRootIsDecorated(false);
    connect(m_d->m_ui.fileView, SIGNAL(doubleClicked(QModelIndex)),
            this, SLOT(diffActivated(QModelIndex)));
con's avatar
con committed
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191

    // Text
    m_d->m_ui.description->setFont(QFont(QLatin1String("Courier")));

    setFocusPolicy(Qt::StrongFocus);
    setFocusProxy(m_d->m_ui.description);
}

SubmitEditorWidget::~SubmitEditorWidget()
{
    delete m_d;
}

void SubmitEditorWidget::registerActions(QAction *editorUndoAction,  QAction *editorRedoAction,
                         QAction *submitAction, QAction *diffAction)
{
    if (editorUndoAction) {
        editorUndoAction->setEnabled(m_d->m_ui.description->document()->isUndoAvailable());
        connect(m_d->m_ui.description, SIGNAL(undoAvailable(bool)), editorUndoAction, SLOT(setEnabled(bool)));
        connect(editorUndoAction, SIGNAL(triggered()), m_d->m_ui.description, SLOT(undo()));
    }
    if (editorRedoAction) {
        editorRedoAction->setEnabled(m_d->m_ui.description->document()->isRedoAvailable());
        connect(m_d->m_ui.description, SIGNAL(redoAvailable(bool)), editorRedoAction, SLOT(setEnabled(bool)));
        connect(editorRedoAction, SIGNAL(triggered()), m_d->m_ui.description, SLOT(redo()));
    }

    if (submitAction) {
192
193
194
195
        if (debug) {
            int count = 0;
            if (const QAbstractItemModel *model = m_d->m_ui.fileView->model())
                count = model->rowCount();
196
            qDebug() << Q_FUNC_INFO << submitAction << count << "items" << m_d->m_filesChecked;
197
        }
con's avatar
con committed
198
199
        submitAction->setEnabled(m_d->m_filesChecked);
        connect(this, SIGNAL(fileCheckStateChanged(bool)), submitAction, SLOT(setEnabled(bool)));
200
        m_d->m_ui.buttonLayout->addWidget(new QActionPushButton(submitAction));
con's avatar
con committed
201
202
203
204
    }
    if (diffAction) {
        if (debug)
            qDebug() << diffAction << m_d->m_filesSelected;
205
206
207
208
        diffAction->setEnabled(m_d->m_filesSelected);
        connect(this, SIGNAL(fileSelectionChanged(bool)), diffAction, SLOT(setEnabled(bool)));
        connect(diffAction, SIGNAL(triggered()), this, SLOT(triggerDiffSelected()));
        m_d->m_ui.buttonLayout->addWidget(new QActionPushButton(diffAction));
con's avatar
con committed
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
    }
}

void SubmitEditorWidget::unregisterActions(QAction *editorUndoAction,  QAction *editorRedoAction,
                                           QAction *submitAction, QAction *diffAction)
{
    if (editorUndoAction) {
        disconnect(m_d->m_ui.description, SIGNAL(undoAvailableChanged(bool)), editorUndoAction, SLOT(setEnabled(bool)));
        disconnect(editorUndoAction, SIGNAL(triggered()), m_d->m_ui.description, SLOT(undo()));
    }
    if (editorRedoAction) {
        disconnect(m_d->m_ui.description, SIGNAL(redoAvailableChanged(bool)), editorRedoAction, SLOT(setEnabled(bool)));
        disconnect(editorRedoAction, SIGNAL(triggered()), m_d->m_ui.description, SLOT(redo()));
    }

    if (submitAction)
        disconnect(this, SIGNAL(fileCheckStateChanged(bool)), submitAction, SLOT(setEnabled(bool)));

    if (diffAction) {
         disconnect(this, SIGNAL(fileSelectionChanged(bool)), diffAction, SLOT(setEnabled(bool)));
         disconnect(diffAction, SIGNAL(triggered()), this, SLOT(triggerDiffSelected()));
    }
}

233
234
// Make sure we have one terminating NL
static inline QString trimMessageText(const QString &t)
con's avatar
con committed
235
{
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
    QString rc = t.trimmed();
    rc += QLatin1Char('\n');
    return rc;
}

// Extract the wrapped text from a text edit, which performs
// the wrapping only optically.
static QString wrappedText(const QTextEdit *e)
{
    const QChar newLine = QLatin1Char('\n');
    QString rc;
    QTextCursor cursor(e->document());
    cursor.movePosition(QTextCursor::Start);    
    while (!cursor.atEnd()) {
        cursor.select(QTextCursor::LineUnderCursor);
        rc += cursor.selectedText();
        rc += newLine;
        cursor.movePosition(QTextCursor::Right);
    }
    return rc;
con's avatar
con committed
256
257
258
259
}

QString SubmitEditorWidget::descriptionText() const
{
260
    QString rc = trimMessageText(lineWrap() ? wrappedText(m_d->m_ui.description) : m_d->m_ui.description->toPlainText());
261
262
263
264
265
266
267
268
269
270
271
    // append field entries
    foreach(const SubmitEditorWidgetPrivate::FieldEntry &fe, m_d->m_fieldEntries) {
        const QString fieldText = fe.second->text().trimmed();
        if (!fieldText.isEmpty()) {
            rc += fe.first;
            rc += QLatin1Char(' ');
            rc += fieldText;
            rc += QLatin1Char('\n');
        }
    }
    return rc;
con's avatar
con committed
272
273
274
275
276
277
278
}

void SubmitEditorWidget::setDescriptionText(const QString &text)
{
    m_d->m_ui.description->setPlainText(text);
}

279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
bool SubmitEditorWidget::lineWrap() const
{
    return m_d->m_ui.description->lineWrapMode() != QTextEdit::NoWrap;
}

void SubmitEditorWidget::setLineWrap(bool v)
{
    if (debug)
        qDebug() << Q_FUNC_INFO << v;
    if (v) {
        m_d->m_ui.description->setLineWrapColumnOrWidth(m_d->m_lineWidth);
        m_d->m_ui.description->setLineWrapMode(QTextEdit::FixedColumnWidth);        
    } else {
        m_d->m_ui.description->setLineWrapMode(QTextEdit::NoWrap);
    }
}

int SubmitEditorWidget::lineWrapWidth() const
{
    return m_d->m_lineWidth;
}

void SubmitEditorWidget::setLineWrapWidth(int v)
{
    if (debug)
        qDebug() << Q_FUNC_INFO << v << lineWrap();
    if (m_d->m_lineWidth == v)
        return;
    m_d->m_lineWidth = v;
    if (lineWrap())
        m_d->m_ui.description->setLineWrapColumnOrWidth(v);
}

312
int SubmitEditorWidget::fileNameColumn() const
con's avatar
con committed
313
{
314
    return m_d->m_fileNameColumn;
con's avatar
con committed
315
316
}

317
void SubmitEditorWidget::setFileNameColumn(int c)
con's avatar
con committed
318
{
319
    m_d->m_fileNameColumn = c;
con's avatar
con committed
320
321
}

322
323
324
325
326
327
328
329
330
331
QAbstractItemView::SelectionMode SubmitEditorWidget::fileListSelectionMode() const
{
    return m_d->m_ui.fileView->selectionMode();
}

void SubmitEditorWidget::setFileListSelectionMode(QAbstractItemView::SelectionMode sm)
{
    m_d->m_ui.fileView->setSelectionMode(sm);
}

332
void SubmitEditorWidget::setFileModel(QAbstractItemModel *model)
con's avatar
con committed
333
{
334
    m_d->m_ui.fileView->clearSelection(); // trigger the change signals
con's avatar
con committed
335

336
    m_d->m_ui.fileView->setModel(model);
con's avatar
con committed
337

338
339
340
341
    if (model->rowCount()) {
        const int columnCount = model->columnCount();
        for (int c = 0;  c < columnCount; c++)
            m_d->m_ui.fileView->resizeColumnToContents(c);
con's avatar
con committed
342
    }
343
344
345
346
347
348
349
350
351
352
353
354

    connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)),
            this, SLOT(updateSubmitAction()));
    connect(model, SIGNAL(modelReset()),
            this, SLOT(updateSubmitAction()));
    connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)),
            this, SLOT(updateSubmitAction()));
    connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)),
            this, SLOT(updateSubmitAction()));
    connect(m_d->m_ui.fileView->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
            this, SLOT(updateDiffAction()));
    updateActions();
con's avatar
con committed
355
356
}

357
QAbstractItemModel *SubmitEditorWidget::fileModel() const
con's avatar
con committed
358
{
359
    return m_d->m_ui.fileView->model();
con's avatar
con committed
360
361
362
363
}

QStringList SubmitEditorWidget::selectedFiles() const
{
364
365
366
367
    const QList<int> selection = selectedRows(m_d->m_ui.fileView);
    if (selection.empty())
        return QStringList();

con's avatar
con committed
368
    QStringList rc;
369
370
371
372
    const QAbstractItemModel *model = m_d->m_ui.fileView->model();
    const int count = selection.size();
    for (int i = 0; i < count; i++)
        rc.push_back(listModelText(model, selection.at(i), fileNameColumn()));
con's avatar
con committed
373
374
375
376
377
378
    return rc;
}

QStringList SubmitEditorWidget::checkedFiles() const
{
    QStringList rc;
379
380
381
382
383
384
385
    const QAbstractItemModel *model = m_d->m_ui.fileView->model();
    if (!model)
        return rc;
    const int count = model->rowCount();
    for (int i = 0; i < count; i++)
        if (listModelChecked(model, i, 0))
            rc.push_back(listModelText(model, i, fileNameColumn()));
con's avatar
con committed
386
387
388
    return rc;
}

389
QTextEdit *SubmitEditorWidget::descriptionEdit() const
con's avatar
con committed
390
391
392
393
394
395
396
397
398
399
400
{
    return m_d->m_ui.description;
}

void SubmitEditorWidget::triggerDiffSelected()
{
    const QStringList sel = selectedFiles();
    if (!sel.empty())
        emit diffSelected(sel);
}

401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
void SubmitEditorWidget::diffActivatedDelayed()
{
    const QStringList files = QStringList(listModelText(m_d->m_ui.fileView->model(), m_d->m_activatedRow, fileNameColumn()));
    emit diffSelected(files);
}

void SubmitEditorWidget::diffActivated(const QModelIndex &index)
{
    // We need to delay the signal, otherwise, the diff editor will not
    // be in the foreground.
    m_d->m_activatedRow = index.row();
    QTimer::singleShot(0, this, SLOT(diffActivatedDelayed()));
}

void SubmitEditorWidget::updateActions()
{
    updateSubmitAction();
    updateDiffAction();
}

// Enable submit depending on having checked files
void SubmitEditorWidget::updateSubmitAction()
{
    const bool newFilesCheckedState = hasCheckedFiles();
    if (m_d->m_filesChecked != newFilesCheckedState) {
        m_d->m_filesChecked = newFilesCheckedState;
        emit fileCheckStateChanged(m_d->m_filesChecked);
con's avatar
con committed
428
429
430
    }
}

431
432
// Enable diff depending on selected files
void SubmitEditorWidget::updateDiffAction()
con's avatar
con committed
433
{
434
435
436
    const bool filesSelected = hasSelection();
    if (m_d->m_filesSelected != filesSelected) {
        m_d->m_filesSelected = filesSelected;
con's avatar
con committed
437
438
439
440
        emit fileSelectionChanged(m_d->m_filesSelected);
    }
}

441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
bool SubmitEditorWidget::hasSelection() const
{
    // Not present until model is set
    if (const QItemSelectionModel *sm = m_d->m_ui.fileView->selectionModel())
        return sm->hasSelection();
    return false;
}

bool SubmitEditorWidget::hasCheckedFiles() const
{
    if (const QAbstractItemModel *model = m_d->m_ui.fileView->model())
        return listModelContainsCheckedItem(model);
    return false;
}

con's avatar
con committed
456
457
void SubmitEditorWidget::changeEvent(QEvent *e)
{
458
    QWidget::changeEvent(e);
hjk's avatar
hjk committed
459
    switch (e->type()) {
con's avatar
con committed
460
461
462
463
464
465
466
467
468
469
470
471
472
    case QEvent::LanguageChange:
        m_d->m_ui.retranslateUi(this);
        break;
    default:
        break;
    }
}

void SubmitEditorWidget::insertTopWidget(QWidget *w)
{
    m_d->m_ui.vboxLayout->insertWidget(0, w);
}

473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
void SubmitEditorWidget::addDescriptionEditContextMenuAction(QAction *a)
{
    m_d->descriptionEditContextMenuActions.push_back(SubmitEditorWidgetPrivate::AdditionalContextMenuAction(-1, a));
}

void SubmitEditorWidget::insertDescriptionEditContextMenuAction(int pos, QAction *a)
{
    m_d->descriptionEditContextMenuActions.push_back(SubmitEditorWidgetPrivate::AdditionalContextMenuAction(pos, a));
}

void SubmitEditorWidget::editorCustomContextMenuRequested(const QPoint &pos)
{
    QMenu *menu = m_d->m_ui.description->createStandardContextMenu();    
    // Extend
    foreach (const SubmitEditorWidgetPrivate::AdditionalContextMenuAction &a, m_d->descriptionEditContextMenuActions) {
        if (a.second) {
            if (a.first >= 0) {
                menu->insertAction(menu->actions().at(a.first), a.second);
            } else {
                menu->addAction(a.second);
            }
        }
    }
    menu->exec(m_d->m_ui.description->mapToGlobal(pos));
    delete menu;
}

QLineEdit *SubmitEditorWidget::addField(const QString &label, bool hasDialogButton)
{
    // Insert  the form layout below the editor
    if (!m_d->m_fieldLayout) {
        QHBoxLayout *outerLayout = new QHBoxLayout;
        m_d->m_fieldLayout = new QFormLayout;
        outerLayout->addLayout(m_d->m_fieldLayout);
        outerLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Ignored));
        QBoxLayout *descrLayout = qobject_cast<QBoxLayout*>(m_d->m_ui.descriptionBox->layout());
        Q_ASSERT(descrLayout);
        descrLayout->addLayout(outerLayout);
    }
    if (hasDialogButton && !m_d->m_fieldSignalMapper) {
        m_d->m_fieldSignalMapper = new QSignalMapper;
        connect(m_d->m_fieldSignalMapper, SIGNAL(mapped(int)), this, SIGNAL(fieldDialogRequested(int)));
    }
    // Add a field row consisting of label and line edit
    QLineEdit *lineEdit = new QLineEdit;
    QHBoxLayout *fieldLayout = new QHBoxLayout;
    fieldLayout->addWidget(lineEdit);
    if (hasDialogButton) {
        QToolButton *dialogButton = new QToolButton;
        dialogButton->setText(tr("..."));
        connect(dialogButton, SIGNAL(clicked()), m_d->m_fieldSignalMapper, SLOT(map()));
        m_d->m_fieldSignalMapper->setMapping(dialogButton, m_d->m_fieldEntries.size());
        fieldLayout->addWidget(dialogButton);
    }
    QToolButton *clearButton = new QToolButton;
    clearButton->setText(tr("Clear"));
    connect(clearButton, SIGNAL(clicked()), lineEdit, SLOT(clear()));    
    fieldLayout->addWidget(clearButton);
    m_d->m_fieldLayout->addRow(label, fieldLayout);
    m_d->m_fieldEntries.push_back(SubmitEditorWidgetPrivate::FieldEntry(label, lineEdit));
    return lineEdit;
}

QLineEdit *SubmitEditorWidget::fieldLineEdit(int i) const
{
    return m_d->m_fieldEntries.at(i).second;
}

con's avatar
con committed
541
542
} // namespace Utils
} // namespace Core
543
544

#include "submiteditorwidget.moc"