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