Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
8990603
Add drag-handle overlay to edit per-note playback start/duration offsets
tharos-devs Aug 13, 2026
b84570b
Rebuild note-offset overlay geometry on style changes
tharos-devs Aug 13, 2026
d14be1b
Merge branch 'feature/note-offsets' into feature/note-offset-drag-han…
tharos-devs Aug 14, 2026
b74429c
Fix crash and lifecycle bugs in the note-offset drag-handle overlay
tharos-devs Aug 14, 2026
5fc3d1a
Merge branch 'feature/note-offsets' into feature/note-offset-drag-han…
tharos-devs Aug 14, 2026
52bd183
Fix codestyle (uncrustify) violation in NotationNoteOffsetController
tharos-devs Aug 14, 2026
fdd2101
Merge branch 'feature/note-offsets' into feature/note-offset-drag-han…
tharos-devs Aug 14, 2026
b2c9257
Merge branch 'feature/note-offsets' into feature/note-offset-drag-han…
tharos-devs Aug 14, 2026
7e4762d
Merge branch 'feature/note-offsets' into feature/note-offset-drag-han…
tharos-devs Aug 14, 2026
1100bcf
Merge branch 'feature/note-offsets' into feature/note-offset-drag-han…
tharos-devs Aug 14, 2026
142dde7
Fix note-offset rectangle width when another voice has a shorter note
tharos-devs Aug 14, 2026
8816db7
Color-code note-offset rectangles by modification/selection state
tharos-devs Aug 15, 2026
76ac7a2
Add "Reset note offsets" context-menu item
tharos-devs Aug 15, 2026
4dc153a
Tint note-offset drag handles by the rectangle's own state color
tharos-devs Aug 15, 2026
5efb9d5
Extend note-offset overlay rectangles across full tie chains
tharos-devs Aug 18, 2026
71efc99
Make note-offset Properties panel spinboxes tie-aware
tharos-devs Aug 18, 2026
69b9700
Merge branch 'feature/note-offsets' into feature/note-offset-drag-han…
tharos-devs Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/notation/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ target_sources(notation PRIVATE
inotationselectionfilter.h
inotationselectionrange.h
inotationautomation.h
inotationnoteoffsets.h
inotationinteraction.h
inotationstyle.h
inotationundostack.h
Expand Down Expand Up @@ -83,6 +84,8 @@ target_sources(notation PRIVATE
internal/notationcontextconfiguration.h
internal/notationautomation.cpp
internal/notationautomation.h
internal/notationnoteoffsets.cpp
internal/notationnoteoffsets.h
internal/notationelements.cpp
internal/notationelements.h
internal/notationinteraction.cpp
Expand Down
1 change: 1 addition & 0 deletions src/notation/imasternotation.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class IMasterNotation
virtual void initNotationSoloMuteState(const INotationPtr notation) = 0;

virtual INotationAutomationPtr automation() const = 0;
virtual INotationNoteOffsetsPtr noteOffsets() const = 0;
};

using IMasterNotationPtr = std::shared_ptr<IMasterNotation>;
Expand Down
3 changes: 3 additions & 0 deletions src/notation/inotation_fwd.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,7 @@ using INotationPlaybackPtr = std::shared_ptr<INotationPlayback>;

class INotationAutomation;
using INotationAutomationPtr = std::shared_ptr<INotationAutomation>;

class INotationNoteOffsets;
using INotationNoteOffsetsPtr = std::shared_ptr<INotationNoteOffsets>;
}
39 changes: 39 additions & 0 deletions src/notation/inotationnoteoffsets.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-Studio-CLA-applies
*
* MuseScore Studio
* Music Composition & Notation
*
* Copyright (C) 2026 MuseScore Limited
*
* 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 <https://www.gnu.org/licenses/>.
*/

#pragma once

#include "async/notification.h"

namespace mu::notation {
class INotationNoteOffsets
{
public:
virtual ~INotationNoteOffsets() = default;

virtual bool isEditModeEnabled() const = 0;
virtual void setEditModeEnabled(bool enabled) = 0;
virtual muse::async::Notification editModeEnabledChanged() const = 0;
};

using INotationNoteOffsetsPtr = std::shared_ptr<INotationNoteOffsets>;
}
7 changes: 7 additions & 0 deletions src/notation/internal/masternotation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
#include "excerptnotation.h"
#include "masternotationparts.h"
#include "notationautomation.h"
#include "notationnoteoffsets.h"
#include "types/scorecreateoptions.h"

