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.
31 using System.Net.Http;
32 using System.Threading.Tasks;
33 using System.Windows.Forms;
34 using OpenTween.Models;
35 using OpenTween.OpenTweenCustomControl;
39 public sealed class TimelineListViewDrawer : IDisposable
41 public bool IsDisposed { get; private set; } = false;
43 public ThemeManager Theme { get; set; }
45 public MyCommon.IconSizes IconSize { get; set; }
47 private bool Use2ColumnsMode
48 => this.IconSize == MyCommon.IconSizes.Icon48_2;
50 private int IconSizeNumeric
51 => this.IconSize switch
53 MyCommon.IconSizes.Icon16 => 16,
54 MyCommon.IconSizes.Icon24 => 26, // 24x24の場合に26と指定しているのはMSゴシック系フォントのための仕様
55 MyCommon.IconSizes.Icon48 => 48,
56 MyCommon.IconSizes.Icon48_2 => 48,
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の高さ変更用
67 public TimelineListViewDrawer(
68 DetailsListView listView,
70 TimelineListViewCache listViewCache,
75 this.listView = listView;
76 this.parentForm = (OTBaseForm)listView.FindForm();
78 this.listViewCache = listViewCache;
79 this.iconCache = iconCache;
82 this.RegisterHandlers();
83 this.listView.SmallImageList = this.listViewImageList;
84 this.listView.OwnerDraw = true;
87 private void RegisterHandlers()
89 this.listView.DrawItem += this.ListView_DrawItem;
90 this.listView.DrawSubItem += this.ListView_DrawSubItem;
93 private void UnregisterHandlers()
95 this.listView.DrawItem -= this.ListView_DrawItem;
96 this.listView.DrawSubItem -= this.ListView_DrawSubItem;
99 public void UpdateItemHeight()
101 // ディスプレイの DPI 設定を考慮したサイズを設定する
102 var scaledIconHeight = this.IconSize != MyCommon.IconSizes.IconNone
103 ? this.listView.LogicalToDeviceUnits(this.IconSizeNumeric)
106 // アイコンサイズと発言一覧のフォントサイズのどちらか大きい方を一件分の高さとする
107 var fontHeight = this.Theme.FontReaded.Height;
108 var itemHeight = Math.Max(scaledIconHeight, fontHeight);
110 this.listViewImageList.ImageSize = new(1, itemHeight);
113 private void DrawListViewItemIcon(DrawListViewItemEventArgs e)
115 if (this.IconSize == 0) return;
119 // e.Bounds.Leftが常に0を指すから自前で計算
120 var itemRect = item.Bounds;
121 var col0 = e.Item.ListView.Columns[0];
122 itemRect.Width = col0.Width;
124 if (col0.DisplayIndex > 0)
126 foreach (ColumnHeader clm in e.Item.ListView.Columns)
128 if (clm.DisplayIndex < col0.DisplayIndex)
129 itemRect.X += clm.Width;
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();
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));
141 var post = this.tab[item.Index];
142 this.DrawListViewItemProfileImage(e.Graphics, post, scaledIconSize, iconRect);
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);
148 private void DrawListViewItemStateIcon(Graphics g, PostClass post, Rectangle stateRect)
150 if (post.StateIndex == -1)
153 if (stateRect.Width <= 0)
156 g.DrawIcon(this.GetPostStateIcon(post.StateIndex), stateRect);
159 private void DrawListViewItemProfileImage(Graphics g, PostClass post, Size scaledIconSize, Rectangle iconRect)
161 if (scaledIconSize.Width <= 0)
164 var normalImageUrl = post.ImageUrl;
165 if (MyCommon.IsNullOrEmpty(normalImageUrl))
168 var sizeName = Twitter.DecideProfileImageSize(scaledIconSize.Width);
169 var cachedImage = this.iconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, sizeName);
171 if (cachedImage != null)
173 g.FillRectangle(Brushes.White, iconRect);
174 g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
177 g.DrawImage(cachedImage.Image, iconRect);
179 catch (ArgumentException)
185 // キャッシュにない画像の場合は読み込みが完了してから再描画する
186 async Task RefreshProfileImageLazy()
188 await this.LoadProfileImage(normalImageUrl, sizeName);
190 if (this.listView.IsDisposed)
193 if (this.listView.VirtualListSize == 0)
196 // ロード中に index の指す行が変化している可能性がある
197 var newIndex = this.tab.IndexOf(post.StatusId);
199 this.listView.RedrawItems(newIndex, newIndex, true);
202 _ = RefreshProfileImageLazy();
206 private async Task LoadProfileImage(string normalImageUrl, string sizeName)
210 var imageUrl = Twitter.CreateProfileImageUrl(normalImageUrl, sizeName);
211 await this.iconCache.DownloadImageAsync(imageUrl);
213 catch (InvalidImageException)
217 catch (HttpRequestException)
221 catch (OperationCanceledException)
227 private Icon GetPostStateIcon(int stateIndex)
229 return stateIndex switch
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(),
250 private Brush GetBackColorBrush(ListItemBackColor backColor)
252 return backColor switch
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,
264 private Color GetForeColor(ListItemForeColor foreColor)
266 return foreColor switch
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,
276 private Font GetFont(ListItemFont font)
280 ListItemFont.Unread => this.Theme.FontUnread,
281 _ => this.Theme.FontReaded,
285 private Font GetFontBold(ListItemFont font)
289 ListItemFont.Unread => this.Theme.FontUnreadBold,
290 _ => this.Theme.FontReadedBold,
294 private void ListView_DrawItem(object sender, DrawListViewItemEventArgs e)
296 if (e.State == 0) return;
297 e.DrawDefault = false;
299 var style = this.listViewCache.GetStyle(e.ItemIndex);
302 if (!e.Item.Selected) // e.ItemStateでうまく判定できない???
304 brs2 = this.GetBackColorBrush(style.BackColor);
309 if (((Control)sender).Focused)
310 brs2 = this.Theme.BrushHighLight;
312 brs2 = this.Theme.BrushDeactiveSelection;
314 e.Graphics.FillRectangle(brs2, e.Bounds);
315 e.DrawFocusRectangle();
316 this.DrawListViewItemIcon(e);
319 private void ListView_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
321 if (e.ItemState == 0) return;
323 if (e.ColumnIndex > 0)
326 var post = this.tab[e.ItemIndex];
327 var style = this.listViewCache.GetStyle(e.ItemIndex);
328 var font = this.GetFont(style.Font);
330 RectangleF rct = e.Bounds;
331 rct.Width = e.Header.Width;
332 var fontHeight = font.Height;
333 if (this.Use2ColumnsMode)
336 rct.Height -= fontHeight;
339 var drawLineCount = Math.Max(1, Math.DivRem((int)rct.Height, fontHeight, out var heightDiff));
341 // フォントの高さの半分を足してるのは保険。無くてもいいかも。
342 if (this.Use2ColumnsMode || drawLineCount > 1)
344 if (heightDiff < fontHeight * 0.7)
346 // 最終行が70%以上欠けていたら、最終行は表示しない
347 rct.Height = (fontHeight * drawLineCount) - 1;
360 color = ((Control)sender).Focused
361 ? this.Theme.ColorHighLight
362 : this.Theme.ColorUnread;
366 color = this.GetForeColor(style.ForeColor);
369 if (this.Use2ColumnsMode)
372 rctB.Width = e.Header.Width;
373 rctB.Height = fontHeight;
375 var fontBold = this.GetFontBold(style.Font);
377 var formatFlags1 = TextFormatFlags.WordBreak |
378 TextFormatFlags.EndEllipsis |
379 TextFormatFlags.GlyphOverhangPadding |
380 TextFormatFlags.NoPrefix;
382 TextRenderer.DrawText(
384 post.IsDeleted ? "(DELETED)" : post.TextSingleLine,
386 Rectangle.Round(rct),
390 var formatFlags2 = TextFormatFlags.SingleLine |
391 TextFormatFlags.EndEllipsis |
392 TextFormatFlags.GlyphOverhangPadding |
393 TextFormatFlags.NoPrefix;
395 TextRenderer.DrawText(
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 + "]",
406 if (e.ColumnIndex != 2)
407 text = e.SubItem.Text;
409 text = post.IsDeleted ? "(DELETED)" : post.TextSingleLine;
411 if (drawLineCount == 1)
413 var formatFlags = TextFormatFlags.SingleLine |
414 TextFormatFlags.EndEllipsis |
415 TextFormatFlags.GlyphOverhangPadding |
416 TextFormatFlags.NoPrefix |
417 TextFormatFlags.VerticalCenter;
419 TextRenderer.DrawText(
423 Rectangle.Round(rct),
429 var formatFlags = TextFormatFlags.WordBreak |
430 TextFormatFlags.EndEllipsis |
431 TextFormatFlags.GlyphOverhangPadding |
432 TextFormatFlags.NoPrefix;
434 TextRenderer.DrawText(
438 Rectangle.Round(rct),
447 public void Dispose()
452 this.UnregisterHandlers();
453 this.listView.SmallImageList = null;
454 this.listViewImageList.Dispose();
455 this.IsDisposed = true;