OSDN Git Service

MemoryImage.Clone() で内部の byte[] をコピーせずに再利用する
[opentween/open-tween.git] / OpenTween / TweetDetailsView.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.Collections.Generic;
31 using System.ComponentModel;
32 using System.Data;
33 using System.Drawing;
34 using System.IO;
35 using System.Linq;
36 using System.Net;
37 using System.Net.Http;
38 using System.Text;
39 using System.Text.RegularExpressions;
40 using System.Threading;
41 using System.Threading.Tasks;
42 using System.Windows.Forms;
43 using OpenTween.Models;
44 using OpenTween.Setting;
45
46 namespace OpenTween
47 {
48     public partial class TweetDetailsView : UserControl
49     {
50         public TweenMain Owner { get; set; } = null!;
51
52         /// <summary>プロフィール画像のキャッシュ</summary>
53         public ImageCache IconCache { get; set; } = null!;
54
55         /// <summary><see cref="PostClass"/> のダンプを表示するか</summary>
56         public bool DumpPostClass { get; set; }
57
58         /// <summary>現在表示中の発言</summary>
59         public PostClass? CurrentPost { get; private set; }
60
61         [DefaultValue(false)]
62         public new bool TabStop
63         {
64             get => base.TabStop;
65             set => base.TabStop = value;
66         }
67
68         /// <summary>ステータスバーに表示するテキストの変化を通知するイベント</summary>
69         public event EventHandler<TweetDetailsViewStatusChengedEventArgs>? StatusChanged;
70
71         /// <summary><see cref="ContextMenuPostBrowser"/> 展開時の <see cref="PostBrowser"/>.StatusText を保持するフィールド</summary>
72         private string postBrowserStatusText = "";
73
74         public TweetDetailsView()
75         {
76             this.InitializeComponent();
77
78             this.TabStop = false;
79
80             // 発言詳細部の初期化
81             this.AuthorNameLinkLabel.Text = "";
82             this.RetweetedByLinkLabel.Text = "";
83             this.DateTimeLabel.Text = "";
84             this.SourceLinkLabel.Text = "";
85
86             new InternetSecurityManager(this.PostBrowser);
87             this.PostBrowser.AllowWebBrowserDrop = false;  // COMException を回避するため、ActiveX の初期化が終わってから設定する
88         }
89
90         public void ClearPostBrowser()
91             => this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml("");
92
93         public async Task ShowPostDetails(PostClass post)
94         {
95             this.CurrentPost = post;
96
97             var loadTasks = new List<Task>();
98
99             using (ControlTransaction.Update(this.TableLayoutPanel1))
100             {
101                 this.SourceLinkLabel.Text = post.Source;
102                 this.SourceLinkLabel.TabStop = false; // Text を更新すると勝手に true にされる
103
104                 string nameText;
105                 if (post.IsDm)
106                 {
107                     if (post.IsOwl)
108                         nameText = "DM FROM <- ";
109                     else
110                         nameText = "DM TO -> ";
111                 }
112                 else
113                 {
114                     nameText = "";
115                 }
116                 nameText += post.ScreenName + "/" + post.Nickname;
117                 this.AuthorNameLinkLabel.Text = nameText;
118
119                 if (post.RetweetedId != null)
120                 {
121                     this.RetweetedByLinkLabel.Visible = true;
122                     this.RetweetedByLinkLabel.Text = $"(RT:{post.RetweetedBy})";
123                 }
124                 else
125                 {
126                     this.RetweetedByLinkLabel.Visible = false;
127                     this.RetweetedByLinkLabel.Text = "";
128                 }
129
130                 var nameForeColor = SystemColors.ControlText;
131                 if (post.IsOwl && (SettingManager.Common.OneWayLove || post.IsDm))
132                     nameForeColor = SettingManager.Local.ColorOWL;
133                 if (post.RetweetedId != null)
134                     nameForeColor = SettingManager.Local.ColorRetweet;
135                 if (post.IsFav)
136                     nameForeColor = SettingManager.Local.ColorFav;
137
138                 this.AuthorNameLinkLabel.LinkColor = nameForeColor;
139                 this.AuthorNameLinkLabel.ActiveLinkColor = nameForeColor;
140                 this.RetweetedByLinkLabel.LinkColor = nameForeColor;
141                 this.RetweetedByLinkLabel.ActiveLinkColor = nameForeColor;
142
143                 loadTasks.Add(this.SetUserPictureAsync(post.ImageUrl));
144
145                 this.DateTimeLabel.Text = post.CreatedAt.ToLocalTimeString();
146             }
147
148             if (this.DumpPostClass)
149             {
150                 var sb = new StringBuilder(512);
151
152                 sb.Append("-----Start PostClass Dump<br>");
153                 sb.AppendFormat("TextFromApi           : {0}<br>", post.TextFromApi);
154                 sb.AppendFormat("(PlainText)    : <xmp>{0}</xmp><br>", post.TextFromApi);
155                 sb.AppendFormat("StatusId             : {0}<br>", post.StatusId);
156                 sb.AppendFormat("ImageUrl       : {0}<br>", post.ImageUrl);
157                 sb.AppendFormat("InReplyToStatusId    : {0}<br>", post.InReplyToStatusId);
158                 sb.AppendFormat("InReplyToUser  : {0}<br>", post.InReplyToUser);
159                 sb.AppendFormat("IsDM           : {0}<br>", post.IsDm);
160                 sb.AppendFormat("IsFav          : {0}<br>", post.IsFav);
161                 sb.AppendFormat("IsMark         : {0}<br>", post.IsMark);
162                 sb.AppendFormat("IsMe           : {0}<br>", post.IsMe);
163                 sb.AppendFormat("IsOwl          : {0}<br>", post.IsOwl);
164                 sb.AppendFormat("IsProtect      : {0}<br>", post.IsProtect);
165                 sb.AppendFormat("IsRead         : {0}<br>", post.IsRead);
166                 sb.AppendFormat("IsReply        : {0}<br>", post.IsReply);
167
168                 foreach (var nm in post.ReplyToList.Select(x => x.ScreenName))
169                 {
170                     sb.AppendFormat("ReplyToList    : {0}<br>", nm);
171                 }
172
173                 sb.AppendFormat("ScreenName           : {0}<br>", post.ScreenName);
174                 sb.AppendFormat("NickName       : {0}<br>", post.Nickname);
175                 sb.AppendFormat("Text   : {0}<br>", post.Text);
176                 sb.AppendFormat("(PlainText)    : <xmp>{0}</xmp><br>", post.Text);
177                 sb.AppendFormat("CreatedAt          : {0}<br>", post.CreatedAt.ToLocalTimeString());
178                 sb.AppendFormat("Source         : {0}<br>", post.Source);
179                 sb.AppendFormat("UserId            : {0}<br>", post.UserId);
180                 sb.AppendFormat("FilterHit      : {0}<br>", post.FilterHit);
181                 sb.AppendFormat("RetweetedBy    : {0}<br>", post.RetweetedBy);
182                 sb.AppendFormat("RetweetedId    : {0}<br>", post.RetweetedId);
183
184                 sb.AppendFormat("Media.Count    : {0}<br>", post.Media.Count);
185                 if (post.Media.Count > 0)
186                 {
187                     for (var i = 0; i < post.Media.Count; i++)
188                     {
189                         var info = post.Media[i];
190                         sb.AppendFormat("Media[{0}].Url         : {1}<br>", i, info.Url);
191                         sb.AppendFormat("Media[{0}].VideoUrl    : {1}<br>", i, info.VideoUrl ?? "---");
192                     }
193                 }
194                 sb.Append("-----End PostClass Dump<br>");
195
196                 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(sb.ToString());
197                 return;
198             }
199
200             using (ControlTransaction.Update(this.PostBrowser))
201             {
202                 this.PostBrowser.DocumentText =
203                     this.Owner.CreateDetailHtml(post.IsDeleted ? "(DELETED)" : post.Text);
204
205                 this.PostBrowser.Document.Window.ScrollTo(0, 0);
206             }
207
208             loadTasks.Add(this.AppendQuoteTweetAsync(post));
209
210             await Task.WhenAll(loadTasks);
211         }
212
213         public void ScrollDownPostBrowser(bool forward)
214         {
215             var doc = this.PostBrowser.Document;
216             if (doc == null) return;
217
218             var tags = doc.GetElementsByTagName("html");
219             if (tags.Count > 0)
220             {
221                 if (forward)
222                     tags[0].ScrollTop += SettingManager.Local.FontDetail.Height;
223                 else
224                     tags[0].ScrollTop -= SettingManager.Local.FontDetail.Height;
225             }
226         }
227
228         public void PageDownPostBrowser(bool forward)
229         {
230             var doc = this.PostBrowser.Document;
231             if (doc == null) return;
232
233             var tags = doc.GetElementsByTagName("html");
234             if (tags.Count > 0)
235             {
236                 if (forward)
237                     tags[0].ScrollTop += this.PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
238                 else
239                     tags[0].ScrollTop -= this.PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
240             }
241         }
242
243         public HtmlElement[] GetLinkElements()
244         {
245             return this.PostBrowser.Document.Links.Cast<HtmlElement>()
246                 .Where(x => x.GetAttribute("className") != "tweet-quote-link") // 引用ツイートで追加されたリンクを除く
247                 .ToArray();
248         }
249
250         private async Task SetUserPictureAsync(string normalImageUrl, bool force = false)
251         {
252             if (MyCommon.IsNullOrEmpty(normalImageUrl))
253                 return;
254
255             if (this.IconCache == null)
256                 return;
257
258             this.ClearUserPicture();
259
260             var imageSize = Twitter.DecideProfileImageSize(this.UserPicture.Width);
261             var cachedImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, imageSize);
262             if (cachedImage != null)
263             {
264                 // 既にキャッシュされていればそれを表示して終了
265                 this.UserPicture.Image = cachedImage.Clone();
266                 return;
267             }
268
269             // 小さいサイズの画像がキャッシュにある場合は高解像度の画像が取得できるまでの間表示する
270             var fallbackImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, "mini");
271             if (fallbackImage != null)
272                 this.UserPicture.Image = fallbackImage.Clone();
273
274             await this.UserPicture.SetImageFromTask(
275                 async () =>
276                 {
277                     var imageUrl = Twitter.CreateProfileImageUrl(normalImageUrl, imageSize);
278                     var image = await this.IconCache.DownloadImageAsync(imageUrl, force)
279                         .ConfigureAwait(false);
280
281                     return image.Clone();
282                 },
283                 useStatusImage: false
284             );
285         }
286
287         /// <summary>
288         /// UserPicture.Image に設定されている画像を破棄します。
289         /// </summary>
290         private void ClearUserPicture()
291         {
292             if (this.UserPicture.Image != null)
293             {
294                 var oldImage = this.UserPicture.Image;
295                 this.UserPicture.Image = null;
296                 oldImage.Dispose();
297             }
298         }
299
300         /// <summary>
301         /// 発言詳細欄のツイートURLを展開する
302         /// </summary>
303         private async Task AppendQuoteTweetAsync(PostClass post)
304         {
305             var quoteStatusIds = post.QuoteStatusIds;
306             if (quoteStatusIds.Length == 0 && post.InReplyToStatusId == null)
307                 return;
308
309             // 「読み込み中」テキストを表示
310             var loadingQuoteHtml = quoteStatusIds.Select(x => FormatQuoteTweetHtml(x, Properties.Resources.LoadingText, isReply: false));
311
312             var loadingReplyHtml = string.Empty;
313             if (post.InReplyToStatusId != null)
314                 loadingReplyHtml = FormatQuoteTweetHtml(post.InReplyToStatusId.Value, Properties.Resources.LoadingText, isReply: true);
315
316             var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml;
317
318             using (ControlTransaction.Update(this.PostBrowser))
319                 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body);
320
321             // 引用ツイートを読み込み
322             var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList();
323
324             if (post.InReplyToStatusId != null)
325                 loadTweetTasks.Add(this.CreateQuoteTweetHtml(post.InReplyToStatusId.Value, isReply: true));
326
327             var quoteHtmls = await Task.WhenAll(loadTweetTasks);
328
329             // 非同期処理中に表示中のツイートが変わっていたらキャンセルされたものと扱う
330             if (this.CurrentPost != post || this.CurrentPost.IsDeleted)
331                 return;
332
333             body = post.Text + string.Concat(quoteHtmls);
334
335             using (ControlTransaction.Update(this.PostBrowser))
336                 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body);
337         }
338
339         private async Task<string> CreateQuoteTweetHtml(long statusId, bool isReply)
340         {
341             var post = TabInformations.GetInstance()[statusId];
342             if (post == null)
343             {
344                 try
345                 {
346                     post = await this.Owner.TwitterInstance.GetStatusApi(false, statusId)
347                         .ConfigureAwait(false);
348                 }
349                 catch (WebApiException ex)
350                 {
351                     return FormatQuoteTweetHtml(statusId, WebUtility.HtmlEncode($"Err:{ex.Message}(GetStatus)"), isReply);
352                 }
353
354                 post.IsRead = true;
355                 if (!TabInformations.GetInstance().AddQuoteTweet(post))
356                     return FormatQuoteTweetHtml(statusId, "This Tweet is unavailable.", isReply);
357             }
358
359             return FormatQuoteTweetHtml(post, isReply);
360         }
361
362         internal static string FormatQuoteTweetHtml(PostClass post, bool isReply)
363         {
364             var innerHtml = "<p>" + StripLinkTagHtml(post.Text) + "</p>" +
365                 " &mdash; " + WebUtility.HtmlEncode(post.Nickname) +
366                 " (@" + WebUtility.HtmlEncode(post.ScreenName) + ") " +
367                 WebUtility.HtmlEncode(post.CreatedAt.ToLocalTimeString());
368
369             return FormatQuoteTweetHtml(post.StatusId, innerHtml, isReply);
370         }
371
372         internal static string FormatQuoteTweetHtml(long statusId, string innerHtml, bool isReply)
373         {
374             var blockClassName = "quote-tweet";
375
376             if (isReply)
377                 blockClassName += " reply";
378
379             return "<a class=\"quote-tweet-link\" href=\"//opentween/status/" + statusId + "\">" +
380                 $"<blockquote class=\"{blockClassName}\">{innerHtml}</blockquote>" +
381                 "</a>";
382         }
383
384         /// <summary>
385         /// 指定されたHTMLからリンクを除去します
386         /// </summary>
387         internal static string StripLinkTagHtml(string html)
388             => Regex.Replace(html, @"<a[^>]*>(.*?)</a>", "$1"); // a 要素はネストされていない前提の正規表現パターン
389
390         public async Task DoTranslation()
391         {
392             if (this.CurrentPost == null || this.CurrentPost.IsDeleted)
393                 return;
394
395             await this.DoTranslation(this.CurrentPost.TextFromApi);
396         }
397
398         private async Task DoTranslation(string str)
399         {
400             if (MyCommon.IsNullOrEmpty(str))
401                 return;
402
403             var bing = new Bing();
404             try
405             {
406                 var translatedText = await bing.TranslateAsync(str,
407                     langFrom: null,
408                     langTo: SettingManager.Common.TranslateLanguage);
409
410                 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(translatedText);
411             }
412             catch (WebApiException e)
413             {
414                 this.RaiseStatusChanged("Err:" + e.Message);
415             }
416             catch (OperationCanceledException)
417             {
418                 this.RaiseStatusChanged("Err:Timeout");
419             }
420         }
421
422         private async Task DoSearchToolStrip(string url)
423         {
424             // 発言詳細で「選択文字列で検索」(選択文字列取得)
425             var selText = this.PostBrowser.GetSelectedText();
426
427             if (selText != null)
428             {
429                 if (url == Properties.Resources.SearchItem4Url)
430                 {
431                     // 公式検索
432                     this.Owner.AddNewTabForSearch(selText);
433                     return;
434                 }
435
436                 var tmp = string.Format(url, Uri.EscapeDataString(selText));
437                 await MyCommon.OpenInBrowserAsync(this, tmp);
438             }
439         }
440
441         private string? GetUserId()
442         {
443             var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?$");
444             if (m.Success && this.Owner.IsTwitterId(m.Result("${ScreenName}")))
445                 return m.Result("${ScreenName}");
446             else
447                 return null;
448         }
449
450         protected void RaiseStatusChanged(string statusText)
451             => this.StatusChanged?.Invoke(this, new TweetDetailsViewStatusChengedEventArgs(statusText));
452
453         private void TweetDetailsView_FontChanged(object sender, EventArgs e)
454         {
455             // OTBaseForm.GlobalFont による UI フォントの変更に対応
456             var origFont = this.AuthorNameLinkLabel.Font;
457             this.AuthorNameLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
458             this.RetweetedByLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
459         }
460
461         #region TableLayoutPanel1
462
463         private async void UserPicture_Click(object sender, EventArgs e)
464         {
465             var screenName = this.CurrentPost?.ScreenName;
466             if (screenName != null)
467                 await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
468         }
469
470         private async void PostBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs e)
471         {
472             if (e.Url.AbsoluteUri != "about:blank")
473             {
474                 await this.ShowPostDetails(this.CurrentPost!); // 現在の発言を表示し直す (Navigated の段階ではキャンセルできない)
475                 await MyCommon.OpenInBrowserAsync(this, e.Url.OriginalString);
476             }
477         }
478
479         private async void PostBrowser_Navigating(object sender, WebBrowserNavigatingEventArgs e)
480         {
481             if (e.Url.Scheme == "data")
482             {
483                 this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
484             }
485             else if (e.Url.AbsoluteUri != "about:blank")
486             {
487                 e.Cancel = true;
488                 // Ctrlを押しながらリンクを開いた場合は、設定と逆の動作をするフラグを true としておく
489                 await this.Owner.OpenUriAsync(e.Url, MyCommon.IsKeyDown(Keys.Control));
490             }
491         }
492
493         private async void PostBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
494         {
495             var keyRes = this.Owner.CommonKeyDown(e.KeyData, FocusedControl.PostBrowser, out var asyncTask);
496             if (keyRes)
497             {
498                 e.IsInputKey = true;
499             }
500             else
501             {
502                 if (Enum.IsDefined(typeof(Shortcut), (Shortcut)e.KeyData))
503                 {
504                     var shortcut = (Shortcut)e.KeyData;
505                     switch (shortcut)
506                     {
507                         case Shortcut.CtrlA:
508                         case Shortcut.CtrlC:
509                         case Shortcut.CtrlIns:
510                             // 既定の動作を有効にする
511                             break;
512                         default:
513                             // その他のショートカットキーは無効にする
514                             e.IsInputKey = true;
515                             break;
516                     }
517                 }
518             }
519
520             if (asyncTask != null)
521                 await asyncTask;
522         }
523
524         private void PostBrowser_StatusTextChanged(object sender, EventArgs e)
525         {
526             try
527             {
528                 if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal)
529                     || this.PostBrowser.StatusText.StartsWith("ftp", StringComparison.Ordinal)
530                     || this.PostBrowser.StatusText.StartsWith("data", StringComparison.Ordinal))
531                 {
532                     this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
533                 }
534                 if (MyCommon.IsNullOrEmpty(this.PostBrowser.StatusText))
535                 {
536                     this.RaiseStatusChanged(statusText: "");
537                 }
538             }
539             catch (Exception)
540             {
541             }
542         }
543
544         private async void SourceLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
545         {
546             var sourceUri = this.CurrentPost?.SourceUri;
547             if (sourceUri != null && e.Button == MouseButtons.Left)
548             {
549                 await MyCommon.OpenInBrowserAsync(this, sourceUri.AbsoluteUri);
550             }
551         }
552
553         private void SourceLinkLabel_MouseEnter(object sender, EventArgs e)
554         {
555             var sourceUri = this.CurrentPost?.SourceUri;
556             if (sourceUri != null)
557             {
558                 this.RaiseStatusChanged(MyCommon.ConvertToReadableUrl(sourceUri.AbsoluteUri));
559             }
560         }
561
562         private void SourceLinkLabel_MouseLeave(object sender, EventArgs e)
563             => this.RaiseStatusChanged(statusText: "");
564
565         #endregion
566
567         #region ContextMenuUserPicture
568
569         private void ContextMenuUserPicture_Opening(object sender, CancelEventArgs e)
570         {
571             // 発言詳細のアイコン右クリック時のメニュー制御
572             if (this.CurrentPost != null)
573             {
574                 var name = this.CurrentPost.ImageUrl;
575                 if (!MyCommon.IsNullOrEmpty(name))
576                 {
577                     var idx = name.LastIndexOf('/');
578                     if (idx != -1)
579                     {
580                         name = Path.GetFileName(name.Substring(idx));
581                         if (name.Contains("_normal.") || name.EndsWith("_normal", StringComparison.Ordinal))
582                         {
583                             name = name.Replace("_normal", "");
584                             this.IconNameToolStripMenuItem.Text = name;
585                             this.IconNameToolStripMenuItem.Enabled = true;
586                         }
587                         else
588                         {
589                             this.IconNameToolStripMenuItem.Enabled = false;
590                             this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
591                         }
592                     }
593                     else
594                     {
595                         this.IconNameToolStripMenuItem.Enabled = false;
596                         this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
597                     }
598
599                     this.ReloadIconToolStripMenuItem.Enabled = true;
600
601                     if (this.IconCache.TryGetFromCache(this.CurrentPost.ImageUrl) != null)
602                     {
603                         this.SaveIconPictureToolStripMenuItem.Enabled = true;
604                     }
605                     else
606                     {
607                         this.SaveIconPictureToolStripMenuItem.Enabled = false;
608                     }
609                 }
610                 else
611                 {
612                     this.IconNameToolStripMenuItem.Enabled = false;
613                     this.ReloadIconToolStripMenuItem.Enabled = false;
614                     this.SaveIconPictureToolStripMenuItem.Enabled = false;
615                     this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
616                 }
617             }
618             else
619             {
620                 this.IconNameToolStripMenuItem.Enabled = false;
621                 this.ReloadIconToolStripMenuItem.Enabled = false;
622                 this.SaveIconPictureToolStripMenuItem.Enabled = false;
623                 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText2;
624             }
625             if (this.CurrentPost != null)
626             {
627                 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
628                 {
629                     this.FollowToolStripMenuItem.Enabled = false;
630                     this.UnFollowToolStripMenuItem.Enabled = false;
631                     this.ShowFriendShipToolStripMenuItem.Enabled = false;
632                     this.ShowUserStatusToolStripMenuItem.Enabled = true;
633                     this.SearchPostsDetailNameToolStripMenuItem.Enabled = true;
634                     this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
635                     this.ListManageUserContextToolStripMenuItem3.Enabled = true;
636                 }
637                 else
638                 {
639                     this.FollowToolStripMenuItem.Enabled = true;
640                     this.UnFollowToolStripMenuItem.Enabled = true;
641                     this.ShowFriendShipToolStripMenuItem.Enabled = true;
642                     this.ShowUserStatusToolStripMenuItem.Enabled = true;
643                     this.SearchPostsDetailNameToolStripMenuItem.Enabled = true;
644                     this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = true;
645                     this.ListManageUserContextToolStripMenuItem3.Enabled = true;
646                 }
647             }
648             else
649             {
650                 this.FollowToolStripMenuItem.Enabled = false;
651                 this.UnFollowToolStripMenuItem.Enabled = false;
652                 this.ShowFriendShipToolStripMenuItem.Enabled = false;
653                 this.ShowUserStatusToolStripMenuItem.Enabled = false;
654                 this.SearchPostsDetailNameToolStripMenuItem.Enabled = false;
655                 this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
656                 this.ListManageUserContextToolStripMenuItem3.Enabled = false;
657             }
658         }
659
660         private async void FollowToolStripMenuItem_Click(object sender, EventArgs e)
661         {
662             if (this.CurrentPost == null)
663                 return;
664
665             if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
666                 return;
667
668             await this.Owner.FollowCommand(this.CurrentPost.ScreenName);
669         }
670
671         private async void UnFollowToolStripMenuItem_Click(object sender, EventArgs e)
672         {
673             if (this.CurrentPost == null)
674                 return;
675
676             if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
677                 return;
678
679             await this.Owner.RemoveCommand(this.CurrentPost.ScreenName, false);
680         }
681
682         private async void ShowFriendShipToolStripMenuItem_Click(object sender, EventArgs e)
683         {
684             if (this.CurrentPost == null)
685                 return;
686
687             if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
688                 return;
689
690             await this.Owner.ShowFriendship(this.CurrentPost.ScreenName);
691         }
692
693         // ListManageUserContextToolStripMenuItem3.Click は ListManageUserContextToolStripMenuItem_Click を共用
694
695         private async void ShowUserStatusToolStripMenuItem_Click(object sender, EventArgs e)
696         {
697             if (this.CurrentPost == null)
698                 return;
699
700             await this.Owner.ShowUserStatus(this.CurrentPost.ScreenName, false);
701         }
702
703         private async void SearchPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
704         {
705             if (this.CurrentPost == null)
706                 return;
707
708             await this.Owner.AddNewTabForUserTimeline(this.CurrentPost.ScreenName);
709         }
710
711         private void SearchAtPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
712         {
713             if (this.CurrentPost == null)
714                 return;
715
716             this.Owner.AddNewTabForSearch("@" + this.CurrentPost.ScreenName);
717         }
718
719         private async void IconNameToolStripMenuItem_Click(object sender, EventArgs e)
720         {
721             var imageNormalUrl = this.CurrentPost?.ImageUrl;
722             if (MyCommon.IsNullOrEmpty(imageNormalUrl))
723                 return;
724
725             var imageOriginalUrl = Twitter.CreateProfileImageUrl(imageNormalUrl, "original");
726             await MyCommon.OpenInBrowserAsync(this, imageOriginalUrl);
727         }
728
729         private async void ReloadIconToolStripMenuItem_Click(object sender, EventArgs e)
730         {
731             var imageUrl = this.CurrentPost?.ImageUrl;
732             if (MyCommon.IsNullOrEmpty(imageUrl))
733                 return;
734
735             await this.SetUserPictureAsync(imageUrl, force: true);
736         }
737
738         private void SaveIconPictureToolStripMenuItem_Click(object sender, EventArgs e)
739         {
740             var imageUrl = this.CurrentPost?.ImageUrl;
741             if (MyCommon.IsNullOrEmpty(imageUrl))
742                 return;
743
744             var memoryImage = this.IconCache.TryGetFromCache(imageUrl);
745             if (memoryImage == null)
746                 return;
747
748             this.Owner.SaveFileDialog1.FileName = imageUrl.Substring(imageUrl.LastIndexOf('/') + 1);
749
750             if (this.Owner.SaveFileDialog1.ShowDialog() == DialogResult.OK)
751             {
752                 try
753                 {
754                     using var orgBmp = new Bitmap(memoryImage.Image);
755                     using var bmp2 = new Bitmap(orgBmp.Size.Width, orgBmp.Size.Height);
756
757                     using (var g = Graphics.FromImage(bmp2))
758                     {
759                         g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
760                         g.DrawImage(orgBmp, 0, 0, orgBmp.Size.Width, orgBmp.Size.Height);
761                     }
762                     bmp2.Save(this.Owner.SaveFileDialog1.FileName);
763                 }
764                 catch (Exception)
765                 {
766                     // 処理中にキャッシュアウトする可能性あり
767                 }
768             }
769         }
770
771         #endregion
772
773         #region ContextMenuPostBrowser
774
775         private void ContextMenuPostBrowser_Opening(object ender, CancelEventArgs e)
776         {
777             // URLコピーの項目の表示/非表示
778             if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal))
779             {
780                 this.postBrowserStatusText = this.PostBrowser.StatusText;
781                 var name = this.GetUserId();
782                 this.UrlCopyContextMenuItem.Enabled = true;
783                 if (name != null)
784                 {
785                     this.FollowContextMenuItem.Enabled = true;
786                     this.RemoveContextMenuItem.Enabled = true;
787                     this.FriendshipContextMenuItem.Enabled = true;
788                     this.ShowUserStatusContextMenuItem.Enabled = true;
789                     this.SearchPostsDetailToolStripMenuItem.Enabled = true;
790                     this.IdFilterAddMenuItem.Enabled = true;
791                     this.ListManageUserContextToolStripMenuItem.Enabled = true;
792                     this.SearchAtPostsDetailToolStripMenuItem.Enabled = true;
793                 }
794                 else
795                 {
796                     this.FollowContextMenuItem.Enabled = false;
797                     this.RemoveContextMenuItem.Enabled = false;
798                     this.FriendshipContextMenuItem.Enabled = false;
799                     this.ShowUserStatusContextMenuItem.Enabled = false;
800                     this.SearchPostsDetailToolStripMenuItem.Enabled = false;
801                     this.IdFilterAddMenuItem.Enabled = false;
802                     this.ListManageUserContextToolStripMenuItem.Enabled = false;
803                     this.SearchAtPostsDetailToolStripMenuItem.Enabled = false;
804                 }
805
806                 if (Regex.IsMatch(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23"))
807                     this.UseHashtagMenuItem.Enabled = true;
808                 else
809                     this.UseHashtagMenuItem.Enabled = false;
810             }
811             else
812             {
813                 this.postBrowserStatusText = "";
814                 this.UrlCopyContextMenuItem.Enabled = false;
815                 this.FollowContextMenuItem.Enabled = false;
816                 this.RemoveContextMenuItem.Enabled = false;
817                 this.FriendshipContextMenuItem.Enabled = false;
818                 this.ShowUserStatusContextMenuItem.Enabled = false;
819                 this.SearchPostsDetailToolStripMenuItem.Enabled = false;
820                 this.SearchAtPostsDetailToolStripMenuItem.Enabled = false;
821                 this.UseHashtagMenuItem.Enabled = false;
822                 this.IdFilterAddMenuItem.Enabled = false;
823                 this.ListManageUserContextToolStripMenuItem.Enabled = false;
824             }
825             // 文字列選択されていないときは選択文字列関係の項目を非表示に
826             var selText = this.PostBrowser.GetSelectedText();
827             if (selText == null)
828             {
829                 this.SelectionSearchContextMenuItem.Enabled = false;
830                 this.SelectionCopyContextMenuItem.Enabled = false;
831                 this.SelectionTranslationToolStripMenuItem.Enabled = false;
832             }
833             else
834             {
835                 this.SelectionSearchContextMenuItem.Enabled = true;
836                 this.SelectionCopyContextMenuItem.Enabled = true;
837                 this.SelectionTranslationToolStripMenuItem.Enabled = true;
838             }
839             // 発言内に自分以外のユーザーが含まれてればフォロー状態全表示を有効に
840             var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
841             var fAllFlag = false;
842             foreach (Match mu in ma)
843             {
844                 if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase))
845                 {
846                     fAllFlag = true;
847                     break;
848                 }
849             }
850             this.FriendshipAllMenuItem.Enabled = fAllFlag;
851
852             if (this.CurrentPost == null)
853                 this.TranslationToolStripMenuItem.Enabled = false;
854             else
855                 this.TranslationToolStripMenuItem.Enabled = true;
856
857             e.Cancel = false;
858         }
859
860         private async void SearchGoogleContextMenuItem_Click(object sender, EventArgs e)
861             => await this.DoSearchToolStrip(Properties.Resources.SearchItem2Url);
862
863         private async void SearchWikipediaContextMenuItem_Click(object sender, EventArgs e)
864             => await this.DoSearchToolStrip(Properties.Resources.SearchItem1Url);
865
866         private async void SearchPublicSearchContextMenuItem_Click(object sender, EventArgs e)
867             => await this.DoSearchToolStrip(Properties.Resources.SearchItem4Url);
868
869         private void CurrentTabToolStripMenuItem_Click(object sender, EventArgs e)
870         {
871             // 発言詳細の選択文字列で現在のタブを検索
872             var selText = this.PostBrowser.GetSelectedText();
873
874             if (selText != null)
875             {
876                 var searchOptions = new SearchWordDialog.SearchOptions(
877                     SearchWordDialog.SearchType.Timeline,
878                     selText,
879                     NewTab: false,
880                     CaseSensitive: false,
881                     UseRegex: false
882                 );
883
884                 this.Owner.SearchDialog.ResultOptions = searchOptions;
885
886                 this.Owner.DoTabSearch(
887                     searchOptions.Query,
888                     searchOptions.CaseSensitive,
889                     searchOptions.UseRegex,
890                     TweenMain.SEARCHTYPE.NextSearch);
891             }
892         }
893
894         private void SelectionCopyContextMenuItem_Click(object sender, EventArgs e)
895         {
896             // 発言詳細で「選択文字列をコピー」
897             var selText = this.PostBrowser.GetSelectedText();
898             try
899             {
900                 Clipboard.SetDataObject(selText, false, 5, 100);
901             }
902             catch (Exception ex)
903             {
904                 MessageBox.Show(ex.Message);
905             }
906         }
907
908         private void UrlCopyContextMenuItem_Click(object sender, EventArgs e)
909         {
910             try
911             {
912                 foreach (var link in this.PostBrowser.Document.Links.Cast<HtmlElement>())
913                 {
914                     if (link.GetAttribute("href") == this.postBrowserStatusText)
915                     {
916                         var linkStr = link.GetAttribute("title");
917                         if (MyCommon.IsNullOrEmpty(linkStr))
918                             linkStr = link.GetAttribute("href");
919
920                         Clipboard.SetDataObject(linkStr, false, 5, 100);
921                         return;
922                     }
923                 }
924
925                 Clipboard.SetDataObject(this.postBrowserStatusText, false, 5, 100);
926             }
927             catch (Exception ex)
928             {
929                 MessageBox.Show(ex.Message);
930             }
931         }
932
933         private void SelectionAllContextMenuItem_Click(object sender, EventArgs e)
934             => this.PostBrowser.Document.ExecCommand("SelectAll", false, null); // 発言詳細ですべて選択
935
936         private async void FollowContextMenuItem_Click(object sender, EventArgs e)
937         {
938             var name = this.GetUserId();
939             if (name != null)
940                 await this.Owner.FollowCommand(name);
941         }
942
943         private async void RemoveContextMenuItem_Click(object sender, EventArgs e)
944         {
945             var name = this.GetUserId();
946             if (name != null)
947                 await this.Owner.RemoveCommand(name, false);
948         }
949
950         private async void FriendshipContextMenuItem_Click(object sender, EventArgs e)
951         {
952             var name = this.GetUserId();
953             if (name != null)
954                 await this.Owner.ShowFriendship(name);
955         }
956
957         private async void FriendshipAllMenuItem_Click(object sender, EventArgs e)
958         {
959             var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
960             var ids = new List<string>();
961             foreach (Match mu in ma)
962             {
963                 if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase))
964                 {
965                     ids.Add(mu.Result("${ScreenName}"));
966                 }
967             }
968
969             await this.Owner.ShowFriendship(ids.ToArray());
970         }
971
972         private async void ShowUserStatusContextMenuItem_Click(object sender, EventArgs e)
973         {
974             var name = this.GetUserId();
975             if (name != null)
976                 await this.Owner.ShowUserStatus(name);
977         }
978
979         private async void SearchPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
980         {
981             var name = this.GetUserId();
982             if (name != null)
983                 await this.Owner.AddNewTabForUserTimeline(name);
984         }
985
986         private void SearchAtPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
987         {
988             var name = this.GetUserId();
989             if (name != null) this.Owner.AddNewTabForSearch("@" + name);
990         }
991
992         private void IdFilterAddMenuItem_Click(object sender, EventArgs e)
993         {
994             var name = this.GetUserId();
995             if (name != null)
996                 this.Owner.AddFilterRuleByScreenName(name);
997         }
998
999         private void ListManageUserContextToolStripMenuItem_Click(object sender, EventArgs e)
1000         {
1001             var menuItem = (ToolStripMenuItem)sender;
1002
1003             string? user;
1004             if (menuItem.Owner == this.ContextMenuPostBrowser)
1005             {
1006                 user = this.GetUserId();
1007                 if (user == null) return;
1008             }
1009             else if (this.CurrentPost != null)
1010             {
1011                 user = this.CurrentPost.ScreenName;
1012             }
1013             else
1014             {
1015                 return;
1016             }
1017
1018             this.Owner.ListManageUserContext(user);
1019         }
1020
1021         private void UseHashtagMenuItem_Click(object sender, EventArgs e)
1022         {
1023             var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23(?<hash>.+)$");
1024             if (m.Success)
1025                 this.Owner.SetPermanentHashtag(Uri.UnescapeDataString(m.Groups["hash"].Value));
1026         }
1027
1028         private async void SelectionTranslationToolStripMenuItem_Click(object sender, EventArgs e)
1029         {
1030             var text = this.PostBrowser.GetSelectedText();
1031             await this.DoTranslation(text);
1032         }
1033
1034         private async void TranslationToolStripMenuItem_Click(object sender, EventArgs e)
1035             => await this.DoTranslation();
1036
1037         #endregion
1038
1039         #region ContextMenuSource
1040
1041         private void ContextMenuSource_Opening(object sender, CancelEventArgs e)
1042         {
1043             if (this.CurrentPost == null || this.CurrentPost.IsDeleted || this.CurrentPost.IsDm)
1044             {
1045                 this.SourceCopyMenuItem.Enabled = false;
1046                 this.SourceUrlCopyMenuItem.Enabled = false;
1047             }
1048             else
1049             {
1050                 this.SourceCopyMenuItem.Enabled = true;
1051                 this.SourceUrlCopyMenuItem.Enabled = true;
1052             }
1053         }
1054
1055         private void SourceCopyMenuItem_Click(object sender, EventArgs e)
1056         {
1057             if (this.CurrentPost == null)
1058                 return;
1059
1060             try
1061             {
1062                 Clipboard.SetDataObject(this.CurrentPost.Source, false, 5, 100);
1063             }
1064             catch (Exception ex)
1065             {
1066                 MessageBox.Show(ex.Message);
1067             }
1068         }
1069
1070         private void SourceUrlCopyMenuItem_Click(object sender, EventArgs e)
1071         {
1072             var sourceUri = this.CurrentPost?.SourceUri;
1073             if (sourceUri == null)
1074                 return;
1075
1076             try
1077             {
1078                 Clipboard.SetDataObject(sourceUri.AbsoluteUri, false, 5, 100);
1079             }
1080             catch (Exception ex)
1081             {
1082                 MessageBox.Show(ex.Message);
1083             }
1084         }
1085
1086         #endregion
1087
1088         private async void AuthorNameLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
1089         {
1090             var screenName = this.CurrentPost?.ScreenName;
1091             if (screenName != null)
1092                 await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
1093         }
1094
1095         private async void RetweetedByLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
1096         {
1097             var screenName = this.CurrentPost?.RetweetedBy;
1098             if (screenName != null)
1099                 await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
1100         }
1101     }
1102
1103     public class TweetDetailsViewStatusChengedEventArgs : EventArgs
1104     {
1105         /// <summary>ステータスバーに表示するテキスト</summary>
1106         /// <remarks>
1107         /// 空文字列の場合は <see cref="TweenMain"/> の既定のテキストを表示する
1108         /// </remarks>
1109         public string StatusText { get; }
1110
1111         public TweetDetailsViewStatusChengedEventArgs(string statusText)
1112             => this.StatusText = statusText;
1113     }
1114 }