1 //=============================================================================
2 // MusE Score
3 // Linux Music Score Editor
4 //
5 // Copyright (C) 2002-2008 Werner Schweer and others
6 //
7 // This program is free software; you can redistribute it and/or modify
8 // it under the terms of the GNU General Public License version 2.
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, write to the Free Software
17 // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18 //=============================================================================
19
20 #include "metaedit.h"
21 #include "libmscore/score.h"
22 #include "libmscore/undo.h"
23 #include "musescore.h"
24 #include "preferences.h"
25 #include "icons.h"
26 #include "openfilelocation.h"
27
28 namespace Ms {
29
30 //---------------------------------------------------------
31 // MetaEditDialog
32 //---------------------------------------------------------
33
MetaEditDialog(Score * score,QWidget * parent)34 MetaEditDialog::MetaEditDialog(Score* score, QWidget* parent)
35 : QDialog(parent),
36 m_score(score),
37 m_dirty(false)
38 {
39 setObjectName("MetaEditDialog");
40
41 setupUi(this);
42 QDialog::setWindowFlag(Qt::WindowContextHelpButtonHint, false);
43 QDialog::setWindowFlag(Qt::WindowMinMaxButtonsHint);
44
45 version->setText(m_score->mscoreVersion());
46 level->setText(QString::number(m_score->mscVersion()));
47
48 int rev = m_score->mscoreRevision();
49 if (rev > 99999) // MuseScore 1.3 is decimal 5702, 2.0 and later uses a 7-digit hex SHA
50 revision->setText(QString::number(rev, 16));
51 else
52 revision->setText(QString::number(rev, 10));
53
54 QString currentFileName = score->masterScore()->fileInfo()->absoluteFilePath();
55 QString previousFileName = score->importedFilePath();
56 if (previousFileName.isEmpty() || previousFileName == currentFileName) // New score or no "Save as" used
57 filePath->setText(previousFileName);
58 else
59 filePath->setText(QString("%1\n%2\n%3")
60 .arg(QFileInfo::exists(currentFileName) ? currentFileName : "<b>" + tr("Not saved yet,") + "</b>",
61 tr("initially read from:"), previousFileName));
62 filePath->setTextInteractionFlags(Qt::TextSelectableByMouse);
63
64 QMapIterator<QString, QString> iterator(score->metaTags());
65 while (iterator.hasNext()) {
66 iterator.next();
67 const QString key = iterator.key();
68 addTag(key, iterator.value(), isBuiltinTag(key));
69 }
70
71 scrollAreaLayout->setColumnStretch(1, 1); // The 'value' column should be expanding
72
73 connect(newButton, &QPushButton::clicked, this, &MetaEditDialog::newClicked);
74 connect(buttonBox, SIGNAL(clicked(QAbstractButton*)), SLOT(buttonBoxClicked(QAbstractButton*)));
75 buttonBox->button(QDialogButtonBox::Save)->setEnabled(m_dirty);
76 if (!QFileInfo::exists(score->importedFilePath()))
77 revealButton->setEnabled(false);
78
79 revealButton->setIcon(*icons[int(Icons::fileOpen_ICON)]);
80 revealButton->setToolTip(OpenFileLocation::platformText());
81
82 connect(revealButton, &QPushButton::clicked, this, &MetaEditDialog::openFileLocation);
83 MuseScore::restoreGeometry(this);
84 }
85
86 //---------------------------------------------------------
87 // addTag
88 /// Add a tag to the displayed list
89 /// returns a pair of widget corresponding to the key and value:
90 /// QPair<QLineEdit* key, QLineEdit* value>
91 //---------------------------------------------------------
92
addTag(const QString & key,const QString & value,const bool builtinTag)93 QPair<QLineEdit*, QLineEdit*> MetaEditDialog::addTag(const QString& key, const QString& value, const bool builtinTag)
94 {
95 QLineEdit* tagWidget = new QLineEdit(key);
96 QLineEdit* valueWidget = new QLineEdit(value);
97
98 connect(valueWidget, &QLineEdit::textChanged, this, [this]() { setDirty(); });
99
100 const int numFlags = scrollAreaLayout->rowCount();
101 if (builtinTag) {
102 tagWidget->setReadOnly(true);
103 // Make it clear that builtin tags are not editable
104 tagWidget->setStyleSheet("QLineEdit { background: transparent; }");
105 tagWidget->setFrame(false);
106 tagWidget->setFocusPolicy(Qt::NoFocus);
107 tagWidget->setToolTip(tr("This is a builtin tag. Its name cannot be modified."));
108 }
109 else {
110 tagWidget->setPlaceholderText(tr("Name"));
111 QToolButton* deleteButton = new QToolButton();
112 deleteButton->setIcon(*icons[int (Icons::bin_ICON)]);
113
114 // follow gui scaling. The '+ 2' at the end is the margin. (2 * 1px).for top and bottoms.
115 const double size = preferences.getInt(PREF_UI_THEME_ICONWIDTH) * guiScaling * .5 + 2;
116 deleteButton->setIconSize(QSize(size, size));
117
118 connect(tagWidget, &QLineEdit::textChanged, this, [this]() { setDirty(); });
119 connect(deleteButton, &QToolButton::clicked, this,
120 [this, tagWidget, valueWidget, deleteButton]() { setDirty();
121 tagWidget->deleteLater();
122 valueWidget->deleteLater();
123 deleteButton->deleteLater(); });
124
125 scrollAreaLayout->addWidget(deleteButton, numFlags, 2);
126 }
127 scrollAreaLayout->addWidget(tagWidget, numFlags, 0);
128 scrollAreaLayout->addWidget(valueWidget, numFlags, 1);
129
130 return QPair<QLineEdit*, QLineEdit*>(tagWidget, valueWidget);
131 }
132
133 //---------------------------------------------------------
134 // newClicked
135 /// When the 'New' button is clicked, a new tag is appended,
136 /// and focus is set to the QLineEdit corresponding to its name.
137 //---------------------------------------------------------
138
newClicked()139 void MetaEditDialog::newClicked()
140 {
141 QPair<QLineEdit*, QLineEdit*> pair = addTag("", "", false);
142
143 pair.first->setFocus();
144 pair.second->setPlaceholderText(tr("Value"));
145 // scroll down to see the newly created tag.
146 // ugly workaround because scrolling to maximum doesn't completely scroll
147 // to the maximum, for some unknow reason.
148 // See https://www.qtcentre.org/threads/32852-How-can-I-always-keep-the-scroll-bar-at-the-bottom-of-a-QScrollArea
149 QScrollBar* scrollBar = scrollArea->verticalScrollBar();
150 scrollBar->setMaximum(scrollBar->maximum() + 1);
151 scrollBar->setValue(scrollBar->maximum());
152
153 setDirty();
154 }
155
156 //---------------------------------------------------------
157 // isBuiltinTag
158 /// returns true if the tag is one of Musescore's builtin tags
159 /// see also MasterScore::MasterScore()
160 //---------------------------------------------------------
161
isBuiltinTag(const QString & tag) const162 bool MetaEditDialog::isBuiltinTag(const QString& tag) const
163 {
164 return (tag == "platform" || tag == "movementNumber" || tag == "movementTitle"
165 || tag == "workNumber" || tag == "workTitle" || tag == "arranger"
166 || tag == "composer" || tag == "lyricist" || tag == "poet"
167 || tag == "translator" || tag == "source" || tag == "copyright"
168 || tag == "creationDate");
169 }
170
171 //---------------------------------------------------------
172 // setDirty
173 /// Sets the editor as having unsaved changes
174 //---------------------------------------------------------
175
setDirty(const bool dirty)176 void MetaEditDialog::setDirty(const bool dirty)
177 {
178 if (dirty == m_dirty)
179 return;
180
181 buttonBox->button(QDialogButtonBox::Save)->setEnabled(dirty);
182 setWindowTitle(tr("Score properties: %1%2").arg(m_score->title()).arg((dirty ? "*" : "")));
183
184 m_dirty = dirty;
185 }
186
187 //---------------------------------------------------------
188 // openFileLocation
189 /// Opens the file location with a QMessageBox::warning on failure
190 //---------------------------------------------------------
191
openFileLocation()192 void MetaEditDialog::openFileLocation()
193 {
194 if (!OpenFileLocation::openFileLocation(filePath->text()))
195 QMessageBox::warning(this, tr("Open Containing Folder Error"),
196 tr("Could not open containing folder"));
197 }
198
199 //---------------------------------------------------------
200 // buttonBoxClicked
201 //---------------------------------------------------------
202
buttonBoxClicked(QAbstractButton * button)203 void MetaEditDialog::buttonBoxClicked(QAbstractButton* button)
204 {
205 switch (buttonBox->buttonRole(button)) {
206 case QDialogButtonBox::ApplyRole:
207 save();
208 break;
209 case QDialogButtonBox::AcceptRole:
210 accept();
211 // fall through
212 case QDialogButtonBox::RejectRole:
213 close();
214 default:
215 break;
216 }
217 }
218
219 //---------------------------------------------------------
220 // save
221 /// Save the currently displayed metatags
222 //---------------------------------------------------------
223
save()224 bool MetaEditDialog::save()
225 {
226 if (m_dirty) {
227 const int idx = scrollAreaLayout->rowCount();
228 QMap<QString, QString> map;
229 for (int i = 0; i < idx; ++i) {
230 QLayoutItem *tagItem = scrollAreaLayout->itemAtPosition(i, 0);
231 QLayoutItem *valueItem = scrollAreaLayout->itemAtPosition(i, 1);
232 if (tagItem && valueItem) {
233 QLineEdit *tag = static_cast<QLineEdit*>(tagItem->widget());
234 QLineEdit *value = static_cast<QLineEdit*>(valueItem->widget());
235
236 QString tagText = tag->text();
237 if (tagText.isEmpty()) {
238 QMessageBox::warning(this, tr("MuseScore"),
239 tr("Tags can't have empty names."),
240 QMessageBox::Ok, QMessageBox::Ok);
241 tag->setFocus();
242 return false;
243 }
244 if (map.contains(tagText)) {
245 if (isBuiltinTag(tagText)) {
246 QMessageBox::warning(this, tr("MuseScore"),
247 tr("%1 is a reserved builtin tag.\n"
248 "It can't be used.").arg(tagText),
249 QMessageBox::Ok, QMessageBox::Ok);
250 tag->setFocus();
251 return false;
252 }
253 QMessageBox::warning(this, tr("MuseScore"),
254 tr("You have multiple tags with the same name."),
255 QMessageBox::Ok, QMessageBox::Ok);
256 tag->setFocus();
257 return false;
258 }
259 map.insert(tagText, value->text());
260 }
261 else
262 qDebug("MetaEditDialog: abnormal configuration: %i", i);
263 }
264 m_score->undo(new ChangeMetaTags(m_score, map));
265 setDirty(false);
266 }
267 return true;
268 }
269
270 //---------------------------------------------------------
271 // accept
272 /// Reimplemented to save modifications before closing the dialog.
273 //---------------------------------------------------------
274
accept()275 void MetaEditDialog::accept()
276 {
277 if (!save())
278 return;
279
280 QDialog::accept();
281 }
282
283 //---------------------------------------------------------
284 // hideEvent
285 /// Reimplemented to notify the user that he/she is quitting without saving
286 //---------------------------------------------------------
287
closeEvent(QCloseEvent * event)288 void MetaEditDialog::closeEvent(QCloseEvent* event)
289 {
290 if (m_dirty) {
291 QMessageBox::StandardButton button = QMessageBox::warning(this, tr("MuseScore"),
292 tr("You have unsaved changes.\nSave?"),
293 QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
294 QMessageBox::Save);
295 if (button == QMessageBox::Save) {
296 if (!save()) {
297 event->ignore();
298 return;
299 }
300 }
301 else if (button == QMessageBox::Cancel) {
302 event->ignore();
303 return;
304 }
305 }
306 MuseScore::saveGeometry(this);
307 event->accept();
308 }
309
310 } // namespace Ms
311