diff --git a/sources/titleblock/templateview.cpp b/sources/titleblock/templateview.cpp index b41881d82..a9ccc33b4 100644 --- a/sources/titleblock/templateview.cpp +++ b/sources/titleblock/templateview.cpp @@ -999,34 +999,47 @@ void TitleBlockTemplateView::updateDisplayedMinMaxWidth() int max_width = tbtemplate_ -> maximumWidth(); QString min_max_width_sentence; - if (min_width != -1 && max_width != -1) { - min_max_width_sentence = QString( - tr( - "Longueur minimale : %1px\nLongueur maximale : %2px\n", - "tooltip showing the minimum and/or maximum width of the edited template" - ) - ).arg(min_width).arg(max_width); - } else if (min_width != -1) { - min_max_width_sentence = QString( - tr( - "Longueur minimale : %1px\n", - "tooltip showing the minimum width of the edited template" - ) - ).arg(min_width); - } else if (max_width != -1) { - min_max_width_sentence = QString( - tr( - "Longueur maximale : %1px\n", - "tooltip showing the maximum width of the edited template" - ) - ).arg(max_width); - } else { - min_max_width_sentence = QString( - tr( - "Longueur non contrainte.\n", - "tooltip shown when the edited template has neither a minimum nor a maximum width constraint" - ) - ); + + switch (static_cast(min_width)) { + case TitleBlockTemplate::WidthConstraintCase::RelativeWidthExceeds100Percent: + // minimumWidth() and maximumWidth() both check infeasibility + // first, identically, so max_width reports the same case + // here -- no need to check it separately. + min_max_width_sentence = tr( + "Attention : la somme des largeurs relatives dépasse 100%% de la largeur totale, ce modèle de cartouche ne peut être satisfait par aucune largeur.\n", + "tooltip warning shown when a template's relative-to-total-length columns alone already exceed 100%% of the total width" + ); + break; + case TitleBlockTemplate::WidthConstraintCase::AbsoluteColumnsExceedRemainingWidth: + min_max_width_sentence = tr( + "Attention : les colonnes de largeur fixe ne peuvent pas tenir dans la largeur restante, ce modèle de cartouche ne peut être satisfait par aucune largeur.\n", + "tooltip warning shown when a template's relative-to-total-length columns already consume all available width, leaving no room for its fixed-width columns" + ); + break; + default: + if (min_width != static_cast(TitleBlockTemplate::WidthConstraintCase::Unconstrained)) { + min_max_width_sentence += QString( + tr( + "Longueur minimale : %1px\n", + "tooltip showing the minimum width of the edited template" + ) + ).arg(min_width); + } + if (max_width != static_cast(TitleBlockTemplate::WidthConstraintCase::Unconstrained)) { + min_max_width_sentence += QString( + tr( + "Longueur maximale : %1px\n", + "tooltip showing the maximum width of the edited template" + ) + ).arg(max_width); + } + if (min_max_width_sentence.isEmpty()) { + min_max_width_sentence = tr( + "Longueur non contrainte.\n", + "tooltip shown when the edited template has neither a minimum nor a maximum width constraint" + ); + } + break; } // the tooltip may also display the split label for readability purpose diff --git a/sources/titleblocktemplate.cpp b/sources/titleblocktemplate.cpp index 1e9b45a41..4379605c1 100644 --- a/sources/titleblocktemplate.cpp +++ b/sources/titleblocktemplate.cpp @@ -960,47 +960,130 @@ int TitleBlockTemplate::columnTypeTotal(QET::TitleBlockColumnLength type) { } /** - @return the minimum width for this template + @brief TitleBlockTemplate::classifyWidthConstraint + Classifies this template's absolute-width (ABS) and + relative-to-total-length (RTT) columns, independently of whether a + minimum or a maximum width is being computed. + + The classification is derived from the same inequality + minimumWidth() solves for a minimum width: + @code + TOT >= ((sum(RTT)/100)*TOT) + sum(ABS) + @endcode + Regardless of TOT, the RTT term above scales linearly with TOT. If + sum(RTT) alone already accounts for 100% or more of TOT, no choice + of TOT -- however large -- can make room for it (and for any ABS + columns on top of it): making the template wider grows the RTT + columns' pixel footprint by the same proportion, so the shortfall + never resolves. This function exists so both minimumWidth() and + maximumWidth() report that consistently, instead of maximumWidth() + incorrectly treating a misconfigured template as "no upper bound". + + @param[out] abs_total set to columnTypeTotal(QET::Absolute). + @param[out] remaining_width_fraction set to the fraction of the + total template width left over once the RTT columns have taken + their share: (100.0 - sum(RTT)) / 100.0. Zero means the RTT + columns claim the entire width, leaving nothing for ABS columns; + negative means they claim more than the entire width, which is + unsatisfiable regardless of any ABS columns. Only meaningful when + this function returns std::nullopt. + @return WidthConstraintCase::RelativeWidthExceeds100Percent if + sum(RTT) exceeds 100% (unsatisfiable at any width, with or without + ABS columns), WidthConstraintCase::AbsoluteColumnsExceedRemainingWidth + if sum(RTT) equals exactly 100% and at least one ABS column is also + present (unsatisfiable at any width, since the RTT columns leave no + room for it), WidthConstraintCase::Unconstrained if sum(RTT) equals + exactly 100% with no ABS columns (every term vanishes, any width + holds -- an ordinary, valid template), or std::nullopt if this + template's columns admit a genuine finite width. +*/ +std::optional TitleBlockTemplate::classifyWidthConstraint(int &abs_total, qreal &remaining_width_fraction) +{ + abs_total = columnTypeTotal(QET::Absolute); + remaining_width_fraction = (100.0 - columnTypeTotal(QET::RelativeToTotalLength)) / 100.0; + + if (remaining_width_fraction < 0.0) { + return WidthConstraintCase::RelativeWidthExceeds100Percent; + } + if (remaining_width_fraction == 0.0) { + return abs_total == 0 + ? WidthConstraintCase::Unconstrained + : WidthConstraintCase::AbsoluteColumnsExceedRemainingWidth; + } + return std::nullopt; // a finite width exists: remaining_width_fraction > 0 +} + +/** + @brief TitleBlockTemplate::minimumWidth + @return the minimum width, in pixels, required for this template's + absolute-width (ABS) and relative-to-total-length (RTT) columns to + all fit. + + Derivation: writing TOT for the (variable) total template width, + the minimum size enforced by the ABS and RTT columns is: + @code + TOT >= ((sum(RTT)/100)*TOT) + sum(ABS) + => (1 - (sum(RTT)/100))*TOT >= sum(ABS) + => TOT >= sum(ABS) / (1 - (sum(RTT)/100)) + => TOT >= sum(ABS) / ((100 - sum(RTT))/100) + @endcode + relative-to-remaining-length (RTR) columns do not constrain the + minimum width, since by definition they only ever claim a share of + whatever space is left over after the ABS and RTT columns are laid + out. + + If no finite minimum width applies, returns one of + WidthConstraintCase::Unconstrained, + WidthConstraintCase::RelativeWidthExceeds100Percent, or + WidthConstraintCase::AbsoluteColumnsExceedRemainingWidth (cast to + int) instead -- see classifyWidthConstraint() and that enum's + documentation for when each case applies. */ int TitleBlockTemplate::minimumWidth() { - // Abbreviations: ABS: absolute, RTT: relative to total, RTR: - // relative to remaining, - // TOT: total diagram/TBT width (variable). - - // Minimum size may be enforced by ABS and RTT widths: - // TOT >= ((sum(REL)/100)*TOT)+sum(ABS) - // => (1 - (sum(REL)/100))TOT >= sum(ABS) - // => TOT >= sum(ABS) / (1 - (sum(REL)/100)) - // => TOT >= sum(ABS) / ((100 - sum(REL))/100)) - int abs_total = columnTypeTotal(QET::Absolute); - qreal denominator = (100.0 - columnTypeTotal(QET::RelativeToTotalLength)) / 100.0; - - if (denominator <= 0.0) { - // The relative-to-total-length columns alone already consume - // 100% (or more) of the available width, so the formula above - // would divide by zero (or go negative). There is no finite - // minimum width this formula can determine. Report "no - // constraint", the same convention maximumWidth() uses. - return -1; + int abs_total; + qreal remaining_width_fraction; + if (auto width_case = classifyWidthConstraint(abs_total, remaining_width_fraction)) { + return static_cast(*width_case); } - - return(qRound(abs_total / denominator)); + return qRound(abs_total / remaining_width_fraction); } /** @brief TitleBlockTemplate::maximumWidth - @return the maximum width for this template, - or -1 if it does not have any. + @return the maximum width, in pixels, this template may be + rendered at. + + If this template is composed entirely of absolute-width (ABS) + columns, the maximum is fixed: the template cannot extend beyond + their sum, since nothing in it scales with the template's total + width. Otherwise, at least one column scales with the total width + (relative-to-total-length or relative-to-remaining-length), so + there is ordinarily no upper bound: returns + WidthConstraintCase::Unconstrained (cast to int) -- the common case + for most templates. + + The exception is when this template's columns cannot be satisfied + by any width at all (see classifyWidthConstraint()) -- in that case + there is no width, however large, that works, and this returns + WidthConstraintCase::RelativeWidthExceeds100Percent or + WidthConstraintCase::AbsoluteColumnsExceedRemainingWidth (cast to + int) instead of Unconstrained, matching minimumWidth()'s handling + of the same cases. */ int TitleBlockTemplate::maximumWidth() { + int abs_total; + qreal remaining_width_fraction; + if (auto width_case = classifyWidthConstraint(abs_total, remaining_width_fraction)) { + return static_cast(*width_case); + } if (columnTypeCount(QET::Absolute) == columns_width_.count()) { // The template is composed of absolute widths only, // therefore it may not extend beyond their sum. - return(columnTypeTotal(QET::Absolute)); + return abs_total; // already computed by classifyWidthConstraint() } - return(-1); + return static_cast(WidthConstraintCase::Unconstrained); } /** diff --git a/sources/titleblocktemplate.h b/sources/titleblocktemplate.h index e8d5cb3a4..a7f6c9686 100644 --- a/sources/titleblocktemplate.h +++ b/sources/titleblocktemplate.h @@ -24,6 +24,7 @@ #include #include +#include /** @brief The TitleBlockTemplate class @@ -36,9 +37,73 @@ */ class TitleBlockTemplate : public QObject { Q_OBJECT + + public: + /** + @brief The TitleBlockTemplate::WidthConstraintCase enum Distinguishes + the possible outcomes of minimumWidth() or maximumWidth() for a + template's column layout: either a genuine finite pixel width, or one + of the cases below where no single finite width applies -- some of which are + perfectly ordinary (Unconstrained), and some of which indicate the + template's columns cannot be laid out at any width. + + Both functions return one of the non-Bounded* cases below, + cast to int, in place of a genuine width whenever no finite + width applies. (*minimumWidth()/maximumWidth() never return + a literal "Bounded" value -- when a finite width exists, + they return that width directly. Bounded only appears as a + concept in classifyWidthConstraint()'s std::nullopt return.) + The int-based encoding exists because this project + currently targets C++17. + + @todo Once this project's minimum supported C++ standard + reaches C++23, migrate minimumWidth() and maximumWidth() to + return std::expected instead of + encoding these cases as negative int sentinels. That removes + the possibility of a caller silently misinterpreting a + sentinel as a real pixel width -- something the current + int-based encoding cannot prevent at compile time. + */ + enum class WidthConstraintCase : int { + /** + There is no finite width constraint in this direction: + any width is valid. This is a perfectly ordinary case, + not a problem with the template -- for minimumWidth(), + it happens when the relative-to-total-length (RTT) + columns account for exactly 100% of the total width and + there are no absolute-width (ABS) columns competing for + space -- every term in the minimum-width inequality + vanishes (0 >= 0), which holds for any width. For + maximumWidth(), it happens whenever at least one column + scales with the total width, so nothing caps how wide + the template may grow -- the common case for most + templates. + */ + Unconstrained = -1, + + /** + The RTT columns alone already exceed 100% of the total + width (sum(RTT) > 100), independently of whether any ABS + columns exist. The minimum-width inequality then only + holds for a non-positive width, which cannot represent a + real template: this indicates a genuinely misconfigured + template that cannot be laid out at any width. + */ + RelativeWidthExceeds100Percent = -2, + + /** + The RTT columns account for exactly 100% of the total + width, but at least one ABS column also needs a nonzero, + fixed amount of space on top of that. The minimum-width + inequality reduces to "0 >= (a positive number)", which + never holds: this also indicates a genuinely + misconfigured template that cannot be laid out at any + width. + */ + AbsoluteColumnsExceedRemainingWidth = -3 + }; // constructors, destructor - public: TitleBlockTemplate(QObject * = nullptr); ~TitleBlockTemplate() override; private: @@ -146,6 +211,20 @@ class TitleBlockTemplate : public QObject { bool checkCell(const QDomElement &, TitleBlockCell ** = nullptr); void flushCells(); void initCells(); + /** + @brief TitleBlockTemplate::classifyWidthConstraint Classifies this template's absolute-width (ABS) and + relative-to-total-length (RTT) columns, independently of whether a minimum or a maximum width is being computed -- see + minimumWidth() for the derivation this is based on. + @param[out] abs_total set to columnTypeTotal(QET::Absolute). + @param[out] remaining_width_fraction set to the fraction of the total template width left over once the RTT columns have taken + their share: (100.0 - sum(RTT)) / 100.0. Zero means the RTT columns claim the entire width, leaving nothing for ABS + columns; negative means they claim more than the entire width, which is unsatisfiable regardless of any ABS columns. Only + meaningful when this function returns std::nullopt. + @return WidthConstraintCase::RelativeWidthExceeds100Percent, WidthConstraintCase::AbsoluteColumnsExceedRemainingWidth, or + WidthConstraintCase::Unconstrained if no finite width exists for this template's columns, or std::nullopt if a finite width + does exist (computable as qRound(abs_total / remaining_width_fraction)). + */ + std::optional classifyWidthConstraint(int &abs_total, qreal &remaining_width_fraction); int lengthRange(int, int, const QList &) const; QString finalTextForCell( const TitleBlockCell &,