OSDN Git Service

c6481dd719541264563372c52ea8880e832d0f4c
[opentween/open-tween.git] / OpenTween / Models / TabModel.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      Egtra (@egtra) <http://dev.activebasic.com/egtra/>
8 //           (c) 2012      kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
9 // All rights reserved.
10 //
11 // This file is part of OpenTween.
12 //
13 // This program is free software; you can redistribute it and/or modify it
14 // under the terms of the GNU General Public License as published by the Free
15 // Software Foundation; either version 3 of the License, or (at your option)
16 // any later version.
17 //
18 // This program is distributed in the hope that it will be useful, but
19 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
20 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
21 // for more details.
22 //
23 // You should have received a copy of the GNU General Public License along
24 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
25 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
26 // Boston, MA 02110-1301, USA.
27
28 using System;
29 using System.Collections.Concurrent;
30 using System.Collections.Generic;
31 using System.Linq;
32 using System.Text;
33 using System.Threading;
34 using System.Threading.Tasks;
35 using System.Windows.Forms;
36
37 namespace OpenTween.Models
38 {
39     public abstract class TabModel
40     {
41         public string TabName { get; set; }
42
43         public bool UnreadManage { get; set; } = true;
44         public bool Protected { get; set; }
45         public bool Notify { get; set; } = true;
46         public string SoundFile { get; set; } = "";
47
48         public ComparerMode SortMode { get; private set; }
49         public SortOrder SortOrder { get; private set; }
50
51         public long OldestId { get; set; } = long.MaxValue;
52         public long SinceId { get; set; }
53
54         public abstract MyCommon.TabUsageType TabType { get; }
55
56         public virtual ConcurrentDictionary<long, PostClass> Posts
57             => TabInformations.GetInstance().Posts;
58
59         public int AllCount => this._ids.Count;
60         public long[] StatusIds => this._ids.ToArray();
61
62         public bool IsDefaultTabType => this.TabType.IsDefault();
63         public bool IsDistributableTabType => this.TabType.IsDistributable();
64         public bool IsInnerStorageTabType => this.TabType.IsInnerStorage();
65
66         /// <summary>
67         /// 次回起動時にも保持されるタブか(SettingTabsに保存されるか)
68         /// </summary>
69         public virtual bool IsPermanentTabType => true;
70
71         private IndexedSortedSet<long> _ids = new IndexedSortedSet<long>();
72         private ConcurrentQueue<TemporaryId> addQueue = new ConcurrentQueue<TemporaryId>();
73         private ConcurrentQueue<long> removeQueue = new ConcurrentQueue<long>();
74         private SortedSet<long> unreadIds = new SortedSet<long>();
75
76         private readonly object _lockObj = new object();
77
78         protected TabModel(string tabName)
79         {
80             this.TabName = tabName;
81         }
82
83         public abstract Task RefreshAsync(Twitter tw, bool backward, bool startup, IProgress<string> progress);
84
85         private struct TemporaryId
86         {
87             public long StatusId { get; }
88             public bool Read { get; }
89
90             public TemporaryId(long statusId, bool read)
91             {
92                 this.StatusId = statusId;
93                 this.Read = read;
94             }
95         }
96
97         public virtual void AddPostQueue(PostClass post)
98         {
99             this.addQueue.Enqueue(new TemporaryId(post.StatusId, post.IsRead));
100         }
101
102         //無条件に追加
103         internal void AddPostImmediately(long statusId, bool read)
104         {
105             if (this._ids.Contains(statusId)) return;
106
107             this._ids.Add(statusId);
108
109             if (!read)
110                 this.unreadIds.Add(statusId);
111         }
112
113         public IList<long> AddSubmit()
114         {
115             var addedIds = new List<long>();
116
117             TemporaryId tId;
118             while (this.addQueue.TryDequeue(out tId))
119             {
120                 this.AddPostImmediately(tId.StatusId, tId.Read);
121                 addedIds.Add(tId.StatusId);
122             }
123
124             return addedIds;
125         }
126
127         public virtual void EnqueueRemovePost(long statusId, bool setIsDeleted)
128         {
129             this.removeQueue.Enqueue(statusId);
130         }
131
132         public virtual bool RemovePostImmediately(long statusId)
133         {
134             if (!this._ids.Remove(statusId))
135                 return false;
136
137             this.unreadIds.Remove(statusId);
138             return true;
139         }
140
141         public IReadOnlyList<long> RemoveSubmit()
142         {
143             var removedIds = new List<long>();
144
145             long statusId;
146             while (this.removeQueue.TryDequeue(out statusId))
147             {
148                 if (this.RemovePostImmediately(statusId))
149                     removedIds.Add(statusId);
150             }
151
152             return removedIds;
153         }
154
155         public virtual void ClearIDs()
156         {
157             this._ids.Clear();
158             this.unreadIds.Clear();
159
160             Interlocked.Exchange(ref this.addQueue, new ConcurrentQueue<TemporaryId>());
161         }
162
163         /// <summary>
164         /// タブ更新時に使用する SinceId, OldestId をリセットする
165         /// </summary>
166         public void ResetFetchIds()
167         {
168             this.SinceId = 0L;
169             this.OldestId = long.MaxValue;
170         }
171
172         /// <summary>
173         /// ソート対象のフィールドとソート順を設定し、ソートを実行します
174         /// </summary>
175         public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
176         {
177             this.SortMode = mode;
178             this.SortOrder = sortOrder;
179
180             this.ApplySortMode();
181         }
182
183         private void ApplySortMode()
184         {
185             var sign = this.SortOrder == SortOrder.Ascending ? 1 : -1;
186
187             Comparison<long> comparison;
188             if (this.SortMode == ComparerMode.Id)
189             {
190                 comparison = (x, y) => sign * x.CompareTo(y);
191             }
192             else
193             {
194                 Comparison<PostClass> postComparison;
195                 switch (this.SortMode)
196                 {
197                     default:
198                     case ComparerMode.Data:
199                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.TextFromApi, y?.TextFromApi);
200                         break;
201                     case ComparerMode.Name:
202                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.ScreenName, y?.ScreenName);
203                         break;
204                     case ComparerMode.Nickname:
205                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Nickname, y?.Nickname);
206                         break;
207                     case ComparerMode.Source:
208                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Source, y?.Source);
209                         break;
210                 }
211
212                 comparison = (x, y) =>
213                 {
214                     PostClass xPost, yPost;
215                     this.Posts.TryGetValue(x, out xPost);
216                     this.Posts.TryGetValue(y, out yPost);
217
218                     var compare = sign * postComparison(xPost, yPost);
219                     if (compare != 0)
220                         return compare;
221
222                     // 同値であれば status_id で比較する
223                     return sign * x.CompareTo(y);
224                 };
225             }
226
227             var comparer = Comparer<long>.Create(comparison);
228
229             this._ids = new IndexedSortedSet<long>(this._ids, comparer);
230             this.unreadIds = new SortedSet<long>(this.unreadIds, comparer);
231         }
232
233         /// <summary>
234         /// 次に表示する未読ツイートのIDを返します。
235         /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
236         /// </summary>
237         public long NextUnreadId
238         {
239             get
240             {
241                 if (!this.UnreadManage || !SettingCommon.Instance.UnreadManage)
242                     return -1L;
243
244                 if (this.unreadIds.Count == 0)
245                     return -1L;
246
247                 // unreadIds はリストのインデックス番号順に並んでいるため、
248                 // 例えば ID 順の整列であれば昇順なら上から、降順なら下から順に返せば過去→現在の順になる
249                 return this.SortOrder == SortOrder.Ascending ? this.unreadIds.Min : this.unreadIds.Max;
250             }
251         }
252
253         /// <summary>
254         /// 次に表示する未読ツイートのインデックス番号を返します。
255         /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
256         /// </summary>
257         public int NextUnreadIndex
258         {
259             get
260             {
261                 var unreadId = this.NextUnreadId;
262                 return unreadId != -1 ? this.IndexOf(unreadId) : -1;
263             }
264         }
265
266         /// <summary>
267         /// 未読ツイートの件数を返します。
268         /// ただし、未読がない場合または UnreadManage が false の場合は 0 を返します
269         /// </summary>
270         public int UnreadCount
271         {
272             get
273             {
274                 if (!this.UnreadManage || !SettingCommon.Instance.UnreadManage)
275                     return 0;
276
277                 return this.unreadIds.Count;
278             }
279         }
280
281         /// <summary>
282         /// 未読ツイートの ID を配列で返します
283         /// </summary>
284         public long[] GetUnreadIds()
285         {
286             lock (this._lockObj)
287                 return this.unreadIds.ToArray();
288         }
289
290         /// <summary>
291         /// タブ内の既読状態を変更します
292         /// </summary>
293         /// <remarks>
294         /// 全タブを横断して既読状態を変える TabInformation.SetReadAllTab() の内部で呼び出されるメソッドです
295         /// </remarks>
296         /// <param name="statusId">変更するツイートのID</param>
297         /// <param name="read">既読状態</param>
298         /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
299         internal bool SetReadState(long statusId, bool read)
300         {
301             if (!this._ids.Contains(statusId))
302                 throw new ArgumentOutOfRangeException(nameof(statusId));
303
304             if (this.IsInnerStorageTabType)
305                 this.Posts[statusId].IsRead = read;
306
307             if (read)
308                 return this.unreadIds.Remove(statusId);
309             else
310                 return this.unreadIds.Add(statusId);
311         }
312
313         public bool Contains(long statusId)
314             => this._ids.Contains(statusId);
315
316         public PostClass this[int index]
317         {
318             get
319             {
320                 PostClass post;
321                 if (!this.Posts.TryGetValue(this.GetStatusIdAt(index), out post))
322                     throw new ArgumentOutOfRangeException("Post not exists", nameof(index));
323
324                 return post;
325             }
326         }
327
328         public PostClass[] this[int startIndex, int endIndex]
329         {
330             get
331             {
332                 if (startIndex < 0)
333                     throw new ArgumentOutOfRangeException(nameof(startIndex));
334                 if (endIndex >= this.AllCount)
335                     throw new ArgumentOutOfRangeException(nameof(endIndex));
336                 if (startIndex > endIndex)
337                     throw new ArgumentException($"{nameof(startIndex)} must be equal to or less than {nameof(endIndex)}.", nameof(startIndex));
338
339                 var length = endIndex - startIndex + 1;
340                 var posts = new PostClass[length];
341
342                 var i = 0;
343                 foreach (var idx in Enumerable.Range(startIndex, length))
344                 {
345                     var statusId = this.GetStatusIdAt(idx);
346                     this.Posts.TryGetValue(statusId, out posts[i++]);
347                 }
348
349                 return posts;
350             }
351         }
352
353         public long[] GetStatusIdAt(IEnumerable<int> indexes)
354             => indexes.Select(x => this.GetStatusIdAt(x)).ToArray();
355
356         public long GetStatusIdAt(int index)
357             => this._ids[index];
358
359         public int[] IndexOf(long[] statusIds)
360         {
361             if (statusIds == null)
362                 throw new ArgumentNullException(nameof(statusIds));
363
364             return statusIds.Select(x => this.IndexOf(x)).ToArray();
365         }
366
367         public int IndexOf(long statusId)
368             => this._ids.IndexOf(statusId);
369
370         public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer)
371             => this.SearchPostsAll(stringComparer, reverse: false);
372
373         public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer, int startIndex)
374             => this.SearchPostsAll(stringComparer, startIndex, reverse: false);
375
376         public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer, bool reverse)
377         {
378             var startIndex = reverse ? this.AllCount - 1 : 0;
379
380             return this.SearchPostsAll(stringComparer, startIndex, reverse: false);
381         }
382
383         /// <summary>
384         /// タブ内の発言を指定された条件で検索します
385         /// </summary>
386         /// <param name="stringComparer">発言内容、スクリーン名、名前と比較する条件。マッチしたら true を返す</param>
387         /// <param name="startIndex">検索を開始する位置</param>
388         /// <param name="reverse">インデックスの昇順に検索する場合は false、降順の場合は true</param>
389         /// <returns></returns>
390         public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer, int startIndex, bool reverse)
391         {
392             if (this.AllCount == 0)
393                 yield break;
394
395             var searchIndices = Enumerable.Empty<int>();
396
397             if (!reverse)
398             {
399                 // startindex ...末尾
400                 if (startIndex != this.AllCount - 1)
401                     searchIndices = MyCommon.CountUp(startIndex, this.AllCount - 1);
402
403                 // 先頭 ... (startIndex - 1)
404                 if (startIndex != 0)
405                     searchIndices = searchIndices.Concat(MyCommon.CountUp(0, startIndex - 1));
406             }
407             else
408             {
409                 // startIndex ... 先頭
410                 if (startIndex != 0)
411                     searchIndices = MyCommon.CountDown(startIndex, 0);
412
413                 // 末尾 ... (startIndex - 1)
414                 if (startIndex != this.AllCount - 1)
415                     searchIndices = searchIndices.Concat(MyCommon.CountDown(this.AllCount - 1, startIndex - 1));
416             }
417
418             foreach (var index in searchIndices)
419             {
420                 PostClass post;
421                 if (!this.Posts.TryGetValue(this.GetStatusIdAt(index), out post))
422                     continue;
423
424                 if (stringComparer(post.Nickname) || stringComparer(post.TextFromApi) || stringComparer(post.ScreenName))
425                 {
426                     yield return index;
427                 }
428             }
429         }
430     }
431 }