Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/reusable-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ jobs:
working-directory: loop
run: python3 scripts/ci/check_version_policy.py
- name: Verify processing-budget exhaustion corpus
working-directory: loupe
working-directory: loop
run: python3 scripts/budget_exhaustion/generate_corpus.py --check

- name: Prepare vcpkg directories
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/reusable-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ jobs:
--page-count 256 --operations 256 --family pathological-vector

- name: Verify processing-budget exhaustion corpus
working-directory: loupe
working-directory: loop
shell: pwsh
run: python scripts\budget_exhaustion\generate_corpus.py --check

Expand Down
4 changes: 4 additions & 0 deletions LoopLibQuick/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ qt_add_library(LoopLibQuick SHARED
sources/loopcanvasitemscene.cpp
sources/loopcanvasaccessible.cpp
sources/loopcanvasaccessible.h
sources/looptokens.cpp
sources/looptokens.h
sources/loopstatevisual.cpp
sources/loopstatevisual.h
)

# No QML_FILES. LoopCanvasItem is registered from C++ with QML_NAMED_ELEMENT,
Expand Down
114 changes: 114 additions & 0 deletions LoopLibQuick/sources/loopstatevisual.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// MIT License
//
// Copyright (c) 2018-2025 Jakub Melka and Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.


#include "loopstatevisual.h"

#include "preflightengine.h"

namespace pdfquick::tokens
{

namespace
{

LoopStateVisual fromFinding(const pdf::PreflightFinding& finding)
{
const QString severity = finding.severity.trimmed();

if (severity.compare(QLatin1String("error"), Qt::CaseInsensitive) == 0)
{
return { StateKind::Error, ColorRole::SeverityError, StateIcon::FilledCircle };
}
if (severity.compare(QLatin1String("warning"), Qt::CaseInsensitive) == 0)
{
return { StateKind::Warning, ColorRole::SeverityWarning, StateIcon::FilledTriangle };
}
if (severity.compare(QLatin1String("info"), Qt::CaseInsensitive) == 0)
{
return { StateKind::Info, ColorRole::SeverityInfo, StateIcon::FilledSquare };
}

// profile.schema.json admits only error/warning/info. A finding with
// anything else is data this build does not understand -- the safe
// reading is "cannot vouch for this", not "no problem here", so it takes
// the same never-green treatment as an incomplete check rather than
// silently falling through to Passed.
return { StateKind::Incomplete, ColorRole::StateIncomplete, StateIcon::Hatched };
}

LoopStateVisual fromStatus(const pdf::PreflightCheckStatus& status)
{
if (status.status.compare(QLatin1String("ok"), Qt::CaseInsensitive) == 0)
{
return { StateKind::Passed, ColorRole::Success, StateIcon::Checkmark };
}

// Every other status literal this build emits -- failed, warning, skipped,
// incomplete, unsupported -- and any literal a future check adds all take
// this branch. That is deliberately coarser than the run-level verdict in
// pdf::reducePreflightVerdict(): a caller presenting one check's
// completion, without a specific finding to show, only ever needs to know
// "clean pass" from "not that", and the second must never render as the
// first.
return { StateKind::Incomplete, ColorRole::StateIncomplete, StateIcon::Hatched };
}

} // namespace

LoopStateVisual resolveStateVisual(const pdf::PreflightFinding* finding,
const pdf::PreflightCheckStatus* status,
const pdf::PreflightDecision* decision,
const QString& currentDocumentDigest,
const QString& currentProfileDigest)
{
// Checked first and unconditionally: a waived finding is presented as
// waived regardless of its severity or the check's completion status.
// resolveState() -- not the stored kind alone -- decides "active", so a
// decision recorded against a document revision or profile that no longer
// matches falls through instead of masking the finding (mirrors
// PreflightDecision::countsForSignoff(), issue #126).
if (decision != nullptr && decision->kind == pdf::PreflightDecisionKind::Waive)
{
const pdf::PreflightDecisionState state = decision->resolveState(currentDocumentDigest, currentProfileDigest);
if (state == pdf::PreflightDecisionState::Active)
{
return { StateKind::Waived, ColorRole::SeverityWarning, StateIcon::BadgeOverlay };
}
}

if (finding != nullptr)
{
return fromFinding(*finding);
}

if (status != nullptr)
{
return fromStatus(*status);
}

// No finding, no check status, no active waiver: nothing has run for this
// revision yet.
return { StateKind::NotChecked, ColorRole::StateNotChecked, StateIcon::Outline };
}

} // namespace pdfquick::tokens
115 changes: 115 additions & 0 deletions LoopLibQuick/sources/loopstatevisual.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// MIT License
//
// Copyright (c) 2018-2025 Jakub Melka and Contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.


