OSDN Git Service

FavoriteTweet/UnfavoriteTweetを使用したFav追加・削除に対応
[opentween/open-tween.git] / OpenTween / TimelineListViewDrawer.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.Drawing;
31 using System.Net.Http;
32 using System.Threading.Tasks;
33 using System.Windows.Forms;
34 using OpenTween.Models;
35 using OpenTween.OpenTweenCustomControl;
36
37 namespace OpenTween
38 {
39     public sealed class TimelineListViewDrawer : IDisposable
40     {
41         public bool IsDisposed { get; private set; } = false;
42
43         public ThemeManager Theme { get; set; }
44
45         public MyCommon.IconSizes IconSize { get; set; }
46
47         private bool Use2ColumnsMode
48             => this.IconSize == MyCommon.IconSizes.Icon48_2;
49
50         private int IconSizeNumeric
51             => this.IconSize switch
52             {
53                 MyCommon.IconSizes.Icon16 => 16,
54                 MyCommon.IconSizes.Icon24 => 26, // 24x24の場合に26と指定しているのはMSゴシック系フォントのための仕様
55                 MyCommon.IconSizes.Icon48 => 48,
56                 MyCommon.IconSizes.Icon48_2 => 48,
57                 _ => 0,
58             };
59
60         private readonly DetailsListView listView;
61         private readonly OTBaseForm parentForm;
62         private readonly TabModel tab;
63         private readonly TimelineListViewCache listViewCache;
64         private readonly ImageCache iconCache;
65         private readonly ImageList listViewImageList = new(); // ListViewItemの高さ変更用
66
67         public TimelineListViewDrawer(
68             DetailsListView listView,
69             TabModel tab,
70             TimelineListViewCache listViewCache,
71             ImageCache iconCache,
72             ThemeManager theme
73         )
74         {
75             this.listView = listView;
76             this.parentForm = (OTBaseForm)listView.FindForm();
77             this.tab = tab;
78             this.listViewCache = listViewCache;
79             this.iconCache = iconCache;
80             this.Theme = theme;
81
82             this.RegisterHandlers();
83             this.listView.SmallImageList = this.listViewImageList;
84             this.listView.OwnerDraw = true;
85         }
86
87         private void RegisterHandlers()
88         {
89             this.listView.DrawItem += this.ListView_DrawItem;
90             this.listView.DrawSubItem += this.ListView_DrawSubItem;
91         }
92
93         private void UnregisterHandlers()
94         {
95             this.listView.DrawItem -= this.ListView_DrawItem;
96             this.listView.DrawSubItem -= this.ListView_DrawSubItem;
97         }
98
99         public void UpdateItemHeight()
100         {
101             // ディスプレイの DPI 設定を考慮したサイズを設定する
102             var scaledIconHeight = this.IconSize != MyCommon.IconSizes.IconNone
103                 ? this.listView.LogicalToDeviceUnits(this.IconSizeNumeric)
104                 : 1;
105
106             // アイコンサイズと発言一覧のフォントサイズのどちらか大きい方を一件分の高さとする
107             var fontHeight = this.Theme.FontReaded.Height;
108             var itemHeight = Math.Max(scaledIconHeight, fontHeight);
109
110             this.listViewImageList.ImageSize = new(1, itemHeight);
111         }
112
113         private void DrawListViewItemIcon(DrawListViewItemEventArgs e)
114         {
115             if (this.IconSize == 0) return;
116
117             var item = e.Item;
118
119             // e.Bounds.Leftが常に0を指すから自前で計算
120             var itemRect = item.Bounds;
121             var col0 = e.Item.ListView.Columns[0];
122             itemRect.Width = col0.Width;
123
124             if (col0.DisplayIndex > 0)
125             {
126                 foreach (ColumnHeader clm in e.Item.ListView.Columns)
127                 {
128                     if (clm.DisplayIndex < col0.DisplayIndex)
129                         itemRect.X += clm.Width;
130                 }
131             }
132
133             // ディスプレイの DPI 設定を考慮したアイコンサイズ
134             var scaleFactor = this.listView.DeviceDpi / 96f;
135             var scaledIconSize = new SizeF(this.IconSizeNumeric * scaleFactor, this.IconSizeNumeric * scaleFactor).ToSize();
136             var scaledStateSize = new SizeF(16 * scaleFactor, 16 * scaleFactor).ToSize();
137
138             var iconRect = Rectangle.Intersect(new Rectangle(e.Item.GetBounds(ItemBoundsPortion.Icon).Location, scaledIconSize), itemRect);
139             iconRect.Offset(0, Math.Max(0, (itemRect.Height - scaledIconSize.Height) / 2));
140
141             var post = this.tab[item.Index];
142             this.DrawListViewItemProfileImage(e.Graphics, post, scaledIconSize, iconRect);
143
144             var stateRect = Rectangle.Intersect(new Rectangle(new Point(iconRect.X + scaledIconSize.Width + 2, iconRect.Y), scaledStateSize), itemRect);
145             this.DrawListViewItemStateIcon(e.Graphics, post, stateRect);
146         }
147
148         private void DrawListViewItemStateIcon(Graphics g, PostClass post, Rectangle stateRect)
149         {
150             if (post.StateIndex == -1)
151                 return;
152
153             if (stateRect.Width <= 0)
154                 return;
155
156             g.DrawIcon(this.GetPostStateIcon(post.StateIndex), stateRect);
157         }
158
159         private void DrawListViewItemProfileImage(Graphics g, PostClass post, Size scaledIconSize, Rectangle iconRect)
160         {
161             if (scaledIconSize.Width <= 0)
162                 return;
163
164             var normalImageUrl = post.ImageUrl;
165             if (MyCommon.IsNullOrEmpty(normalImageUrl))
166                 return;
167
168             var sizeName = Twitter.DecideProfileImageSize(scaledIconSize.Width);
169             var cachedImage = this.iconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, sizeName);
170
171             if (cachedImage != null)
172             {
173                 g.FillRectangle(Brushes.White, iconRect);
174                 g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
175                 try
176                 {
177                     g.DrawImage(cachedImage.Image, iconRect);
178                 }
179                 catch (ArgumentException)
180                 {
181                 }
182             }
183             else
184             {
185                 // キャッシュにない画像の場合は読み込みが完了してから再描画する
186                 async Task RefreshProfileImageLazy()
187                 {
188                     await this.LoadProfileImage(normalImageUrl, sizeName);
189
190                     if (this.listView.IsDisposed)
191                         return;
192
193                     if (this.listView.VirtualListSize == 0)
194                         return;
195
196                     // ロード中に index の指す行が変化している可能性がある
197                     var newIndex = this.tab.IndexOf(post.StatusId);
198                     if (newIndex != -1)
199                         this.listView.RedrawItems(newIndex, newIndex, true);
200                 }
201
202                 _ = RefreshProfileImageLazy();
203             }
204         }
205
206         private async Task LoadProfileImage(string normalImageUrl, string sizeName)
207         {
208             try
209             {
210                 var imageUrl = Twitter.CreateProfileImageUrl(normalImageUrl, sizeName);
211                 await this.iconCache.DownloadImageAsync(imageUrl);
212             }
213             catch (InvalidImageException)
214             {
215                 return;
216             }
217             catch (HttpRequestException)
218             {
219                 return;
220             }
221             catch (OperationCanceledException)
222             {
223                 return;
224             }
225         }
226
227         private Icon GetPostStateIcon(int stateIndex)
228         {
229             return stateIndex switch
230             {
231                 0 => Properties.Resources.PostState00,
232                 1 => Properties.Resources.PostState01,
233                 2 => Properties.Resources.PostState02,
234                 3 => Properties.Resources.PostState03,
235                 4 => Properties.Resources.PostState04,
236                 5 => Properties.Resources.PostState05,
237                 6 => Properties.Resources.PostState06,
238                 7 => Properties.Resources.PostState07,
239                 8 => Properties.Resources.PostState08,
240                 9 => Properties.Resources.PostState09,
241                 10 => Properties.Resources.PostState10,
242                 11 => Properties.Resources.PostState11,
243                 12 => Properties.Resources.PostState12,
244                 13 => Properties.Resources.PostState13,
245                 14 => Properties.Resources.PostState14,
246                 _ => throw new IndexOutOfRangeException(),
247             };
248         }
249
250         private Brush GetBackColorBrush(ListItemBackColor backColor)
251         {
252             return backColor switch
253             {
254                 ListItemBackColor.Self => this.Theme.BrushSelf,
255                 ListItemBackColor.AtSelf => this.Theme.BrushAtSelf,
256                 ListItemBackColor.Target => this.Theme.BrushTarget,
257                 ListItemBackColor.AtTarget => this.Theme.BrushAtTarget,
258                 ListItemBackColor.AtFromTarget => this.Theme.BrushAtFromTarget,
259                 ListItemBackColor.AtTo => this.Theme.BrushAtTo,
260                 _ => this.Theme.BrushListBackcolor,
261             };
262         }
263
264         private Color GetForeColor(ListItemForeColor foreColor)
265         {
266             return foreColor switch
267             {
268                 ListItemForeColor.Fav => this.Theme.ColorFav,
269                 ListItemForeColor.Retweet => this.Theme.ColorRetweet,
270                 ListItemForeColor.OWL => this.Theme.ColorOWL,
271                 ListItemForeColor.Unread => this.Theme.ColorUnread,
272                 _ => this.Theme.ColorRead,
273             };
274         }
275
276         private Font GetFont(ListItemFont font)
277         {
278             return font switch
279             {
280                 ListItemFont.Unread => this.Theme.FontUnread,
281                 _ => this.Theme.FontReaded,
282             };
283         }
284
285         private Font GetFontBold(ListItemFont font)
286         {
287             return font switch
288             {
289                 ListItemFont.Unread => this.Theme.FontUnreadBold,
290                 _ => this.Theme.FontReadedBold,
291             };
292         }
293
294         private void ListView_DrawItem(object sender, DrawListViewItemEventArgs e)
295         {
296             if (e.State == 0) return;
297             e.DrawDefault = false;
298
299             var style = this.listViewCache.GetStyle(e.ItemIndex);
300
301             Brush brs2;
302             if (!e.Item.Selected) // e.ItemStateでうまく判定できない???
303             {
304                 brs2 = this.GetBackColorBrush(style.BackColor);
305             }
306             else
307             {
308                 // 選択中の行
309                 if (((Control)sender).Focused)
310                     brs2 = this.Theme.BrushHighLight;
311                 else
312                     brs2 = this.Theme.BrushDeactiveSelection;
313             }
314             e.Graphics.FillRectangle(brs2, e.Bounds);
315             e.DrawFocusRectangle();
316             this.DrawListViewItemIcon(e);
317         }
318
319         private void ListView_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
320         {
321             if (e.ItemState == 0) return;
322
323             if (e.ColumnIndex > 0)
324             {
325                 // アイコン以外の列
326                 var post = this.tab[e.ItemIndex];
327                 var style = this.listViewCache.GetStyle(e.ItemIndex);
328                 var font = this.GetFont(style.Font);
329
330                 RectangleF rct = e.Bounds;
331                 rct.Width = e.Header.Width;
332                 var fontHeight = font.Height;
333                 if (this.Use2ColumnsMode)
334                 {
335                     rct.Y += fontHeight;
336                     rct.Height -= fontHeight;
337                 }
338
339                 var drawLineCount = Math.Max(1, Math.DivRem((int)rct.Height, fontHeight, out var heightDiff));
340
341                 // フォントの高さの半分を足してるのは保険。無くてもいいかも。
342                 if (this.Use2ColumnsMode || drawLineCount > 1)
343                 {
344                     if (heightDiff < fontHeight * 0.7)
345                     {
346                         // 最終行が70%以上欠けていたら、最終行は表示しない
347                         rct.Height = (fontHeight * drawLineCount) - 1;
348                     }
349                     else
350                     {
351                         drawLineCount += 1;
352                     }
353                 }
354
355                 if (rct.Width > 0)
356                 {
357                     Color color;
358                     if (e.Item.Selected)
359                     {
360                         color = ((Control)sender).Focused
361                             ? this.Theme.ColorHighLight
362                             : this.Theme.ColorUnread;
363                     }
364                     else
365                     {
366                         color = this.GetForeColor(style.ForeColor);
367                     }
368
369                     if (this.Use2ColumnsMode)
370                     {
371                         var rctB = e.Bounds;
372                         rctB.Width = e.Header.Width;
373                         rctB.Height = fontHeight;
374
375                         var fontBold = this.GetFontBold(style.Font);
376
377                         var formatFlags1 = TextFormatFlags.WordBreak |
378                             TextFormatFlags.EndEllipsis |
379                             TextFormatFlags.GlyphOverhangPadding |
380                             TextFormatFlags.NoPrefix;
381
382                         TextRenderer.DrawText(
383                             e.Graphics,
384                             post.IsDeleted ? "(DELETED)" : post.TextSingleLine,
385                             font,
386                             Rectangle.Round(rct),
387                             color,
388                             formatFlags1);
389
390                         var formatFlags2 = TextFormatFlags.SingleLine |
391                             TextFormatFlags.EndEllipsis |
392                             TextFormatFlags.GlyphOverhangPadding |
393                             TextFormatFlags.NoPrefix;
394
395                         TextRenderer.DrawText(
396                             e.Graphics,
397                             e.Item.SubItems[4].Text + " / " + e.Item.SubItems[1].Text + " (" + e.Item.SubItems[3].Text + ") " + e.Item.SubItems[5].Text + e.Item.SubItems[6].Text + " [" + e.Item.SubItems[7].Text + "]",
398                             fontBold,
399                             rctB,
400                             color,
401                             formatFlags2);
402                     }
403                     else
404                     {
405                         string text;
406                         if (e.ColumnIndex != 2)
407                             text = e.SubItem.Text;
408                         else
409                             text = post.IsDeleted ? "(DELETED)" : post.TextSingleLine;
410
411                         if (drawLineCount == 1)
412                         {
413                             var formatFlags = TextFormatFlags.SingleLine |
414                                 TextFormatFlags.EndEllipsis |
415                                 TextFormatFlags.GlyphOverhangPadding |
416                                 TextFormatFlags.NoPrefix |
417                                 TextFormatFlags.VerticalCenter;
418
419                             TextRenderer.DrawText(
420                                 e.Graphics,
421                                 text,
422                                 font,
423                                 Rectangle.Round(rct),
424                                 color,
425                                 formatFlags);
426                         }
427                         else
428                         {
429                             var formatFlags = TextFormatFlags.WordBreak |
430                                 TextFormatFlags.EndEllipsis |
431                                 TextFormatFlags.GlyphOverhangPadding |
432                                 TextFormatFlags.NoPrefix;
433
434                             TextRenderer.DrawText(
435                                 e.Graphics,
436                                 text,
437                                 font,
438                                 Rectangle.Round(rct),
439                                 color,
440                                 formatFlags);
441                         }
442                     }
443                 }
444             }
445         }
446
447         public void Dispose()
448         {
449             if (this.IsDisposed)
450                 return;
451
452             this.UnregisterHandlers();
453             this.listView.SmallImageList = null;
454             this.listViewImageList.Dispose();
455             this.IsDisposed = true;
456         }
457     }
458 }