OSDN Git Service

ListViewのスクロール位置・選択状態を保持するTimelineListViewStateを追加
[opentween/open-tween.git] / OpenTween / TimelineListViewState.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 //           (c) 2008-2011 Moz (@syo68k)
4 //           (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 //           (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 //           (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 //           (c) 2011      kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
8 // All rights reserved.
9 //
10 // This file is part of OpenTween.
11 //
12 // This program is free software; you can redistribute it and/or modify it
13 // under the terms of the GNU General public License as published by the Free
14 // Software Foundation; either version 3 of the License, or (at your option)
15 // any later version.
16 //
17 // This program is distributed in the hope that it will be useful, but
18 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
19 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
20 // for more details.
21 //
22 // You should have received a copy of the GNU General public License along
23 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
24 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
25 // Boston, MA 02110-1301, USA.
26
27 #nullable enable
28
29 using System;
30 using System.Collections.Generic;
31 using System.Drawing;
32 using System.Linq;
33 using System.Windows.Forms;
34 using OpenTween.Models;
35 using OpenTween.OpenTweenCustomControl;
36
37 namespace OpenTween
38 {
39     public class TimelineListViewState
40     {
41         private readonly DetailsListView listView;
42         private readonly TabModel tab;
43
44         private ListViewScroll savedScrollState;
45         private ListViewSelection savedSelectionState;
46
47         internal readonly record struct ListViewScroll(
48             ScrollLockMode ScrollLockMode,
49             long? TopItemStatusId
50         );
51
52         internal enum ScrollLockMode
53         {
54             /// <summary>固定しない</summary>
55             None,
56
57             /// <summary>最上部に固定する</summary>
58             FixedToTop,
59
60             /// <summary>最下部に固定する</summary>
61             FixedToBottom,
62
63             /// <summary><see cref="ListViewScroll.TopItemStatusId"/> の位置に固定する</summary>
64             FixedToItem,
65         }
66
67         internal readonly record struct ListViewSelection(
68             long[] SelectedStatusIds,
69             long? SelectionMarkStatusId,
70             long? FocusedStatusId
71         );
72
73         public TimelineListViewState(DetailsListView listView, TabModel tab)
74         {
75             this.listView = listView;
76             this.tab = tab;
77         }
78
79         public void Save(bool lockScroll)
80         {
81             this.savedScrollState = this.SaveListViewScroll(lockScroll);
82             this.savedSelectionState = this.SaveListViewSelection();
83         }
84
85         public void Restore()
86         {
87             this.RestoreScroll();
88             this.RestoreSelection();
89         }
90
91         public void RestoreScroll()
92             => this.RestoreListViewScroll(this.savedScrollState);
93
94         public void RestoreSelection()
95             => this.RestoreListViewSelection(this.savedSelectionState);
96
97         /// <summary>
98         /// <see cref="ListView"/> のスクロール位置に関する情報を <see cref="ListViewScroll"/> として返します
99         /// </summary>
100         private ListViewScroll SaveListViewScroll(bool lockScroll)
101         {
102             var lockMode = this.GetScrollLockMode(lockScroll);
103             long? topItemStatusId = null;
104
105             if (lockMode == ScrollLockMode.FixedToItem)
106             {
107                 var topItemIndex = this.listView.TopItem?.Index ?? -1;
108                 if (topItemIndex != -1 && topItemIndex < this.tab.AllCount)
109                     topItemStatusId = this.tab.GetStatusIdAt(topItemIndex);
110             }
111
112             return new ListViewScroll
113             {
114                 ScrollLockMode = lockMode,
115                 TopItemStatusId = topItemStatusId,
116             };
117         }
118
119         private ScrollLockMode GetScrollLockMode(bool lockScroll)
120         {
121             if (this.tab.SortMode == ComparerMode.Id)
122             {
123                 if (this.tab.SortOrder == SortOrder.Ascending)
124                 {
125                     // Id昇順
126                     if (lockScroll)
127                         return ScrollLockMode.None;
128
129                     // 最下行が表示されていたら、最下行へ強制スクロール。最下行が表示されていなかったら制御しない
130
131                     // 一番下に表示されているアイテム
132                     var bottomItem = this.listView.GetItemAt(0, this.listView.ClientSize.Height - 1);
133                     if (bottomItem == null || bottomItem.Index == this.listView.VirtualListSize - 1)
134                         return ScrollLockMode.FixedToBottom;
135                     else
136                         return ScrollLockMode.None;
137                 }
138                 else
139                 {
140                     // Id降順
141                     if (lockScroll)
142                         return ScrollLockMode.FixedToItem;
143
144                     // 最上行が表示されていたら、制御しない。最上行が表示されていなかったら、現在表示位置へ強制スクロール
145                     var topItem = this.listView.TopItem;
146                     if (topItem == null || topItem.Index == 0)
147                         return ScrollLockMode.FixedToTop;
148                     else
149                         return ScrollLockMode.FixedToItem;
150                 }
151             }
152             else
153             {
154                 return ScrollLockMode.FixedToItem;
155             }
156         }
157
158         /// <summary>
159         /// <see cref="ListView"/> の選択状態を <see cref="ListViewSelection"/> として返します
160         /// </summary>
161         private ListViewSelection SaveListViewSelection()
162         {
163             if (this.listView.VirtualListSize == 0)
164             {
165                 return new ListViewSelection
166                 {
167                     SelectedStatusIds = Array.Empty<long>(),
168                     SelectionMarkStatusId = null,
169                     FocusedStatusId = null,
170                 };
171             }
172
173             return new ListViewSelection
174             {
175                 SelectedStatusIds = this.tab.SelectedStatusIds,
176                 FocusedStatusId = this.GetFocusedStatusId(),
177                 SelectionMarkStatusId = this.GetSelectionMarkStatusId(),
178             };
179         }
180
181         private long? GetFocusedStatusId()
182         {
183             var index = this.listView.FocusedItem?.Index ?? -1;
184
185             return index != -1 && index < this.tab.AllCount ? this.tab.GetStatusIdAt(index) : (long?)null;
186         }
187
188         private long? GetSelectionMarkStatusId()
189         {
190             var index = this.listView.SelectionMark;
191
192             return index != -1 && index < this.tab.AllCount ? this.tab.GetStatusIdAt(index) : (long?)null;
193         }
194
195         /// <summary>
196         /// <see cref="SaveListViewScroll"/> によって保存されたスクロール位置を復元します
197         /// </summary>
198         private void RestoreListViewScroll(ListViewScroll listScroll)
199         {
200             if (this.listView.VirtualListSize == 0)
201                 return;
202
203             switch (listScroll.ScrollLockMode)
204             {
205                 case ScrollLockMode.FixedToTop:
206                     this.listView.EnsureVisible(0);
207                     break;
208                 case ScrollLockMode.FixedToBottom:
209                     this.listView.EnsureVisible(this.listView.VirtualListSize - 1);
210                     break;
211                 case ScrollLockMode.FixedToItem:
212                     var topIndex = listScroll.TopItemStatusId != null ? this.tab.IndexOf(listScroll.TopItemStatusId.Value) : -1;
213                     if (topIndex != -1)
214                     {
215                         var topItem = this.listView.Items[topIndex];
216                         try
217                         {
218                             this.listView.TopItem = topItem;
219                         }
220                         catch (NullReferenceException)
221                         {
222                             this.listView.EnsureVisible(this.listView.VirtualListSize - 1);
223                             this.listView.EnsureVisible(topIndex);
224                         }
225                     }
226                     break;
227                 case ScrollLockMode.None:
228                 default:
229                     break;
230             }
231         }
232
233         /// <summary>
234         /// <see cref="SaveListViewSelection"/> によって保存された選択状態を復元します
235         /// </summary>
236         private void RestoreListViewSelection(ListViewSelection listSelection)
237         {
238             var invalidateBounds = (Rectangle?)null;
239
240             var previousFocusedItem = this.listView.FocusedItem;
241             if (previousFocusedItem != null)
242                 invalidateBounds = previousFocusedItem.Bounds;
243
244             // status_id から ListView 上のインデックスに変換
245             if (listSelection.SelectedStatusIds != null)
246             {
247                 var selectedIndices = this.tab.IndexOf(listSelection.SelectedStatusIds).Where(x => x != -1).ToArray();
248                 this.listView.SelectItems(selectedIndices);
249             }
250
251             if (listSelection.FocusedStatusId != null)
252             {
253                 var focusedIndex = this.tab.IndexOf(listSelection.FocusedStatusId.Value);
254                 if (focusedIndex != -1)
255                     this.listView.Items[focusedIndex].Focused = true;
256             }
257
258             if (listSelection.SelectionMarkStatusId != null)
259             {
260                 var selectionMarkIndex = this.tab.IndexOf(listSelection.SelectionMarkStatusId.Value);
261                 if (selectionMarkIndex != -1)
262                     this.listView.SelectionMark = selectionMarkIndex;
263             }
264
265             if (invalidateBounds != null)
266                 this.listView.Invalidate(invalidateBounds.Value);
267         }
268     }
269 }