Skip to content

Add Page video export style#33030

Open
mike-spa wants to merge 1 commit intomusescore:4.7from
mike-spa:addPageVideoStyle
Open

Add Page video export style#33030
mike-spa wants to merge 1 commit intomusescore:4.7from
mike-spa:addPageVideoStyle

Conversation

@mike-spa
Copy link
Copy Markdown
Contributor

@mike-spa mike-spa commented Apr 16, 2026

Summary by CodeRabbit

  • New Features

    • Added video export view mode selection with "Page Full" and "Flexible" options in the export dialog.
    • View modes customize how scores are rendered and centered in video frames.
  • UI/UX

    • Increased export dialog height to accommodate new view mode controls.
    • Updated video resolution label formatting.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

The changes introduce a new view mode system for video export, replacing the existing auto-mode and paged-mode enum values with PageFull and Flexible modes. The system updates the export configuration defaults, implements conditional rendering logic in the video writer based on selected mode, and exposes view mode selection in the export dialog UI.

Changes

Cohort / File(s) Summary
ViewMode Enum Redesign
src/importexport/videoexport/videoexporttypes.h
Replaced Auto, PagedFloat, PagedOriginal, PagedFloatHeight, and Pano with PageFull and Flexible mode values.
Configuration & Export Defaults
src/importexport/videoexport/internal/videoexportconfiguration.cpp
Changed default ViewMode from Auto to PageFull.
VideoWriter Rendering Logic
src/importexport/videoexport/internal/videowriter.h, src/importexport/videoexport/internal/videowriter.cpp
Added viewMode and moveToCenter fields to VideoWriter::Config. Implemented conditional rendering: PageFull mode computes page-fitting scale and centers content; Flexible mode suppresses instrument names/vbox. Changed frame background to black and applied translation only to score rendering.
Export Dialog Model API
src/project/qml/MuseScore/Project/internal/Export/exportdialogmodel.h, src/project/qml/MuseScore/Project/internal/Export/exportdialogmodel.cpp
Added public viewMode Q_PROPERTY, availableViewModes() invokable, and viewModeChanged() signal to expose view mode selection to QML layer.
Export Dialog UI
src/project/qml/MuseScore/Project/ExportDialog.qml, src/project/qml/MuseScore/Project/internal/Export/Mp4SettingsPage.qml
Increased dialog height from 372 to 420 pixels. Added radio button group for view mode selection in MP4 settings, displaying PageFull and Flexible options with two-way binding to model.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as MP4SettingsPage
    participant Model as ExportDialogModel
    participant Config as VideoExportConfiguration
    participant Writer as VideoWriter
    participant Renderer as Rendering Engine

    User->>UI: Select ViewMode (PageFull/Flexible)
    UI->>Model: setViewMode(mode)
    Model->>Config: setViewMode(mode)
    Note over Config: Stores selected mode
    
    User->>UI: Click Export
    UI->>Model: Trigger export
    Model->>Writer: Create VideoWriter with config
    Writer->>Config: viewMode()
    Config-->>Writer: Return selected mode
    
    alt PageFull Mode
        Writer->>Writer: Calculate page-fitting scale
        Writer->>Writer: Set moveToCenter for alignment
        Writer->>Renderer: Render score at calculated scale
        Renderer-->>Writer: Rendered frame
    else Flexible Mode
        Writer->>Writer: Suppress instrument names/vbox
        Writer->>Renderer: Render score with defaults
        Renderer-->>Writer: Rendered frame
    end
    
    Writer-->>User: Export video
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is completely empty, lacking any explanation of changes, motivation, or issue resolution, and no checklist items are completed. Add a description explaining the changes, their motivation, the resolved issue number, and complete the applicable checklist items to meet repository standards.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'Add Page video export style' clearly and concisely describes the main change: introducing a new page-based video export style option to the application.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

width: videoSettingsComp.width
spacing: 4

