// OpenTween - Client of Twitter // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) // (c) 2008-2011 Moz (@syo68k) // (c) 2008-2011 takeshik (@takeshik) // (c) 2010-2011 anis774 (@anis774) // (c) 2010-2011 fantasticswallow (@f_swallow) // (c) 2011 kim_upsilon (@kim_upsilon) // All rights reserved. // // This file is part of OpenTween. // // This program is free software; you can redistribute it and/or modify it // under the terms of the GNU General public License as published by the Free // Software Foundation; either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License // for more details. // // You should have received a copy of the GNU General public License along // with this program. If not, see , or write to // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor, // Boston, MA 02110-1301, USA. #nullable enable using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using OpenTween.Models; using OpenTween.Setting; namespace OpenTween { public partial class TweetDetailsView : UserControl { private TweenMain Owner => this.owner ?? throw this.NotInitializedException(); /// プロフィール画像のキャッシュ private ImageCache IconCache => this.iconCache ?? throw this.NotInitializedException(); private DetailsHtmlBuilder HtmlBuilder => this.detailsHtmlBuilder ?? throw this.NotInitializedException(); /// のダンプを表示するか [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool DumpPostClass { get; set; } /// 現在表示中の発言 public PostClass? CurrentPost { get; private set; } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ThemeManager Theme { get => this.themeManager ?? throw this.NotInitializedException(); set => this.themeManager = value; } [DefaultValue(false)] public new bool TabStop { get => base.TabStop; set => base.TabStop = value; } /// ステータスバーに表示するテキストの変化を通知するイベント public event EventHandler? StatusChanged; /// 展開時の .StatusText を保持するフィールド private string postBrowserStatusText = ""; private TweenMain? owner; private ImageCache? iconCache; private ThemeManager? themeManager; private DetailsHtmlBuilder? detailsHtmlBuilder; public TweetDetailsView() { this.InitializeComponent(); this.TabStop = false; // 発言詳細部の初期化 this.NameLinkLabel.Text = ""; this.DateTimeLabel.Text = ""; this.SourceLinkLabel.Text = ""; new InternetSecurityManager(this.PostBrowser); this.PostBrowser.AllowWebBrowserDrop = false; // COMException を回避するため、ActiveX の初期化が終わってから設定する } public void Initialize(TweenMain owner, ImageCache iconCache, ThemeManager themeManager, DetailsHtmlBuilder detailsHtmlBuilder) { this.owner = owner; this.iconCache = iconCache; this.themeManager = themeManager; this.detailsHtmlBuilder = detailsHtmlBuilder; } private Exception NotInitializedException() => new InvalidOperationException("Cannot call before initialization"); public void ClearPostBrowser() => this.PostBrowser.DocumentText = this.HtmlBuilder.Build(""); public async Task ShowPostDetails(PostClass post) { this.CurrentPost = post; var loadTasks = new TaskCollection(); using (ControlTransaction.Update(this.TableLayoutPanel1)) { this.SourceLinkLabel.Text = post.Source; this.SourceLinkLabel.TabStop = false; // Text を更新すると勝手に true にされる string nameText; if (post.IsDm) { if (post.IsOwl) nameText = "DM FROM <- "; else nameText = "DM TO -> "; } else { nameText = ""; } nameText += post.ScreenName + "/" + post.Nickname; if (post.RetweetedId != null) nameText += $" (RT:{post.RetweetedBy})"; this.NameLinkLabel.Text = nameText; var nameForeColor = SystemColors.ControlText; if (post.IsOwl && (SettingManager.Instance.Common.OneWayLove || post.IsDm)) nameForeColor = this.Theme.ColorOWL; if (post.RetweetedId != null) nameForeColor = this.Theme.ColorRetweet; if (post.IsFav) nameForeColor = this.Theme.ColorFav; this.NameLinkLabel.LinkColor = nameForeColor; this.NameLinkLabel.ActiveLinkColor = nameForeColor; loadTasks.Add(() => this.SetUserPictureAsync(post.ImageUrl)); this.DateTimeLabel.Text = post.CreatedAt.ToLocalTimeString(); } if (this.DumpPostClass) { var sb = new StringBuilder(512); sb.Append("-----Start PostClass Dump
"); sb.AppendFormat("TextFromApi : {0}
", post.TextFromApi); sb.AppendFormat("(PlainText) : {0}
", post.TextFromApi); sb.AppendFormat("StatusId : {0}
", post.StatusId); sb.AppendFormat("ImageUrl : {0}
", post.ImageUrl); sb.AppendFormat("InReplyToStatusId : {0}
", post.InReplyToStatusId); sb.AppendFormat("InReplyToUser : {0}
", post.InReplyToUser); sb.AppendFormat("IsDM : {0}
", post.IsDm); sb.AppendFormat("IsFav : {0}
", post.IsFav); sb.AppendFormat("IsMark : {0}
", post.IsMark); sb.AppendFormat("IsMe : {0}
", post.IsMe); sb.AppendFormat("IsOwl : {0}
", post.IsOwl); sb.AppendFormat("IsProtect : {0}
", post.IsProtect); sb.AppendFormat("IsRead : {0}
", post.IsRead); sb.AppendFormat("IsReply : {0}
", post.IsReply); foreach (var nm in post.ReplyToList.Select(x => x.ScreenName)) { sb.AppendFormat("ReplyToList : {0}
", nm); } sb.AppendFormat("ScreenName : {0}
", post.ScreenName); sb.AppendFormat("NickName : {0}
", post.Nickname); sb.AppendFormat("Text : {0}
", post.Text); sb.AppendFormat("(PlainText) : {0}
", post.Text); sb.AppendFormat("CreatedAt : {0}
", post.CreatedAt.ToLocalTimeString()); sb.AppendFormat("Source : {0}
", post.Source); sb.AppendFormat("UserId : {0}
", post.UserId); sb.AppendFormat("FilterHit : {0}
", post.FilterHit); sb.AppendFormat("RetweetedBy : {0}
", post.RetweetedBy); sb.AppendFormat("RetweetedId : {0}
", post.RetweetedId); sb.AppendFormat("Media.Count : {0}
", post.Media.Count); if (post.Media.Count > 0) { for (var i = 0; i < post.Media.Count; i++) { var info = post.Media[i]; sb.AppendFormat("Media[{0}].Url : {1}
", i, info.Url); sb.AppendFormat("Media[{0}].VideoUrl : {1}
", i, info.VideoUrl ?? "---"); } } sb.Append("-----End PostClass Dump
"); this.PostBrowser.DocumentText = this.HtmlBuilder.Build(sb.ToString()); return; } using (ControlTransaction.Update(this.PostBrowser)) { this.PostBrowser.DocumentText = this.HtmlBuilder.Build(post.IsDeleted ? "(DELETED)" : post.Text); this.PostBrowser.Document.Window.ScrollTo(0, 0); } loadTasks.Add(() => this.AppendQuoteTweetAsync(post)); await loadTasks.RunAll(); } public void ScrollDownPostBrowser(bool forward) { var doc = this.PostBrowser.Document; if (doc == null) return; var tags = doc.GetElementsByTagName("html"); if (tags.Count > 0) { if (forward) tags[0].ScrollTop += this.Theme.FontDetail.Height; else tags[0].ScrollTop -= this.Theme.FontDetail.Height; } } public void PageDownPostBrowser(bool forward) { var doc = this.PostBrowser.Document; if (doc == null) return; var tags = doc.GetElementsByTagName("html"); if (tags.Count > 0) { if (forward) tags[0].ScrollTop += this.PostBrowser.ClientRectangle.Height - this.Theme.FontDetail.Height; else tags[0].ScrollTop -= this.PostBrowser.ClientRectangle.Height - this.Theme.FontDetail.Height; } } public HtmlElement[] GetLinkElements() { return this.PostBrowser.Document.Links.Cast() .Where(x => x.GetAttribute("className") != "tweet-quote-link") // 引用ツイートで追加されたリンクを除く .ToArray(); } private async Task SetUserPictureAsync(string normalImageUrl, bool force = false) { if (MyCommon.IsNullOrEmpty(normalImageUrl)) return; if (this.IconCache == null) return; this.ClearUserPicture(); var imageSize = Twitter.DecideProfileImageSize(this.UserPicture.Width); var cachedImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, imageSize); if (cachedImage != null) { // 既にキャッシュされていればそれを表示して終了 this.UserPicture.Image = cachedImage.Clone(); return; } // 小さいサイズの画像がキャッシュにある場合は高解像度の画像が取得できるまでの間表示する var fallbackImage = this.IconCache.TryGetLargerOrSameSizeFromCache(normalImageUrl, "mini"); if (fallbackImage != null) this.UserPicture.Image = fallbackImage.Clone(); await this.UserPicture.SetImageFromTask( async () => { var imageUrl = Twitter.CreateProfileImageUrl(normalImageUrl, imageSize); var image = await this.IconCache.DownloadImageAsync(imageUrl, force) .ConfigureAwait(false); return image.Clone(); }, useStatusImage: false ); } /// /// UserPicture.Image に設定されている画像を破棄します。 /// private void ClearUserPicture() { if (this.UserPicture.Image != null) { var oldImage = this.UserPicture.Image; this.UserPicture.Image = null; oldImage.Dispose(); } } /// /// 発言詳細欄のツイートURLを展開する /// private async Task AppendQuoteTweetAsync(PostClass post) { var quoteStatusIds = post.QuoteStatusIds; if (quoteStatusIds.Length == 0 && post.InReplyToStatusId == null) return; // 「読み込み中」テキストを表示 var loadingQuoteHtml = quoteStatusIds.Select(x => FormatQuoteTweetHtml(x, Properties.Resources.LoadingText, isReply: false)); var loadingReplyHtml = string.Empty; if (post.InReplyToStatusId != null) loadingReplyHtml = FormatQuoteTweetHtml(post.InReplyToStatusId, Properties.Resources.LoadingText, isReply: true); var body = post.Text + string.Concat(loadingQuoteHtml) + loadingReplyHtml; using (ControlTransaction.Update(this.PostBrowser)) this.PostBrowser.DocumentText = this.HtmlBuilder.Build(body); // 引用ツイートを読み込み var loadTweetTasks = quoteStatusIds.Select(x => this.CreateQuoteTweetHtml(x, isReply: false)).ToList(); if (post.InReplyToStatusId != null) loadTweetTasks.Add(this.CreateQuoteTweetHtml(post.InReplyToStatusId, isReply: true)); var quoteHtmls = await Task.WhenAll(loadTweetTasks); // 非同期処理中に表示中のツイートが変わっていたらキャンセルされたものと扱う if (this.CurrentPost != post || this.CurrentPost.IsDeleted) return; body = post.Text + string.Concat(quoteHtmls); using (ControlTransaction.Update(this.PostBrowser)) this.PostBrowser.DocumentText = this.HtmlBuilder.Build(body); } private async Task CreateQuoteTweetHtml(PostId statusId, bool isReply) { var post = TabInformations.GetInstance()[statusId]; if (post == null) { try { post = await this.Owner.TwitterInstance.GetStatusApi(false, statusId.ToTwitterStatusId()) .ConfigureAwait(false); } catch (WebApiException ex) { return FormatQuoteTweetHtml(statusId, WebUtility.HtmlEncode($"Err:{ex.Message}(GetStatus)"), isReply); } post.IsRead = true; if (!TabInformations.GetInstance().AddQuoteTweet(post)) return FormatQuoteTweetHtml(statusId, "This Tweet is unavailable.", isReply); } return FormatQuoteTweetHtml(post, isReply); } internal static string FormatQuoteTweetHtml(PostClass post, bool isReply) { var innerHtml = "