#ifdef MUE_BUILD_ENGRAVING_PLAYBACK
Expand Down Expand Up @@ -92,6 +93,7 @@ MasterNotation::MasterNotation(project::INotationProject* project, const muse::m
#endif

m_notationAutomation = std::make_shared<NotationAutomation>(undoStack());
m_notationNoteOffsets = std::make_shared<NotationNoteOffsets>();

m_parts->partsChanged().onNotify(this, [this]() {
notifyAboutNotationChanged();
Expand Down Expand Up @@ -766,6 +768,11 @@ INotationAutomationPtr MasterNotation::automation() const
return m_notationAutomation;
}

INotationNoteOffsetsPtr MasterNotation::noteOffsets() const
{
return m_notationNoteOffsets;
}

void MasterNotation::initNotationSoloMuteState(const INotationPtr notation)
{
IF_ASSERT_FAILED(notation) {
Expand Down
2 changes: 2 additions & 0 deletions src/notation/internal/masternotation.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class MasterNotation : public IMasterNotation, public Notation, public std::enab
void initNotationSoloMuteState(const INotationPtr notation) override;

INotationAutomationPtr automation() const override;
INotationNoteOffsetsPtr noteOffsets() const override;

private:
friend class project::NotationProject;
Expand Down Expand Up @@ -102,6 +103,7 @@ class MasterNotation : public IMasterNotation, public Notation, public std::enab
muse::async::Notification m_excerptsChanged;
INotationPlaybackPtr m_notationPlayback = nullptr;
INotationAutomationPtr m_notationAutomation = nullptr;
INotationNoteOffsetsPtr m_notationNoteOffsets = nullptr;
muse::async::Notification m_hasPartsChanged;

mutable ExcerptNotationList m_potentialExcerpts;
Expand Down
44 changes: 44 additions & 0 deletions src/notation/internal/notationnoteoffsets.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-Studio-CLA-applies
*
* MuseScore Studio
* Music Composition & Notation
*
* Copyright (C) 2026 MuseScore Limited
*
* 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 <https://www.gnu.org/licenses/>.
*/

#include "notationnoteoffsets.h"

using namespace mu::notation;

bool NotationNoteOffsets::isEditModeEnabled() const
{
return m_isEditModeEnabled;
}

void NotationNoteOffsets::setEditModeEnabled(bool enabled)
{
if (m_isEditModeEnabled == enabled) {
return;
}
m_isEditModeEnabled = enabled;
m_editModeEnabledChanged.notify();
}

muse::async::Notification NotationNoteOffsets::editModeEnabledChanged() const
{
return m_editModeEnabledChanged;
}
40 changes: 40 additions & 0 deletions src/notation/internal/notationnoteoffsets.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* SPDX-License-Identifier: GPL-3.0-only
* MuseScore-Studio-CLA-applies
*
* MuseScore Studio
* Music Composition & Notation
*
* Copyright (C) 2026 MuseScore Limited
*
* 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 <https://www.gnu.org/licenses/>.
*/
#pragma once

#include "../inotationnoteoffsets.h"

#include "async/notification.h"

namespace mu::notation {
class NotationNoteOffsets : public INotationNoteOffsets
{
public:
bool isEditModeEnabled() const override;
void setEditModeEnabled(bool enabled) override;
muse::async::Notification editModeEnabledChanged() const override;

private:
bool m_isEditModeEnabled = false;
muse::async::Notification m_editModeEnabledChanged;
};
}
3 changes: 3 additions & 0 deletions src/notationscene/inotationcommandscontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ class INotationCommandsController : MODULE_CONTEXT_INTERFACE
virtual bool isAutomationModeEnabled() const = 0;
virtual muse::async::Notification automationModeEnabledChanged() const = 0;

virtual bool isNoteOffsetEditModeEnabled() const = 0;
virtual muse::async::Notification noteOffsetEditModeEnabledChanged() const = 0;

virtual bool isDebuggingCommandEnabled(const muse::rcommand::Command& command) const = 0;
virtual muse::async::Notification debuggingOptionsChanged() const = 0;
};
Expand Down
55 changes: 55 additions & 0 deletions src/notationscene/internal/notationactioncontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#include "engraving/dom/harmony.h"
#include "engraving/dom/masterscore.h"
#include "engraving/dom/note.h"
#include "engraving/dom/property.h"
#include "engraving/dom/chord.h"
#include "engraving/dom/text.h"
#include "engraving/dom/sig.h"
Expand All @@ -39,6 +40,7 @@
#include "notation/imasternotation.h"
#include "notation/inotation.h"
#include "notation/inotationautomation.h" // IWYU pragma: keep
#include "notation/inotationnoteoffsets.h" // IWYU pragma: keep
#include "notation/inotationelements.h"
#include "notation/inotationmidiinput.h"
#include "notation/inotationnoteinput.h"
Expand Down Expand Up @@ -581,6 +583,8 @@ void NotationActionController::init()

registerCommand(TOGGLE_AUTOMATION_COMMAND, &Controller::toggleAutomation);
registerQueryCommand(SELECT_AUTOMATION_TYPE_COMMAND, &Controller::selectAutomationType);
registerCommand(TOGGLE_NOTE_OFFSET_EDITOR_COMMAND, &Controller::toggleNoteOffsetEditor);
registerCommand(RESET_NOTE_OFFSETS_COMMAND, &Controller::resetNoteOffsets);

// TAB
registerCommand(SET_DURATION_WHOLE_TAB_COMMAND, [this]() { setDuration(DurationType::V_WHOLE); });
Expand Down Expand Up @@ -1052,6 +1056,7 @@ void NotationActionController::init()
{ "scoop", ADD_SCOOP_COMMAND, {} },
{ "hammer-on-pull-off", ADD_HAMMER_ON_PULL_OFF_COMMAND, {} },
{ "toggle-automation", TOGGLE_AUTOMATION_COMMAND, {} },
{ "toggle-note-offset-editor", TOGGLE_NOTE_OFFSET_EDITOR_COMMAND, {} },
{ "string-up", GOTO_STRING_ABOVE_COMMAND, {} },
{ "string-down", GOTO_STRING_BELOW_COMMAND, {} },
{ "move-up", MOVE_UP_COMMAND, {} },
Expand Down Expand Up @@ -1129,6 +1134,10 @@ void NotationActionController::init()
masterNotation->automation()->automationModeEnabledChanged().onNotify(this, [this]() {
m_automationModeEnabledChanged.notify();
}, Asyncable::Mode::SetReplace);

masterNotation->noteOffsets()->editModeEnabledChanged().onNotify(this, [this]() {
m_noteOffsetEditModeEnabledChanged.notify();
}, Asyncable::Mode::SetReplace);
}
}

Expand Down Expand Up @@ -3187,6 +3196,16 @@ bool NotationActionController::isAutomationModeEnabled() const
return currentMasterNotation() ? currentMasterNotation()->automation()->isAutomationModeEnabled() : false;
}

bool NotationActionController::isNoteOffsetEditModeEnabled() const
{
return currentMasterNotation() ? currentMasterNotation()->noteOffsets()->isEditModeEnabled() : false;
}

muse::async::Notification NotationActionController::noteOffsetEditModeEnabledChanged() const
{
return m_noteOffsetEditModeEnabledChanged;
}

muse::async::Notification NotationActionController::automationModeEnabledChanged() const
{
return m_automationModeEnabledChanged;
Expand Down Expand Up @@ -3259,6 +3278,42 @@ void NotationActionController::toggleAutomation()
masterNotation->automation()->setAutomationModeEnabled(!isEnabled);
}

void NotationActionController::toggleNoteOffsetEditor()
{
TRACEFUNC;

IMasterNotationPtr masterNotation = currentMasterNotation();
if (!masterNotation) {
return;
}

const bool isEnabled = masterNotation->noteOffsets()->isEditModeEnabled();
masterNotation->noteOffsets()->setEditModeEnabled(!isEnabled);
}

void NotationActionController::resetNoteOffsets()
{
TRACEFUNC;

INotationSelectionPtr selection = currentNotationSelection();
std::vector<Note*> notes = selection ? selection->notes() : std::vector<Note*>();
if (notes.empty()) {
return;
}

INotationUndoStackPtr undoStack = currentNotationUndoStack();
if (!undoStack) {
return;
}

undoStack->prepareChanges(TranslatableString("undoableAction", "Reset note offsets"));
for (Note* note : notes) {
note->undoChangeProperty(Pid::PLAYBACK_START_OFFSET, 0, mu::engraving::PropertyFlags::NOSTYLE);
note->undoChangeProperty(Pid::PLAYBACK_DURATION_OFFSET, 0, mu::engraving::PropertyFlags::NOSTYLE);
}
undoStack->commitChanges();
}

muse::Ret NotationActionController::selectAutomationType(const muse::rcommand::CommandQuery& query)
{
const std::string type = query.param("type").toString();
Expand Down
6 changes: 6 additions & 0 deletions src/notationscene/internal/notationactioncontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ class NotationActionController : public INotationCommandsController, public muse
bool isAutomationModeEnabled() const override;
muse::async::Notification automationModeEnabledChanged() const override;

bool isNoteOffsetEditModeEnabled() const override;
muse::async::Notification noteOffsetEditModeEnabledChanged() const override;

bool isDebuggingCommandEnabled(const muse::rcommand::Command& command) const override;
muse::async::Notification debuggingOptionsChanged() const override;

Expand Down Expand Up @@ -269,6 +272,8 @@ class NotationActionController : public INotationCommandsController, public muse

void toggleAutomation();
muse::Ret selectAutomationType(const muse::rcommand::CommandQuery& query);
void toggleNoteOffsetEditor();
void resetNoteOffsets();

// commands
void registerCommand(const muse::rcommand::Command&, std::function<void()>);
Expand Down Expand Up @@ -311,6 +316,7 @@ class NotationActionController : public INotationCommandsController, public muse
muse::async::Channel<ScoreConfigType> m_scoreConfigChanged;
muse::async::Notification m_currentNotationStyleChanged;
muse::async::Notification m_automationModeEnabledChanged;
muse::async::Notification m_noteOffsetEditModeEnabledChanged;

using IsActionEnabledFunc = std::function<bool ()>;
std::map<muse::actions::ActionCode, IsActionEnabledFunc> m_isEnabledMap;
Expand Down
14 changes: 14 additions & 0 deletions src/notationscene/internal/notationcommandsregister.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2914,6 +2914,20 @@ static const std::vector<CommandInfo> s_commandInfos = {
InputSchema(),
Decoration(IconCode::Code::AUTOMATION, rcommand::Checkable::Yes)
},
CommandInfo {
TOGGLE_NOTE_OFFSET_EDITOR_COMMAND,
TranslatableString("action", "Note offsets"),
TranslatableString("action", "Toggle note offset editor"),
InputSchema(),
Decoration(IconCode::Code::CLOCK, rcommand::Checkable::Yes)
},
CommandInfo {
RESET_NOTE_OFFSETS_COMMAND,
TranslatableString("action", "Reset note offsets"),
TranslatableString("action", "Reset note offsets"),
InputSchema(),
Decoration()
},
CommandInfo {
SELECT_AUTOMATION_TYPE_COMMAND,
TranslatableString::untranslatable("Automation type"),
Expand Down
8 changes: 8 additions & 0 deletions src/notationscene/internal/notationcommandsstate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,10 @@ void NotationCommandsState::init()
updateCommandStates({ TOGGLE_AUTOMATION_COMMAND });
});

controller()->noteOffsetEditModeEnabledChanged().onNotify(this, [this]() {
updateCommandStates({ TOGGLE_NOTE_OFFSET_EDITOR_COMMAND });
});

controller()->debuggingOptionsChanged().onNotify(this, [this]() {
updateCommandStates(DEBUG_COMMANDS);
});
Expand Down Expand Up @@ -489,6 +493,10 @@ CommandState NotationCommandsState::doCommandState(const Command& command) const
return CommandState(true, controller()->isAutomationModeEnabled());
}

if (command == TOGGLE_NOTE_OFFSET_EDITOR_COMMAND) {
return CommandState(true, controller()->isNoteOffsetEditModeEnabled());
}

if (muse::contains(DEBUG_COMMANDS, command)) {
return CommandState(true, controller()->isDebuggingCommandEnabled(command));
}
Expand Down
Loading