OSDN Git Service

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