" + StripLinkTagHtml(post.Text) + "

" + " — " + WebUtility.HtmlEncode(post.Nickname) + " (@" + WebUtility.HtmlEncode(post.ScreenName) + ") " + WebUtility.HtmlEncode(post.CreatedAt.ToLocalTimeString()); return FormatQuoteTweetHtml(post.StatusId, innerHtml, isReply); } internal static string FormatQuoteTweetHtml(PostId statusId, string innerHtml, bool isReply) { var blockClassName = "quote-tweet"; if (isReply) blockClassName += " reply"; return $"""""" + $"""
{innerHtml}
""" + "
"; } /// /// 指定されたHTMLからリンクを除去します /// internal static string StripLinkTagHtml(string html) => Regex.Replace(html, @"]*>(.*?)", "$1"); // a 要素はネストされていない前提の正規表現パターン public async Task DoTranslation() { if (this.CurrentPost == null || this.CurrentPost.IsDeleted) return; await this.DoTranslation(this.CurrentPost.TextFromApi); } private async Task DoTranslation(string str) { if (MyCommon.IsNullOrEmpty(str)) return; var bing = new Bing(); try { var translatedText = await bing.TranslateAsync(str, langFrom: null, langTo: SettingManager.Instance.Common.TranslateLanguage); this.PostBrowser.DocumentText = this.HtmlBuilder.Build(translatedText); } catch (WebApiException e) { this.RaiseStatusChanged("Err:" + e.Message); } catch (OperationCanceledException) { this.RaiseStatusChanged("Err:Timeout"); } } private async Task DoSearchToolStrip(string url) { // 発言詳細で「選択文字列で検索」(選択文字列取得) var selText = this.PostBrowser.GetSelectedText(); if (selText != null) { if (url == Properties.Resources.SearchItem4Url) { // 公式検索 this.Owner.AddNewTabForSearch(selText); return; } var tmp = string.Format(url, Uri.EscapeDataString(selText)); await MyCommon.OpenInBrowserAsync(this, tmp); } } private string? GetUserId() { var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/(#!/)?(?[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?$"); if (m.Success && this.Owner.IsTwitterId(m.Result("${ScreenName}"))) return m.Result("${ScreenName}"); else return null; } protected void RaiseStatusChanged(string statusText) => this.StatusChanged?.Invoke(this, new TweetDetailsViewStatusChengedEventArgs(statusText)); private void TweetDetailsView_FontChanged(object sender, EventArgs e) { // OTBaseForm.GlobalFont による UI フォントの変更に対応 var origFont = this.NameLinkLabel.Font; this.NameLinkLabel.Font = new Font(this.Font.Name, origFont.Size, origFont.Style); } #region TableLayoutPanel1 private async void UserPicture_Click(object sender, EventArgs e) { var screenName = this.CurrentPost?.ScreenName; if (screenName != null) await this.Owner.ShowUserStatus(screenName, showInputDialog: false); } private async void PostBrowser_Navigated(object sender, WebBrowserNavigatedEventArgs e) { if (e.Url.AbsoluteUri != "about:blank") { await this.ShowPostDetails(this.CurrentPost!); // 現在の発言を表示し直す (Navigated の段階ではキャンセルできない) await MyCommon.OpenInBrowserAsync(this, e.Url); } } private async void PostBrowser_Navigating(object sender, WebBrowserNavigatingEventArgs e) { if (e.Url.Scheme == "data") { this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&")); } else if (e.Url.AbsoluteUri != "about:blank") { e.Cancel = true; // Ctrlを押しながらリンクを開いた場合は、設定と逆の動作をするフラグを true としておく await this.Owner.OpenUriAsync(e.Url, MyCommon.IsKeyDown(Keys.Control)); } } private async void PostBrowser_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) { var keyRes = this.Owner.CommonKeyDown(e.KeyData, FocusedControl.PostBrowser, out var asyncTask); if (keyRes) { e.IsInputKey = true; } else { if (Enum.IsDefined(typeof(Shortcut), (Shortcut)e.KeyData)) { var shortcut = (Shortcut)e.KeyData; switch (shortcut) { case Shortcut.CtrlA: case Shortcut.CtrlC: case Shortcut.CtrlIns: // 既定の動作を有効にする break; default: // その他のショートカットキーは無効にする e.IsInputKey = true; break; } } } if (asyncTask != null) await asyncTask; } private void PostBrowser_StatusTextChanged(object sender, EventArgs e) { try { if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal) || this.PostBrowser.StatusText.StartsWith("ftp", StringComparison.Ordinal) || this.PostBrowser.StatusText.StartsWith("data", StringComparison.Ordinal)) { this.RaiseStatusChanged(this.PostBrowser.StatusText.Replace("&", "&&")); } if (MyCommon.IsNullOrEmpty(this.PostBrowser.StatusText)) { this.RaiseStatusChanged(statusText: ""); } } catch (Exception) { } } private async void SourceLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { var sourceUri = this.CurrentPost?.SourceUri; if (sourceUri != null && e.Button == MouseButtons.Left) { await MyCommon.OpenInBrowserAsync(this, sourceUri); } } private void SourceLinkLabel_MouseEnter(object sender, EventArgs e) { var sourceUri = this.CurrentPost?.SourceUri; if (sourceUri != null) { this.RaiseStatusChanged(MyCommon.ConvertToReadableUrl(sourceUri.AbsoluteUri)); } } private void SourceLinkLabel_MouseLeave(object sender, EventArgs e) => this.RaiseStatusChanged(statusText: ""); #endregion #region ContextMenuUserPicture private void ContextMenuUserPicture_Opening(object sender, CancelEventArgs e) { // 発言詳細のアイコン右クリック時のメニュー制御 if (this.CurrentPost != null) { var name = this.CurrentPost.ImageUrl; if (!MyCommon.IsNullOrEmpty(name)) { var idx = name.LastIndexOf('/'); if (idx != -1) { name = Path.GetFileName(name.Substring(idx)); if (name.Contains("_normal.") || name.EndsWith("_normal", StringComparison.Ordinal)) { name = name.Replace("_normal", ""); this.IconNameToolStripMenuItem.Text = name; this.IconNameToolStripMenuItem.Enabled = true; } else { this.IconNameToolStripMenuItem.Enabled = false; this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1; } } else { this.IconNameToolStripMenuItem.Enabled = false; this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1; } this.ReloadIconToolStripMenuItem.Enabled = true; if (this.IconCache.TryGetFromCache(this.CurrentPost.ImageUrl) != null) { this.SaveIconPictureToolStripMenuItem.Enabled = true; } else { this.SaveIconPictureToolStripMenuItem.Enabled = false; } } else { this.IconNameToolStripMenuItem.Enabled = false; this.ReloadIconToolStripMenuItem.Enabled = false; this.SaveIconPictureToolStripMenuItem.Enabled = false; this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText1; } } else { this.IconNameToolStripMenuItem.Enabled = false; this.ReloadIconToolStripMenuItem.Enabled = false; this.SaveIconPictureToolStripMenuItem.Enabled = false; this.IconNameToolStripMenuItem.Text = Properties.Resources.ContextMenuStrip3_OpeningText2; } if (this.CurrentPost != null) { if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId) { this.FollowToolStripMenuItem.Enabled = false; this.UnFollowToolStripMenuItem.Enabled = false; this.ShowFriendShipToolStripMenuItem.Enabled = false; this.ShowUserStatusToolStripMenuItem.Enabled = true; this.SearchPostsDetailNameToolStripMenuItem.Enabled = true; this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false; this.ListManageUserContextToolStripMenuItem3.Enabled = true; } else { this.FollowToolStripMenuItem.Enabled = true; this.UnFollowToolStripMenuItem.Enabled = true; this.ShowFriendShipToolStripMenuItem.Enabled = true; this.ShowUserStatusToolStripMenuItem.Enabled = true; this.SearchPostsDetailNameToolStripMenuItem.Enabled = true; this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = true; this.ListManageUserContextToolStripMenuItem3.Enabled = true; } } else { this.FollowToolStripMenuItem.Enabled = false; this.UnFollowToolStripMenuItem.Enabled = false; this.ShowFriendShipToolStripMenuItem.Enabled = false; this.ShowUserStatusToolStripMenuItem.Enabled = false; this.SearchPostsDetailNameToolStripMenuItem.Enabled = false; this.SearchAtPostsDetailNameToolStripMenuItem.Enabled = false; this.ListManageUserContextToolStripMenuItem3.Enabled = false; } } private async void FollowToolStripMenuItem_Click(object sender, EventArgs e) { if (this.CurrentPost == null) return; if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId) return; await this.Owner.FollowCommand(this.CurrentPost.ScreenName); } private async void UnFollowToolStripMenuItem_Click(object sender, EventArgs e) { if (this.CurrentPost == null) return; if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId) return; await this.Owner.RemoveCommand(this.CurrentPost.ScreenName, false); } private async void ShowFriendShipToolStripMenuItem_Click(object sender, EventArgs e) { if (this.CurrentPost == null) return; if (this.CurrentPost.UserId == this.Owner.TwitterInstance.UserId) return; await this.Owner.ShowFriendship(this.CurrentPost.ScreenName); } // ListManageUserContextToolStripMenuItem3.Click は ListManageUserContextToolStripMenuItem_Click を共用 private async void ShowUserStatusToolStripMenuItem_Click(object sender, EventArgs e) { if (this.CurrentPost == null) return; await this.Owner.ShowUserStatus(this.CurrentPost.ScreenName, false); } private async void SearchPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e) { if (this.CurrentPost == null) return; await this.Owner.AddNewTabForUserTimeline(this.CurrentPost.ScreenName); } private void SearchAtPostsDetailNameToolStripMenuItem_Click(object sender, EventArgs e) { if (this.CurrentPost == null) return; this.Owner.AddNewTabForSearch("@" + this.CurrentPost.ScreenName); } private async void IconNameToolStripMenuItem_Click(object sender, EventArgs e) { var imageNormalUrl = this.CurrentPost?.ImageUrl; if (MyCommon.IsNullOrEmpty(imageNormalUrl)) return; var imageOriginalUrl = Twitter.CreateProfileImageUrl(imageNormalUrl, "original"); await MyCommon.OpenInBrowserAsync(this, imageOriginalUrl); } private async void ReloadIconToolStripMenuItem_Click(object sender, EventArgs e) { var imageUrl = this.CurrentPost?.ImageUrl; if (MyCommon.IsNullOrEmpty(imageUrl)) return; await this.SetUserPictureAsync(imageUrl, force: true); } private void SaveIconPictureToolStripMenuItem_Click(object sender, EventArgs e) { var imageUrl = this.CurrentPost?.ImageUrl; if (MyCommon.IsNullOrEmpty(imageUrl)) return; var memoryImage = this.IconCache.TryGetFromCache(imageUrl); if (memoryImage == null) return; this.Owner.SaveFileDialog1.FileName = imageUrl.Substring(imageUrl.LastIndexOf('/') + 1); if (this.Owner.SaveFileDialog1.ShowDialog() == DialogResult.OK) { try { using var orgBmp = new Bitmap(memoryImage.Image); using var bmp2 = new Bitmap(orgBmp.Size.Width, orgBmp.Size.Height); using (var g = Graphics.FromImage(bmp2)) { g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High; g.DrawImage(orgBmp, 0, 0, orgBmp.Size.Width, orgBmp.Size.Height); } bmp2.Save(this.Owner.SaveFileDialog1.FileName); } catch (Exception) { // 処理中にキャッシュアウトする可能性あり } } } #endregion #region ContextMenuPostBrowser private void ContextMenuPostBrowser_Opening(object ender, CancelEventArgs e) { // URLコピーの項目の表示/非表示 if (this.PostBrowser.StatusText.StartsWith("http", StringComparison.Ordinal)) { this.postBrowserStatusText = this.PostBrowser.StatusText; var name = this.GetUserId(); this.UrlCopyContextMenuItem.Enabled = true; if (name != null) { this.FollowContextMenuItem.Enabled = true; this.RemoveContextMenuItem.Enabled = true; this.FriendshipContextMenuItem.Enabled = true; this.ShowUserStatusContextMenuItem.Enabled = true; this.SearchPostsDetailToolStripMenuItem.Enabled = true; this.IdFilterAddMenuItem.Enabled = true; this.ListManageUserContextToolStripMenuItem.Enabled = true; this.SearchAtPostsDetailToolStripMenuItem.Enabled = true; } else { this.FollowContextMenuItem.Enabled = false; this.RemoveContextMenuItem.Enabled = false; this.FriendshipContextMenuItem.Enabled = false; this.ShowUserStatusContextMenuItem.Enabled = false; this.SearchPostsDetailToolStripMenuItem.Enabled = false; this.IdFilterAddMenuItem.Enabled = false; this.ListManageUserContextToolStripMenuItem.Enabled = false; this.SearchAtPostsDetailToolStripMenuItem.Enabled = false; } if (Regex.IsMatch(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23")) this.UseHashtagMenuItem.Enabled = true; else this.UseHashtagMenuItem.Enabled = false; } else { this.postBrowserStatusText = ""; this.UrlCopyContextMenuItem.Enabled = false; this.FollowContextMenuItem.Enabled = false; this.RemoveContextMenuItem.Enabled = false; this.FriendshipContextMenuItem.Enabled = false; this.ShowUserStatusContextMenuItem.Enabled = false; this.SearchPostsDetailToolStripMenuItem.Enabled = false; this.SearchAtPostsDetailToolStripMenuItem.Enabled = false; this.UseHashtagMenuItem.Enabled = false; this.IdFilterAddMenuItem.Enabled = false; this.ListManageUserContextToolStripMenuItem.Enabled = false; } // 文字列選択されていないときは選択文字列関係の項目を非表示に var selText = this.PostBrowser.GetSelectedText(); if (selText == null) { this.SelectionSearchContextMenuItem.Enabled = false; this.SelectionCopyContextMenuItem.Enabled = false; this.SelectionTranslationToolStripMenuItem.Enabled = false; } else { this.SelectionSearchContextMenuItem.Enabled = true; this.SelectionCopyContextMenuItem.Enabled = true; this.SelectionTranslationToolStripMenuItem.Enabled = true; } // 発言内に自分以外のユーザーが含まれてればフォロー状態全表示を有効に var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?"""); var fAllFlag = false; foreach (Match mu in ma) { if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase)) { fAllFlag = true; break; } } this.FriendshipAllMenuItem.Enabled = fAllFlag; if (this.CurrentPost == null) this.TranslationToolStripMenuItem.Enabled = false; else this.TranslationToolStripMenuItem.Enabled = true; e.Cancel = false; } private async void SearchGoogleContextMenuItem_Click(object sender, EventArgs e) => await this.DoSearchToolStrip(Properties.Resources.SearchItem2Url); private async void SearchWikipediaContextMenuItem_Click(object sender, EventArgs e) => await this.DoSearchToolStrip(Properties.Resources.SearchItem1Url); private async void SearchPublicSearchContextMenuItem_Click(object sender, EventArgs e) => await this.DoSearchToolStrip(Properties.Resources.SearchItem4Url); private void CurrentTabToolStripMenuItem_Click(object sender, EventArgs e) { // 発言詳細の選択文字列で現在のタブを検索 var selText = this.PostBrowser.GetSelectedText(); if (selText != null) { var searchOptions = new SearchWordDialog.SearchOptions( SearchWordDialog.SearchType.Timeline, selText, NewTab: false, CaseSensitive: false, UseRegex: false ); this.Owner.SearchDialog.ResultOptions = searchOptions; this.Owner.DoTabSearch( searchOptions.Query, searchOptions.CaseSensitive, searchOptions.UseRegex, TweenMain.SEARCHTYPE.NextSearch); } } private void SelectionCopyContextMenuItem_Click(object sender, EventArgs e) { // 発言詳細で「選択文字列をコピー」 var selText = this.PostBrowser.GetSelectedText(); try { Clipboard.SetDataObject(selText, false, 5, 100); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void UrlCopyContextMenuItem_Click(object sender, EventArgs e) { try { foreach (var link in this.PostBrowser.Document.Links.Cast()) { if (link.GetAttribute("href") == this.postBrowserStatusText) { var linkStr = link.GetAttribute("title"); if (MyCommon.IsNullOrEmpty(linkStr)) linkStr = link.GetAttribute("href"); Clipboard.SetDataObject(linkStr, false, 5, 100); return; } } Clipboard.SetDataObject(this.postBrowserStatusText, false, 5, 100); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void SelectionAllContextMenuItem_Click(object sender, EventArgs e) => this.PostBrowser.Document.ExecCommand("SelectAll", false, null); // 発言詳細ですべて選択 private async void FollowContextMenuItem_Click(object sender, EventArgs e) { var name = this.GetUserId(); if (name != null) await this.Owner.FollowCommand(name); } private async void RemoveContextMenuItem_Click(object sender, EventArgs e) { var name = this.GetUserId(); if (name != null) await this.Owner.RemoveCommand(name, false); } private async void FriendshipContextMenuItem_Click(object sender, EventArgs e) { var name = this.GetUserId(); if (name != null) await this.Owner.ShowFriendship(name); } private async void FriendshipAllMenuItem_Click(object sender, EventArgs e) { var ma = Regex.Matches(this.PostBrowser.DocumentText, @"href=""https?://twitter.com/(#!/)?(?[a-zA-Z0-9_]+)(/status(es)?/[0-9]+)?"""); var ids = new List(); foreach (Match mu in ma) { if (!mu.Result("${ScreenName}").Equals(this.Owner.TwitterInstance.Username, StringComparison.InvariantCultureIgnoreCase)) { ids.Add(mu.Result("${ScreenName}")); } } await this.Owner.ShowFriendship(ids.ToArray()); } private async void ShowUserStatusContextMenuItem_Click(object sender, EventArgs e) { var name = this.GetUserId(); if (name != null) await this.Owner.ShowUserStatus(name); } private async void SearchPostsDetailToolStripMenuItem_Click(object sender, EventArgs e) { var name = this.GetUserId(); if (name != null) await this.Owner.AddNewTabForUserTimeline(name); } private void SearchAtPostsDetailToolStripMenuItem_Click(object sender, EventArgs e) { var name = this.GetUserId(); if (name != null) this.Owner.AddNewTabForSearch("@" + name); } private void IdFilterAddMenuItem_Click(object sender, EventArgs e) { var name = this.GetUserId(); if (name != null) this.Owner.AddFilterRuleByScreenName(name); } private void ListManageUserContextToolStripMenuItem_Click(object sender, EventArgs e) { var menuItem = (ToolStripMenuItem)sender; string? user; if (menuItem.Owner == this.ContextMenuPostBrowser) { user = this.GetUserId(); if (user == null) return; } else if (this.CurrentPost != null) { user = this.CurrentPost.ScreenName; } else { return; } this.Owner.ListManageUserContext(user); } private void UseHashtagMenuItem_Click(object sender, EventArgs e) { var m = Regex.Match(this.postBrowserStatusText, @"^https?://twitter.com/search\?q=%23(?.+)$"); if (m.Success) this.Owner.SetPermanentHashtag(Uri.UnescapeDataString(m.Groups["hash"].Value)); } private async void SelectionTranslationToolStripMenuItem_Click(object sender, EventArgs e) { var text = this.PostBrowser.GetSelectedText(); await this.DoTranslation(text); } private async void TranslationToolStripMenuItem_Click(object sender, EventArgs e) => await this.DoTranslation(); #endregion #region ContextMenuSource private void ContextMenuSource_Opening(object sender, CancelEventArgs e) { if (this.CurrentPost == null || this.CurrentPost.IsDeleted || this.CurrentPost.IsDm) { this.SourceCopyMenuItem.Enabled = false; this.SourceUrlCopyMenuItem.Enabled = false; } else { this.SourceCopyMenuItem.Enabled = true; this.SourceUrlCopyMenuItem.Enabled = true; } } private void SourceCopyMenuItem_Click(object sender, EventArgs e) { if (this.CurrentPost == null) return; try { Clipboard.SetDataObject(this.CurrentPost.Source, false, 5, 100); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void SourceUrlCopyMenuItem_Click(object sender, EventArgs e) { var sourceUri = this.CurrentPost?.SourceUri; if (sourceUri == null) return; try { Clipboard.SetDataObject(sourceUri.AbsoluteUri, false, 5, 100); } catch (Exception ex) { MessageBox.Show(ex.Message); } } #endregion private async void NameLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { var screenName = this.CurrentPost?.ScreenName; if (screenName != null) await this.Owner.ShowUserStatus(screenName, showInputDialog: false); } private async void DateTimeLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { if (this.CurrentPost == null) return; if (this.CurrentPost.StatusId is not TwitterStatusId) return; var statusUrl = MyCommon.GetStatusUrl(this.CurrentPost); await MyCommon.OpenInBrowserAsync(this, statusUrl); } } public class TweetDetailsViewStatusChengedEventArgs : EventArgs { /// ステータスバーに表示するテキスト /// /// 空文字列の場合は の既定のテキストを表示する /// public string StatusText { get; } public TweetDetailsViewStatusChengedEventArgs(string statusText) => this.StatusText = statusText; } }