1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 // (c) 2008-2011 Moz (@syo68k)
4 // (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 // (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 // (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 // (c) 2011 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
8 // All rights reserved.
10 // This file is part of OpenTween.
12 // This program is free software; you can redistribute it and/or modify it
13 // under the terms of the GNU General public License as published by the Free
14 // Software Foundation; either version 3 of the License, or (at your option)
17 // This program is distributed in the hope that it will be useful, but
18 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
19 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
22 // You should have received a copy of the GNU General public License along
23 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
24 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
25 // Boston, MA 02110-1301, USA.
30 using System.Collections.Generic;
31 using System.ComponentModel;
37 using System.Net.Http;
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;
48 public partial class TweetDetailsView : UserControl
50 public TweenMain Owner { get; set; } = null!;
52 /// <summary>プロフィール画像のキャッシュ</summary>
53 public ImageCache IconCache { get; set; } = null!;
55 /// <summary><see cref="PostClass"/> のダンプを表示するか</summary>
56 public bool DumpPostClass { get; set; }
58 /// <summary>現在表示中の発言</summary>
59 public PostClass? CurrentPost { get; private set; }
62 public new bool TabStop
65 set => base.TabStop = value;
68 /// <summary>ステータスバーに表示するテキストの変化を通知するイベント</summary>
69 public event EventHandler<TweetDetailsViewStatusChengedEventArgs>? StatusChanged;
71 /// <summary><see cref="ContextMenuPostBrowser"/> 展開時の <see cref="PostBrowser"/>.StatusText を保持するフィールド</summary>
72 private string postBrowserStatusText = "";
74 public TweetDetailsView()
76 this.InitializeComponent();
81 this.AuthorNameLinkLabel.Text = "";
82 this.RetweetedByLinkLabel.Text = "";
83 this.DateTimeLabel.Text = "";
84 this.SourceLinkLabel.Text = "";
86 new InternetSecurityManager(this.PostBrowser);
87 this.PostBrowser.AllowWebBrowserDrop = false; // COMException を回避するため、ActiveX の初期化が終わってから設定する
90 public void ClearPostBrowser()
91 => this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml("");
93 public async Task ShowPostDetails(PostClass post)
95 this.CurrentPost = post;
97 var loadTasks = new List<Task>();
99 using (ControlTransaction.Update(this.TableLayoutPanel1))
101 this.SourceLinkLabel.Text = post.Source;
102 this.SourceLinkLabel.TabStop = false; // Text を更新すると勝手に true にされる
108 nameText = "DM FROM <- ";
110 nameText = "DM TO -> ";
116 nameText += post.ScreenName + "/" + post.Nickname;
117 this.AuthorNameLinkLabel.Text = nameText;
119 if (post.RetweetedId != null)
121 this.RetweetedByLinkLabel.Visible = true;
122 this.RetweetedByLinkLabel.Text = $"(RT:{post.RetweetedBy})";
126 this.RetweetedByLinkLabel.Visible = false;
127 this.RetweetedByLinkLabel.Text = "";
130 var nameForeColor = SystemColors.ControlText;
131 if (post.IsOwl && (SettingManager.Common.OneWayLove || post.IsDm))
132 nameForeColor = SettingManager.Local.ColorOWL;
133 if (post.RetweetedId != null)
134 nameForeColor = SettingManager.Local.ColorRetweet;
136 nameForeColor = SettingManager.Local.ColorFav;
138 this.AuthorNameLinkLabel.LinkColor = nameForeColor;
139 this.AuthorNameLinkLabel.ActiveLinkColor = nameForeColor;
140 this.RetweetedByLinkLabel.LinkColor = nameForeColor;
141 this.RetweetedByLinkLabel.ActiveLinkColor = nameForeColor;
143 loadTasks.Add(this.SetUserPictureAsync(post.ImageUrl));
145 this.DateTimeLabel.Text = post.CreatedAt.ToLocalTimeString();
148 if (this.DumpPostClass)
150 var sb = new StringBuilder(512);
152 sb.Append("-----Start PostClass Dump<br>");
153 sb.AppendFormat("TextFromApi : {0}<br>", post.TextFromApi);
154 sb.AppendFormat("(PlainText) : <xmp>{0}</xmp><br>", post.TextFromApi);
155 sb.AppendFormat("StatusId : {0}<br>", post.StatusId);
156 sb.AppendFormat("ImageUrl : {0}<br>", post.ImageUrl);
157 sb.AppendFormat("InReplyToStatusId : {0}<br>", post.InReplyToStatusId);
158 sb.AppendFormat("InReplyToUser : {0}<br>", post.InReplyToUser);
159 sb.AppendFormat("IsDM : {0}<br>", post.IsDm);
160 sb.AppendFormat("IsFav : {0}<br>", post.IsFav);
161 sb.AppendFormat("IsMark : {0}<br>", post.IsMark);
162 sb.AppendFormat("IsMe : {0}<br>", post.IsMe);
163 sb.AppendFormat("IsOwl : {0}<br>", post.IsOwl);
164 sb.AppendFormat("IsProtect : {0}<br>", post.IsProtect);
165 sb.AppendFormat("IsRead : {0}<br>", post.IsRead);
166 sb.AppendFormat("IsReply : {0}<br>", post.IsReply);
168 foreach (var nm in post.ReplyToList.Select(x => x.ScreenName))
170 sb.AppendFormat("ReplyToList : {0}<br>", nm);
173 sb.AppendFormat("ScreenName : {0}<br>", post.ScreenName);
174 sb.AppendFormat("NickName : {0}<br>", post.Nickname);
175 sb.AppendFormat("Text : {0}<br>", post.Text);
176 sb.AppendFormat("(PlainText) : <xmp>{0}</xmp><br>", post.Text);
177 sb.AppendFormat("CreatedAt : {0}<br>", post.CreatedAt.ToLocalTimeString());
178 sb.AppendFormat("Source : {0}<br>", post.Source);
179 sb.AppendFormat("UserId : {0}<br>", post.UserId);
180 sb.AppendFormat("FilterHit : {0}<br>", post.FilterHit);
181 sb.AppendFormat("RetweetedBy : {0}<br>", post.RetweetedBy);
182 sb.AppendFormat("RetweetedId : {0}<br>", post.RetweetedId);
184 sb.AppendFormat("Media.Count : {0}<br>", post.Media.Count);
185 if (post.Media.Count > 0)
187 for (var i = 0; i < post.Media.Count; i++)
189 var info = post.Media[i];
190 sb.AppendFormat("Media[{0}].Url : {1}<br>", i, info.Url);
191 sb.AppendFormat("Media[{0}].VideoUrl : {1}<br>", i, info.VideoUrl ?? "---");
194 sb.Append("-----End PostClass Dump<br>");
196 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(sb.ToString());
200 using (ControlTransaction.Update(this.PostBrowser))
202 this.PostBrowser.DocumentText =
203 this.Owner.CreateDetailHtml(post.IsDeleted ? "(DELETED)" : post.Text);
205 this.PostBrowser.Document.Window.ScrollTo(0, 0);
208 loadTasks.Add(this.AppendQuoteTweetAsync(post));
210 await Task.WhenAll(loadTasks);
213 public void ScrollDownPostBrowser(bool forward)
215 var doc = this.PostBrowser.Document;
216 if (doc == null) return;
218 var tags = doc.GetElementsByTagName("html");
222 tags[0].ScrollTop += SettingManager.Local.FontDetail.Height;
224 tags[0].ScrollTop -= SettingManager.Local.FontDetail.Height;
228 public void PageDownPostBrowser(bool forward)
230 var doc = this.PostBrowser.Document;
231 if (doc == null) return;
233 var tags = doc.GetElementsByTagName("html");
237 tags[0].ScrollTop += this.PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
239 tags[0].ScrollTop -= this.PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
243 public HtmlElement[] GetLinkElements()
245 return this.PostBrowser.Document.Links.Cast<HtmlElement>()
246 .Where(x => x.GetAttribute("className") != "tweet-quote-link") // 引用ツイートで追加されたリンクを除く
250 private async Task SetUserPictureAsync(string imageUrl, bool force = false)
252 if (MyCommon.IsNullOrEmpty(imageUrl))
255 if (this.IconCache == null)
258 this.ClearUserPicture();
260 await this.UserPicture.SetImageFromTask(async () =>
262 var image = await this.IconCache.DownloadImageAsync(imageUrl, force)
263 .ConfigureAwait(false);
265 return await image.CloneAsync()
266 .ConfigureAwait(false);
271 /// UserPicture.Image に設定されている画像を破棄します。
273 private void ClearUserPicture()
275 if (this.UserPicture.Image != null)
277 var oldImage = this.UserPicture.Image;
278 this.UserPicture.Image = null;
284 /// 発言詳細欄のツイートURLを展開する
286 private async Task AppendQuoteTweetAsync(PostClass post)
288 var quoteStatusIds = post.QuoteStatusIds;
289 if (quoteStatusIds.Length == 0 && post.InReplyToStatusId == null)
293 var loadingQuoteHtml = quoteStatusIds.Select(x => FormatQuoteTweetHtml(x, Properties.Resources.LoadingText, isReply: false));
295 var loadingReplyHtml = string.Empty;
296 if (post.InReplyToStatusId != null)
297 loadingReplyHtml = FormatQuoteTweetHtml(post.InReplyToStatusId.Value, Properties.Resources.LoadingText, isReply: true);
299 var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml;
301 using (ControlTransaction.Update(this.PostBrowser))
302 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body);
305 var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList();
307 if (post.InReplyToStatusId != null)
308 loadTweetTasks.Add(this.CreateQuoteTweetHtml(post.InReplyToStatusId.Value, isReply: true));
310 var quoteHtmls = await Task.WhenAll(loadTweetTasks);
312 // 非同期処理中に表示中のツイートが変わっていたらキャンセルされたものと扱う
313 if (this.CurrentPost != post || this.CurrentPost.IsDeleted)
316 body = post.Text + string.Concat(quoteHtmls);
318 using (ControlTransaction.Update(this.PostBrowser))
319 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(body);
322 private async Task<string> CreateQuoteTweetHtml(long statusId, bool isReply)
324 var post = TabInformations.GetInstance()[statusId];
329 post = await this.Owner.TwitterInstance.GetStatusApi(false, statusId)
330 .ConfigureAwait(false);
332 catch (WebApiException ex)
334 return FormatQuoteTweetHtml(statusId, WebUtility.HtmlEncode($"Err:{ex.Message}(GetStatus)"), isReply);
338 if (!TabInformations.GetInstance().AddQuoteTweet(post))
339 return FormatQuoteTweetHtml(statusId, "This Tweet is unavailable.", isReply);
342 return FormatQuoteTweetHtml(post, isReply);
345 internal static string FormatQuoteTweetHtml(PostClass post, bool isReply)
347 var innerHtml = "<p>" + StripLinkTagHtml(post.Text) + "</p>" +
348 " — " + WebUtility.HtmlEncode(post.Nickname) +
349 " (@" + WebUtility.HtmlEncode(post.ScreenName) + ") " +
350 WebUtility.HtmlEncode(post.CreatedAt.ToLocalTimeString());
352 return FormatQuoteTweetHtml(post.StatusId, innerHtml, isReply);
355 internal static string FormatQuoteTweetHtml(long statusId, string innerHtml, bool isReply)
357 var blockClassName = "quote-tweet";
360 blockClassName += " reply";
362 return "<a class=\"quote-tweet-link\" href=\"//opentween/status/" + statusId + "\">" +
363 $"<blockquote class=\"{blockClassName}\">{innerHtml}</blockquote>" +
368 /// 指定されたHTMLからリンクを除去します
370 internal static string StripLinkTagHtml(string html)
371 => Regex.Replace(html, @"<a[^>]*>(.*?)</a>", "$1"); // a 要素はネストされていない前提の正規表現パターン
373 public async Task DoTranslation()
375 if (this.CurrentPost == null || this.CurrentPost.IsDeleted)
378 await this.DoTranslation(this.CurrentPost.TextFromApi);
381 private async Task DoTranslation(string str)
383 if (MyCommon.IsNullOrEmpty(str))
386 var bing = new Bing();
389 var translatedText = await bing.TranslateAsync(str,
391 langTo: SettingManager.Common.TranslateLanguage);
393 this.PostBrowser.DocumentText = this.Owner.CreateDetailHtml(translatedText);
395 catch (WebApiException e)
397 this.RaiseStatusChanged("Err:" + e.Message);
399 catch (OperationCanceledException)
401 this.RaiseStatusChanged("Err:Timeout");
405 private async Task DoSearchToolStrip(string url)
407 // 発言詳細で「選択文字列で検索」(選択文字列取得)
408 var selText = this.PostBrowser.GetSelectedText();
412 if (url == Properties.Resources.SearchItem4Url)
415 this.Owner.AddNewTabForSearch(selText);
419 var tmp = string.Format(url, Uri.EscapeDataString(selText));
420 await MyCommon.OpenInBrowserAsync(this, tmp);
424 private string? GetUserId()
426 var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?$");
427 if (m.Success && this.Owner.IsTwitterId(m.Result("${ScreenName}")))
428 return m.Result("${ScreenName}");
433 protected void RaiseStatusChanged(string statusText)
434 => this.StatusChanged?.Invoke(this, new TweetDetailsViewStatusChengedEventArgs(statusText));
436 private void TweetDetailsView_FontChanged(object sender, EventArgs e)
438 // OTBaseForm.GlobalFont による UI フォントの変更に対応
439 var origFont = this.AuthorNameLinkLabel.Font;
440 this.AuthorNameLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
441 this.RetweetedByLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
444 #region TableLayoutPanel1
446 private async void UserPicture_Click(object sender, EventArgs e)
448 var screenName = this.CurrentPost?.ScreenName;
449 if (screenName != null)
450 await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
453 private async void PostBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs e)
455 if (e.Url.AbsoluteUri != "about:blank")
457 await this.ShowPostDetails(this.CurrentPost!); // 現在の発言を表示し直す (Navigated の段階ではキャンセルできない)
458 await MyCommon.OpenInBrowserAsync(this, e.Url.OriginalString);
462 private async void PostBrowser_Navigating(object sender, WebBrowserNavigatingEventArgs e)
464 if (e.Url.Scheme == "data")
466 this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
468 else if (e.Url.AbsoluteUri != "about:blank")
471 // Ctrlを押しながらリンクを開いた場合は、設定と逆の動作をするフラグを true としておく
472 await this.Owner.OpenUriAsync(e.Url, MyCommon.IsKeyDown(Keys.Control));
476 private async void PostBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
478 var keyRes = this.Owner.CommonKeyDown(e.KeyData, FocusedControl.PostBrowser, out var asyncTask);
485 if (Enum.IsDefined(typeof(Shortcut), (Shortcut)e.KeyData))
487 var shortcut = (Shortcut)e.KeyData;
492 case Shortcut.CtrlIns:
496 // その他のショートカットキーは無効にする
503 if (asyncTask != null)
507 private void PostBrowser_StatusTextChanged(object sender, EventArgs e)
511 if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal)
512 || this.PostBrowser.StatusText.StartsWith("ftp", StringComparison.Ordinal)
513 || this.PostBrowser.StatusText.StartsWith("data", StringComparison.Ordinal))
515 this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
517 if (MyCommon.IsNullOrEmpty(this.PostBrowser.StatusText))
519 this.RaiseStatusChanged(statusText: "");
527 private async void SourceLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
529 var sourceUri = this.CurrentPost?.SourceUri;
530 if (sourceUri != null && e.Button == MouseButtons.Left)
532 await MyCommon.OpenInBrowserAsync(this, sourceUri.AbsoluteUri);
536 private void SourceLinkLabel_MouseEnter(object sender, EventArgs e)
538 var sourceUri = this.CurrentPost?.SourceUri;
539 if (sourceUri != null)
541 this.RaiseStatusChanged(MyCommon.ConvertToReadableUrl(sourceUri.AbsoluteUri));
545 private void SourceLinkLabel_MouseLeave(object sender, EventArgs e)
546 => this.RaiseStatusChanged(statusText: "");
550 #region ContextMenuUserPicture
552 private void ContextMenuUserPicture_Opening(object sender, CancelEventArgs e)
554 // 発言詳細のアイコン右クリック時のメニュー制御
555 if (this.CurrentPost != null)
557 var name = this.CurrentPost.ImageUrl;
558 if (!MyCommon.IsNullOrEmpty(name))
560 var idx = name.LastIndexOf('/');
563 name = Path.GetFileName(name.Substring(idx));
564 if (name.Contains("_normal.") || name.EndsWith("_normal", StringComparison.Ordinal))
566 name = name.Replace("_normal", "");
567 this.IconNameToolStripMenuItem.Text = name;
568 this.IconNameToolStripMenuItem.Enabled = true;
572 this.IconNameToolStripMenuItem.Enabled = false;
573 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
578 this.IconNameToolStripMenuItem.Enabled = false;
579 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
582 this.ReloadIconToolStripMenuItem.Enabled = true;
584 if (this.IconCache.TryGetFromCache(this.CurrentPost.ImageUrl) != null)
586 this.SaveIconPictureToolStripMenuItem.Enabled = true;
590 this.SaveIconPictureToolStripMenuItem.Enabled = false;
595 this.IconNameToolStripMenuItem.Enabled = false;
596 this.ReloadIconToolStripMenuItem.Enabled = false;
597 this.SaveIconPictureToolStripMenuItem.Enabled = false;
598 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
603 this.IconNameToolStripMenuItem.Enabled = false;
604 this.ReloadIconToolStripMenuItem.Enabled = false;
605 this.SaveIconPictureToolStripMenuItem.Enabled = false;
606 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText2;
608 if (this.CurrentPost != null)
610 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
612 this.FollowToolStripMenuItem.Enabled = false;
613 this.UnFollowToolStripMenuItem.Enabled = false;
614 this.ShowFriendShipToolStripMenuItem.Enabled = false;
615 this.ShowUserStatusToolStripMenuItem.Enabled = true;
616 this.SearchPostsDetailNameToolStripMenuItem.Enabled = true;
617 this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
618 this.ListManageUserContextToolStripMenuItem3.Enabled = true;
622 this.FollowToolStripMenuItem.Enabled = true;
623 this.UnFollowToolStripMenuItem.Enabled = true;
624 this.ShowFriendShipToolStripMenuItem.Enabled = true;
625 this.ShowUserStatusToolStripMenuItem.Enabled = true;
626 this.SearchPostsDetailNameToolStripMenuItem.Enabled = true;
627 this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = true;
628 this.ListManageUserContextToolStripMenuItem3.Enabled = true;
633 this.FollowToolStripMenuItem.Enabled = false;
634 this.UnFollowToolStripMenuItem.Enabled = false;
635 this.ShowFriendShipToolStripMenuItem.Enabled = false;
636 this.ShowUserStatusToolStripMenuItem.Enabled = false;
637 this.SearchPostsDetailNameToolStripMenuItem.Enabled = false;
638 this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
639 this.ListManageUserContextToolStripMenuItem3.Enabled = false;
643 private async void FollowToolStripMenuItem_Click(object sender, EventArgs e)
645 if (this.CurrentPost == null)
648 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
651 await this.Owner.FollowCommand(this.CurrentPost.ScreenName);
654 private async void UnFollowToolStripMenuItem_Click(object sender, EventArgs e)
656 if (this.CurrentPost == null)
659 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
662 await this.Owner.RemoveCommand(this.CurrentPost.ScreenName, false);
665 private async void ShowFriendShipToolStripMenuItem_Click(object sender, EventArgs e)
667 if (this.CurrentPost == null)
670 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
673 await this.Owner.ShowFriendship(this.CurrentPost.ScreenName);
676 // ListManageUserContextToolStripMenuItem3.Click は ListManageUserContextToolStripMenuItem_Click を共用
678 private async void ShowUserStatusToolStripMenuItem_Click(object sender, EventArgs e)
680 if (this.CurrentPost == null)
683 await this.Owner.ShowUserStatus(this.CurrentPost.ScreenName, false);
686 private async void SearchPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
688 if (this.CurrentPost == null)
691 await this.Owner.AddNewTabForUserTimeline(this.CurrentPost.ScreenName);
694 private void SearchAtPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
696 if (this.CurrentPost == null)
699 this.Owner.AddNewTabForSearch("@" + this.CurrentPost.ScreenName);
702 private async void IconNameToolStripMenuItem_Click(object sender, EventArgs e)
704 var imageNormalUrl = this.CurrentPost?.ImageUrl;
705 if (MyCommon.IsNullOrEmpty(imageNormalUrl))
708 var imageOriginalUrl = Twitter.CreateProfileImageUrl(imageNormalUrl, "original");
709 await MyCommon.OpenInBrowserAsync(this, imageOriginalUrl);
712 private async void ReloadIconToolStripMenuItem_Click(object sender, EventArgs e)
714 var imageUrl = this.CurrentPost?.ImageUrl;
715 if (MyCommon.IsNullOrEmpty(imageUrl))
718 await this.SetUserPictureAsync(imageUrl, force: true);
721 private void SaveIconPictureToolStripMenuItem_Click(object sender, EventArgs e)
723 var imageUrl = this.CurrentPost?.ImageUrl;
724 if (MyCommon.IsNullOrEmpty(imageUrl))
727 var memoryImage = this.IconCache.TryGetFromCache(imageUrl);
728 if (memoryImage == null)
731 this.Owner.SaveFileDialog1.FileName = imageUrl.Substring(imageUrl.LastIndexOf('/') + 1);
733 if (this.Owner.SaveFileDialog1.ShowDialog() == DialogResult.OK)
737 using var orgBmp = new Bitmap(memoryImage.Image);
738 using var bmp2 = new Bitmap(orgBmp.Size.Width, orgBmp.Size.Height);
740 using (var g = Graphics.FromImage(bmp2))
742 g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
743 g.DrawImage(orgBmp, 0, 0, orgBmp.Size.Width, orgBmp.Size.Height);
745 bmp2.Save(this.Owner.SaveFileDialog1.FileName);
749 // 処理中にキャッシュアウトする可能性あり
756 #region ContextMenuPostBrowser
758 private void ContextMenuPostBrowser_Opening(object ender, CancelEventArgs e)
761 if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal))
763 this.postBrowserStatusText = this.PostBrowser.StatusText;
764 var name = this.GetUserId();
765 this.UrlCopyContextMenuItem.Enabled = true;
768 this.FollowContextMenuItem.Enabled = true;
769 this.RemoveContextMenuItem.Enabled = true;
770 this.FriendshipContextMenuItem.Enabled = true;
771 this.ShowUserStatusContextMenuItem.Enabled = true;
772 this.SearchPostsDetailToolStripMenuItem.Enabled = true;
773 this.IdFilterAddMenuItem.Enabled = true;
774 this.ListManageUserContextToolStripMenuItem.Enabled = true;
775 this.SearchAtPostsDetailToolStripMenuItem.Enabled = true;
779 this.FollowContextMenuItem.Enabled = false;
780 this.RemoveContextMenuItem.Enabled = false;
781 this.FriendshipContextMenuItem.Enabled = false;
782 this.ShowUserStatusContextMenuItem.Enabled = false;
783 this.SearchPostsDetailToolStripMenuItem.Enabled = false;
784 this.IdFilterAddMenuItem.Enabled = false;
785 this.ListManageUserContextToolStripMenuItem.Enabled = false;
786 this.SearchAtPostsDetailToolStripMenuItem.Enabled = false;
789 if (Regex.IsMatch(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23"))
790 this.UseHashtagMenuItem.Enabled = true;
792 this.UseHashtagMenuItem.Enabled = false;
796 this.postBrowserStatusText = "";
797 this.UrlCopyContextMenuItem.Enabled = false;
798 this.FollowContextMenuItem.Enabled = false;
799 this.RemoveContextMenuItem.Enabled = false;
800 this.FriendshipContextMenuItem.Enabled = false;
801 this.ShowUserStatusContextMenuItem.Enabled = false;
802 this.SearchPostsDetailToolStripMenuItem.Enabled = false;
803 this.SearchAtPostsDetailToolStripMenuItem.Enabled = false;
804 this.UseHashtagMenuItem.Enabled = false;
805 this.IdFilterAddMenuItem.Enabled = false;
806 this.ListManageUserContextToolStripMenuItem.Enabled = false;
808 // 文字列選択されていないときは選択文字列関係の項目を非表示に
809 var selText = this.PostBrowser.GetSelectedText();
812 this.SelectionSearchContextMenuItem.Enabled = false;
813 this.SelectionCopyContextMenuItem.Enabled = false;
814 this.SelectionTranslationToolStripMenuItem.Enabled = false;
818 this.SelectionSearchContextMenuItem.Enabled = true;
819 this.SelectionCopyContextMenuItem.Enabled = true;
820 this.SelectionTranslationToolStripMenuItem.Enabled = true;
822 // 発言内に自分以外のユーザーが含まれてればフォロー状態全表示を有効に
823 var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
824 var fAllFlag = false;
825 foreach (Match mu in ma)
827 if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase))
833 this.FriendshipAllMenuItem.Enabled = fAllFlag;
835 if (this.CurrentPost == null)
836 this.TranslationToolStripMenuItem.Enabled = false;
838 this.TranslationToolStripMenuItem.Enabled = true;
843 private async void SearchGoogleContextMenuItem_Click(object sender, EventArgs e)
844 => await this.DoSearchToolStrip(Properties.Resources.SearchItem2Url);
846 private async void SearchWikipediaContextMenuItem_Click(object sender, EventArgs e)
847 => await this.DoSearchToolStrip(Properties.Resources.SearchItem1Url);
849 private async void SearchPublicSearchContextMenuItem_Click(object sender, EventArgs e)
850 => await this.DoSearchToolStrip(Properties.Resources.SearchItem4Url);
852 private void CurrentTabToolStripMenuItem_Click(object sender, EventArgs e)
854 // 発言詳細の選択文字列で現在のタブを検索
855 var selText = this.PostBrowser.GetSelectedText();
859 var searchOptions = new SearchWordDialog.SearchOptions(
860 SearchWordDialog.SearchType.Timeline,
863 caseSensitive: false,
866 this.Owner.SearchDialog.ResultOptions = searchOptions;
868 this.Owner.DoTabSearch(
870 searchOptions.CaseSensitive,
871 searchOptions.UseRegex,
872 TweenMain.SEARCHTYPE.NextSearch);
876 private void SelectionCopyContextMenuItem_Click(object sender, EventArgs e)
879 var selText = this.PostBrowser.GetSelectedText();
882 Clipboard.SetDataObject(selText, false, 5, 100);
886 MessageBox.Show(ex.Message);
890 private void UrlCopyContextMenuItem_Click(object sender, EventArgs e)
894 foreach (var link in this.PostBrowser.Document.Links.Cast<HtmlElement>())
896 if (link.GetAttribute("href") == this.postBrowserStatusText)
898 var linkStr = link.GetAttribute("title");
899 if (MyCommon.IsNullOrEmpty(linkStr))
900 linkStr = link.GetAttribute("href");
902 Clipboard.SetDataObject(linkStr, false, 5, 100);
907 Clipboard.SetDataObject(this.postBrowserStatusText, false, 5, 100);
911 MessageBox.Show(ex.Message);
915 private void SelectionAllContextMenuItem_Click(object sender, EventArgs e)
916 => this.PostBrowser.Document.ExecCommand("SelectAll", false, null); // 発言詳細ですべて選択
918 private async void FollowContextMenuItem_Click(object sender, EventArgs e)
920 var name = this.GetUserId();
922 await this.Owner.FollowCommand(name);
925 private async void RemoveContextMenuItem_Click(object sender, EventArgs e)
927 var name = this.GetUserId();
929 await this.Owner.RemoveCommand(name, false);
932 private async void FriendshipContextMenuItem_Click(object sender, EventArgs e)
934 var name = this.GetUserId();
936 await this.Owner.ShowFriendship(name);
939 private async void FriendshipAllMenuItem_Click(object sender, EventArgs e)
941 var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
942 var ids = new List<string>();
943 foreach (Match mu in ma)
945 if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase))
947 ids.Add(mu.Result("${ScreenName}"));
951 await this.Owner.ShowFriendship(ids.ToArray());
954 private async void ShowUserStatusContextMenuItem_Click(object sender, EventArgs e)
956 var name = this.GetUserId();
958 await this.Owner.ShowUserStatus(name);
961 private async void SearchPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
963 var name = this.GetUserId();
965 await this.Owner.AddNewTabForUserTimeline(name);
968 private void SearchAtPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
970 var name = this.GetUserId();
971 if (name != null) this.Owner.AddNewTabForSearch("@" + name);
974 private void IdFilterAddMenuItem_Click(object sender, EventArgs e)
976 var name = this.GetUserId();
978 this.Owner.AddFilterRuleByScreenName(name);
981 private void ListManageUserContextToolStripMenuItem_Click(object sender, EventArgs e)
983 var menuItem = (ToolStripMenuItem)sender;
986 if (menuItem.Owner == this.ContextMenuPostBrowser)
988 user = this.GetUserId();
989 if (user == null) return;
991 else if (this.CurrentPost != null)
993 user = this.CurrentPost.ScreenName;
1000 this.Owner.ListManageUserContext(user);
1003 private void UseHashtagMenuItem_Click(object sender, EventArgs e)
1005 var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23(?<hash>.+)$");
1007 this.Owner.SetPermanentHashtag(Uri.UnescapeDataString(m.Groups["hash"].Value));
1010 private async void SelectionTranslationToolStripMenuItem_Click(object sender, EventArgs e)
1012 var text = this.PostBrowser.GetSelectedText();
1013 await this.DoTranslation(text);
1016 private async void TranslationToolStripMenuItem_Click(object sender, EventArgs e)
1017 => await this.DoTranslation();
1021 #region ContextMenuSource
1023 private void ContextMenuSource_Opening(object sender, CancelEventArgs e)
1025 if (this.CurrentPost == null || this.CurrentPost.IsDeleted || this.CurrentPost.IsDm)
1027 this.SourceCopyMenuItem.Enabled = false;
1028 this.SourceUrlCopyMenuItem.Enabled = false;
1032 this.SourceCopyMenuItem.Enabled = true;
1033 this.SourceUrlCopyMenuItem.Enabled = true;
1037 private void SourceCopyMenuItem_Click(object sender, EventArgs e)
1039 if (this.CurrentPost == null)
1044 Clipboard.SetDataObject(this.CurrentPost.Source, false, 5, 100);
1046 catch (Exception ex)
1048 MessageBox.Show(ex.Message);
1052 private void SourceUrlCopyMenuItem_Click(object sender, EventArgs e)
1054 var sourceUri = this.CurrentPost?.SourceUri;
1055 if (sourceUri == null)
1060 Clipboard.SetDataObject(sourceUri.AbsoluteUri, false, 5, 100);
1062 catch (Exception ex)
1064 MessageBox.Show(ex.Message);
1070 private async void AuthorNameLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
1072 var screenName = this.CurrentPost?.ScreenName;
1073 if (screenName != null)
1074 await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
1077 private async void RetweetedByLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
1079 var screenName = this.CurrentPost?.RetweetedBy;
1080 if (screenName != null)
1081 await this.Owner.ShowUserStatus(screenName, showInputDialog: false);
1085 public class TweetDetailsViewStatusChengedEventArgs : EventArgs
1087 /// <summary>ステータスバーに表示するテキスト</summary>
1089 /// 空文字列の場合は <see cref="TweenMain"/> の既定のテキストを表示する
1091 public string StatusText { get; }
1093 public TweetDetailsViewStatusChengedEventArgs(string statusText)
1094 => this.StatusText = statusText;