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.
28 using System.Collections.Generic;
29 using System.ComponentModel;
35 using System.Net.Http;
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;
46 public partial class TweetDetailsView : UserControl
48 public TweenMain Owner { get; set; }
50 /// <summary>プロフィール画像のキャッシュ</summary>
51 public ImageCache IconCache { get; set; }
53 /// <summary><see cref="PostClass"/> のダンプを表示するか</summary>
54 public bool DumpPostClass { get; set; }
56 /// <summary>現在表示中の発言</summary>
57 public PostClass CurrentPost { get; private set; }
60 public new bool TabStop
62 get { return base.TabStop; }
63 set { base.TabStop = value; }
66 /// <summary>ステータスバーに表示するテキストの変化を通知するイベント</summary>
67 public event EventHandler<TweetDetailsViewStatusChengedEventArgs> StatusChanged;
69 /// <summary><see cref="ContextMenuPostBrowser"/> 展開時の <see cref="PostBrowser"/>.StatusText を保持するフィールド</summary>
70 private string _postBrowserStatusText = "";
72 public TweetDetailsView()
74 this.InitializeComponent();
80 DateTimeLabel.Text = "";
81 SourceLinkLabel.Text = "";
83 new InternetSecurityManager(PostBrowser);
84 this.PostBrowser.AllowWebBrowserDrop = false; // COMException を回避するため、ActiveX の初期化が終わってから設定する
87 public async Task ShowPostDetails(PostClass post)
89 this.CurrentPost = post;
91 var loadTasks = new List<Task>();
93 using (ControlTransaction.Update(this.TableLayoutPanel1))
95 SourceLinkLabel.Text = post.Source;
96 SourceLinkLabel.TabStop = false; // Text を更新すると勝手に true にされる
102 nameText = "DM FROM <- ";
104 nameText = "DM TO -> ";
110 nameText += post.ScreenName + "/" + post.Nickname;
111 if (post.RetweetedId != null)
112 nameText += " (RT:" + post.RetweetedBy + ")";
114 NameLabel.Text = nameText;
116 var nameForeColor = SystemColors.ControlText;
117 if (post.IsOwl && (SettingCommon.Instance.OneWayLove || post.IsDm))
118 nameForeColor = SettingManager.Local.ColorOWL;
119 if (post.RetweetedId != null)
120 nameForeColor = SettingManager.Local.ColorRetweet;
122 nameForeColor = SettingManager.Local.ColorFav;
123 NameLabel.ForeColor = nameForeColor;
125 loadTasks.Add(this.SetUserPictureAsync(post.ImageUrl));
127 DateTimeLabel.Text = post.CreatedAt.ToString();
130 if (this.DumpPostClass)
132 StringBuilder sb = new StringBuilder(512);
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);
151 foreach (string nm in post.ReplyToList)
153 sb.AppendFormat("ReplyToList : {0}<br>", nm);
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);
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);
167 sb.AppendFormat("Media.Count : {0}<br>", post.Media.Count);
168 if (post.Media.Count > 0)
170 for (int i = 0; i < post.Media.Count; i++)
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 ?? "---");
177 sb.Append("-----End PostClass Dump<br>");
179 PostBrowser.DocumentText = this.Owner.createDetailHtml(sb.ToString());
183 using (ControlTransaction.Update(this.PostBrowser))
185 this.PostBrowser.DocumentText =
186 this.Owner.createDetailHtml(post.IsDeleted ? "(DELETED)" : post.Text);
188 this.PostBrowser.Document.Window.ScrollTo(0, 0);
191 loadTasks.Add(this.AppendQuoteTweetAsync(post));
193 await Task.WhenAll(loadTasks);
196 public void ScrollDownPostBrowser(bool forward)
198 var doc = PostBrowser.Document;
199 if (doc == null) return;
201 var tags = doc.GetElementsByTagName("html");
205 tags[0].ScrollTop += SettingManager.Local.FontDetail.Height;
207 tags[0].ScrollTop -= SettingManager.Local.FontDetail.Height;
211 public void PageDownPostBrowser(bool forward)
213 var doc = PostBrowser.Document;
214 if (doc == null) return;
216 var tags = doc.GetElementsByTagName("html");
220 tags[0].ScrollTop += PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
222 tags[0].ScrollTop -= PostBrowser.ClientRectangle.Height - SettingManager.Local.FontDetail.Height;
226 public HtmlElement[] GetLinkElements()
228 return this.PostBrowser.Document.Links.Cast<HtmlElement>()
229 .Where(x => x.GetAttribute("className") != "tweet-quote-link") // 引用ツイートで追加されたリンクを除く
233 private async Task SetUserPictureAsync(string imageUrl, bool force = false)
235 if (string.IsNullOrEmpty(imageUrl))
238 if (this.IconCache == null)
241 this.ClearUserPicture();
243 await this.UserPicture.SetImageFromTask(async () =>
245 var image = await this.IconCache.DownloadImageAsync(imageUrl, force)
246 .ConfigureAwait(false);
248 return await image.CloneAsync()
249 .ConfigureAwait(false);
254 /// UserPicture.Image に設定されている画像を破棄します。
256 private void ClearUserPicture()
258 if (this.UserPicture.Image != null)
260 var oldImage = this.UserPicture.Image;
261 this.UserPicture.Image = null;
267 /// 発言詳細欄のツイートURLを展開する
269 private async Task AppendQuoteTweetAsync(PostClass post)
271 var quoteStatusIds = post.QuoteStatusIds;
272 if (quoteStatusIds.Length == 0 && post.InReplyToStatusId == null)
276 var loadingQuoteHtml = quoteStatusIds.Select(x => FormatQuoteTweetHtml(x, Properties.Resources.LoadingText, isReply: false));
278 var loadingReplyHtml = string.Empty;
279 if (post.InReplyToStatusId != null)
280 loadingReplyHtml = FormatQuoteTweetHtml(post.InReplyToStatusId.Value, Properties.Resources.LoadingText, isReply: true);
282 var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml;
284 using (ControlTransaction.Update(this.PostBrowser))
285 this.PostBrowser.DocumentText = this.Owner.createDetailHtml(body);
288 var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList();
290 if (post.InReplyToStatusId != null)
291 loadTweetTasks.Add(this.CreateQuoteTweetHtml(post.InReplyToStatusId.Value, isReply: true));
293 var quoteHtmls = await Task.WhenAll(loadTweetTasks);
295 // 非同期処理中に表示中のツイートが変わっていたらキャンセルされたものと扱う
296 if (this.CurrentPost != post || this.CurrentPost.IsDeleted)
299 body = post.Text + string.Concat(quoteHtmls);
301 using (ControlTransaction.Update(this.PostBrowser))
302 this.PostBrowser.DocumentText = this.Owner.createDetailHtml(body);
305 private async Task<string> CreateQuoteTweetHtml(long statusId, bool isReply)
307 var post = TabInformations.GetInstance()[statusId];
312 post = await this.Owner.TwitterInstance.GetStatusApi(false, statusId)
313 .ConfigureAwait(false);
315 catch (WebApiException ex)
317 return FormatQuoteTweetHtml(statusId, WebUtility.HtmlEncode($"Err:{ex.Message}(GetStatus)"), isReply);
321 if (!TabInformations.GetInstance().AddQuoteTweet(post))
322 return FormatQuoteTweetHtml(statusId, "This Tweet is unavailable.", isReply);
325 return FormatQuoteTweetHtml(post, isReply);
328 internal static string FormatQuoteTweetHtml(PostClass post, bool isReply)
330 var innerHtml = "<p>" + StripLinkTagHtml(post.Text) + "</p>" +
331 " — " + WebUtility.HtmlEncode(post.Nickname) +
332 " (@" + WebUtility.HtmlEncode(post.ScreenName) + ") " +
333 WebUtility.HtmlEncode(post.CreatedAt.ToString());
335 return FormatQuoteTweetHtml(post.StatusId, innerHtml, isReply);
338 internal static string FormatQuoteTweetHtml(long statusId, string innerHtml, bool isReply)
340 var blockClassName = "quote-tweet";
343 blockClassName += " reply";
345 return "<a class=\"quote-tweet-link\" href=\"//opentween/status/" + statusId + "\">" +
346 $"<blockquote class=\"{blockClassName}\">{innerHtml}</blockquote>" +
351 /// 指定されたHTMLからリンクを除去します
353 internal static string StripLinkTagHtml(string html)
355 // a 要素はネストされていない前提の正規表現パターン
356 return Regex.Replace(html, @"<a[^>]*>(.*?)</a>", "$1");
359 public async Task DoTranslation()
361 if (this.CurrentPost == null || this.CurrentPost.IsDeleted)
364 await this.DoTranslation(this.CurrentPost.TextFromApi);
367 private async Task DoTranslation(string str)
369 if (string.IsNullOrEmpty(str))
372 var bing = new Bing();
375 var translatedText = await bing.TranslateAsync(str,
377 langTo: SettingCommon.Instance.TranslateLanguage);
379 this.PostBrowser.DocumentText = this.Owner.createDetailHtml(translatedText);
381 catch (HttpRequestException e)
383 this.RaiseStatusChanged("Err:" + e.Message);
385 catch (OperationCanceledException)
387 this.RaiseStatusChanged("Err:Timeout");
391 private async Task DoSearchToolStrip(string url)
393 //発言詳細で「選択文字列で検索」(選択文字列取得)
394 string _selText = this.PostBrowser.GetSelectedText();
396 if (_selText != null)
398 if (url == Properties.Resources.SearchItem4Url)
401 this.Owner.AddNewTabForSearch(_selText);
405 string tmp = string.Format(url, Uri.EscapeDataString(_selText));
406 await this.Owner.OpenUriInBrowserAsync(tmp);
410 private string GetUserId()
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}");
419 protected void RaiseStatusChanged(string statusText)
421 this.StatusChanged?.Invoke(this, new TweetDetailsViewStatusChengedEventArgs(statusText));
424 private void TweetDetailsView_FontChanged(object sender, EventArgs e)
426 // OTBaseForm.GlobalFont による UI フォントの変更に対応
427 var origFont = this.NameLabel.Font;
428 this.NameLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style);
431 #region TableLayoutPanel1
433 private async void UserPicture_DoubleClick(object sender, EventArgs e)
435 if (this.CurrentPost == null)
438 await this.Owner.OpenUriInBrowserAsync(MyCommon.TwitterUrl + this.CurrentPost.ScreenName);
441 private void UserPicture_MouseEnter(object sender, EventArgs e)
443 this.UserPicture.Cursor = Cursors.Hand;
446 private void UserPicture_MouseLeave(object sender, EventArgs e)
448 this.UserPicture.Cursor = Cursors.Default;
451 private async void PostBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs e)
453 if (e.Url.AbsoluteUri != "about:blank")
455 await this.ShowPostDetails(this.CurrentPost); // 現在の発言を表示し直す (Navigated の段階ではキャンセルできない)
456 await this.Owner.OpenUriInBrowserAsync(e.Url.OriginalString);
460 private async void PostBrowser_Navigating(object sender, WebBrowserNavigatingEventArgs e)
462 if (e.Url.Scheme == "data")
464 this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
466 else if (e.Url.AbsoluteUri != "about:blank")
469 // Ctrlを押しながらリンクを開いた場合は、設定と逆の動作をするフラグを true としておく
470 await this.Owner.OpenUriAsync(e.Url, MyCommon.IsKeyDown(Keys.Control));
474 private async void PostBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
477 bool KeyRes = this.Owner.CommonKeyDown(e.KeyData, FocusedControl.PostBrowser, out asyncTask);
484 if (Enum.IsDefined(typeof(Shortcut), (Shortcut)e.KeyData))
486 var shortcut = (Shortcut)e.KeyData;
491 case Shortcut.CtrlIns:
495 // その他のショートカットキーは無効にする
502 if (asyncTask != null)
506 private void PostBrowser_StatusTextChanged(object sender, EventArgs e)
510 if (PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal)
511 || PostBrowser.StatusText.StartsWith("ftp", StringComparison.Ordinal)
512 || PostBrowser.StatusText.StartsWith("data", StringComparison.Ordinal))
514 this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&"));
516 if (string.IsNullOrEmpty(PostBrowser.StatusText))
518 this.RaiseStatusChanged(statusText: "");
526 private async void SourceLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
528 var sourceUri = this.CurrentPost?.SourceUri;
529 if (sourceUri != null && e.Button == MouseButtons.Left)
531 await this.Owner.OpenUriInBrowserAsync(sourceUri.AbsoluteUri);
535 private void SourceLinkLabel_MouseEnter(object sender, EventArgs e)
537 var sourceUri = this.CurrentPost?.SourceUri;
538 if (sourceUri != null)
540 this.RaiseStatusChanged(MyCommon.ConvertToReadableUrl(sourceUri.AbsoluteUri));
544 private void SourceLinkLabel_MouseLeave(object sender, EventArgs e)
546 this.RaiseStatusChanged(statusText: "");
551 #region ContextMenuUserPicture
553 private void ContextMenuUserPicture_Opening(object sender, CancelEventArgs e)
555 //発言詳細のアイコン右クリック時のメニュー制御
556 if (this.CurrentPost != null)
558 string name = this.CurrentPost.ImageUrl;
559 if (!string.IsNullOrEmpty(name))
561 int idx = name.LastIndexOf('/');
564 name = Path.GetFileName(name.Substring(idx));
565 if (name.Contains("_normal.") || name.EndsWith("_normal", StringComparison.Ordinal))
567 name = name.Replace("_normal", "");
568 this.IconNameToolStripMenuItem.Text = name;
569 this.IconNameToolStripMenuItem.Enabled = true;
573 this.IconNameToolStripMenuItem.Enabled = false;
574 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
579 this.IconNameToolStripMenuItem.Enabled = false;
580 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
583 this.ReloadIconToolStripMenuItem.Enabled = true;
585 if (this.IconCache.TryGetFromCache(this.CurrentPost.ImageUrl) != null)
587 this.SaveIconPictureToolStripMenuItem.Enabled = true;
591 this.SaveIconPictureToolStripMenuItem.Enabled = false;
596 this.IconNameToolStripMenuItem.Enabled = false;
597 this.ReloadIconToolStripMenuItem.Enabled = false;
598 this.SaveIconPictureToolStripMenuItem.Enabled = false;
599 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1;
604 this.IconNameToolStripMenuItem.Enabled = false;
605 this.ReloadIconToolStripMenuItem.Enabled = false;
606 this.SaveIconPictureToolStripMenuItem.Enabled = false;
607 this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText2;
609 if (this.CurrentPost != null)
611 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
613 FollowToolStripMenuItem.Enabled = false;
614 UnFollowToolStripMenuItem.Enabled = false;
615 ShowFriendShipToolStripMenuItem.Enabled = false;
616 ShowUserStatusToolStripMenuItem.Enabled = true;
617 SearchPostsDetailNameToolStripMenuItem.Enabled = true;
618 SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
619 ListManageUserContextToolStripMenuItem3.Enabled = true;
623 FollowToolStripMenuItem.Enabled = true;
624 UnFollowToolStripMenuItem.Enabled = true;
625 ShowFriendShipToolStripMenuItem.Enabled = true;
626 ShowUserStatusToolStripMenuItem.Enabled = true;
627 SearchPostsDetailNameToolStripMenuItem.Enabled = true;
628 SearchAtPostsDetailNameToolStripMenuItem.Enabled = true;
629 ListManageUserContextToolStripMenuItem3.Enabled = true;
634 FollowToolStripMenuItem.Enabled = false;
635 UnFollowToolStripMenuItem.Enabled = false;
636 ShowFriendShipToolStripMenuItem.Enabled = false;
637 ShowUserStatusToolStripMenuItem.Enabled = false;
638 SearchPostsDetailNameToolStripMenuItem.Enabled = false;
639 SearchAtPostsDetailNameToolStripMenuItem.Enabled = false;
640 ListManageUserContextToolStripMenuItem3.Enabled = false;
644 private async void FollowToolStripMenuItem_Click(object sender, EventArgs e)
646 if (this.CurrentPost == null)
649 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
652 await this.Owner.FollowCommand(this.CurrentPost.ScreenName);
655 private async void UnFollowToolStripMenuItem_Click(object sender, EventArgs e)
657 if (this.CurrentPost == null)
660 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
663 await this.Owner.RemoveCommand(this.CurrentPost.ScreenName, false);
666 private async void ShowFriendShipToolStripMenuItem_Click(object sender, EventArgs e)
668 if (this.CurrentPost == null)
671 if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId)
674 await this.Owner.ShowFriendship(this.CurrentPost.ScreenName);
677 // ListManageUserContextToolStripMenuItem3.Click は ListManageUserContextToolStripMenuItem_Click を共用
679 private async void ShowUserStatusToolStripMenuItem_Click(object sender, EventArgs e)
681 if (this.CurrentPost == null)
684 await this.Owner.ShowUserStatus(this.CurrentPost.ScreenName, false);
687 private void SearchPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
689 if (this.CurrentPost == null)
692 this.Owner.AddNewTabForUserTimeline(this.CurrentPost.ScreenName);
695 private void SearchAtPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e)
697 if (this.CurrentPost == null)
700 this.Owner.AddNewTabForSearch("@" + this.CurrentPost.ScreenName);
703 private async void IconNameToolStripMenuItem_Click(object sender, EventArgs e)
705 var imageUrl = this.CurrentPost?.ImageUrl;
706 if (string.IsNullOrEmpty(imageUrl))
709 await this.Owner.OpenUriInBrowserAsync(imageUrl.Remove(imageUrl.LastIndexOf("_normal", StringComparison.Ordinal), 7)); // "_normal".Length
712 private async void ReloadIconToolStripMenuItem_Click(object sender, EventArgs e)
714 var imageUrl = this.CurrentPost?.ImageUrl;
715 if (string.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 (string.IsNullOrEmpty(imageUrl))
727 this.Owner.SaveFileDialog1.FileName = imageUrl.Substring(imageUrl.LastIndexOf('/') + 1);
729 if (this.Owner.SaveFileDialog1.ShowDialog() == DialogResult.OK)
733 using (Image orgBmp = new Bitmap(IconCache.TryGetFromCache(imageUrl).Image))
735 using (Bitmap bmp2 = new Bitmap(orgBmp.Size.Width, orgBmp.Size.Height))
737 using (Graphics g = Graphics.FromImage(bmp2))
739 g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
740 g.DrawImage(orgBmp, 0, 0, orgBmp.Size.Width, orgBmp.Size.Height);
742 bmp2.Save(this.Owner.SaveFileDialog1.FileName);
748 //処理中にキャッシュアウトする可能性あり
755 #region ContextMenuPostBrowser
757 private void ContextMenuPostBrowser_Opening(object ender, CancelEventArgs e)
760 if (PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal))
762 this._postBrowserStatusText = PostBrowser.StatusText;
763 string name = GetUserId();
764 UrlCopyContextMenuItem.Enabled = true;
767 FollowContextMenuItem.Enabled = true;
768 RemoveContextMenuItem.Enabled = true;
769 FriendshipContextMenuItem.Enabled = true;
770 ShowUserStatusContextMenuItem.Enabled = true;
771 SearchPostsDetailToolStripMenuItem.Enabled = true;
772 IdFilterAddMenuItem.Enabled = true;
773 ListManageUserContextToolStripMenuItem.Enabled = true;
774 SearchAtPostsDetailToolStripMenuItem.Enabled = true;
778 FollowContextMenuItem.Enabled = false;
779 RemoveContextMenuItem.Enabled = false;
780 FriendshipContextMenuItem.Enabled = false;
781 ShowUserStatusContextMenuItem.Enabled = false;
782 SearchPostsDetailToolStripMenuItem.Enabled = false;
783 IdFilterAddMenuItem.Enabled = false;
784 ListManageUserContextToolStripMenuItem.Enabled = false;
785 SearchAtPostsDetailToolStripMenuItem.Enabled = false;
788 if (Regex.IsMatch(this._postBrowserStatusText, @"^https?://twitter.com/search\?q=%23"))
789 UseHashtagMenuItem.Enabled = true;
791 UseHashtagMenuItem.Enabled = false;
795 this._postBrowserStatusText = "";
796 UrlCopyContextMenuItem.Enabled = false;
797 FollowContextMenuItem.Enabled = false;
798 RemoveContextMenuItem.Enabled = false;
799 FriendshipContextMenuItem.Enabled = false;
800 ShowUserStatusContextMenuItem.Enabled = false;
801 SearchPostsDetailToolStripMenuItem.Enabled = false;
802 SearchAtPostsDetailToolStripMenuItem.Enabled = false;
803 UseHashtagMenuItem.Enabled = false;
804 IdFilterAddMenuItem.Enabled = false;
805 ListManageUserContextToolStripMenuItem.Enabled = false;
807 // 文字列選択されていないときは選択文字列関係の項目を非表示に
808 string _selText = this.PostBrowser.GetSelectedText();
809 if (_selText == null)
811 SelectionSearchContextMenuItem.Enabled = false;
812 SelectionCopyContextMenuItem.Enabled = false;
813 SelectionTranslationToolStripMenuItem.Enabled = false;
817 SelectionSearchContextMenuItem.Enabled = true;
818 SelectionCopyContextMenuItem.Enabled = true;
819 SelectionTranslationToolStripMenuItem.Enabled = true;
821 //発言内に自分以外のユーザーが含まれてればフォロー状態全表示を有効に
822 MatchCollection ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
823 bool fAllFlag = false;
824 foreach (Match mu in ma)
826 if (mu.Result("${ScreenName}").ToLowerInvariant() != this.Owner.TwitterInstance.Username.ToLowerInvariant())
832 this.FriendshipAllMenuItem.Enabled = fAllFlag;
834 if (this.CurrentPost == null)
835 TranslationToolStripMenuItem.Enabled = false;
837 TranslationToolStripMenuItem.Enabled = true;
842 private async void SearchGoogleContextMenuItem_Click(object sender, EventArgs e)
844 await this.DoSearchToolStrip(Properties.Resources.SearchItem2Url);
847 private async void SearchWikipediaContextMenuItem_Click(object sender, EventArgs e)
849 await this.DoSearchToolStrip(Properties.Resources.SearchItem1Url);
852 private async void SearchPublicSearchContextMenuItem_Click(object sender, EventArgs e)
854 await this.DoSearchToolStrip(Properties.Resources.SearchItem4Url);
857 private void CurrentTabToolStripMenuItem_Click(object sender, EventArgs e)
859 //発言詳細の選択文字列で現在のタブを検索
860 string _selText = this.PostBrowser.GetSelectedText();
862 if (_selText != null)
864 var searchOptions = new SearchWordDialog.SearchOptions(
865 SearchWordDialog.SearchType.Timeline,
868 caseSensitive: false,
871 this.Owner.SearchDialog.ResultOptions = searchOptions;
873 this.Owner.DoTabSearch(
875 searchOptions.CaseSensitive,
876 searchOptions.UseRegex,
877 TweenMain.SEARCHTYPE.NextSearch);
881 private void SelectionCopyContextMenuItem_Click(object sender, EventArgs e)
884 string _selText = this.PostBrowser.GetSelectedText();
887 Clipboard.SetDataObject(_selText, false, 5, 100);
891 MessageBox.Show(ex.Message);
895 private void UrlCopyContextMenuItem_Click(object sender, EventArgs e)
899 foreach (var link in this.PostBrowser.Document.Links.Cast<HtmlElement>())
901 if (link.GetAttribute("href") == this._postBrowserStatusText)
903 var linkStr = link.GetAttribute("title");
904 if (string.IsNullOrEmpty(linkStr))
905 linkStr = link.GetAttribute("href");
907 Clipboard.SetDataObject(linkStr, false, 5, 100);
912 Clipboard.SetDataObject(this._postBrowserStatusText, false, 5, 100);
916 MessageBox.Show(ex.Message);
920 private void SelectionAllContextMenuItem_Click(object sender, EventArgs e)
923 PostBrowser.Document.ExecCommand("SelectAll", false, null);
926 private async void FollowContextMenuItem_Click(object sender, EventArgs e)
928 string name = GetUserId();
930 await this.Owner.FollowCommand(name);
933 private async void RemoveContextMenuItem_Click(object sender, EventArgs e)
935 string name = GetUserId();
937 await this.Owner.RemoveCommand(name, false);
940 private async void FriendshipContextMenuItem_Click(object sender, EventArgs e)
942 string name = GetUserId();
944 await this.Owner.ShowFriendship(name);
947 private async void FriendshipAllMenuItem_Click(object sender, EventArgs e)
949 MatchCollection ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?""");
950 List<string> ids = new List<string>();
951 foreach (Match mu in ma)
953 if (mu.Result("${ScreenName}").ToLower() != this.Owner.TwitterInstance.Username.ToLower())
955 ids.Add(mu.Result("${ScreenName}"));
959 await this.Owner.ShowFriendship(ids.ToArray());
962 private async void ShowUserStatusContextMenuItem_Click(object sender, EventArgs e)
964 string name = GetUserId();
966 await this.Owner.ShowUserStatus(name);
969 private void SearchPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
971 string name = GetUserId();
972 if (name != null) this.Owner.AddNewTabForUserTimeline(name);
975 private void SearchAtPostsDetailToolStripMenuItem_Click(object sender, EventArgs e)
977 string name = GetUserId();
978 if (name != null) this.Owner.AddNewTabForSearch("@" + name);
981 private void IdFilterAddMenuItem_Click(object sender, EventArgs e)
983 string name = GetUserId();
985 this.Owner.AddFilterRuleByScreenName(name);
988 private void ListManageUserContextToolStripMenuItem_Click(object sender, EventArgs e)
990 ToolStripMenuItem menuItem = (ToolStripMenuItem)sender;
993 if (menuItem.Owner == this.ContextMenuPostBrowser)
996 if (user == null) return;
998 else if (this.CurrentPost != null)
1000 user = this.CurrentPost.ScreenName;
1007 this.Owner.ListManageUserContext(user);
1010 private void UseHashtagMenuItem_Click(object sender, EventArgs e)
1012 Match m = Regex.Match(this._postBrowserStatusText, @"^https?://twitter.com/search\?q=%23(?<hash>.+)$");
1014 this.Owner.SetPermanentHashtag(Uri.UnescapeDataString(m.Groups["hash"].Value));
1017 private async void SelectionTranslationToolStripMenuItem_Click(object sender, EventArgs e)
1019 var text = this.PostBrowser.GetSelectedText();
1020 await this.DoTranslation(text);
1023 private async void TranslationToolStripMenuItem_Click(object sender, EventArgs e)
1025 await this.DoTranslation();
1030 #region ContextMenuSource
1032 private void ContextMenuSource_Opening(object sender, CancelEventArgs e)
1034 if (this.CurrentPost == null || this.CurrentPost.IsDeleted || this.CurrentPost.IsDm)
1036 SourceCopyMenuItem.Enabled = false;
1037 SourceUrlCopyMenuItem.Enabled = false;
1041 SourceCopyMenuItem.Enabled = true;
1042 SourceUrlCopyMenuItem.Enabled = true;
1046 private void SourceCopyMenuItem_Click(object sender, EventArgs e)
1048 if (this.CurrentPost == null)
1053 Clipboard.SetDataObject(this.CurrentPost.Source, false, 5, 100);
1055 catch (Exception ex)
1057 MessageBox.Show(ex.Message);
1061 private void SourceUrlCopyMenuItem_Click(object sender, EventArgs e)
1063 var sourceUri = this.CurrentPost?.SourceUri;
1064 if (sourceUri == null)
1069 Clipboard.SetDataObject(sourceUri.AbsoluteUri, false, 5, 100);
1071 catch (Exception ex)
1073 MessageBox.Show(ex.Message);
1080 public class TweetDetailsViewStatusChengedEventArgs : EventArgs
1082 /// <summary>ステータスバーに表示するテキスト</summary>
1084 /// 空文字列の場合は <see cref="TweenMain"/> の既定のテキストを表示する
1086 public string StatusText { get; }
1088 public TweetDetailsViewStatusChengedEventArgs(string statusText)
1090 this.StatusText = statusText;