@@ -2411,6 +2411,9 @@ void HistoryInner::itemRemoved(not_null<const HistoryItem*> item) {
24112411 if (_accessibilityFocusedItem == item) {
24122412 _accessibilityFocusedItem = nullptr ;
24132413 }
2414+ if (_accessibilitySelectionAnchor == item) {
2415+ _accessibilitySelectionAnchor = nullptr ;
2416+ }
24142417 _accessibilityIdentities.remove (item);
24152418
24162419 if ((_dragSelFrom && _dragSelFrom->data () == item)
@@ -4065,47 +4068,64 @@ void HistoryInner::keyPressEvent(QKeyEvent *e) {
40654068 }
40664069 }
40674070 }
4071+ const auto modifiers = e->modifiers ()
4072+ & ~(Qt::KeypadModifier | Qt::GroupSwitchModifier);
4073+ const auto plainKey = (modifiers == Qt::NoModifier);
4074+ const auto shiftRange = (modifiers == Qt::ShiftModifier)
4075+ && (e->key () == Qt::Key_Up || e->key () == Qt::Key_Down);
40684076 auto newIndex = _accessibilityFocusedIndex;
40694077 switch (e->key ()) {
40704078 case Qt::Key_Down:
4071- newIndex = std::min (
4072- (newIndex < 0 ) ? (count - 1 ) : (newIndex + 1 ),
4073- count - 1 );
4079+ if (plainKey || shiftRange) {
4080+ newIndex = std::min (
4081+ (newIndex < 0 ) ? (count - 1 ) : (newIndex + 1 ),
4082+ count - 1 );
4083+ }
40744084 break ;
40754085 case Qt::Key_Up:
4076- newIndex = std::max (
4077- (newIndex < 0 ) ? (count - 1 ) : (newIndex - 1 ),
4078- 0 );
4086+ if (plainKey || shiftRange) {
4087+ newIndex = std::max (
4088+ (newIndex < 0 ) ? (count - 1 ) : (newIndex - 1 ),
4089+ 0 );
4090+ }
40794091 break ;
40804092 case Qt::Key_PageDown: {
4081- const auto pageHeight = _visibleAreaBottom
4082- - _visibleAreaTop;
4083- auto remaining = pageHeight;
4084- while (newIndex + 1 < count && remaining > 0 ) {
4085- ++newIndex;
4086- const auto rect = accessibilityChildRect (
4087- newIndex);
4088- remaining -= rect.height ();
4093+ if (plainKey) {
4094+ const auto pageHeight = _visibleAreaBottom
4095+ - _visibleAreaTop;
4096+ auto remaining = pageHeight;
4097+ while (newIndex + 1 < count && remaining > 0 ) {
4098+ ++newIndex;
4099+ const auto rect = accessibilityChildRect (
4100+ newIndex);
4101+ remaining -= rect.height ();
4102+ }
40894103 }
40904104 break ;
40914105 }
40924106 case Qt::Key_PageUp: {
4093- const auto pageHeight = _visibleAreaBottom
4094- - _visibleAreaTop;
4095- auto remaining = pageHeight;
4096- while (newIndex - 1 >= 0 && remaining > 0 ) {
4097- --newIndex;
4098- const auto rect = accessibilityChildRect (
4099- newIndex);
4100- remaining -= rect.height ();
4107+ if (plainKey) {
4108+ const auto pageHeight = _visibleAreaBottom
4109+ - _visibleAreaTop;
4110+ auto remaining = pageHeight;
4111+ while (newIndex - 1 >= 0 && remaining > 0 ) {
4112+ --newIndex;
4113+ const auto rect = accessibilityChildRect (
4114+ newIndex);
4115+ remaining -= rect.height ();
4116+ }
41014117 }
41024118 break ;
41034119 }
41044120 case Qt::Key_Home:
4105- newIndex = 0 ;
4121+ if (plainKey) {
4122+ newIndex = 0 ;
4123+ }
41064124 break ;
41074125 case Qt::Key_End:
4108- newIndex = count - 1 ;
4126+ if (plainKey) {
4127+ newIndex = count - 1 ;
4128+ }
41094129 break ;
41104130 default :
41114131 break ;
@@ -4124,6 +4144,13 @@ void HistoryInner::keyPressEvent(QKeyEvent *e) {
41244144 && elementIndex < int (elements.size ()))
41254145 ? elements[elementIndex]->data ().get ()
41264146 : nullptr ;
4147+ if (shiftRange) {
4148+ extendAccessibilitySelection (
4149+ _accessibilityFocusedIndex,
4150+ newIndex);
4151+ } else {
4152+ _accessibilitySelectionAnchor = nullptr ;
4153+ }
41274154 setAccessibilityFocusedItem (newIndex, item);
41284155
41294156 const auto rect = accessibilityChildRect (newIndex);
@@ -4150,14 +4177,32 @@ void HistoryInner::keyPressEvent(QKeyEvent *e) {
41504177 return ;
41514178 }
41524179
4180+ if (shiftRange) {
4181+ e->accept ();
4182+ return ;
4183+ }
4184+
41534185 if (e->key () == Qt::Key_Space) {
4154- if (hasSelectedItems ()) {
4186+ // Ctrl+Space toggles selection of the focused message and
4187+ // thereby enters "selection mode". While any message stays
4188+ // selected, plain Space keeps toggling (mirroring how mouse
4189+ // clicks work for sighted users once selection is active);
4190+ // with nothing selected it plays/pauses the focused media.
4191+ if (modifiers == Qt::ControlModifier) {
4192+ _accessibilitySelectionAnchor = nullptr ;
41554193 toggleMessageSelection ();
4156- } else {
4157- playPauseFocusedMedia ();
4194+ e->accept ();
4195+ return ;
4196+ } else if (modifiers == Qt::NoModifier) {
4197+ if (hasSelectedItems ()) {
4198+ _accessibilitySelectionAnchor = nullptr ;
4199+ toggleMessageSelection ();
4200+ } else {
4201+ playPauseFocusedMedia ();
4202+ }
4203+ e->accept ();
4204+ return ;
41584205 }
4159- e->accept ();
4160- return ;
41614206 }
41624207 }
41634208
@@ -5813,30 +5858,109 @@ void HistoryInner::announceAccessibilityFocus(int index) {
58135858}
58145859
58155860void HistoryInner::toggleMessageSelection () {
5816- if (!hasSelectedItems () || _accessibilityFocusedIndex < 0 ) {
5817- return ;
5818- }
5861+ changeAccessibilitySelection (
5862+ _accessibilityFocusedIndex,
5863+ SelectAction::Invert);
5864+ }
5865+
5866+ void HistoryInner::changeAccessibilitySelection (
5867+ int index,
5868+ SelectAction action) {
58195869 const auto barIndex = accessibilityUnreadBarIndex ();
5820- if (barIndex >= 0 && _accessibilityFocusedIndex == barIndex) {
5870+ if (index < 0 || ( barIndex >= 0 && index == barIndex) ) {
58215871 return ;
58225872 }
58235873 const auto elements = accessibleElements ();
5824- const auto elementIndex = (barIndex >= 0
5825- && _accessibilityFocusedIndex > barIndex)
5826- ? (_accessibilityFocusedIndex - 1 )
5827- : _accessibilityFocusedIndex;
5874+ const auto elementIndex = (barIndex >= 0 && index > barIndex)
5875+ ? (index - 1 )
5876+ : index;
58285877 if (elementIndex < 0 || elementIndex >= int (elements.size ())) {
58295878 return ;
58305879 }
58315880 const auto item = elements[elementIndex]->data ();
5881+ // Detect the change by container size, not by the group membership
5882+ // flip: deselecting a partially selected album really mutates the
5883+ // selection while isSelectedAsGroup() stays false both before and
5884+ // after. Selection changes here are pure adds or removes, so an
5885+ // unchanged size means nothing changed and no repaint, top bar
5886+ // update or announcement is due.
5887+ const auto sizeBefore = _selected.size ();
5888+ changeSelectionAsGroup (&_selected, item, action);
5889+ if (_selected.size () == sizeBefore) {
5890+ return ;
5891+ }
58325892 clearTextSelection ();
5833- changeSelectionAsGroup (&_selected, item, SelectAction::Invert);
58345893 repaintItem (item);
58355894 _widget->updateTopBarSelection ();
5836- accessibilityChildStateChanged (
5837- _accessibilityFocusedIndex,
5838- { .selected = true });
5839- accessibilityChildNameChanged (_accessibilityFocusedIndex);
5895+ accessibilityChildStateChanged (index, { .selected = true });
5896+ accessibilityChildNameChanged (index);
5897+ }
5898+
5899+ void HistoryInner::extendAccessibilitySelection (
5900+ int oldIndex,
5901+ int newIndex) {
5902+ // Windows-Explorer-style Shift+arrow range selection. The anchor is
5903+ // the row the current range grows from: re-established here whenever
5904+ // it is missing, no longer listed or the selection is empty, so an
5905+ // interrupted or cancelled range simply starts a fresh one at the
5906+ // focused row. Growing away from the anchor selects the row being
5907+ // entered (and the origin row on the first step), stepping back
5908+ // towards it deselects the row being left.
5909+ const auto elements = accessibleElements ();
5910+ const auto barIndex = accessibilityUnreadBarIndex ();
5911+ const auto itemAt = [&](int index) -> HistoryItem* {
5912+ if (barIndex >= 0 && index == barIndex) {
5913+ return nullptr ;
5914+ }
5915+ const auto elementIndex = (barIndex >= 0 && index > barIndex)
5916+ ? (index - 1 )
5917+ : index;
5918+ return (elementIndex >= 0
5919+ && elementIndex < int (elements.size ()))
5920+ ? elements[elementIndex]->data ().get ()
5921+ : nullptr ;
5922+ };
5923+ if (oldIndex < 0 ) {
5924+ _accessibilitySelectionAnchor = itemAt (newIndex);
5925+ changeAccessibilitySelection (newIndex, SelectAction::Select);
5926+ return ;
5927+ }
5928+ auto anchorIndex = -1 ;
5929+ if (_accessibilitySelectionAnchor && hasSelectedItems ()) {
5930+ for (auto i = 0 , n = int (elements.size ()); i != n; ++i) {
5931+ if (elements[i]->data ().get ()
5932+ == _accessibilitySelectionAnchor) {
5933+ anchorIndex = (barIndex >= 0 && i >= barIndex)
5934+ ? (i + 1 )
5935+ : i;
5936+ break ;
5937+ }
5938+ }
5939+ }
5940+ if (anchorIndex < 0 ) {
5941+ _accessibilitySelectionAnchor = itemAt (oldIndex);
5942+ if (!_accessibilitySelectionAnchor) {
5943+ // Re-anchoring on the unread-bar row (it has no item): anchor
5944+ // to the row being entered instead, so the next Shift+arrow
5945+ // can resolve the anchor and shrink the range. Leaving the
5946+ // anchor null here would make every following step re-anchor
5947+ // at its own old index and always read as growing.
5948+ _accessibilitySelectionAnchor = itemAt (newIndex);
5949+ }
5950+ anchorIndex = oldIndex;
5951+ }
5952+ const auto growing = std::abs (newIndex - anchorIndex)
5953+ > std::abs (oldIndex - anchorIndex);
5954+ if (growing) {
5955+ if (oldIndex == anchorIndex) {
5956+ changeAccessibilitySelection (
5957+ oldIndex,
5958+ SelectAction::Select);
5959+ }
5960+ changeAccessibilitySelection (newIndex, SelectAction::Select);
5961+ } else {
5962+ changeAccessibilitySelection (oldIndex, SelectAction::Deselect);
5963+ }
58405964}
58415965
58425966void HistoryInner::playPauseFocusedMedia () {
@@ -6521,6 +6645,7 @@ void HistoryInner::applyAccessibilityFocus(
65216645 : nullptr ;
65226646 const auto changed = (_accessibilityFocusedIndex != index)
65236647 || (_accessibilityFocusedItem != item);
6648+ _accessibilitySelectionAnchor = nullptr ;
65246649 _accessibilityFocusedIndex = index;
65256650 _accessibilityFocusedItem = item;
65266651 // Exactly one announcement: directly when the widget already has
0 commit comments