diff --git a/TODO.md b/TODO.md index c600123ad95ab26189d3d8994ce179d29a7855c8..6deb750952eceb49fc9972bbc2e2752baab61c63 100644 --- a/TODO.md +++ b/TODO.md @@ -12,7 +12,6 @@ - fix detecting last note duration in exercises/exams - randomly generated melodies have no rests yet, but see below for a reason - add an option to skip rests (pitch detection gaps) and treat their duration as previous note was longer - - manual adding/editing ties - finish tuner, functionality similar to Android one (show tuning, middle A freq) - level creator: - question page icon might change depends on is there single note or a melody @@ -21,7 +20,6 @@ - music XML import dialog, to select voice or staff if there are more, and so - exam summary - give more valuable info, wear it nicely - charts - show preview of entire melody, chart tip is not suitable for it - - charts - revert analyzing latest exercise - \[Taction\] add common method for creating key shortcuts - migrate on preferred 48000 sample rate (ogg files and audio methods). DO NOT forget to resize output samples length diff --git a/changes b/changes index 9f2f4786483b73eb026993e1f588062e49c2c44c..ca4b1499f8adbdf2f576ba96981b6e6c3427e066 100644 --- a/changes +++ b/changes @@ -1,6 +1,7 @@ 1.7.1 beta2 - improved analysis charts (look and behavior) - added handy drawer with exam list and chart options + - notes with the same pitches can be tied by user - disabling level creator pages which are currently unused - added help topic selector on 'help page' with all the context - make controls look (check boxes, radios, sliders) consistent diff --git a/src/libs/core/score/tscoreobject.cpp b/src/libs/core/score/tscoreobject.cpp index 3f92fcdab081e5c914d946923847c03a0e423eb0..097fcaa244438c0685e52863a9c480c566a198a6 100644 --- a/src/libs/core/score/tscoreobject.cpp +++ b/src/libs/core/score/tscoreobject.cpp @@ -1083,6 +1083,12 @@ void TscoreObject::enableActions() { m_lowerAct = new Taction(tr("lower", "as such as flats lower note"), QStringLiteral("tipbg"), this); connect(m_lowerAct, &Taction::triggered, this, &TscoreObject::handleNoteAction); m_lowerAct->setShortcut(createQmlShortcut(m_qmlComponent, "\"@\"")); + + m_tieAct = new Taction(QGuiApplication::translate("ScoreToolbox", "tie", + "To translate it properly, check please meaning of 'tie' in musical context."), + QStringLiteral("tipbg"), this); + connect(m_tieAct, &Taction::triggered, this, &TscoreObject::checkTieOfSelected); + m_tieAct->setShortcut(createQmlShortcut(m_qmlComponent, "\"l\"")); } } @@ -1154,6 +1160,54 @@ void TscoreObject::handleNoteAction() { } } + +void TscoreObject::checkTieOfSelected() { + if (m_selectedItem && m_selectedItem->index() > 0) { + auto prevNote = m_segments[m_selectedItem->index() - 1]; + auto n = *m_selectedItem->note(); + if (m_selectedItem->note()->rtm.tie() > Trhythm::e_tieStart) { // disconnect + prevNote->disconnectTie(TnotePair::e_untiePrev); + n.rtm.setTie(n.rtm.tie() == Trhythm::e_tieEnd ? Trhythm::e_noTie : Trhythm::e_tieStart); + m_selectedItem->wrapper()->setNote(n); + emit m_selectedItem->hasTieChanged(); + if (m_selectedItem->staff()->firstNote()->item() == m_selectedItem) + m_selectedItem->staff()->deleteExtraTie(); + } else { + if (!m_selectedItem->note()->isRest() && m_selectedItem->note()->chromatic() == prevNote->note()->chromatic()) { + n.rtm.setTie(n.rtm.tie() == Trhythm::e_noTie ? Trhythm::e_tieEnd : Trhythm::e_tieCont); + m_selectedItem->wrapper()->setNote(n); + auto pn = *prevNote->note(); + pn.rtm.setTie(pn.rtm.tie() == Trhythm::e_noTie ? Trhythm::e_tieStart : Trhythm::e_tieCont); + prevNote->setNote(pn); + emit m_selectedItem->hasTieChanged(); + if (m_selectedItem->staff()->firstNote()->item() == m_selectedItem) + m_selectedItem->staff()->createExtraTie(m_selectedItem); + } + } + auto notesForAlterCheck = tieRange(prevNote->item()); + bool fitStaff = false; + auto measureToRefresh = m_segments[notesForAlterCheck.x()]->item()->measure(); + notesForAlterCheck.setX(m_segments[notesForAlterCheck.x()]->item()->measure()->firstNoteId()); + notesForAlterCheck.setY(m_segments[notesForAlterCheck.y()]->item()->measure()->lastNoteId()); + for (int i = notesForAlterCheck.x(); i <= notesForAlterCheck.y(); ++i) { + if (m_segments[i]->note()->note() == n.note()) { + fitStaff = true; + m_segments[i]->item()->updateAlter(); + } + if (m_segments[i]->item()->measure() != measureToRefresh) { + measureToRefresh->refresh(); + measureToRefresh = m_segments[i]->item()->measure(); + } + } + measureToRefresh->refresh(); + if (fitStaff) { + m_segments[notesForAlterCheck.x()]->item()->staff()->fit(); + if (m_segments[notesForAlterCheck.y()]->item()->staff() != m_segments[notesForAlterCheck.x()]->item()->staff()) + m_segments[notesForAlterCheck.y()]->item()->staff()->fit(); + } + } +} + //################################################################################################# //################### PROTECTED ############################################ //################################################################################################# diff --git a/src/libs/core/score/tscoreobject.h b/src/libs/core/score/tscoreobject.h index 80f9cea7ab9944acd760a898da2e4eb1871274f8..a5ad5785af9fcc6212ccb215692c82fac0a206e9 100644 --- a/src/libs/core/score/tscoreobject.h +++ b/src/libs/core/score/tscoreobject.h @@ -420,6 +420,7 @@ public: Taction* dotNoteAct() { return m_dotNoteAct; } Taction* riseAct() { return m_riseAct; } Taction* lowerAct() { return m_lowerAct; } + Taction* tieAct() { return m_tieAct; } Q_INVOKABLE void enableActions(); @@ -436,6 +437,15 @@ public: TclefOffset clefOffset() const { return m_clefOffset; } + /** + * Connects or disconnects selected note (if any) with previous one if their pitches are the same. + * Set/unset ties, emits @p hasTieChanged() to inform score toolbox. + * Checks alters of all affected notes (adding tie on measure beginning changes the rule of displaying alter) + * Fit staff if necessary. + * Adds/removes extra tie at the staff beginning when needed + */ + Q_INVOKABLE void checkTieOfSelected(); + signals: void meterChanged(); @@ -703,6 +713,7 @@ private: Taction *m_insertNoteAct = nullptr; Taction *m_riseAct = nullptr; Taction *m_lowerAct = nullptr; + Taction *m_tieAct = nullptr; }; diff --git a/src/main/tmainscoreobject.cpp b/src/main/tmainscoreobject.cpp index 5fc20f97b3f8ce6bfab79edb2f6ea9c2d824ea89..462769b48b0f860c09b9b58bfff78234eb081c75 100644 --- a/src/main/tmainscoreobject.cpp +++ b/src/main/tmainscoreobject.cpp @@ -146,7 +146,7 @@ void TmainScoreObject::setScoreObject(TscoreObject* scoreObj) { m_scoreActions << m_scoreObj->insertNoteAct() << m_scoreObj->deleteNoteAct() << m_scoreObj->clearScoreAct() << m_notesMenuAct; m_noteActions << m_scoreObj->riseAct() << m_scoreObj->lowerAct() << m_scoreObj->wholeNoteAct() << m_scoreObj->halfNoteAct() << m_scoreObj->quarterNoteAct() << m_scoreObj->eighthNoteAct() - << m_scoreObj->sixteenthNoteAct() << m_scoreObj->restNoteAct() << m_scoreObj->dotNoteAct(); + << m_scoreObj->sixteenthNoteAct() << m_scoreObj->restNoteAct() << m_scoreObj->dotNoteAct() << m_scoreObj->tieAct(); connect(m_nextNoteAct, &Taction::triggered, [=]{ if (!GLOB->isSingleNote()) { diff --git a/src/qml/ScoreMenuContent.qml b/src/qml/ScoreMenuContent.qml index a108587dc4c75cef90b95e11247e3625ac075cb8..7806db02f5c7fb17ee5952d4249cb071a07d486c 100644 --- a/src/qml/ScoreMenuContent.qml +++ b/src/qml/ScoreMenuContent.qml @@ -36,7 +36,7 @@ Tmenu { } } - property var rGlyphs: [ "#", "b", "C", "D", "E", "F", "G", "\ue109", "." ] + property var rGlyphs: [ "#", "b", "C", "D", "E", "F", "G", "\ue109", ".", "\ue18c" ] Loader { id: notesLoader; sourceComponent: notesComp; active: false } Component { id: notesComp @@ -50,6 +50,7 @@ Tmenu { padding: 0 width: menu.width contentItem: MenuButton { + id: mb action: modelData onClicked: close() Rectangle { width: parent.width; height: index === score.noteActions.length - 1 ? 0 : 1; color: activPal.text; y: parent.height - 1 } @@ -58,6 +59,8 @@ Tmenu { color: activPal.text x: (Noo.fontSize() * 3.2 - width) / 2; anchors.verticalCenter: parent.verticalCenter font { pixelSize: Noo.fontSize() * 1.5; family: "nootka"; } + scale: GLOB.useAnimations && !mb.containsPress && mb.containsMouse ? 1.4 : 1.0 + Behavior on scale { enabled: GLOB.useAnimations; NumberAnimation { duration: 150 }} } } } diff --git a/src/qml/score/ControlButton.qml b/src/qml/score/ControlButton.qml index 59f06a4a491e99b63c4b17b5a049010440f58a61..f853ce9616e997868c8f76a2f26320fe78d1f2fc 100644 --- a/src/qml/score/ControlButton.qml +++ b/src/qml/score/ControlButton.qml @@ -30,7 +30,7 @@ Rectangle { Text { id: txt - x: (factor * 2 - width) / 2 + x: (cb.width - width) / 2 height: factor * 3 style: Text.Normal color: cb.enabled ? activPal.text : disdPal.text diff --git a/src/qml/score/ScoreToolbox.qml b/src/qml/score/ScoreToolbox.qml index 79d052084573955707035b1e4762eb606b28bf16..e6027549b219844f0b41b868bb033406107822fa 100644 --- a/src/qml/score/ScoreToolbox.qml +++ b/src/qml/score/ScoreToolbox.qml @@ -20,7 +20,7 @@ ControlBase { property string rhythmText: Noo.rhythmText(scoreObj.workRhythm) property bool triplet: false - property bool tie: false + property bool tie: scoreObj.selectedItem && scoreObj.selectedItem.hasTie // Accidentals property int selectedId: idArray[scoreObj.cursorAlter + 2] @@ -87,7 +87,7 @@ ControlBase { Loader { // triplet id: tripLoad sourceComponent: ctrlButtonComp - onLoaded: { item.rhythm = 0; item.text = "\u0183"; item.enabled = false } + onLoaded: { item.rhythm = 0; item.text = " " /*"\u0183"*/ } Binding { target: tripLoad.item; property: "selected"; value: toolbox.triplet } // Connections { // target: tripLoad.item @@ -108,17 +108,23 @@ ControlBase { } ControlButton { // tie - visible: false //score.meter !== Tmeter.NoMeter + visible: score.meter !== Tmeter.NoMeter anchors.horizontalCenter: parent.horizontalCenter - factor: toolbox.factor * 1.2 + factor: toolbox.factor selected: toolbox.tie - height: factor * 1.5 - yOffset: factor * -2.2 + height: factor * 1.5; width: factor * 2.7 + yOffset: factor * -2.3 font { family: "nootka"; pointSize: factor * 3.6 } text: "\ue18c" -// onClicked: toolbox.tie = !selected - onEntered: hideTimer.stop() - onExited: hideTimer.restart() + onClicked: scoreObj.checkTieOfSelected() + onEntered: { + hideTimer.stop() + Noo.setStatusTip(qsTr("Tie - connect or disconnect selected note with previous one if both notes have the same pitch.") + "<br><b>(L)</b>", Item.Top) + } + onExited: { + hideTimer.restart() + Noo.setStatusTip("", Item.Top) + } } Rectangle { visible: scoreObj.enableTechnical; width: toolbox.width; height: 1; color: activPal.text }