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 "pathlisteditor.h"
27 
28 #include "hostosinfo.h"
29 #include "stringutils.h"
30 
31 #include <QDebug>
32 #include <QFileDialog>
33 #include <QMenu>
34 #include <QMimeData>
35 #include <QPlainTextEdit>
36 #include <QPushButton>
37 #include <QSharedPointer>
38 #include <QTextBlock>
39 #include <QVBoxLayout>
40 
41 /*!
42     \class Utils::PathListEditor
43 
44     \brief The PathListEditor class is a control that lets the user edit a list
45     of (directory) paths
46     using the platform separator (';',':').
47 
48     Typically used for
49     path lists controlled by environment variables, such as
50     PATH. It is based on a QPlainTextEdit as it should
51     allow for convenient editing and non-directory type elements like
52     \code
53     "etc/mydir1:$SPECIAL_SYNTAX:/etc/mydir2".
54     \endcode
55 
56     When pasting text into it, the platform separator will be replaced
57     by new line characters for convenience.
58  */
59 
60 namespace Utils {
61 
62 const int PathListEditor::lastInsertButtonIndex = 0;
63 
64 // ------------ PathListPlainTextEdit:
65 // Replaces the platform separator ';',':' by '\n'
66 // when inserting, allowing for pasting in paths
67 // from the terminal or such.
68 
69 class PathListPlainTextEdit : public QPlainTextEdit {
70 public:
71     explicit PathListPlainTextEdit(QWidget *parent = nullptr);
72 protected:
73     void insertFromMimeData (const QMimeData *source) override;
74 };
75 
PathListPlainTextEdit(QWidget * parent)76 PathListPlainTextEdit::PathListPlainTextEdit(QWidget *parent) :
77     QPlainTextEdit(parent)
78 {
79     // No wrapping, scroll at all events
80     setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
81     setLineWrapMode(QPlainTextEdit::NoWrap);
82 }
83 
insertFromMimeData(const QMimeData * source)84 void PathListPlainTextEdit::insertFromMimeData(const QMimeData *source)
85 {
86     if (source->hasText()) {
87         // replace separator
88         QString text = source->text().trimmed();
89         text.replace(HostOsInfo::pathListSeparator(), QLatin1Char('\n'));
90         QSharedPointer<QMimeData> fixed(new QMimeData);
91         fixed->setText(text);
92         QPlainTextEdit::insertFromMimeData(fixed.data());
93     } else {
94         QPlainTextEdit::insertFromMimeData(source);
95     }
96 }
97 
98 // ------------ PathListEditorPrivate
99 struct PathListEditorPrivate {
100     PathListEditorPrivate();
101 
102     QHBoxLayout *layout;
103     QVBoxLayout *buttonLayout;
104     QPlainTextEdit *edit;
105     QString fileDialogTitle;
106 };
107 
PathListEditorPrivate()108 PathListEditorPrivate::PathListEditorPrivate()   :
109         layout(new QHBoxLayout),
110         buttonLayout(new QVBoxLayout),
111         edit(new PathListPlainTextEdit)
112 {
113     layout->setContentsMargins(0, 0, 0, 0);
114     layout->addWidget(edit);
115     layout->addLayout(buttonLayout);
116     buttonLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Ignored,
117                                           QSizePolicy::MinimumExpanding));
118 }
119 
PathListEditor(QWidget * parent)120 PathListEditor::PathListEditor(QWidget *parent) :
121         QWidget(parent),
122         d(new PathListEditorPrivate)
123 {
124     setLayout(d->layout);
125     addButton(tr("Insert..."), this, [this](){
126         const QString dir = QFileDialog::getExistingDirectory(this, d->fileDialogTitle);
127         if (!dir.isEmpty())
128             insertPathAtCursor(QDir::toNativeSeparators(dir));
129     });
130     addButton(tr("Delete Line"), this, [this](){
131         deletePathAtCursor();
132     });
133     addButton(tr("Clear"), this, [this](){
134         d->edit->clear();
135     });
136 }
137 
~PathListEditor()138 PathListEditor::~PathListEditor()
139 {
140     delete d;
141 }
142 
addButton(const QString & text,QObject * parent,std::function<void ()> slotFunc)143 QPushButton *PathListEditor::addButton(const QString &text, QObject *parent,
144                                        std::function<void()> slotFunc)
145 {
146     return insertButton(d->buttonLayout->count() - 1, text, parent, slotFunc);
147 }
148 
insertButton(int index,const QString & text,QObject * parent,std::function<void ()> slotFunc)149 QPushButton *PathListEditor::insertButton(int index /* -1 */, const QString &text, QObject *parent,
150                                           std::function<void()> slotFunc)
151 {
152     auto rc = new QPushButton(text, this);
153     QObject::connect(rc, &QPushButton::pressed, parent, slotFunc);
154     d->buttonLayout->insertWidget(index, rc);
155     return rc;
156 }
157 
pathListString() const158 QString PathListEditor::pathListString() const
159 {
160     return pathList().join(HostOsInfo::pathListSeparator());
161 }
162 
pathList() const163 QStringList PathListEditor::pathList() const
164 {
165     const QString text = d->edit->toPlainText().trimmed();
166     if (text.isEmpty())
167         return QStringList();
168     // trim each line
169     QStringList rc = text.split('\n', Qt::SkipEmptyParts);
170     const QStringList::iterator end = rc.end();
171     for (QStringList::iterator it = rc.begin(); it != end; ++it)
172         *it = it->trimmed();
173     return rc;
174 }
175 
setPathList(const QStringList & l)176 void PathListEditor::setPathList(const QStringList &l)
177 {
178     d->edit->setPlainText(l.join(QLatin1Char('\n')));
179 }
180 
setPathList(const QString & pathString)181 void PathListEditor::setPathList(const QString &pathString)
182 {
183     if (pathString.isEmpty()) {
184         clear();
185     } else {
186         setPathList(pathString.split(HostOsInfo::pathListSeparator(),
187                                      Qt::SkipEmptyParts));
188     }
189 }
190 
fileDialogTitle() const191 QString PathListEditor::fileDialogTitle() const
192 {
193     return d->fileDialogTitle;
194 }
195 
setFileDialogTitle(const QString & l)196 void PathListEditor::setFileDialogTitle(const QString &l)
197 {
198     d->fileDialogTitle = l;
199 }
200 
clear()201 void PathListEditor::clear()
202 {
203     d->edit->clear();
204 }
205 
text() const206 QString PathListEditor::text() const
207 {
208     return d->edit->toPlainText();
209 }
210 
setText(const QString & t)211 void PathListEditor::setText(const QString &t)
212 {
213     d->edit->setPlainText(t);
214 }
215 
insertPathAtCursor(const QString & path)216 void PathListEditor::insertPathAtCursor(const QString &path)
217 {
218     // If the cursor is at an empty line or at end(),
219     // just insert. Else insert line before
220     QTextCursor cursor = d->edit->textCursor();
221     QTextBlock block = cursor.block();
222     const bool needNewLine = !block.text().isEmpty();
223     if (needNewLine) {
224         cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor);
225         cursor.insertBlock();
226         cursor.movePosition(QTextCursor::PreviousBlock, QTextCursor::MoveAnchor);
227     }
228     cursor.insertText(path);
229     if (needNewLine) {
230         cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor);
231         d->edit->setTextCursor(cursor);
232     }
233 }
234 
deletePathAtCursor()235 void PathListEditor::deletePathAtCursor()
236 {
237     // Delete current line
238     QTextCursor cursor = d->edit->textCursor();
239     if (cursor.block().isValid()) {
240         cursor.movePosition(QTextCursor::StartOfLine, QTextCursor::MoveAnchor);
241         // Select down or until end of [last] line
242         if (!cursor.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor))
243             cursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor);
244         cursor.removeSelectedText();
245         d->edit->setTextCursor(cursor);
246     }
247 }
248 
249 } // namespace Utils
250