OSDN Git Service

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