| Age | Commit message (Collapse) | Author | Files | Lines |
|
A thread's card has rendered one message since item 66, but every tag
action still acted on the entire conversation. Delete, Archive,
Important, Mark spam and Toggle unread now act on the message the card
shows; the whole-thread versions move to a "Whole thread" submenu in the
Message menu and the thread list's context menu, on Ctrl+Alt+<key>.
Closes items 87, 88, 105, 106, 107, 108, 109, 110 and 111.
The defects fixed along the way, several found by reading rather than by
report:
- threadAt(current.row()) answered about the wrong thread for a reply
row, because a tree numbers rows per parent. The audit found four live
sites, not the one reported: Delete and Toggle unread each chose their
DIRECTION from an unrelated thread, and the tag dialog counted the
wrong thread's tags. threadFor(index) replaces them.
- A message-scoped write made no optimistic model update and no reply
row carried a doomed cue, so acting on a reply moved the pending-edit
count and changed nothing on screen.
- Both toggles read the state of a reply's THREAD, which a
message-scoped write never changes, so they were one-way: the second
press re-sent a tag the message already had.
- flushHeldEdits() re-sent only thread-scoped edits, so a tag change
made on one message during a sync was applied to the row, counted as
unsynced, and then dropped without ever being written.
- applyTagChange() updated a thread's summary but not its loaded
replies, leaving an expanded thread's rows describing a state the
database no longer held.
- A thread's first message is not among its children, so both
message-scoped lookups missed it: acting on a root card repainted
nothing and emptied the message pane's chip row.
- ThreadSummary::tags is notmuch's union over the thread, so a card
standing for one message drew tags belonging to its siblings. The
worker now reads that message's own tags in the walk that already
finds its id, so the split is known before a row is ever opened.
The card shows both tiers: its own message's tags at full size, the rest
of the conversation's smaller and muted, so nothing appears to vanish
when a row is selected.
Auto mark-read is message-scoped as a result, and now arms for a reply,
which it never did. With maildir.synchronize_flags on, the old
thread-wide write reached the server for mail that had never been
displayed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Items 70 and 69, the second folded into the first as item 70's own size note
predicted it should be.
The panes drew their state marks as font glyphs: U+1F4CE for an attachment and
U+2605 for a flagged thread, each with a fallback for a font that cannot render
it. Both fell back to "*", so on such a font a flagged thread and one carrying
an attachment were indistinguishable, which is a defect the fallback introduced
rather than prevented. What a mark looks like was also the desktop's decision
rather than this application's, and the panes are exactly where it should not
be: the user asked for the toolbar and menus to keep following their icon theme
while the panes stop.
Six marks now ship in assets/icons/marks/: flagged, attachment, passed, replied
and the two expander triangles. QIcon::fromTheme still resolves every toolbar
and menu icon and was not touched.
Licensing chose the shapes. The look came from a GPL3 icon theme, and this
project is GPLv2-only, which are incompatible: GPLv2's "no further
restrictions" clause bars shipping GPL3 assets in a v2-only work. The six were
drawn fresh in the same idiom instead, with no path data copied. The idiom is
generic: solid single-path silhouettes at 16x16 with no strokes.
They are compiled in as string literals rather than loaded from a .qrc.
src/CMakeLists.txt already records why resources belong to the executable: a
qrc in the static library registers itself from a global initialiser the linker
drops. The tests link the library, so a resource-based mark would be missing
exactly where it needs asserting. assets/icons/marks/ stays the editable
source.
One asset serves both palettes. Every payload paints with fill="currentColor",
which QSvgRenderer renders black rather than resolving, so Marks::pixmap
composites the wanted colour with CompositionMode_SourceIn. A mark then takes
the card's own pen colour and follows selection and the read/unread dimming
without a second variant to keep in step.
CardLayout reserves a rect per mark and CardDelegate paints into it. The marks
were glyphs inside the subject STRING, so their width came free from the text
metrics; as icons the geometry has to know they are there or the subject runs
underneath them. The expander pill had the same trap, its triangle being a
glyph in expanderLabel(), and now reserves that width explicitly.
Item 69's part: passed and replied were words in the tag strip and are marks
beside the subject now. The message pane's header carries the flagged and
attachment marks next to the subject, per the user's decision that the right
pane needs those two and only outside the message area.
A duplicate that no test caught is worth recording. Every geometry assertion
passed while a card showed passed as BOTH an arrow and a green tag chip: the
chip filter had no reason to know a mark had appeared. It was found by
rendering real cards to an image and looking at them. isDrawnAsAMark() is now
one list consulted by both PillTagsRole and MessageOwnTagsRole, since two
copies drifting apart is how a tag ends up drawn twice on one row and not at
all on another.
Fourteen tests: nine in test_marks, four in test_cardlayout, one in
test_threadlistmodel. Mutation-checked at four points, each failing a test: the
subject ignoring the marks, the flag not indenting the subject, the pill
forgetting the triangle's width, and the recolour composite removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
|
Adds [general] date_format, a QDateTime pattern for the date a thread card
shows. Absent or empty means the system locale's short format, which is what
every other application on the desktop uses and stays the default.
The format reaches the LAYOUT, not only the painter. CardLayout::compute()
reserves the date's width from widestDateSample(), so a pattern that arrived
only at the drawText call would be elided into a rect sized for the old
format, which is the clipping the bold-font fault already produced once. It
rides on CardLayout::Input and defaults to an empty string, leaving every
existing call site unchanged. Confirmed by mutation: making the width ignore
the format fails the test.
widestDateSample() memoised its result in a static, which would have sized
every format after the first from whichever arrived first. It is a plain call
now, costing one QLocale lookup per row, the same as formatting the date.
Validation rejects only a pattern whose output is CONSTANT, found by
formatting two different instants and comparing. QDateTime::toString() treats
nearly every letter as a field, so "banana" formats as "bpmnpmnpm" and
"hello" as "22ello": nonsense, but they vary with the instant, and a check
claiming to find "no date field" cannot reject them. What harms the user is
the pattern that prints the same text on every card, and that is what is
refused, with the value named in the message.
The model supplies the pattern through DateFormatRole for the same reason it
supplies the tag colours: it is the one object here holding config, and a
delegate reading config itself would be a second source of truth.
Backlog item 62.
|
|
The five real account colours are all mid-tone, because they were chosen as
CHIP fills with legible text on top, and three pixels of a mid-tone colour
barely registers beside a card's own background. The bar now lifts saturation
and lightness to a floor.
A floor, not a repaint: a colour already past it is returned untouched, so a
deliberately vivid choice is preserved and only the muted ones move. Hue is
never altered, because hue is the entire information the bar carries and a
shifted one would stop matching the account's chip and its dropdown swatch.
0.65 and 0.50 were chosen by rendering all five accounts as 3px bars against
both a dark and a light card background and looking at them. Higher pushed the
weakest of them, a green at S 0.33, toward a neon that no longer matched its own
chip; lower left it where it started.
accentLineColour had no test at all until now, which is how two earlier versions
of it shipped wrong: one blended toward Base until it WAS the background, the
other passed a chip colour through unchanged. test_carddelegate covers the lift,
the floor's idempotence, hue preservation across all five accounts, and the
fallback for a thread with no account tag.
|
|
Four faults from the first hand test, two of them behavioural.
An expander that opened onto nothing. setThreadMessages kept only nodes with
depth > 0, and notmuch_thread_get_toplevel_messages returns every message at
depth 0 when a thread carries no usable In-Reply-To, so a flat thread
contributed no children while its card still advertised the count. Measured in
the user's database: of 396 inbox threads three are flat, one of them nine
messages long, and every two-message thread of that kind was affected, which is
exactly why the fault looked like "the expander only works with more than one
reply". The rule is now position, not depth: every message except the first,
which is the root card itself. That is also the correct rule rather than a
workaround, since the row under the root is the second message however notmuch
chose to nest it.
The thread's first message was unreachable. Selecting a root card loaded the
whole thread, so the pane showed every message with only the last expanded, and
no row in the list offered the first one: the reply rows are messages two
onward. The root card now renders its own message, which is what the card
already claims to be. It keeps its thread id, unlike the message-row path, so
mark-read and the tag-change repaint still work; that is asserted, because
clearing it is the obvious way to write this and silently disables both. Before
the replies are loaded the model has no first message to name and the whole
thread stays the honest answer.
Dates ignored the locale. One hardcoded "yyyy-MM-dd hh:mm" produced a US-looking
format on an Italian desktop; QLocale::system() now formats it, and the width
reserved for the date comes from the same function so a longer locale cannot
clip.
The expander was a bare number on the card's own background. It is a pill now,
carrying "3 replies" (and "1 reply", singular), sized from the label actually
drawn and measured in both glyph states so it does not resize under the pointer
on click. Its fill is blended from Text toward Base rather than taken from
QPalette::Button, which is #2b2b2b against a Base of #2b2b2b on the user's
theme: byte identical, so the pill was invisible. A theme may make any two roles
equal; a blend is defined against the surface it sits on and cannot collide with
it. Checked by rendering both a dark and a light palette and looking.
|
|
Both found by rendering real cards to an image and looking at them, not by any
assertion. The suite was green through both.
The date lost the leading digit of its year on every UNREAD card. The layout
reserves the date's width from the font it is handed, which is the view's
regular font, while the delegate paints with the bold one the model supplies for
unread: 154px reserved against 170px needed. CardLayout now measures the date
bold whatever font it is given, so the reserved width cannot be narrower than
what is drawn. A few pixels are wasted on a read card, which is the cheap side of
the trade.
The accent bar was painted correctly and was invisible. Blending the account
colour 0.35 toward the palette's Base, as the plan specified, is a fraction OF
THE ACCOUNT COLOUR, so on a dark theme it produced (0.18, 0.22, 0.26) against a
Base of (0.169, 0.169, 0.169): the background. The blend is dropped entirely.
An account colour is already chosen to be a chip's fill carrying legible text,
so it is muted to begin with, and nothing is drawn on the bar that needs that
contrast. The spine keeps a blend, at 0.55, because it runs the full height of
every reply in an expansion and is a different problem from a 3px edge marker.
The bar is still faint at 3px on a dark theme, since the account colours are
chosen as chip fills. Whether kAccentWidth needs raising cannot be settled
without the user's own accounts, screen and theme; that is Task 10's open
question and it is left open.
|
|
Replaces SubjectDelegate. The tag chips come home from the view: the strip was
painted there only because a delegate cannot paint outside its column and the
strip spanned all five, and with one column there is nothing to span.
RowStyleDelegate is inherited rather than dropped. Its job survives the
redesign: Qt resolves ForegroundRole into the palette's Text roles and prefers
those over HighlightedText, so the read/unread dimming would win on a selected
row and land as grey on the highlight. What it loses is the rest of its body,
which aligned cells against a text band and centred two marker columns; both
described a grid that no longer exists.
A reply's Re: prefix is stripped here. Every reply repeating the thread's
subject is the visual signature of a table of records, which is the thing item
53 is about.
The account chip becomes a bar down the card's left edge, and the reply spines
inherit its colour, so an expanded thread is bounded by one accent from its root
to its last reply without a second line in the gutter. Neither uses the raw
account colour: that colour is chosen to be a chip's fill with legible text on
top, and the same value as a thin line has to be followable down an expansion
without competing with the senders, so it is blended toward the palette's Base
by the weight threadLineColour() already uses. A reply resolves its THREAD's
colour by walking to the root, since AccountColourRole is empty on a message
row and a neutral spine under an accented root would break the continuous edge.
The build is red at this commit; the view and window still name the old
delegate.
|