#ifndef LOOPSTATEVISUAL_H
#define LOOPSTATEVISUAL_H

#include "loopquickglobal.h"
#include "looptokens.h"

#include <QString>

namespace pdf
{
struct PreflightFinding;
struct PreflightCheckStatus;
struct PreflightDecision;
} // namespace pdf

namespace pdfquick::tokens
{

/// The finding/check state a surface is presenting. Kept separate from
/// `ColorRole` (below) even though today it maps one-to-one, because a state
/// is a fact about a finding and a colour role is a fact about a pixel; a
/// future high-contrast or print treatment that wants to give two states the
/// same colour role must still tell them apart by `kind`.
enum class StateKind
{
Error,
Warning,
Info,
Incomplete,
NotChecked,
Passed,
Waived
};

/// Shape carries the state distinction alongside colour, so the mapping
/// survives colour-blindness and greyscale printing (docs/ACCESSIBILITY_BASELINE.md,
/// issue #25). `BadgeOverlay` is drawn in addition to the underlying severity
/// treatment, not instead of it -- a waived error still shows as an error with
/// a badge, it never becomes indistinguishable from a plain warning.
enum class StateIcon
{
FilledCircle, // Error
FilledTriangle, // Warning
FilledSquare, // Info
Hatched, // Incomplete
Outline, // Not checked
Checkmark, // Passed
BadgeOverlay // Waived
};

struct LoopStateVisual
{
StateKind kind = StateKind::NotChecked;
ColorRole colorRole = ColorRole::StateNotChecked;
StateIcon icon = StateIcon::Outline;
};

/// Single source of truth for finding/check presentation (issue #194). Every
/// surface that draws a finding, a check row, or a run summary -- finding
/// cards, the report dock, canvas overlays, the Inspector (#127), the status
/// bar -- calls this; none derives its own colour or icon from `severity`,
/// `status`, or a decision's kind directly.
///
/// `finding` is the specific finding being presented, or null when the caller
/// is presenting a check's overall status rather than one of its findings (for
/// example, a check row with zero findings). `status` is the
/// PreflightCheckStatus for the check `finding` belongs to (or the check being
/// summarised), or null when no run exists yet for the current document
/// revision. `decision` is the operator decision recorded against
/// `finding->stableId()`, or null when none was recorded; `currentDocumentDigest`
/// and `currentProfileDigest` are passed through to
/// `PreflightDecision::resolveState()` so a decision made against a stale
/// document or profile is never read as active (mirrors
/// `PreflightDecision::countsForSignoff()`, issue #126).
///
/// Two invariants hold for every input combination and are asserted by
/// tst_loopstatevisualtest.cpp:
///
/// - `StateKind::Incomplete` never resolves to the same colour role or icon
/// as `StateKind::Passed`. An incomplete check must never render as a
/// clean pass (issue #133).
/// - An active Waive decision never resolves to `StateKind::Passed`. Waived
/// always renders as `StateKind::Waived`, distinct from Passed.
LOOPLIBQUICK_EXPORT LoopStateVisual resolveStateVisual(const pdf::PreflightFinding* finding,
const pdf::PreflightCheckStatus* status,
const pdf::PreflightDecision* decision,
const QString& currentDocumentDigest = QString(),
const QString& currentProfileDigest = QString());

} // namespace pdfquick::tokens

#endif // LOOPSTATEVISUAL_H
Loading
Loading