Open Recent, print sizes through an invalid resolution, Color Grading under Color (12.14b)

This commit is contained in:
雷电芽衣 committed 2026-09-30 01:43:23 -04:00
1 parent 94b884a2ef
commit 2183348aa6
21 files changed
+717 -38

No files matched your search

+16
View File
@@ -1,10 +1,12 @@
#include "IO/ProjectController.h"
#include "IO/RecentProjects.h"
#include "Document/ProjectWorkspace.h"
#include "IO/ProjectStore.h"
#include "Logging.h"
#include <QFileDialog>
#include <QFileInfo>
#include <QFutureWatcher>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QTimer>
@@ -187,6 +189,7 @@ void ProjectController::write(const Prepared &prepared, std::function<void(bool)
session.history.markSaved(revision);
session.setProjectPath(destination);
m_saveGeneration += 1;
RecentProjects::shared().note(destination);
rememberProjectDigest(destination, [this, destination, landed] {
watchProject(destination);
externalChanges.saving = false;
@@ -241,6 +244,12 @@ void ProjectController::open(std::optional<QString> path, std::function<void(boo
// A folder's trailing slash would leave it without a name.
while (source.endsWith(QLatin1Char('/')))
source.chop(1);
// A listed project deleted since: name it, not its manifest.
if (!QFileInfo::exists(source)) {
RecentProjects::shared().refresh();
refuse(QStringLiteral("The file “%1” couldn’t be opened because there is no such file.").arg(QFileInfo(source).fileName()));
return;
}
// A write in flight lands first: no read mid-save.
finishWriting([this, end, refuse, source] {
// Validated first: a corrupt project never discards the live document.
@@ -258,6 +267,7 @@ void ProjectController::open(std::optional<QString> path, std::function<void(boo
// Saving in the confirmation can replace the opened file.
if (m_saveGeneration == previousSave || !samePlace(session.projectPath(), source)) {
session.installProject(*first.snapshot, source);
RecentProjects::shared().note(source);
rememberProjectDigest(source, [this, source, end] {
watchProject(source);
end(true);
@@ -270,6 +280,7 @@ void ProjectController::open(std::optional<QString> path, std::function<void(boo
return;
}
session.installProject(*again.snapshot, source);
RecentProjects::shared().note(source);
rememberProjectDigest(source, [this, source, end] {
watchProject(source);
end(true);
@@ -380,6 +391,11 @@ void ProjectController::showError(const QString &title, const QString &message,
alert->setIcon(QMessageBox::Warning);
alert->setText(title);
alert->setInformativeText(message);
// A file's name is read literally, never as markup.
auto *words = alert->findChild<QLabel *>(QStringLiteral("qt_msgbox_informativelabel"));
if (!words)
throw std::logic_error("the alert has no informative label");
words->setTextFormat(Qt::PlainText);
alert->addButton(QStringLiteral("OK"), QMessageBox::AcceptRole);
connect(alert, &QDialog::finished, this, std::move(then));
alert->open();
+66
View File
@@ -0,0 +1,66 @@
#include "IO/RecentProjects.h"
#include <QFileInfo>
#include <QGuiApplication>
#include <QSettings>
namespace {
const QString key = QStringLiteral("recentProjects");
// One place however it was reached, as Finder keeps it.
QString resolved(const QString &path)
{
const QString canonical = QFileInfo(path).canonicalFilePath();
return canonical.isEmpty() ? QFileInfo(path).absoluteFilePath() : canonical;
}
}
RecentProjects &RecentProjects::shared()
{
static RecentProjects recent;
return recent;
}
RecentProjects::RecentProjects()
{
refresh();
// Checked again on activation; a GUI-less run has none.
if (auto *app = qobject_cast<QGuiApplication *>(QCoreApplication::instance())) {
connect(app, &QGuiApplication::applicationStateChanged, this, [this](Qt::ApplicationState state) {
if (state == Qt::ApplicationActive)
refresh();
});
}
}
void RecentProjects::note(const QString &path)
{
const QString place = resolved(path);
QStringList stored = QSettings().value(key).toStringList();
stored.removeAll(place);
stored.prepend(place);
store(stored.mid(0, limit));
}
void RecentProjects::clear()
{
store({});
}
void RecentProjects::refresh()
{
QStringList existing;
for (const QString &path : QSettings().value(key).toStringList()) {
if (QFileInfo::exists(path))
existing << path;
}
if (existing == m_paths)
return;
m_paths = existing;
emit changed();
}
void RecentProjects::store(const QStringList &paths)
{
QSettings().setValue(key, paths);
refresh();
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <QObject>
#include <QStringList>
// Swift's RecentProjects: newest first, kept in QSettings.
class RecentProjects : public QObject {
Q_OBJECT
public:
static RecentProjects &shared();
// At most this many, as macOS keeps by default.
static constexpr int limit = 10;
// The projects that still exist, newest first.
const QStringList &paths() const { return m_paths; }
void note(const QString &path);
void clear();
// Drops the projects since moved or deleted.
void refresh();
signals:
void changed();
private:
RecentProjects();
void store(const QStringList &paths);
QStringList m_paths;
};
+12 -12
View File
@@ -21,9 +21,9 @@
namespace {
QString title(int section)
{
static const std::array<QString, 10> titles{QStringLiteral("Light"), QStringLiteral("Color"), QStringLiteral("Effects"),
QStringLiteral("Curve"), QStringLiteral("Color Mixer"), QStringLiteral("Color Grading"),
QStringLiteral("Detail"), QStringLiteral("Optics"), QStringLiteral("Geometry"),
static const std::array<QString, 10> titles{QStringLiteral("Light"), QStringLiteral("Color"), QStringLiteral("Color Grading"),
QStringLiteral("Effects"), QStringLiteral("Curve"), QStringLiteral("Color Mixer"),
QStringLiteral("Detail"), QStringLiteral("Optics"), QStringLiteral("Geometry"),
QStringLiteral("Calibration")};
return titles.at(size_t(section));
}
@@ -31,8 +31,8 @@ QString title(int section)
// The group's eye in CameraRawGroups, by section.
bool &shows(CameraRawGroups &groups, int section)
{
const std::array fields{&CameraRawGroups::light, &CameraRawGroups::color, &CameraRawGroups::effects, &CameraRawGroups::curve,
&CameraRawGroups::mixer, &CameraRawGroups::grading, &CameraRawGroups::detail, &CameraRawGroups::optics,
const std::array fields{&CameraRawGroups::light, &CameraRawGroups::color, &CameraRawGroups::grading, &CameraRawGroups::effects,
&CameraRawGroups::curve, &CameraRawGroups::mixer, &CameraRawGroups::detail, &CameraRawGroups::optics,
&CameraRawGroups::geometry, &CameraRawGroups::calibration};
return groups.*fields.at(size_t(section));
}
@@ -42,10 +42,10 @@ bool adjusts(const CameraRawSettings &raw, int section)
switch (section) {
case 0: return raw.adjustsLight();
case 1: return raw.adjustsColor();
case 2: return raw.adjustsEffects();
case 3: return raw.curve.adjusts();
case 4: return raw.mixer.adjusts();
case 5: return raw.grading.adjusts();
case 2: return raw.grading.adjusts();
case 3: return raw.adjustsEffects();
case 4: return raw.curve.adjusts();
case 5: return raw.mixer.adjusts();
case 6: return raw.detail.adjusts();
case 7: return raw.optics.adjusts();
case 8: return raw.geometry.adjusts();
@@ -194,10 +194,10 @@ CameraRawControls::CameraRawControls(EditorSession &session, QWidget *parent)
groups->setSpacing(12);
light(group(Section::light, groups));
color(group(Section::color, groups));
group(Section::colorGrading, groups)->addWidget(new CameraRawGradingControls(session));
effects(group(Section::effects, groups));
group(Section::curve, groups)->addWidget(new CameraRawCurveControls(session));
group(Section::colorMixer, groups)->addWidget(new CameraRawMixerControls(session));
group(Section::colorGrading, groups)->addWidget(new CameraRawGradingControls(session));
group(Section::detail, groups)->addWidget(new CameraRawDetailControls(session));
group(Section::optics, groups)->addWidget(new CameraRawOpticsControls(session));
group(Section::geometry, groups)->addWidget(new CameraRawGeometryControls(session));
@@ -246,8 +246,8 @@ QVBoxLayout *CameraRawControls::group(Section section, QVBoxLayout *column)
auto *rows = new QVBoxLayout(body);
rows->setContentsMargins(18, 0, 0, 0);
rows->setSpacing(8);
// Light and Color open first, Swift's `expanded`.
body->setVisible(section == Section::light || section == Section::color);
// Light, Color and Color Grading open first, Swift's `expanded`.
body->setVisible(section == Section::light || section == Section::color || section == Section::colorGrading);
connect(disclosure, &QToolButton::clicked, this, [this, body] {
body->setVisible(body->isHidden());
synchronize();
+1 -1
View File
@@ -23,7 +23,7 @@ protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
enum class Section { light, color, effects, curve, colorMixer, colorGrading, detail, optics, geometry, calibration };
enum class Section { light, color, colorGrading, effects, curve, colorMixer, detail, optics, geometry, calibration };
struct Group {
Section section;
QToolButton *disclosure;
+19
View File
@@ -1,4 +1,5 @@
#include "UI/CompositorMenus.h"
#include "IO/RecentProjects.h"
#include "Logging.h"
#include "UI/KeyboardShortcuts.h"
#include "UI/NativeLayerList.h"
@@ -37,6 +38,10 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
QMenu *file = bar.addMenu(QStringLiteral("&File"));
add(file, QStringLiteral("newCanvas"), QStringLiteral("New Canvas…"), QKeySequence(Qt::CTRL | Qt::Key_N), [this] { projects().newCanvas(); });
add(file, QStringLiteral("openProject"), QStringLiteral("Open Project…"), QKeySequence(Qt::CTRL | Qt::Key_O), [this] { projects().open(); });
QMenu *recent = file->addMenu(QStringLiteral("Open Recent"));
recent->menuAction()->setObjectName(QStringLiteral("openRecent"));
listRecent(recent);
connect(&RecentProjects::shared(), &RecentProjects::changed, this, [this, recent] { listRecent(recent); });
add(file, QStringLiteral("importImages"), QStringLiteral("Import Images…"), QKeySequence(), [this] { session().setShowsImporter(true); });
file->addSeparator();
add(file, QStringLiteral("save"), QStringLiteral("Save"), QKeySequence(Qt::CTRL | Qt::Key_S), [this] { projects().save(); });
@@ -272,6 +277,19 @@ CompositorMenus::CompositorMenus(ProjectWorkspace &workspace, QMenuBar &bar, QWi
watchFront();
}
void CompositorMenus::listRecent(QMenu *recent)
{
recent->clear();
for (const QString &path : RecentProjects::shared().paths())
// A name's ampersand is literal, no mnemonic.
connect(recent->addAction(ProjectTab::nameWithoutSuffix(path).replace(QLatin1Char('&'), QStringLiteral("&&"))), &QAction::triggered, this, [this, path] { projects().open(path); });
recent->addSeparator();
QAction *clear = recent->addAction(QStringLiteral("Clear Menu"));
clear->setObjectName(QStringLiteral("clearRecent"));
clear->setEnabled(!RecentProjects::shared().paths().isEmpty());
connect(clear, &QAction::triggered, this, [] { RecentProjects::shared().clear(); });
}
QAction *CompositorMenus::add(QMenu *menu, const QString &name, const QString &text, const QKeySequence &shortcut, const std::function<void()> &run)
{
QAction *made = menu->addAction(text);
@@ -327,6 +345,7 @@ void CompositorMenus::synchronize()
const bool typing = m_field != nullptr || s.textDraft().has_value();
action(QStringLiteral("newCanvas"))->setEnabled(p.canStart());
action(QStringLiteral("openProject"))->setEnabled(p.canStart());
action(QStringLiteral("openRecent"))->setEnabled(p.canStart());
action(QStringLiteral("importImages"))->setEnabled(!s.levels() && !s.showsBusy() && !s.isImporting() && !s.showsNewDocument());
action(QStringLiteral("save"))->setEnabled(drawn && p.canStart());
action(QStringLiteral("saveAs"))->setEnabled(drawn && p.canStart());
+1
View File
@@ -23,6 +23,7 @@ private:
void watchFront();
EditorSession &session() const { return m_workspace.current().session; }
ProjectController &projects() const { return m_workspace.current().controller; }
void listRecent(QMenu *recent);
QAction *add(QMenu *menu, const QString &name, const QString &text, const QKeySequence &shortcut, const std::function<void()> &run);
// Each entry's own key, remapped from ShortcutSettings.
+12 -7
View File
@@ -49,7 +49,7 @@ bool physical(const QString &unit)
ImageSizeSheet::ImageSizeSheet(const CanvasDocument &document, std::function<void(std::optional<ImageSizeOptions>)> finish, QWidget *parent)
: QWidget(parent), m_originalWidth(document.width), m_originalHeight(document.height), m_finish(std::move(finish)),
m_width(double(document.width)), m_height(double(document.height)), m_resolution(document.resolution), m_units(new QComboBox(this)),
m_width(double(document.width)), m_height(double(document.height)), m_resolution(document.resolution), m_lastResolution(document.resolution), m_units(new QComboBox(this)),
m_widthField(dimension(true)), m_heightField(dimension(false)), m_lock(new QCheckBox(QStringLiteral("Lock aspect ratio"), this)),
m_resolutionField(new PickerField([this] {
bool number = false;
@@ -204,10 +204,11 @@ PickerField *ImageSizeSheet::dimension(bool isWidth)
// Swift's binding: pixels, or without resampling the resolution.
void ImageSizeSheet::setDimension(double value, bool isWidth)
{
if (!(std::isfinite(value) && value > 0))
// A print size means nothing without a usable resolution.
if (!(std::isfinite(value) && value > 0) || !canScrubDimensions())
return;
if (!m_resample) {
m_resolution = (isWidth ? m_width : m_height) / value * (m_unit == QLatin1String("Centimeters") ? 2.54 : 1);
setResolution((isWidth ? m_width : m_height) / value * (m_unit == QLatin1String("Centimeters") ? 2.54 : 1));
return;
}
double pixels = value;
@@ -230,11 +231,15 @@ void ImageSizeSheet::setDimension(double value, bool isWidth)
void ImageSizeSheet::setResolution(double value)
{
const double old = std::exchange(m_resolution, value);
if (m_resample && physical(m_unit) && old > 0 && value > 0 && std::isfinite(value)) {
m_width *= value / old;
m_height *= value / old;
m_resolution = value;
if (!(std::isfinite(value) && value > 0))
return;
// Scaled from the last usable one, past any invalid entry.
if (m_resample && physical(m_unit)) {
m_width *= value / m_lastResolution;
m_height *= value / m_lastResolution;
}
m_lastResolution = value;
}
NumericScrub *ImageSizeSheet::scrub(QLabel *title, bool isWidth)
+1
View File
@@ -39,6 +39,7 @@ private:
double m_width;
double m_height;
double m_resolution;
double m_lastResolution;
bool m_locked = true;
bool m_resample = true;
QString m_unit = QStringLiteral("Pixels");