model: [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's move it to c++ model, see available... methods in ExportDialogModel for example

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d9e5afd0-69fa-45b7-8fbc-9ae2b4a205c7

📥 Commits

Reviewing files that changed from the base of the PR and between 37dca8f and 59a8c25.

📒 Files selected for processing (9)
  • src/importexport/videoexport/internal/videoexportconfiguration.cpp
  • src/importexport/videoexport/internal/videoexportconfiguration.h
  • src/importexport/videoexport/internal/videowriter.cpp
  • src/importexport/videoexport/internal/videowriter.h
  • src/importexport/videoexport/ivideoexportconfiguration.h
  • src/project/qml/MuseScore/Project/ExportDialog.qml
  • src/project/qml/MuseScore/Project/internal/Export/Mp4SettingsPage.qml
  • src/project/qml/MuseScore/Project/internal/Export/exportdialogmodel.cpp
  • src/project/qml/MuseScore/Project/internal/Export/exportdialogmodel.h

Comment on lines +110 to +119

int VideoExportConfiguration::videoStyle() const
{
return m_videoStyle ? m_videoStyle.value() : 0;
}

void VideoExportConfiguration::setVideoStyle(int v)
{
m_videoStyle = v;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider using a named constant for the default value.

Other configuration values in this file use named constants (e.g., DEFAULT_VIEW_MODE, DEFAULT_FPS). The hard-coded 0 on line 113 would be clearer with a constant.

♻️ Proposed change for consistency

Add at the top of the file with other defaults:

static const int DEFAULT_VIDEO_STYLE = 0; // VideoStyle::PAGE

Then update the getter:

 int VideoExportConfiguration::videoStyle() const
 {
-    return m_videoStyle ? m_videoStyle.value() : 0;
+    return m_videoStyle ? m_videoStyle.value() : DEFAULT_VIDEO_STYLE;
 }

cfg.leadingSec = configuration()->leadingSec();
cfg.trailingSec = configuration()->trailingSec();

cfg.style = static_cast<VideoStyle>(configuration()->videoStyle());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clamp videoStyle before casting.

videoStyle() is an int, so a stale/corrupted setting can produce an enum value that matches neither PAGE nor FLEXIBLE. That drops export into an unintended mixed path below. Default invalid values here instead of static_casting blindly.

🛡️ Proposed fix
-    cfg.style = static_cast<VideoStyle>(configuration()->videoStyle());
+    const int rawStyle = configuration()->videoStyle();
+    cfg.style = rawStyle == static_cast<int>(VideoStyle::FLEXIBLE)
+                ? VideoStyle::FLEXIBLE
+                : VideoStyle::PAGE;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
cfg.style = static_cast<VideoStyle>(configuration()->videoStyle());
const int rawStyle = configuration()->videoStyle();
cfg.style = rawStyle == static_cast<int>(VideoStyle::FLEXIBLE)
? VideoStyle::FLEXIBLE
: VideoStyle::PAGE;

Comment thread src/importexport/videoexport/internal/videowriter.cpp Outdated
Comment on lines +61 to +63

virtual int videoStyle() const = 0;
virtual void setVideoStyle(int v) = 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Inconsistent setter signature pattern.

Other setters in this interface use std::optional<T> (e.g., setViewMode(std::optional<ViewMode>), setFps(std::optional<int>)), but setVideoStyle takes a plain int. This breaks the established pattern and prevents resetting to default.

Consider aligning with the existing convention:

♻️ Proposed change for consistency
-    virtual void setVideoStyle(int v) = 0;
+    virtual void setVideoStyle(std::optional<int> v) = 0;

This would also require updating the implementation in VideoExportConfiguration.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
virtual int videoStyle() const = 0;
virtual void setVideoStyle(int v) = 0;
virtual int videoStyle() const = 0;
virtual void setVideoStyle(std::optional<int> v) = 0;

Comment on lines +138 to +160
ExportOptionItem {
id: videoStyleLabel
text: qsTrc("project/export", "Video style")

RadioButtonGroup {
width: videoSettingsComp.width
spacing: 4

model: [
{ text: qsTrc("project/export", "Page"), value: 0 },
{ text: qsTrc("project/export", "Flexible"), value: 1 }
]

delegate: FlatRadioButton {
width: 126
height: 30

text: modelData.text
checked: modelData.value === root.model.videoStyle
onToggled: root.model.videoStyle = modelData.value
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing null guard on root.model access.

The radio button bindings access root.model.videoStyle without null checks (lines 156-157), unlike other controls in this file that guard with root.model ? (e.g., lines 126, 130). This could cause errors if the model is not yet initialized.

🛡️ Proposed fix to add null guard
                 delegate: FlatRadioButton {
                     width: 126
                     height: 30

                     text: modelData.text
-                    checked: modelData.value === root.model.videoStyle
-                    onToggled: root.model.videoStyle = modelData.value
+                    checked: root.model ? modelData.value === root.model.videoStyle : false
+                    onToggled: {
+                        if (root.model) {
+                            root.model.videoStyle = modelData.value
+                        }
+                    }
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ExportOptionItem {
id: videoStyleLabel
text: qsTrc("project/export", "Video style")
RadioButtonGroup {
width: videoSettingsComp.width
spacing: 4
model: [
{ text: qsTrc("project/export", "Page"), value: 0 },
{ text: qsTrc("project/export", "Flexible"), value: 1 }
]
delegate: FlatRadioButton {
width: 126
height: 30
text: modelData.text
checked: modelData.value === root.model.videoStyle
onToggled: root.model.videoStyle = modelData.value
}
}
}
ExportOptionItem {
id: videoStyleLabel
text: qsTrc("project/export", "Video style")
RadioButtonGroup {
width: videoSettingsComp.width
spacing: 4
model: [
{ text: qsTrc("project/export", "Page"), value: 0 },
{ text: qsTrc("project/export", "Flexible"), value: 1 }
]
delegate: FlatRadioButton {
width: 126
height: 30
text: modelData.text
checked: root.model ? modelData.value === root.model.videoStyle : false
onToggled: {
if (root.model) {
root.model.videoStyle = modelData.value
}
}
}
}
}

@mike-spa mike-spa force-pushed the addPageVideoStyle branch from 59a8c25 to 41de519 Compare April 16, 2026 16:56
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/project/qml/MuseScore/Project/internal/Export/Mp4SettingsPage.qml (1)

147-153: ⚠️ Potential issue | 🟡 Minor

Missing null guard on root.model access.

The delegate bindings access root.model.viewMode without null checks (lines 151-152), unlike other controls in this file that guard with root.model ? (e.g., lines 126, 130, 143). This could cause errors if the model is not yet initialized.

🛡️ Proposed fix to add null guard
                 delegate: RoundedRadioButton {
                     required property var modelData

                     text: modelData.text
-                    checked: root.model.viewMode === modelData.value
-                    onToggled: root.model.viewMode = modelData.value
+                    checked: root.model ? root.model.viewMode === modelData.value : false
+                    onToggled: {
+                        if (root.model) {
+                            root.model.viewMode = modelData.value
+                        }
+                    }
                 }

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: ca1438b8-6342-457b-8014-6e1322fd569a

📥 Commits

Reviewing files that changed from the base of the PR and between 59a8c25 and 41de519.

📒 Files selected for processing (8)
  • src/importexport/videoexport/internal/videoexportconfiguration.cpp
  • src/importexport/videoexport/internal/videowriter.cpp
  • src/importexport/videoexport/internal/videowriter.h
  • src/importexport/videoexport/videoexporttypes.h
  • src/project/qml/MuseScore/Project/ExportDialog.qml
  • src/project/qml/MuseScore/Project/internal/Export/Mp4SettingsPage.qml
  • src/project/qml/MuseScore/Project/internal/Export/exportdialogmodel.cpp
  • src/project/qml/MuseScore/Project/internal/Export/exportdialogmodel.h

Comment on lines +323 to +330
if (config.viewMode == ViewMode::PageFull) {
double scaleX = config.width / page->width();
double scaleY = config.height / page->height();
double scale = std::min(scaleX, scaleY);
config.canvasDpi = scale * engraving::DPI;
config.moveToCenter = muse::PointF(0.5 * (config.width / scale - page->width()), 0.5 * (config.height / scale - page->height()));
return result;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider guarding against zero page dimensions.

The scaling calculation divides by page->width() and page->height(). While pages should always have positive dimensions after doLayout(), a defensive check would prevent potential division by zero if layout fails silently.

🛡️ Proposed defensive check
     if (config.viewMode == ViewMode::PageFull) {
+        if (page->width() <= 0 || page->height() <= 0) {
+            LOGE() << "Invalid page dimensions";
+            restoreScore(notation, result);
+            return std::nullopt;
+        }
         double scaleX = config.width / page->width();
         double scaleY = config.height / page->height();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants