OSDN Git Service

TabModel.SelectedIndex プロパティを追加, TweenMain._curItemIndex フィールドを廃止
[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 using OpenTween.Setting;
37
38 namespace OpenTween.Models
39 {
40     public abstract class TabModel
41     {
42         public string TabName { get; set; }
43
44         public bool UnreadManage { get; set; } = true;
45         public bool Protected { get; set; }
46         public bool Notify { get; set; } = true;
47         public string SoundFile { get; set; } = "";
48
49         public ComparerMode SortMode { get; private set; }
50         public SortOrder SortOrder { get; private set; }
51
52         public long OldestId { get; set; } = long.MaxValue;
53         public long SinceId { get; set; }
54
55         public abstract MyCommon.TabUsageType TabType { get; }
56
57         public virtual ConcurrentDictionary<long, PostClass> Posts
58             => TabInformations.GetInstance().Posts;
59
60         public int AllCount => this._ids.Count;
61         public long[] StatusIds => this._ids.ToArray();
62
63         public bool IsDefaultTabType => this.TabType.IsDefault();
64         public bool IsDistributableTabType => this.TabType.IsDistributable();
65         public bool IsInnerStorageTabType => this.TabType.IsInnerStorage();
66
67         /// <summary>
68         /// 次回起動時にも保持されるタブか(SettingTabsに保存されるか)
69         /// </summary>
70         public virtual bool IsPermanentTabType => true;
71
72         public long[] SelectedStatusIds
73             => this.selectedStatusIds.ToArray();
74
75         public long SelectedStatusId
76             => this.selectedStatusIds.DefaultIfEmpty(-1).First();
77
78         public PostClass[] SelectedPosts
79             => this.selectedStatusIds.Select(x => this.Posts[x]).ToArray();
80
81         public PostClass SelectedPost
82             => this.selectedStatusIds.Select(x => this.Posts[x]).FirstOrDefault();
83
84         public int SelectedIndex
85         {
86             get
87             {
88                 var statusId = this.SelectedStatusId;
89                 return statusId != -1 ? this.IndexOf(statusId) : -1;
90             }
91         }
92
93         private IndexedSortedSet<long> _ids = new IndexedSortedSet<long>();
94         private ConcurrentQueue<TemporaryId> addQueue = new ConcurrentQueue<TemporaryId>();
95         private ConcurrentQueue<long> removeQueue = new ConcurrentQueue<long>();
96         private SortedSet<long> unreadIds = new SortedSet<long>();
97         private List<long> selectedStatusIds = new List<long>();
98
99         private readonly object _lockObj = new object();
100
101         protected TabModel(string tabName)
102             => this.TabName = tabName;
103
104         public abstract Task RefreshAsync(Twitter tw, bool backward, bool startup, IProgress<string> progress);
105
106         private struct TemporaryId
107         {
108             public long StatusId { get; }
109             public bool Read { get; }
110
111             public TemporaryId(long statusId, bool read)
112             {
113                 this.StatusId = statusId;
114                 this.Read = read;
115             }
116         }
117
118         public virtual void AddPostQueue(PostClass post)
119         {
120             if (!this.Posts.ContainsKey(post.StatusId))
121                 throw new ArgumentException("Specified post not exists in storage", nameof(post));
122
123             this.addQueue.Enqueue(new TemporaryId(post.StatusId, post.IsRead));
124         }
125
126         //無条件に追加
127         internal bool AddPostImmediately(long statusId, bool read)
128         {
129             if (!this._ids.Add(statusId))
130                 return false;
131
132             if (!read)
133                 this.unreadIds.Add(statusId);
134
135             return true;
136         }
137
138         public IReadOnlyList<long> AddSubmit()
139         {
140             var addedIds = new List<long>();
141
142             while (this.addQueue.TryDequeue(out var tId))
143             {
144                 if (this.AddPostImmediately(tId.StatusId, tId.Read))
145                     addedIds.Add(tId.StatusId);
146             }
147
148             return addedIds;
149         }
150
151         public virtual void EnqueueRemovePost(long statusId, bool setIsDeleted)
152             => this.removeQueue.Enqueue(statusId);
153
154         public virtual bool RemovePostImmediately(long statusId)
155         {
156             if (!this._ids.Remove(statusId))
157                 return false;
158
159             this.unreadIds.Remove(statusId);
160             this.selectedStatusIds.Remove(statusId);
161             return true;
162         }
163
164         public IReadOnlyList<long> RemoveSubmit()
165         {
166             var removedIds = new List<long>();
167
168             while (this.removeQueue.TryDequeue(out var statusId))
169             {
170                 if (this.RemovePostImmediately(statusId))
171                     removedIds.Add(statusId);
172             }
173
174             return removedIds;
175         }
176
177         public void SelectPosts(int[] indices)
178         {
179             bool IsValidIndex(int index)
180                 => index >= 0 && index < this.AllCount;
181
182             var firstErrorId = indices.FirstOrDefault(x => !IsValidIndex(x));
183             if (firstErrorId != default)
184                 throw new ArgumentOutOfRangeException($"Invalid index: {firstErrorId}", nameof(indices));
185
186             var statusIds = indices.Select(x => this.GetStatusIdAt(x)).ToList();
187             this.selectedStatusIds = statusIds;
188         }
189
190         public virtual void ClearIDs()
191         {
192             this._ids.Clear();
193             this.unreadIds.Clear();
194             this.selectedStatusIds.Clear();
195
196             Interlocked.Exchange(ref this.addQueue, new ConcurrentQueue<TemporaryId>());
197         }
198
199         /// <summary>
200         /// タブ更新時に使用する SinceId, OldestId をリセットする
201         /// </summary>
202         public void ResetFetchIds()
203         {
204             this.SinceId = 0L;
205             this.OldestId = long.MaxValue;
206         }
207
208         /// <summary>
209         /// ソート対象のフィールドとソート順を設定し、ソートを実行します
210         /// </summary>
211         public void SetSortMode(ComparerMode mode, SortOrder sortOrder)
212         {
213             this.SortMode = mode;
214             this.SortOrder = sortOrder;
215
216             this.ApplySortMode();
217         }
218
219         private void ApplySortMode()
220         {
221             var sign = this.SortOrder == SortOrder.Ascending ? 1 : -1;
222
223             Comparison<long> comparison;
224             if (this.SortMode == ComparerMode.Id)
225             {
226                 comparison = (x, y) => sign * x.CompareTo(y);
227             }
228             else
229             {
230                 Comparison<PostClass> postComparison;
231                 switch (this.SortMode)
232                 {
233                     default:
234                     case ComparerMode.Data:
235                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.TextFromApi, y?.TextFromApi);
236                         break;
237                     case ComparerMode.Name:
238                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.ScreenName, y?.ScreenName);
239                         break;
240                     case ComparerMode.Nickname:
241                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Nickname, y?.Nickname);
242                         break;
243                     case ComparerMode.Source:
244                         postComparison = (x, y) => Comparer<string>.Default.Compare(x?.Source, y?.Source);
245                         break;
246                 }
247
248                 comparison = (x, y) =>
249                 {
250                     this.Posts.TryGetValue(x, out var xPost);
251                     this.Posts.TryGetValue(y, out var yPost);
252
253                     var compare = sign * postComparison(xPost, yPost);
254                     if (compare != 0)
255                         return compare;
256
257                     // 同値であれば status_id で比較する
258                     return sign * x.CompareTo(y);
259                 };
260             }
261
262             var comparer = Comparer<long>.Create(comparison);
263
264             this._ids = new IndexedSortedSet<long>(this._ids, comparer);
265             this.unreadIds = new SortedSet<long>(this.unreadIds, comparer);
266         }
267
268         /// <summary>
269         /// 次に表示する未読ツイートのIDを返します。
270         /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
271         /// </summary>
272         public long NextUnreadId
273         {
274             get
275             {
276                 if (!this.UnreadManage || !SettingManager.Common.UnreadManage)
277                     return -1L;
278
279                 if (this.unreadIds.Count == 0)
280                     return -1L;
281
282                 // unreadIds はリストのインデックス番号順に並んでいるため、
283                 // 例えば ID 順の整列であれば昇順なら上から、降順なら下から順に返せば過去→現在の順になる
284                 return this.SortOrder == SortOrder.Ascending ? this.unreadIds.Min : this.unreadIds.Max;
285             }
286         }
287
288         /// <summary>
289         /// 次に表示する未読ツイートのインデックス番号を返します。
290         /// ただし、未読がない場合または UnreadManage が false の場合は -1 を返します
291         /// </summary>
292         public int NextUnreadIndex
293         {
294             get
295             {
296                 var unreadId = this.NextUnreadId;
297                 return unreadId != -1 ? this.IndexOf(unreadId) : -1;
298             }
299         }
300
301         /// <summary>
302         /// 未読ツイートの件数を返します。
303         /// ただし、未読がない場合または UnreadManage が false の場合は 0 を返します
304         /// </summary>
305         public int UnreadCount
306         {
307             get
308             {
309                 if (!this.UnreadManage || !SettingManager.Common.UnreadManage)
310                     return 0;
311
312                 return this.unreadIds.Count;
313             }
314         }
315
316         /// <summary>
317         /// 未読ツイートの ID を配列で返します
318         /// </summary>
319         public long[] GetUnreadIds()
320         {
321             lock (this._lockObj)
322                 return this.unreadIds.ToArray();
323         }
324
325         /// <summary>
326         /// タブ内の既読状態を変更します
327         /// </summary>
328         /// <remarks>
329         /// 全タブを横断して既読状態を変える TabInformation.SetReadAllTab() の内部で呼び出されるメソッドです
330         /// </remarks>
331         /// <param name="statusId">変更するツイートのID</param>
332         /// <param name="read">既読状態</param>
333         /// <returns>既読状態に変化があれば true、変化がなければ false</returns>
334         internal virtual bool SetReadState(long statusId, bool read)
335         {
336             if (!this._ids.Contains(statusId))
337                 throw new ArgumentOutOfRangeException(nameof(statusId));
338
339             if (read)
340                 return this.unreadIds.Remove(statusId);
341             else
342                 return this.unreadIds.Add(statusId);
343         }
344
345         public bool Contains(long statusId)
346             => this._ids.Contains(statusId);
347
348         public PostClass this[int index]
349         {
350             get
351             {
352                 if (!this.Posts.TryGetValue(this.GetStatusIdAt(index), out var post))
353                     throw new ArgumentOutOfRangeException(nameof(index), "Post not exists");
354
355                 return post;
356             }
357         }
358
359         public PostClass[] this[int startIndex, int endIndex]
360         {
361             get
362             {
363                 if (startIndex < 0)
364                     throw new ArgumentOutOfRangeException(nameof(startIndex));
365                 if (endIndex >= this.AllCount)
366                     throw new ArgumentOutOfRangeException(nameof(endIndex));
367                 if (startIndex > endIndex)
368                     throw new ArgumentException($"{nameof(startIndex)} must be equal to or less than {nameof(endIndex)}.", nameof(startIndex));
369
370                 var length = endIndex - startIndex + 1;
371                 var posts = new PostClass[length];
372
373                 var i = 0;
374                 foreach (var idx in Enumerable.Range(startIndex, length))
375                 {
376                     var statusId = this.GetStatusIdAt(idx);
377                     this.Posts.TryGetValue(statusId, out posts[i++]);
378                 }
379
380                 return posts;
381             }
382         }
383
384         public long[] GetStatusIdAt(IEnumerable<int> indexes)
385             => indexes.Select(x => this.GetStatusIdAt(x)).ToArray();
386
387         public long GetStatusIdAt(int index)
388             => this._ids[index];
389
390         public int[] IndexOf(long[] statusIds)
391         {
392             if (statusIds == null)
393                 throw new ArgumentNullException(nameof(statusIds));
394
395             return statusIds.Select(x => this.IndexOf(x)).ToArray();
396         }
397
398         public int IndexOf(long statusId)
399             => this._ids.IndexOf(statusId);
400
401         public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer)
402             => this.SearchPostsAll(stringComparer, reverse: false);
403
404         public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer, int startIndex)
405             => this.SearchPostsAll(stringComparer, startIndex, reverse: false);
406
407         public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer, bool reverse)
408         {
409             var startIndex = reverse ? this.AllCount - 1 : 0;
410
411             return this.SearchPostsAll(stringComparer, startIndex, reverse: false);
412         }
413
414         /// <summary>
415         /// タブ内の発言を指定された条件で検索します
416         /// </summary>
417         /// <param name="stringComparer">発言内容、スクリーン名、名前と比較する条件。マッチしたら true を返す</param>
418         /// <param name="startIndex">検索を開始する位置</param>
419         /// <param name="reverse">インデックスの昇順に検索する場合は false、降順の場合は true</param>
420         /// <returns></returns>
421         public IEnumerable<int> SearchPostsAll(Func<string, bool> stringComparer, int startIndex, bool reverse)
422         {
423             if (this.AllCount == 0)
424                 yield break;
425
426             IEnumerable<int> searchIndices;
427
428             if (!reverse)
429                 searchIndices = MyCommon.CircularCountUp(this.AllCount, startIndex);
430             else
431                 searchIndices = MyCommon.CircularCountDown(this.AllCount, startIndex);
432
433             foreach (var index in searchIndices)
434             {
435                 if (!this.Posts.TryGetValue(this.GetStatusIdAt(index), out var post))
436                     continue;
437
438                 if (stringComparer(post.Nickname) || stringComparer(post.TextFromApi) || stringComparer(post.ScreenName))
439                 {
440                     yield return index;
441                 }
442             }
443         }
444     }
445 }