From efeae2957c96138cf7e59849bc0ce68eeb9be192 Mon Sep 17 00:00:00 2001 From: Michele Spagnolo Date: Fri, 28 Apr 2023 16:27:06 +0200 Subject: [PATCH] A deep overhaul of dynamics and expression 1. Don't automatically flag as "other" dynamic with text. As long as the string contains the substring identifying the dynamic type, we should retain the type. NOTE: this also resolves the issue that dynamics with text don't have playback. 2. Keep dynamics aligned properly (i.e. dynamic under the notehead) when custom text is added. This is now the defaul, with an option to revert to old behaviour. 3. Offset dynamis laterally to avoide collision with barlines (with toggle/untoggle option). In general, dynamics must be detached from text settings. Specifically: 4. Dynamics symbols now have the possibility to override the score music font (independently of text font) 5. Dynamics symbols can have custom size (independent from text size) expressed as a percentage of the default. 6. Dynamics text (i.e. text typed into a dynamic) must be treated as expression text, and it now takes all its defaults from there. The Style -> Text Styles -> Dynamics page has been removed. 7. Expressions have a new dedicated class of their own. "Old" expressions, i.e. staff text with an "expression" style, are mapped into the new expression item preserving the old layout. 8. Expressions entered on the same chord as a dynamic mark will automatically align to the dynamic (with toggle/untoggle option). 9. An expression + dynamics snapped pair is dragged together. 10. Long awaited quality-of-life improvement: when dropping (or clicking and entering) a new dynamic onto an existing one, it will replace the old one with the new one (instead of having to delete and re-enter to change a dynamic). 11. A new inspector panel for Dynamics, containing all the relevant applicable settings. Some of these settings are moved here from the Text inspector panel, mainly the Above - Below and the Frame Settings. 12. The Text inspector panel is not shown when a "pure" dynamic is selected. It is only shown when a dynamic+custom text is selected, and the options only refer to the text part. 13. New options in the Style -> Dynamics submenu. 14. A new inspector panel for Expressions, containing (for now) only the "Snap to dynamics" option. --- fonts/mscore/MusescoreIcon.ttf | Bin 147748 -> 151472 bytes src/engraving/layout/layoutsystem.cpp | 18 +- src/engraving/libmscore/chordrest.cpp | 1 + src/engraving/libmscore/dynamic.cpp | 265 +++++++++++-- src/engraving/libmscore/dynamic.h | 15 + .../libmscore/dynamichairpingroup.cpp | 63 +++ src/engraving/libmscore/dynamichairpingroup.h | 24 ++ src/engraving/libmscore/edit.cpp | 5 + src/engraving/libmscore/engravingobject.cpp | 3 +- src/engraving/libmscore/engravingobject.h | 3 + src/engraving/libmscore/expression.cpp | 204 ++++++++++ src/engraving/libmscore/expression.h | 41 ++ src/engraving/libmscore/factory.cpp | 9 + src/engraving/libmscore/factory.h | 2 + src/engraving/libmscore/libmscore.cmake | 2 + src/engraving/libmscore/measure.cpp | 1 + src/engraving/libmscore/mscore.h | 4 + src/engraving/libmscore/note.cpp | 1 + src/engraving/libmscore/paste.cpp | 1 + src/engraving/libmscore/property.cpp | 5 + src/engraving/libmscore/property.h | 5 + src/engraving/libmscore/rest.cpp | 1 + src/engraving/libmscore/segment.cpp | 5 + src/engraving/libmscore/shape.cpp | 4 +- src/engraving/libmscore/textbase.cpp | 35 +- src/engraving/rw/400/measurerw.cpp | 25 +- src/engraving/rw/400/tread.cpp | 19 +- src/engraving/rw/400/tread.h | 2 + src/engraving/rw/400/twrite.cpp | 19 +- src/engraving/rw/400/twrite.h | 4 +- src/engraving/style/styledef.cpp | 9 +- src/engraving/style/styledef.h | 8 +- src/engraving/style/textstyle.cpp | 38 +- src/engraving/style/textstyle.h | 1 + src/engraving/types/types.h | 1 + src/engraving/types/typesconv.cpp | 1 + src/framework/ui/view/iconcodes.h | 4 + src/inspector/CMakeLists.txt | 4 + .../models/abstractinspectormodel.cpp | 28 +- src/inspector/models/abstractinspectormodel.h | 4 +- src/inspector/models/inspectorlistmodel.cpp | 8 +- src/inspector/models/inspectorlistmodel.h | 3 +- .../models/inspectormodelcreator.cpp | 7 +- .../dynamics/dynamicsettingsmodel.cpp | 159 ++++++++ .../notation/dynamics/dynamicsettingsmodel.h | 82 ++++ .../expressions/expressionsettingsmodel.cpp | 39 ++ .../expressions/expressionsettingsmodel.h | 28 ++ .../models/text/textsettingsmodel.cpp | 59 +++ src/inspector/models/text/textsettingsmodel.h | 13 +- src/inspector/types/texttypes.h | 1 + src/inspector/view/inspector_resources.qrc | 3 + .../FlatRadioButtonGroupPropertyView.qml | 5 +- .../Inspector/common/FrameSettings.qml | 169 ++++++++ .../common/InspectorPropertyView.qml | 1 + .../NotationInspectorSectionLoader.qml | 14 + .../notation/dynamics/DynamicsSettings.qml | 159 ++++++++ .../expressions/ExpressionsSettings.qml | 88 +++++ .../Inspector/text/TextInspectorView.qml | 1 + .../MuseScore/Inspector/text/TextSettings.qml | 141 +------ src/notation/view/widgets/editstyle.cpp | 42 +- src/notation/view/widgets/editstyle.ui | 365 ++++++++++++------ src/palette/internal/palettecreator.cpp | 7 +- 62 files changed, 1934 insertions(+), 344 deletions(-) create mode 100644 src/engraving/libmscore/expression.cpp create mode 100644 src/engraving/libmscore/expression.h create mode 100644 src/inspector/models/notation/dynamics/dynamicsettingsmodel.cpp create mode 100644 src/inspector/models/notation/dynamics/dynamicsettingsmodel.h create mode 100644 src/inspector/models/notation/expressions/expressionsettingsmodel.cpp create mode 100644 src/inspector/models/notation/expressions/expressionsettingsmodel.h create mode 100644 src/inspector/view/qml/MuseScore/Inspector/common/FrameSettings.qml create mode 100644 src/inspector/view/qml/MuseScore/Inspector/notation/dynamics/DynamicsSettings.qml create mode 100644 src/inspector/view/qml/MuseScore/Inspector/notation/expressions/ExpressionsSettings.qml diff --git a/fonts/mscore/MusescoreIcon.ttf b/fonts/mscore/MusescoreIcon.ttf index 7d6edb15f03f25b0c668ed51f80c651a98aa82ef..0e3708d0ad6f5968ab29706666947b11d5020581 100644 GIT binary patch delta 6156 zcmcIo4Rn;%nSS2;&EGdanV)ZdlF1~Q%p{ZlkjzXn0Rjnx1S|*$kpf~3iWqB9wCExm zI~4y?jV&Hn@rPEdtcSXKWR)(lM$0*M_pp2P9JhzPPNxTQ)(C04AG>@!JVtFA2e z?9QC$yZ5{A-@W&J?)!Z+ySM0jAJ(%F03KdUKv!4yh4;1_&q6KQ&e6!+Ih|dolK3mQG1_#=kxnkOAH=yJFqy^+zu~TnN|>a_sUeZrTvFc8rby!7|FDYu8`3?yh^c zt^n+{fMfkttG~6L;~x53!`60H-_6;zU;oYNp8`d#P(}T{SFT=D@N)NSbmpW&b1w&M z%JggA&*6Pp@45{eUoR-VmG?&g%dx)eu2?PoANMmL<(5mpyY_G}(5E+4IZ^ z^_+R-*yz~k=?}&p94n`#Hi^i12w)XQa$6ta#<#O|!UhG?iSM3xBbiTeR7b*#*jZIEEAFDK~3&U@!7;uw%eU3Y|df+>p|OBuYBK4Kh;??e`5bXGPDl{z&E47H4Jb~H_+!NDC?u&Yc~Sd^^$Uw zT~C|r4eh`N55a&NMt~dX^u|2|o!qn-_^SoP0I<sS>7V6wWom+;1Z`1DE^pWi*jsctViQT|=7K zZ-#)kb^-snh8PBZHxD@01N=Ts^b@psJ4$eZZ?peSKCzQ0n|}Xr3V4_PPEda0An@J* z@P3vG@4pIsFhqO`oNOnK10%bD5840l2=GT5ezbsi5je%2`FJz%CqJ{A9j zT}vDSMt1{agDgz>#1KSYfv}#4)&nv1Ld?4%R?4g|LbA4Dh|WQsiuN_cVTgmqPL4U> zgSgs>+lYgZyqyrY6XIe0^XAY`aNNh%cR$3SQ)4H@KL80VfCO2~g8Lw$Cm{KAAO*B3 zI1CB9>d1x=>2OA+jr0v6$KSDo0qHH&H9;5F)PRNf(Ab)p| zkIpA|LiXMUd77*J2_5clfeb|<&r#|5LC6aXbl@e(&-X$8$pbm~DdeBGLjHvgU$zkk zA^+M3IYhgEr|z%V9?pln!QgLD=hw77vJvvz9>|-N9bFFjos+m9axBY0$M!&eKZn>3 zd3yoTOXT{8A@5Lrd=T=72FSaMAt$JJ;t1ru7a{NOf_$(Sa`HCFhzar`o&K>M@=-qI zR3+qN`uX@EaHbdntfO3}+eFSBmd}5H;2j%q>8wuL`wnF*oBiI6!e;BH;AACO`Aup4q zYKwXj4pcyzg8{GKGDQ;UjJn8ETIzYz?N*Q44P*2J!>14EWp?mz7F`|BDt0!>0qqVH zpp5-w&N5HpF&fuMEYajmH&2&jn;2fJrAEA|WQoYyRE4kD6E=1kMIGTpBHU4A?5cY> z(bCb;l6Y9Vy)1J2M#_yJ1j`Er;Y8qvqxVSXL)BxbeuyE)ANiZqX2$eoQHuQF;qyoWerujoaCe(IU0#!+pye-GLbgP-_8vW=+x10@c!}se2fosh zAa}3T#jc0-rEe{@7ua?Cm5aWx$}eB!%+KUR``IK9+PUWQ+5a6=8YDlP4En9-=7c;s zpc`R*U_cKWx$D{fxyd4(Uoe<3=Jb%u%K;O#_FHdg-+x29#E)mSvD3M6?FjD0c09&J z#r;L%w*>qatKZ5CzwuZi-JD8hnllZ}spd?wxv9A^%}Zvc@Y2BE3`vuU71T|Pb1_pd z%(k2Pj>jyaV&QQJ`gs;&IdLi(nkk`VFvLzaHBEx4Vv1A25Gir?TP*DQg8_da7&3e7 zBpEVu%gmfhMecxQnptW1Q6lb|IVC9AxJ*3-}qcjn| zZ^gyeo3+`Rt>r?CX<;htGO~s%fALL9U4M~Hw>-6}_eX`wlBbl#q=|_y;C_#2h51Sa zyqegpA||Ei`}AhP7`*(YQ&KVpkJsytM%}8$;?jgV4$Z97xV2`sQRr*bY?{rc#KjS- zSUeR0@U2&btnKwN8@`JDG5BlaA59&6fTvFwREthFEc6w}VGOdrFBdOW{ zm#m&wQq#>mE?F-tLD273mR$AZP>KBCB=$V>n*D5I-{jUW(hCj!PGMynup1G5$zPV( z^UQ1Z%M!cLWf-o}doI$A!rWx+1A4^Jm%@JfeYB~cX$Roqd(|||#R{y&_plR>3!h8k z=UHt^&y-ZMSe5&Osbo`{M=&5fNV#KNVdcS|F4lM^IaAuCxhb2jVQb}?wDLKUJFS77 zRVG=iv0&4rUYdB=LKDI~R&ffOLOEYextc(~dDoB#A7T?(+ z!K99QX8+1%wHIfKXQrZ~A9hyFYOC%#-RPQL@AumbbJ(nU#O<({(pt!_F0e$R1z{uW zb(Pc@?vh_Rf87*lbCsEvx5gJjX7V)oL-Dl!V>UU7i31=zQTqj?!auf z+3t3m?4GBbW>=mi7Q3}F?lG3eJb|aIc}B?9kSO)VL&@({&GYgZ&X;mi8s$CfO1RPJ*Mzgf3il<^Z6L-*=vzZ$9l*A%c`&(RM^Vw8&wbx=1 z#Uf_+Dcxn(yiSunQkq{^S*UDYo2emD+hjNCQ%ZG{$z--Dy}%h@0kV%hfHu=rlSG}0 zE-b?pxC;;9vD{x3XNa0T52iNfi29{G&AnKhi>LMK3{lT&{TWZQ<6NKgK}vs)>iIwVoIhvB+pXKPd!6Zoy?-w|l{Zf+p zN(%g5hh>7MU!A44;1@~ij|V7sckH#W$59bcK2Z)Uf};wlVq7Fak-bE@w(WYsW@AWn#;#aYK}8!2{r!y zGEr*JvhIv2t5^lU6XPK|4zbQ<$15buGHd2|U7o3Bj9iHEvYe}yEP-bNm~(ZVMx5JZ zM7P9q+T+Th@#*BAs6q40Kyp~@mPAoyL$I{g)3m5H)i&HTqcK`ABeS%k+>@~B0Yle@w4j&u z;0mih-_Qp2U`big^op{)u&+_Jlw_iXv9P~PS1#bPpGjSS7V#Zsk2SxiZ*|+kHI~@a3}ABDCW<1fhNN{M%q=aDfgH5e*q4kQTG4< delta 2436 zcmXYz4Nz3q8HS(l^5gO+8-EbZs#Hm$aj8f&HHrllbtg4SMxtoQB#fYI3c69WXlRxg z(i%%M+e(49jDc!1OvXm5WW-8mXlqlOp{-4w1}$kwstHCSDrjiM(03hn=Gk-3_w#<| ze)sM@d+d#v!+T?b7~s}dBrIQ^_eAJa>3W>SE?GS*a+WP;rd`0v?$-Lj)oW&FEK6Ot z1ZMY4o@?NO(F8b{;8A-Zo> z-)ntM{WJU546GU+hCX-Sn!ZqfQvZVfr-p~!{q8>Zm4uxM#R)~>>=Vf`SqZ^RrI(9V zZNVk#b#2w0G-9=mpCMYbE5(lP zfy8prt9#`Uo$8hH$CL@>1;@@;ha@}>Q6bK&e=0VePNlV?LT%3m-pBRK?+4@DvzB~jgG_FvxFH%U{ zr=q@2F$CZ0g_Vu)#7fZ)tL6$7U7Z3?N^4C3o>HB4ZIE9jf<3Ta0qa#P{H$&83MFqS z6AFAf8w&E^86`iXO+Bl^g&H>=gW^gt0Gm_6uLAyj*pe*d+ald>S3pS#l*+eNIoopJ zg>gdlUKoV!(hP1tqKQOys7P6^sD_;`Q2@Ilg-Os@u#q3ec zo_2Uqc`r(%B46~vcRivRUQ*#;s%V2sFTAYz%ayR#r~AKGVy{#|Re?~ztM&TrR)`+> zzC`!=gyyelgZpQ|52~SB;;$84_~{@tjDv$+@C$A1 z&1`t<7#!_}ca(SfjPC#MTi}lp{BsEYBIo-G`XC3o+Tat3{l_ob;nGUDTm+xa7nMRe zpQQ?)(7eYZ8sUnBKOcgt((9GKw*$VYhHLU)>(X1H4Xz_{L{PIEhvB9cH!nfI5(jd{ zS-7Rn+^&KzlSMrYx`b^1Rl)zIi%z)H3_~HfEAhJ${^~poXJUFqnU!EpGZv}WR^(YM zsvV1#ExHXGb>p85>l|yVJd~}i5e3dFuhF7-H6>+ zjg860#zu-(Y}_$yybnuKPEt2EAz7Tk?w4(90Gk%V9uhO>W3%%J+B`3oS&n5D=wN8Z z9@i#TcVbWWU~99n4KA#(9otfc?L4a;D{+51Rx^avcIk!Lg8i%l`?+!(c2UJID&}9x{jdW2cM?7;?=Kd78L^4(F9A?4j2!1Qd`aK0GQVby`7v&MPc(ITQ?$iP& zBP)$2J&WN_BxATWGLIt8Qby*{B$#hx9?L9ydt@HRSjVY*c%hS)wSi^f^ENU@BB}gs zWFAG5xkl#EOfv7tJeIY#c4QvMgLbj5cA*oBdWKu-nqP?4XHcaBZoyBXN#1wVVh_K^z Nm8%_ZWcaVd{{d1hGfMyf diff --git a/src/engraving/layout/layoutsystem.cpp b/src/engraving/layout/layoutsystem.cpp index adb884a0ee713..964373fbea697 100644 --- a/src/engraving/layout/layoutsystem.cpp +++ b/src/engraving/layout/layoutsystem.cpp @@ -957,8 +957,8 @@ void LayoutSystem::layoutSystemElements(const LayoutOptions& options, LayoutCont if (e->isDynamic()) { Dynamic* d = toDynamic(e); d->layout(); - if (d->autoplace()) { + d->manageBarlineCollisions(); d->autoplaceSegmentElement(false); dynamics.push_back(d); } @@ -970,7 +970,6 @@ void LayoutSystem::layoutSystemElements(const LayoutOptions& options, LayoutCont } // add dynamics shape to skyline - for (Dynamic* d : dynamics) { if (!d->addToSkyline()) { continue; @@ -981,6 +980,21 @@ void LayoutSystem::layoutSystemElements(const LayoutOptions& options, LayoutCont system->staff(si)->skyline().add(d->shape().translate(d->pos() + s->pos() + m->pos())); } + //------------------------------------------------------------- + // Expressions + // Must be done after dynamics. Remember that expressions may + // also snap into alignment with dynamics. + //------------------------------------------------------------- + for (Segment* s : sl) { + Measure* m = s->measure(); + for (EngravingItem* e : s->annotations()) { + if (e->isExpression()) { + e->layout(); + system->staff(e->staffIdx())->skyline().add(e->shape().translate(e->pos() + s->pos() + m->pos())); + } + } + } + //------------------------------------------------------------- // layout SpannerSegments for current system // voltas and tempo change lines are collected here, but laid out later diff --git a/src/engraving/libmscore/chordrest.cpp b/src/engraving/libmscore/chordrest.cpp index ef204cbe04b1e..f9c74bd6c50d1 100644 --- a/src/engraving/libmscore/chordrest.cpp +++ b/src/engraving/libmscore/chordrest.cpp @@ -242,6 +242,7 @@ EngravingItem* ChordRest::drop(EditData& data) // fall through case ElementType::TEMPO_TEXT: case ElementType::DYNAMIC: + case ElementType::EXPRESSION: case ElementType::FRET_DIAGRAM: case ElementType::TREMOLOBAR: case ElementType::SYMBOL: diff --git a/src/engraving/libmscore/dynamic.cpp b/src/engraving/libmscore/dynamic.cpp index 9574aece087da..85ae968ddf45e 100644 --- a/src/engraving/libmscore/dynamic.cpp +++ b/src/engraving/libmscore/dynamic.cpp @@ -26,6 +26,7 @@ #include "chord.h" #include "dynamichairpingroup.h" +#include "expression.h" #include "measure.h" #include "mscore.h" #include "rest.h" @@ -112,6 +113,9 @@ static Dyn dynList[] = { static const ElementStyle dynamicsStyle { { Sid::dynamicsPlacement, Pid::PLACEMENT }, { Sid::dynamicsMinDistance, Pid::MIN_DISTANCE }, + { Sid::avoidBarLines, Pid::AVOID_BARLINES }, + { Sid::dynamicsSize, Pid::DYNAMICS_SIZE }, + { Sid::centerOnNotehead, Pid::CENTER_ON_NOTEHEAD }, }; //--------------------------------------------------------- @@ -137,6 +141,9 @@ Dynamic::Dynamic(const Dynamic& d) _dynRange = d._dynRange; _changeInVelocity = d._changeInVelocity; _velChangeSpeed = d._velChangeSpeed; + _avoidBarLines = d._avoidBarLines; + _dynamicsSize = d._dynamicsSize; + _centerOnNotehead = d._centerOnNotehead; } //--------------------------------------------------------- @@ -231,6 +238,8 @@ bool Dynamic::isVelocityChangeAvailable() const void Dynamic::layout() { + _snappedExpression = nullptr; // Here we reset it. It will become known again when we layout expression + const StaffType* stType = staffType(); _skipDraw = false; @@ -242,44 +251,85 @@ void Dynamic::layout() TextBase::layout(); Segment* s = segment(); - if (s) { - track_idx_t t = track() & ~0x3; - for (voice_idx_t voice = 0; voice < VOICES; ++voice) { - EngravingItem* e = s->element(t + voice); - if (!e || (e->isRest() && toRest(e)->ticks() >= measure()->ticks() && measure()->hasVoices(e->staffIdx()))) { - continue; - } - if (e->isChord() && (align() == AlignH::HCENTER)) { - SymId symId = TConv::symId(dynamicType()); - - // FIRST: move it at the center of the notehead width - // Note: the simplest way to get the correct notehead width (accounting for all - // the various spatium changes, staff scaling, note scaling...) is to just look - // at the first note of the chord and get it from there. - Note* note = toChord(e)->notes().at(0); - double noteHeadWidth = note->headWidth();// * dynamicMag; - movePosX(noteHeadWidth * .5); - // SECOND: center around the SMUFL optical center, rather than using the geometrical - // center of the bounding box - double opticalCenter = symSmuflAnchor(symId, SmuflAnchorId::opticalCenter).x(); - if (symId != SymId::noSym && opticalCenter) { - double symWidth = symBbox(symId).width(); - double offset = symWidth / 2 - opticalCenter + symBbox(symId).left(); - // Account for scaling factors - double spatiumScaling = spatium() / score()->spatium(); - static const double DEFAULT_DYNAMIC_FONT_SIZE = 10.0; - double fontScaling = size() / DEFAULT_DYNAMIC_FONT_SIZE; - offset *= spatiumScaling * fontScaling; - movePosX(offset); - } - } else { - movePosX(e->width() * .5); + if (!s || (!_centerOnNotehead && align().horizontal == AlignH::LEFT)) { + return; + } + + EngravingItem* itemToAlign = nullptr; + track_idx_t startTrack = staff2track(staffIdx()); + track_idx_t endTrack = startTrack + VOICES; + for (track_idx_t track = startTrack; track < endTrack; ++track) { + EngravingItem* e = s->elementAt(track); + if (!e || (e->isRest() && toRest(e)->ticks() >= measure()->ticks() && measure()->hasVoices(e->staffIdx()))) { + continue; + } + itemToAlign = e; + break; + } + + if (!itemToAlign->isChord()) { + movePosX(itemToAlign->width() * 0.5); + return; + } + + Chord* chord = toChord(itemToAlign); + bool centerOnNote = _centerOnNotehead || (!_centerOnNotehead && align().horizontal == AlignH::HCENTER); + + // Move to center of notehead width + Note* note = chord->notes().at(0); + double noteHeadWidth = note->headWidth(); + movePosX(noteHeadWidth * (centerOnNote ? 0.5 : 1)); + + if (!_centerOnNotehead) { + return; + } + + // Use Smufl optical center for dynamic if available + SymId symId = TConv::symId(dynamicType()); + double opticalCenter = symSmuflAnchor(symId, SmuflAnchorId::opticalCenter).x(); + if (symId != SymId::noSym && opticalCenter) { + double symWidth = symBbox(symId).width(); + double offset = symWidth / 2 - opticalCenter + symBbox(symId).left(); + double spatiumScaling = spatium() / score()->spatium(); + offset *= spatiumScaling; + movePosX(offset); + } + + // If the dynamic contains custom text, keep it aligned + movePosX(-customTextOffset()); +} + +double Dynamic::customTextOffset() +{ + if (!_centerOnNotehead || _dynamicType == DynamicType::OTHER) { + return 0.0; + } + + String referenceString = String::fromUtf8(dynList[int(_dynamicType)].text); + if (xmlText() == referenceString) { + return 0.0; + } + + Dynamic referenceDynamic(*this); + referenceDynamic.setXmlText(referenceString); + toTextBase(&referenceDynamic)->layout(); + TextFragment referenceFragment; + if (!referenceDynamic.textBlockList().empty()) { + TextBlock referenceBlock = referenceDynamic.textBlockList().front(); + if (!referenceBlock.fragments().empty()) { + referenceFragment = referenceDynamic.textBlockList().front().fragments().front(); + } + } + + for (TextBlock block : textBlockList()) { + for (TextFragment fragment : block.fragments()) { + if (fragment.text == referenceFragment.text) { + return fragment.pos.x() - referenceFragment.pos.x(); } - break; } - } else { - setPos(PointF()); } + + return 0.0; } //------------------------------------------------------------------- @@ -317,6 +367,95 @@ void Dynamic::doAutoplace() } } +//-------------------------------------------------------------------------- +// manageBarlineCollisions +// If necessary, offset dynamic left/right to clear barline collisions +//-------------------------------------------------------------------------- + +void Dynamic::manageBarlineCollisions() +{ + if (!_avoidBarLines || score()->nstaves() <= 1) { + return; + } + + Segment* thisSegment = segment(); + if (!thisSegment) { + return; + } + + System* system = measure()->system(); + if (!system) { + return; + } + + staff_idx_t barLineStaff = mu::nidx; + if (placeAbove()) { + // need to find the barline from the staff above + // taking into account there could be invisible staves + if (staffIdx() == 0) { + return; + } + for (int staffIndex = static_cast(staffIdx()) - 1; staffIndex >= 0; --staffIndex) { + if (system->staff(staffIndex)->show()) { + barLineStaff = staffIndex; + break; + } + } + } else { + barLineStaff = staffIdx(); + } + + if (barLineStaff == mu::nidx) { + return; + } + + if (score()->staff(barLineStaff)->barLineSpan() < 1) { + return; // Barline doesn't extend through staves + } + + const double minBarLineDistance = 0.25 * spatium(); + + // Check barlines to the left + Segment* leftBarLineSegment = nullptr; + for (Segment* segment = thisSegment; segment && segment->measure()->system() == system; segment = segment->prev1()) { + if (segment->segmentType() & SegmentType::BarLineType) { + leftBarLineSegment = segment; + break; + } + } + if (leftBarLineSegment) { + EngravingItem* e = leftBarLineSegment->elementAt(barLineStaff * VOICES); + if (e) { + double leftMargin = bbox().translated(pagePos() - offset()).left() - e->bbox().translated(e->pagePos()).right() + - minBarLineDistance; + if (leftMargin < 0) { + movePosX(-leftMargin); + return; + } + } + } + + // Check barlines to the right + Segment* rightBarLineSegment = nullptr; + for (Segment* segment = thisSegment; segment && segment->measure()->system() == system; segment = segment->next1()) { + if (segment->segmentType() & SegmentType::BarLineType) { + rightBarLineSegment = segment; + break; + } + } + if (rightBarLineSegment) { + EngravingItem* e = rightBarLineSegment->elementAt(barLineStaff * VOICES); + if (e) { + double rightMargin = e->bbox().translated(e->pagePos()).left() - bbox().translated(pagePos() - offset()).right() + - minBarLineDistance; + if (rightMargin < 0) { + movePosX(rightMargin); + return; + } + } + } +} + //--------------------------------------------------------- // setDynamicType //--------------------------------------------------------- @@ -342,6 +481,29 @@ String Dynamic::dynamicText(DynamicType t) return String::fromUtf8(dynList[int(t)].text); } +bool Dynamic::acceptDrop(EditData& ed) const +{ + ElementType droppedType = ed.dropElement->type(); + return droppedType == ElementType::DYNAMIC || droppedType == ElementType::EXPRESSION; +} + +EngravingItem* Dynamic::drop(EditData& ed) +{ + EngravingItem* item = ed.dropElement; + if (!(item->isDynamic() || item->isExpression())) { + return nullptr; + } + + item->setTrack(track()); + item->setParent(segment()); + score()->undoAddElement(item); + item->undoChangeProperty(Pid::PLACEMENT, placement(), PropertyFlags::UNSTYLED); + if (item->isDynamic()) { + score()->undoRemoveElement(this); // swap this dynamic for the newly added one + } + return item; +} + TranslatableString Dynamic::subtypeUserName() const { return TranslatableString::untranslatable(TConv::toXml(dynamicType()).ascii()); @@ -368,7 +530,7 @@ void Dynamic::startEdit(EditData& ed) void Dynamic::endEdit(EditData& ed) { TextBase::endEdit(ed); - if (xmlText() != String::fromUtf8(dynList[int(_dynamicType)].text)) { + if (!xmlText().contains(String::fromUtf8(dynList[int(_dynamicType)].text))) { _dynamicType = DynamicType::OTHER; } } @@ -394,6 +556,9 @@ std::unique_ptr Dynamic::getDragGroup(std::function(); break; + case Pid::AVOID_BARLINES: + setAvoidBarLines(v.toBool()); + break; + case Pid::DYNAMICS_SIZE: + _dynamicsSize = v.toDouble(); + break; + case Pid::CENTER_ON_NOTEHEAD: + _centerOnNotehead = v.toBool(); + break; default: if (!TextBase::setProperty(propertyId, v)) { return false; @@ -529,6 +709,17 @@ PropertyValue Dynamic::propertyDefault(Pid id) const } } +void Dynamic::undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps) +{ + TextBase::undoChangeProperty(id, v, ps); + if (_snappedExpression) { + if (id == Pid::OFFSET && _snappedExpression->offset() != v.value() + || id == Pid::PLACEMENT && _snappedExpression->placement() != v.value()) { + _snappedExpression->undoChangeProperty(id, v, ps); + } + } +} + //--------------------------------------------------------- // accessibleInfo //--------------------------------------------------------- diff --git a/src/engraving/libmscore/dynamic.h b/src/engraving/libmscore/dynamic.h index 6edd680366d7b..192c2ec7b759e 100644 --- a/src/engraving/libmscore/dynamic.h +++ b/src/engraving/libmscore/dynamic.h @@ -50,6 +50,11 @@ class Dynamic final : public TextBase private: DynamicType _dynamicType; + Expression* _snappedExpression = nullptr; + + M_PROPERTY(bool, avoidBarLines, setAvoidBarLines) + M_PROPERTY(double, dynamicsSize, setDynamicsSize) + M_PROPERTY(bool, centerOnNotehead, setCenterOnNotehead) mutable mu::PointF dragOffset; int _velocity; // associated midi velocity 0-127 @@ -76,6 +81,7 @@ class Dynamic final : public TextBase String translatedSubtypeUserName() const override; void layout() override; + double customTextOffset(); bool isEditable() const override { return true; } void startEdit(EditData&) override; @@ -99,14 +105,23 @@ class Dynamic final : public TextBase PropertyValue getProperty(Pid propertyId) const override; bool setProperty(Pid propertyId, const PropertyValue&) override; PropertyValue propertyDefault(Pid id) const override; + void undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps) override; std::unique_ptr getDragGroup(std::function isDragged) override; String accessibleInfo() const override; String screenReaderInfo() const override; void doAutoplace(); + void manageBarlineCollisions(); static String dynamicText(DynamicType t); + bool hasCustomText() const { return dynamicText(_dynamicType) != xmlText(); } + + void setSnappedExpression(Expression* e) { _snappedExpression = e; } + Expression* snappedExpression() const { return _snappedExpression; } + + bool acceptDrop(EditData& ed) const override; + EngravingItem* drop(EditData& ed) override; }; } // namespace mu::engraving diff --git a/src/engraving/libmscore/dynamichairpingroup.cpp b/src/engraving/libmscore/dynamichairpingroup.cpp index 36e720683eb71..2f7e9048f004e 100644 --- a/src/engraving/libmscore/dynamichairpingroup.cpp +++ b/src/engraving/libmscore/dynamichairpingroup.cpp @@ -23,9 +23,11 @@ #include "dynamichairpingroup.h" #include "dynamic.h" +#include "expression.h" #include "hairpin.h" #include "score.h" #include "segment.h" +#include "undo.h" using namespace mu; @@ -217,4 +219,65 @@ void DynamicNearHairpinsDragGroup::endDrag(EditData& ed) dynamic->endDrag(ed); dynamic->triggerLayout(); } + +//------------------------------------------------------- +// DynamicExpressionDragGroup +//------------------------------------------------------- + +std::unique_ptr DynamicExpressionDragGroup::detectFor(Dynamic* d, std::function isDragged) +{ + Segment* segment = d->segment(); + Expression* expression = segment ? toExpression(segment->findAnnotation(ElementType::EXPRESSION, d->track(), d->track())) : nullptr; + if (expression && !isDragged(expression) && expression->snapToDynamics()) { + return std::unique_ptr(new DynamicExpressionDragGroup(d, expression)); + } + return nullptr; +} + +std::unique_ptr DynamicExpressionDragGroup::detectFor(Expression* e, std::function isDragged) +{ + if (!e->snapToDynamics()) { + return nullptr; + } + Segment* segment = e->explicitParent() ? toSegment(e->explicitParent()) : nullptr; + Dynamic* dynamic = segment ? toDynamic(segment->findAnnotation(ElementType::DYNAMIC, e->track(), e->track())) : nullptr; + if (dynamic && !isDragged(dynamic)) { + return std::unique_ptr(new DynamicExpressionDragGroup(dynamic, e)); + } + return nullptr; +} + +void DynamicExpressionDragGroup::startDrag(EditData& ed) +{ + dynamic->startDrag(ed); + expression->startDrag(ed); +} + +RectF DynamicExpressionDragGroup::drag(EditData& ed) +{ + RectF r = static_cast(dynamic)->drag(ed); + + // Dynamic may snap to a different segment upon dragging, + // in which case move the expression with it + Segment* newSegment = dynamic->segment(); + Segment* oldSegment = toSegment(expression->explicitParent()); + if (newSegment != oldSegment) { + Score* score = newSegment->score(); + staff_idx_t staffIdx = expression->staffIdx(); + score->undo(new ChangeParent(expression, newSegment, staffIdx)); + } + + dynamic->triggerLayout(); + expression->triggerLayout(); + + return r; +} + +void DynamicExpressionDragGroup::endDrag(EditData& ed) +{ + dynamic->endDrag(ed); + dynamic->triggerLayout(); + expression->endDrag(ed); + expression->triggerLayout(); +} } // namespace mu::engraving diff --git a/src/engraving/libmscore/dynamichairpingroup.h b/src/engraving/libmscore/dynamichairpingroup.h index 49b7fdca18ed3..37559c6eec909 100644 --- a/src/engraving/libmscore/dynamichairpingroup.h +++ b/src/engraving/libmscore/dynamichairpingroup.h @@ -33,6 +33,7 @@ namespace mu::engraving { class Dynamic; class Hairpin; class HairpinSegment; +class Expression; //------------------------------------------------------------------- // HairpinWithDynamicsDragGroup @@ -81,6 +82,29 @@ class DynamicNearHairpinsDragGroup : public ElementGroup static std::unique_ptr detectFor(Dynamic* d, std::function isDragged); }; + +//------------------------------------------------------------------- +// DynamicExpressionDragGroup +//------------------------------------------------------------------- + +class DynamicExpressionDragGroup : public ElementGroup +{ + OBJECT_ALLOCATOR(engraving, DynamicNearHairpinsDragGroup) + + Dynamic* dynamic; + Expression* expression; + +public: + DynamicExpressionDragGroup(Dynamic* d, Expression* e) + : dynamic(d), expression(e) {} + + void startDrag(EditData& ed) override; + mu::RectF drag(EditData& ed) override; + void endDrag(EditData& ed) override; + + static std::unique_ptr detectFor(Dynamic* d, std::function isDragged); + static std::unique_ptr detectFor(Expression* e, std::function isDragged); +}; } // namespace mu::engraving #endif diff --git a/src/engraving/libmscore/edit.cpp b/src/engraving/libmscore/edit.cpp index 281fc6164bf66..e4b8b98c5c453 100644 --- a/src/engraving/libmscore/edit.cpp +++ b/src/engraving/libmscore/edit.cpp @@ -2215,6 +2215,7 @@ void Score::cmdFlip() || e->isSticking() || e->isFingering() || e->isDynamic() + || e->isExpression() || e->isHarmony() || e->isFretDiagram() || e->isHairpin() @@ -5613,6 +5614,7 @@ void Score::undoAddElement(EngravingItem* element, bool addToLinkedStaves, bool && et != ElementType::PEDAL && et != ElementType::BREATH && et != ElementType::DYNAMIC + && et != ElementType::EXPRESSION && et != ElementType::STAFF_TEXT && et != ElementType::SYSTEM_TEXT && et != ElementType::TRIPLET_FEEL @@ -5683,6 +5685,7 @@ void Score::undoAddElement(EngravingItem* element, bool addToLinkedStaves, bool || element->isImage() || element->isTremoloBar() || element->isDynamic() + || element->isExpression() || element->isStaffText() || element->isPlayTechAnnotation() || element->isSticking() @@ -5716,6 +5719,7 @@ void Score::undoAddElement(EngravingItem* element, bool addToLinkedStaves, bool case ElementType::HARMONY: case ElementType::FIGURED_BASS: case ElementType::DYNAMIC: + case ElementType::EXPRESSION: case ElementType::LYRICS: // not normally segment-attached continue; default: @@ -5809,6 +5813,7 @@ void Score::undoAddElement(EngravingItem* element, bool addToLinkedStaves, bool || element->isImage() || element->isTremoloBar() || element->isDynamic() + || element->isExpression() || element->isStaffText() || element->isPlayTechAnnotation() || element->isSticking() diff --git a/src/engraving/libmscore/engravingobject.cpp b/src/engraving/libmscore/engravingobject.cpp index a2759251b779d..de56bd99c101d 100644 --- a/src/engraving/libmscore/engravingobject.cpp +++ b/src/engraving/libmscore/engravingobject.cpp @@ -425,7 +425,7 @@ void EngravingObject::undoChangeProperty(Pid id, const PropertyValue& v, Propert } else { sp = score()->spatium(); } - EngravingObject::undoChangeProperty(Pid::OFFSET, score()->styleV(getPropertyStyle(Pid::OFFSET)).value() * sp); + setProperty(Pid::OFFSET, score()->styleV(getPropertyStyle(Pid::OFFSET)).value() * sp); EngravingItem* e = toEngravingItem(this); e->setOffsetChanged(false); } @@ -694,6 +694,7 @@ bool EngravingObject::isTextBase() const return type() == ElementType::TEXT || type() == ElementType::LYRICS || type() == ElementType::DYNAMIC + || type() == ElementType::EXPRESSION || type() == ElementType::FINGERING || type() == ElementType::HARMONY || type() == ElementType::MARKER diff --git a/src/engraving/libmscore/engravingobject.h b/src/engraving/libmscore/engravingobject.h index fd2b0a2d8383f..cf01242cc4bc2 100644 --- a/src/engraving/libmscore/engravingobject.h +++ b/src/engraving/libmscore/engravingobject.h @@ -69,6 +69,7 @@ class Clef; class DeadSlapped; class DurationElement; class Dynamic; +class Expression; class EngravingItem; class EngravingObject; class FBox; @@ -362,6 +363,7 @@ class EngravingObject CONVERT(Tuplet, TUPLET) CONVERT(NoteDot, NOTEDOT) CONVERT(Dynamic, DYNAMIC) + CONVERT(Expression, EXPRESSION) CONVERT(InstrumentName, INSTRUMENT_NAME) CONVERT(Accidental, ACCIDENTAL) CONVERT(TextLine, TEXTLINE) @@ -708,6 +710,7 @@ CONVERT(MMRest) CONVERT(Tuplet) CONVERT(NoteDot) CONVERT(Dynamic) +CONVERT(Expression) CONVERT(InstrumentName) CONVERT(Accidental) CONVERT(TextLine) diff --git a/src/engraving/libmscore/expression.cpp b/src/engraving/libmscore/expression.cpp new file mode 100644 index 0000000000000..610ee75e4203b --- /dev/null +++ b/src/engraving/libmscore/expression.cpp @@ -0,0 +1,204 @@ +#include "chord.h" +#include "dynamic.h" +#include "dynamichairpingroup.h" +#include "expression.h" +#include "note.h" +#include "segment.h" +#include "score.h" +#include "stafftext.h" + +namespace mu::engraving { +static const ElementStyle expressionStyle { + { Sid::expressionPlacement, Pid::PLACEMENT }, + { Sid::expressionMinDistance, Pid::MIN_DISTANCE }, + { Sid::snapToDynamics, Pid::SNAP_TO_DYNAMICS }, +}; + +Expression::Expression(Segment* parent) + : TextBase(ElementType::EXPRESSION, parent, TextStyleType::EXPRESSION, ElementFlag::MOVABLE | ElementFlag::ON_STAFF) +{ + initElementStyle(&expressionStyle); +} + +Expression::Expression(const Expression& expression) + : TextBase(expression) +{ + _snapToDynamics = expression._snapToDynamics; +} + +PropertyValue Expression::propertyDefault(Pid id) const +{ + switch (id) { + case Pid::TEXT_STYLE: + return TextStyleType::EXPRESSION; + default: + return TextBase::propertyDefault(id); + } +} + +void Expression::layout() +{ + TextBase::layout(); + + Segment* segment = explicitParent() ? toSegment(explicitParent()) : nullptr; + if (!segment) { + return; + } + + if (align().horizontal != AlignH::LEFT) { + Chord* chordToAlign = nullptr; + // Look for chord in this staff + track_idx_t startTrack = track2staff(staffIdx()); + track_idx_t endTrack = startTrack + VOICES; + for (track_idx_t track = startTrack; track < endTrack; ++track) { + EngravingItem* item = segment->elementAt(track); + if (item && item->isChord()) { + chordToAlign = toChord(item); + break; + } + } + + if (chordToAlign) { + Note* note = chordToAlign->notes().at(0); + double headWidth = note->headWidth(); + bool center = align().horizontal == AlignH::HCENTER; + movePosX(headWidth * (center ? 0.5 : 1)); + } + } + + _snappedDynamic = nullptr; + if (!_snapToDynamics) { + autoplaceSegmentElement(); + return; + } + + Dynamic* dynamic = toDynamic(segment->findAnnotation(ElementType::DYNAMIC, track(), track())); + if (!dynamic || dynamic->placeAbove() != placeAbove()) { + autoplaceSegmentElement(); + return; + } + + _snappedDynamic = dynamic; + dynamic->setSnappedExpression(this); + + // If there is a dynamic on same segment and track, lock this expression to it + double padding = computeDynamicExpressionDistance(); + double dynamicRight = dynamic->shape().translate(dynamic->pos()).right(); + double expressionLeft = bbox().translated(pos()).left(); + double difference = expressionLeft - dynamicRight - padding; + movePosX(-difference); + + // Keep expression and dynamic vertically aligned + autoplaceSegmentElement(); + bool above = placeAbove(); + double yExpression = pos().y(); + double yDynamic = dynamic->pos().y(); + bool expressionIsOuter = above ? yExpression < yDynamic : yExpression > yDynamic; + if (expressionIsOuter) { + dynamic->movePosY((yExpression - yDynamic)); + } else { + movePosY((yDynamic - yExpression)); + } +} + +double Expression::computeDynamicExpressionDistance() const +{ + if (!_snappedDynamic) { + return 0.0; + } + // We are essentially faking the kerning behaviour of dynamic VS expression text + // There's no other way to do this because the dynamic is a different font. + String dynamicTextString = _snappedDynamic->xmlText(); + String f = String::fromStdString("dynamicForte"); + double distance = (dynamicTextString.endsWith(f) ? 0.2 : 0.5) * spatium(); + distance *= 0.5 * (_snappedDynamic->dynamicsSize() + (size() / 10)); + return distance; +} + +std::unique_ptr Expression::getDragGroup(std::function isDragged) +{ + if (auto g = DynamicExpressionDragGroup::detectFor(this, isDragged)) { + return g; + } + return TextBase::getDragGroup(isDragged); +} + +void Expression::undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps) +{ + TextBase::undoChangeProperty(id, v, ps); + if (_snappedDynamic) { + if (id == Pid::OFFSET && _snappedDynamic->offset() != v.value() + || id == Pid::PLACEMENT && _snappedDynamic->placement() != v.value()) { + _snappedDynamic->undoChangeProperty(id, v, ps); + } + } +} + +bool Expression::acceptDrop(EditData& ed) const +{ + return ed.dropElement->type() == ElementType::DYNAMIC; +} + +EngravingItem* Expression::drop(EditData& ed) +{ + EngravingItem* item = ed.dropElement; + if (!item->isDynamic()) { + return nullptr; + } + if (_snappedDynamic) { + return _snappedDynamic->drop(ed); + } + item->setTrack(track()); + item->setParent(segment()); + score()->undoAddElement(item); + item->undoChangeProperty(Pid::PLACEMENT, placement(), PropertyFlags::UNSTYLED); + return item; +} + +PropertyValue Expression::getProperty(Pid propertyId) const +{ + switch (propertyId) { + case Pid::SNAP_TO_DYNAMICS: + return _snapToDynamics; + default: + return TextBase::getProperty(propertyId); + } +} + +bool Expression::setProperty(Pid propertyId, const PropertyValue& v) +{ + switch (propertyId) { + case Pid::SNAP_TO_DYNAMICS: + if (_snapToDynamics == false && v.toBool() == true) { + resetProperty(Pid::OFFSET); + } + setSnapToDynamics(v.toBool()); + break; + default: + if (!TextBase::setProperty(propertyId, v)) { + return false; + } + break; + } + triggerLayout(); + return true; +} + +void Expression::mapPropertiesFromOldExpressions(StaffText* staffText) +{ + if (staffText->minDistance() != propertyDefault(Pid::MIN_DISTANCE).value()) { + setMinDistance(staffText->minDistance()); + setPropertyFlags(Pid::MIN_DISTANCE, PropertyFlags::UNSTYLED); + } + if (staffText->placement() != propertyDefault(Pid::PLACEMENT).value()) { + setPlacement(staffText->placement()); + setPropertyFlags(Pid::PLACEMENT, PropertyFlags::UNSTYLED); + } + if (staffText->offset() != propertyDefault(Pid::OFFSET).value()) { + setOffset(staffText->offset()); + setSnapToDynamics(false); + setPropertyFlags(Pid::OFFSET, PropertyFlags::UNSTYLED); + setPropertyFlags(Pid::SNAP_TO_DYNAMICS, PropertyFlags::UNSTYLED); + } +} +} // namespace mu::engraving diff --git a/src/engraving/libmscore/expression.h b/src/engraving/libmscore/expression.h new file mode 100644 index 0000000000000..cff9308719a50 --- /dev/null +++ b/src/engraving/libmscore/expression.h @@ -0,0 +1,41 @@ +#ifndef EXPRESSION_H +#define EXPRESSION_H + +#include "textbase.h" + +namespace mu::engraving { +class Dynamic; + +class Expression final : public TextBase +{ + M_PROPERTY(bool, snapToDynamics, setSnapToDynamics) + DECLARE_CLASSOF(ElementType::EXPRESSION) +private: + Dynamic* _snappedDynamic = nullptr; +public: + Expression(Segment* parent); + Expression(const Expression& expression); + Expression* clone() const override { return new Expression(*this); } + + Segment* segment() const { return toSegment(explicitParent()); } + + PropertyValue propertyDefault(Pid id) const override; + + void layout() override; + double computeDynamicExpressionDistance() const; + + std::unique_ptr getDragGroup(std::function isDragged) override; + + void undoChangeProperty(Pid id, const PropertyValue& v, PropertyFlags ps) override; + + bool acceptDrop(EditData& ed) const override; + EngravingItem* drop(EditData& ed) override; + + PropertyValue getProperty(Pid propertyId) const override; + bool setProperty(Pid propertyId, const PropertyValue& v) override; + void mapPropertiesFromOldExpressions(StaffText* staffText); + + Dynamic* snappedDynamic() const { return _snappedDynamic; } +}; +} // namespace mu::engraving +#endif // EXPRESSION_H diff --git a/src/engraving/libmscore/factory.cpp b/src/engraving/libmscore/factory.cpp index 64c0a000ba84a..f22fe9e36bff4 100644 --- a/src/engraving/libmscore/factory.cpp +++ b/src/engraving/libmscore/factory.cpp @@ -40,6 +40,7 @@ #include "chordline.h" #include "deadslapped.h" #include "dynamic.h" +#include "expression.h" #include "fermata.h" #include "figuredbass.h" #include "fingering.h" @@ -155,6 +156,7 @@ EngravingItem* Factory::doCreateItem(ElementType type, EngravingItem* parent) case ElementType::CHORDLINE: return new ChordLine(parent->isChord() ? toChord(parent) : dummy->chord()); case ElementType::ACCIDENTAL: return new Accidental(parent); case ElementType::DYNAMIC: return new Dynamic(parent->isSegment() ? toSegment(parent) : dummy->segment()); + case ElementType::EXPRESSION: return new Expression(parent->isSegment() ? toSegment(parent) : dummy->segment()); case ElementType::TEXT: return new Text(parent); case ElementType::MEASURE_NUMBER: return new MeasureNumber(parent->isMeasure() ? toMeasure(parent) : dummy->measure()); case ElementType::MMREST_RANGE: return new MMRestRange(parent->isMeasure() ? toMeasure(parent) : dummy->measure()); @@ -508,6 +510,13 @@ StaffText* Factory::createStaffText(Segment * parent, TextStyleType textStyleTyp return staffText; } +Expression* Factory::createExpression(Segment* parent, bool isAccessibleEnabled) +{ + Expression* expression = new Expression(parent); + expression->setAccessibleEnabled(isAccessibleEnabled); + return expression; +} + CREATE_ITEM_IMPL(RehearsalMark, ElementType::REHEARSAL_MARK, Segment, isAccessibleEnabled) CREATE_ITEM_IMPL(Stem, ElementType::STEM, Chord, isAccessibleEnabled) diff --git a/src/engraving/libmscore/factory.h b/src/engraving/libmscore/factory.h index 59d2dd489da62..4e8dcbe232173 100644 --- a/src/engraving/libmscore/factory.h +++ b/src/engraving/libmscore/factory.h @@ -150,6 +150,8 @@ class Factory static StaffText* createStaffText(Segment* parent, TextStyleType textStyleType = TextStyleType::STAFF, bool isAccessibleEnabled = true); + static Expression* createExpression(Segment* parent, bool isAccessibleEnabled = true); + static RehearsalMark* createRehearsalMark(Segment* parent, bool isAccessibleEnabled = true); static Stem* createStem(Chord* parent, bool isAccessibleEnabled = true); diff --git a/src/engraving/libmscore/libmscore.cmake b/src/engraving/libmscore/libmscore.cmake index 937d787e9458b..246a3517bc00b 100644 --- a/src/engraving/libmscore/libmscore.cmake +++ b/src/engraving/libmscore/libmscore.cmake @@ -83,6 +83,8 @@ set(LIBMSCORE_SRC ${CMAKE_CURRENT_LIST_DIR}/durationtype.h ${CMAKE_CURRENT_LIST_DIR}/dynamic.cpp ${CMAKE_CURRENT_LIST_DIR}/dynamic.h + ${CMAKE_CURRENT_LIST_DIR}/expression.cpp + ${CMAKE_CURRENT_LIST_DIR}/expression.h ${CMAKE_CURRENT_LIST_DIR}/dynamichairpingroup.cpp ${CMAKE_CURRENT_LIST_DIR}/dynamichairpingroup.h ${CMAKE_CURRENT_LIST_DIR}/easeInOut.cpp diff --git a/src/engraving/libmscore/measure.cpp b/src/engraving/libmscore/measure.cpp index 941e6a441fa6b..0eadaacbd0536 100644 --- a/src/engraving/libmscore/measure.cpp +++ b/src/engraving/libmscore/measure.cpp @@ -1576,6 +1576,7 @@ EngravingItem* Measure::drop(EditData& data) return e; case ElementType::DYNAMIC: + case ElementType::EXPRESSION: case ElementType::FRET_DIAGRAM: e->setParent(seg); e->setTrack(staffIdx * VOICES); diff --git a/src/engraving/libmscore/mscore.h b/src/engraving/libmscore/mscore.h index 33525d1424558..ebd3d6d0eb25a 100644 --- a/src/engraving/libmscore/mscore.h +++ b/src/engraving/libmscore/mscore.h @@ -122,6 +122,10 @@ static constexpr double DPI = 72.0 * DPI_F; static constexpr double SPATIUM20 = 5.0 * (DPI / 72.0); static constexpr double DPMM = DPI / INCH; +// NOTE: the Smufl default is actually 20pt. We use 10 for historical reasons +// and back-compatibility, but this will be multiplied x2 during dynamic layout. +static constexpr double DYNAMICS_DEFAULT_FONT_SIZE = 10.0; + static constexpr int MAX_STAVES = 4; static constexpr char mimeSymbolFormat[] = "application/musescore/symbol"; diff --git a/src/engraving/libmscore/note.cpp b/src/engraving/libmscore/note.cpp index 9d15d01b47ab5..0811363324c0d 100644 --- a/src/engraving/libmscore/note.cpp +++ b/src/engraving/libmscore/note.cpp @@ -1666,6 +1666,7 @@ bool Note::acceptDrop(EditData& data) const || type == ElementType::CHORD || type == ElementType::HARMONY || type == ElementType::DYNAMIC + || type == ElementType::EXPRESSION || (type == ElementType::ACTION_ICON && toActionIcon(e)->actionType() == ActionIconType::ACCIACCATURA) || (type == ElementType::ACTION_ICON && toActionIcon(e)->actionType() == ActionIconType::APPOGGIATURA) || (type == ElementType::ACTION_ICON && toActionIcon(e)->actionType() == ActionIconType::GRACE4) diff --git a/src/engraving/libmscore/paste.cpp b/src/engraving/libmscore/paste.cpp index 5fc94e84dfd44..6ae4a4235455b 100644 --- a/src/engraving/libmscore/paste.cpp +++ b/src/engraving/libmscore/paste.cpp @@ -389,6 +389,7 @@ bool Score::pasteStaff(XmlReader& e, Segment* dst, staff_idx_t dstStaff, Fractio undoAddElement(harmony); pastedHarmony.push_back(harmony); } else if (tag == "Dynamic" + || tag == "Expression" || tag == "Symbol" || tag == "FretDiagram" || tag == "TremoloBar" diff --git a/src/engraving/libmscore/property.cpp b/src/engraving/libmscore/property.cpp index 81e4ababe599f..d2b364b758f98 100644 --- a/src/engraving/libmscore/property.cpp +++ b/src/engraving/libmscore/property.cpp @@ -342,6 +342,11 @@ static constexpr PropertyMetaData propertyList[] = { { Pid::END_FONT_STYLE, false, "endFontStyle", P_TYPE::INT, DUMMY_QT_TR_NOOP("propertyName", "end font style") }, { Pid::END_TEXT_OFFSET, false, "endTextOffset", P_TYPE::POINT, DUMMY_QT_TR_NOOP("propertyName", "end text offset") }, + { Pid::AVOID_BARLINES, false, "avoidBarLines", P_TYPE::BOOL, DUMMY_QT_TR_NOOP("propertyName", "avoid bar lines") }, + { Pid::DYNAMICS_SIZE, false, "dynamicsSize", P_TYPE::REAL, DUMMY_QT_TR_NOOP("propertyName", "dynamic size") }, + { Pid::CENTER_ON_NOTEHEAD, false, "centerOnNotehead", P_TYPE::BOOL, DUMMY_QT_TR_NOOP("propertyName", "use text alignment") }, + { Pid::SNAP_TO_DYNAMICS, false, "snapToDynamics", P_TYPE::BOOL, DUMMY_QT_TR_NOOP("propertyName", "snap expression") }, + { Pid::POS_ABOVE, false, "posAbove", P_TYPE::MILLIMETRE, DUMMY_QT_TR_NOOP("propertyName", "position above") }, { Pid::LOCATION_STAVES, false, "staves", P_TYPE::INT, DUMMY_QT_TR_NOOP("propertyName", "staves distance") }, diff --git a/src/engraving/libmscore/property.h b/src/engraving/libmscore/property.h index d474ac326f58d..17834d74052b7 100644 --- a/src/engraving/libmscore/property.h +++ b/src/engraving/libmscore/property.h @@ -350,6 +350,11 @@ enum class Pid { END_FONT_STYLE, END_TEXT_OFFSET, + AVOID_BARLINES, // meant for Dynamics + DYNAMICS_SIZE, + CENTER_ON_NOTEHEAD, + SNAP_TO_DYNAMICS, + POS_ABOVE, LOCATION_STAVES, diff --git a/src/engraving/libmscore/rest.cpp b/src/engraving/libmscore/rest.cpp index a4b26546e31fe..28484e869f86e 100644 --- a/src/engraving/libmscore/rest.cpp +++ b/src/engraving/libmscore/rest.cpp @@ -203,6 +203,7 @@ bool Rest::acceptDrop(EditData& data) const || (type == ElementType::STAFF_STATE) || (type == ElementType::INSTRUMENT_CHANGE) || (type == ElementType::DYNAMIC) + || (type == ElementType::EXPRESSION) || (type == ElementType::HARMONY) || (type == ElementType::TEMPO_TEXT) || (type == ElementType::REHEARSAL_MARK) diff --git a/src/engraving/libmscore/segment.cpp b/src/engraving/libmscore/segment.cpp index 52004a029d049..b4b56696f614a 100644 --- a/src/engraving/libmscore/segment.cpp +++ b/src/engraving/libmscore/segment.cpp @@ -585,6 +585,7 @@ void Segment::add(EngravingItem* el) case ElementType::TEMPO_TEXT: case ElementType::DYNAMIC: + case ElementType::EXPRESSION: case ElementType::HARMONY: case ElementType::SYMBOL: case ElementType::FRET_DIAGRAM: @@ -751,6 +752,7 @@ void Segment::remove(EngravingItem* el) break; case ElementType::DYNAMIC: + case ElementType::EXPRESSION: case ElementType::FIGURED_BASS: case ElementType::FRET_DIAGRAM: case ElementType::HARMONY: @@ -1767,6 +1769,7 @@ EngravingItem* Segment::nextElement(staff_idx_t activeStaff) switch (e->type()) { case ElementType::DYNAMIC: case ElementType::HARMONY: + case ElementType::EXPRESSION: case ElementType::SYMBOL: case ElementType::FERMATA: case ElementType::FRET_DIAGRAM: @@ -1909,6 +1912,7 @@ EngravingItem* Segment::prevElement(staff_idx_t activeStaff) } switch (e->type()) { case ElementType::DYNAMIC: + case ElementType::EXPRESSION: case ElementType::HARMONY: case ElementType::SYMBOL: case ElementType::FERMATA: @@ -2289,6 +2293,7 @@ void Segment::createShape(staff_idx_t staffIdx) && !e->isHarmony() && !e->isTempoText() && !e->isDynamic() + && !e->isExpression() && !e->isFiguredBass() && !e->isSymbol() && !e->isFSymbol() diff --git a/src/engraving/libmscore/shape.cpp b/src/engraving/libmscore/shape.cpp index a3b3d42dcdef6..5defb3dfe9773 100644 --- a/src/engraving/libmscore/shape.cpp +++ b/src/engraving/libmscore/shape.cpp @@ -230,7 +230,7 @@ bool Shape::clearsVertically(const Shape& a) const double Shape::left() const { - double dist = 0.0; + double dist = 10000.0; for (const ShapeElement& r : *this) { if (r.height() != 0.0 && !(r.toItem && r.toItem->isTextBase()) && r.left() < dist) { // if (r.left() < dist) @@ -247,7 +247,7 @@ double Shape::left() const double Shape::right() const { - double dist = 0.0; + double dist = -10000.0; for (const RectF& r : *this) { if (r.right() > dist) { dist = r.right(); diff --git a/src/engraving/libmscore/textbase.cpp b/src/engraving/libmscore/textbase.cpp index c89d76537f32c..632a9912f746f 100644 --- a/src/engraving/libmscore/textbase.cpp +++ b/src/engraving/libmscore/textbase.cpp @@ -797,10 +797,18 @@ mu::draw::Font TextFragment::font(const TextBase* t) const draw::Font::Type fontType = draw::Font::Type::Unknown; if (format.fontFamily() == "ScoreText") { if (t->isDynamic() || t->textStyleType() == TextStyleType::OTTAVA) { - family - = String::fromStdString(engravingFonts()->fontByName(t->score()->styleSt(Sid::MusicalSymbolFont).toStdString())->family()); + std::string fontName = engravingFonts()->fontByName(t->score()->styleSt(Sid::MusicalSymbolFont).toStdString())->family(); + family = String::fromStdString(fontName); fontType = draw::Font::Type::MusicSymbol; - // to keep desired size ratio (based on 20pt symbol size to 10pt text size) + if (t->isDynamic()) { + m = DYNAMICS_DEFAULT_FONT_SIZE * t->getProperty(Pid::DYNAMICS_SIZE).toDouble(); + if (t->score()->styleB(Sid::dynamicsOverrideFont)) { + std::string fontName = engravingFonts()->fontByName(t->score()->styleSt(Sid::dynamicsFont).toStdString())->family(); + family = String::fromStdString(fontName); + } + } + // We use a default font size of 10pt for historical reasons, + // but Smufl standard is 20pt so multiply x2 here. m *= 2; } else if (t->isTempoText()) { family = t->score()->styleSt(Sid::MusicalTextFont); @@ -972,19 +980,19 @@ void TextBlock::layout(TextBase* t) _lineSpacing *= t->textLineSpacing(); double rx = 0; - switch (t->align().horizontal) { - case AlignH::LEFT: - rx = -_bbox.left(); - break; - case AlignH::HCENTER: + AlignH alignH = t->align().horizontal; + bool dynamicAlwaysCentered = t->isDynamic() && t->getProperty(Pid::CENTER_ON_NOTEHEAD).toBool(); + + if (alignH == AlignH::HCENTER || dynamicAlwaysCentered) { rx = (layoutWidth - (_bbox.left() + _bbox.right())) * .5; - break; - case AlignH::RIGHT: + } else if (alignH == AlignH::LEFT) { + rx = -_bbox.left(); + } else if (alignH == AlignH::RIGHT) { rx = layoutWidth - _bbox.right(); - break; } rx += lm; + for (TextFragment& f : _fragments) { f.pos.rx() += rx; } @@ -1840,7 +1848,7 @@ bool TextBase::prepareFormat(const String& token, CharFormat& format) //--------------------------------------------------------- void TextBase::prepareFormat(const String& token, TextCursor& cursor) { - if (prepareFormat(token, *cursor.format())) { + if (prepareFormat(token, *cursor.format()) && cursor.format()->fontFamily() != propertyDefault(Pid::FONT_FACE).value()) { setPropertyFlags(Pid::FONT_FACE, PropertyFlags::UNSTYLED); } } @@ -2377,9 +2385,6 @@ void TextBase::setXmlText(const String& s) void TextBase::checkCustomFormatting(const String& s) { - if (s.contains(u"setTrack(ctx.track()); TRead::read(dyn, e, ctx); segment->add(dyn); + } else if (tag == "Expression") { + segment = measure->getSegment(SegmentType::ChordRest, ctx.tick()); + Expression* expr = Factory::createExpression(segment); + expr->setTrack(ctx.track()); + TRead::read(expr, e, ctx); + segment->add(expr); } else if (tag == "Harmony") { // hack - getSegment needed because tick tags are unreliable in 1.3 scores // for symbols attached to anything but a measure @@ -503,13 +510,23 @@ void MeasureRW::readVoice(Measure* measure, XmlReader& e, ReadContext& ctx, int // for symbols attached to anything but a measure segment = measure->getSegment(SegmentType::ChordRest, ctx.tick()); StaffText* el = Factory::createStaffText(segment); - el->setTrack(ctx.track()); TRead::read(el, e, ctx); - if (el->systemFlag() && el->isTopSystemObject()) { - el->setTrack(0); // original system object always goes on top + if (el->textStyleType() != TextStyleType::EXPRESSION) { + if (el->systemFlag() && el->isTopSystemObject()) { + el->setTrack(0); // original system object always goes on top + } + segment->add(el); + } else { + // Starting from version 4.1, Expressions have their own class. + // Map "old" expression objects into the "new" expression object. + Expression* expression = Factory::createExpression(segment); + expression->setTrack(ctx.track()); + expression->setXmlText(el->xmlText()); + expression->mapPropertiesFromOldExpressions(el); + segment->add(expression); + delete el; } - segment->add(el); } else if (tag == "Sticking" || tag == "SystemText" || tag == "PlayTechAnnotation" diff --git a/src/engraving/rw/400/tread.cpp b/src/engraving/rw/400/tread.cpp index fb3561dd9b4c8..4280303f2db53 100644 --- a/src/engraving/rw/400/tread.cpp +++ b/src/engraving/rw/400/tread.cpp @@ -40,7 +40,7 @@ #include "../../libmscore/drumset.h" #include "../../libmscore/dynamic.h" - +#include "../../libmscore/expression.h" #include "../../libmscore/harmony.h" #include "../../libmscore/harmonicmark.h" #include "../../libmscore/chordlist.h" @@ -147,7 +147,7 @@ using namespace mu::engraving::rw400; using ReadTypes = rtti::TypeListsetChangeInVelocity(e.readInt()); } else if (tag == "veloChangeSpeed") { d->setVelChangeSpeed(TConv::fromXml(e.readAsciiText(), DynamicSpeed::NORMAL)); + } else if (readProperty(d, tag, e, ctx, Pid::AVOID_BARLINES)) { + } else if (readProperty(d, tag, e, ctx, Pid::DYNAMICS_SIZE)) { + } else if (readProperty(d, tag, e, ctx, Pid::CENTER_ON_NOTEHEAD)) { } else if (!readProperties(static_cast(d), e, ctx)) { e.unknown(); } } } +void TRead::read(Expression* expr, XmlReader& xml, ReadContext& ctx) +{ + while (xml.readNextStartElement()) { + const AsciiStringView tag = xml.name(); + if (tag == "snapToDynamics") { + readProperty(expr, xml, ctx, Pid::SNAP_TO_DYNAMICS); + } else if (!readProperties(static_cast(expr), xml, ctx)) { + xml.unknown(); + } + } +} + void TRead::read(FretDiagram* d, XmlReader& e, ReadContext& ctx) { // Read the old format first diff --git a/src/engraving/rw/400/tread.h b/src/engraving/rw/400/tread.h index da31458ff4d72..49e8ac09710a9 100644 --- a/src/engraving/rw/400/tread.h +++ b/src/engraving/rw/400/tread.h @@ -36,6 +36,7 @@ class EngravingItem; class TextBase; class TempoText; class Dynamic; +class Expression; class FretDiagram; class Sticking; class SystemText; @@ -205,6 +206,7 @@ class TRead static void read(Clef* c, XmlReader& xml, ReadContext& ctx); static void read(Excerpt* item, XmlReader& xml, ReadContext& ctx); + static void read(Expression* item, XmlReader& xml, ReadContext& ctx); static void read(Fermata* f, XmlReader& xml, ReadContext& ctx); static void read(FiguredBass* b, XmlReader& xml, ReadContext& ctx); diff --git a/src/engraving/rw/400/twrite.cpp b/src/engraving/rw/400/twrite.cpp index 414c15c2e0c45..12a77bb0b4b1c 100644 --- a/src/engraving/rw/400/twrite.cpp +++ b/src/engraving/rw/400/twrite.cpp @@ -63,7 +63,7 @@ #include "../../libmscore/drumset.h" #include "../../libmscore/dynamic.h" - +#include "../../libmscore/expression.h" #include "../../libmscore/fermata.h" #include "../../libmscore/figuredbass.h" #include "../../libmscore/fingering.h" @@ -162,7 +162,7 @@ using namespace mu::engraving::rw400; using WriteTypes = rtti::TypeListisVelocityChangeAvailable()) { writeProperty(item, xml, Pid::VELO_CHANGE); writeProperty(item, xml, Pid::VELO_CHANGE_SPEED); } - writeProperties(static_cast(item), xml, ctx, item->dynamicType() == DynamicType::OTHER); + writeProperties(static_cast(item), xml, ctx, toDynamic(item)->hasCustomText()); + xml.endElement(); +} + +void TWrite::write(const Expression* item, XmlWriter& xml, WriteContext& ctx) +{ + if (!ctx.canWrite(item)) { + return; + } + xml.startElement(item); + writeProperties(static_cast(item), xml, ctx, true); xml.endElement(); } diff --git a/src/engraving/rw/400/twrite.h b/src/engraving/rw/400/twrite.h index 5b77c5e576048..20a64c5097a90 100644 --- a/src/engraving/rw/400/twrite.h +++ b/src/engraving/rw/400/twrite.h @@ -61,7 +61,7 @@ class Clef; class DurationElement; class Dynamic; - +class Expression; class Fermata; class FiguredBass; class FiguredBassItem; @@ -198,7 +198,7 @@ class TWrite static void write(const Clef* item, XmlWriter& xml, WriteContext& ctx); static void write(const Dynamic* item, XmlWriter& xml, WriteContext& ctx); - + static void write(const Expression* item, XmlWriter& xml, WriteContext& ctx); static void write(const Fermata* item, XmlWriter& xml, WriteContext& ctx); static void write(const FiguredBass* item, XmlWriter& xml, WriteContext& ctx); static void write(const FiguredBassItem* item, XmlWriter& xml, WriteContext& ctx); diff --git a/src/engraving/style/styledef.cpp b/src/engraving/style/styledef.cpp index 115b4b56d3bf5..d5a6d38b545f8 100644 --- a/src/engraving/style/styledef.cpp +++ b/src/engraving/style/styledef.cpp @@ -626,10 +626,15 @@ const std::array StyleDef::styleValue { Sid::autoplaceHairpinDynamicsDistance, "autoplaceHairpinDynamicsDistance", Spatium(0.5) }, + { Sid::dynamicsOverrideFont, "dynamicsOverrideFont", false }, + { Sid::dynamicsFont, "dynamicsFont", PropertyValue(String(u"Leland")) }, + { Sid::dynamicsSize, "dynamicsSize", 1.0 }, // percentage of the standard size { Sid::dynamicsPlacement, "dynamicsPlacement", PlacementV::BELOW }, { Sid::dynamicsPosAbove, "dynamicsPosAbove", PointF(.0, -1.5) }, { Sid::dynamicsPosBelow, "dynamicsPosBelow", PointF(.0, 2.5) }, - + { Sid::avoidBarLines, "avoidBarLines", true }, + { Sid::snapToDynamics, "snapToDynamics", true }, + { Sid::centerOnNotehead, "centerOnNotehead", true }, { Sid::dynamicsMinDistance, "dynamicsMinDistance", Spatium(0.5) }, { Sid::autoplaceVerticalAlignRange, "autoplaceVerticalAlignRange", int(VerticalAlignRange::SYSTEM) }, @@ -844,6 +849,7 @@ const std::array StyleDef::styleValue { Sid::partInstrumentFrameFgColor, "partInstrumentFrameFgColor", PropertyValue::fromValue(Color::BLACK) }, { Sid::partInstrumentFrameBgColor, "partInstrumentFrameBgColor", PropertyValue::fromValue(Color::transparent) }, + // OBSOLETE after version 4.1. Dynamic text now takes its setting from expression. { Sid::dynamicsFontFace, "dynamicsFontFace", "Edwin" }, { Sid::dynamicsFontSize, "dynamicsFontSize", 10.0 }, { Sid::dynamicsLineSpacing, "dynamicsLineSpacing", 1.0 }, @@ -873,6 +879,7 @@ const std::array StyleDef::styleValue { Sid::expressionFrameRound, "expressionFrameRound", 0 }, { Sid::expressionFrameFgColor, "expressionFrameFgColor", PropertyValue::fromValue(Color::BLACK) }, { Sid::expressionFrameBgColor, "expressionFrameBgColor", PropertyValue::fromValue(Color::transparent) }, + { Sid::expressionMinDistance, "expressionMinDistance", Spatium(.5) }, { Sid::tempoFontFace, "tempoFontFace", "Edwin" }, { Sid::tempoFontSize, "tempoFontSize", 12.0 }, diff --git a/src/engraving/style/styledef.h b/src/engraving/style/styledef.h index 4e97747bed581..08b739dc4b56d 100644 --- a/src/engraving/style/styledef.h +++ b/src/engraving/style/styledef.h @@ -640,10 +640,15 @@ enum class Sid { autoplaceHairpinDynamicsDistance, + dynamicsOverrideFont, + dynamicsFont, + dynamicsSize, dynamicsPlacement, dynamicsPosAbove, dynamicsPosBelow, - + avoidBarLines, + snapToDynamics, + centerOnNotehead, dynamicsMinDistance, autoplaceVerticalAlignRange, @@ -887,6 +892,7 @@ enum class Sid { expressionFrameRound, expressionFrameFgColor, expressionFrameBgColor, + expressionMinDistance, tempoFontFace, tempoFontSize, diff --git a/src/engraving/style/textstyle.cpp b/src/engraving/style/textstyle.cpp index a574ec3d07206..635badfb6e6c3 100644 --- a/src/engraving/style/textstyle.cpp +++ b/src/engraving/style/textstyle.cpp @@ -265,21 +265,22 @@ const TextStyle partInstrumentTextStyle { { { TextStylePropertyType::FrameFillColor, Sid::partInstrumentFrameBgColor, Pid::FRAME_BG_COLOR }, } }; +// With the single exception of text alignment, after 4.1 Dynamics use Expression text style const TextStyle dynamicsTextStyle { { - { TextStylePropertyType::FontFace, Sid::dynamicsFontFace, Pid::FONT_FACE }, - { TextStylePropertyType::FontSize, Sid::dynamicsFontSize, Pid::FONT_SIZE }, - { TextStylePropertyType::LineSpacing, Sid::dynamicsLineSpacing, Pid::TEXT_LINE_SPACING }, - { TextStylePropertyType::SizeSpatiumDependent, Sid::dynamicsFontSpatiumDependent, Pid::SIZE_SPATIUM_DEPENDENT }, - { TextStylePropertyType::FontStyle, Sid::dynamicsFontStyle, Pid::FONT_STYLE }, - { TextStylePropertyType::Color, Sid::dynamicsColor, Pid::COLOR }, + { TextStylePropertyType::FontFace, Sid::expressionFontFace, Pid::FONT_FACE }, + { TextStylePropertyType::FontSize, Sid::expressionFontSize, Pid::FONT_SIZE }, + { TextStylePropertyType::LineSpacing, Sid::expressionLineSpacing, Pid::TEXT_LINE_SPACING }, + { TextStylePropertyType::SizeSpatiumDependent, Sid::expressionFontSpatiumDependent, Pid::SIZE_SPATIUM_DEPENDENT }, + { TextStylePropertyType::FontStyle, Sid::expressionFontStyle, Pid::FONT_STYLE }, + { TextStylePropertyType::Color, Sid::expressionColor, Pid::COLOR }, { TextStylePropertyType::TextAlign, Sid::dynamicsAlign, Pid::ALIGN }, - { TextStylePropertyType::Offset, Sid::dynamicsPosBelow, Pid::OFFSET }, - { TextStylePropertyType::FrameType, Sid::dynamicsFrameType, Pid::FRAME_TYPE }, - { TextStylePropertyType::FramePadding, Sid::dynamicsFramePadding, Pid::FRAME_PADDING }, - { TextStylePropertyType::FrameWidth, Sid::dynamicsFrameWidth, Pid::FRAME_WIDTH }, - { TextStylePropertyType::FrameRound, Sid::dynamicsFrameRound, Pid::FRAME_ROUND }, - { TextStylePropertyType::FrameBorderColor, Sid::dynamicsFrameFgColor, Pid::FRAME_FG_COLOR }, - { TextStylePropertyType::FrameFillColor, Sid::dynamicsFrameBgColor, Pid::FRAME_BG_COLOR }, + { TextStylePropertyType::Offset, Sid::expressionOffset, Pid::OFFSET }, + { TextStylePropertyType::FrameType, Sid::expressionFrameType, Pid::FRAME_TYPE }, + { TextStylePropertyType::FramePadding, Sid::expressionFramePadding, Pid::FRAME_PADDING }, + { TextStylePropertyType::FrameWidth, Sid::expressionFrameWidth, Pid::FRAME_WIDTH }, + { TextStylePropertyType::FrameRound, Sid::expressionFrameRound, Pid::FRAME_ROUND }, + { TextStylePropertyType::FrameBorderColor, Sid::expressionFrameFgColor, Pid::FRAME_FG_COLOR }, + { TextStylePropertyType::FrameFillColor, Sid::expressionFrameBgColor, Pid::FRAME_BG_COLOR }, } }; const TextStyle expressionTextStyle { { @@ -1157,4 +1158,15 @@ const std::vector& primaryTextStyles() { return _primaryTextStyles; } + +static std::vector _editableTextStyles; + +const std::vector& editableTextStyles() +{ + if (_editableTextStyles.empty()) { + _editableTextStyles = allTextStyles(); + mu::remove(_editableTextStyles, TextStyleType::DYNAMICS); + } + return _editableTextStyles; +} } diff --git a/src/engraving/style/textstyle.h b/src/engraving/style/textstyle.h index 4ea4c4097bce6..1bec2dd836730 100644 --- a/src/engraving/style/textstyle.h +++ b/src/engraving/style/textstyle.h @@ -56,6 +56,7 @@ typedef std::array TextStyle; const TextStyle* textStyle(TextStyleType); const std::vector& allTextStyles(); +const std::vector& editableTextStyles(); const std::vector& primaryTextStyles(); } diff --git a/src/engraving/types/types.h b/src/engraving/types/types.h index 7d7f953223d91..41f0863501454 100644 --- a/src/engraving/types/types.h +++ b/src/engraving/types/types.h @@ -96,6 +96,7 @@ enum class ElementType { FERMATA, CHORDLINE, DYNAMIC, + EXPRESSION, BEAM, BEAM_SEGMENT, HOOK, diff --git a/src/engraving/types/typesconv.cpp b/src/engraving/types/typesconv.cpp index 9b68df0fa3e7c..3aad3f37f1117 100644 --- a/src/engraving/types/typesconv.cpp +++ b/src/engraving/types/typesconv.cpp @@ -168,6 +168,7 @@ static const std::vector > ELEMENT_TYPES = { { ElementType::FERMATA, "Fermata", TranslatableString("engraving", "Fermata") }, { ElementType::CHORDLINE, "ChordLine", TranslatableString("engraving", "Chord line") }, { ElementType::DYNAMIC, "Dynamic", TranslatableString("engraving", "Dynamic") }, + { ElementType::EXPRESSION, "Expression", TranslatableString("engraving", "Expression") }, { ElementType::BEAM, "Beam", TranslatableString("engraving", "Beam") }, { ElementType::BEAM_SEGMENT, "BeamSegment", TranslatableString("engraving", "Beam segment") }, { ElementType::HOOK, "Hook", TranslatableString("engraving", "Flag") }, // internally called "Hook", but "Flag" in SMuFL, so here externally too diff --git a/src/framework/ui/view/iconcodes.h b/src/framework/ui/view/iconcodes.h index 92add3f6cd29e..734ee80190dd3 100644 --- a/src/framework/ui/view/iconcodes.h +++ b/src/framework/ui/view/iconcodes.h @@ -381,6 +381,10 @@ class IconCode QUAVER_REST = 0xF44C, + DYNAMIC_CENTER_1 = 0xF451, + DYNAMIC_CENTER_2 = 0xF452, + EXPRESSION = 0xF453, + NONE = 0xFFFF }; diff --git a/src/inspector/CMakeLists.txt b/src/inspector/CMakeLists.txt index 31bfef351b86a..5238330148dae 100644 --- a/src/inspector/CMakeLists.txt +++ b/src/inspector/CMakeLists.txt @@ -202,6 +202,10 @@ set(MODULE_SRC ${CMAKE_CURRENT_LIST_DIR}/models/notation/rests/beams/restbeamsettingsmodel.h ${CMAKE_CURRENT_LIST_DIR}/models/notation/rests/restsettingsproxymodel.cpp ${CMAKE_CURRENT_LIST_DIR}/models/notation/rests/restsettingsproxymodel.h + ${CMAKE_CURRENT_LIST_DIR}/models/notation/dynamics/dynamicsettingsmodel.cpp + ${CMAKE_CURRENT_LIST_DIR}/models/notation/dynamics/dynamicsettingsmodel.h + ${CMAKE_CURRENT_LIST_DIR}/models/notation/expressions/expressionsettingsmodel.h + ${CMAKE_CURRENT_LIST_DIR}/models/notation/expressions/expressionsettingsmodel.cpp ${CMAKE_CURRENT_LIST_DIR}/models/inspectorpopupcontroller.cpp ${CMAKE_CURRENT_LIST_DIR}/models/inspectorpopupcontroller.h ${CMAKE_CURRENT_LIST_DIR}/internal/services/elementrepositoryservice.cpp diff --git a/src/inspector/models/abstractinspectormodel.cpp b/src/inspector/models/abstractinspectormodel.cpp index e9907ee1b66aa..9e977e84a6988 100644 --- a/src/inspector/models/abstractinspectormodel.cpp +++ b/src/inspector/models/abstractinspectormodel.cpp @@ -20,6 +20,7 @@ * along with this program. If not, see . */ #include "abstractinspectormodel.h" +#include "libmscore/dynamic.h" #include "types/texttypes.h" @@ -91,7 +92,9 @@ static const QMap NOTATION_ELEME { mu::engraving::ElementType::GRADUAL_TEMPO_CHANGE_SEGMENT, InspectorModelType::TYPE_GRADUAL_TEMPO_CHANGE }, { mu::engraving::ElementType::INSTRUMENT_NAME, InspectorModelType::TYPE_INSTRUMENT_NAME }, { mu::engraving::ElementType::LYRICS, InspectorModelType::TYPE_LYRICS }, - { mu::engraving::ElementType::REST, InspectorModelType::TYPE_REST } + { mu::engraving::ElementType::REST, InspectorModelType::TYPE_REST }, + { mu::engraving::ElementType::DYNAMIC, InspectorModelType::TYPE_DYNAMIC }, + { mu::engraving::ElementType::EXPRESSION, InspectorModelType::TYPE_EXPRESSION } }; static QMap HAIRPIN_ELEMENT_MODEL_TYPES = { @@ -213,7 +216,25 @@ InspectorModelTypeSet AbstractInspectorModel::modelTypesByElementKeys(const Elem return types; } -InspectorSectionTypeSet AbstractInspectorModel::sectionTypesByElementKeys(const ElementKeySet& elementKeySet, bool isRange) +static bool isPureDynamics(const QList& selectedElementList) +{ + for (EngravingItem* item : selectedElementList) { + if (!item->isTextBase()) { + continue; + } + if (!item->isDynamic()) { + return false; + } + Dynamic* dynamic = toDynamic(item); + if (dynamic->hasCustomText()) { + return false; + } + } + return true; +} + +InspectorSectionTypeSet AbstractInspectorModel::sectionTypesByElementKeys(const ElementKeySet& elementKeySet, bool isRange, + const QList& selectedElementList) { InspectorSectionTypeSet types; @@ -223,7 +244,8 @@ InspectorSectionTypeSet AbstractInspectorModel::sectionTypesByElementKeys(const types << InspectorSectionType::SECTION_NOTATION; } - if (TEXT_ELEMENT_TYPES.contains(key.type)) { + // Don't show the "Text" inspector panel for "pure" dynamics (i.e. without custom text) + if (TEXT_ELEMENT_TYPES.contains(key.type) && !isPureDynamics(selectedElementList)) { types << InspectorSectionType::SECTION_TEXT; } diff --git a/src/inspector/models/abstractinspectormodel.h b/src/inspector/models/abstractinspectormodel.h index 505160ce37c18..97d0937e25f00 100644 --- a/src/inspector/models/abstractinspectormodel.h +++ b/src/inspector/models/abstractinspectormodel.h @@ -117,6 +117,7 @@ class AbstractInspectorModel : public QObject, public async::Asyncable TYPE_TREMOLO, TYPE_MEASURE_REPEAT, TYPE_DYNAMIC, + TYPE_EXPRESSION, TYPE_TUPLET, TYPE_TEXT_LINE, TYPE_GRADUAL_TEMPO_CHANGE, @@ -139,7 +140,8 @@ class AbstractInspectorModel : public QObject, public async::Asyncable static InspectorModelType modelTypeByElementKey(const ElementKey& elementKey); static QSet modelTypesByElementKeys(const ElementKeySet& elementKeySet); - static QSet sectionTypesByElementKeys(const ElementKeySet& elementKeySet, bool isRange); + static QSet sectionTypesByElementKeys(const ElementKeySet& elementKeySet, bool isRange, + const QList& selectedElementList = {}); virtual bool isEmpty() const; diff --git a/src/inspector/models/inspectorlistmodel.cpp b/src/inspector/models/inspectorlistmodel.cpp index ffc8ee38ae41e..1cb2fb4aaf9cb 100644 --- a/src/inspector/models/inspectorlistmodel.cpp +++ b/src/inspector/models/inspectorlistmodel.cpp @@ -47,12 +47,14 @@ InspectorListModel::InspectorListModel(QObject* parent) }); } -void InspectorListModel::buildModelsForSelectedElements(const ElementKeySet& selectedElementKeySet, bool isRangeSelection) +void InspectorListModel::buildModelsForSelectedElements(const ElementKeySet& selectedElementKeySet, bool isRangeSelection, + const QList& selectedElementList) { removeUnusedModels(selectedElementKeySet, isRangeSelection); InspectorSectionTypeSet buildingSectionTypeSet = AbstractInspectorModel::sectionTypesByElementKeys(selectedElementKeySet, - isRangeSelection); + isRangeSelection, + selectedElementList); createModelsBySectionType(buildingSectionTypeSet.values(), selectedElementKeySet); @@ -90,7 +92,7 @@ void InspectorListModel::setElementList(const QListtype(), element->subtype()); } - buildModelsForSelectedElements(newElementKeySet, selectionState == SelectionState::RANGE); + buildModelsForSelectedElements(newElementKeySet, selectionState == SelectionState::RANGE, selectedElementList); } m_repository->updateElementList(selectedElementList, selectionState); diff --git a/src/inspector/models/inspectorlistmodel.h b/src/inspector/models/inspectorlistmodel.h index 7263ca7b8193d..9ec8249f1ad89 100644 --- a/src/inspector/models/inspectorlistmodel.h +++ b/src/inspector/models/inspectorlistmodel.h @@ -58,7 +58,8 @@ class InspectorListModel : public QAbstractListModel, public mu::async::Asyncabl notation::SelectionState selectionState = notation::SelectionState::NONE); void buildModelsForEmptySelection(); - void buildModelsForSelectedElements(const ElementKeySet& selectedElementKeySet, bool isRangeSelection); + void buildModelsForSelectedElements(const ElementKeySet& selectedElementKeySet, bool isRangeSelection, + const QList& selectedElementList); void createModelsBySectionType(const QList& sectionTypeList, const ElementKeySet& selectedElementKeySet = {}); void removeUnusedModels(const ElementKeySet& newElementKeySet, bool isRangeSelection, diff --git a/src/inspector/models/inspectormodelcreator.cpp b/src/inspector/models/inspectormodelcreator.cpp index c0e5a30dbd177..397026942e00d 100644 --- a/src/inspector/models/inspectormodelcreator.cpp +++ b/src/inspector/models/inspectormodelcreator.cpp @@ -71,6 +71,8 @@ #include "notation/lyrics/lyricssettingsmodel.h" #include "notation/rests/beams/restbeamsettingsmodel.h" #include "notation/rests/restsettingsproxymodel.h" +#include "notation/dynamics/dynamicsettingsmodel.h" +#include "notation/expressions/expressionsettingsmodel.h" using namespace mu::inspector; @@ -184,9 +186,12 @@ AbstractInspectorModel* InspectorModelCreator::newInspectorModel(InspectorModelT return new RestSettingsProxyModel(parent, repository); case InspectorModelType::TYPE_REST_BEAM: return new RestBeamSettingsModel(parent, repository); + case InspectorModelType::TYPE_DYNAMIC: + return new DynamicsSettingsModel(parent, repository); + case InspectorModelType::TYPE_EXPRESSION: + return new ExpressionSettingsModel(parent, repository); case InspectorModelType::TYPE_BREATH: case InspectorModelType::TYPE_ARPEGGIO: - case InspectorModelType::TYPE_DYNAMIC: case InspectorModelType::TYPE_UNDEFINED: break; } diff --git a/src/inspector/models/notation/dynamics/dynamicsettingsmodel.cpp b/src/inspector/models/notation/dynamics/dynamicsettingsmodel.cpp new file mode 100644 index 0000000000000..ad281fb40b930 --- /dev/null +++ b/src/inspector/models/notation/dynamics/dynamicsettingsmodel.cpp @@ -0,0 +1,159 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore BVBA and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "dynamicsettingsmodel.h" + +#include "translation.h" + +#include "types/texttypes.h" + +using namespace mu::inspector; + +DynamicsSettingsModel::DynamicsSettingsModel(QObject* parent, IElementRepositoryService* repository) + : AbstractInspectorModel(parent, repository) +{ + setModelType(InspectorModelType::TYPE_DYNAMIC); + setTitle(qtrc("inspector ", "Dynamics")); + setIcon(ui::IconCode::Code::DYNAMIC_FORTE); + createProperties(); +} + +void DynamicsSettingsModel::createProperties() +{ + m_avoidBarLines = buildPropertyItem(mu::engraving::Pid::AVOID_BARLINES); + m_dynamicSize = buildPropertyItem(mu::engraving::Pid::DYNAMICS_SIZE, [this](const mu::engraving::Pid pid, const QVariant& newValue) { + onPropertyValueChanged(pid, newValue.toDouble() / 100); + }); + m_centerOnNotehead = buildPropertyItem(mu::engraving::Pid::CENTER_ON_NOTEHEAD); + m_placement = buildPropertyItem(mu::engraving::Pid::PLACEMENT); + + m_frameType = buildPropertyItem(mu::engraving::Pid::FRAME_TYPE, [this](const mu::engraving::Pid pid, const QVariant& newValue) { + onPropertyValueChanged(pid, newValue); + updateFramePropertiesAvailability(); + }); + m_frameBorderColor = buildPropertyItem(mu::engraving::Pid::FRAME_FG_COLOR); + m_frameFillColor = buildPropertyItem(mu::engraving::Pid::FRAME_BG_COLOR); + m_frameThickness = buildPropertyItem(mu::engraving::Pid::FRAME_WIDTH); + m_frameMargin = buildPropertyItem(mu::engraving::Pid::FRAME_PADDING); + m_frameCornerRadius = buildPropertyItem(mu::engraving::Pid::FRAME_ROUND); +} + +void DynamicsSettingsModel::requestElements() +{ + m_elementList = m_repository->findElementsByType(mu::engraving::ElementType::DYNAMIC); +} + +void DynamicsSettingsModel::loadProperties() +{ + loadPropertyItem(m_avoidBarLines); + loadPropertyItem(m_dynamicSize, [](const QVariant& elementPropertyValue) -> QVariant { + return DataFormatter::roundDouble(elementPropertyValue.toDouble()) * 100; + }); + loadPropertyItem(m_centerOnNotehead); + loadPropertyItem(m_placement); + + loadPropertyItem(m_frameType); + loadPropertyItem(m_frameBorderColor); + loadPropertyItem(m_frameFillColor); + loadPropertyItem(m_frameThickness, formatDoubleFunc); + loadPropertyItem(m_frameMargin, formatDoubleFunc); + loadPropertyItem(m_frameCornerRadius, formatDoubleFunc); + + updateFramePropertiesAvailability(); +} + +void DynamicsSettingsModel::resetProperties() +{ + m_avoidBarLines->resetToDefault(); + m_dynamicSize->resetToDefault(); + m_centerOnNotehead->resetToDefault(); + m_placement->resetToDefault(); + + m_frameType->resetToDefault(); + m_frameBorderColor->resetToDefault(); + m_frameFillColor->resetToDefault(); + m_frameThickness->resetToDefault(); + m_frameMargin->resetToDefault(); + m_frameCornerRadius->resetToDefault(); +} + +PropertyItem* DynamicsSettingsModel::avoidBarLines() const +{ + return m_avoidBarLines; +} + +PropertyItem* DynamicsSettingsModel::dynamicSize() const +{ + return m_dynamicSize; +} + +PropertyItem* DynamicsSettingsModel::centerOnNotehead() const +{ + return m_centerOnNotehead; +} + +PropertyItem* DynamicsSettingsModel::placement() const +{ + return m_placement; +} + +PropertyItem* DynamicsSettingsModel::frameType() const +{ + return m_frameType; +} + +PropertyItem* DynamicsSettingsModel::frameBorderColor() const +{ + return m_frameBorderColor; +} + +PropertyItem* DynamicsSettingsModel::frameFillColor() const +{ + return m_frameFillColor; +} + +PropertyItem* DynamicsSettingsModel::frameThickness() const +{ + return m_frameThickness; +} + +PropertyItem* DynamicsSettingsModel::frameMargin() const +{ + return m_frameMargin; +} + +PropertyItem* DynamicsSettingsModel::frameCornerRadius() const +{ + return m_frameCornerRadius; +} + +void DynamicsSettingsModel::updateFramePropertiesAvailability() +{ + bool isFrameVisible = static_cast(m_frameType->value().toInt()) + != TextTypes::FrameType::FRAME_TYPE_NONE; + + m_frameThickness->setIsEnabled(isFrameVisible); + m_frameBorderColor->setIsEnabled(isFrameVisible); + m_frameFillColor->setIsEnabled(isFrameVisible); + m_frameMargin->setIsEnabled(isFrameVisible); + m_frameCornerRadius->setIsEnabled( + static_cast(m_frameType->value().toInt()) == TextTypes::FrameType::FRAME_TYPE_SQUARE); +} diff --git a/src/inspector/models/notation/dynamics/dynamicsettingsmodel.h b/src/inspector/models/notation/dynamics/dynamicsettingsmodel.h new file mode 100644 index 0000000000000..afc759a7ff2c9 --- /dev/null +++ b/src/inspector/models/notation/dynamics/dynamicsettingsmodel.h @@ -0,0 +1,82 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore BVBA and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef MU_INSPECTOR_DYNAMICSETTINGSMODEL_H +#define MU_INSPECTOR_DYNAMICSETTINGSMODEL_H + +#include "models/abstractinspectormodel.h" + +namespace mu::inspector { +class DynamicsSettingsModel : public AbstractInspectorModel +{ + Q_OBJECT + + Q_PROPERTY(PropertyItem * avoidBarLines READ avoidBarLines CONSTANT) + Q_PROPERTY(PropertyItem * dynamicSize READ dynamicSize CONSTANT) + Q_PROPERTY(PropertyItem * centerOnNotehead READ centerOnNotehead CONSTANT) + Q_PROPERTY(PropertyItem * placement READ placement CONSTANT) + + // Frame-related settings + Q_PROPERTY(PropertyItem * frameType READ frameType CONSTANT) + Q_PROPERTY(PropertyItem * frameBorderColor READ frameBorderColor CONSTANT) + Q_PROPERTY(PropertyItem * frameFillColor READ frameFillColor CONSTANT) + Q_PROPERTY(PropertyItem * frameThickness READ frameThickness CONSTANT) + Q_PROPERTY(PropertyItem * frameMargin READ frameMargin CONSTANT) + Q_PROPERTY(PropertyItem * frameCornerRadius READ frameCornerRadius CONSTANT) + +public: + explicit DynamicsSettingsModel(QObject* parent, IElementRepositoryService* repository); + + void createProperties() override; + void requestElements() override; + void loadProperties() override; + void resetProperties() override; + + PropertyItem* avoidBarLines() const; + PropertyItem* dynamicSize() const; + PropertyItem* centerOnNotehead() const; + PropertyItem* placement() const; + + PropertyItem* frameType() const; + PropertyItem* frameBorderColor() const; + PropertyItem* frameFillColor() const; + PropertyItem* frameThickness() const; + PropertyItem* frameMargin() const; + PropertyItem* frameCornerRadius() const; + +private: + PropertyItem* m_avoidBarLines = nullptr; + PropertyItem* m_dynamicSize = nullptr; + PropertyItem* m_centerOnNotehead = nullptr; + PropertyItem* m_placement = nullptr; + + PropertyItem* m_frameType = nullptr; + PropertyItem* m_frameBorderColor = nullptr; + PropertyItem* m_frameFillColor = nullptr; + PropertyItem* m_frameThickness = nullptr; + PropertyItem* m_frameMargin = nullptr; + PropertyItem* m_frameCornerRadius = nullptr; + + void updateFramePropertiesAvailability(); +}; +} + +#endif // MU_INSPECTOR_DYNAMICSETTINGSMODEL_H diff --git a/src/inspector/models/notation/expressions/expressionsettingsmodel.cpp b/src/inspector/models/notation/expressions/expressionsettingsmodel.cpp new file mode 100644 index 0000000000000..de1d5ea1c5efb --- /dev/null +++ b/src/inspector/models/notation/expressions/expressionsettingsmodel.cpp @@ -0,0 +1,39 @@ +#include "expressionsettingsmodel.h" + +#include "translation.h" + +using namespace mu::inspector; + +ExpressionSettingsModel::ExpressionSettingsModel(QObject* parent, IElementRepositoryService* repository) + : AbstractInspectorModel(parent, repository) +{ + setModelType(InspectorModelType::TYPE_EXPRESSION); + setTitle(qtrc("inspector ", "Expression")); + setIcon(ui::IconCode::Code::EXPRESSION); + createProperties(); +} + +void ExpressionSettingsModel::createProperties() +{ + m_snapExpression = buildPropertyItem(mu::engraving::Pid::SNAP_TO_DYNAMICS); +} + +void ExpressionSettingsModel::requestElements() +{ + m_elementList = m_repository->findElementsByType(mu::engraving::ElementType::EXPRESSION); +} + +void ExpressionSettingsModel::loadProperties() +{ + loadPropertyItem(m_snapExpression); +} + +void ExpressionSettingsModel::resetProperties() +{ + m_snapExpression->resetToDefault(); +} + +PropertyItem* ExpressionSettingsModel::snapExpression() const +{ + return m_snapExpression; +} diff --git a/src/inspector/models/notation/expressions/expressionsettingsmodel.h b/src/inspector/models/notation/expressions/expressionsettingsmodel.h new file mode 100644 index 0000000000000..72088750329aa --- /dev/null +++ b/src/inspector/models/notation/expressions/expressionsettingsmodel.h @@ -0,0 +1,28 @@ +#ifndef EXPRESSIONSETTINGSMODEL_H +#define EXPRESSIONSETTINGSMODEL_H + +#include "models/abstractinspectormodel.h" + +namespace mu::inspector { +class ExpressionSettingsModel : public AbstractInspectorModel +{ + Q_OBJECT + + Q_PROPERTY(PropertyItem * snapExpression READ snapExpression CONSTANT) + +public: + explicit ExpressionSettingsModel(QObject* parent, IElementRepositoryService* repository); + + void createProperties() override; + void requestElements() override; + void loadProperties() override; + void resetProperties() override; + + PropertyItem* snapExpression() const; + +private: + PropertyItem* m_snapExpression = nullptr; +}; +} + +#endif // EXPRESSIONSETTINGSMODEL_H diff --git a/src/inspector/models/text/textsettingsmodel.cpp b/src/inspector/models/text/textsettingsmodel.cpp index 585565c95376e..1f5a923e7f0d0 100644 --- a/src/inspector/models/text/textsettingsmodel.cpp +++ b/src/inspector/models/text/textsettingsmodel.cpp @@ -26,6 +26,7 @@ #include "types/commontypes.h" #include "types/texttypes.h" +#include "engraving/libmscore/dynamic.h" #include "engraving/libmscore/textbase.h" #include "engraving/types/typesconv.h" @@ -149,6 +150,8 @@ void TextSettingsModel::loadProperties() updateFramePropertiesAvailability(); updateStaffPropertiesAvailability(); + updateIsDynamicSpecificSettings(); + updateIsHorizontalAlignmentAvailable(); } void TextSettingsModel::resetProperties() @@ -193,6 +196,7 @@ void TextSettingsModel::onNotationChanged(const PropertyIdSet&, const StyleIdSet return; } } + updateIsHorizontalAlignmentAvailable(); } void TextSettingsModel::insertSpecialCharacters() @@ -317,6 +321,16 @@ bool TextSettingsModel::isSpecialCharactersInsertionAvailable() const return m_isSpecialCharactersInsertionAvailable; } +bool TextSettingsModel::isDynamicSpecificSettings() const +{ + return m_isDynamicSpecificSettings; +} + +bool TextSettingsModel::isHorizontalAlignmentAvailable() const +{ + return m_isHorizontalAlignmentAvailable; +} + void TextSettingsModel::setAreStaffTextPropertiesAvailable(bool areStaffTextPropertiesAvailable) { if (m_areStaffTextPropertiesAvailable == areStaffTextPropertiesAvailable) { @@ -337,6 +351,26 @@ void TextSettingsModel::setIsSpecialCharactersInsertionAvailable(bool isSpecialC emit isSpecialCharactersInsertionAvailableChanged(m_isSpecialCharactersInsertionAvailable); } +void TextSettingsModel::setIsDynamicSpecificSettings(bool isOnlyDynamics) +{ + if (isOnlyDynamics == m_isDynamicSpecificSettings) { + return; + } + + m_isDynamicSpecificSettings = isOnlyDynamics; + emit isDynamicSpecificSettingsChanged(m_isDynamicSpecificSettings); +} + +void TextSettingsModel::setIsHorizontalAlignmentAvailable(bool isHorizontalAlignmentAvailable) +{ + if (isHorizontalAlignmentAvailable == m_isHorizontalAlignmentAvailable) { + return; + } + + m_isHorizontalAlignmentAvailable = isHorizontalAlignmentAvailable; + emit isHorizontalAlignmentAvailableChanged(m_isHorizontalAlignmentAvailable); +} + void TextSettingsModel::updateFramePropertiesAvailability() { bool isFrameVisible = static_cast(m_frameType->value().toInt()) @@ -358,6 +392,31 @@ void TextSettingsModel::updateStaffPropertiesAvailability() setAreStaffTextPropertiesAvailable(isAvailable && !m_textType->isUndefined()); } +void TextSettingsModel::updateIsDynamicSpecificSettings() +{ + bool isOnlyDynamic = true; + for (EngravingItem* item : m_elementList) { + if (!item->isDynamic()) { + isOnlyDynamic = false; + break; + } + } + setIsDynamicSpecificSettings(isOnlyDynamic); +} + +void TextSettingsModel::updateIsHorizontalAlignmentAvailable() +{ + bool available = false; + for (EngravingItem* item : m_elementList) { + bool isPureAlignedDynamic = item->isDynamic() && toDynamic(item)->centerOnNotehead(); + if (!isPureAlignedDynamic) { + available = true; + break; + } + } + setIsHorizontalAlignmentAvailable(available); +} + bool TextSettingsModel::isTextEditingStarted() const { IF_ASSERT_FAILED(context() && context()->currentNotation()) { diff --git a/src/inspector/models/text/textsettingsmodel.h b/src/inspector/models/text/textsettingsmodel.h index f4d9c58760e78..0cf96ea06ca80 100644 --- a/src/inspector/models/text/textsettingsmodel.h +++ b/src/inspector/models/text/textsettingsmodel.h @@ -59,6 +59,8 @@ class TextSettingsModel : public AbstractInspectorModel Q_PROPERTY(bool areStaffTextPropertiesAvailable READ areStaffTextPropertiesAvailable NOTIFY areStaffTextPropertiesAvailableChanged) Q_PROPERTY( bool isSpecialCharactersInsertionAvailable READ isSpecialCharactersInsertionAvailable NOTIFY isSpecialCharactersInsertionAvailableChanged) + Q_PROPERTY(bool isDynamicSpecificSettings READ isDynamicSpecificSettings NOTIFY isDynamicSpecificSettingsChanged) + Q_PROPERTY(bool isHorizontalAlignmentAvailable READ isHorizontalAlignmentAvailable NOTIFY isHorizontalAlignmentAvailableChanged) public: explicit TextSettingsModel(QObject* parent, IElementRepositoryService* repository); @@ -96,18 +98,23 @@ class TextSettingsModel : public AbstractInspectorModel QVariantList textStyles(); bool areStaffTextPropertiesAvailable() const; - bool isSpecialCharactersInsertionAvailable() const; + bool isDynamicSpecificSettings() const; + bool isHorizontalAlignmentAvailable() const; public slots: void setAreStaffTextPropertiesAvailable(bool areStaffTextPropertiesAvailable); void setIsSpecialCharactersInsertionAvailable(bool isSpecialCharactersInsertionAvailable); + void setIsDynamicSpecificSettings(bool isOnlyDynamics); + void setIsHorizontalAlignmentAvailable(bool isHorizontalAlignmentAvailable); signals: void textStylesChanged(); void areStaffTextPropertiesAvailableChanged(bool areStaffTextPropertiesAvailable); void isSpecialCharactersInsertionAvailableChanged(bool isSpecialCharactersInsertionAvailable); + void isDynamicSpecificSettingsChanged(bool isDynamicSpecificSettings); + void isHorizontalAlignmentAvailableChanged(bool isHorizontalAlignmentAvailable); private: bool isTextEditingStarted() const; @@ -115,6 +122,8 @@ public slots: void updateFramePropertiesAvailability(); void updateStaffPropertiesAvailability(); + void updateIsDynamicSpecificSettings(); + void updateIsHorizontalAlignmentAvailable(); PropertyItem* m_fontFamily = nullptr; PropertyItem* m_fontStyle = nullptr; @@ -139,6 +148,8 @@ public slots: bool m_areStaffTextPropertiesAvailable = false; bool m_isSpecialCharactersInsertionAvailable = false; + bool m_isDynamicSpecificSettings = false; + bool m_isHorizontalAlignmentAvailable = true; }; } diff --git a/src/inspector/types/texttypes.h b/src/inspector/types/texttypes.h index 3258d09f76ecc..f3d709eaf8aa5 100644 --- a/src/inspector/types/texttypes.h +++ b/src/inspector/types/texttypes.h @@ -142,6 +142,7 @@ static const QList TEXT_ELEMENT_TYPES = { mu::engraving::ElementType::SYSTEM_TEXT, mu::engraving::ElementType::TRIPLET_FEEL, mu::engraving::ElementType::DYNAMIC, + mu::engraving::ElementType::EXPRESSION, mu::engraving::ElementType::FIGURED_BASS, mu::engraving::ElementType::FINGERING, mu::engraving::ElementType::HARMONY, diff --git a/src/inspector/view/inspector_resources.qrc b/src/inspector/view/inspector_resources.qrc index 19e77b75c0bec..047bfdb6edac2 100644 --- a/src/inspector/view/inspector_resources.qrc +++ b/src/inspector/view/inspector_resources.qrc @@ -99,5 +99,8 @@ qml/MuseScore/Inspector/notation/lyrics/LyricsSettings.qml qml/MuseScore/Inspector/notation/rests/RestBeamSettings.qml qml/MuseScore/Inspector/notation/rests/RestSettings.qml + qml/MuseScore/Inspector/notation/dynamics/DynamicsSettings.qml + qml/MuseScore/Inspector/notation/expressions/ExpressionsSettings.qml + qml/MuseScore/Inspector/common/FrameSettings.qml diff --git a/src/inspector/view/qml/MuseScore/Inspector/common/FlatRadioButtonGroupPropertyView.qml b/src/inspector/view/qml/MuseScore/Inspector/common/FlatRadioButtonGroupPropertyView.qml index 661b3108fecde..35d928cf1c3c3 100644 --- a/src/inspector/view/qml/MuseScore/Inspector/common/FlatRadioButtonGroupPropertyView.qml +++ b/src/inspector/view/qml/MuseScore/Inspector/common/FlatRadioButtonGroupPropertyView.qml @@ -30,6 +30,8 @@ InspectorPropertyView { property alias radioButtonGroup: radioButtonGroupItem property alias model: radioButtonGroupItem.model + property int requestHeight: 30 + property int requestIconFontSize: 0 navigationRowEnd: navigationRowStart /* Menu button */ + radioButtonGroupItem.count /* FlatRadioButtons */ @@ -43,6 +45,7 @@ InspectorPropertyView { text: modelData["text"] ?? "" iconCode: modelData["iconCode"] ?? IconCode.NONE + iconFontSize: root.requestIconFontSize != 0 ? root.requestIconFontSize : ui.theme.iconsFont.pixelSize navigation.name: root.navigationName + (Boolean(text) ? text : modelData["title"]) navigation.panel: root.navigationPanel @@ -62,7 +65,7 @@ InspectorPropertyView { RadioButtonGroup { id: radioButtonGroupItem - height: 30 + height: root.requestHeight width: parent.width delegate: Delegate {} diff --git a/src/inspector/view/qml/MuseScore/Inspector/common/FrameSettings.qml b/src/inspector/view/qml/MuseScore/Inspector/common/FrameSettings.qml new file mode 100644 index 0000000000000..e586c810bfe91 --- /dev/null +++ b/src/inspector/view/qml/MuseScore/Inspector/common/FrameSettings.qml @@ -0,0 +1,169 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore BVBA and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.15 + +import MuseScore.Ui 1.0 +import MuseScore.UiComponents 1.0 +import MuseScore.Inspector 1.0 + +Column { + id: frameSettings + + width: parent.width + spacing: 12 + + property string navigationName: "FrameSettings" + property NavigationPanel navigationPanel: null + property int navigationRowStart: 0 + + required property QtObject frameTypePropertyItem + required property QtObject frameBorderColorPropertyItem + required property QtObject frameFillColorPropertyItem + required property QtObject frameThicknessPropertyItem + required property QtObject frameMarginPropertyItem + required property QtObject frameCornerRadiusPropertyItem + + FlatRadioButtonGroupPropertyView { + id: frameSection + titleText: qsTrc("inspector", "Frame") + propertyItem: frameSettings.frameTypePropertyItem ? frameSettings.frameTypePropertyItem : null + + navigationName: "FrameMenu" + navigationPanel: frameSettings.navigationPanel + navigationRowStart: frameSettings.navigationRowStart + + model: [ + { text: qsTrc("inspector", "None"), value: TextTypes.FRAME_TYPE_NONE, titleRole: qsTrc("inspector", "None") }, + { iconCode: IconCode.FRAME_SQUARE, value: TextTypes.FRAME_TYPE_SQUARE, titleRole: qsTrc("inspector", "Rectangle") }, + { iconCode: IconCode.FRAME_CIRCLE, value: TextTypes.FRAME_TYPE_CIRCLE, titleRole: qsTrc("inspector", "Circle") } + ] + } + + Item { + height: childrenRect.height + width: parent.width + + ColorSection { + id: borderColorSection + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.rightMargin: 2 + + navigationName: "BorderColorMenu" + navigationPanel: frameSettings.navigationPanel + navigationRowStart: frameSection.navigationRowEnd + 1 + + visible: frameSettings.frameBorderColorPropertyItem.isEnabled + height: visible ? implicitHeight : 0 + + titleText: qsTrc("inspector", "Border") + propertyItem: frameSettings.frameBorderColorPropertyItem + } + + ColorSection { + id: highlightColorSection + anchors.left: parent.horizontalCenter + anchors.leftMargin: 2 + anchors.right: parent.right + + navigationName: "HighlightColorMenu" + navigationPanel: frameSettings.navigationPanel + navigationRowStart: borderColorSection.navigationRowEnd + 1 + + visible: frameSettings.frameFillColorPropertyItem.isEnabled + height: visible ? implicitHeight : 0 + + titleText: qsTrc("inspector", "Fill color") + propertyItem: frameSettings.frameFillColorPropertyItem + } + } + + Item { + height: childrenRect.height + width: parent.width + + SpinBoxPropertyView { + id: thicknessSection + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.rightMargin: 2 + + navigationName: "Thickness" + navigationPanel: frameSettings.navigationPanel + navigationRowStart: highlightColorSection.navigationRowEnd + 1 + + visible: frameSettings.frameThicknessPropertyItem.isEnabled + height: visible ? implicitHeight : 0 + + titleText: qsTrc("inspector", "Thickness") + propertyItem: frameSettings.frameThicknessPropertyItem + + step: 0.1 + minValue: 0 + maxValue: 5 + } + + SpinBoxPropertyView { + id: marginSection + anchors.left: parent.horizontalCenter + anchors.leftMargin: 2 + anchors.right: parent.right + + navigationName: "Margin" + navigationPanel: frameSettings.navigationPanel + navigationRowStart: thicknessSection.navigationRowEnd + 1 + + visible: frameSettings.frameMarginPropertyItem.isEnabled + height: visible ? implicitHeight : 0 + + titleText: qsTrc("inspector", "Margin") + propertyItem: frameSettings.frameMarginPropertyItem + + step: 0.1 + minValue: 0 + maxValue: 5 + } + } + + SpinBoxPropertyView { + id: cornerRadiusSection + anchors.left: parent.left + anchors.right: parent.horizontalCenter + anchors.rightMargin: 2 + + navigationName: "Corner radius" + navigationPanel: frameSettings.navigationPanel + navigationRowStart: marginSection.navigationRowEnd + 1 + + visible: frameSettings.frameCornerRadiusPropertyItem.isEnabled + height: visible ? implicitHeight : 0 + + titleText: qsTrc("inspector", "Corner radius") + propertyItem: frameSettings.frameCornerRadiusPropertyItem + + step: 1 + decimals: 2 + minValue: 0 + maxValue: 100 + } +} diff --git a/src/inspector/view/qml/MuseScore/Inspector/common/InspectorPropertyView.qml b/src/inspector/view/qml/MuseScore/Inspector/common/InspectorPropertyView.qml index b0df74d0cf3e7..9f16b4ee2ed2c 100644 --- a/src/inspector/view/qml/MuseScore/Inspector/common/InspectorPropertyView.qml +++ b/src/inspector/view/qml/MuseScore/Inspector/common/InspectorPropertyView.qml @@ -100,6 +100,7 @@ Column { active: visible sourceComponent: root.isStyled ? menuButtonComponent : resetButtonComponent + Layout.alignment: Qt.AlignRight Component { id: resetButtonComponent diff --git a/src/inspector/view/qml/MuseScore/Inspector/notation/NotationInspectorSectionLoader.qml b/src/inspector/view/qml/MuseScore/Inspector/notation/NotationInspectorSectionLoader.qml index 0a263b06947bf..2865eb62c0186 100644 --- a/src/inspector/view/qml/MuseScore/Inspector/notation/NotationInspectorSectionLoader.qml +++ b/src/inspector/view/qml/MuseScore/Inspector/notation/NotationInspectorSectionLoader.qml @@ -58,6 +58,8 @@ import "tuplets" import "instrumentname" import "lyrics" import "rests" +import "dynamics" +import "expressions" Loader { id: root @@ -129,6 +131,8 @@ Loader { case Inspector.TYPE_LYRICS: return lyricsComp case Inspector.TYPE_REST: return restComp case Inspector.TYPE_REST_BEAM: return restComp + case Inspector.TYPE_DYNAMIC: return dynamicComp + case Inspector.TYPE_EXPRESSION: return expressionComp } return null @@ -327,4 +331,14 @@ Loader { id: restComp RestSettings {} } + + Component { + id: dynamicComp + DynamicsSettings {} + } + + Component { + id: expressionComp + ExpressionsSettings {} + } } diff --git a/src/inspector/view/qml/MuseScore/Inspector/notation/dynamics/DynamicsSettings.qml b/src/inspector/view/qml/MuseScore/Inspector/notation/dynamics/DynamicsSettings.qml new file mode 100644 index 0000000000000..646dd759d0575 --- /dev/null +++ b/src/inspector/view/qml/MuseScore/Inspector/notation/dynamics/DynamicsSettings.qml @@ -0,0 +1,159 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore BVBA and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick 2.15 + +import MuseScore.Ui 1.0 +import MuseScore.UiComponents 1.0 +import MuseScore.Inspector 1.0 + +import "../../common" + +Column { + id: root + width: parent.width + height: childrenRect.height + + property QtObject model: null + + property NavigationPanel navigationPanel: null + property int navigationRowStart: 1 + + objectName: "DynamicsSettings" + + spacing: 12 + + function focusOnFirst() { + avoidBarLines.navigation.requestActive() + } + + SpinBoxPropertyView { + id: dynamicSize + + anchors.left: parent.left + + navigationName: "Scale" + navigationPanel: root.navigationPanel + navigationRowStart: root.navigationRowStart + 1 + + titleText: qsTrc("inspector", "Scale") + measureUnitsSymbol: qsTrc("global", "%") + propertyItem: root.model ? root.model.dynamicSize : null + + decimals: 0 + step: 1 + minValue: 0 + maxValue: 1000 + } + + InspectorPropertyView { + id: avoidBarLinesSection + titleText: "" + propertyItem: root.model ? root.model.avoidBarLines : null + + isModified: root.model ? root.model.avoidBarLines.isModified: false + + onRequestResetToDefault: { + if (root.model) { + root.model.avoidBarLines.resetToDefault() + } + } + + onRequestApplyToStyle: { + if (root.model) { + root.model.avoidBarLines.applyToStyle() + } + } + + Item { + CheckBoxPropertyView { + id: avoidBarLines + + navigation.name: "Avoid bar lines" + navigation.panel: root.navigationPanel + navigation.row: dynamicSize.navigationRowEnd + 1 + + text: qsTrc("inspector", "Avoid bar lines") + propertyItem: root.model ? root.model.avoidBarLines : null + height: implicitHeight + } + } + } + + + PlacementSection { + id: dynamicsPlacementSection + propertyItem: root.model ? root.model.placement : null + + navigationPanel: root.navigationPanel + navigationRowStart: avoidBarLinesSection.navigationRowEnd + 1 + } + + ExpandableBlank { + id: showItem + isExpanded: false + title: isExpanded ? qsTrc("inspector", "Show less") : qsTrc("inspector", "Show more") + + width: parent.width + + navigation.panel: root.navigationPanel + navigation.row: dynamicsPlacementSection.navigationRowEnd + 1 + + contentItemComponent: Column { + width: parent.width + spacing: root.spacing + + FlatRadioButtonGroupPropertyView { + id: dynamicAlignmentGroup + requestHeight: 55 + requestIconFontSize: 32 + + titleText: qsTrc("inspector", "Alignment with notehead") + propertyItem: root.model ? root.model.centerOnNotehead : null + + navigationPanel: root.navigationPanel + navigationRowStart: showItem.navigationRowStart + 1 + + model: [ + { iconCode: IconCode.DYNAMIC_CENTER_1, value: true, title: qsTrc("inspector", "Center on notehead") }, + { iconCode: IconCode.DYNAMIC_CENTER_2, value: false, title: qsTrc("inspector", "Use text centering") } + ] + } + + FrameSettings { + id: frameSettings + + navigationPanel: root.navigationPanel + navigationRowStart: parent.navigationRowEnd + 1 + + frameTypePropertyItem: root.model ? root.model.frameType : null + frameBorderColorPropertyItem: root.model ? root.model.frameBorderColor : null + frameFillColorPropertyItem: root.model ? root.model.frameFillColor : null + frameThicknessPropertyItem: root.model ? root.model.frameThickness : null + frameMarginPropertyItem: root.model ? root.model.frameMargin : null + frameCornerRadiusPropertyItem: root.model ? root.model.frameCornerRadius : null + } + } + } +} + + + diff --git a/src/inspector/view/qml/MuseScore/Inspector/notation/expressions/ExpressionsSettings.qml b/src/inspector/view/qml/MuseScore/Inspector/notation/expressions/ExpressionsSettings.qml new file mode 100644 index 0000000000000..e0eac4a042494 --- /dev/null +++ b/src/inspector/view/qml/MuseScore/Inspector/notation/expressions/ExpressionsSettings.qml @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore BVBA and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick 2.15 + +import MuseScore.Ui 1.0 +import MuseScore.UiComponents 1.0 +import MuseScore.Inspector 1.0 + +import "../../common" + +Column { + id: root + + property QtObject model: null + + property NavigationPanel navigationPanel: null + property int navigationRowStart: 1 + + objectName: "ExpressionsSettings" + + spacing: 12 + + function focusOnFirst() { + snapExpression.navigation.requestActive() + } + + InspectorPropertyView { + id: layoutSection + titleText: "" + propertyItem: root.model ? root.model.snapExpression : null + + navigationName: "LayoutMenu" + navigationPanel: root.navigationPanel + navigationRowStart: size.navigationRowEnd + 1 + navigationRowEnd: verticalAlignmentButtonList.navigationRowEnd + + isModified: root.model ? root.model.snapExpression.isModified : false + + onRequestResetToDefault: { + if (root.model) { + root.model.snapExpression.resetToDefault() + } + } + + onRequestApplyToStyle: { + if (root.model) { + root.model.snapExpression.applyToStyle() + } + } + + Item { + anchors.right: root.right + anchors.left: root.left + height: childrenRect.height + + CheckBoxPropertyView { + id: snapExpression + + navigation.name: "SnapExpressionCheckBox" + navigation.panel: root.navigationPanel + navigation.row: root.navigationRowStart + + text: qsTrc("inspector", "Snap to dynamic") + propertyItem: root.model ? root.model.snapExpression : null + } + } + + } +} diff --git a/src/inspector/view/qml/MuseScore/Inspector/text/TextInspectorView.qml b/src/inspector/view/qml/MuseScore/Inspector/text/TextInspectorView.qml index d16000842e21d..a20e90ec1816b 100644 --- a/src/inspector/view/qml/MuseScore/Inspector/text/TextInspectorView.qml +++ b/src/inspector/view/qml/MuseScore/Inspector/text/TextInspectorView.qml @@ -187,6 +187,7 @@ InspectorSectionView { width: parent.width RadioButtonGroup { + enabled: root.model ? root.model.isHorizontalAlignmentAvailable : false id: horizontalAlignmentButtonList property int navigationRowStart: alignmentSection.navigationRowStart + 1 diff --git a/src/inspector/view/qml/MuseScore/Inspector/text/TextSettings.qml b/src/inspector/view/qml/MuseScore/Inspector/text/TextSettings.qml index 0b26e10123be5..16d8ce17d93d9 100644 --- a/src/inspector/view/qml/MuseScore/Inspector/text/TextSettings.qml +++ b/src/inspector/view/qml/MuseScore/Inspector/text/TextSettings.qml @@ -99,128 +99,20 @@ Column { } } - FlatRadioButtonGroupPropertyView { - id: frameSection - titleText: qsTrc("inspector", "Frame") - propertyItem: root.model ? root.model.frameType : null + FrameSettings { + id: frameSettings + visible: root.model ? !root.model.isDynamicSpecificSettings : false + height: visible ? implicitHeight : 0 - navigationName: "FrameMenu" navigationPanel: root.navigationPanel navigationRowStart: subscriptOptionsButtonList.navigationRowEnd + 1 - model: [ - { text: qsTrc("inspector", "None"), value: TextTypes.FRAME_TYPE_NONE, titleRole: qsTrc("inspector", "None") }, - { iconCode: IconCode.FRAME_SQUARE, value: TextTypes.FRAME_TYPE_SQUARE, titleRole: qsTrc("inspector", "Rectangle") }, - { iconCode: IconCode.FRAME_CIRCLE, value: TextTypes.FRAME_TYPE_CIRCLE, titleRole: qsTrc("inspector", "Circle") } - ] - } - - Item { - height: childrenRect.height - width: parent.width - - ColorSection { - id: borderColorSection - anchors.left: parent.left - anchors.right: parent.horizontalCenter - anchors.rightMargin: 2 - - navigationName: "BorderColorMenu" - navigationPanel: root.navigationPanel - navigationRowStart: frameSection.navigationRowEnd + 1 - - visible: root.model ? root.model.frameBorderColor.isEnabled : false - height: visible ? implicitHeight : 0 - - titleText: qsTrc("inspector", "Border") - propertyItem: root.model ? root.model.frameBorderColor : null - } - - ColorSection { - id: highlightColorSection - anchors.left: parent.horizontalCenter - anchors.leftMargin: 2 - anchors.right: parent.right - - navigationName: "HighlightColorMenu" - navigationPanel: root.navigationPanel - navigationRowStart: borderColorSection.navigationRowEnd + 1 - - visible: root.model ? root.model.frameFillColor.isEnabled : false - height: visible ? implicitHeight : 0 - - titleText: qsTrc("inspector", "Fill color") - propertyItem: root.model ? root.model.frameFillColor : null - } - } - - Item { - height: childrenRect.height - width: parent.width - - SpinBoxPropertyView { - id: thicknessSection - anchors.left: parent.left - anchors.right: parent.horizontalCenter - anchors.rightMargin: 2 - - navigationName: "Thickness" - navigationPanel: root.navigationPanel - navigationRowStart: highlightColorSection.navigationRowEnd + 1 - - visible: root.model ? root.model.frameThickness.isEnabled : false - height: visible ? implicitHeight : 0 - - titleText: qsTrc("inspector", "Thickness") - propertyItem: root.model ? root.model.frameThickness : null - - step: 0.1 - minValue: 0 - maxValue: 5 - } - - SpinBoxPropertyView { - id: marginSection - anchors.left: parent.horizontalCenter - anchors.leftMargin: 2 - anchors.right: parent.right - - navigationName: "Margin" - navigationPanel: root.navigationPanel - navigationRowStart: thicknessSection.navigationRowEnd + 1 - - visible: root.model ? root.model.frameMargin.isEnabled : false - height: visible ? implicitHeight : 0 - - titleText: qsTrc("inspector", "Margin") - propertyItem: root.model ? root.model.frameMargin : null - - step: 0.1 - minValue: 0 - maxValue: 5 - } - } - - SpinBoxPropertyView { - id: cornerRadiusSection - anchors.left: parent.left - anchors.right: parent.horizontalCenter - anchors.rightMargin: 2 - - navigationName: "Corner radius" - navigationPanel: root.navigationPanel - navigationRowStart: marginSection.navigationRowEnd + 1 - - visible: root.model ? root.model.frameCornerRadius.isEnabled : false - height: visible ? implicitHeight : 0 - - titleText: qsTrc("inspector", "Corner radius") - propertyItem: root.model ? root.model.frameCornerRadius : null - - step: 1 - decimals: 2 - minValue: 0 - maxValue: 100 + frameTypePropertyItem: root.model ? root.model.frameType : null + frameBorderColorPropertyItem: root.model ? root.model.frameBorderColor : null + frameFillColorPropertyItem: root.model ? root.model.frameFillColor : null + frameThicknessPropertyItem: root.model ? root.model.frameThickness : null + frameMarginPropertyItem: root.model ? root.model.frameMargin : null + frameCornerRadiusPropertyItem: root.model ? root.model.frameCornerRadius : null } SeparatorLine { anchors.margins: -12 } @@ -233,7 +125,7 @@ Column { navigationName: "Line Spacing" navigationPanel: root.navigationPanel - navigationRowStart: cornerRadiusSection.navigationRowEnd + 1 + navigationRowStart: frameSettings.navigationRowEnd + 1 titleText: qsTrc("inspector", "Line spacing") //: Stands for "Lines". Used for text line spacing controls, for example. @@ -246,7 +138,10 @@ Column { maxValue: 10 } - SeparatorLine { anchors.margins: -12 } + SeparatorLine { + visible: root.model ? !root.model.isDynamicSpecificSettings : false + anchors.margins: -12 + } DropdownPropertyView { id: textStyleSection @@ -257,6 +152,9 @@ Column { navigationPanel: root.navigationPanel navigationRowStart: textLineSpacingSection.navigationRowEnd + 1 + visible: root.model ? !root.model.isDynamicSpecificSettings : false + height: visible ? implicitHeight : 0 + model: root.model ? root.model.textStyles : [] } @@ -264,6 +162,9 @@ Column { id: textPlacementSection propertyItem: root.model ? root.model.textPlacement : null + visible: root.model ? !root.model.isDynamicSpecificSettings : false + height: visible ? implicitHeight : 0 + navigationPanel: root.navigationPanel navigationRowStart: textStyleSection.navigationRowEnd + 1 } diff --git a/src/notation/view/widgets/editstyle.cpp b/src/notation/view/widgets/editstyle.cpp index b117bde91bbce..b35ce4124d641 100644 --- a/src/notation/view/widgets/editstyle.cpp +++ b/src/notation/view/widgets/editstyle.cpp @@ -535,6 +535,11 @@ EditStyle::EditStyle(QWidget* parent) { StyleId::dynamicsPosAbove, false, dynamicsPosAbove, resetDynamicsPosAbove }, { StyleId::dynamicsPosBelow, false, dynamicsPosBelow, resetDynamicsPosBelow }, { StyleId::dynamicsMinDistance, false, dynamicsMinDistance, resetDynamicsMinDistance }, + { StyleId::avoidBarLines, false, avoidBarLines, resetAvoidBarLines }, + { StyleId::snapToDynamics, false, snapExpression, resetSnapExpression }, + { StyleId::dynamicsSize, true, dynamicsSize, resetDynamicsSize }, + { StyleId::dynamicsOverrideFont, false, dynamicsOverrideFont, 0 }, + { StyleId::dynamicsFont, false, dynamicsFont, 0 }, { StyleId::tempoPlacement, false, tempoTextPlacement, resetTempoTextPlacement }, { StyleId::tempoPosAbove, false, tempoTextPosAbove, resetTempoTextPosAbove }, @@ -698,9 +703,10 @@ EditStyle::EditStyle(QWidget* parent) tupletBracketType->addItem(qtrc("notation/editstyle", "None", "no tuplet bracket type"), int(TupletBracketType::SHOW_NO_BRACKET)); musicalSymbolFont->clear(); - + dynamicsFont->clear(); for (auto i : engravingFonts()->fonts()) { musicalSymbolFont->addItem(QString::fromStdString(i->name()), QString::fromStdString(i->name())); + dynamicsFont->addItem(QString::fromStdString(i->name()), QString::fromStdString(i->name())); } static const SymId ids[] = { @@ -879,7 +885,7 @@ EditStyle::EditStyle(QWidget* parent) Score* score = globalContext()->currentNotation()->elements()->msScore(); textStyles->clear(); - for (TextStyleType textStyleType : allTextStyles()) { + for (TextStyleType textStyleType : editableTextStyles()) { QListWidgetItem* item = new QListWidgetItem(score->getTextStyleUserName(textStyleType).qTranslated()); item->setData(Qt::UserRole, int(textStyleType)); textStyles->addItem(item); @@ -1339,6 +1345,8 @@ EditStyle::EditStylePage EditStyle::pageForElement(EngravingItem* e) return &EditStyle::PageLyrics; case ElementType::DYNAMIC: return &EditStyle::PageDynamics; + case ElementType::EXPRESSION: + return &EditStyle::PageExpression; case ElementType::REHEARSAL_MARK: return &EditStyle::PageRehearsalMarks; case ElementType::FIGURED_BASS: @@ -1775,6 +1783,17 @@ void EditStyle::setValues() } ++idx; } + + QString dynFont(styleValue(StyleId::dynamicsFont).value()); + idx = 0; + for (const auto& i : engravingFonts()->fonts()) { + if (QString::fromStdString(i->name()).toLower() == dynFont.toLower()) { + dynamicsFont->setCurrentIndex(idx); + break; + } + ++idx; + } + musicalTextFont->blockSignals(true); musicalTextFont->clear(); // CAUTION: the second element, the itemdata, is a font family name! @@ -2062,15 +2081,20 @@ void EditStyle::valueChanged(int i) StyleId idx = (StyleId)i; PropertyValue val = getValue(idx); bool setValue = false; - if (idx == StyleId::MusicalSymbolFont && optimizeStyleCheckbox->isChecked()) { - IEngravingFontPtr scoreFont = engravingFonts()->fontByName(val.value().toStdString()); - if (scoreFont) { - for (auto j : scoreFont->engravingDefaults()) { - setStyleValue(j.first, j.second); + if (idx == StyleId::MusicalSymbolFont) { + bool dynamicsOverrideFont = getValue(StyleId::dynamicsOverrideFont).toBool(); + if (!dynamicsOverrideFont) { + setStyleValue(StyleId::dynamicsFont, val); // Match dynamics font + } + if (optimizeStyleCheckbox->isChecked()) { + IEngravingFontPtr scoreFont = engravingFonts()->fontByName(val.value().toStdString()); + if (scoreFont) { + for (auto j : scoreFont->engravingDefaults()) { + setStyleValue(j.first, j.second); + } } + setValue = true; } - - setValue = true; } setStyleValue(idx, val); diff --git a/src/notation/view/widgets/editstyle.ui b/src/notation/view/widgets/editstyle.ui index b86384220fe66..cefda21718a06 100644 --- a/src/notation/view/widgets/editstyle.ui +++ b/src/notation/view/widgets/editstyle.ui @@ -10,59 +10,16 @@ 752 + + + 0 + 0 + + Style - - - - 6 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 30 - 16777215 - - - - - - - true - - - - - - - Reset All Styles to Default - - - - - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - @@ -247,6 +204,11 @@ Dynamics + + + Expression + + Rehearsal Marks @@ -10529,7 +10491,20 @@ By default, they will be placed such as that their right end are at the same lev Dynamics - + + + + % + + + 0 + + + 1000.000000000000000 + + + + Autoplace min. distance: @@ -10539,69 +10514,119 @@ By default, they will be placed such as that their right end are at the same lev - - - - sp + + + + ... - - 1 + + + + + + Position above: - - 0.000000000000000 + + dynamicsPosAbove - - 1000.000000000000000 + + + + + + Override score font - - 0.200000000000000 + + true - - 0.000000000000000 + + + + + Font: + + + + + + + + 0 + 0 + + + + + + + + + + + ... - - + + + + + + + Position below: + + + dynamicsPosBelow + + + + + Reset to default - Reset 'Autoplace min. distance' value + Reset 'Position below' value - - + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Avoid bar lines: + + + + + Reset to default - Reset 'Position above' value + Reset 'Placement' value - - - - - Above - - - - - Below - - - - - + Placement: @@ -10611,91 +10636,146 @@ By default, they will be placed such as that their right end are at the same lev - - + + + + + Reset to default - Reset 'Placement' value + Reset 'Autoplace min. distance' value - - + + - Position above: + Size: - - dynamicsPosAbove + + 0 - - - - Position below: + + + + sp - - dynamicsPosBelow + + 1 + + + 0.000000000000000 + + + 1000.000000000000000 + + + 0.200000000000000 + + + 0.000000000000000 - - + + Reset to default - Reset 'Position below' value + Reset 'Position above' value - - - - Qt::StrongFocus - + + + + + Above + + + + + Below + + - - - - Qt::StrongFocus + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + + Expression + + + + + + Snap to dynamics - - + + Qt::Horizontal - 0 - 0 + 40 + 20 + + + + ... + + + - + Qt::Vertical - 0 - 0 + 20 + 40 @@ -13059,6 +13139,55 @@ By default, they will be placed such as that their right end are at the same lev + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 30 + 16777215 + + + + + + + true + + + + + + + Reset All Styles to Default + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + diff --git a/src/palette/internal/palettecreator.cpp b/src/palette/internal/palettecreator.cpp index eca8df01b857c..89ae3c6b0552f 100644 --- a/src/palette/internal/palettecreator.cpp +++ b/src/palette/internal/palettecreator.cpp @@ -44,6 +44,7 @@ #include "libmscore/clef.h" #include "libmscore/drumset.h" #include "libmscore/dynamic.h" +#include "libmscore/expression.h" #include "libmscore/fermata.h" #include "libmscore/fingering.h" #include "libmscore/fret.h" @@ -105,6 +106,7 @@ MAKE_ELEMENT(Hairpin, score->dummy()->segment()) MAKE_ELEMENT(SystemText, score->dummy()->segment()) MAKE_ELEMENT(TempoText, score->dummy()->segment()) MAKE_ELEMENT(StaffText, score->dummy()->segment()) +MAKE_ELEMENT(Expression, score->dummy()->segment()) MAKE_ELEMENT(PlayTechAnnotation, score->dummy()->segment()) MAKE_ELEMENT(RehearsalMark, score->dummy()->segment()) @@ -1521,11 +1523,10 @@ PalettePtr PaletteCreator::newTextPalette(bool defaultPalette) systemTextLine->setEndHookType(HookType::HOOK_90); sp->appendElement(systemTextLine, QT_TRANSLATE_NOOP("palette", "System text line")); - auto expressionText = makeElement(gpaletteScore); + auto expressionText = makeElement(gpaletteScore); expressionText->setTextStyleType(TextStyleType::EXPRESSION); - expressionText->setXmlText(QT_TRANSLATE_NOOP("palette", "Expression")); + expressionText->setXmlText(QT_TRANSLATE_NOOP("palette", "expression")); expressionText->setPlacement(PlacementV::BELOW); - expressionText->setPropertyFlags(Pid::PLACEMENT, PropertyFlags::UNSTYLED); sp->appendElement(expressionText, QT_TRANSLATE_NOOP("palette", "Expression text"))->setElementTranslated(true); auto is = makeElement(gpaletteScore);