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.
10 // This file is part of OpenTween.
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)
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
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.
30 using System.Collections.Generic;
31 using System.Diagnostics.CodeAnalysis;
34 using System.Threading;
35 using System.Windows.Forms;
36 using OpenTween.Models;
37 using OpenTween.OpenTweenCustomControl;
41 public sealed class TimelineListViewCache : IDisposable
43 public bool IsDisposed { get; private set; } = false;
45 public bool IsListSizeMismatched
46 => this.listView.VirtualListSize != this.tab.AllCount;
48 private readonly DetailsListView listView;
49 private readonly TabModel tab;
50 private readonly SettingCommon settings;
53 /// 現在表示している発言一覧の <see cref="ListView"/> に対するキャッシュ
56 /// キャッシュクリアのために null が代入されることがあるため、
57 /// 使用する場合には <see cref="listItemCache"/> に対して直接メソッド等を呼び出さずに
58 /// 一旦ローカル変数に代入してから参照すること。
60 private ListViewItemCache? listItemCache = null;
62 public TimelineListViewCache(
63 DetailsListView listView,
65 SettingCommon settings
68 this.listView = listView;
70 this.settings = settings;
72 this.RegisterHandlers();
73 this.listView.VirtualMode = true;
74 this.UpdateListSize();
77 private void RegisterHandlers()
79 this.listView.CacheVirtualItems += this.ListView_CacheVirtualItems;
80 this.listView.RetrieveVirtualItem += this.ListView_RetrieveVirtualItem;
83 private void UnregisterHandlers()
85 this.listView.CacheVirtualItems -= this.ListView_CacheVirtualItems;
86 this.listView.RetrieveVirtualItem -= this.ListView_RetrieveVirtualItem;
89 public void UpdateListSize()
94 this.listView.VirtualListSize = this.tab.AllCount;
96 catch (NullReferenceException ex)
98 // WinForms 内部で ListView.set_TopItem が発生させている例外
99 // https://ja.osdn.net/ticket/browse.php?group_id=6526&tid=36588
100 MyCommon.TraceOut(ex, $"TabType: {this.tab.TabType}, Count: {this.tab.AllCount}, ListSize: {this.listView.VirtualListSize}");
104 internal void CreateCache(int startIndex, int endIndex)
106 if (this.tab.AllCount == 0)
109 // インデックスを 0...(tabInfo.AllCount - 1) の範囲内にする
110 int FilterRange(int index)
111 => Math.Max(Math.Min(index, this.tab.AllCount - 1), 0);
113 // キャッシュ要求(要求範囲±30を作成)
114 startIndex = FilterRange(startIndex - 30);
115 endIndex = FilterRange(endIndex + 30);
117 var cacheLength = endIndex - startIndex + 1;
119 var posts = this.tab[startIndex, endIndex]; // 配列で取得
120 var listItems = Enumerable.Range(0, cacheLength)
121 .Select(x => this.CreateItem(posts[x]))
124 var listCache = new ListViewItemCache(
130 Interlocked.Exchange(ref this.listItemCache, listCache);
134 /// DetailsListView のための ListViewItem のキャッシュを消去する
136 public void PurgeCache()
137 => Interlocked.Exchange(ref this.listItemCache, null);
139 private (ListViewItem Item, ListItemStyle Style) CreateItem(PostClass post)
141 var mk = new StringBuilder();
143 if (post.FavoritedCount > 0) mk.Append("+" + post.FavoritedCount);
146 if (post.RetweetedId == null)
152 post.IsDeleted ? "(DELETED)" : post.AccessibleText.Replace('\n', ' '),
153 post.CreatedAt.ToLocalTimeString(this.settings.DateTimeFormat),
159 itm = new ListViewItem(sitem);
167 post.IsDeleted ? "(DELETED)" : post.AccessibleText.Replace('\n', ' '),
168 post.CreatedAt.ToLocalTimeString(this.settings.DateTimeFormat),
169 post.ScreenName + Environment.NewLine + "(RT:" + post.RetweetedBy + ")",
174 itm = new ListViewItem(sitem);
177 var style = this.DetermineListItemStyle(post);
178 this.ApplyListItemStyle(itm, style);
183 public void RefreshStyle(int index)
185 var post = this.tab[index];
186 var style = this.DetermineListItemStyle(post);
188 var listCache = this.listItemCache;
189 if (listCache != null && listCache.TryGetValue(index, out var item, out var currentStyle))
191 // スタイルに変化がない場合は何もせず終了
192 if (currentStyle == style)
195 listCache.UpdateStyle(index, style);
199 item = this.listView.Items[index];
202 // ValidateRectが呼ばれる前に選択色などの描画を済ませておく
203 this.listView.Update();
205 this.ApplyListItemStyle(item, style);
206 this.listView.RefreshItem(index);
209 public void RefreshStyle()
211 var listCache = this.listItemCache;
212 if (listCache == null)
215 var updatedIndices = new List<int>();
217 foreach (var (_, currentStyle, index) in listCache.WithIndex())
219 var post = this.tab[index];
220 var style = this.DetermineListItemStyle(post);
221 if (currentStyle == style)
224 listCache.UpdateStyle(index, style);
225 updatedIndices.Add(index);
228 // ValidateRectが呼ばれる前に選択色などの描画を済ませておく
229 this.listView.Update();
231 foreach (var index in updatedIndices)
233 if (!listCache.TryGetValue(index, out var item, out var style))
236 this.ApplyListItemStyle(item, style);
239 updatedIndices.Add(this.tab.SelectedIndex);
240 this.listView.RefreshItems(updatedIndices);
243 public ListViewItem GetItem(int index)
245 var listCache = this.listItemCache;
246 if (listCache != null)
248 if (listCache.TryGetValue(index, out var item, out _))
252 var post = this.tab[index];
253 return this.CreateItem(post).Item;
256 public ListItemStyle GetStyle(int index)
258 var listCache = this.listItemCache;
259 if (listCache != null)
261 if (listCache.TryGetValue(index, out _, out var style))
265 var post = this.tab[index];
266 return this.DetermineListItemStyle(post);
269 private void ApplyListItemStyle(ListViewItem item, ListItemStyle style)
270 => item.SubItems[5].Text = this.GetUnreadMark(style.UnreadMark);
272 private string GetUnreadMark(bool unreadMark)
273 => unreadMark ? "★" : "";
275 private ListItemStyle DetermineListItemStyle(PostClass post)
277 var unreadManageEnabled = this.tab.UnreadManage && this.settings.UnreadManage;
278 var useUnreadStyle = unreadManageEnabled && this.settings.UseUnreadStyle;
280 var basePost = this.tab.AnchorPost ?? this.tab.SelectedPost;
283 this.DetermineUnreadMark(post, unreadManageEnabled),
284 this.DetermineBackColor(basePost, post),
285 this.DetermineForeColor(post, useUnreadStyle),
286 this.DetermineFont(post, useUnreadStyle)
290 private bool DetermineUnreadMark(PostClass post, bool unreadManageEnabled)
292 if (!unreadManageEnabled)
298 private ListItemBackColor DetermineBackColor(PostClass? basePost, PostClass post)
300 if (basePost == null)
301 return ListItemBackColor.None;
304 if (post.StatusId == basePost.InReplyToStatusId)
305 return ListItemBackColor.AtTo;
309 return ListItemBackColor.Self;
313 return ListItemBackColor.AtSelf;
316 if (basePost.ReplyToList.Any(x => x.UserId == post.UserId))
317 return ListItemBackColor.AtFromTarget;
320 if (post.ReplyToList.Any(x => x.UserId == basePost.UserId))
321 return ListItemBackColor.AtTarget;
324 if (post.UserId == basePost.UserId)
325 return ListItemBackColor.Target;
328 return ListItemBackColor.None;
331 private ListItemForeColor DetermineForeColor(PostClass post, bool useUnreadStyle)
334 return ListItemForeColor.Fav;
336 if (post.RetweetedId != null)
337 return ListItemForeColor.Retweet;
339 if (post.IsOwl && (post.IsDm || this.settings.OneWayLove))
340 return ListItemForeColor.OWL;
342 if (useUnreadStyle && !post.IsRead)
343 return ListItemForeColor.Unread;
345 return ListItemForeColor.None;
348 private ListItemFont DetermineFont(PostClass post, bool useUnreadStyle)
350 if (useUnreadStyle && !post.IsRead)
351 return ListItemFont.Unread;
353 return ListItemFont.Readed;
356 private void ListView_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e)
358 var listCache = this.listItemCache;
359 if (listCache != null && listCache.IsSupersetOf(e.StartIndex, e.EndIndex))
361 // If the newly requested cache is a subset of the old cache,
362 // no need to rebuild everything, so do nothing.
366 // Now we need to rebuild the cache.
367 this.CreateCache(e.StartIndex, e.EndIndex);
370 private void ListView_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
371 => e.Item = this.GetItem(e.ItemIndex);
373 public void Dispose()
378 // RetrieveVirtualItem が呼ばれないようにするため 0 をセットする
379 this.listView.VirtualListSize = 0;
381 this.UnregisterHandlers();
383 this.IsDisposed = true;
387 public enum ListItemBackColor
398 public enum ListItemForeColor
407 public enum ListItemFont
413 public readonly record struct ListItemStyle(
415 ListItemBackColor BackColor,
416 ListItemForeColor ForeColor,
420 public class ListViewItemCache
422 /// <summary>キャッシュする範囲の開始インデックス</summary>
423 public int StartIndex { get; }
425 /// <summary>キャッシュする範囲の終了インデックス</summary>
426 public int EndIndex { get; }
428 /// <summary>キャッシュされた範囲に対応する <see cref="ListViewItem"/> と <see cref="ListItemStyle"> の配列</summary>
429 public (ListViewItem, ListItemStyle)[] Cache { get; }
431 /// <summary>キャッシュされたアイテムの件数</summary>
433 => this.EndIndex - this.StartIndex + 1;
435 public ListViewItemCache(int startIndex, int endIndex, (ListViewItem, ListItemStyle)[] cache)
437 if (!IsCacheSizeValid(startIndex, endIndex, cache))
438 throw new ArgumentException("Cache size mismatch", nameof(cache));
440 this.StartIndex = startIndex;
441 this.EndIndex = endIndex;
445 /// <summary>指定されたインデックスがキャッシュの範囲内であるか判定します</summary>
446 /// <returns><paramref name="index"/> がキャッシュの範囲内であれば true、それ以外は false</returns>
447 public bool Contains(int index)
448 => index >= this.StartIndex && index <= this.EndIndex;
450 /// <summary>指定されたインデックスの範囲が全てキャッシュの範囲内であるか判定します</summary>
451 /// <returns><paramref name="rangeStart"/> から <paramref name="rangeEnd"/> の範囲が全てキャッシュの範囲内であれば true、それ以外は false</returns>
452 public bool IsSupersetOf(int rangeStart, int rangeEnd)
453 => rangeStart >= this.StartIndex && rangeEnd <= this.EndIndex;
455 /// <summary>指定されたインデックスの <see cref="ListViewItem"/> をキャッシュから取得することを試みます</summary>
456 /// <returns>取得に成功すれば true、それ以外は false</returns>
457 public bool TryGetValue(int index, [NotNullWhen(true)] out ListViewItem? item, out ListItemStyle style)
459 if (this.Contains(index))
461 (item, style) = this.Cache[index - this.StartIndex];
472 public IEnumerable<(ListViewItem Item, ListItemStyle Stype, int Index)> WithIndex()
474 foreach (var ((item, style), index) in this.Cache.WithIndex())
475 yield return (item, style, index + this.StartIndex);
478 public void UpdateStyle(int index, ListItemStyle style)
480 if (!this.Contains(index))
483 this.Cache[index - this.StartIndex].Item2 = style;
486 private static bool IsCacheSizeValid<T>(int startIndex, int endIndex, T[] cache)
487 => cache.Length == (endIndex - startIndex + 1);