OSDN Git Service

93c423cabf7c7c49b28b85fb4625b47657a88274
[opentween/open-tween.git] / OpenTween / Tween.cs
1 // OpenTween - Client of Twitter
2 // Copyright (c) 2007-2011 kiri_feather (@kiri_feather) <kiri.feather@gmail.com>
3 //           (c) 2008-2011 Moz (@syo68k)
4 //           (c) 2008-2011 takeshik (@takeshik) <http://www.takeshik.org/>
5 //           (c) 2010-2011 anis774 (@anis774) <http://d.hatena.ne.jp/anis774/>
6 //           (c) 2010-2011 fantasticswallow (@f_swallow) <http://twitter.com/f_swallow>
7 //           (c) 2011      kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
8 // All rights reserved.
9 //
10 // This file is part of OpenTween.
11 //
12 // This program is free software; you can redistribute it and/or modify it
13 // under the terms of the GNU General public License as published by the Free
14 // Software Foundation; either version 3 of the License, or (at your option)
15 // any later version.
16 //
17 // This program is distributed in the hope that it will be useful, but
18 // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
19 // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General public License
20 // for more details.
21 //
22 // You should have received a copy of the GNU General public License along
23 // with this program. If not, see <http://www.gnu.org/licenses/>, or write to
24 // the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
25 // Boston, MA 02110-1301, USA.
26
27 #nullable enable
28
29 // コンパイル後コマンド
30 // "c:\Program Files\Microsoft.NET\SDK\v2.0\Bin\sgen.exe" /f /a:"$(TargetPath)"
31 // "C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\sgen.exe" /f /a:"$(TargetPath)"
32
33 using System;
34 using System.Collections.Concurrent;
35 using System.Collections.Generic;
36 using System.ComponentModel;
37 using System.Diagnostics;
38 using System.Diagnostics.CodeAnalysis;
39 using System.Drawing;
40 using System.Globalization;
41 using System.IO;
42 using System.Linq;
43 using System.Media;
44 using System.Net;
45 using System.Net.Http;
46 using System.Reflection;
47 using System.Runtime.InteropServices;
48 using System.Text;
49 using System.Text.RegularExpressions;
50 using System.Threading;
51 using System.Threading.Tasks;
52 using System.Windows.Forms;
53 using OpenTween.Api;
54 using OpenTween.Api.DataModel;
55 using OpenTween.Api.GraphQL;
56 using OpenTween.Api.TwitterV2;
57 using OpenTween.Connection;
58 using OpenTween.MediaUploadServices;
59 using OpenTween.Models;
60 using OpenTween.OpenTweenCustomControl;
61 using OpenTween.Setting;
62 using OpenTween.SocialProtocol;
63 using OpenTween.SocialProtocol.Twitter;
64 using OpenTween.Thumbnail;
65
66 namespace OpenTween
67 {
68     public partial class TweenMain : OTBaseForm
69     {
70         // 各種設定
71
72         /// <summary>画面サイズ</summary>
73         private Size mySize;
74
75         /// <summary>画面位置</summary>
76         private Point myLoc;
77
78         /// <summary>区切り位置</summary>
79         private int mySpDis;
80
81         /// <summary>発言欄区切り位置</summary>
82         private int mySpDis2;
83
84         /// <summary>プレビュー区切り位置</summary>
85         private int mySpDis3;
86
87         // 雑多なフラグ類
88         private bool initial; // true:起動時処理中
89         private bool initialLayout = true;
90         private bool ignoreConfigSave; // true:起動時処理中
91
92         /// <summary>タブドラッグ中フラグ(DoDragDropを実行するかの判定用)</summary>
93         private bool tabDrag;
94
95         private TabPage? beforeSelectedTab; // タブが削除されたときに前回選択されていたときのタブを選択する為に保持
96         private Point tabMouseDownPoint;
97
98         /// <summary>右クリックしたタブの名前(Tabコントロール機能不足対応)</summary>
99         private string? rclickTabName;
100
101         private readonly object syncObject = new(); // ロック用
102
103         private readonly DetailsHtmlBuilder detailsHtmlBuilder = new();
104
105         private bool myStatusError = false;
106         private bool myStatusOnline = false;
107         private bool soundfileListup = false;
108         private FormWindowState formWindowState = FormWindowState.Normal; // フォームの状態保存用 通知領域からアイコンをクリックして復帰した際に使用する
109
110         // 設定ファイル
111         private readonly SettingManager settings;
112
113         // ユーザーアカウント
114         private readonly AccountCollection accounts;
115
116 #pragma warning disable SA1300
117         private Twitter tw => ((TwitterAccount)this.accounts.Primary).Legacy; // AccountCollection への移行用
118 #pragma warning restore SA1300
119
120         // Growl呼び出し部
121         private readonly GrowlHelper gh = new(ApplicationSettings.ApplicationName);
122
123         // サブ画面インスタンス
124
125         /// <summary>検索画面インスタンス</summary>
126         internal SearchWordDialog SearchDialog = new();
127
128         private readonly OpenURL urlDialog = new();
129
130         /// <summary>@id補助</summary>
131         public AtIdSupplement AtIdSupl = null!;
132
133         /// <summary>Hashtag補助</summary>
134         public AtIdSupplement HashSupl = null!;
135
136         public HashtagManage HashMgr = null!;
137
138         // 表示フォント、色、アイコン
139         private ThemeManager themeManager;
140
141         /// <summary>アイコン画像リスト</summary>
142         private readonly ImageCache iconCache;
143
144         private readonly IconAssetsManager iconAssets;
145
146         private readonly ThumbnailGenerator thumbGenerator;
147
148         /// <summary>発言履歴</summary>
149         private readonly StatusTextHistory history = new();
150
151         // 発言投稿時のAPI引数(発言編集時に設定。手書きreplyでは設定されない)
152
153         /// <summary>リプライ先のステータスID・スクリーン名</summary>
154         private (PostId StatusId, string ScreenName)? inReplyTo = null;
155
156         // 時速表示用
157         private readonly List<DateTimeUtc> postTimestamps = new();
158         private readonly List<DateTimeUtc> favTimestamps = new();
159
160         // 以下DrawItem関連
161         private readonly StringFormat sfTab = new();
162
163         //////////////////////////////////////////////////////////////////////////////////////////////////////////
164
165         /// <summary>発言保持クラス</summary>
166         private readonly TabInformations statuses;
167
168         private TimelineListViewCache? listCache;
169         private TimelineListViewDrawer? listDrawer;
170         private readonly Dictionary<string, TimelineListViewState> listViewState = new();
171
172         private bool isColumnChanged = false;
173
174         private const int MaxWorderThreads = 20;
175         private readonly SemaphoreSlim workerSemaphore = new(MaxWorderThreads);
176         private readonly CancellationTokenSource workerCts = new();
177         private readonly IProgress<string> workerProgress = null!;
178
179         private int unreadCounter = -1;
180         private int unreadAtCounter = -1;
181
182         private readonly string[] columnOrgText = new string[9];
183         private readonly string[] columnText = new string[9];
184
185         private bool doFavRetweetFlags = false;
186
187         //////////////////////////////////////////////////////////////////////////////////////////////////////////
188
189         private readonly TimelineScheduler timelineScheduler = new();
190         private readonly DebounceTimer selectionDebouncer;
191         private readonly DebounceTimer saveConfigDebouncer;
192
193         private readonly string recommendedStatusFooter;
194
195         internal bool SeparateUrlAndFullwidthCharacter { get; set; } = false;
196
197         private bool preventSmsCommand = true;
198
199         // URL短縮のUndo用
200         private readonly record struct UrlUndo(
201             string Before,
202             string After
203         );
204
205         private List<UrlUndo>? urlUndoBuffer = null;
206
207         private readonly record struct ReplyChain(
208             PostId OriginalId,
209             PostId InReplyToId,
210             TabModel OriginalTab
211         );
212
213         /// <summary>[, ]でのリプライ移動の履歴</summary>
214         private Stack<ReplyChain>? replyChains;
215
216         /// <summary>ポスト選択履歴</summary>
217         private readonly Stack<(TabModel, PostClass?)> selectPostChains = new();
218
219         public TabModel CurrentTab
220             => this.statuses.SelectedTab;
221
222         public string CurrentTabName
223             => this.statuses.SelectedTabName;
224
225         public TabPage CurrentTabPage
226             => this.ListTab.TabPages[this.statuses.Tabs.IndexOf(this.CurrentTabName)];
227
228         public DetailsListView CurrentListView
229             => (DetailsListView)this.CurrentTabPage.Tag;
230
231         public PostClass? CurrentPost
232             => this.CurrentTab.SelectedPost;
233
234         public bool Use2ColumnsMode
235             => this.settings.Common.IconSize == MyCommon.IconSizes.Icon48_2;
236
237         /// <summary>検索処理タイプ</summary>
238         internal enum SEARCHTYPE
239         {
240             DialogSearch,
241             NextSearch,
242             PrevSearch,
243         }
244
245         private readonly HookGlobalHotkey hookGlobalHotkey;
246
247         public TweenMain(
248             SettingManager settingManager,
249             TabInformations tabInfo,
250             AccountCollection accounts,
251             ImageCache imageCache,
252             IconAssetsManager iconAssets,
253             ThumbnailGenerator thumbGenerator
254         )
255         {
256             this.settings = settingManager;
257             this.statuses = tabInfo;
258             this.accounts = accounts;
259             this.iconCache = imageCache;
260             this.iconAssets = iconAssets;
261             this.thumbGenerator = thumbGenerator;
262
263             this.InitializeComponent();
264
265             if (!this.DesignMode)
266             {
267                 // デザイナでの編集時にレイアウトが縦方向に数pxずれる問題の対策
268                 this.StatusText.Dock = DockStyle.Fill;
269             }
270
271             this.hookGlobalHotkey = new HookGlobalHotkey(this);
272
273             this.hookGlobalHotkey.HotkeyPressed += this.HookGlobalHotkey_HotkeyPressed;
274             this.gh.NotifyClicked += this.GrowlHelper_Callback;
275
276             // メイリオフォント指定時にタブの最小幅が広くなる問題の対策
277             this.ListTab.HandleCreated += (s, e) => NativeMethods.SetMinTabWidth((TabControl)s, 40);
278
279             this.ImageSelector.Visible = false;
280             this.ImageSelector.Enabled = false;
281             this.ImageSelector.FilePickDialog = this.OpenFileDialog1;
282
283             this.workerProgress = new Progress<string>(x => this.StatusLabel.Text = x);
284
285             this.ReplaceAppName();
286             this.InitializeShortcuts();
287
288             this.ignoreConfigSave = true;
289
290             this.TraceOutToolStripMenuItem.Checked = MyCommon.TraceFlag;
291
292             Microsoft.Win32.SystemEvents.PowerModeChanged += this.SystemEvents_PowerModeChanged;
293
294             Regex.CacheSize = 100;
295
296             // アイコン設定
297             this.Icon = this.iconAssets.IconMain; // メインフォーム(TweenMain)
298             this.NotifyIcon1.Icon = this.iconAssets.IconTray; // タスクトレイ
299             this.TabImage.Images.Add(this.iconAssets.IconTab); // タブ見出し
300
301             // <<<<<<<<<設定関連>>>>>>>>>
302             // 設定読み出し
303             this.LoadConfig();
304
305             // 現在の DPI と設定保存時の DPI との比を取得する
306             var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions);
307
308             this.initial = true;
309
310             // サムネイル関連の初期化
311             // プロキシ設定等の通信まわりの初期化が済んでから処理する
312             var imgazyobizinet = this.thumbGenerator.ImgAzyobuziNet;
313             imgazyobizinet.Enabled = this.settings.Common.EnableImgAzyobuziNet;
314             imgazyobizinet.DisabledInDM = this.settings.Common.ImgAzyobuziNetDisabledInDM;
315
316             Thumbnail.Services.TonTwitterCom.GetApiConnection = () => this.tw.Api.Connection;
317
318             // 画像投稿サービス
319             this.ImageSelector.Model.InitializeServices(this.tw, this.tw.Configuration);
320             this.ImageSelector.Model.SelectMediaService(this.settings.Common.UseImageServiceName, this.settings.Common.UseImageService);
321
322             this.tweetThumbnail1.Model.Initialize(this.thumbGenerator);
323
324             // ハッシュタグ/@id関連
325             this.AtIdSupl = new AtIdSupplement(this.settings.AtIdList.AtIdList, "@");
326             this.HashSupl = new AtIdSupplement(this.settings.Common.HashTags, "#");
327             this.HashMgr = new HashtagManage(this.HashSupl,
328                                     this.settings.Common.HashTags.ToArray(),
329                                     this.settings.Common.HashSelected,
330                                     this.settings.Common.HashIsPermanent,
331                                     this.settings.Common.HashIsHead,
332                                     this.settings.Common.HashIsNotAddToAtReply);
333             if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash) && this.HashMgr.IsPermanent) this.HashStripSplitButton.Text = this.HashMgr.UseHash;
334
335             // フォント&文字色&背景色保持
336             this.themeManager = new(this.settings.Local);
337             this.tweetDetailsView.Initialize(this, this.iconCache, this.themeManager, this.detailsHtmlBuilder);
338
339             // StringFormatオブジェクトへの事前設定
340             this.sfTab.Alignment = StringAlignment.Center;
341             this.sfTab.LineAlignment = StringAlignment.Center;
342
343             this.detailsHtmlBuilder.Prepare(this.settings.Common, this.themeManager);
344             this.tweetDetailsView.ClearPostBrowser();
345
346             this.recommendedStatusFooter = " [TWNv" + Regex.Replace(MyCommon.FileVersion.Replace(".", ""), "^0*", "") + "]";
347
348             this.inReplyTo = null;
349
350             // 各種ダイアログ設定
351             this.SearchDialog.Owner = this;
352             this.urlDialog.Owner = this;
353
354             // 新着バルーン通知のチェック状態設定
355             this.NewPostPopMenuItem.Checked = this.settings.Common.NewAllPop;
356             this.NotifyFileMenuItem.Checked = this.NewPostPopMenuItem.Checked;
357
358             // 新着取得時のリストスクロールをするか。trueならスクロールしない
359             this.ListLockMenuItem.Checked = this.settings.Common.ListLock;
360             this.LockListFileMenuItem.Checked = this.settings.Common.ListLock;
361             // サウンド再生(タブ別設定より優先)
362             this.PlaySoundMenuItem.Checked = this.settings.Common.PlaySound;
363             this.PlaySoundFileMenuItem.Checked = this.settings.Common.PlaySound;
364
365             // ウィンドウ設定
366             this.ClientSize = ScaleBy(configScaleFactor, this.settings.Local.FormSize);
367             this.mySize = this.ClientSize; // サイズ保持(最小化・最大化されたまま終了した場合の対応用)
368             this.myLoc = this.settings.Local.FormLocation;
369             // タイトルバー領域
370             if (this.WindowState != FormWindowState.Minimized)
371             {
372                 var tbarRect = new Rectangle(this.myLoc, new Size(this.mySize.Width, SystemInformation.CaptionHeight));
373                 var outOfScreen = true;
374                 if (Screen.AllScreens.Length == 1) // ハングするとの報告
375                 {
376                     foreach (var scr in Screen.AllScreens)
377                     {
378                         if (!Rectangle.Intersect(tbarRect, scr.Bounds).IsEmpty)
379                         {
380                             outOfScreen = false;
381                             break;
382                         }
383                     }
384
385                     if (outOfScreen)
386                         this.myLoc = new Point(0, 0);
387                 }
388                 this.DesktopLocation = this.myLoc;
389             }
390             this.TopMost = this.settings.Common.AlwaysTop;
391             this.mySpDis = ScaleBy(configScaleFactor.Height, this.settings.Local.SplitterDistance);
392             this.mySpDis2 = ScaleBy(configScaleFactor.Height, this.settings.Local.StatusTextHeight);
393             this.mySpDis3 = ScaleBy(configScaleFactor.Width, this.settings.Local.PreviewDistance);
394
395             this.PlaySoundMenuItem.Checked = this.settings.Common.PlaySound;
396             this.PlaySoundFileMenuItem.Checked = this.settings.Common.PlaySound;
397             // 入力欄
398             this.StatusText.Font = this.themeManager.FontInputFont;
399             this.StatusText.ForeColor = this.themeManager.ColorInputFont;
400
401             // SplitContainer2.Panel2MinSize を一行表示の入力欄の高さに合わせる (MS UI Gothic 12pt (96dpi) の場合は 19px)
402             this.StatusText.Multiline = false; // this.settings.Local.StatusMultiline の設定は後で反映される
403             this.SplitContainer2.Panel2MinSize = this.StatusText.Height;
404
405             // 必要であれば、発言一覧と発言詳細部・入力欄の上下を入れ替える
406             this.SplitContainer1.IsPanelInverted = !this.settings.Common.StatusAreaAtBottom;
407
408             // 全新着通知のチェック状態により、Reply&DMの新着通知有効無効切り替え(タブ別設定にするため削除予定)
409             if (this.settings.Common.UnreadManage == false)
410             {
411                 this.ReadedStripMenuItem.Enabled = false;
412                 this.UnreadStripMenuItem.Enabled = false;
413             }
414
415             // リンク先URL表示部の初期化(画面左下)
416             this.StatusLabelUrl.Text = "";
417             // 状態表示部の初期化(画面右下)
418             this.StatusLabel.Text = "";
419             this.StatusLabel.AutoToolTip = false;
420             this.StatusLabel.ToolTipText = "";
421             // 文字カウンタ初期化
422             this.lblLen.Text = this.GetRestStatusCount(this.FormatStatusTextExtended("")).ToString();
423
424             this.JumpReadOpMenuItem.ShortcutKeyDisplayString = "Space";
425             this.CopySTOTMenuItem.ShortcutKeyDisplayString = "Ctrl+C";
426             this.CopyURLMenuItem.ShortcutKeyDisplayString = "Ctrl+Shift+C";
427             this.CopyUserIdStripMenuItem.ShortcutKeyDisplayString = "Shift+Alt+C";
428
429             // SourceLinkLabel のテキストが SplitContainer2.Panel2.AccessibleName にセットされるのを防ぐ
430             // (タブオーダー順で SourceLinkLabel の次にある PostBrowser が TabStop = false となっているため、
431             // さらに次のコントロールである SplitContainer2.Panel2 の AccessibleName がデフォルトで SourceLinkLabel のテキストになってしまう)
432             this.SplitContainer2.Panel2.AccessibleName = "";
433
434             ////////////////////////////////////////////////////////////////////////////////
435             var sortOrder = (SortOrder)this.settings.Common.SortOrder;
436             var mode = this.settings.Common.SortColumn switch
437             {
438                 // 0:アイコン,5:未読マーク,6:プロテクト・フィルターマーク
439                 0 or 5 or 6 => ComparerMode.Id, // Idソートに読み替え
440                 1 => ComparerMode.Nickname, // ニックネーム
441                 2 => ComparerMode.Data, // 本文
442                 3 => ComparerMode.Id, // 時刻=発言Id
443                 4 => ComparerMode.Name, // 名前
444                 7 => ComparerMode.Source, // Source
445                 _ => ComparerMode.Id,
446             };
447             this.statuses.SetSortMode(mode, sortOrder);
448             ////////////////////////////////////////////////////////////////////////////////
449
450             this.ApplyListViewIconSize(this.settings.Common.IconSize);
451
452             // <<<<<<<<タブ関連>>>>>>>
453             foreach (var tab in this.statuses.Tabs)
454             {
455                 if (!this.AddNewTab(tab, startup: true))
456                     throw new TabException(Properties.Resources.TweenMain_LoadText1);
457             }
458
459             this.ListTabSelect(this.ListTab.SelectedTab);
460
461             // タブの位置を調整する
462             this.SetTabAlignment();
463
464             MyCommon.TwitterApiInfo.AccessLimitUpdated += this.TwitterApiStatus_AccessLimitUpdated;
465             Microsoft.Win32.SystemEvents.TimeChanged += this.SystemEvents_TimeChanged;
466
467             if (this.settings.Common.TabIconDisp)
468             {
469                 this.ListTab.DrawMode = TabDrawMode.Normal;
470             }
471             else
472             {
473                 this.ListTab.DrawMode = TabDrawMode.OwnerDrawFixed;
474                 this.ListTab.DrawItem += this.ListTab_DrawItem;
475                 this.ListTab.ImageList = null;
476             }
477
478             if (this.settings.Common.HotkeyEnabled)
479             {
480                 // グローバルホットキーの登録
481                 var modKey = HookGlobalHotkey.ModKeys.None;
482                 if ((this.settings.Common.HotkeyModifier & Keys.Alt) == Keys.Alt)
483                     modKey |= HookGlobalHotkey.ModKeys.Alt;
484                 if ((this.settings.Common.HotkeyModifier & Keys.Control) == Keys.Control)
485                     modKey |= HookGlobalHotkey.ModKeys.Ctrl;
486                 if ((this.settings.Common.HotkeyModifier & Keys.Shift) == Keys.Shift)
487                     modKey |= HookGlobalHotkey.ModKeys.Shift;
488                 if ((this.settings.Common.HotkeyModifier & Keys.LWin) == Keys.LWin)
489                     modKey |= HookGlobalHotkey.ModKeys.Win;
490
491                 this.hookGlobalHotkey.RegisterOriginalHotkey(this.settings.Common.HotkeyKey, this.settings.Common.HotkeyValue, modKey);
492             }
493
494             if (this.settings.Common.IsUseNotifyGrowl)
495                 this.gh.RegisterGrowl();
496
497             this.StatusLabel.Text = Properties.Resources.Form1_LoadText1;       // 画面右下の状態表示を変更
498
499             this.SetMainWindowTitle();
500             this.SetNotifyIconText();
501
502             if (this.settings.Common.MinimizeToTray && this.WindowState == FormWindowState.Minimized)
503             {
504                 this.Visible = false;
505             }
506
507             // タイマー設定
508
509             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Home] = () => this.InvokeAsync(() => this.RefreshTabAsync<HomeTabModel>());
510             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Mention] = () => this.InvokeAsync(() => this.RefreshTabAsync<MentionsTabModel>());
511             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Dm] = () => this.InvokeAsync(() => this.RefreshTabAsync<DirectMessagesTabModel>());
512             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.PublicSearch] = () => this.InvokeAsync(() => this.RefreshTabAsync<PublicSearchTabModel>());
513             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.User] = () => this.InvokeAsync(() => this.RefreshTabAsync<UserTimelineTabModel>());
514             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.List] = () => this.InvokeAsync(() => this.RefreshTabAsync<ListTimelineTabModel>());
515             this.timelineScheduler.UpdateFunc[TimelineSchedulerTaskType.Config] = () => this.InvokeAsync(() => Task.WhenAll(new[]
516             {
517                 this.DoGetFollowersMenu(),
518                 this.RefreshBlockIdsAsync(),
519                 this.RefreshMuteUserIdsAsync(),
520                 this.RefreshNoRetweetIdsAsync(),
521                 this.RefreshTwitterConfigurationAsync(),
522             }));
523             this.RefreshTimelineScheduler();
524
525             this.selectionDebouncer = DebounceTimer.Create(() => this.InvokeAsync(() => this.UpdateSelectedPost()), TimeSpan.FromMilliseconds(100), leading: true);
526             this.saveConfigDebouncer = DebounceTimer.Create(() => this.InvokeAsync(() => this.SaveConfigsAll(ifModified: true)), TimeSpan.FromSeconds(1));
527
528             // 更新中アイコンアニメーション間隔
529             this.TimerRefreshIcon.Interval = 200;
530             this.TimerRefreshIcon.Enabled = false;
531
532             this.ignoreConfigSave = false;
533             this.ApplyLayoutFromSettings();
534         }
535
536         private void TweenMain_Activated(object sender, EventArgs e)
537         {
538             // 画面がアクティブになったら、発言欄の背景色戻す
539             if (this.StatusText.Focused)
540             {
541                 this.StatusText_Enter(this.StatusText, System.EventArgs.Empty);
542             }
543         }
544
545         private bool disposed = false;
546
547         /// <summary>
548         /// 使用中のリソースをすべてクリーンアップします。
549         /// </summary>
550         /// <param name="disposing">マネージ リソースが破棄される場合 true、破棄されない場合は false です。</param>
551         protected override void Dispose(bool disposing)
552         {
553             base.Dispose(disposing);
554
555             if (this.disposed)
556                 return;
557
558             if (disposing)
559             {
560                 this.components?.Dispose();
561
562                 // 後始末
563                 this.SearchDialog.Dispose();
564                 this.urlDialog.Dispose();
565                 this.themeManager.Dispose();
566                 this.sfTab.Dispose();
567
568                 this.timelineScheduler.Dispose();
569                 this.workerCts.Cancel();
570                 this.thumbnailTokenSource?.Dispose();
571
572                 this.hookGlobalHotkey.Dispose();
573             }
574
575             // 終了時にRemoveHandlerしておかないとメモリリークする
576             // http://msdn.microsoft.com/ja-jp/library/microsoft.win32.systemevents.powermodechanged.aspx
577             Microsoft.Win32.SystemEvents.PowerModeChanged -= this.SystemEvents_PowerModeChanged;
578             Microsoft.Win32.SystemEvents.TimeChanged -= this.SystemEvents_TimeChanged;
579             MyCommon.TwitterApiInfo.AccessLimitUpdated -= this.TwitterApiStatus_AccessLimitUpdated;
580
581             this.disposed = true;
582         }
583
584         private void InitColumns(ListView list, bool startup)
585         {
586             this.InitColumnText();
587
588             ColumnHeader[]? columns = null;
589             try
590             {
591                 if (this.Use2ColumnsMode)
592                 {
593                     columns = new[]
594                     {
595                         new ColumnHeader(), // アイコン
596                         new ColumnHeader(), // 本文
597                     };
598
599                     columns[0].Text = this.columnText[0];
600                     columns[1].Text = this.columnText[2];
601
602                     if (startup)
603                     {
604                         var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width;
605
606                         columns[0].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[0]);
607                         columns[1].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[2]);
608                         columns[0].DisplayIndex = 0;
609                         columns[1].DisplayIndex = 1;
610                     }
611                     else
612                     {
613                         var idx = 0;
614                         foreach (var curListColumn in this.CurrentListView.Columns.Cast<ColumnHeader>())
615                         {
616                             columns[idx].Width = curListColumn.Width;
617                             columns[idx].DisplayIndex = curListColumn.DisplayIndex;
618                             idx++;
619                         }
620                     }
621                 }
622                 else
623                 {
624                     columns = new[]
625                     {
626                         new ColumnHeader(), // アイコン
627                         new ColumnHeader(), // ニックネーム
628                         new ColumnHeader(), // 本文
629                         new ColumnHeader(), // 日付
630                         new ColumnHeader(), // ユーザID
631                         new ColumnHeader(), // 未読
632                         new ColumnHeader(), // マーク&プロテクト
633                         new ColumnHeader(), // ソース
634                     };
635
636                     foreach (var i in Enumerable.Range(0, columns.Length))
637                         columns[i].Text = this.columnText[i];
638
639                     if (startup)
640                     {
641                         var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width;
642
643                         foreach (var (column, index) in columns.WithIndex())
644                         {
645                             column.Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[index]);
646                             column.DisplayIndex = this.settings.Local.ColumnsOrder[index];
647                         }
648                     }
649                     else
650                     {
651                         var idx = 0;
652                         foreach (var curListColumn in this.CurrentListView.Columns.Cast<ColumnHeader>())
653                         {
654                             columns[idx].Width = curListColumn.Width;
655                             columns[idx].DisplayIndex = curListColumn.DisplayIndex;
656                             idx++;
657                         }
658                     }
659                 }
660
661                 list.Columns.AddRange(columns);
662
663                 columns = null;
664             }
665             finally
666             {
667                 if (columns != null)
668                 {
669                     foreach (var column in columns)
670                         column?.Dispose();
671                 }
672             }
673         }
674
675         private void InitColumnText()
676         {
677             this.columnText[0] = "";
678             this.columnText[1] = Properties.Resources.AddNewTabText2;
679             this.columnText[2] = Properties.Resources.AddNewTabText3;
680             this.columnText[3] = Properties.Resources.AddNewTabText4_2;
681             this.columnText[4] = Properties.Resources.AddNewTabText5;
682             this.columnText[5] = "";
683             this.columnText[6] = "";
684             this.columnText[7] = "Source";
685
686             this.columnOrgText[0] = "";
687             this.columnOrgText[1] = Properties.Resources.AddNewTabText2;
688             this.columnOrgText[2] = Properties.Resources.AddNewTabText3;
689             this.columnOrgText[3] = Properties.Resources.AddNewTabText4_2;
690             this.columnOrgText[4] = Properties.Resources.AddNewTabText5;
691             this.columnOrgText[5] = "";
692             this.columnOrgText[6] = "";
693             this.columnOrgText[7] = "Source";
694
695             var c = this.statuses.SortMode switch
696             {
697                 ComparerMode.Nickname => 1, // ニックネーム
698                 ComparerMode.Data => 2, // 本文
699                 ComparerMode.Id => 3, // 時刻=発言Id
700                 ComparerMode.Name => 4, // 名前
701                 ComparerMode.Source => 7, // Source
702                 _ => 0,
703             };
704
705             if (this.Use2ColumnsMode)
706             {
707                 if (this.statuses.SortOrder == SortOrder.Descending)
708                 {
709                     // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE
710                     this.columnText[2] = this.columnOrgText[2] + "▾";
711                 }
712                 else
713                 {
714                     // U+25B4 BLACK UP-POINTING SMALL TRIANGLE
715                     this.columnText[2] = this.columnOrgText[2] + "▴";
716                 }
717             }
718             else
719             {
720                 if (this.statuses.SortOrder == SortOrder.Descending)
721                 {
722                     // U+25BE BLACK DOWN-POINTING SMALL TRIANGLE
723                     this.columnText[c] = this.columnOrgText[c] + "▾";
724                 }
725                 else
726                 {
727                     // U+25B4 BLACK UP-POINTING SMALL TRIANGLE
728                     this.columnText[c] = this.columnOrgText[c] + "▴";
729                 }
730             }
731         }
732
733         private void ListTab_DrawItem(object sender, DrawItemEventArgs e)
734         {
735             string txt;
736             try
737             {
738                 txt = this.statuses.Tabs[e.Index].TabName;
739             }
740             catch (Exception)
741             {
742                 return;
743             }
744
745             e.Graphics.FillRectangle(System.Drawing.SystemBrushes.Control, e.Bounds);
746             if (e.State == DrawItemState.Selected)
747             {
748                 e.DrawFocusRectangle();
749             }
750             Brush fore;
751             try
752             {
753                 if (this.statuses.Tabs[txt].UnreadCount > 0)
754                     fore = Brushes.Red;
755                 else
756                     fore = System.Drawing.SystemBrushes.ControlText;
757             }
758             catch (Exception)
759             {
760                 fore = System.Drawing.SystemBrushes.ControlText;
761             }
762             e.Graphics.DrawString(txt, e.Font, fore, e.Bounds, this.sfTab);
763         }
764
765         private void LoadConfig()
766         {
767             this.statuses.LoadTabsFromSettings(this.settings.Tabs);
768             this.statuses.AddDefaultTabs();
769         }
770
771         private void TimerInterval_Changed(object sender, IntervalChangedEventArgs e)
772         {
773             this.RefreshTimelineScheduler();
774         }
775
776         private void RefreshTimelineScheduler()
777         {
778             static TimeSpan IntervalSecondsOrDisabled(int seconds)
779                 => seconds == 0 ? Timeout.InfiniteTimeSpan : TimeSpan.FromSeconds(seconds);
780
781             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Home] = IntervalSecondsOrDisabled(this.settings.Common.TimelinePeriod);
782             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Mention] = IntervalSecondsOrDisabled(this.settings.Common.ReplyPeriod);
783             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Dm] = IntervalSecondsOrDisabled(this.settings.Common.DMPeriod);
784             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.PublicSearch] = IntervalSecondsOrDisabled(this.settings.Common.PubSearchPeriod);
785             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.User] = IntervalSecondsOrDisabled(this.settings.Common.UserTimelinePeriod);
786             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.List] = IntervalSecondsOrDisabled(this.settings.Common.ListsPeriod);
787             this.timelineScheduler.UpdateInterval[TimelineSchedulerTaskType.Config] = TimeSpan.FromHours(6);
788             this.timelineScheduler.UpdateAfterSystemResume = TimeSpan.FromSeconds(30);
789
790             this.timelineScheduler.RefreshSchedule();
791         }
792
793         private void MarkSettingCommonModified()
794         {
795             if (this.saveConfigDebouncer == null)
796                 return;
797
798             this.ModifySettingCommon = true;
799             _ = this.saveConfigDebouncer.Call();
800         }
801
802         private void MarkSettingLocalModified()
803         {
804             if (this.saveConfigDebouncer == null)
805                 return;
806
807             this.ModifySettingLocal = true;
808             _ = this.saveConfigDebouncer.Call();
809         }
810
811         internal void MarkSettingAtIdModified()
812         {
813             if (this.saveConfigDebouncer == null)
814                 return;
815
816             this.ModifySettingAtId = true;
817             _ = this.saveConfigDebouncer.Call();
818         }
819
820         internal void RefreshTimeline()
821         {
822             var curListView = this.CurrentListView;
823
824             // 現在表示中のタブのスクロール位置を退避
825             var currentListViewState = this.listViewState[this.CurrentTabName];
826             currentListViewState.Save(this.ListLockMenuItem.Checked);
827
828             // 更新確定
829             int addCount;
830             addCount = this.statuses.SubmitUpdate(
831                 out var soundFile,
832                 out var notifyPosts,
833                 out var newMentionOrDm,
834                 out var isDelete);
835
836             if (MyCommon.EndingFlag) return;
837
838             // リストに反映&選択状態復元
839             if (this.listCache != null && (this.listCache.IsListSizeMismatched || isDelete))
840             {
841                 using (ControlTransaction.Update(curListView))
842                 {
843                     this.listCache.PurgeCache();
844                     this.listCache.UpdateListSize();
845
846                     // 選択位置などを復元
847                     currentListViewState.RestoreSelection();
848                 }
849             }
850
851             if (addCount > 0)
852             {
853                 if (this.settings.Common.TabIconDisp)
854                 {
855                     foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
856                     {
857                         var tabPage = this.ListTab.TabPages[index];
858                         if (tab.UnreadCount > 0 && tabPage.ImageIndex != 0)
859                             tabPage.ImageIndex = 0; // 未読アイコン
860                     }
861                 }
862                 else
863                 {
864                     this.ListTab.Refresh();
865                 }
866             }
867
868             // スクロール位置を復元
869             currentListViewState.RestoreScroll();
870
871             // 新着通知
872             this.NotifyNewPosts(notifyPosts, soundFile, addCount, newMentionOrDm);
873
874             this.SetMainWindowTitle();
875             if (!this.StatusLabelUrl.Text.StartsWith("http", StringComparison.Ordinal)) this.SetStatusLabelUrl();
876
877             this.HashSupl.AddRangeItem(this.tw.GetHashList());
878         }
879
880         private bool BalloonRequired()
881         {
882             if (this.initial)
883                 return false;
884
885             if (NativeMethods.IsScreenSaverRunning())
886                 return false;
887
888             // 「新着通知」が無効
889             if (!this.NewPostPopMenuItem.Checked)
890                 return false;
891
892             // 「画面最小化・アイコン時のみバルーンを表示する」が有効
893             if (this.settings.Common.LimitBalloon)
894             {
895                 if (this.WindowState != FormWindowState.Minimized && this.Visible && Form.ActiveForm != null)
896                     return false;
897             }
898
899             return true;
900         }
901
902         private void NotifyNewPosts(PostClass[] notifyPosts, string soundFile, int addCount, bool newMentions)
903         {
904             if (this.settings.Common.ReadOwnPost)
905             {
906                 if (notifyPosts != null && notifyPosts.Length > 0 && notifyPosts.All(x => x.UserId == this.tw.UserId))
907                     return;
908             }
909
910             // 新着通知
911             if (this.BalloonRequired())
912             {
913                 if (notifyPosts != null && notifyPosts.Length > 0)
914                 {
915                     // Growlは一個ずつばらして通知。ただし、3ポスト以上あるときはまとめる
916                     if (this.settings.Common.IsUseNotifyGrowl)
917                     {
918                         var sb = new StringBuilder();
919                         var reply = false;
920                         var dm = false;
921
922                         foreach (var post in notifyPosts)
923                         {
924                             if (!(notifyPosts.Length > 3))
925                             {
926                                 sb.Clear();
927                                 reply = false;
928                                 dm = false;
929                             }
930                             if (post.IsReply && !post.IsExcludeReply) reply = true;
931                             if (post.IsDm) dm = true;
932                             if (sb.Length > 0) sb.Append(System.Environment.NewLine);
933                             switch (this.settings.Common.NameBalloon)
934                             {
935                                 case MyCommon.NameBalloonEnum.UserID:
936                                     sb.Append(post.ScreenName).Append(" : ");
937                                     break;
938                                 case MyCommon.NameBalloonEnum.NickName:
939                                     sb.Append(post.Nickname).Append(" : ");
940                                     break;
941                             }
942                             sb.Append(post.TextFromApi);
943                             if (notifyPosts.Length > 3)
944                             {
945                                 if (notifyPosts.Last() != post) continue;
946                             }
947
948                             var title = new StringBuilder();
949                             GrowlHelper.NotifyType nt;
950                             if (this.settings.Common.DispUsername)
951                             {
952                                 title.Append(this.tw.Username);
953                                 title.Append(" - ");
954                             }
955
956                             if (dm)
957                             {
958                                 title.Append(ApplicationSettings.ApplicationName);
959                                 title.Append(" [DM] ");
960                                 title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
961                                 nt = GrowlHelper.NotifyType.DirectMessage;
962                             }
963                             else if (reply)
964                             {
965                                 title.Append(ApplicationSettings.ApplicationName);
966                                 title.Append(" [Reply!] ");
967                                 title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
968                                 nt = GrowlHelper.NotifyType.Reply;
969                             }
970                             else
971                             {
972                                 title.Append(ApplicationSettings.ApplicationName);
973                                 title.Append(" ");
974                                 title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
975                                 nt = GrowlHelper.NotifyType.Notify;
976                             }
977                             var bText = sb.ToString();
978                             if (MyCommon.IsNullOrEmpty(bText)) return;
979
980                             var image = this.iconCache.TryGetFromCache(post.ImageUrl);
981                             this.gh.Notify(nt, post.StatusId.Id, title.ToString(), bText, image?.Image, post.ImageUrl);
982                         }
983                     }
984                     else
985                     {
986                         var sb = new StringBuilder();
987                         var reply = false;
988                         var dm = false;
989                         foreach (var post in notifyPosts)
990                         {
991                             if (post.IsReply && !post.IsExcludeReply) reply = true;
992                             if (post.IsDm) dm = true;
993                             if (sb.Length > 0) sb.Append(System.Environment.NewLine);
994                             switch (this.settings.Common.NameBalloon)
995                             {
996                                 case MyCommon.NameBalloonEnum.UserID:
997                                     sb.Append(post.ScreenName).Append(" : ");
998                                     break;
999                                 case MyCommon.NameBalloonEnum.NickName:
1000                                     sb.Append(post.Nickname).Append(" : ");
1001                                     break;
1002                             }
1003                             sb.Append(post.TextFromApi);
1004                         }
1005
1006                         var title = new StringBuilder();
1007                         ToolTipIcon ntIcon;
1008                         if (this.settings.Common.DispUsername)
1009                         {
1010                             title.Append(this.tw.Username);
1011                             title.Append(" - ");
1012                         }
1013
1014                         if (dm)
1015                         {
1016                             ntIcon = ToolTipIcon.Warning;
1017                             title.Append(ApplicationSettings.ApplicationName);
1018                             title.Append(" [DM] ");
1019                             title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
1020                         }
1021                         else if (reply)
1022                         {
1023                             ntIcon = ToolTipIcon.Warning;
1024                             title.Append(ApplicationSettings.ApplicationName);
1025                             title.Append(" [Reply!] ");
1026                             title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
1027                         }
1028                         else
1029                         {
1030                             ntIcon = ToolTipIcon.Info;
1031                             title.Append(ApplicationSettings.ApplicationName);
1032                             title.Append(" ");
1033                             title.AppendFormat(Properties.Resources.RefreshTimeline_NotifyText, addCount);
1034                         }
1035                         var bText = sb.ToString();
1036                         if (MyCommon.IsNullOrEmpty(bText)) return;
1037
1038                         this.NotifyIcon1.BalloonTipTitle = title.ToString();
1039                         this.NotifyIcon1.BalloonTipText = bText;
1040                         this.NotifyIcon1.BalloonTipIcon = ntIcon;
1041                         this.NotifyIcon1.ShowBalloonTip(500);
1042                     }
1043                 }
1044             }
1045
1046             // サウンド再生
1047             if (!this.initial && this.settings.Common.PlaySound && !MyCommon.IsNullOrEmpty(soundFile))
1048             {
1049                 try
1050                 {
1051                     var dir = Application.StartupPath;
1052                     if (Directory.Exists(Path.Combine(dir, "Sounds")))
1053                     {
1054                         dir = Path.Combine(dir, "Sounds");
1055                     }
1056                     using var player = new SoundPlayer(Path.Combine(dir, soundFile));
1057                     player.Play();
1058                 }
1059                 catch (Exception)
1060                 {
1061                 }
1062             }
1063
1064             // mentions新着時に画面ブリンク
1065             if (!this.initial && this.settings.Common.BlinkNewMentions && newMentions && Form.ActiveForm == null)
1066             {
1067                 NativeMethods.FlashMyWindow(this.Handle, 3);
1068             }
1069         }
1070
1071         private async void MyList_SelectedIndexChanged(object sender, EventArgs e)
1072         {
1073             var listView = this.CurrentListView;
1074             if (listView != sender)
1075                 return;
1076
1077             var indices = listView.SelectedIndices.Cast<int>().ToArray();
1078             this.CurrentTab.SelectPosts(indices);
1079
1080             if (indices.Length != 1)
1081                 return;
1082
1083             var index = indices[0];
1084             if (index > listView.VirtualListSize - 1) return;
1085
1086             this.PushSelectPostChain();
1087
1088             var post = this.CurrentPost!;
1089             this.statuses.SetReadAllTab(post.StatusId, read: true);
1090
1091             this.listCache?.RefreshStyle();
1092             await this.selectionDebouncer.Call();
1093         }
1094
1095         private void StatusTextHistoryBack()
1096         {
1097             this.history.SetCurrentItem(this.StatusText.Text, this.inReplyTo);
1098             var historyItem = this.history.Back();
1099             this.inReplyTo = historyItem.InReplyTo;
1100             this.StatusText.Text = historyItem.Status;
1101             this.StatusText.SelectionStart = this.StatusText.Text.Length;
1102         }
1103
1104         private void StatusTextHistoryForward()
1105         {
1106             this.history.SetCurrentItem(this.StatusText.Text, this.inReplyTo);
1107             var historyItem = this.history.Forward();
1108             this.inReplyTo = historyItem.InReplyTo;
1109             this.StatusText.Text = historyItem.Status;
1110             this.StatusText.SelectionStart = this.StatusText.Text.Length;
1111         }
1112
1113         private async void PostButton_Click(object sender, EventArgs e)
1114         {
1115             if (this.StatusText.Text.Trim().Length == 0)
1116             {
1117                 if (!this.ImageSelector.Enabled)
1118                 {
1119                     await this.DoRefresh();
1120                     return;
1121                 }
1122             }
1123
1124             var currentPost = this.CurrentPost;
1125             if (this.ExistCurrentPost && currentPost != null && this.StatusText.Text.Trim() == string.Format("RT @{0}: {1}", currentPost.ScreenName, currentPost.TextFromApi))
1126             {
1127                 var rtResult = MessageBox.Show(string.Format(Properties.Resources.PostButton_Click1, Environment.NewLine),
1128                                                                "Retweet",
1129                                                                MessageBoxButtons.YesNoCancel,
1130                                                                MessageBoxIcon.Question);
1131                 switch (rtResult)
1132                 {
1133                     case DialogResult.Yes:
1134                         this.StatusText.Text = "";
1135                         await this.DoReTweetOfficial(false);
1136                         return;
1137                     case DialogResult.Cancel:
1138                         return;
1139                 }
1140             }
1141
1142             if (TextContainsOnlyMentions(this.StatusText.Text))
1143             {
1144                 var message = string.Format(Properties.Resources.PostConfirmText, this.StatusText.Text);
1145                 var ret = MessageBox.Show(message, ApplicationSettings.ApplicationName, MessageBoxButtons.OKCancel, MessageBoxIcon.Warning, MessageBoxDefaultButton.Button2);
1146
1147                 if (ret != DialogResult.OK)
1148                     return;
1149             }
1150
1151             if (this.settings.Common.Nicoms)
1152             {
1153                 this.StatusText.SelectionStart = this.StatusText.Text.Length;
1154                 await this.UrlConvertAsync(MyCommon.UrlConverter.Nicoms);
1155             }
1156
1157             this.StatusText.SelectionStart = this.StatusText.Text.Length;
1158             this.CheckReplyTo(this.StatusText.Text);
1159
1160             var status = new PostStatusParams();
1161
1162             var statusTextCompat = this.FormatStatusText(this.StatusText.Text);
1163             if (this.GetRestStatusCount(statusTextCompat) >= 0 && this.tw.Api.AuthType == APIAuthType.OAuth1)
1164             {
1165                 // auto_populate_reply_metadata や attachment_url を使用しなくても 140 字以内に
1166                 // 収まる場合はこれらのオプションを使用せずに投稿する
1167                 status.Text = statusTextCompat;
1168                 status.InReplyToStatusId = this.inReplyTo?.StatusId;
1169             }
1170             else
1171             {
1172                 status.Text = this.FormatStatusTextExtended(this.StatusText.Text, out var autoPopulatedUserIds, out var attachmentUrl);
1173                 status.InReplyToStatusId = this.inReplyTo?.StatusId;
1174
1175                 status.AttachmentUrl = attachmentUrl;
1176
1177                 // リプライ先がセットされていても autoPopulatedUserIds が空の場合は auto_populate_reply_metadata を有効にしない
1178                 //  (非公式 RT の場合など)
1179                 var replyToPost = this.inReplyTo != null ? this.statuses[this.inReplyTo.Value.StatusId] : null;
1180                 if (replyToPost != null && autoPopulatedUserIds.Length != 0)
1181                 {
1182                     status.AutoPopulateReplyMetadata = true;
1183
1184                     // ReplyToList のうち autoPopulatedUserIds に含まれていないユーザー ID を抽出
1185                     status.ExcludeReplyUserIds = replyToPost.ReplyToList.Select(x => x.UserId).Except(autoPopulatedUserIds)
1186                         .ToArray();
1187                 }
1188             }
1189
1190             if (this.GetRestStatusCount(status.Text) < 0)
1191             {
1192                 // 文字数制限を超えているが強制的に投稿するか
1193                 var ret = MessageBox.Show(Properties.Resources.PostLengthOverMessage1, Properties.Resources.PostLengthOverMessage2, MessageBoxButtons.OKCancel, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2);
1194                 if (ret != DialogResult.OK)
1195                     return;
1196             }
1197
1198             IMediaUploadService? uploadService = null;
1199             IMediaItem[]? uploadItems = null;
1200             if (this.ImageSelector.Visible)
1201             {
1202                 // 画像投稿
1203                 if (!this.ImageSelector.TryGetSelectedMedia(out var serviceName, out uploadItems))
1204                     return;
1205
1206                 this.ImageSelector.EndSelection();
1207                 uploadService = this.ImageSelector.Model.GetService(serviceName);
1208             }
1209
1210             this.history.AddLast(this.StatusText.Text, this.inReplyTo);
1211
1212             this.inReplyTo = null;
1213             this.StatusText.Text = "";
1214             if (!this.settings.Common.FocusLockToStatusText)
1215                 this.CurrentListView.Focus();
1216             this.urlUndoBuffer = null;
1217             this.UrlUndoToolStripMenuItem.Enabled = false;  // Undoをできないように設定
1218
1219             // Google検索(試験実装)
1220             if (this.StatusText.Text.StartsWith("Google:", StringComparison.OrdinalIgnoreCase) && this.StatusText.Text.Trim().Length > 7)
1221             {
1222                 var tmp = string.Format(Properties.Resources.SearchItem2Url, Uri.EscapeDataString(this.StatusText.Text.Substring(7)));
1223                 await MyCommon.OpenInBrowserAsync(this, tmp);
1224             }
1225
1226             await this.PostMessageAsync(status, uploadService, uploadItems);
1227         }
1228
1229         private void EndToolStripMenuItem_Click(object sender, EventArgs e)
1230         {
1231             MyCommon.EndingFlag = true;
1232             this.Close();
1233         }
1234
1235         private void TweenMain_FormClosing(object sender, FormClosingEventArgs e)
1236         {
1237             if (!this.settings.Common.CloseToExit && e.CloseReason == CloseReason.UserClosing && MyCommon.EndingFlag == false)
1238             {
1239                 // _endingFlag=false:フォームの×ボタン
1240                 e.Cancel = true;
1241                 this.Visible = false;
1242             }
1243             else
1244             {
1245                 this.hookGlobalHotkey.UnregisterAllOriginalHotkey();
1246                 this.ignoreConfigSave = true;
1247                 MyCommon.EndingFlag = true;
1248                 this.timelineScheduler.Enabled = false;
1249                 this.TimerRefreshIcon.Enabled = false;
1250             }
1251         }
1252
1253         private void NotifyIcon1_BalloonTipClicked(object sender, EventArgs e)
1254         {
1255             this.Visible = true;
1256             if (this.WindowState == FormWindowState.Minimized)
1257             {
1258                 this.WindowState = FormWindowState.Normal;
1259             }
1260             this.Activate();
1261             this.BringToFront();
1262         }
1263
1264         private static int errorCount = 0;
1265
1266         private static bool CheckAccountValid()
1267         {
1268             if (Twitter.AccountState != MyCommon.ACCOUNT_STATE.Valid)
1269             {
1270                 errorCount += 1;
1271                 if (errorCount > 5)
1272                 {
1273                     errorCount = 0;
1274                     Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
1275                     return true;
1276                 }
1277                 return false;
1278             }
1279             errorCount = 0;
1280             return true;
1281         }
1282
1283         /// <summary>指定された型 <typeparamref name="T"/> に合致する全てのタブを更新します</summary>
1284         private Task RefreshTabAsync<T>()
1285             where T : TabModel
1286             => this.RefreshTabAsync<T>(backward: false);
1287
1288         /// <summary>指定された型 <typeparamref name="T"/> に合致する全てのタブを更新します</summary>
1289         private Task RefreshTabAsync<T>(bool backward)
1290             where T : TabModel
1291         {
1292             var loadTasks =
1293                 from tab in this.statuses.GetTabsByType<T>()
1294                 select this.RefreshTabAsync(tab, backward);
1295
1296             return Task.WhenAll(loadTasks);
1297         }
1298
1299         /// <summary>指定されたタブ <paramref name="tab"/> を更新します</summary>
1300         private Task RefreshTabAsync(TabModel tab)
1301             => this.RefreshTabAsync(tab, backward: false);
1302
1303         /// <summary>指定されたタブ <paramref name="tab"/> を更新します</summary>
1304         private async Task RefreshTabAsync(TabModel tab, bool backward)
1305         {
1306             await this.workerSemaphore.WaitAsync();
1307
1308             try
1309             {
1310                 this.RefreshTasktrayIcon();
1311                 await Task.Run(() => tab.RefreshAsync(this.tw, backward, this.initial, this.workerProgress));
1312             }
1313             catch (WebApiException ex)
1314             {
1315                 this.myStatusError = true;
1316                 var tabType = tab switch
1317                 {
1318                     HomeTabModel => "GetTimeline",
1319                     MentionsTabModel => "GetTimeline",
1320                     DirectMessagesTabModel => "GetDirectMessage",
1321                     FavoritesTabModel => "GetFavorites",
1322                     PublicSearchTabModel => "GetSearch",
1323                     UserTimelineTabModel => "GetUserTimeline",
1324                     ListTimelineTabModel => "GetListStatus",
1325                     RelatedPostsTabModel => "GetRelatedTweets",
1326                     _ => tab.GetType().Name.Replace("Model", ""),
1327                 };
1328                 this.StatusLabel.Text = $"Err:{ex.Message}({tabType})";
1329             }
1330             finally
1331             {
1332                 this.RefreshTimeline();
1333                 this.workerSemaphore.Release();
1334             }
1335         }
1336
1337         private async Task FavAddAsync(PostId statusId, TabModel tab)
1338         {
1339             await this.workerSemaphore.WaitAsync();
1340
1341             try
1342             {
1343                 var progress = new Progress<string>(x => this.StatusLabel.Text = x);
1344
1345                 this.RefreshTasktrayIcon();
1346                 await this.FavAddAsyncInternal(progress, this.workerCts.Token, statusId, tab);
1347             }
1348             catch (WebApiException ex)
1349             {
1350                 this.myStatusError = true;
1351                 this.StatusLabel.Text = $"Err:{ex.Message}(PostFavAdd)";
1352             }
1353             finally
1354             {
1355                 this.workerSemaphore.Release();
1356             }
1357         }
1358
1359         private async Task FavAddAsyncInternal(IProgress<string> p, CancellationToken ct, PostId statusId, TabModel tab)
1360         {
1361             if (ct.IsCancellationRequested)
1362                 return;
1363
1364             if (!CheckAccountValid())
1365                 throw new WebApiException("Auth error. Check your account");
1366
1367             if (!tab.Posts.TryGetValue(statusId, out var post))
1368                 return;
1369
1370             if (post.IsFav)
1371                 return;
1372
1373             await Task.Run(async () =>
1374             {
1375                 p.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText15, 0, 1, 0));
1376
1377                 try
1378                 {
1379                     var twitterStatusId = (post.RetweetedId ?? post.StatusId).ToTwitterStatusId();
1380                     try
1381                     {
1382                         await this.tw.Api.FavoritesCreate(twitterStatusId)
1383                             .IgnoreResponse()
1384                             .ConfigureAwait(false);
1385                     }
1386                     catch (TwitterApiException ex)
1387                         when (ex.Errors.All(x => x.Code == TwitterErrorCode.AlreadyFavorited))
1388                     {
1389                         // エラーコード 139 のみの場合は成功と見なす
1390                     }
1391
1392                     if (this.settings.Common.RestrictFavCheck)
1393                     {
1394                         var status = await this.tw.Api.StatusesShow(twitterStatusId)
1395                             .ConfigureAwait(false);
1396
1397                         if (status.Favorited != true)
1398                             throw new WebApiException("NG(Restricted?)");
1399                     }
1400
1401                     this.favTimestamps.Add(DateTimeUtc.Now);
1402
1403                     // TLでも取得済みならfav反映
1404                     if (this.statuses.Posts.TryGetValue(statusId, out var postTl))
1405                     {
1406                         postTl.IsFav = true;
1407
1408                         var favTab = this.statuses.FavoriteTab;
1409                         favTab.AddPostQueue(postTl);
1410                     }
1411
1412                     // 検索,リスト,UserTimeline,Relatedの各タブに反映
1413                     foreach (var tb in this.statuses.GetTabsInnerStorageType())
1414                     {
1415                         if (tb.Contains(statusId))
1416                             tb.Posts[statusId].IsFav = true;
1417                     }
1418
1419                     p.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText15, 1, 1, 0));
1420                 }
1421                 catch (WebApiException)
1422                 {
1423                     p.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText15, 1, 1, 1));
1424                     throw;
1425                 }
1426
1427                 // 時速表示用
1428                 var oneHour = DateTimeUtc.Now - TimeSpan.FromHours(1);
1429                 foreach (var i in MyCommon.CountDown(this.favTimestamps.Count - 1, 0))
1430                 {
1431                     if (this.favTimestamps[i] < oneHour)
1432                         this.favTimestamps.RemoveAt(i);
1433                 }
1434
1435                 this.statuses.DistributePosts();
1436             });
1437
1438             if (ct.IsCancellationRequested)
1439                 return;
1440
1441             this.RefreshTimeline();
1442
1443             if (this.CurrentTabName == tab.TabName)
1444             {
1445                 using (ControlTransaction.Update(this.CurrentListView))
1446                 {
1447                     var idx = tab.IndexOf(statusId);
1448                     if (idx != -1)
1449                         this.listCache?.RefreshStyle(idx);
1450                 }
1451
1452                 var currentPost = this.CurrentPost;
1453                 if (currentPost != null && statusId == currentPost.StatusId)
1454                     this.DispSelectedPost(true); // 選択アイテム再表示
1455             }
1456         }
1457
1458         private async Task FavRemoveAsync(IReadOnlyList<PostId> statusIds, TabModel tab)
1459         {
1460             await this.workerSemaphore.WaitAsync();
1461
1462             try
1463             {
1464                 var progress = new Progress<string>(x => this.StatusLabel.Text = x);
1465
1466                 this.RefreshTasktrayIcon();
1467                 await this.FavRemoveAsyncInternal(progress, this.workerCts.Token, statusIds, tab);
1468             }
1469             catch (WebApiException ex)
1470             {
1471                 this.myStatusError = true;
1472                 this.StatusLabel.Text = $"Err:{ex.Message}(PostFavRemove)";
1473             }
1474             finally
1475             {
1476                 this.workerSemaphore.Release();
1477             }
1478         }
1479
1480         private async Task FavRemoveAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<PostId> statusIds, TabModel tab)
1481         {
1482             if (ct.IsCancellationRequested)
1483                 return;
1484
1485             if (!CheckAccountValid())
1486                 throw new WebApiException("Auth error. Check your account");
1487
1488             var successIds = new List<PostId>();
1489
1490             await Task.Run(async () =>
1491             {
1492                 // スレッド処理はしない
1493                 var allCount = 0;
1494                 var failedCount = 0;
1495                 foreach (var statusId in statusIds)
1496                 {
1497                     allCount++;
1498
1499                     var post = tab.Posts[statusId];
1500
1501                     p.Report(string.Format(Properties.Resources.GetTimelineWorker_RunWorkerCompletedText17, allCount, statusIds.Count, failedCount));
1502
1503                     if (!post.IsFav)
1504                         continue;
1505
1506                     var twitterStatusId = (post.RetweetedId ?? post.StatusId).ToTwitterStatusId();
1507
1508                     try
1509                     {
1510                         await this.tw.Api.FavoritesDestroy(twitterStatusId)
1511                             .IgnoreResponse()
1512                             .ConfigureAwait(false);
1513                     }
1514                     catch (WebApiException)
1515                     {
1516                         failedCount++;
1517                         continue;
1518                     }
1519
1520                     successIds.Add(statusId);
1521                     post.IsFav = false; // リスト再描画必要
1522
1523                     if (this.statuses.Posts.TryGetValue(statusId, out var tabinfoPost))
1524                         tabinfoPost.IsFav = false;
1525
1526                     // 検索,リスト,UserTimeline,Relatedの各タブに反映
1527                     foreach (var tb in this.statuses.GetTabsInnerStorageType())
1528                     {
1529                         if (tb.Contains(statusId))
1530                             tb.Posts[statusId].IsFav = false;
1531                     }
1532                 }
1533             });
1534
1535             if (ct.IsCancellationRequested)
1536                 return;
1537
1538             var favTab = this.statuses.FavoriteTab;
1539             foreach (var statusId in successIds)
1540             {
1541                 // ツイートが削除された訳ではないので IsDeleted はセットしない
1542                 favTab.EnqueueRemovePost(statusId, setIsDeleted: false);
1543             }
1544
1545             this.RefreshTimeline();
1546
1547             if (this.CurrentTabName == tab.TabName)
1548             {
1549                 if (tab.TabType == MyCommon.TabUsageType.Favorites)
1550                 {
1551                     // 色変えは不要
1552                 }
1553                 else
1554                 {
1555                     using (ControlTransaction.Update(this.CurrentListView))
1556                     {
1557                         foreach (var statusId in successIds)
1558                         {
1559                             var idx = tab.IndexOf(statusId);
1560                             if (idx != -1)
1561                                 this.listCache?.RefreshStyle(idx);
1562                         }
1563                     }
1564
1565                     var currentPost = this.CurrentPost;
1566                     if (currentPost != null && successIds.Contains(currentPost.StatusId))
1567                         this.DispSelectedPost(true); // 選択アイテム再表示
1568                 }
1569             }
1570         }
1571
1572         private async Task PostMessageAsync(PostStatusParams postParams, IMediaUploadService? uploadService, IMediaItem[]? uploadItems)
1573         {
1574             await this.workerSemaphore.WaitAsync();
1575
1576             try
1577             {
1578                 var progress = new Progress<string>(x => this.StatusLabel.Text = x);
1579
1580                 this.RefreshTasktrayIcon();
1581                 await this.PostMessageAsyncInternal(progress, this.workerCts.Token, postParams, uploadService, uploadItems);
1582             }
1583             catch (WebApiException ex)
1584             {
1585                 this.myStatusError = true;
1586                 this.StatusLabel.Text = $"Err:{ex.Message}(PostMessage)";
1587             }
1588             finally
1589             {
1590                 this.workerSemaphore.Release();
1591             }
1592         }
1593
1594         private async Task PostMessageAsyncInternal(
1595             IProgress<string> p,
1596             CancellationToken ct,
1597             PostStatusParams postParams,
1598             IMediaUploadService? uploadService,
1599             IMediaItem[]? uploadItems)
1600         {
1601             if (ct.IsCancellationRequested)
1602                 return;
1603
1604             if (!CheckAccountValid())
1605                 throw new WebApiException("Auth error. Check your account");
1606
1607             p.Report("Posting...");
1608
1609             PostClass? post = null;
1610             var errMsg = "";
1611
1612             try
1613             {
1614                 await Task.Run(async () =>
1615                 {
1616                     var postParamsWithMedia = postParams;
1617
1618                     if (uploadService != null && uploadItems != null && uploadItems.Length > 0)
1619                     {
1620                         postParamsWithMedia = await uploadService.UploadAsync(uploadItems, postParamsWithMedia)
1621                             .ConfigureAwait(false);
1622                     }
1623
1624                     post = await this.tw.PostStatus(postParamsWithMedia)
1625                         .ConfigureAwait(false);
1626                 });
1627
1628                 p.Report(Properties.Resources.PostWorker_RunWorkerCompletedText4);
1629             }
1630             catch (WebApiException ex)
1631             {
1632                 // 処理は中断せずエラーの表示のみ行う
1633                 errMsg = $"Err:{ex.Message}(PostMessage)";
1634                 p.Report(errMsg);
1635                 this.myStatusError = true;
1636             }
1637             catch (UnauthorizedAccessException ex)
1638             {
1639                 // アップロード対象のファイルが開けなかった場合など
1640                 errMsg = $"Err:{ex.Message}(PostMessage)";
1641                 p.Report(errMsg);
1642                 this.myStatusError = true;
1643             }
1644             finally
1645             {
1646                 // 使い終わった MediaItem は破棄する
1647                 if (uploadItems != null)
1648                 {
1649                     foreach (var disposableItem in uploadItems.OfType<IDisposable>())
1650                     {
1651                         disposableItem.Dispose();
1652                     }
1653                 }
1654             }
1655
1656             if (ct.IsCancellationRequested)
1657                 return;
1658
1659             if (!MyCommon.IsNullOrEmpty(errMsg) &&
1660                 !errMsg.StartsWith("OK:", StringComparison.Ordinal) &&
1661                 !errMsg.StartsWith("Warn:", StringComparison.Ordinal))
1662             {
1663                 var message = string.Format(Properties.Resources.StatusUpdateFailed, errMsg, postParams.Text);
1664
1665                 var ret = MessageBox.Show(
1666                     message,
1667                     "Failed to update status",
1668                     MessageBoxButtons.RetryCancel,
1669                     MessageBoxIcon.Question);
1670
1671                 if (ret == DialogResult.Retry)
1672                 {
1673                     await this.PostMessageAsync(postParams, uploadService, uploadItems);
1674                 }
1675                 else
1676                 {
1677                     this.StatusTextHistoryBack();
1678                     this.StatusText.Focus();
1679
1680                     // 連投モードのときだけEnterイベントが起きないので強制的に背景色を戻す
1681                     if (this.settings.Common.FocusLockToStatusText)
1682                         this.StatusText_Enter(this.StatusText, EventArgs.Empty);
1683                 }
1684                 return;
1685             }
1686
1687             this.postTimestamps.Add(DateTimeUtc.Now);
1688
1689             var oneHour = DateTimeUtc.Now - TimeSpan.FromHours(1);
1690             foreach (var i in MyCommon.CountDown(this.postTimestamps.Count - 1, 0))
1691             {
1692                 if (this.postTimestamps[i] < oneHour)
1693                     this.postTimestamps.RemoveAt(i);
1694             }
1695
1696             if (!this.HashMgr.IsPermanent && !MyCommon.IsNullOrEmpty(this.HashMgr.UseHash))
1697             {
1698                 this.HashMgr.ClearHashtag();
1699                 this.HashStripSplitButton.Text = "#[-]";
1700                 this.HashTogglePullDownMenuItem.Checked = false;
1701                 this.HashToggleMenuItem.Checked = false;
1702             }
1703
1704             this.SetMainWindowTitle();
1705
1706             // TLに反映
1707             if (post != null)
1708             {
1709                 this.statuses.AddPost(post);
1710                 this.statuses.DistributePosts();
1711                 this.RefreshTimeline();
1712             }
1713
1714             if (this.settings.Common.PostAndGet)
1715                 await this.RefreshTabAsync<HomeTabModel>();
1716         }
1717
1718         private async Task RetweetAsync(IReadOnlyList<PostId> statusIds)
1719         {
1720             await this.workerSemaphore.WaitAsync();
1721
1722             try
1723             {
1724                 var progress = new Progress<string>(x => this.StatusLabel.Text = x);
1725
1726                 this.RefreshTasktrayIcon();
1727                 await this.RetweetAsyncInternal(progress, this.workerCts.Token, statusIds);
1728             }
1729             catch (WebApiException ex)
1730             {
1731                 this.myStatusError = true;
1732                 this.StatusLabel.Text = $"Err:{ex.Message}(PostRetweet)";
1733             }
1734             finally
1735             {
1736                 this.workerSemaphore.Release();
1737             }
1738         }
1739
1740         private async Task RetweetAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<PostId> statusIds)
1741         {
1742             if (ct.IsCancellationRequested)
1743                 return;
1744
1745             if (!CheckAccountValid())
1746                 throw new WebApiException("Auth error. Check your account");
1747
1748             bool read;
1749             if (!this.settings.Common.UnreadManage)
1750                 read = true;
1751             else
1752                 read = this.initial && this.settings.Common.Read;
1753
1754             p.Report("Posting...");
1755
1756             var posts = new List<PostClass>();
1757
1758             await Task.Run(async () =>
1759             {
1760                 foreach (var statusId in statusIds)
1761                 {
1762                     var post = await this.tw.PostRetweet(statusId, read).ConfigureAwait(false);
1763                     if (post != null) posts.Add(post);
1764                 }
1765             });
1766
1767             if (ct.IsCancellationRequested)
1768                 return;
1769
1770             p.Report(Properties.Resources.PostWorker_RunWorkerCompletedText4);
1771
1772             this.postTimestamps.Add(DateTimeUtc.Now);
1773
1774             var oneHour = DateTimeUtc.Now - TimeSpan.FromHours(1);
1775             foreach (var i in MyCommon.CountDown(this.postTimestamps.Count - 1, 0))
1776             {
1777                 if (this.postTimestamps[i] < oneHour)
1778                     this.postTimestamps.RemoveAt(i);
1779             }
1780
1781             // 自分のRTはTLの更新では取得できない場合があるので、
1782             // 投稿時取得の有無に関わらず追加しておく
1783             posts.ForEach(post => this.statuses.AddPost(post));
1784
1785             if (this.settings.Common.PostAndGet)
1786             {
1787                 await this.RefreshTabAsync<HomeTabModel>();
1788             }
1789             else
1790             {
1791                 this.statuses.DistributePosts();
1792                 this.RefreshTimeline();
1793             }
1794         }
1795
1796         private async Task RefreshFollowerIdsAsync()
1797         {
1798             await this.workerSemaphore.WaitAsync();
1799
1800             try
1801             {
1802                 this.RefreshTasktrayIcon();
1803                 this.StatusLabel.Text = Properties.Resources.UpdateFollowersMenuItem1_ClickText1;
1804
1805                 await this.tw.RefreshFollowerIds();
1806
1807                 this.StatusLabel.Text = Properties.Resources.UpdateFollowersMenuItem1_ClickText3;
1808
1809                 this.RefreshTimeline();
1810                 this.listCache?.PurgeCache();
1811                 this.CurrentListView.Refresh();
1812             }
1813             catch (WebApiException ex)
1814             {
1815                 this.StatusLabel.Text = $"Err:{ex.Message}(RefreshFollowersIds)";
1816             }
1817             finally
1818             {
1819                 this.workerSemaphore.Release();
1820             }
1821         }
1822
1823         private async Task RefreshNoRetweetIdsAsync()
1824         {
1825             await this.workerSemaphore.WaitAsync();
1826
1827             try
1828             {
1829                 this.RefreshTasktrayIcon();
1830                 await this.tw.RefreshNoRetweetIds();
1831
1832                 this.StatusLabel.Text = "NoRetweetIds refreshed";
1833             }
1834             catch (WebApiException ex)
1835             {
1836                 this.StatusLabel.Text = $"Err:{ex.Message}(RefreshNoRetweetIds)";
1837             }
1838             finally
1839             {
1840                 this.workerSemaphore.Release();
1841             }
1842         }
1843
1844         private async Task RefreshBlockIdsAsync()
1845         {
1846             await this.workerSemaphore.WaitAsync();
1847
1848             try
1849             {
1850                 this.RefreshTasktrayIcon();
1851                 this.StatusLabel.Text = Properties.Resources.UpdateBlockUserText1;
1852
1853                 await this.tw.RefreshBlockIds();
1854
1855                 this.StatusLabel.Text = Properties.Resources.UpdateBlockUserText3;
1856             }
1857             catch (WebApiException ex)
1858             {
1859                 this.StatusLabel.Text = $"Err:{ex.Message}(RefreshBlockIds)";
1860             }
1861             finally
1862             {
1863                 this.workerSemaphore.Release();
1864             }
1865         }
1866
1867         private async Task RefreshTwitterConfigurationAsync()
1868         {
1869             await this.workerSemaphore.WaitAsync();
1870
1871             try
1872             {
1873                 this.RefreshTasktrayIcon();
1874                 await this.tw.RefreshConfiguration();
1875
1876                 if (this.tw.Configuration.PhotoSizeLimit != 0)
1877                 {
1878                     foreach (var (_, service) in this.ImageSelector.Model.MediaServices)
1879                     {
1880                         service.UpdateTwitterConfiguration(this.tw.Configuration);
1881                     }
1882                 }
1883
1884                 this.listCache?.PurgeCache();
1885                 this.CurrentListView.Refresh();
1886             }
1887             catch (WebApiException ex)
1888             {
1889                 this.StatusLabel.Text = $"Err:{ex.Message}(RefreshConfiguration)";
1890             }
1891             finally
1892             {
1893                 this.workerSemaphore.Release();
1894             }
1895         }
1896
1897         private async Task RefreshMuteUserIdsAsync()
1898         {
1899             this.StatusLabel.Text = Properties.Resources.UpdateMuteUserIds_Start;
1900
1901             try
1902             {
1903                 await this.tw.RefreshMuteUserIdsAsync();
1904             }
1905             catch (WebApiException ex)
1906             {
1907                 this.StatusLabel.Text = string.Format(Properties.Resources.UpdateMuteUserIds_Error, ex.Message);
1908                 return;
1909             }
1910
1911             this.StatusLabel.Text = Properties.Resources.UpdateMuteUserIds_Finish;
1912         }
1913
1914         private void NotifyIcon1_MouseClick(object sender, MouseEventArgs e)
1915         {
1916             if (e.Button == MouseButtons.Left)
1917             {
1918                 this.Visible = true;
1919                 if (this.WindowState == FormWindowState.Minimized)
1920                 {
1921                     this.WindowState = this.formWindowState;
1922                 }
1923                 this.Activate();
1924                 this.BringToFront();
1925             }
1926         }
1927
1928         private async void MyList_MouseDoubleClick(object sender, MouseEventArgs e)
1929             => await this.ListItemDoubleClickAction();
1930
1931         private async Task ListItemDoubleClickAction()
1932         {
1933             switch (this.settings.Common.ListDoubleClickAction)
1934             {
1935                 case MyCommon.ListItemDoubleClickActionType.Reply:
1936                     this.MakeReplyText();
1937                     break;
1938                 case MyCommon.ListItemDoubleClickActionType.ReplyAll:
1939                     this.MakeReplyText(atAll: true);
1940                     break;
1941                 case MyCommon.ListItemDoubleClickActionType.Favorite:
1942                     await this.FavoriteChange(true);
1943                     break;
1944                 case MyCommon.ListItemDoubleClickActionType.ShowProfile:
1945                     var post = this.CurrentPost;
1946                     if (post != null)
1947                         await this.ShowUserStatus(post.ScreenName, false);
1948                     break;
1949                 case MyCommon.ListItemDoubleClickActionType.ShowTimeline:
1950                     await this.ShowUserTimeline();
1951                     break;
1952                 case MyCommon.ListItemDoubleClickActionType.ShowRelated:
1953                     this.ShowRelatedStatusesMenuItem_Click(this.ShowRelatedStatusesMenuItem, EventArgs.Empty);
1954                     break;
1955                 case MyCommon.ListItemDoubleClickActionType.OpenHomeInBrowser:
1956                     this.AuthorOpenInBrowserMenuItem_Click(this.AuthorOpenInBrowserContextMenuItem, EventArgs.Empty);
1957                     break;
1958                 case MyCommon.ListItemDoubleClickActionType.OpenStatusInBrowser:
1959                     this.StatusOpenMenuItem_Click(this.StatusOpenMenuItem, EventArgs.Empty);
1960                     break;
1961                 case MyCommon.ListItemDoubleClickActionType.None:
1962                 default:
1963                     // 動作なし
1964                     break;
1965             }
1966         }
1967
1968         private async void FavAddToolStripMenuItem_Click(object sender, EventArgs e)
1969             => await this.FavoriteChange(true);
1970
1971         private async void FavRemoveToolStripMenuItem_Click(object sender, EventArgs e)
1972             => await this.FavoriteChange(false);
1973
1974         private async void FavoriteRetweetMenuItem_Click(object sender, EventArgs e)
1975             => await this.FavoritesRetweetOfficial();
1976
1977         private async void FavoriteRetweetUnofficialMenuItem_Click(object sender, EventArgs e)
1978             => await this.FavoritesRetweetUnofficial();
1979
1980         private async Task FavoriteChange(bool favAdd, bool multiFavoriteChangeDialogEnable = true)
1981         {
1982             var tab = this.CurrentTab;
1983             var posts = tab.SelectedPosts;
1984
1985             // trueでFavAdd,falseでFavRemove
1986             if (tab.TabType == MyCommon.TabUsageType.DirectMessage || posts.Length == 0
1987                 || !this.ExistCurrentPost) return;
1988
1989             if (posts.Length > 1)
1990             {
1991                 if (favAdd)
1992                 {
1993                     // 複数ツイートの一括ふぁぼは禁止
1994                     // https://support.twitter.com/articles/76915#favoriting
1995                     MessageBox.Show(string.Format(Properties.Resources.FavoriteLimitCountText, 1));
1996                     this.doFavRetweetFlags = false;
1997                     return;
1998                 }
1999                 else
2000                 {
2001                     if (multiFavoriteChangeDialogEnable)
2002                     {
2003                         var confirm = MessageBox.Show(
2004                             Properties.Resources.FavRemoveToolStripMenuItem_ClickText1,
2005                             Properties.Resources.FavRemoveToolStripMenuItem_ClickText2,
2006                             MessageBoxButtons.OKCancel,
2007                             MessageBoxIcon.Question);
2008
2009                         if (confirm == DialogResult.Cancel)
2010                             return;
2011                     }
2012                 }
2013             }
2014
2015             if (favAdd)
2016             {
2017                 var selectedPost = posts.Single();
2018                 if (selectedPost.IsFav)
2019                 {
2020                     this.StatusLabel.Text = Properties.Resources.FavAddToolStripMenuItem_ClickText4;
2021                     return;
2022                 }
2023
2024                 await this.FavAddAsync(selectedPost.StatusId, tab);
2025             }
2026             else
2027             {
2028                 var selectedPosts = posts.Where(x => x.IsFav);
2029                 var statusIds = selectedPosts.Select(x => x.StatusId).ToArray();
2030                 if (statusIds.Length == 0)
2031                 {
2032                     this.StatusLabel.Text = Properties.Resources.FavRemoveToolStripMenuItem_ClickText4;
2033                     return;
2034                 }
2035
2036                 await this.FavRemoveAsync(statusIds, tab);
2037             }
2038         }
2039
2040         private async void AuthorOpenInBrowserMenuItem_Click(object sender, EventArgs e)
2041         {
2042             var post = this.CurrentPost;
2043             if (post != null)
2044                 await MyCommon.OpenInBrowserAsync(this, MyCommon.TwitterUrl + post.ScreenName);
2045             else
2046                 await MyCommon.OpenInBrowserAsync(this, MyCommon.TwitterUrl);
2047         }
2048
2049         private void TweenMain_ClientSizeChanged(object sender, EventArgs e)
2050         {
2051             if ((!this.initialLayout) && this.Visible)
2052             {
2053                 if (this.WindowState == FormWindowState.Normal)
2054                 {
2055                     this.mySize = this.ClientSize;
2056                     this.mySpDis = this.SplitContainer1.SplitterDistance;
2057                     this.mySpDis3 = this.SplitContainer3.SplitterDistance;
2058                     if (this.StatusText.Multiline) this.mySpDis2 = this.StatusText.Height;
2059                     this.MarkSettingLocalModified();
2060                 }
2061             }
2062         }
2063
2064         private void MyList_ColumnClick(object sender, ColumnClickEventArgs e)
2065         {
2066             var comparerMode = this.GetComparerModeByColumnIndex(e.Column);
2067             if (comparerMode == null)
2068                 return;
2069
2070             this.SetSortColumn(comparerMode.Value);
2071         }
2072
2073         /// <summary>
2074         /// 列インデックスからソートを行う ComparerMode を求める
2075         /// </summary>
2076         /// <param name="columnIndex">ソートを行うカラムのインデックス (表示上の順序とは異なる)</param>
2077         /// <returns>ソートを行う ComparerMode。null であればソートを行わない</returns>
2078         private ComparerMode? GetComparerModeByColumnIndex(int columnIndex)
2079         {
2080             if (this.Use2ColumnsMode)
2081                 return ComparerMode.Id;
2082
2083             return columnIndex switch
2084             {
2085                 1 => ComparerMode.Nickname, // ニックネーム
2086                 2 => ComparerMode.Data, // 本文
2087                 3 => ComparerMode.Id, // 時刻=発言Id
2088                 4 => ComparerMode.Name, // 名前
2089                 7 => ComparerMode.Source, // Source
2090                 _ => (ComparerMode?)null, // 0:アイコン, 5:未読マーク, 6:プロテクト・フィルターマーク
2091             };
2092         }
2093
2094         /// <summary>
2095         /// 発言一覧の指定した位置の列でソートする
2096         /// </summary>
2097         /// <param name="columnIndex">ソートする列の位置 (表示上の順序で指定)</param>
2098         private void SetSortColumnByDisplayIndex(int columnIndex)
2099         {
2100             // 表示上の列の位置から ColumnHeader を求める
2101             var col = this.CurrentListView.Columns.Cast<ColumnHeader>()
2102                 .FirstOrDefault(x => x.DisplayIndex == columnIndex);
2103
2104             if (col == null)
2105                 return;
2106
2107             var comparerMode = this.GetComparerModeByColumnIndex(col.Index);
2108             if (comparerMode == null)
2109                 return;
2110
2111             this.SetSortColumn(comparerMode.Value);
2112         }
2113
2114         /// <summary>
2115         /// 発言一覧の最後列の項目でソートする
2116         /// </summary>
2117         private void SetSortLastColumn()
2118         {
2119             // 表示上の最後列にある ColumnHeader を求める
2120             var col = this.CurrentListView.Columns.Cast<ColumnHeader>()
2121                 .OrderByDescending(x => x.DisplayIndex)
2122                 .First();
2123
2124             var comparerMode = this.GetComparerModeByColumnIndex(col.Index);
2125             if (comparerMode == null)
2126                 return;
2127
2128             this.SetSortColumn(comparerMode.Value);
2129         }
2130
2131         /// <summary>
2132         /// 発言一覧を指定された ComparerMode に基づいてソートする
2133         /// </summary>
2134         private void SetSortColumn(ComparerMode sortColumn)
2135         {
2136             if (this.settings.Common.SortOrderLock)
2137                 return;
2138
2139             this.statuses.ToggleSortOrder(sortColumn);
2140             this.InitColumnText();
2141
2142             var list = this.CurrentListView;
2143             if (this.Use2ColumnsMode)
2144             {
2145                 list.Columns[0].Text = this.columnText[0];
2146                 list.Columns[1].Text = this.columnText[2];
2147             }
2148             else
2149             {
2150                 for (var i = 0; i <= 7; i++)
2151                 {
2152                     list.Columns[i].Text = this.columnText[i];
2153                 }
2154             }
2155
2156             this.listCache?.PurgeCache();
2157
2158             var tab = this.CurrentTab;
2159             var post = this.CurrentPost;
2160             if (tab.AllCount > 0 && post != null)
2161             {
2162                 var idx = tab.IndexOf(post.StatusId);
2163                 if (idx > -1)
2164                 {
2165                     this.SelectListItem(list, idx);
2166                     list.EnsureVisible(idx);
2167                 }
2168             }
2169             list.Refresh();
2170
2171             this.MarkSettingCommonModified();
2172         }
2173
2174         private void TweenMain_LocationChanged(object sender, EventArgs e)
2175         {
2176             if (this.WindowState == FormWindowState.Normal && !this.initialLayout)
2177             {
2178                 this.myLoc = this.DesktopLocation;
2179                 this.MarkSettingLocalModified();
2180             }
2181         }
2182
2183         private void ContextMenuOperate_Opening(object sender, CancelEventArgs e)
2184         {
2185             var post = this.CurrentPost;
2186             if (!this.ExistCurrentPost)
2187             {
2188                 this.ReplyStripMenuItem.Enabled = false;
2189                 this.ReplyAllStripMenuItem.Enabled = false;
2190                 this.DMStripMenuItem.Enabled = false;
2191                 this.TabMenuItem.Enabled = false;
2192                 this.IDRuleMenuItem.Enabled = false;
2193                 this.SourceRuleMenuItem.Enabled = false;
2194                 this.ReadedStripMenuItem.Enabled = false;
2195                 this.UnreadStripMenuItem.Enabled = false;
2196                 this.AuthorContextMenuItem.Visible = false;
2197                 this.RetweetedByContextMenuItem.Visible = false;
2198             }
2199             else
2200             {
2201                 this.ReplyStripMenuItem.Enabled = true;
2202                 this.ReplyAllStripMenuItem.Enabled = true;
2203                 this.DMStripMenuItem.Enabled = true;
2204                 this.TabMenuItem.Enabled = true;
2205                 this.IDRuleMenuItem.Enabled = true;
2206                 this.SourceRuleMenuItem.Enabled = true;
2207                 this.ReadedStripMenuItem.Enabled = true;
2208                 this.UnreadStripMenuItem.Enabled = true;
2209                 this.AuthorContextMenuItem.Visible = true;
2210                 this.AuthorContextMenuItem.Text = $"@{post!.ScreenName}";
2211                 this.RetweetedByContextMenuItem.Visible = post.RetweetedByUserId != null;
2212                 this.RetweetedByContextMenuItem.Text = $"@{post.RetweetedBy}";
2213             }
2214             var tab = this.CurrentTab;
2215             if (tab.TabType == MyCommon.TabUsageType.DirectMessage || !this.ExistCurrentPost || post == null || post.IsDm)
2216             {
2217                 this.FavAddToolStripMenuItem.Enabled = false;
2218                 this.FavRemoveToolStripMenuItem.Enabled = false;
2219                 this.StatusOpenMenuItem.Enabled = false;
2220                 this.ShowRelatedStatusesMenuItem.Enabled = false;
2221
2222                 this.ReTweetStripMenuItem.Enabled = false;
2223                 this.ReTweetUnofficialStripMenuItem.Enabled = false;
2224                 this.QuoteStripMenuItem.Enabled = false;
2225                 this.FavoriteRetweetContextMenu.Enabled = false;
2226                 this.FavoriteRetweetUnofficialContextMenu.Enabled = false;
2227             }
2228             else
2229             {
2230                 this.FavAddToolStripMenuItem.Enabled = true;
2231                 this.FavRemoveToolStripMenuItem.Enabled = true;
2232                 this.StatusOpenMenuItem.Enabled = true;
2233                 this.ShowRelatedStatusesMenuItem.Enabled = true;  // PublicSearchの時問題出るかも
2234
2235                 if (!post.CanRetweetBy(this.tw.UserId))
2236                 {
2237                     this.ReTweetStripMenuItem.Enabled = false;
2238                     this.ReTweetUnofficialStripMenuItem.Enabled = false;
2239                     this.QuoteStripMenuItem.Enabled = false;
2240                     this.FavoriteRetweetContextMenu.Enabled = false;
2241                     this.FavoriteRetweetUnofficialContextMenu.Enabled = false;
2242                 }
2243                 else
2244                 {
2245                     this.ReTweetStripMenuItem.Enabled = true;
2246                     this.ReTweetUnofficialStripMenuItem.Enabled = true;
2247                     this.QuoteStripMenuItem.Enabled = true;
2248                     this.FavoriteRetweetContextMenu.Enabled = true;
2249                     this.FavoriteRetweetUnofficialContextMenu.Enabled = true;
2250                 }
2251             }
2252
2253             if (!this.ExistCurrentPost || post == null || post.InReplyToStatusId == null)
2254             {
2255                 this.RepliedStatusOpenMenuItem.Enabled = false;
2256             }
2257             else
2258             {
2259                 this.RepliedStatusOpenMenuItem.Enabled = true;
2260             }
2261
2262             if (this.ExistCurrentPost && post != null)
2263             {
2264                 this.DeleteStripMenuItem.Enabled = post.CanDeleteBy(this.tw.UserId);
2265                 if (post.RetweetedByUserId == this.tw.UserId)
2266                     this.DeleteStripMenuItem.Text = Properties.Resources.DeleteMenuText2;
2267                 else
2268                     this.DeleteStripMenuItem.Text = Properties.Resources.DeleteMenuText1;
2269             }
2270         }
2271
2272         private void ReplyStripMenuItem_Click(object sender, EventArgs e)
2273             => this.MakeReplyText();
2274
2275         private void DMStripMenuItem_Click(object sender, EventArgs e)
2276             => this.MakeDirectMessageText();
2277
2278         private async Task DoStatusDelete()
2279         {
2280             var posts = this.CurrentTab.SelectedPosts;
2281             if (posts.Length == 0)
2282                 return;
2283
2284             // 選択されたツイートの中に削除可能なものが一つでもあるか
2285             if (!posts.Any(x => x.CanDeleteBy(this.tw.UserId)))
2286                 return;
2287
2288             var ret = MessageBox.Show(
2289                 this,
2290                 string.Format(Properties.Resources.DeleteStripMenuItem_ClickText1, Environment.NewLine),
2291                 Properties.Resources.DeleteStripMenuItem_ClickText2,
2292                 MessageBoxButtons.OKCancel,
2293                 MessageBoxIcon.Question);
2294
2295             if (ret != DialogResult.OK)
2296                 return;
2297
2298             var currentListView = this.CurrentListView;
2299             var focusedIndex = currentListView.FocusedItem?.Index ?? currentListView.TopItem?.Index ?? 0;
2300
2301             using (ControlTransaction.Cursor(this, Cursors.WaitCursor))
2302             {
2303                 Exception? lastException = null;
2304                 foreach (var post in posts)
2305                 {
2306                     if (!post.CanDeleteBy(this.tw.UserId))
2307                         continue;
2308
2309                     try
2310                     {
2311                         if (post.StatusId is TwitterDirectMessageId dmId)
2312                         {
2313                             await this.tw.Api.DirectMessagesEventsDestroy(dmId);
2314                         }
2315                         else
2316                         {
2317                             if (post.RetweetedByUserId == this.tw.UserId)
2318                             {
2319                                 // 自分が RT したツイート (自分が RT した自分のツイートも含む)
2320                                 //   => RT を取り消し
2321                                 await this.tw.DeleteRetweet(post);
2322                             }
2323                             else
2324                             {
2325                                 if (post.UserId == this.tw.UserId)
2326                                 {
2327                                     if (post.RetweetedId != null)
2328                                     {
2329                                         // 他人に RT された自分のツイート
2330                                         //   => RT 元の自分のツイートを削除
2331                                         await this.tw.DeleteTweet(post.RetweetedId.ToTwitterStatusId());
2332                                     }
2333                                     else
2334                                     {
2335                                         // 自分のツイート
2336                                         //   => ツイートを削除
2337                                         await this.tw.DeleteTweet(post.StatusId.ToTwitterStatusId());
2338                                     }
2339                                 }
2340                             }
2341                         }
2342                     }
2343                     catch (WebApiException ex)
2344                     {
2345                         lastException = ex;
2346                         continue;
2347                     }
2348
2349                     this.statuses.RemovePostFromAllTabs(post.StatusId, setIsDeleted: true);
2350                 }
2351
2352                 if (lastException == null)
2353                     this.StatusLabel.Text = Properties.Resources.DeleteStripMenuItem_ClickText4; // 成功
2354                 else
2355                     this.StatusLabel.Text = Properties.Resources.DeleteStripMenuItem_ClickText3; // 失敗
2356
2357                 using (ControlTransaction.Update(currentListView))
2358                 {
2359                     this.listCache?.PurgeCache();
2360                     this.listCache?.UpdateListSize();
2361
2362                     currentListView.SelectedIndices.Clear();
2363
2364                     var currentTab = this.CurrentTab;
2365                     if (currentTab.AllCount != 0)
2366                     {
2367                         int selectedIndex;
2368                         if (currentTab.AllCount - 1 > focusedIndex && focusedIndex > -1)
2369                             selectedIndex = focusedIndex;
2370                         else
2371                             selectedIndex = currentTab.AllCount - 1;
2372
2373                         currentListView.SelectedIndices.Add(selectedIndex);
2374                         currentListView.EnsureVisible(selectedIndex);
2375                         currentListView.FocusedItem = currentListView.Items[selectedIndex];
2376                     }
2377                 }
2378
2379                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
2380                 {
2381                     var tabPage = this.ListTab.TabPages[index];
2382                     if (this.settings.Common.TabIconDisp && tab.UnreadCount == 0)
2383                     {
2384                         if (tabPage.ImageIndex == 0)
2385                             tabPage.ImageIndex = -1; // タブアイコン
2386                     }
2387                 }
2388
2389                 if (!this.settings.Common.TabIconDisp)
2390                     this.ListTab.Refresh();
2391             }
2392         }
2393
2394         private async void DeleteStripMenuItem_Click(object sender, EventArgs e)
2395             => await this.DoStatusDelete();
2396
2397         private void ReadedStripMenuItem_Click(object sender, EventArgs e)
2398         {
2399             using (ControlTransaction.Update(this.CurrentListView))
2400             {
2401                 var tab = this.CurrentTab;
2402                 foreach (var statusId in tab.SelectedStatusIds)
2403                 {
2404                     this.statuses.SetReadAllTab(statusId, read: true);
2405                     var idx = tab.IndexOf(statusId);
2406                     this.listCache?.RefreshStyle(idx);
2407                 }
2408             }
2409             if (this.settings.Common.TabIconDisp)
2410             {
2411                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
2412                 {
2413                     if (tab.UnreadCount == 0)
2414                     {
2415                         var tabPage = this.ListTab.TabPages[index];
2416                         if (tabPage.ImageIndex == 0)
2417                             tabPage.ImageIndex = -1; // タブアイコン
2418                     }
2419                 }
2420             }
2421             if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh();
2422         }
2423
2424         private void UnreadStripMenuItem_Click(object sender, EventArgs e)
2425         {
2426             using (ControlTransaction.Update(this.CurrentListView))
2427             {
2428                 var tab = this.CurrentTab;
2429                 foreach (var statusId in tab.SelectedStatusIds)
2430                 {
2431                     this.statuses.SetReadAllTab(statusId, read: false);
2432                     var idx = tab.IndexOf(statusId);
2433                     this.listCache?.RefreshStyle(idx);
2434                 }
2435             }
2436             if (this.settings.Common.TabIconDisp)
2437             {
2438                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
2439                 {
2440                     if (tab.UnreadCount > 0)
2441                     {
2442                         var tabPage = this.ListTab.TabPages[index];
2443                         if (tabPage.ImageIndex == -1)
2444                             tabPage.ImageIndex = 0; // タブアイコン
2445                     }
2446                 }
2447             }
2448             if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh();
2449         }
2450
2451         private async void RefreshStripMenuItem_Click(object sender, EventArgs e)
2452             => await this.DoRefresh();
2453
2454         private async Task DoRefresh()
2455             => await this.RefreshTabAsync(this.CurrentTab);
2456
2457         private async Task DoRefreshMore()
2458             => await this.RefreshTabAsync(this.CurrentTab, backward: true);
2459
2460         private DialogResult ShowSettingDialog()
2461         {
2462             using var settingDialog = new AppendSettingDialog();
2463             settingDialog.Icon = this.iconAssets.IconMain;
2464             settingDialog.IntervalChanged += this.TimerInterval_Changed;
2465
2466             settingDialog.LoadConfig(this.settings.Common, this.settings.Local);
2467
2468             DialogResult result;
2469             try
2470             {
2471                 result = settingDialog.ShowDialog(this);
2472             }
2473             catch (Exception)
2474             {
2475                 return DialogResult.Abort;
2476             }
2477
2478             if (result == DialogResult.OK)
2479             {
2480                 lock (this.syncObject)
2481                 {
2482                     settingDialog.SaveConfig(this.settings.Common, this.settings.Local);
2483                 }
2484             }
2485
2486             return result;
2487         }
2488
2489         private async void SettingStripMenuItem_Click(object sender, EventArgs e)
2490         {
2491             // 設定画面表示前のユーザー情報
2492             var previousUserId = this.settings.Common.UserId;
2493             var oldIconCol = this.Use2ColumnsMode;
2494
2495             if (this.ShowSettingDialog() == DialogResult.OK)
2496             {
2497                 lock (this.syncObject)
2498                 {
2499                     this.settings.ApplySettings();
2500
2501                     this.accounts.LoadFromSettings(this.settings.Common);
2502                     this.ImageSelector.Model.InitializeServices(this.tw, this.tw.Configuration);
2503
2504                     try
2505                     {
2506                         if (this.settings.Common.TabIconDisp)
2507                         {
2508                             this.ListTab.DrawItem -= this.ListTab_DrawItem;
2509                             this.ListTab.DrawMode = TabDrawMode.Normal;
2510                             this.ListTab.ImageList = this.TabImage;
2511                         }
2512                         else
2513                         {
2514                             this.ListTab.DrawItem -= this.ListTab_DrawItem;
2515                             this.ListTab.DrawItem += this.ListTab_DrawItem;
2516                             this.ListTab.DrawMode = TabDrawMode.OwnerDrawFixed;
2517                             this.ListTab.ImageList = null;
2518                         }
2519                     }
2520                     catch (Exception ex)
2521                     {
2522                         ex.Data["Instance"] = "ListTab(TabIconDisp)";
2523                         ex.Data["IsTerminatePermission"] = false;
2524                         throw;
2525                     }
2526
2527                     try
2528                     {
2529                         if (!this.settings.Common.UnreadManage)
2530                         {
2531                             this.ReadedStripMenuItem.Enabled = false;
2532                             this.UnreadStripMenuItem.Enabled = false;
2533                             if (this.settings.Common.TabIconDisp)
2534                             {
2535                                 foreach (TabPage myTab in this.ListTab.TabPages)
2536                                 {
2537                                     myTab.ImageIndex = -1;
2538                                 }
2539                             }
2540                         }
2541                         else
2542                         {
2543                             this.ReadedStripMenuItem.Enabled = true;
2544                             this.UnreadStripMenuItem.Enabled = true;
2545                         }
2546                     }
2547                     catch (Exception ex)
2548                     {
2549                         ex.Data["Instance"] = "ListTab(UnreadManage)";
2550                         ex.Data["IsTerminatePermission"] = false;
2551                         throw;
2552                     }
2553
2554                     // タブの表示位置の決定
2555                     this.SetTabAlignment();
2556
2557                     this.SplitContainer1.IsPanelInverted = !this.settings.Common.StatusAreaAtBottom;
2558
2559                     var imgazyobizinet = this.thumbGenerator.ImgAzyobuziNet;
2560                     imgazyobizinet.Enabled = this.settings.Common.EnableImgAzyobuziNet;
2561                     imgazyobizinet.DisabledInDM = this.settings.Common.ImgAzyobuziNetDisabledInDM;
2562
2563                     this.NewPostPopMenuItem.Checked = this.settings.Common.NewAllPop;
2564                     this.NotifyFileMenuItem.Checked = this.settings.Common.NewAllPop;
2565                     this.PlaySoundMenuItem.Checked = this.settings.Common.PlaySound;
2566                     this.PlaySoundFileMenuItem.Checked = this.settings.Common.PlaySound;
2567
2568                     var newTheme = new ThemeManager(this.settings.Local);
2569                     (var oldTheme, this.themeManager) = (this.themeManager, newTheme);
2570                     this.tweetDetailsView.Theme = this.themeManager;
2571                     if (this.listDrawer != null)
2572                         this.listDrawer.Theme = this.themeManager;
2573                     oldTheme.Dispose();
2574
2575                     try
2576                     {
2577                         if (this.StatusText.Focused)
2578                             this.StatusText.BackColor = this.themeManager.ColorInputBackcolor;
2579
2580                         this.StatusText.Font = this.themeManager.FontInputFont;
2581                         this.StatusText.ForeColor = this.themeManager.ColorInputFont;
2582                     }
2583                     catch (Exception ex)
2584                     {
2585                         MessageBox.Show(ex.Message);
2586                     }
2587
2588                     try
2589                     {
2590                         this.detailsHtmlBuilder.Prepare(this.settings.Common, this.themeManager);
2591                     }
2592                     catch (Exception ex)
2593                     {
2594                         ex.Data["Instance"] = "Font";
2595                         ex.Data["IsTerminatePermission"] = false;
2596                         throw;
2597                     }
2598
2599                     try
2600                     {
2601                         if (this.settings.Common.TabIconDisp)
2602                         {
2603                             foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
2604                             {
2605                                 var tabPage = this.ListTab.TabPages[index];
2606                                 if (tab.UnreadCount == 0)
2607                                     tabPage.ImageIndex = -1;
2608                                 else
2609                                     tabPage.ImageIndex = 0;
2610                             }
2611                         }
2612                     }
2613                     catch (Exception ex)
2614                     {
2615                         ex.Data["Instance"] = "ListTab(TabIconDisp no2)";
2616                         ex.Data["IsTerminatePermission"] = false;
2617                         throw;
2618                     }
2619
2620                     try
2621                     {
2622                         this.ApplyListViewIconSize(this.settings.Common.IconSize);
2623
2624                         foreach (TabPage tp in this.ListTab.TabPages)
2625                         {
2626                             var lst = (DetailsListView)tp.Tag;
2627
2628                             using (ControlTransaction.Update(lst))
2629                             {
2630                                 lst.GridLines = this.settings.Common.ShowGrid;
2631
2632                                 if (this.Use2ColumnsMode != oldIconCol)
2633                                     this.ResetColumns(lst);
2634                             }
2635                         }
2636                     }
2637                     catch (Exception ex)
2638                     {
2639                         ex.Data["Instance"] = "ListView(IconSize)";
2640                         ex.Data["IsTerminatePermission"] = false;
2641                         throw;
2642                     }
2643
2644                     this.SetMainWindowTitle();
2645                     this.SetNotifyIconText();
2646
2647                     this.listCache?.PurgeCache();
2648                     this.CurrentListView.Refresh();
2649                     this.ListTab.Refresh();
2650
2651                     this.hookGlobalHotkey.UnregisterAllOriginalHotkey();
2652                     if (this.settings.Common.HotkeyEnabled)
2653                     {
2654                         // グローバルホットキーの登録。設定で変更可能にするかも
2655                         var modKey = HookGlobalHotkey.ModKeys.None;
2656                         if ((this.settings.Common.HotkeyModifier & Keys.Alt) == Keys.Alt)
2657                             modKey |= HookGlobalHotkey.ModKeys.Alt;
2658                         if ((this.settings.Common.HotkeyModifier & Keys.Control) == Keys.Control)
2659                             modKey |= HookGlobalHotkey.ModKeys.Ctrl;
2660                         if ((this.settings.Common.HotkeyModifier & Keys.Shift) == Keys.Shift)
2661                             modKey |= HookGlobalHotkey.ModKeys.Shift;
2662                         if ((this.settings.Common.HotkeyModifier & Keys.LWin) == Keys.LWin)
2663                             modKey |= HookGlobalHotkey.ModKeys.Win;
2664
2665                         this.hookGlobalHotkey.RegisterOriginalHotkey(this.settings.Common.HotkeyKey, this.settings.Common.HotkeyValue, modKey);
2666                     }
2667
2668                     if (this.settings.Common.IsUseNotifyGrowl) this.gh.RegisterGrowl();
2669                     try
2670                     {
2671                         this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
2672                     }
2673                     catch (Exception)
2674                     {
2675                     }
2676                 }
2677             }
2678
2679             Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
2680
2681             this.TopMost = this.settings.Common.AlwaysTop;
2682             this.SaveConfigsAll(false);
2683
2684             if (this.tw.UserId != previousUserId)
2685                 await this.DoGetFollowersMenu();
2686         }
2687
2688         /// <summary>
2689         /// タブの表示位置を設定する
2690         /// </summary>
2691         private void SetTabAlignment()
2692         {
2693             var newAlignment = this.settings.Common.ViewTabBottom ? TabAlignment.Bottom : TabAlignment.Top;
2694             if (this.ListTab.Alignment == newAlignment) return;
2695
2696             // リスト上の選択位置などを退避
2697             var currentListViewState = this.listViewState[this.CurrentTabName];
2698             currentListViewState.Save(this.ListLockMenuItem.Checked);
2699
2700             this.ListTab.Alignment = newAlignment;
2701
2702             currentListViewState.Restore(forceScroll: true);
2703         }
2704
2705         private void ApplyListViewIconSize(MyCommon.IconSizes iconSz)
2706         {
2707             // アイコンサイズの再設定
2708             if (this.listDrawer != null)
2709             {
2710                 this.listDrawer.IconSize = iconSz;
2711                 this.listDrawer.UpdateItemHeight();
2712             }
2713
2714             this.listCache?.PurgeCache();
2715         }
2716
2717         private void ResetColumns(DetailsListView list)
2718         {
2719             using (ControlTransaction.Update(list))
2720             using (ControlTransaction.Layout(list, false))
2721             {
2722                 // カラムヘッダの再設定
2723                 list.ColumnClick -= this.MyList_ColumnClick;
2724                 list.DrawColumnHeader -= this.MyList_DrawColumnHeader;
2725                 list.ColumnReordered -= this.MyList_ColumnReordered;
2726                 list.ColumnWidthChanged -= this.MyList_ColumnWidthChanged;
2727
2728                 var cols = list.Columns.Cast<ColumnHeader>().ToList();
2729                 list.Columns.Clear();
2730                 cols.ForEach(col => col.Dispose());
2731                 cols.Clear();
2732
2733                 this.InitColumns(list, true);
2734
2735                 list.ColumnClick += this.MyList_ColumnClick;
2736                 list.DrawColumnHeader += this.MyList_DrawColumnHeader;
2737                 list.ColumnReordered += this.MyList_ColumnReordered;
2738                 list.ColumnWidthChanged += this.MyList_ColumnWidthChanged;
2739             }
2740         }
2741
2742         public void AddNewTabForSearch(string searchWord)
2743         {
2744             // 同一検索条件のタブが既に存在すれば、そのタブアクティブにして終了
2745             foreach (var tb in this.statuses.GetTabsByType<PublicSearchTabModel>())
2746             {
2747                 if (tb.SearchWords == searchWord && MyCommon.IsNullOrEmpty(tb.SearchLang))
2748                 {
2749                     var tabIndex = this.statuses.Tabs.IndexOf(tb);
2750                     this.ListTab.SelectedIndex = tabIndex;
2751                     return;
2752                 }
2753             }
2754             // ユニークなタブ名生成
2755             var tabName = searchWord;
2756             for (var i = 0; i <= 100; i++)
2757             {
2758                 if (this.statuses.ContainsTab(tabName))
2759                     tabName += "_";
2760                 else
2761                     break;
2762             }
2763             // タブ追加
2764             var tab = new PublicSearchTabModel(tabName);
2765             this.statuses.AddTab(tab);
2766             this.AddNewTab(tab, startup: false);
2767             // 追加したタブをアクティブに
2768             this.ListTab.SelectedIndex = this.statuses.Tabs.Count - 1;
2769             // 検索条件の設定
2770             var tabPage = this.CurrentTabPage;
2771             var cmb = (ComboBox)tabPage.Controls["panelSearch"].Controls["comboSearch"];
2772             cmb.Items.Add(searchWord);
2773             cmb.Text = searchWord;
2774             this.SaveConfigsTabs();
2775             // 検索実行
2776             this.SearchButton_Click(tabPage.Controls["panelSearch"].Controls["comboSearch"], EventArgs.Empty);
2777         }
2778
2779         private async Task ShowUserTimeline()
2780         {
2781             var post = this.CurrentPost;
2782             if (post == null || !this.ExistCurrentPost) return;
2783             await this.AddNewTabForUserTimeline(post.ScreenName);
2784         }
2785
2786         private async Task ShowRetweeterTimeline()
2787         {
2788             var retweetedBy = this.CurrentPost?.RetweetedBy;
2789             if (retweetedBy == null || !this.ExistCurrentPost) return;
2790             await this.AddNewTabForUserTimeline(retweetedBy);
2791         }
2792
2793         private void SearchComboBox_KeyDown(object sender, KeyEventArgs e)
2794         {
2795             if (e.KeyCode == Keys.Escape)
2796             {
2797                 this.RemoveSpecifiedTab(this.CurrentTabName, false);
2798                 this.SaveConfigsTabs();
2799                 e.SuppressKeyPress = true;
2800             }
2801         }
2802
2803         public async Task AddNewTabForUserTimeline(string user)
2804         {
2805             // 同一検索条件のタブが既に存在すれば、そのタブアクティブにして終了
2806             foreach (var tb in this.statuses.GetTabsByType<UserTimelineTabModel>())
2807             {
2808                 if (tb.ScreenName == user)
2809                 {
2810                     var tabIndex = this.statuses.Tabs.IndexOf(tb);
2811                     this.ListTab.SelectedIndex = tabIndex;
2812                     return;
2813                 }
2814             }
2815             // ユニークなタブ名生成
2816             var tabName = "user:" + user;
2817             while (this.statuses.ContainsTab(tabName))
2818             {
2819                 tabName += "_";
2820             }
2821             // タブ追加
2822             var tab = new UserTimelineTabModel(tabName, user);
2823             this.statuses.AddTab(tab);
2824             this.AddNewTab(tab, startup: false);
2825             // 追加したタブをアクティブに
2826             this.ListTab.SelectedIndex = this.statuses.Tabs.Count - 1;
2827             this.SaveConfigsTabs();
2828             // 検索実行
2829             await this.RefreshTabAsync(tab);
2830         }
2831
2832         public bool AddNewTab(TabModel tab, bool startup)
2833         {
2834             // 重複チェック
2835             if (this.ListTab.TabPages.Cast<TabPage>().Any(x => x.Text == tab.TabName))
2836                 return false;
2837
2838             // 新規タブ名チェック
2839             if (tab.TabName == Properties.Resources.AddNewTabText1) return false;
2840
2841             var tabPage = new TabPage();
2842             var listCustom = new DetailsListView();
2843
2844             var cnt = this.statuses.Tabs.Count;
2845
2846             // ToDo:Create and set controls follow tabtypes
2847
2848             using (ControlTransaction.Update(listCustom))
2849             using (ControlTransaction.Layout(this.SplitContainer1.Panel1, false))
2850             using (ControlTransaction.Layout(this.SplitContainer1.Panel2, false))
2851             using (ControlTransaction.Layout(this.SplitContainer1, false))
2852             using (ControlTransaction.Layout(this.ListTab, false))
2853             using (ControlTransaction.Layout(this))
2854             using (ControlTransaction.Layout(tabPage, false))
2855             {
2856                 tabPage.Controls.Add(listCustom);
2857
2858                 // UserTimeline関連
2859                 var userTab = tab as UserTimelineTabModel;
2860                 var listTab = tab as ListTimelineTabModel;
2861                 var searchTab = tab as PublicSearchTabModel;
2862
2863                 if (userTab != null || listTab != null)
2864                 {
2865                     var label = new Label
2866                     {
2867                         Dock = DockStyle.Top,
2868                         Name = "labelUser",
2869                         TabIndex = 0,
2870                     };
2871
2872                     if (listTab != null)
2873                     {
2874                         label.Text = listTab.ListInfo.ToString();
2875                     }
2876                     else if (userTab != null)
2877                     {
2878                         label.Text = userTab.ScreenName + "'s Timeline";
2879                     }
2880                     label.TextAlign = ContentAlignment.MiddleLeft;
2881                     using (var tmpComboBox = new ComboBox())
2882                     {
2883                         label.Height = tmpComboBox.Height;
2884                     }
2885                     tabPage.Controls.Add(label);
2886                 }
2887                 // 検索関連の準備
2888                 else if (searchTab != null)
2889                 {
2890                     var pnl = new Panel();
2891
2892                     var lbl = new Label();
2893                     var cmb = new ComboBox();
2894                     var btn = new Button();
2895                     var cmbLang = new ComboBox();
2896
2897                     using (ControlTransaction.Layout(pnl, false))
2898                     {
2899                         pnl.Controls.Add(cmb);
2900                         pnl.Controls.Add(cmbLang);
2901                         pnl.Controls.Add(btn);
2902                         pnl.Controls.Add(lbl);
2903                         pnl.Name = "panelSearch";
2904                         pnl.TabIndex = 0;
2905                         pnl.Dock = DockStyle.Top;
2906                         pnl.Height = cmb.Height;
2907                         pnl.Enter += this.SearchControls_Enter;
2908                         pnl.Leave += this.SearchControls_Leave;
2909
2910                         cmb.Text = "";
2911                         cmb.Anchor = AnchorStyles.Left | AnchorStyles.Right;
2912                         cmb.Dock = DockStyle.Fill;
2913                         cmb.Name = "comboSearch";
2914                         cmb.DropDownStyle = ComboBoxStyle.DropDown;
2915                         cmb.ImeMode = ImeMode.NoControl;
2916                         cmb.TabStop = false;
2917                         cmb.TabIndex = 1;
2918                         cmb.AutoCompleteMode = AutoCompleteMode.None;
2919                         cmb.KeyDown += this.SearchComboBox_KeyDown;
2920
2921                         cmbLang.Text = "";
2922                         cmbLang.Anchor = AnchorStyles.Left | AnchorStyles.Right;
2923                         cmbLang.Dock = DockStyle.Right;
2924                         cmbLang.Width = 50;
2925                         cmbLang.Name = "comboLang";
2926                         cmbLang.DropDownStyle = ComboBoxStyle.DropDownList;
2927                         cmbLang.TabStop = false;
2928                         cmbLang.TabIndex = 2;
2929                         cmbLang.Items.Add("");
2930                         cmbLang.Items.Add("ja");
2931                         cmbLang.Items.Add("en");
2932                         cmbLang.Items.Add("ar");
2933                         cmbLang.Items.Add("da");
2934                         cmbLang.Items.Add("nl");
2935                         cmbLang.Items.Add("fa");
2936                         cmbLang.Items.Add("fi");
2937                         cmbLang.Items.Add("fr");
2938                         cmbLang.Items.Add("de");
2939                         cmbLang.Items.Add("hu");
2940                         cmbLang.Items.Add("is");
2941                         cmbLang.Items.Add("it");
2942                         cmbLang.Items.Add("no");
2943                         cmbLang.Items.Add("pl");
2944                         cmbLang.Items.Add("pt");
2945                         cmbLang.Items.Add("ru");
2946                         cmbLang.Items.Add("es");
2947                         cmbLang.Items.Add("sv");
2948                         cmbLang.Items.Add("th");
2949
2950                         lbl.Text = "Search(C-S-f)";
2951                         lbl.Name = "label1";
2952                         lbl.Dock = DockStyle.Left;
2953                         lbl.Width = 90;
2954                         lbl.Height = cmb.Height;
2955                         lbl.TextAlign = ContentAlignment.MiddleLeft;
2956                         lbl.TabIndex = 0;
2957
2958                         btn.Text = "Search";
2959                         btn.Name = "buttonSearch";
2960                         btn.UseVisualStyleBackColor = true;
2961                         btn.Dock = DockStyle.Right;
2962                         btn.TabStop = false;
2963                         btn.TabIndex = 3;
2964                         btn.Click += this.SearchButton_Click;
2965
2966                         if (!MyCommon.IsNullOrEmpty(searchTab.SearchWords))
2967                         {
2968                             cmb.Items.Add(searchTab.SearchWords);
2969                             cmb.Text = searchTab.SearchWords;
2970                         }
2971
2972                         cmbLang.Text = searchTab.SearchLang;
2973
2974                         tabPage.Controls.Add(pnl);
2975                     }
2976                 }
2977
2978                 tabPage.Tag = listCustom;
2979                 this.ListTab.Controls.Add(tabPage);
2980
2981                 tabPage.Location = new Point(4, 4);
2982                 tabPage.Name = "CTab" + cnt;
2983                 tabPage.Size = new Size(380, 260);
2984                 tabPage.TabIndex = 2 + cnt;
2985                 tabPage.Text = tab.TabName;
2986                 tabPage.UseVisualStyleBackColor = true;
2987                 tabPage.AccessibleRole = AccessibleRole.PageTab;
2988
2989                 listCustom.AccessibleName = Properties.Resources.AddNewTab_ListView_AccessibleName;
2990                 listCustom.TabIndex = 1;
2991                 listCustom.AllowColumnReorder = true;
2992                 listCustom.ContextMenuStrip = this.ContextMenuOperate;
2993                 listCustom.ColumnHeaderContextMenuStrip = this.ContextMenuColumnHeader;
2994                 listCustom.Dock = DockStyle.Fill;
2995                 listCustom.FullRowSelect = true;
2996                 listCustom.HideSelection = false;
2997                 listCustom.Location = new Point(0, 0);
2998                 listCustom.Margin = new Padding(0);
2999                 listCustom.Name = "CList" + Environment.TickCount;
3000                 listCustom.ShowItemToolTips = true;
3001                 listCustom.Size = new Size(380, 260);
3002                 listCustom.UseCompatibleStateImageBehavior = false;
3003                 listCustom.View = View.Details;
3004                 listCustom.OwnerDraw = true;
3005                 listCustom.VirtualMode = true;
3006
3007                 listCustom.GridLines = this.settings.Common.ShowGrid;
3008                 listCustom.AllowDrop = true;
3009
3010                 this.InitColumns(listCustom, startup);
3011
3012                 listCustom.SelectedIndexChanged += this.MyList_SelectedIndexChanged;
3013                 listCustom.MouseDoubleClick += this.MyList_MouseDoubleClick;
3014                 listCustom.ColumnClick += this.MyList_ColumnClick;
3015                 listCustom.DrawColumnHeader += this.MyList_DrawColumnHeader;
3016                 listCustom.DragDrop += this.TweenMain_DragDrop;
3017                 listCustom.DragEnter += this.TweenMain_DragEnter;
3018                 listCustom.DragOver += this.TweenMain_DragOver;
3019                 listCustom.MouseClick += this.MyList_MouseClick;
3020                 listCustom.ColumnReordered += this.MyList_ColumnReordered;
3021                 listCustom.ColumnWidthChanged += this.MyList_ColumnWidthChanged;
3022                 listCustom.HScrolled += this.MyList_HScrolled;
3023             }
3024
3025             var state = new TimelineListViewState(listCustom, tab);
3026             this.listViewState[tab.TabName] = state;
3027
3028             return true;
3029         }
3030
3031         public bool RemoveSpecifiedTab(string tabName, bool confirm)
3032         {
3033             var tabInfo = this.statuses.GetTabByName(tabName);
3034             if (tabInfo == null || tabInfo.IsDefaultTabType || tabInfo.Protected)
3035                 return false;
3036
3037             if (confirm)
3038             {
3039                 var tmp = string.Format(Properties.Resources.RemoveSpecifiedTabText1, Environment.NewLine);
3040                 var result = MessageBox.Show(
3041                     tmp,
3042                     tabName + " " + Properties.Resources.RemoveSpecifiedTabText2,
3043                     MessageBoxButtons.OKCancel,
3044                     MessageBoxIcon.Question,
3045                     MessageBoxDefaultButton.Button2);
3046                 if (result == DialogResult.Cancel)
3047                 {
3048                     return false;
3049                 }
3050             }
3051
3052             var tabIndex = this.statuses.Tabs.IndexOf(tabName);
3053             if (tabIndex == -1)
3054                 return false;
3055
3056             var tabPage = this.ListTab.TabPages[tabIndex];
3057
3058             this.SetListProperty();   // 他のタブに列幅等を反映
3059
3060             this.listViewState.Remove(tabName);
3061
3062             // オブジェクトインスタンスの削除
3063             var listCustom = (DetailsListView)tabPage.Tag;
3064             tabPage.Tag = null;
3065
3066             using (ControlTransaction.Layout(this.SplitContainer1.Panel1, false))
3067             using (ControlTransaction.Layout(this.SplitContainer1.Panel2, false))
3068             using (ControlTransaction.Layout(this.SplitContainer1, false))
3069             using (ControlTransaction.Layout(this.ListTab, false))
3070             using (ControlTransaction.Layout(this))
3071             using (ControlTransaction.Layout(tabPage, false))
3072             {
3073                 if (this.CurrentTabName == tabName)
3074                 {
3075                     this.ListTab.SelectTab((this.beforeSelectedTab != null && this.ListTab.TabPages.Contains(this.beforeSelectedTab)) ? this.beforeSelectedTab : this.ListTab.TabPages[0]);
3076                     this.beforeSelectedTab = null;
3077                 }
3078                 this.ListTab.Controls.Remove(tabPage);
3079
3080                 // 後付けのコントロールを破棄
3081                 if (tabInfo.TabType == MyCommon.TabUsageType.UserTimeline || tabInfo.TabType == MyCommon.TabUsageType.Lists)
3082                 {
3083                     using var label = tabPage.Controls["labelUser"];
3084                     tabPage.Controls.Remove(label);
3085                 }
3086                 else if (tabInfo.TabType == MyCommon.TabUsageType.PublicSearch)
3087                 {
3088                     using var pnl = tabPage.Controls["panelSearch"];
3089
3090                     pnl.Enter -= this.SearchControls_Enter;
3091                     pnl.Leave -= this.SearchControls_Leave;
3092                     tabPage.Controls.Remove(pnl);
3093
3094                     foreach (Control ctrl in pnl.Controls)
3095                     {
3096                         if (ctrl.Name == "buttonSearch")
3097                         {
3098                             ctrl.Click -= this.SearchButton_Click;
3099                         }
3100                         else if (ctrl.Name == "comboSearch")
3101                         {
3102                             ctrl.KeyDown -= this.SearchComboBox_KeyDown;
3103                         }
3104                         pnl.Controls.Remove(ctrl);
3105                         ctrl.Dispose();
3106                     }
3107                 }
3108
3109                 tabPage.Controls.Remove(listCustom);
3110
3111                 listCustom.SelectedIndexChanged -= this.MyList_SelectedIndexChanged;
3112                 listCustom.MouseDoubleClick -= this.MyList_MouseDoubleClick;
3113                 listCustom.ColumnClick -= this.MyList_ColumnClick;
3114                 listCustom.DrawColumnHeader -= this.MyList_DrawColumnHeader;
3115                 listCustom.DragDrop -= this.TweenMain_DragDrop;
3116                 listCustom.DragEnter -= this.TweenMain_DragEnter;
3117                 listCustom.DragOver -= this.TweenMain_DragOver;
3118                 listCustom.MouseClick -= this.MyList_MouseClick;
3119                 listCustom.ColumnReordered -= this.MyList_ColumnReordered;
3120                 listCustom.ColumnWidthChanged -= this.MyList_ColumnWidthChanged;
3121                 listCustom.HScrolled -= this.MyList_HScrolled;
3122
3123                 var cols = listCustom.Columns.Cast<ColumnHeader>().ToList<ColumnHeader>();
3124                 listCustom.Columns.Clear();
3125                 cols.ForEach(col => col.Dispose());
3126                 cols.Clear();
3127
3128                 listCustom.ContextMenuStrip = null;
3129                 listCustom.ColumnHeaderContextMenuStrip = null;
3130                 listCustom.Font = null;
3131
3132                 listCustom.SmallImageList = null;
3133                 listCustom.ListViewItemSorter = null;
3134
3135                 // キャッシュのクリア
3136                 this.listCache?.PurgeCache();
3137             }
3138
3139             tabPage.Dispose();
3140             listCustom.Dispose();
3141             this.statuses.RemoveTab(tabName);
3142
3143             return true;
3144         }
3145
3146         private void ListTab_Deselected(object sender, TabControlEventArgs e)
3147         {
3148             this.listCache?.PurgeCache();
3149             this.beforeSelectedTab = e.TabPage;
3150         }
3151
3152         private void ListTab_MouseMove(object sender, MouseEventArgs e)
3153         {
3154             // タブのD&D
3155
3156             if (!this.settings.Common.TabMouseLock && e.Button == MouseButtons.Left && this.tabDrag)
3157             {
3158                 var tn = "";
3159                 var dragEnableRectangle = new Rectangle(this.tabMouseDownPoint.X - (SystemInformation.DragSize.Width / 2), this.tabMouseDownPoint.Y - (SystemInformation.DragSize.Height / 2), SystemInformation.DragSize.Width, SystemInformation.DragSize.Height);
3160                 if (!dragEnableRectangle.Contains(e.Location))
3161                 {
3162                     // タブが多段の場合にはMouseDownの前の段階で選択されたタブの段が変わっているので、このタイミングでカーソルの位置からタブを判定出来ない。
3163                     tn = this.CurrentTabName;
3164                 }
3165
3166                 if (MyCommon.IsNullOrEmpty(tn)) return;
3167
3168                 var tabIndex = this.statuses.Tabs.IndexOf(tn);
3169                 if (tabIndex != -1)
3170                 {
3171                     var tabPage = this.ListTab.TabPages[tabIndex];
3172                     this.ListTab.DoDragDrop(tabPage, DragDropEffects.All);
3173                 }
3174             }
3175             else
3176             {
3177                 this.tabDrag = false;
3178             }
3179
3180             var cpos = new Point(e.X, e.Y);
3181             foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
3182             {
3183                 var rect = this.ListTab.GetTabRect(index);
3184                 if (rect.Contains(cpos))
3185                 {
3186                     this.rclickTabName = tab.TabName;
3187                     break;
3188                 }
3189             }
3190         }
3191
3192         private void ListTab_SelectedIndexChanged(object sender, EventArgs e)
3193         {
3194             this.SetMainWindowTitle();
3195             this.SetStatusLabelUrl();
3196             this.SetApiStatusLabel();
3197             if (this.ListTab.Focused || ((Control)this.CurrentTabPage.Tag).Focused)
3198                 this.Tag = this.ListTab.Tag;
3199             this.TabMenuControl(this.CurrentTabName);
3200             this.PushSelectPostChain();
3201             this.DispSelectedPost();
3202         }
3203
3204         private void SetListProperty()
3205         {
3206             if (!this.isColumnChanged) return;
3207
3208             var currentListView = this.CurrentListView;
3209
3210             var dispOrder = new int[currentListView.Columns.Count];
3211             for (var i = 0; i < currentListView.Columns.Count; i++)
3212             {
3213                 for (var j = 0; j < currentListView.Columns.Count; j++)
3214                 {
3215                     if (currentListView.Columns[j].DisplayIndex == i)
3216                     {
3217                         dispOrder[i] = j;
3218                         break;
3219                     }
3220                 }
3221             }
3222
3223             // 列幅、列並びを他のタブに設定
3224             foreach (TabPage tb in this.ListTab.TabPages)
3225             {
3226                 if (tb.Text == this.CurrentTabName)
3227                     continue;
3228
3229                 if (tb.Tag != null && tb.Controls.Count > 0)
3230                 {
3231                     var lst = (DetailsListView)tb.Tag;
3232                     for (var i = 0; i < lst.Columns.Count; i++)
3233                     {
3234                         lst.Columns[dispOrder[i]].DisplayIndex = i;
3235                         lst.Columns[i].Width = currentListView.Columns[i].Width;
3236                     }
3237                 }
3238             }
3239
3240             this.isColumnChanged = false;
3241         }
3242
3243         private void StatusText_KeyPress(object sender, KeyPressEventArgs e)
3244         {
3245             if (e.KeyChar == '@')
3246             {
3247                 if (!this.settings.Common.UseAtIdSupplement) return;
3248                 // @マーク
3249                 var cnt = this.AtIdSupl.ItemCount;
3250                 this.ShowSuplDialog(this.StatusText, this.AtIdSupl);
3251                 if (cnt != this.AtIdSupl.ItemCount)
3252                     this.MarkSettingAtIdModified();
3253                 e.Handled = true;
3254             }
3255             else if (e.KeyChar == '#')
3256             {
3257                 if (!this.settings.Common.UseHashSupplement) return;
3258                 this.ShowSuplDialog(this.StatusText, this.HashSupl);
3259                 e.Handled = true;
3260             }
3261         }
3262
3263         public void ShowSuplDialog(TextBox owner, AtIdSupplement dialog)
3264             => this.ShowSuplDialog(owner, dialog, 0, "");
3265
3266         public void ShowSuplDialog(TextBox owner, AtIdSupplement dialog, int offset)
3267             => this.ShowSuplDialog(owner, dialog, offset, "");
3268
3269         public void ShowSuplDialog(TextBox owner, AtIdSupplement dialog, int offset, string startswith)
3270         {
3271             dialog.StartsWith = startswith;
3272             if (dialog.Visible)
3273             {
3274                 dialog.Focus();
3275             }
3276             else
3277             {
3278                 dialog.ShowDialog();
3279             }
3280             this.TopMost = this.settings.Common.AlwaysTop;
3281             var selStart = owner.SelectionStart;
3282             var fHalf = "";
3283             var eHalf = "";
3284             if (dialog.DialogResult == DialogResult.OK)
3285             {
3286                 if (!MyCommon.IsNullOrEmpty(dialog.InputText))
3287                 {
3288                     if (selStart > 0)
3289                     {
3290                         fHalf = owner.Text.Substring(0, selStart - offset);
3291                     }
3292                     if (selStart < owner.Text.Length)
3293                     {
3294                         eHalf = owner.Text.Substring(selStart);
3295                     }
3296                     owner.Text = fHalf + dialog.InputText + eHalf;
3297                     owner.SelectionStart = selStart + dialog.InputText.Length;
3298                 }
3299             }
3300             else
3301             {
3302                 if (selStart > 0)
3303                 {
3304                     fHalf = owner.Text.Substring(0, selStart);
3305                 }
3306                 if (selStart < owner.Text.Length)
3307                 {
3308                     eHalf = owner.Text.Substring(selStart);
3309                 }
3310                 owner.Text = fHalf + eHalf;
3311                 if (selStart > 0)
3312                 {
3313                     owner.SelectionStart = selStart;
3314                 }
3315             }
3316             owner.Focus();
3317         }
3318
3319         private void StatusText_KeyUp(object sender, KeyEventArgs e)
3320         {
3321             // スペースキーで未読ジャンプ
3322             if (!e.Alt && !e.Control && !e.Shift)
3323             {
3324                 if (e.KeyCode == Keys.Space || e.KeyCode == Keys.ProcessKey)
3325                 {
3326                     var isSpace = false;
3327                     foreach (var c in this.StatusText.Text)
3328                     {
3329                         if (c == ' ' || c == ' ')
3330                         {
3331                             isSpace = true;
3332                         }
3333                         else
3334                         {
3335                             isSpace = false;
3336                             break;
3337                         }
3338                     }
3339                     if (isSpace)
3340                     {
3341                         e.Handled = true;
3342                         this.StatusText.Text = "";
3343                         this.JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty);
3344                     }
3345                 }
3346             }
3347             this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
3348         }
3349
3350         private void StatusText_TextChanged(object sender, EventArgs e)
3351         {
3352             // 文字数カウント
3353             var pLen = this.GetRestStatusCount(this.FormatStatusTextExtended(this.StatusText.Text));
3354             this.lblLen.Text = pLen.ToString();
3355             if (pLen < 0)
3356             {
3357                 this.StatusText.ForeColor = Color.Red;
3358             }
3359             else
3360             {
3361                 this.StatusText.ForeColor = this.themeManager.ColorInputFont;
3362             }
3363
3364             this.StatusText.AccessibleDescription = string.Format(Properties.Resources.StatusText_AccessibleDescription, pLen);
3365
3366             if (MyCommon.IsNullOrEmpty(this.StatusText.Text))
3367             {
3368                 this.inReplyTo = null;
3369             }
3370         }
3371
3372         /// <summary>
3373         /// メンション以外の文字列が含まれていないテキストであるか判定します
3374         /// </summary>
3375         internal static bool TextContainsOnlyMentions(string text)
3376         {
3377             var mentions = TweetExtractor.ExtractMentionEntities(text).OrderBy(x => x.Indices[0]);
3378             var startIndex = 0;
3379
3380             foreach (var mention in mentions)
3381             {
3382                 var textPart = text.Substring(startIndex, mention.Indices[0] - startIndex);
3383
3384                 if (!string.IsNullOrWhiteSpace(textPart))
3385                     return false;
3386
3387                 startIndex = mention.Indices[1];
3388             }
3389
3390             var textPartLast = text.Substring(startIndex);
3391
3392             if (!string.IsNullOrWhiteSpace(textPartLast))
3393                 return false;
3394
3395             return true;
3396         }
3397
3398         /// <summary>
3399         /// 投稿時に auto_populate_reply_metadata オプションによって自動で追加されるメンションを除去します
3400         /// </summary>
3401         private string RemoveAutoPopuratedMentions(string statusText, out long[] autoPopulatedUserIds)
3402         {
3403             var autoPopulatedUserIdList = new List<long>();
3404
3405             var replyToPost = this.inReplyTo != null ? this.statuses[this.inReplyTo.Value.StatusId] : null;
3406             if (replyToPost != null)
3407             {
3408                 if (statusText.StartsWith($"@{replyToPost.ScreenName} ", StringComparison.Ordinal))
3409                 {
3410                     statusText = statusText.Substring(replyToPost.ScreenName.Length + 2);
3411                     autoPopulatedUserIdList.Add(replyToPost.UserId);
3412
3413                     foreach (var (userId, screenName) in replyToPost.ReplyToList)
3414                     {
3415                         if (statusText.StartsWith($"@{screenName} ", StringComparison.Ordinal))
3416                         {
3417                             statusText = statusText.Substring(screenName.Length + 2);
3418                             autoPopulatedUserIdList.Add(userId);
3419                         }
3420                     }
3421                 }
3422             }
3423
3424             autoPopulatedUserIds = autoPopulatedUserIdList.ToArray();
3425
3426             return statusText;
3427         }
3428
3429         /// <summary>
3430         /// attachment_url に指定可能な URL が含まれていれば除去
3431         /// </summary>
3432         private string RemoveAttachmentUrl(string statusText, out string? attachmentUrl)
3433         {
3434             attachmentUrl = null;
3435
3436             // attachment_url は media_id と同時に使用できない
3437             if (this.ImageSelector.Visible && this.ImageSelector.Model.SelectedMediaService is TwitterPhoto)
3438                 return statusText;
3439
3440             var match = Twitter.AttachmentUrlRegex.Match(statusText);
3441             if (!match.Success)
3442                 return statusText;
3443
3444             attachmentUrl = match.Value;
3445
3446             // マッチした URL を空白に置換
3447             statusText = statusText.Substring(0, match.Index);
3448
3449             // テキストと URL の間にスペースが含まれていれば除去
3450             return statusText.TrimEnd(' ');
3451         }
3452
3453         private string FormatStatusTextExtended(string statusText)
3454             => this.FormatStatusTextExtended(statusText, out _, out _);
3455
3456         /// <summary>
3457         /// <see cref="FormatStatusText"/> に加えて、拡張モードで140字にカウントされない文字列の除去を行います
3458         /// </summary>
3459         private string FormatStatusTextExtended(string statusText, out long[] autoPopulatedUserIds, out string? attachmentUrl)
3460         {
3461             statusText = this.RemoveAutoPopuratedMentions(statusText, out autoPopulatedUserIds);
3462
3463             statusText = this.RemoveAttachmentUrl(statusText, out attachmentUrl);
3464
3465             return this.FormatStatusText(statusText);
3466         }
3467
3468         internal string FormatStatusText(string statusText)
3469             => this.FormatStatusText(statusText, Control.ModifierKeys);
3470
3471         /// <summary>
3472         /// ツイート投稿前のフッター付与などの前処理を行います
3473         /// </summary>
3474         internal string FormatStatusText(string statusText, Keys modifierKeys)
3475         {
3476             statusText = statusText.Replace("\r\n", "\n");
3477
3478             if (this.SeparateUrlAndFullwidthCharacter)
3479             {
3480                 // URLと全角文字の切り離し
3481                 statusText = Regex.Replace(statusText, @"https?:\/\/[-_.!~*'()a-zA-Z0-9;\/?:\@&=+\$,%#^]+", "$& ");
3482             }
3483
3484             if (this.settings.Common.WideSpaceConvert)
3485             {
3486                 // 文中の全角スペースを半角スペース1個にする
3487                 statusText = statusText.Replace(" ", " ");
3488             }
3489
3490             // DM の場合はこれ以降の処理を行わない
3491             if (statusText.StartsWith("D ", StringComparison.OrdinalIgnoreCase))
3492                 return statusText;
3493
3494             bool disableFooter;
3495             if (this.settings.Common.PostShiftEnter)
3496             {
3497                 disableFooter = MyCommon.IsKeyDown(modifierKeys, Keys.Control);
3498             }
3499             else
3500             {
3501                 if (this.settings.Local.StatusMultiline && !this.settings.Common.PostCtrlEnter)
3502                     disableFooter = MyCommon.IsKeyDown(modifierKeys, Keys.Control);
3503                 else
3504                     disableFooter = MyCommon.IsKeyDown(modifierKeys, Keys.Shift);
3505             }
3506
3507             if (statusText.Contains("RT @"))
3508                 disableFooter = true;
3509
3510             // 自分宛のリプライの場合は先頭の「@screen_name 」の部分を除去する (in_reply_to_status_id は維持される)
3511             if (this.inReplyTo != null && this.inReplyTo.Value.ScreenName == this.tw.Username)
3512             {
3513                 var mentionSelf = $"@{this.tw.Username} ";
3514                 if (statusText.StartsWith(mentionSelf, StringComparison.OrdinalIgnoreCase))
3515                 {
3516                     if (statusText.Length > mentionSelf.Length || this.GetSelectedImageService() != null)
3517                         statusText = statusText.Substring(mentionSelf.Length);
3518                 }
3519             }
3520
3521             var header = "";
3522             var footer = "";
3523
3524             var hashtag = this.HashMgr.UseHash;
3525             if (!MyCommon.IsNullOrEmpty(hashtag) && !(this.HashMgr.IsNotAddToAtReply && this.inReplyTo != null))
3526             {
3527                 if (this.HashMgr.IsHead)
3528                     header = this.HashMgr.UseHash + " ";
3529                 else
3530                     footer = " " + this.HashMgr.UseHash;
3531             }
3532
3533             if (!disableFooter)
3534             {
3535                 if (this.settings.Local.UseRecommendStatus)
3536                 {
3537                     // 推奨ステータスを使用する
3538                     footer += this.recommendedStatusFooter;
3539                 }
3540                 else if (!MyCommon.IsNullOrEmpty(this.settings.Local.StatusText))
3541                 {
3542                     // テキストボックスに入力されている文字列を使用する
3543                     footer += " " + this.settings.Local.StatusText.Trim();
3544                 }
3545             }
3546
3547             statusText = header + statusText + footer;
3548
3549             if (this.preventSmsCommand)
3550             {
3551                 // ツイートが意図せず SMS コマンドとして解釈されることを回避 (D, DM, M のみ)
3552                 // 参照: https://support.twitter.com/articles/14020
3553
3554                 if (Regex.IsMatch(statusText, @"^[+\-\[\]\s\\.,*/(){}^~|='&%$#""<>?]*(d|dm|m)([+\-\[\]\s\\.,*/(){}^~|='&%$#""<>?]+|$)", RegexOptions.IgnoreCase)
3555                     && !Twitter.DMSendTextRegex.IsMatch(statusText))
3556                 {
3557                     // U+200B (ZERO WIDTH SPACE) を先頭に加えて回避
3558                     statusText = '\u200b' + statusText;
3559                 }
3560             }
3561
3562             return statusText;
3563         }
3564
3565         /// <summary>
3566         /// 投稿欄に表示する入力可能な文字数を計算します
3567         /// </summary>
3568         private int GetRestStatusCount(string statusText)
3569         {
3570             var remainCount = this.tw.GetTextLengthRemain(statusText);
3571
3572             var uploadService = this.GetSelectedImageService();
3573             if (uploadService != null)
3574             {
3575                 // TODO: ImageSelector で選択中の画像の枚数が mediaCount 引数に渡るようにする
3576                 remainCount -= uploadService.GetReservedTextLength(1);
3577             }
3578
3579             return remainCount;
3580         }
3581
3582         private IMediaUploadService? GetSelectedImageService()
3583             => this.ImageSelector.Visible ? this.ImageSelector.Model.SelectedMediaService : null;
3584
3585         /// <summary>
3586         /// 全てのタブの振り分けルールを反映し直します
3587         /// </summary>
3588         private void ApplyPostFilters()
3589         {
3590             using (ControlTransaction.Cursor(this, Cursors.WaitCursor))
3591             {
3592                 this.statuses.FilterAll();
3593
3594                 var listView = this.CurrentListView;
3595                 using (ControlTransaction.Update(listView))
3596                 {
3597                     this.listCache?.PurgeCache();
3598                     this.listCache?.UpdateListSize();
3599                 }
3600
3601                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
3602                 {
3603                     var tabPage = this.ListTab.TabPages[index];
3604
3605                     if (this.settings.Common.TabIconDisp)
3606                     {
3607                         if (tab.UnreadCount > 0)
3608                             tabPage.ImageIndex = 0;
3609                         else
3610                             tabPage.ImageIndex = -1;
3611                     }
3612                 }
3613
3614                 if (!this.settings.Common.TabIconDisp)
3615                     this.ListTab.Refresh();
3616
3617                 this.SetMainWindowTitle();
3618                 this.SetStatusLabelUrl();
3619             }
3620         }
3621
3622         private void MyList_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
3623             => e.DrawDefault = true;
3624
3625         private void MyList_HScrolled(object sender, EventArgs e)
3626             => ((DetailsListView)sender).Refresh();
3627
3628         protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
3629         {
3630             base.ScaleControl(factor, specified);
3631
3632             ScaleChildControl(this.TabImage, factor);
3633
3634             var tabpages = this.ListTab.TabPages.Cast<TabPage>();
3635             var listviews = tabpages.Select(x => x.Tag).Cast<ListView>();
3636
3637             foreach (var listview in listviews)
3638             {
3639                 ScaleChildControl(listview, factor);
3640             }
3641         }
3642
3643         internal void DoTabSearch(string searchWord, bool caseSensitive, bool useRegex, SEARCHTYPE searchType)
3644         {
3645             var tab = this.CurrentTab;
3646
3647             if (tab.AllCount == 0)
3648             {
3649                 MessageBox.Show(Properties.Resources.DoTabSearchText2, Properties.Resources.DoTabSearchText3, MessageBoxButtons.OK, MessageBoxIcon.Information);
3650                 return;
3651             }
3652
3653             var selectedIndex = tab.SelectedIndex;
3654
3655             int startIndex;
3656             switch (searchType)
3657             {
3658                 case SEARCHTYPE.NextSearch: // 次を検索
3659                     if (selectedIndex != -1)
3660                         startIndex = Math.Min(selectedIndex + 1, tab.AllCount - 1);
3661                     else
3662                         startIndex = 0;
3663                     break;
3664                 case SEARCHTYPE.PrevSearch: // 前を検索
3665                     if (selectedIndex != -1)
3666                         startIndex = Math.Max(selectedIndex - 1, 0);
3667                     else
3668                         startIndex = tab.AllCount - 1;
3669                     break;
3670                 case SEARCHTYPE.DialogSearch: // ダイアログからの検索
3671                 default:
3672                     if (selectedIndex != -1)
3673                         startIndex = selectedIndex;
3674                     else
3675                         startIndex = 0;
3676                     break;
3677             }
3678
3679             Func<string, bool> stringComparer;
3680             try
3681             {
3682                 stringComparer = this.CreateSearchComparer(searchWord, useRegex, caseSensitive);
3683             }
3684             catch (ArgumentException)
3685             {
3686                 MessageBox.Show(Properties.Resources.DoTabSearchText1, Properties.Resources.DoTabSearchText3, MessageBoxButtons.OK, MessageBoxIcon.Error);
3687                 return;
3688             }
3689
3690             var reverse = searchType == SEARCHTYPE.PrevSearch;
3691             var foundIndex = tab.SearchPostsAll(stringComparer, startIndex, reverse)
3692                 .DefaultIfEmpty(-1).First();
3693
3694             if (foundIndex == -1)
3695             {
3696                 MessageBox.Show(Properties.Resources.DoTabSearchText2, Properties.Resources.DoTabSearchText3, MessageBoxButtons.OK, MessageBoxIcon.Information);
3697                 return;
3698             }
3699
3700             var listView = this.CurrentListView;
3701             this.SelectListItem(listView, foundIndex);
3702             listView.EnsureVisible(foundIndex);
3703         }
3704
3705         private void MenuItemSubSearch_Click(object sender, EventArgs e)
3706             => this.ShowSearchDialog(); // 検索メニュー
3707
3708         private void MenuItemSearchNext_Click(object sender, EventArgs e)
3709         {
3710             var previousSearch = this.SearchDialog.ResultOptions;
3711             if (previousSearch == null || previousSearch.Type != SearchWordDialog.SearchType.Timeline)
3712             {
3713                 this.SearchDialog.Reset();
3714                 this.ShowSearchDialog();
3715                 return;
3716             }
3717
3718             // 次を検索
3719             this.DoTabSearch(
3720                 previousSearch.Query,
3721                 previousSearch.CaseSensitive,
3722                 previousSearch.UseRegex,
3723                 SEARCHTYPE.NextSearch);
3724         }
3725
3726         private void MenuItemSearchPrev_Click(object sender, EventArgs e)
3727         {
3728             var previousSearch = this.SearchDialog.ResultOptions;
3729             if (previousSearch == null || previousSearch.Type != SearchWordDialog.SearchType.Timeline)
3730             {
3731                 this.SearchDialog.Reset();
3732                 this.ShowSearchDialog();
3733                 return;
3734             }
3735
3736             // 前を検索
3737             this.DoTabSearch(
3738                 previousSearch.Query,
3739                 previousSearch.CaseSensitive,
3740                 previousSearch.UseRegex,
3741                 SEARCHTYPE.PrevSearch);
3742         }
3743
3744         /// <summary>
3745         /// 検索ダイアログを表示し、検索を実行します
3746         /// </summary>
3747         private void ShowSearchDialog()
3748         {
3749             if (this.SearchDialog.ShowDialog(this) != DialogResult.OK)
3750             {
3751                 this.TopMost = this.settings.Common.AlwaysTop;
3752                 return;
3753             }
3754             this.TopMost = this.settings.Common.AlwaysTop;
3755
3756             var searchOptions = this.SearchDialog.ResultOptions!;
3757             if (searchOptions.Type == SearchWordDialog.SearchType.Timeline)
3758             {
3759                 if (searchOptions.NewTab)
3760                 {
3761                     var tabName = Properties.Resources.SearchResults_TabName;
3762
3763                     try
3764                     {
3765                         tabName = this.statuses.MakeTabName(tabName);
3766                     }
3767                     catch (TabException ex)
3768                     {
3769                         MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
3770                     }
3771
3772                     var resultTab = new LocalSearchTabModel(tabName);
3773                     this.AddNewTab(resultTab, startup: false);
3774                     this.statuses.AddTab(resultTab);
3775
3776                     var targetTab = this.CurrentTab;
3777
3778                     Func<string, bool> stringComparer;
3779                     try
3780                     {
3781                         stringComparer = this.CreateSearchComparer(searchOptions.Query, searchOptions.UseRegex, searchOptions.CaseSensitive);
3782                     }
3783                     catch (ArgumentException)
3784                     {
3785                         MessageBox.Show(Properties.Resources.DoTabSearchText1, Properties.Resources.DoTabSearchText3, MessageBoxButtons.OK, MessageBoxIcon.Error);
3786                         return;
3787                     }
3788
3789                     var foundIndices = targetTab.SearchPostsAll(stringComparer).ToArray();
3790                     if (foundIndices.Length == 0)
3791                     {
3792                         MessageBox.Show(Properties.Resources.DoTabSearchText2, Properties.Resources.DoTabSearchText3, MessageBoxButtons.OK, MessageBoxIcon.Information);
3793                         return;
3794                     }
3795
3796                     var foundPosts = foundIndices.Select(x => targetTab[x]);
3797                     foreach (var post in foundPosts)
3798                     {
3799                         resultTab.AddPostQueue(post);
3800                     }
3801
3802                     this.statuses.DistributePosts();
3803                     this.RefreshTimeline();
3804
3805                     var tabIndex = this.statuses.Tabs.IndexOf(tabName);
3806                     this.ListTab.SelectedIndex = tabIndex;
3807                 }
3808                 else
3809                 {
3810                     this.DoTabSearch(
3811                         searchOptions.Query,
3812                         searchOptions.CaseSensitive,
3813                         searchOptions.UseRegex,
3814                         SEARCHTYPE.DialogSearch);
3815                 }
3816             }
3817             else if (searchOptions.Type == SearchWordDialog.SearchType.Public)
3818             {
3819                 this.AddNewTabForSearch(searchOptions.Query);
3820             }
3821         }
3822
3823         /// <summary>発言検索に使用するメソッドを生成します</summary>
3824         /// <exception cref="ArgumentException">
3825         /// <paramref name="useRegex"/> が true かつ、<paramref name="query"/> が不正な正規表現な場合
3826         /// </exception>
3827         private Func<string, bool> CreateSearchComparer(string query, bool useRegex, bool caseSensitive)
3828         {
3829             if (useRegex)
3830             {
3831                 var regexOption = caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase;
3832                 var regex = new Regex(query, regexOption);
3833
3834                 return x => regex.IsMatch(x);
3835             }
3836             else
3837             {
3838                 var comparisonType = caseSensitive ? StringComparison.CurrentCulture : StringComparison.CurrentCultureIgnoreCase;
3839
3840                 return x => x.IndexOf(query, comparisonType) != -1;
3841             }
3842         }
3843
3844         private void AboutMenuItem_Click(object sender, EventArgs e)
3845         {
3846             using (var about = new TweenAboutBox())
3847             {
3848                 about.ShowDialog(this);
3849             }
3850             this.TopMost = this.settings.Common.AlwaysTop;
3851         }
3852
3853         private void JumpUnreadMenuItem_Click(object sender, EventArgs e)
3854         {
3855             var bgnIdx = this.statuses.SelectedTabIndex;
3856
3857             if (this.ImageSelector.Enabled)
3858                 return;
3859
3860             TabModel? foundTab = null;
3861             var foundIndex = 0;
3862
3863             // 現在タブから最終タブまで探索
3864             foreach (var (tab, index) in this.statuses.Tabs.WithIndex().Skip(bgnIdx))
3865             {
3866                 var unreadIndex = tab.NextUnreadIndex;
3867                 if (unreadIndex != -1)
3868                 {
3869                     this.ListTab.SelectedIndex = index;
3870                     foundTab = tab;
3871                     foundIndex = unreadIndex;
3872                     break;
3873                 }
3874             }
3875
3876             // 未読みつからず&現在タブが先頭ではなかったら、先頭タブから現在タブの手前まで探索
3877             if (foundTab == null && bgnIdx > 0)
3878             {
3879                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex().Take(bgnIdx))
3880                 {
3881                     var unreadIndex = tab.NextUnreadIndex;
3882                     if (unreadIndex != -1)
3883                     {
3884                         this.ListTab.SelectedIndex = index;
3885                         foundTab = tab;
3886                         foundIndex = unreadIndex;
3887                         break;
3888                     }
3889                 }
3890             }
3891
3892             DetailsListView lst;
3893
3894             if (foundTab == null)
3895             {
3896                 // 全部調べたが未読見つからず→先頭タブの最新発言へ
3897                 this.ListTab.SelectedIndex = 0;
3898                 var tabPage = this.ListTab.TabPages[0];
3899                 var tab = this.statuses.Tabs[0];
3900
3901                 if (tab.AllCount == 0)
3902                     return;
3903
3904                 if (this.statuses.SortOrder == SortOrder.Ascending)
3905                     foundIndex = tab.AllCount - 1;
3906                 else
3907                     foundIndex = 0;
3908
3909                 lst = (DetailsListView)tabPage.Tag;
3910             }
3911             else
3912             {
3913                 var foundTabIndex = this.statuses.Tabs.IndexOf(foundTab);
3914                 lst = (DetailsListView)this.ListTab.TabPages[foundTabIndex].Tag;
3915             }
3916
3917             this.SelectListItem(lst, foundIndex);
3918
3919             if (this.statuses.SortMode == ComparerMode.Id)
3920             {
3921                 var rowHeight = lst.SmallImageList.ImageSize.Height;
3922                 if (this.statuses.SortOrder == SortOrder.Ascending && lst.Items[foundIndex].Position.Y > lst.ClientSize.Height - rowHeight - 10 ||
3923                     this.statuses.SortOrder == SortOrder.Descending && lst.Items[foundIndex].Position.Y < rowHeight + 10)
3924                 {
3925                     this.MoveTop();
3926                 }
3927                 else
3928                 {
3929                     lst.EnsureVisible(foundIndex);
3930                 }
3931             }
3932             else
3933             {
3934                 lst.EnsureVisible(foundIndex);
3935             }
3936
3937             lst.Focus();
3938         }
3939
3940         private async void StatusOpenMenuItem_Click(object sender, EventArgs e)
3941         {
3942             var tab = this.CurrentTab;
3943             var post = this.CurrentPost;
3944             if (post != null && tab.TabType != MyCommon.TabUsageType.DirectMessage)
3945                 await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(post));
3946         }
3947
3948         private async void VerUpMenuItem_Click(object sender, EventArgs e)
3949             => await this.CheckNewVersion(false);
3950
3951         private void RunTweenUp()
3952         {
3953             var pinfo = new ProcessStartInfo
3954             {
3955                 UseShellExecute = true,
3956                 WorkingDirectory = this.settings.SettingsPath,
3957                 FileName = Path.Combine(this.settings.SettingsPath, "TweenUp3.exe"),
3958                 Arguments = "\"" + Application.StartupPath + "\"",
3959             };
3960
3961             try
3962             {
3963                 Process.Start(pinfo);
3964             }
3965             catch (Exception)
3966             {
3967                 MessageBox.Show("Failed to execute TweenUp3.exe.");
3968             }
3969         }
3970
3971         public readonly record struct VersionInfo(
3972             Version Version,
3973             Uri DownloadUri,
3974             string ReleaseNote
3975         );
3976
3977         /// <summary>
3978         /// OpenTween の最新バージョンの情報を取得します
3979         /// </summary>
3980         public async Task<VersionInfo> GetVersionInfoAsync()
3981         {
3982             var versionInfoUrl = new Uri(ApplicationSettings.VersionInfoUrl + "?" +
3983                 DateTimeUtc.Now.ToString("yyMMddHHmmss") + Environment.TickCount);
3984
3985             var responseText = await Networking.Http.GetStringAsync(versionInfoUrl)
3986                 .ConfigureAwait(false);
3987
3988             // 改行2つで前後パートを分割(前半がバージョン番号など、後半が詳細テキスト)
3989             var msgPart = responseText.Split(new[] { "\n\n", "\r\n\r\n" }, 2, StringSplitOptions.None);
3990
3991             var msgHeader = msgPart[0].Split(new[] { "\n", "\r\n" }, StringSplitOptions.None);
3992             var msgBody = msgPart.Length == 2 ? msgPart[1] : "";
3993
3994             msgBody = Regex.Replace(msgBody, "(?<!\r)\n", "\r\n"); // LF -> CRLF
3995
3996             return new VersionInfo
3997             {
3998                 Version = Version.Parse(msgHeader[0]),
3999                 DownloadUri = new Uri(msgHeader[1]),
4000                 ReleaseNote = msgBody,
4001             };
4002         }
4003
4004         private async Task CheckNewVersion(bool startup = false)
4005         {
4006             if (ApplicationSettings.VersionInfoUrl == null)
4007                 return; // 更新チェック無効化
4008
4009             try
4010             {
4011                 var versionInfo = await this.GetVersionInfoAsync();
4012
4013                 if (versionInfo.Version <= Version.Parse(MyCommon.FileVersion))
4014                 {
4015                     // 更新不要
4016                     if (!startup)
4017                     {
4018                         var msgtext = string.Format(
4019                             Properties.Resources.CheckNewVersionText7,
4020                             MyCommon.GetReadableVersion(),
4021                             MyCommon.GetReadableVersion(versionInfo.Version));
4022                         msgtext = MyCommon.ReplaceAppName(msgtext);
4023
4024                         MessageBox.Show(
4025                             msgtext,
4026                             MyCommon.ReplaceAppName(Properties.Resources.CheckNewVersionText2),
4027                             MessageBoxButtons.OK,
4028                             MessageBoxIcon.Information);
4029                     }
4030                     return;
4031                 }
4032
4033                 if (startup && versionInfo.Version <= this.settings.Common.SkipUpdateVersion)
4034                     return;
4035
4036                 using var dialog = new UpdateDialog();
4037
4038                 dialog.SummaryText = string.Format(Properties.Resources.CheckNewVersionText3,
4039                     MyCommon.GetReadableVersion(versionInfo.Version));
4040                 dialog.DetailsText = versionInfo.ReleaseNote;
4041
4042                 if (dialog.ShowDialog(this) == DialogResult.Yes)
4043                 {
4044                     await MyCommon.OpenInBrowserAsync(this, versionInfo.DownloadUri);
4045                 }
4046                 else if (dialog.SkipButtonPressed)
4047                 {
4048                     this.settings.Common.SkipUpdateVersion = versionInfo.Version;
4049                     this.MarkSettingCommonModified();
4050                 }
4051             }
4052             catch (Exception)
4053             {
4054                 this.StatusLabel.Text = Properties.Resources.CheckNewVersionText9;
4055                 if (!startup)
4056                 {
4057                     MessageBox.Show(
4058                         Properties.Resources.CheckNewVersionText10,
4059                         MyCommon.ReplaceAppName(Properties.Resources.CheckNewVersionText2),
4060                         MessageBoxButtons.OK,
4061                         MessageBoxIcon.Exclamation,
4062                         MessageBoxDefaultButton.Button2);
4063                 }
4064             }
4065         }
4066
4067         private void UpdateSelectedPost()
4068         {
4069             // 件数関連の場合、タイトル即時書き換え
4070             if (this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.None &&
4071                this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.Post &&
4072                this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.Ver &&
4073                this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.OwnStatus)
4074             {
4075                 this.SetMainWindowTitle();
4076             }
4077             if (!this.StatusLabelUrl.Text.StartsWith("http", StringComparison.OrdinalIgnoreCase))
4078                 this.SetStatusLabelUrl();
4079
4080             if (this.settings.Common.TabIconDisp)
4081             {
4082                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
4083                 {
4084                     if (tab.UnreadCount == 0)
4085                     {
4086                         var tabPage = this.ListTab.TabPages[index];
4087                         if (tabPage.ImageIndex == 0)
4088                             tabPage.ImageIndex = -1;
4089                     }
4090                 }
4091             }
4092             else
4093             {
4094                 this.ListTab.Refresh();
4095             }
4096
4097             this.DispSelectedPost();
4098         }
4099
4100         private void DispSelectedPost()
4101             => this.DispSelectedPost(false);
4102
4103         private PostClass displayPost = new();
4104
4105         /// <summary>
4106         /// サムネイル表示に使用する CancellationToken の生成元
4107         /// </summary>
4108         private CancellationTokenSource? thumbnailTokenSource = null;
4109
4110         private void DispSelectedPost(bool forceupdate)
4111         {
4112             var currentPost = this.CurrentPost;
4113             if (currentPost == null)
4114                 return;
4115
4116             var oldDisplayPost = this.displayPost;
4117             this.displayPost = currentPost;
4118
4119             if (!forceupdate && currentPost.Equals(oldDisplayPost))
4120                 return;
4121
4122             var loadTasks = new TaskCollection();
4123             loadTasks.Add(() => this.tweetDetailsView.ShowPostDetails(currentPost));
4124
4125             if (this.settings.Common.PreviewEnable)
4126             {
4127                 var oldTokenSource = Interlocked.Exchange(ref this.thumbnailTokenSource, new CancellationTokenSource());
4128                 oldTokenSource?.Cancel();
4129
4130                 var token = this.thumbnailTokenSource!.Token;
4131                 loadTasks.Add(() => this.PrepareThumbnailControl(currentPost, token));
4132             }
4133             else
4134             {
4135                 this.SplitContainer3.Panel2Collapsed = true;
4136             }
4137
4138             // サムネイルの読み込みを待たずに次に選択されたツイートを表示するため await しない
4139             _ = loadTasks
4140                 .IgnoreException(x => x is OperationCanceledException)
4141                 .RunAll();
4142         }
4143
4144         private async Task PrepareThumbnailControl(PostClass post, CancellationToken token)
4145         {
4146             var prepareTask = this.tweetThumbnail1.Model.PrepareThumbnails(post, token);
4147
4148             var timeout = Task.Delay(100);
4149             if ((await Task.WhenAny(prepareTask, timeout)) == timeout)
4150             {
4151                 token.ThrowIfCancellationRequested();
4152
4153                 // サムネイル情報の読み込みに時間が掛かっている場合は一旦サムネイル領域を非表示にする
4154                 this.SplitContainer3.Panel2Collapsed = true;
4155             }
4156
4157             await prepareTask;
4158             token.ThrowIfCancellationRequested();
4159
4160             this.SplitContainer3.Panel2Collapsed = !this.tweetThumbnail1.Model.ThumbnailAvailable;
4161         }
4162
4163         private async void MatomeMenuItem_Click(object sender, EventArgs e)
4164             => await this.OpenApplicationWebsite();
4165
4166         private async Task OpenApplicationWebsite()
4167             => await MyCommon.OpenInBrowserAsync(this, ApplicationSettings.WebsiteUrl);
4168
4169         private async void ShortcutKeyListMenuItem_Click(object sender, EventArgs e)
4170             => await MyCommon.OpenInBrowserAsync(this, ApplicationSettings.ShortcutKeyUrl);
4171
4172         private async void ListTab_KeyDown(object sender, KeyEventArgs e)
4173         {
4174             var tab = this.CurrentTab;
4175             if (tab.TabType == MyCommon.TabUsageType.PublicSearch)
4176             {
4177                 var pnl = this.CurrentTabPage.Controls["panelSearch"];
4178                 if (pnl.Controls["comboSearch"].Focused ||
4179                     pnl.Controls["comboLang"].Focused ||
4180                     pnl.Controls["buttonSearch"].Focused) return;
4181             }
4182
4183             if (e.Control || e.Shift || e.Alt)
4184                 tab.ClearAnchor();
4185
4186             if (this.CommonKeyDown(e.KeyData, FocusedControl.ListTab, out var asyncTask))
4187             {
4188                 e.Handled = true;
4189                 e.SuppressKeyPress = true;
4190             }
4191
4192             if (asyncTask != null)
4193                 await asyncTask;
4194         }
4195
4196         private ShortcutCommand[] shortcutCommands = Array.Empty<ShortcutCommand>();
4197
4198         private void InitializeShortcuts()
4199         {
4200             this.shortcutCommands = new[]
4201             {
4202                 // リストのカーソル移動関係(上下キー、PageUp/Downに該当)
4203                 ShortcutCommand.Create(Keys.J, Keys.Control | Keys.J, Keys.Shift | Keys.J, Keys.Control | Keys.Shift | Keys.J)
4204                     .FocusedOn(FocusedControl.ListTab)
4205                     .Do(() => SendKeys.Send("{DOWN}")),
4206
4207                 ShortcutCommand.Create(Keys.K, Keys.Control | Keys.K, Keys.Shift | Keys.K, Keys.Control | Keys.Shift | Keys.K)
4208                     .FocusedOn(FocusedControl.ListTab)
4209                     .Do(() => SendKeys.Send("{UP}")),
4210
4211                 ShortcutCommand.Create(Keys.F, Keys.Shift | Keys.F)
4212                     .FocusedOn(FocusedControl.ListTab)
4213                     .Do(() => SendKeys.Send("{PGDN}")),
4214
4215                 ShortcutCommand.Create(Keys.B, Keys.Shift | Keys.B)
4216                     .FocusedOn(FocusedControl.ListTab)
4217                     .Do(() => SendKeys.Send("{PGUP}")),
4218
4219                 ShortcutCommand.Create(Keys.F1)
4220                     .Do(() => this.OpenApplicationWebsite()),
4221
4222                 ShortcutCommand.Create(Keys.F3)
4223                     .Do(() => this.MenuItemSearchNext_Click(this.MenuItemSearchNext, EventArgs.Empty)),
4224
4225                 ShortcutCommand.Create(Keys.F5)
4226                     .Do(() => this.DoRefresh()),
4227
4228                 ShortcutCommand.Create(Keys.F6)
4229                     .Do(() => this.RefreshTabAsync<MentionsTabModel>()),
4230
4231                 ShortcutCommand.Create(Keys.F7)
4232                     .Do(() => this.RefreshTabAsync<DirectMessagesTabModel>()),
4233
4234                 ShortcutCommand.Create(Keys.Space, Keys.ProcessKey)
4235                     .NotFocusedOn(FocusedControl.StatusText)
4236                     .Do(() =>
4237                     {
4238                         this.CurrentTab.ClearAnchor();
4239                         this.JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty);
4240                     }),
4241
4242                 ShortcutCommand.Create(Keys.G)
4243                     .NotFocusedOn(FocusedControl.StatusText)
4244                     .Do(() =>
4245                     {
4246                         this.CurrentTab.ClearAnchor();
4247                         this.ShowRelatedStatusesMenuItem_Click(this.ShowRelatedStatusesMenuItem, EventArgs.Empty);
4248                     }),
4249
4250                 ShortcutCommand.Create(Keys.Right, Keys.N)
4251                     .FocusedOn(FocusedControl.ListTab)
4252                     .Do(() => this.GoRelPost(forward: true)),
4253
4254                 ShortcutCommand.Create(Keys.Left, Keys.P)
4255                     .FocusedOn(FocusedControl.ListTab)
4256                     .Do(() => this.GoRelPost(forward: false)),
4257
4258                 ShortcutCommand.Create(Keys.OemPeriod)
4259                     .FocusedOn(FocusedControl.ListTab)
4260                     .Do(() => this.GoAnchor()),
4261
4262                 ShortcutCommand.Create(Keys.I)
4263                     .FocusedOn(FocusedControl.ListTab)
4264                     .OnlyWhen(() => this.StatusText.Enabled)
4265                     .Do(() => this.StatusText.Focus()),
4266
4267                 ShortcutCommand.Create(Keys.Enter)
4268                     .FocusedOn(FocusedControl.ListTab)
4269                     .Do(() => this.ListItemDoubleClickAction()),
4270
4271                 ShortcutCommand.Create(Keys.R)
4272                     .FocusedOn(FocusedControl.ListTab)
4273                     .Do(() => this.DoRefresh()),
4274
4275                 ShortcutCommand.Create(Keys.L)
4276                     .FocusedOn(FocusedControl.ListTab)
4277                     .Do(() =>
4278                     {
4279                         this.CurrentTab.ClearAnchor();
4280                         this.GoPost(forward: true);
4281                     }),
4282
4283                 ShortcutCommand.Create(Keys.H)
4284                     .FocusedOn(FocusedControl.ListTab)
4285                     .Do(() =>
4286                     {
4287                         this.CurrentTab.ClearAnchor();
4288                         this.GoPost(forward: false);
4289                     }),
4290
4291                 ShortcutCommand.Create(Keys.Z, Keys.Oemcomma)
4292                     .FocusedOn(FocusedControl.ListTab)
4293                     .Do(() =>
4294                     {
4295                         this.CurrentTab.ClearAnchor();
4296                         this.MoveTop();
4297                     }),
4298
4299                 ShortcutCommand.Create(Keys.S)
4300                     .FocusedOn(FocusedControl.ListTab)
4301                     .Do(() =>
4302                     {
4303                         this.CurrentTab.ClearAnchor();
4304                         this.GoNextTab(forward: true);
4305                     }),
4306
4307                 ShortcutCommand.Create(Keys.A)
4308                     .FocusedOn(FocusedControl.ListTab)
4309                     .Do(() =>
4310                     {
4311                         this.CurrentTab.ClearAnchor();
4312                         this.GoNextTab(forward: false);
4313                     }),
4314
4315                 // ] in_reply_to参照元へ戻る
4316                 ShortcutCommand.Create(Keys.Oem4)
4317                     .FocusedOn(FocusedControl.ListTab)
4318                     .Do(() =>
4319                     {
4320                         this.CurrentTab.ClearAnchor();
4321                         return this.GoInReplyToPostTree();
4322                     }),
4323
4324                 // [ in_reply_toへジャンプ
4325                 ShortcutCommand.Create(Keys.Oem6)
4326                     .FocusedOn(FocusedControl.ListTab)
4327                     .Do(() =>
4328                     {
4329                         this.CurrentTab.ClearAnchor();
4330                         this.GoBackInReplyToPostTree();
4331                     }),
4332
4333                 ShortcutCommand.Create(Keys.Escape)
4334                     .FocusedOn(FocusedControl.ListTab)
4335                     .Do(() =>
4336                     {
4337                         this.CurrentTab.ClearAnchor();
4338                         var tab = this.CurrentTab;
4339                         var tabtype = tab.TabType;
4340                         if (tabtype == MyCommon.TabUsageType.Related || tabtype == MyCommon.TabUsageType.UserTimeline || tabtype == MyCommon.TabUsageType.PublicSearch || tabtype == MyCommon.TabUsageType.SearchResults)
4341                         {
4342                             this.RemoveSpecifiedTab(tab.TabName, false);
4343                             this.SaveConfigsTabs();
4344                         }
4345                     }),
4346
4347                 // 上下キー, PageUp/Downキー, Home/Endキー は既定の動作を残しつつアンカー初期化
4348                 ShortcutCommand.Create(Keys.Up, Keys.Down, Keys.PageUp, Keys.PageDown, Keys.Home, Keys.End)
4349                     .FocusedOn(FocusedControl.ListTab)
4350                     .Do(() => this.CurrentTab.ClearAnchor(), preventDefault: false),
4351
4352                 // PreviewKeyDownEventArgs.IsInputKey を true にしてスクロールを発生させる
4353                 ShortcutCommand.Create(Keys.Up, Keys.Down)
4354                     .FocusedOn(FocusedControl.PostBrowser)
4355                     .Do(() => { }),
4356
4357                 ShortcutCommand.Create(Keys.Control | Keys.R)
4358                     .Do(() => this.MakeReplyText()),
4359
4360                 ShortcutCommand.Create(Keys.Control | Keys.D)
4361                     .Do(() => this.DoStatusDelete()),
4362
4363                 ShortcutCommand.Create(Keys.Control | Keys.M)
4364                     .Do(() => this.MakeDirectMessageText()),
4365
4366                 ShortcutCommand.Create(Keys.Control | Keys.S)
4367                     .Do(() => this.FavoriteChange(favAdd: true)),
4368
4369                 ShortcutCommand.Create(Keys.Control | Keys.I)
4370                     .Do(() => this.DoRepliedStatusOpen()),
4371
4372                 ShortcutCommand.Create(Keys.Control | Keys.Q)
4373                     .Do(() => this.DoQuoteOfficial()),
4374
4375                 ShortcutCommand.Create(Keys.Control | Keys.B)
4376                     .Do(() => this.ReadedStripMenuItem_Click(this.ReadedStripMenuItem, EventArgs.Empty)),
4377
4378                 ShortcutCommand.Create(Keys.Control | Keys.T)
4379                     .Do(() => this.HashManageMenuItem_Click(this.HashManageMenuItem, EventArgs.Empty)),
4380
4381                 ShortcutCommand.Create(Keys.Control | Keys.L)
4382                     .Do(() => this.UrlConvertAutoToolStripMenuItem_Click(this.UrlConvertAutoToolStripMenuItem, EventArgs.Empty)),
4383
4384                 ShortcutCommand.Create(Keys.Control | Keys.Y)
4385                     .NotFocusedOn(FocusedControl.PostBrowser)
4386                     .Do(() => this.MultiLineMenuItem_Click(this.MultiLineMenuItem, EventArgs.Empty)),
4387
4388                 ShortcutCommand.Create(Keys.Control | Keys.F)
4389                     .Do(() => this.MenuItemSubSearch_Click(this.MenuItemSubSearch, EventArgs.Empty)),
4390
4391                 ShortcutCommand.Create(Keys.Control | Keys.U)
4392                     .Do(() => this.ShowUserTimeline()),
4393
4394                 ShortcutCommand.Create(Keys.Control | Keys.H)
4395                     .Do(() => this.AuthorOpenInBrowserMenuItem_Click(this.AuthorOpenInBrowserContextMenuItem, EventArgs.Empty)),
4396
4397                 ShortcutCommand.Create(Keys.Control | Keys.O)
4398                     .Do(() => this.StatusOpenMenuItem_Click(this.StatusOpenMenuItem, EventArgs.Empty)),
4399
4400                 ShortcutCommand.Create(Keys.Control | Keys.E)
4401                     .Do(() => this.OpenURLMenuItem_Click(this.OpenURLMenuItem, EventArgs.Empty)),
4402
4403                 ShortcutCommand.Create(Keys.Control | Keys.Home, Keys.Control | Keys.End)
4404                     .FocusedOn(FocusedControl.ListTab)
4405                     .Do(() => this.selectionDebouncer.Call(), preventDefault: false),
4406
4407                 ShortcutCommand.Create(Keys.Control | Keys.N)
4408                     .FocusedOn(FocusedControl.ListTab)
4409                     .Do(() => this.GoNextTab(forward: true)),
4410
4411                 ShortcutCommand.Create(Keys.Control | Keys.P)
4412                     .FocusedOn(FocusedControl.ListTab)
4413                     .Do(() => this.GoNextTab(forward: false)),
4414
4415                 ShortcutCommand.Create(Keys.Control | Keys.C, Keys.Control | Keys.Insert)
4416                     .FocusedOn(FocusedControl.ListTab)
4417                     .Do(() => this.CopyStot()),
4418
4419                 // タブダイレクト選択(Ctrl+1~8,Ctrl+9)
4420                 ShortcutCommand.Create(Keys.Control | Keys.D1)
4421                     .FocusedOn(FocusedControl.ListTab)
4422                     .OnlyWhen(() => this.statuses.Tabs.Count >= 1)
4423                     .Do(() => this.ListTab.SelectedIndex = 0),
4424
4425                 ShortcutCommand.Create(Keys.Control | Keys.D2)
4426                     .FocusedOn(FocusedControl.ListTab)
4427                     .OnlyWhen(() => this.statuses.Tabs.Count >= 2)
4428                     .Do(() => this.ListTab.SelectedIndex = 1),
4429
4430                 ShortcutCommand.Create(Keys.Control | Keys.D3)
4431                     .FocusedOn(FocusedControl.ListTab)
4432                     .OnlyWhen(() => this.statuses.Tabs.Count >= 3)
4433                     .Do(() => this.ListTab.SelectedIndex = 2),
4434
4435                 ShortcutCommand.Create(Keys.Control | Keys.D4)
4436                     .FocusedOn(FocusedControl.ListTab)
4437                     .OnlyWhen(() => this.statuses.Tabs.Count >= 4)
4438                     .Do(() => this.ListTab.SelectedIndex = 3),
4439
4440                 ShortcutCommand.Create(Keys.Control | Keys.D5)
4441                     .FocusedOn(FocusedControl.ListTab)
4442                     .OnlyWhen(() => this.statuses.Tabs.Count >= 5)
4443                     .Do(() => this.ListTab.SelectedIndex = 4),
4444
4445                 ShortcutCommand.Create(Keys.Control | Keys.D6)
4446                     .FocusedOn(FocusedControl.ListTab)
4447                     .OnlyWhen(() => this.statuses.Tabs.Count >= 6)
4448                     .Do(() => this.ListTab.SelectedIndex = 5),
4449
4450                 ShortcutCommand.Create(Keys.Control | Keys.D7)
4451                     .FocusedOn(FocusedControl.ListTab)
4452                     .OnlyWhen(() => this.statuses.Tabs.Count >= 7)
4453                     .Do(() => this.ListTab.SelectedIndex = 6),
4454
4455                 ShortcutCommand.Create(Keys.Control | Keys.D8)
4456                     .FocusedOn(FocusedControl.ListTab)
4457                     .OnlyWhen(() => this.statuses.Tabs.Count >= 8)
4458                     .Do(() => this.ListTab.SelectedIndex = 7),
4459
4460                 ShortcutCommand.Create(Keys.Control | Keys.D9)
4461                     .FocusedOn(FocusedControl.ListTab)
4462                     .Do(() => this.ListTab.SelectedIndex = this.statuses.Tabs.Count - 1),
4463
4464                 ShortcutCommand.Create(Keys.Control | Keys.A)
4465                     .FocusedOn(FocusedControl.StatusText)
4466                     .Do(() => this.StatusText.SelectAll()),
4467
4468                 ShortcutCommand.Create(Keys.Control | Keys.V)
4469                     .FocusedOn(FocusedControl.StatusText)
4470                     .Do(() => this.ProcClipboardFromStatusTextWhenCtrlPlusV()),
4471
4472                 ShortcutCommand.Create(Keys.Control | Keys.Up)
4473                     .FocusedOn(FocusedControl.StatusText)
4474                     .Do(() => this.StatusTextHistoryBack()),
4475
4476                 ShortcutCommand.Create(Keys.Control | Keys.Down)
4477                     .FocusedOn(FocusedControl.StatusText)
4478                     .Do(() => this.StatusTextHistoryForward()),
4479
4480                 ShortcutCommand.Create(Keys.Control | Keys.PageUp, Keys.Control | Keys.P)
4481                     .FocusedOn(FocusedControl.StatusText)
4482                     .Do(() =>
4483                     {
4484                         if (this.ListTab.SelectedIndex == 0)
4485                         {
4486                             this.ListTab.SelectedIndex = this.ListTab.TabCount - 1;
4487                         }
4488                         else
4489                         {
4490                             this.ListTab.SelectedIndex -= 1;
4491                         }
4492                         this.StatusText.Focus();
4493                     }),
4494
4495                 ShortcutCommand.Create(Keys.Control | Keys.PageDown, Keys.Control | Keys.N)
4496                     .FocusedOn(FocusedControl.StatusText)
4497                     .Do(() =>
4498                     {
4499                         if (this.ListTab.SelectedIndex == this.ListTab.TabCount - 1)
4500                         {
4501                             this.ListTab.SelectedIndex = 0;
4502                         }
4503                         else
4504                         {
4505                             this.ListTab.SelectedIndex += 1;
4506                         }
4507                         this.StatusText.Focus();
4508                     }),
4509
4510                 ShortcutCommand.Create(Keys.Control | Keys.Y)
4511                     .FocusedOn(FocusedControl.PostBrowser)
4512                     .Do(() =>
4513                     {
4514                         var multiline = !this.settings.Local.StatusMultiline;
4515                         this.settings.Local.StatusMultiline = multiline;
4516                         this.MultiLineMenuItem.Checked = multiline;
4517                         this.MultiLineMenuItem_Click(this.MultiLineMenuItem, EventArgs.Empty);
4518                     }),
4519
4520                 ShortcutCommand.Create(Keys.Shift | Keys.F3)
4521                     .Do(() => this.MenuItemSearchPrev_Click(this.MenuItemSearchPrev, EventArgs.Empty)),
4522
4523                 ShortcutCommand.Create(Keys.Shift | Keys.F5)
4524                     .Do(() => this.DoRefreshMore()),
4525
4526                 ShortcutCommand.Create(Keys.Shift | Keys.F6)
4527                     .Do(() => this.RefreshTabAsync<MentionsTabModel>(backward: true)),
4528
4529                 ShortcutCommand.Create(Keys.Shift | Keys.F7)
4530                     .Do(() => this.RefreshTabAsync<DirectMessagesTabModel>(backward: true)),
4531
4532                 ShortcutCommand.Create(Keys.Shift | Keys.R)
4533                     .NotFocusedOn(FocusedControl.StatusText)
4534                     .Do(() => this.DoRefreshMore()),
4535
4536                 ShortcutCommand.Create(Keys.Shift | Keys.H)
4537                     .FocusedOn(FocusedControl.ListTab)
4538                     .Do(() => this.GoTopEnd(goTop: true)),
4539
4540                 ShortcutCommand.Create(Keys.Shift | Keys.L)
4541                     .FocusedOn(FocusedControl.ListTab)
4542                     .Do(() => this.GoTopEnd(goTop: false)),
4543
4544                 ShortcutCommand.Create(Keys.Shift | Keys.M)
4545                     .FocusedOn(FocusedControl.ListTab)
4546                     .Do(() => this.GoMiddle()),
4547
4548                 ShortcutCommand.Create(Keys.Shift | Keys.G)
4549                     .FocusedOn(FocusedControl.ListTab)
4550                     .Do(() => this.GoLast()),
4551
4552                 ShortcutCommand.Create(Keys.Shift | Keys.Z)
4553                     .FocusedOn(FocusedControl.ListTab)
4554                     .Do(() => this.MoveMiddle()),
4555
4556                 ShortcutCommand.Create(Keys.Shift | Keys.Oem4)
4557                     .FocusedOn(FocusedControl.ListTab)
4558                     .Do(() => this.GoBackInReplyToPostTree(parallel: true, isForward: false)),
4559
4560                 ShortcutCommand.Create(Keys.Shift | Keys.Oem6)
4561                     .FocusedOn(FocusedControl.ListTab)
4562                     .Do(() => this.GoBackInReplyToPostTree(parallel: true, isForward: true)),
4563
4564                 // お気に入り前後ジャンプ(SHIFT+N←/P→)
4565                 ShortcutCommand.Create(Keys.Shift | Keys.Right, Keys.Shift | Keys.N)
4566                     .FocusedOn(FocusedControl.ListTab)
4567                     .Do(() => this.GoFav(forward: true)),
4568
4569                 // お気に入り前後ジャンプ(SHIFT+N←/P→)
4570                 ShortcutCommand.Create(Keys.Shift | Keys.Left, Keys.Shift | Keys.P)
4571                     .FocusedOn(FocusedControl.ListTab)
4572                     .Do(() => this.GoFav(forward: false)),
4573
4574                 ShortcutCommand.Create(Keys.Shift | Keys.Space)
4575                     .FocusedOn(FocusedControl.ListTab)
4576                     .Do(() => this.GoBackSelectPostChain()),
4577
4578                 ShortcutCommand.Create(Keys.Alt | Keys.R)
4579                     .Do(() => this.DoReTweetOfficial(isConfirm: true)),
4580
4581                 ShortcutCommand.Create(Keys.Alt | Keys.P)
4582                     .OnlyWhen(() => this.CurrentPost != null)
4583                     .Do(() => this.DoShowUserStatus(this.CurrentPost!.ScreenName, showInputDialog: false)),
4584
4585                 ShortcutCommand.Create(Keys.Alt | Keys.Up)
4586                     .Do(() => this.tweetDetailsView.ScrollDownPostBrowser(forward: false)),
4587
4588                 ShortcutCommand.Create(Keys.Alt | Keys.Down)
4589                     .Do(() => this.tweetDetailsView.ScrollDownPostBrowser(forward: true)),
4590
4591                 ShortcutCommand.Create(Keys.Alt | Keys.PageUp)
4592                     .Do(() => this.tweetDetailsView.PageDownPostBrowser(forward: false)),
4593
4594                 ShortcutCommand.Create(Keys.Alt | Keys.PageDown)
4595                     .Do(() => this.tweetDetailsView.PageDownPostBrowser(forward: true)),
4596
4597                 // 別タブの同じ書き込みへ(ALT+←/→)
4598                 ShortcutCommand.Create(Keys.Alt | Keys.Right)
4599                     .FocusedOn(FocusedControl.ListTab)
4600                     .Do(() => this.GoSamePostToAnotherTab(left: false)),
4601
4602                 ShortcutCommand.Create(Keys.Alt | Keys.Left)
4603                     .FocusedOn(FocusedControl.ListTab)
4604                     .Do(() => this.GoSamePostToAnotherTab(left: true)),
4605
4606                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.R)
4607                     .Do(() => this.MakeReplyText(atAll: true)),
4608
4609                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.C, Keys.Control | Keys.Shift | Keys.Insert)
4610                     .Do(() => this.CopyIdUri()),
4611
4612                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.F)
4613                     .OnlyWhen(() => this.CurrentTab.TabType == MyCommon.TabUsageType.PublicSearch)
4614                     .Do(() => this.CurrentTabPage.Controls["panelSearch"].Controls["comboSearch"].Focus()),
4615
4616                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.L)
4617                     .Do(() => this.DoQuoteOfficial()),
4618
4619                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.S)
4620                     .Do(() => this.FavoriteChange(favAdd: false)),
4621
4622                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.B)
4623                     .Do(() => this.UnreadStripMenuItem_Click(this.UnreadStripMenuItem, EventArgs.Empty)),
4624
4625                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.T)
4626                     .Do(() => this.HashToggleMenuItem_Click(this.HashToggleMenuItem, EventArgs.Empty)),
4627
4628                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.P)
4629                     .Do(() => this.ImageSelectMenuItem_Click(this.ImageSelectMenuItem, EventArgs.Empty)),
4630
4631                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.H)
4632                     .Do(() => this.DoMoveToRTHome()),
4633
4634                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.Up)
4635                     .FocusedOn(FocusedControl.StatusText)
4636                     .Do(() =>
4637                     {
4638                         var tab = this.CurrentTab;
4639                         var selectedIndex = tab.SelectedIndex;
4640                         if (selectedIndex != -1 && selectedIndex > 0)
4641                         {
4642                             var listView = this.CurrentListView;
4643                             var idx = selectedIndex - 1;
4644                             this.SelectListItem(listView, idx);
4645                             listView.EnsureVisible(idx);
4646                         }
4647                     }),
4648
4649                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.Down)
4650                     .FocusedOn(FocusedControl.StatusText)
4651                     .Do(() =>
4652                     {
4653                         var tab = this.CurrentTab;
4654                         var selectedIndex = tab.SelectedIndex;
4655                         if (selectedIndex != -1 && selectedIndex < tab.AllCount - 1)
4656                         {
4657                             var listView = this.CurrentListView;
4658                             var idx = selectedIndex + 1;
4659                             this.SelectListItem(listView, idx);
4660                             listView.EnsureVisible(idx);
4661                         }
4662                     }),
4663
4664                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.Space)
4665                     .FocusedOn(FocusedControl.StatusText)
4666                     .Do(() =>
4667                     {
4668                         if (this.StatusText.SelectionStart > 0)
4669                         {
4670                             var endidx = this.StatusText.SelectionStart - 1;
4671                             var startstr = "";
4672                             for (var i = this.StatusText.SelectionStart - 1; i >= 0; i--)
4673                             {
4674                                 var c = this.StatusText.Text[i];
4675                                 if (char.IsLetterOrDigit(c) || c == '_')
4676                                 {
4677                                     continue;
4678                                 }
4679                                 if (c == '@')
4680                                 {
4681                                     startstr = this.StatusText.Text.Substring(i + 1, endidx - i);
4682                                     var cnt = this.AtIdSupl.ItemCount;
4683                                     this.ShowSuplDialog(this.StatusText, this.AtIdSupl, startstr.Length + 1, startstr);
4684                                     if (this.AtIdSupl.ItemCount != cnt)
4685                                         this.MarkSettingAtIdModified();
4686                                 }
4687                                 else if (c == '#')
4688                                 {
4689                                     startstr = this.StatusText.Text.Substring(i + 1, endidx - i);
4690                                     this.ShowSuplDialog(this.StatusText, this.HashSupl, startstr.Length + 1, startstr);
4691                                 }
4692                                 else
4693                                 {
4694                                     break;
4695                                 }
4696                             }
4697                         }
4698                     }),
4699
4700                 // ソートダイレクト選択(Ctrl+Shift+1~8,Ctrl+Shift+9)
4701                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D1)
4702                     .FocusedOn(FocusedControl.ListTab)
4703                     .Do(() => this.SetSortColumnByDisplayIndex(0)),
4704
4705                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D2)
4706                     .FocusedOn(FocusedControl.ListTab)
4707                     .Do(() => this.SetSortColumnByDisplayIndex(1)),
4708
4709                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D3)
4710                     .FocusedOn(FocusedControl.ListTab)
4711                     .Do(() => this.SetSortColumnByDisplayIndex(2)),
4712
4713                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D4)
4714                     .FocusedOn(FocusedControl.ListTab)
4715                     .Do(() => this.SetSortColumnByDisplayIndex(3)),
4716
4717                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D5)
4718                     .FocusedOn(FocusedControl.ListTab)
4719                     .Do(() => this.SetSortColumnByDisplayIndex(4)),
4720
4721                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D6)
4722                     .FocusedOn(FocusedControl.ListTab)
4723                     .Do(() => this.SetSortColumnByDisplayIndex(5)),
4724
4725                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D7)
4726                     .FocusedOn(FocusedControl.ListTab)
4727                     .Do(() => this.SetSortColumnByDisplayIndex(6)),
4728
4729                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D8)
4730                     .FocusedOn(FocusedControl.ListTab)
4731                     .Do(() => this.SetSortColumnByDisplayIndex(7)),
4732
4733                 ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.D9)
4734                     .FocusedOn(FocusedControl.ListTab)
4735                     .Do(() => this.SetSortLastColumn()),
4736
4737                 ShortcutCommand.Create(Keys.Control | Keys.Alt | Keys.S)
4738                     .FocusedOn(FocusedControl.ListTab)
4739                     .Do(() => this.FavoritesRetweetOfficial()),
4740
4741                 ShortcutCommand.Create(Keys.Control | Keys.Alt | Keys.R)
4742                     .FocusedOn(FocusedControl.ListTab)
4743                     .Do(() => this.FavoritesRetweetUnofficial()),
4744
4745                 ShortcutCommand.Create(Keys.Control | Keys.Alt | Keys.H)
4746                     .FocusedOn(FocusedControl.ListTab)
4747                     .Do(() => this.OpenUserAppointUrl()),
4748
4749                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.R)
4750                     .FocusedOn(FocusedControl.PostBrowser)
4751                     .Do(() => this.DoReTweetUnofficial()),
4752
4753                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.T)
4754                     .OnlyWhen(() => this.ExistCurrentPost)
4755                     .Do(() => this.tweetDetailsView.DoTranslation()),
4756
4757                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.R)
4758                     .Do(() => this.DoReTweetUnofficial()),
4759
4760                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.C, Keys.Alt | Keys.Shift | Keys.Insert)
4761                     .Do(() => this.CopyUserId()),
4762
4763                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Up)
4764                     .Do(() => this.tweetThumbnail1.Model.ScrollUp()),
4765
4766                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Down)
4767                     .Do(() => this.tweetThumbnail1.Model.ScrollDown()),
4768
4769                 ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Enter)
4770                     .FocusedOn(FocusedControl.ListTab)
4771                     .OnlyWhen(() => !this.SplitContainer3.Panel2Collapsed)
4772                     .Do(() => this.tweetThumbnail1.OpenImageInBrowser()),
4773             };
4774         }
4775
4776         internal bool CommonKeyDown(Keys keyData, FocusedControl focusedOn, out Task? asyncTask)
4777         {
4778             // Task を返す非同期処理があれば asyncTask に代入する
4779             asyncTask = null;
4780
4781             // ShortcutCommand に対応しているコマンドはここで処理される
4782             foreach (var command in this.shortcutCommands)
4783             {
4784                 if (command.IsMatch(keyData, focusedOn))
4785                 {
4786                     asyncTask = command.RunCommand();
4787                     return command.PreventDefault;
4788                 }
4789             }
4790
4791             return false;
4792         }
4793
4794         private void GoNextTab(bool forward)
4795         {
4796             var idx = this.statuses.SelectedTabIndex;
4797             var tabCount = this.statuses.Tabs.Count;
4798             if (forward)
4799             {
4800                 idx += 1;
4801                 if (idx > tabCount - 1) idx = 0;
4802             }
4803             else
4804             {
4805                 idx -= 1;
4806                 if (idx < 0) idx = tabCount - 1;
4807             }
4808             this.ListTab.SelectedIndex = idx;
4809         }
4810
4811         private void CopyStot()
4812         {
4813             var sb = new StringBuilder();
4814             var tab = this.CurrentTab;
4815             var isProtected = false;
4816             var isDm = tab.TabType == MyCommon.TabUsageType.DirectMessage;
4817             foreach (var post in tab.SelectedPosts)
4818             {
4819                 if (post.IsDeleted) continue;
4820                 if (!isDm)
4821                 {
4822                     if (post.RetweetedId != null)
4823                         sb.AppendFormat("{0}:{1} [https://twitter.com/{0}/status/{2}]{3}", post.ScreenName, post.TextSingleLine, post.RetweetedId, Environment.NewLine);
4824                     else
4825                         sb.AppendFormat("{0}:{1} [https://twitter.com/{0}/status/{2}]{3}", post.ScreenName, post.TextSingleLine, post.StatusId, Environment.NewLine);
4826                 }
4827                 else
4828                 {
4829                     sb.AppendFormat("{0}:{1} [{2}]{3}", post.ScreenName, post.TextSingleLine, post.StatusId, Environment.NewLine);
4830                 }
4831             }
4832             if (isProtected)
4833             {
4834                 MessageBox.Show(Properties.Resources.CopyStotText1);
4835             }
4836             if (sb.Length > 0)
4837             {
4838                 var clstr = sb.ToString();
4839                 try
4840                 {
4841                     Clipboard.SetDataObject(clstr, false, 5, 100);
4842                 }
4843                 catch (Exception ex)
4844                 {
4845                     MessageBox.Show(ex.Message);
4846                 }
4847             }
4848         }
4849
4850         private void CopyIdUri()
4851         {
4852             var tab = this.CurrentTab;
4853             if (tab == null || tab is DirectMessagesTabModel)
4854                 return;
4855
4856             var copyUrls = new List<string>();
4857             foreach (var post in tab.SelectedPosts)
4858                 copyUrls.Add(MyCommon.GetStatusUrl(post));
4859
4860             if (copyUrls.Count == 0)
4861                 return;
4862
4863             try
4864             {
4865                 Clipboard.SetDataObject(string.Join(Environment.NewLine, copyUrls), false, 5, 100);
4866             }
4867             catch (ExternalException ex)
4868             {
4869                 MessageBox.Show(ex.Message);
4870             }
4871         }
4872
4873         private void GoFav(bool forward)
4874         {
4875             var tab = this.CurrentTab;
4876             if (tab.AllCount == 0)
4877                 return;
4878
4879             var selectedIndex = tab.SelectedIndex;
4880
4881             int fIdx;
4882             int toIdx;
4883             int stp;
4884
4885             if (forward)
4886             {
4887                 if (selectedIndex == -1)
4888                 {
4889                     fIdx = 0;
4890                 }
4891                 else
4892                 {
4893                     fIdx = selectedIndex + 1;
4894                     if (fIdx > tab.AllCount - 1)
4895                         return;
4896                 }
4897                 toIdx = tab.AllCount;
4898                 stp = 1;
4899             }
4900             else
4901             {
4902                 if (selectedIndex == -1)
4903                 {
4904                     fIdx = tab.AllCount - 1;
4905                 }
4906                 else
4907                 {
4908                     fIdx = selectedIndex - 1;
4909                     if (fIdx < 0)
4910                         return;
4911                 }
4912                 toIdx = -1;
4913                 stp = -1;
4914             }
4915
4916             for (var idx = fIdx; idx != toIdx; idx += stp)
4917             {
4918                 if (tab[idx].IsFav)
4919                 {
4920                     var listView = this.CurrentListView;
4921                     this.SelectListItem(listView, idx);
4922                     listView.EnsureVisible(idx);
4923                     break;
4924                 }
4925             }
4926         }
4927
4928         private void GoSamePostToAnotherTab(bool left)
4929         {
4930             var tab = this.CurrentTab;
4931
4932             // Directタブは対象外(見つかるはずがない)
4933             if (tab.TabType == MyCommon.TabUsageType.DirectMessage)
4934                 return;
4935
4936             var selectedStatusId = tab.SelectedStatusId;
4937             if (selectedStatusId == null)
4938                 return;
4939
4940             int fIdx, toIdx, stp;
4941
4942             if (left)
4943             {
4944                 // 左のタブへ
4945                 if (this.ListTab.SelectedIndex == 0)
4946                 {
4947                     return;
4948                 }
4949                 else
4950                 {
4951                     fIdx = this.ListTab.SelectedIndex - 1;
4952                 }
4953                 toIdx = -1;
4954                 stp = -1;
4955             }
4956             else
4957             {
4958                 // 右のタブへ
4959                 if (this.ListTab.SelectedIndex == this.ListTab.TabCount - 1)
4960                 {
4961                     return;
4962                 }
4963                 else
4964                 {
4965                     fIdx = this.ListTab.SelectedIndex + 1;
4966                 }
4967                 toIdx = this.ListTab.TabCount;
4968                 stp = 1;
4969             }
4970
4971             for (var tabidx = fIdx; tabidx != toIdx; tabidx += stp)
4972             {
4973                 var targetTab = this.statuses.Tabs[tabidx];
4974
4975                 // Directタブは対象外
4976                 if (targetTab.TabType == MyCommon.TabUsageType.DirectMessage)
4977                     continue;
4978
4979                 var foundIndex = targetTab.IndexOf(selectedStatusId);
4980                 if (foundIndex != -1)
4981                 {
4982                     this.ListTab.SelectedIndex = tabidx;
4983                     var listView = this.CurrentListView;
4984                     this.SelectListItem(listView, foundIndex);
4985                     listView.EnsureVisible(foundIndex);
4986                     return;
4987                 }
4988             }
4989         }
4990
4991         private void GoPost(bool forward)
4992         {
4993             var tab = this.CurrentTab;
4994             var currentPost = this.CurrentPost;
4995
4996             if (currentPost == null)
4997                 return;
4998
4999             var selectedIndex = tab.SelectedIndex;
5000
5001             int fIdx, toIdx, stp;
5002
5003             if (forward)
5004             {
5005                 fIdx = selectedIndex + 1;
5006                 if (fIdx > tab.AllCount - 1) return;
5007                 toIdx = tab.AllCount;
5008                 stp = 1;
5009             }
5010             else
5011             {
5012                 fIdx = selectedIndex - 1;
5013                 if (fIdx < 0) return;
5014                 toIdx = -1;
5015                 stp = -1;
5016             }
5017
5018             string name;
5019             if (currentPost.RetweetedBy == null)
5020             {
5021                 name = currentPost.ScreenName;
5022             }
5023             else
5024             {
5025                 name = currentPost.RetweetedBy;
5026             }
5027             for (var idx = fIdx; idx != toIdx; idx += stp)
5028             {
5029                 var post = tab[idx];
5030                 if (post.RetweetedId == null)
5031                 {
5032                     if (post.ScreenName == name)
5033                     {
5034                         var listView = this.CurrentListView;
5035                         this.SelectListItem(listView, idx);
5036                         listView.EnsureVisible(idx);
5037                         break;
5038                     }
5039                 }
5040                 else
5041                 {
5042                     if (post.RetweetedBy == name)
5043                     {
5044                         var listView = this.CurrentListView;
5045                         this.SelectListItem(listView, idx);
5046                         listView.EnsureVisible(idx);
5047                         break;
5048                     }
5049                 }
5050             }
5051         }
5052
5053         private void GoRelPost(bool forward)
5054         {
5055             var tab = this.CurrentTab;
5056             var selectedIndex = tab.SelectedIndex;
5057
5058             if (selectedIndex == -1)
5059                 return;
5060
5061             int fIdx, toIdx, stp;
5062
5063             if (forward)
5064             {
5065                 fIdx = selectedIndex + 1;
5066                 if (fIdx > tab.AllCount - 1) return;
5067                 toIdx = tab.AllCount;
5068                 stp = 1;
5069             }
5070             else
5071             {
5072                 fIdx = selectedIndex - 1;
5073                 if (fIdx < 0) return;
5074                 toIdx = -1;
5075                 stp = -1;
5076             }
5077
5078             var anchorPost = tab.AnchorPost;
5079             if (anchorPost == null)
5080             {
5081                 var currentPost = this.CurrentPost;
5082                 if (currentPost == null)
5083                     return;
5084
5085                 anchorPost = currentPost;
5086                 tab.AnchorPost = currentPost;
5087             }
5088
5089             for (var idx = fIdx; idx != toIdx; idx += stp)
5090             {
5091                 var post = tab[idx];
5092                 if (post.ScreenName == anchorPost.ScreenName ||
5093                     post.RetweetedBy == anchorPost.ScreenName ||
5094                     post.ScreenName == anchorPost.RetweetedBy ||
5095                     (!MyCommon.IsNullOrEmpty(post.RetweetedBy) && post.RetweetedBy == anchorPost.RetweetedBy) ||
5096                     anchorPost.ReplyToList.Any(x => x.UserId == post.UserId) ||
5097                     anchorPost.ReplyToList.Any(x => x.UserId == post.RetweetedByUserId) ||
5098                     post.ReplyToList.Any(x => x.UserId == anchorPost.UserId) ||
5099                     post.ReplyToList.Any(x => x.UserId == anchorPost.RetweetedByUserId))
5100                 {
5101                     var listView = this.CurrentListView;
5102                     this.SelectListItem(listView, idx);
5103                     listView.EnsureVisible(idx);
5104                     break;
5105                 }
5106             }
5107         }
5108
5109         private void GoAnchor()
5110         {
5111             var anchorStatusId = this.CurrentTab.AnchorStatusId;
5112             if (anchorStatusId == null)
5113                 return;
5114
5115             var idx = this.CurrentTab.IndexOf(anchorStatusId);
5116             if (idx == -1)
5117                 return;
5118
5119             var listView = this.CurrentListView;
5120             this.SelectListItem(listView, idx);
5121             listView.EnsureVisible(idx);
5122         }
5123
5124         private void GoTopEnd(bool goTop)
5125         {
5126             var listView = this.CurrentListView;
5127             if (listView.VirtualListSize == 0)
5128                 return;
5129
5130             ListViewItem item;
5131             int idx;
5132
5133             if (goTop)
5134             {
5135                 item = listView.GetItemAt(0, 25);
5136                 if (item == null)
5137                     idx = 0;
5138                 else
5139                     idx = item.Index;
5140             }
5141             else
5142             {
5143                 item = listView.GetItemAt(0, listView.ClientSize.Height - 1);
5144                 if (item == null)
5145                     idx = listView.VirtualListSize - 1;
5146                 else
5147                     idx = item.Index;
5148             }
5149             this.SelectListItem(listView, idx);
5150         }
5151
5152         private void GoMiddle()
5153         {
5154             var listView = this.CurrentListView;
5155             if (listView.VirtualListSize == 0)
5156                 return;
5157
5158             ListViewItem item;
5159             int idx1;
5160             int idx2;
5161             int idx3;
5162
5163             item = listView.GetItemAt(0, 0);
5164             if (item == null)
5165             {
5166                 idx1 = 0;
5167             }
5168             else
5169             {
5170                 idx1 = item.Index;
5171             }
5172
5173             item = listView.GetItemAt(0, listView.ClientSize.Height - 1);
5174             if (item == null)
5175             {
5176                 idx2 = listView.VirtualListSize - 1;
5177             }
5178             else
5179             {
5180                 idx2 = item.Index;
5181             }
5182             idx3 = (idx1 + idx2) / 2;
5183
5184             this.SelectListItem(listView, idx3);
5185         }
5186
5187         private void GoLast()
5188         {
5189             var listView = this.CurrentListView;
5190             if (listView.VirtualListSize == 0) return;
5191
5192             if (this.statuses.SortOrder == SortOrder.Ascending)
5193             {
5194                 this.SelectListItem(listView, listView.VirtualListSize - 1);
5195                 listView.EnsureVisible(listView.VirtualListSize - 1);
5196             }
5197             else
5198             {
5199                 this.SelectListItem(listView, 0);
5200                 listView.EnsureVisible(0);
5201             }
5202         }
5203
5204         private void MoveTop()
5205         {
5206             var listView = this.CurrentListView;
5207             if (listView.SelectedIndices.Count == 0) return;
5208             var idx = listView.SelectedIndices[0];
5209             if (this.statuses.SortOrder == SortOrder.Ascending)
5210             {
5211                 listView.EnsureVisible(listView.VirtualListSize - 1);
5212             }
5213             else
5214             {
5215                 listView.EnsureVisible(0);
5216             }
5217             listView.EnsureVisible(idx);
5218         }
5219
5220         private async Task GoInReplyToPostTree()
5221         {
5222             var curTabClass = this.CurrentTab;
5223             var currentPost = this.CurrentPost;
5224
5225             if (currentPost == null)
5226                 return;
5227
5228             if (curTabClass.TabType == MyCommon.TabUsageType.PublicSearch && currentPost.InReplyToStatusId == null && currentPost.TextFromApi.Contains("@"))
5229             {
5230                 try
5231                 {
5232                     var post = await this.tw.GetStatusApi(false, currentPost.StatusId.ToTwitterStatusId());
5233
5234                     currentPost = currentPost with
5235                     {
5236                         InReplyToStatusId = post.InReplyToStatusId,
5237                         InReplyToUser = post.InReplyToUser,
5238                         IsReply = post.IsReply,
5239                     };
5240                     curTabClass.ReplacePost(currentPost);
5241                     this.listCache?.PurgeCache();
5242
5243                     var index = curTabClass.SelectedIndex;
5244                     this.CurrentListView.RedrawItems(index, index, false);
5245                 }
5246                 catch (WebApiException ex)
5247                 {
5248                     this.StatusLabel.Text = $"Err:{ex.Message}(GetStatus)";
5249                 }
5250             }
5251
5252             if (!(this.ExistCurrentPost && currentPost.InReplyToUser != null && currentPost.InReplyToStatusId != null)) return;
5253
5254             if (this.replyChains == null || (this.replyChains.Count > 0 && this.replyChains.Peek().InReplyToId != currentPost.StatusId))
5255             {
5256                 this.replyChains = new Stack<ReplyChain>();
5257             }
5258             this.replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId, curTabClass));
5259
5260             int inReplyToIndex;
5261             string inReplyToTabName;
5262             var inReplyToId = currentPost.InReplyToStatusId;
5263             var inReplyToUser = currentPost.InReplyToUser;
5264
5265             var inReplyToPosts = from tab in this.statuses.Tabs
5266                                  orderby tab != curTabClass
5267                                  from post in tab.Posts.Values
5268                                  where post.StatusId == inReplyToId
5269                                  let index = tab.IndexOf(post.StatusId)
5270                                  where index != -1
5271                                  select new { Tab = tab, Index = index };
5272
5273             var inReplyPost = inReplyToPosts.FirstOrDefault();
5274             if (inReplyPost == null)
5275             {
5276                 try
5277                 {
5278                     await Task.Run(async () =>
5279                     {
5280                         var post = await this.tw.GetStatusApi(false, currentPost.InReplyToStatusId.ToTwitterStatusId())
5281                             .ConfigureAwait(false);
5282                         post.IsRead = true;
5283
5284                         this.statuses.AddPost(post);
5285                         this.statuses.DistributePosts();
5286                     });
5287                 }
5288                 catch (WebApiException ex)
5289                 {
5290                     this.StatusLabel.Text = $"Err:{ex.Message}(GetStatus)";
5291                     await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId.ToTwitterStatusId()));
5292                     return;
5293                 }
5294
5295                 this.RefreshTimeline();
5296
5297                 inReplyPost = inReplyToPosts.FirstOrDefault();
5298                 if (inReplyPost == null)
5299                 {
5300                     await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId.ToTwitterStatusId()));
5301                     return;
5302                 }
5303             }
5304             inReplyToTabName = inReplyPost.Tab.TabName;
5305             inReplyToIndex = inReplyPost.Index;
5306
5307             var tabIndex = this.statuses.Tabs.IndexOf(inReplyToTabName);
5308             var tabPage = this.ListTab.TabPages[tabIndex];
5309             var listView = (DetailsListView)tabPage.Tag;
5310
5311             if (this.CurrentTabName != inReplyToTabName)
5312             {
5313                 this.ListTab.SelectedIndex = tabIndex;
5314             }
5315
5316             this.SelectListItem(listView, inReplyToIndex);
5317             listView.EnsureVisible(inReplyToIndex);
5318         }
5319
5320         private void GoBackInReplyToPostTree(bool parallel = false, bool isForward = true)
5321         {
5322             var curTabClass = this.CurrentTab;
5323             var currentPost = this.CurrentPost;
5324
5325             if (currentPost == null)
5326                 return;
5327
5328             if (parallel)
5329             {
5330                 if (currentPost.InReplyToStatusId != null)
5331                 {
5332                     var posts = from t in this.statuses.Tabs
5333                                 from p in t.Posts
5334                                 where p.Value.StatusId != currentPost.StatusId && p.Value.InReplyToStatusId == currentPost.InReplyToStatusId
5335                                 let indexOf = t.IndexOf(p.Value.StatusId)
5336                                 where indexOf > -1
5337                                 orderby isForward ? indexOf : indexOf * -1
5338                                 orderby t != curTabClass
5339                                 select new { Tab = t, Post = p.Value, Index = indexOf };
5340                     try
5341                     {
5342                         var postList = posts.ToList();
5343                         for (var i = postList.Count - 1; i >= 0; i--)
5344                         {
5345                             var index = i;
5346                             if (postList.FindIndex(pst => pst.Post.StatusId == postList[index].Post.StatusId) != index)
5347                             {
5348                                 postList.RemoveAt(index);
5349                             }
5350                         }
5351                         var currentIndex = this.CurrentTab.SelectedIndex;
5352                         var post = postList.FirstOrDefault(pst => pst.Tab == curTabClass && isForward ? pst.Index > currentIndex : pst.Index < currentIndex);
5353                         if (post == null) post = postList.FirstOrDefault(pst => pst.Tab != curTabClass);
5354                         if (post == null) post = postList.First();
5355                         var tabIndex = this.statuses.Tabs.IndexOf(post.Tab);
5356                         this.ListTab.SelectedIndex = tabIndex;
5357                         var listView = this.CurrentListView;
5358                         this.SelectListItem(listView, post.Index);
5359                         listView.EnsureVisible(post.Index);
5360                     }
5361                     catch (InvalidOperationException)
5362                     {
5363                         return;
5364                     }
5365                 }
5366             }
5367             else
5368             {
5369                 if (this.replyChains == null || this.replyChains.Count < 1)
5370                 {
5371                     var posts = from t in this.statuses.Tabs
5372                                 from p in t.Posts
5373                                 where p.Value.InReplyToStatusId == currentPost.StatusId
5374                                 let indexOf = t.IndexOf(p.Value.StatusId)
5375                                 where indexOf > -1
5376                                 orderby indexOf
5377                                 orderby t != curTabClass
5378                                 select new { Tab = t, Index = indexOf };
5379                     try
5380                     {
5381                         var post = posts.First();
5382                         var tabIndex = this.statuses.Tabs.IndexOf(post.Tab);
5383                         this.ListTab.SelectedIndex = tabIndex;
5384                         var listView = this.CurrentListView;
5385                         this.SelectListItem(listView, post.Index);
5386                         listView.EnsureVisible(post.Index);
5387                     }
5388                     catch (InvalidOperationException)
5389                     {
5390                         return;
5391                     }
5392                 }
5393                 else
5394                 {
5395                     var chainHead = this.replyChains.Pop();
5396                     if (chainHead.InReplyToId == currentPost.StatusId)
5397                     {
5398                         var tab = chainHead.OriginalTab;
5399                         if (!this.statuses.Tabs.Contains(tab))
5400                         {
5401                             this.replyChains = null;
5402                         }
5403                         else
5404                         {
5405                             var idx = tab.IndexOf(chainHead.OriginalId);
5406                             if (idx == -1)
5407                             {
5408                                 this.replyChains = null;
5409                             }
5410                             else
5411                             {
5412                                 var tabIndex = this.statuses.Tabs.IndexOf(tab);
5413                                 try
5414                                 {
5415                                     this.ListTab.SelectedIndex = tabIndex;
5416                                 }
5417                                 catch (Exception)
5418                                 {
5419                                     this.replyChains = null;
5420                                 }
5421                                 var listView = this.CurrentListView;
5422                                 this.SelectListItem(listView, idx);
5423                                 listView.EnsureVisible(idx);
5424                             }
5425                         }
5426                     }
5427                     else
5428                     {
5429                         this.replyChains = null;
5430                         this.GoBackInReplyToPostTree(parallel);
5431                     }
5432                 }
5433             }
5434         }
5435
5436         private void GoBackSelectPostChain()
5437         {
5438             if (this.selectPostChains.Count > 1)
5439             {
5440                 var idx = -1;
5441                 TabModel? foundTab = null;
5442
5443                 do
5444                 {
5445                     try
5446                     {
5447                         this.selectPostChains.Pop();
5448                         var (tab, post) = this.selectPostChains.Peek();
5449
5450                         if (!this.statuses.Tabs.Contains(tab))
5451                             continue; // 該当タブが存在しないので無視
5452
5453                         if (post != null)
5454                         {
5455                             idx = tab.IndexOf(post.StatusId);
5456                             if (idx == -1) continue;  // 該当ポストが存在しないので無視
5457                         }
5458
5459                         foundTab = tab;
5460
5461                         this.selectPostChains.Pop();
5462                     }
5463                     catch (InvalidOperationException)
5464                     {
5465                     }
5466
5467                     break;
5468                 }
5469                 while (this.selectPostChains.Count > 1);
5470
5471                 if (foundTab == null)
5472                 {
5473                     // 状態がおかしいので処理を中断
5474                     // 履歴が残り1つであればクリアしておく
5475                     if (this.selectPostChains.Count == 1)
5476                         this.selectPostChains.Clear();
5477                     return;
5478                 }
5479
5480                 var tabIndex = this.statuses.Tabs.IndexOf(foundTab);
5481                 var tabPage = this.ListTab.TabPages[tabIndex];
5482                 var lst = (DetailsListView)tabPage.Tag;
5483                 this.ListTab.SelectedIndex = tabIndex;
5484
5485                 if (idx > -1)
5486                 {
5487                     this.SelectListItem(lst, idx);
5488                     lst.EnsureVisible(idx);
5489                 }
5490                 lst.Focus();
5491             }
5492         }
5493
5494         private void PushSelectPostChain()
5495         {
5496             var currentTab = this.CurrentTab;
5497             var currentPost = this.CurrentPost;
5498
5499             var count = this.selectPostChains.Count;
5500             if (count > 0)
5501             {
5502                 var (tab, post) = this.selectPostChains.Peek();
5503                 if (tab == currentTab)
5504                 {
5505                     if (post == currentPost) return;  // 最新の履歴と同一
5506                     if (post == null) this.selectPostChains.Pop();  // 置き換えるため削除
5507                 }
5508             }
5509             if (count >= 2500) this.TrimPostChain();
5510             this.selectPostChains.Push((currentTab, currentPost));
5511         }
5512
5513         private void TrimPostChain()
5514         {
5515             if (this.selectPostChains.Count <= 2000) return;
5516             var p = new Stack<(TabModel, PostClass?)>(2000);
5517             for (var i = 0; i < 2000; i++)
5518             {
5519                 p.Push(this.selectPostChains.Pop());
5520             }
5521             this.selectPostChains.Clear();
5522             for (var i = 0; i < 2000; i++)
5523             {
5524                 this.selectPostChains.Push(p.Pop());
5525             }
5526         }
5527
5528         private bool GoStatus(PostId statusId)
5529         {
5530             var tab = this.statuses.Tabs
5531                 .Where(x => x.TabType != MyCommon.TabUsageType.DirectMessage)
5532                 .Where(x => x.Contains(statusId))
5533                 .FirstOrDefault();
5534
5535             if (tab == null)
5536                 return false;
5537
5538             var index = tab.IndexOf(statusId);
5539
5540             var tabIndex = this.statuses.Tabs.IndexOf(tab);
5541             this.ListTab.SelectedIndex = tabIndex;
5542
5543             var listView = this.CurrentListView;
5544             this.SelectListItem(listView, index);
5545             listView.EnsureVisible(index);
5546
5547             return true;
5548         }
5549
5550         private bool GoDirectMessage(PostId statusId)
5551         {
5552             var tab = this.statuses.DirectMessageTab;
5553             var index = tab.IndexOf(statusId);
5554
5555             if (index == -1)
5556                 return false;
5557
5558             var tabIndex = this.statuses.Tabs.IndexOf(tab);
5559             this.ListTab.SelectedIndex = tabIndex;
5560
5561             var listView = this.CurrentListView;
5562             this.SelectListItem(listView, index);
5563             listView.EnsureVisible(index);
5564
5565             return true;
5566         }
5567
5568         private void MyList_MouseClick(object sender, MouseEventArgs e)
5569             => this.CurrentTab.ClearAnchor();
5570
5571         private void StatusText_Enter(object sender, EventArgs e)
5572         {
5573             // フォーカスの戻り先を StatusText に設定
5574             this.Tag = this.StatusText;
5575             this.StatusText.BackColor = this.themeManager.ColorInputBackcolor;
5576         }
5577
5578         public Color InputBackColor
5579             => this.themeManager.ColorInputBackcolor;
5580
5581         private void StatusText_Leave(object sender, EventArgs e)
5582         {
5583             // フォーカスがメニューに遷移しないならばフォーカスはタブに移ることを期待
5584             if (this.ListTab.SelectedTab != null && this.MenuStrip1.Tag == null) this.Tag = this.ListTab.SelectedTab.Tag;
5585             this.StatusText.BackColor = Color.FromKnownColor(KnownColor.Window);
5586         }
5587
5588         private async void StatusText_KeyDown(object sender, KeyEventArgs e)
5589         {
5590             if (this.CommonKeyDown(e.KeyData, FocusedControl.StatusText, out var asyncTask))
5591             {
5592                 e.Handled = true;
5593                 e.SuppressKeyPress = true;
5594             }
5595
5596             this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
5597
5598             if (asyncTask != null)
5599                 await asyncTask;
5600         }
5601
5602         private void SaveConfigsAll(bool ifModified)
5603         {
5604             if (!ifModified)
5605             {
5606                 this.SaveConfigsCommon();
5607                 this.SaveConfigsLocal();
5608                 this.SaveConfigsTabs();
5609                 this.SaveConfigsAtId();
5610             }
5611             else
5612             {
5613                 if (this.ModifySettingCommon) this.SaveConfigsCommon();
5614                 if (this.ModifySettingLocal) this.SaveConfigsLocal();
5615                 if (this.ModifySettingAtId) this.SaveConfigsAtId();
5616             }
5617         }
5618
5619         private void SaveConfigsAtId()
5620         {
5621             if (this.ignoreConfigSave || !this.settings.Common.UseAtIdSupplement && this.AtIdSupl == null) return;
5622
5623             this.ModifySettingAtId = false;
5624             this.settings.AtIdList.AtIdList = this.AtIdSupl.GetItemList();
5625             this.settings.SaveAtIdList();
5626         }
5627
5628         private void SaveConfigsCommon()
5629         {
5630             if (this.ignoreConfigSave) return;
5631
5632             this.ModifySettingCommon = false;
5633             lock (this.syncObject)
5634             {
5635                 this.settings.Common.SortOrder = (int)this.statuses.SortOrder;
5636                 this.settings.Common.SortColumn = this.statuses.SortMode switch
5637                 {
5638                     ComparerMode.Nickname => 1, // ニックネーム
5639                     ComparerMode.Data => 2, // 本文
5640                     ComparerMode.Id => 3, // 時刻=発言Id
5641                     ComparerMode.Name => 4, // 名前
5642                     ComparerMode.Source => 7, // Source
5643                     _ => throw new InvalidOperationException($"Invalid sort mode: {this.statuses.SortMode}"),
5644                 };
5645                 this.settings.Common.HashTags = this.HashMgr.HashHistories;
5646                 if (this.HashMgr.IsPermanent)
5647                 {
5648                     this.settings.Common.HashSelected = this.HashMgr.UseHash;
5649                 }
5650                 else
5651                 {
5652                     this.settings.Common.HashSelected = "";
5653                 }
5654                 this.settings.Common.HashIsHead = this.HashMgr.IsHead;
5655                 this.settings.Common.HashIsPermanent = this.HashMgr.IsPermanent;
5656                 this.settings.Common.HashIsNotAddToAtReply = this.HashMgr.IsNotAddToAtReply;
5657                 this.settings.Common.UseImageService = this.ImageSelector.Model.SelectedMediaServiceIndex;
5658                 this.settings.Common.UseImageServiceName = this.ImageSelector.Model.SelectedMediaServiceName;
5659
5660                 this.settings.SaveCommon();
5661             }
5662         }
5663
5664         private void SaveConfigsLocal()
5665         {
5666             if (this.ignoreConfigSave) return;
5667             lock (this.syncObject)
5668             {
5669                 this.ModifySettingLocal = false;
5670                 this.settings.Local.ScaleDimension = this.CurrentAutoScaleDimensions;
5671                 this.settings.Local.FormSize = this.mySize;
5672                 this.settings.Local.FormLocation = this.myLoc;
5673                 this.settings.Local.SplitterDistance = this.mySpDis;
5674                 this.settings.Local.PreviewDistance = this.mySpDis3;
5675                 this.settings.Local.StatusMultiline = this.StatusText.Multiline;
5676                 this.settings.Local.StatusTextHeight = this.mySpDis2;
5677
5678                 if (this.ignoreConfigSave) return;
5679                 this.settings.SaveLocal();
5680             }
5681         }
5682
5683         private void SaveConfigsTabs()
5684         {
5685             var tabSettingList = new List<SettingTabs.SettingTabItem>();
5686
5687             var tabs = this.statuses.Tabs.Append(this.statuses.MuteTab);
5688
5689             foreach (var tab in tabs)
5690             {
5691                 if (!tab.IsPermanentTabType)
5692                     continue;
5693
5694                 var tabSetting = new SettingTabs.SettingTabItem
5695                 {
5696                     TabName = tab.TabName,
5697                     TabType = tab.TabType,
5698                     UnreadManage = tab.UnreadManage,
5699                     Protected = tab.Protected,
5700                     Notify = tab.Notify,
5701                     SoundFile = tab.SoundFile,
5702                 };
5703
5704                 switch (tab)
5705                 {
5706                     case FilterTabModel filterTab:
5707                         tabSetting.FilterArray = filterTab.FilterArray;
5708                         break;
5709                     case UserTimelineTabModel userTab:
5710                         tabSetting.User = userTab.ScreenName;
5711                         tabSetting.UserId = userTab.UserId;
5712                         break;
5713                     case PublicSearchTabModel searchTab:
5714                         tabSetting.SearchWords = searchTab.SearchWords;
5715                         tabSetting.SearchLang = searchTab.SearchLang;
5716                         break;
5717                     case ListTimelineTabModel listTab:
5718                         tabSetting.ListInfo = listTab.ListInfo;
5719                         break;
5720                 }
5721
5722                 tabSettingList.Add(tabSetting);
5723             }
5724
5725             this.settings.Tabs.Tabs = tabSettingList;
5726             this.settings.SaveTabs();
5727         }
5728
5729         private async void OpenURLFileMenuItem_Click(object sender, EventArgs e)
5730         {
5731             static void ShowFormatErrorDialog(IWin32Window owner)
5732             {
5733                 MessageBox.Show(
5734                     owner,
5735                     Properties.Resources.OpenURL_InvalidFormat,
5736                     Properties.Resources.OpenURL_Caption,
5737                     MessageBoxButtons.OK,
5738                     MessageBoxIcon.Error
5739                 );
5740             }
5741
5742             var ret = InputDialog.Show(this, Properties.Resources.OpenURL_InputText, Properties.Resources.OpenURL_Caption, out var inputText);
5743             if (ret != DialogResult.OK)
5744                 return;
5745
5746             var match = Twitter.StatusUrlRegex.Match(inputText);
5747             if (!match.Success)
5748             {
5749                 ShowFormatErrorDialog(this);
5750                 return;
5751             }
5752
5753             try
5754             {
5755                 var statusId = new TwitterStatusId(match.Groups["StatusId"].Value);
5756                 await this.OpenRelatedTab(statusId);
5757             }
5758             catch (OverflowException)
5759             {
5760                 ShowFormatErrorDialog(this);
5761             }
5762             catch (TabException ex)
5763             {
5764                 MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
5765             }
5766         }
5767
5768         private void SaveLogMenuItem_Click(object sender, EventArgs e)
5769         {
5770             var tab = this.CurrentTab;
5771
5772             var rslt = MessageBox.Show(
5773                 string.Format(Properties.Resources.SaveLogMenuItem_ClickText1, Environment.NewLine),
5774                 Properties.Resources.SaveLogMenuItem_ClickText2,
5775                 MessageBoxButtons.YesNoCancel,
5776                 MessageBoxIcon.Question);
5777             if (rslt == DialogResult.Cancel) return;
5778
5779             this.SaveFileDialog1.FileName = $"{ApplicationSettings.AssemblyName}Posts{DateTimeUtc.Now.ToLocalTime():yyMMdd-HHmmss}.tsv";
5780             this.SaveFileDialog1.InitialDirectory = Application.ExecutablePath;
5781             this.SaveFileDialog1.Filter = Properties.Resources.SaveLogMenuItem_ClickText3;
5782             this.SaveFileDialog1.FilterIndex = 0;
5783             this.SaveFileDialog1.Title = Properties.Resources.SaveLogMenuItem_ClickText4;
5784             this.SaveFileDialog1.RestoreDirectory = true;
5785
5786             if (this.SaveFileDialog1.ShowDialog() == DialogResult.OK)
5787             {
5788                 if (!this.SaveFileDialog1.ValidateNames) return;
5789                 using var sw = new StreamWriter(this.SaveFileDialog1.FileName, false, Encoding.UTF8);
5790                 if (rslt == DialogResult.Yes)
5791                 {
5792                     // All
5793                     for (var idx = 0; idx < tab.AllCount; idx++)
5794                     {
5795                         var post = tab[idx];
5796                         var protect = "";
5797                         if (post.IsProtect)
5798                             protect = "Protect";
5799                         sw.WriteLine(post.Nickname + "\t" +
5800                                  "\"" + post.TextFromApi.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
5801                                  post.CreatedAt.ToLocalTimeString() + "\t" +
5802                                  post.ScreenName + "\t" +
5803                                  post.StatusId.Id + "\t" +
5804                                  post.ImageUrl + "\t" +
5805                                  "\"" + post.Text.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
5806                                  protect);
5807                     }
5808                 }
5809                 else
5810                 {
5811                     foreach (var post in this.CurrentTab.SelectedPosts)
5812                     {
5813                         var protect = "";
5814                         if (post.IsProtect)
5815                             protect = "Protect";
5816                         sw.WriteLine(post.Nickname + "\t" +
5817                                  "\"" + post.TextFromApi.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
5818                                  post.CreatedAt.ToLocalTimeString() + "\t" +
5819                                  post.ScreenName + "\t" +
5820                                  post.StatusId.Id + "\t" +
5821                                  post.ImageUrl + "\t" +
5822                                  "\"" + post.Text.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
5823                                  protect);
5824                     }
5825                 }
5826             }
5827             this.TopMost = this.settings.Common.AlwaysTop;
5828         }
5829
5830         public bool TabRename(string origTabName, [NotNullWhen(true)] out string? newTabName)
5831         {
5832             // タブ名変更
5833             newTabName = null;
5834             using (var inputName = new InputTabName())
5835             {
5836                 inputName.TabName = origTabName;
5837                 inputName.ShowDialog();
5838                 if (inputName.DialogResult == DialogResult.Cancel) return false;
5839                 newTabName = inputName.TabName;
5840             }
5841             this.TopMost = this.settings.Common.AlwaysTop;
5842             if (!MyCommon.IsNullOrEmpty(newTabName))
5843             {
5844                 // 新タブ名存在チェック
5845                 if (this.statuses.ContainsTab(newTabName))
5846                 {
5847                     var tmp = string.Format(Properties.Resources.Tabs_DoubleClickText1, newTabName);
5848                     MessageBox.Show(tmp, Properties.Resources.Tabs_DoubleClickText2, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
5849                     return false;
5850                 }
5851
5852                 var tabIndex = this.statuses.Tabs.IndexOf(origTabName);
5853                 var tabPage = this.ListTab.TabPages[tabIndex];
5854
5855                 // タブ名を変更
5856                 if (tabPage != null)
5857                     tabPage.Text = newTabName;
5858
5859                 this.statuses.RenameTab(origTabName, newTabName);
5860
5861                 var state = this.listViewState[origTabName];
5862                 this.listViewState.Remove(origTabName);
5863                 this.listViewState[newTabName] = state;
5864
5865                 this.SaveConfigsCommon();
5866                 this.SaveConfigsTabs();
5867                 this.rclickTabName = newTabName;
5868                 return true;
5869             }
5870             else
5871             {
5872                 return false;
5873             }
5874         }
5875
5876         private void ListTab_MouseClick(object sender, MouseEventArgs e)
5877         {
5878             if (e.Button == MouseButtons.Middle)
5879             {
5880                 foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
5881                 {
5882                     if (this.ListTab.GetTabRect(index).Contains(e.Location))
5883                     {
5884                         this.RemoveSpecifiedTab(tab.TabName, true);
5885                         this.SaveConfigsTabs();
5886                         break;
5887                     }
5888                 }
5889             }
5890         }
5891
5892         private void ListTab_DoubleClick(object sender, MouseEventArgs e)
5893             => this.TabRename(this.CurrentTabName, out _);
5894
5895         private void ListTab_MouseDown(object sender, MouseEventArgs e)
5896         {
5897             if (this.settings.Common.TabMouseLock) return;
5898             if (e.Button == MouseButtons.Left)
5899             {
5900                 foreach (var i in Enumerable.Range(0, this.statuses.Tabs.Count))
5901                 {
5902                     if (this.ListTab.GetTabRect(i).Contains(e.Location))
5903                     {
5904                         this.tabDrag = true;
5905                         this.tabMouseDownPoint = e.Location;
5906                         break;
5907                     }
5908                 }
5909             }
5910             else
5911             {
5912                 this.tabDrag = false;
5913             }
5914         }
5915
5916         private void ListTab_DragEnter(object sender, DragEventArgs e)
5917         {
5918             if (e.Data.GetDataPresent(typeof(TabPage)))
5919                 e.Effect = DragDropEffects.Move;
5920             else
5921                 e.Effect = DragDropEffects.None;
5922         }
5923
5924         private void ListTab_DragDrop(object sender, DragEventArgs e)
5925         {
5926             if (!e.Data.GetDataPresent(typeof(TabPage))) return;
5927
5928             this.tabDrag = false;
5929             var tn = "";
5930             var bef = false;
5931             var cpos = new Point(e.X, e.Y);
5932             var spos = this.ListTab.PointToClient(cpos);
5933             foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
5934             {
5935                 var rect = this.ListTab.GetTabRect(index);
5936                 if (rect.Contains(spos))
5937                 {
5938                     tn = tab.TabName;
5939                     if (spos.X <= (rect.Left + rect.Right) / 2)
5940                         bef = true;
5941                     else
5942                         bef = false;
5943
5944                     break;
5945                 }
5946             }
5947
5948             // タブのないところにドロップ->最後尾へ移動
5949             if (MyCommon.IsNullOrEmpty(tn))
5950             {
5951                 var lastTab = this.statuses.Tabs.Last();
5952                 tn = lastTab.TabName;
5953                 bef = false;
5954             }
5955
5956             var tp = (TabPage)e.Data.GetData(typeof(TabPage));
5957             if (tp.Text == tn) return;
5958
5959             this.ReOrderTab(tp.Text, tn, bef);
5960         }
5961
5962         public void ReOrderTab(string targetTabText, string baseTabText, bool isBeforeBaseTab)
5963         {
5964             var baseIndex = this.GetTabPageIndex(baseTabText);
5965             if (baseIndex == -1)
5966                 return;
5967
5968             var targetIndex = this.GetTabPageIndex(targetTabText);
5969             if (targetIndex == -1)
5970                 return;
5971
5972             using (ControlTransaction.Layout(this.ListTab))
5973             {
5974                 // 選択中のタブを Remove メソッドで取り外すと選択状態が変化して Selecting イベントが発生するが、
5975                 // この時 TabInformations と TabControl の並び順が不一致なままで ListTabSelect メソッドが呼ばれてしまう。
5976                 // これを防ぐために、Remove メソッドを呼ぶ前に選択中のタブを切り替えておく必要がある
5977                 this.ListTab.SelectedIndex = targetIndex == 0 ? 1 : 0;
5978
5979                 var tab = this.statuses.Tabs[targetIndex];
5980                 var tabPage = this.ListTab.TabPages[targetIndex];
5981
5982                 this.ListTab.TabPages.Remove(tabPage);
5983
5984                 if (targetIndex < baseIndex)
5985                     baseIndex--;
5986
5987                 if (!isBeforeBaseTab)
5988                     baseIndex++;
5989
5990                 this.statuses.MoveTab(baseIndex, tab);
5991
5992                 this.ListTab.TabPages.Insert(baseIndex, tabPage);
5993             }
5994
5995             this.SaveConfigsTabs();
5996         }
5997
5998         private void MakeDirectMessageText()
5999         {
6000             var selectedPosts = this.CurrentTab.SelectedPosts;
6001             if (selectedPosts.Length > 1)
6002                 return;
6003
6004             var post = selectedPosts.Single();
6005             var text = $"D {post.ScreenName} {this.StatusText.Text}";
6006
6007             this.inReplyTo = null;
6008             this.StatusText.Text = text;
6009             this.StatusText.SelectionStart = text.Length;
6010             this.StatusText.Focus();
6011         }
6012
6013         private void MakeReplyText(bool atAll = false)
6014         {
6015             var selectedPosts = this.CurrentTab.SelectedPosts;
6016             if (selectedPosts.Any(x => x.IsDm))
6017             {
6018                 this.MakeDirectMessageText();
6019                 return;
6020             }
6021
6022             if (selectedPosts.Length == 1)
6023             {
6024                 var post = selectedPosts.Single();
6025                 var inReplyToStatusId = post.RetweetedId ?? post.StatusId;
6026                 var inReplyToScreenName = post.ScreenName;
6027                 this.inReplyTo = (inReplyToStatusId, inReplyToScreenName);
6028             }
6029             else
6030             {
6031                 this.inReplyTo = null;
6032             }
6033
6034             var selfScreenName = this.tw.Username;
6035             var targetScreenNames = new List<string>();
6036             foreach (var post in selectedPosts)
6037             {
6038                 if (post.ScreenName != selfScreenName)
6039                     targetScreenNames.Add(post.ScreenName);
6040
6041                 if (atAll)
6042                 {
6043                     foreach (var (_, screenName) in post.ReplyToList)
6044                     {
6045                         if (screenName != selfScreenName)
6046                             targetScreenNames.Add(screenName);
6047                     }
6048                 }
6049             }
6050
6051             if (this.inReplyTo != null)
6052             {
6053                 var (_, screenName) = this.inReplyTo.Value;
6054                 if (screenName == selfScreenName)
6055                     targetScreenNames.Insert(0, screenName);
6056             }
6057
6058             var text = this.StatusText.Text;
6059             foreach (var screenName in targetScreenNames.AsEnumerable().Reverse())
6060             {
6061                 var atText = $"@{screenName} ";
6062                 if (!text.Contains(atText))
6063                     text = atText + text;
6064             }
6065
6066             this.StatusText.Text = text;
6067             this.StatusText.SelectionStart = text.Length;
6068             this.StatusText.Focus();
6069         }
6070
6071         private void ListTab_MouseUp(object sender, MouseEventArgs e)
6072             => this.tabDrag = false;
6073
6074         private int iconCnt = 0;
6075         private int blinkCnt = 0;
6076         private bool blink = false;
6077
6078         private void RefreshTasktrayIcon()
6079         {
6080             void EnableTasktrayAnimation()
6081                 => this.TimerRefreshIcon.Enabled = true;
6082
6083             void DisableTasktrayAnimation()
6084                 => this.TimerRefreshIcon.Enabled = false;
6085
6086             var busyTasks = this.workerSemaphore.CurrentCount != MaxWorderThreads;
6087             if (busyTasks)
6088             {
6089                 this.iconCnt += 1;
6090                 if (this.iconCnt >= this.iconAssets.IconTrayRefresh.Length)
6091                     this.iconCnt = 0;
6092
6093                 this.NotifyIcon1.Icon = this.iconAssets.IconTrayRefresh[this.iconCnt];
6094                 this.myStatusError = false;
6095                 EnableTasktrayAnimation();
6096                 return;
6097             }
6098
6099             var replyIconType = this.settings.Common.ReplyIconState;
6100             var reply = false;
6101             if (replyIconType != MyCommon.REPLY_ICONSTATE.None)
6102             {
6103                 var replyTab = this.statuses.GetTabByType<MentionsTabModel>();
6104                 if (replyTab != null && replyTab.UnreadCount > 0)
6105                     reply = true;
6106             }
6107
6108             if (replyIconType == MyCommon.REPLY_ICONSTATE.BlinkIcon && reply)
6109             {
6110                 this.blinkCnt += 1;
6111                 if (this.blinkCnt > 10)
6112                     this.blinkCnt = 0;
6113
6114                 if (this.blinkCnt == 0)
6115                     this.blink = !this.blink;
6116
6117                 this.NotifyIcon1.Icon = this.blink ? this.iconAssets.IconTrayReplyBlink : this.iconAssets.IconTrayReply;
6118                 EnableTasktrayAnimation();
6119                 return;
6120             }
6121
6122             DisableTasktrayAnimation();
6123
6124             this.iconCnt = 0;
6125             this.blinkCnt = 0;
6126             this.blink = false;
6127
6128             // 優先度:リプライ→エラー→オフライン→アイドル
6129             // エラーは更新アイコンでクリアされる
6130             if (replyIconType == MyCommon.REPLY_ICONSTATE.StaticIcon && reply)
6131                 this.NotifyIcon1.Icon = this.iconAssets.IconTrayReply;
6132             else if (this.myStatusError)
6133                 this.NotifyIcon1.Icon = this.iconAssets.IconTrayError;
6134             else if (this.myStatusOnline)
6135                 this.NotifyIcon1.Icon = this.iconAssets.IconTray;
6136             else
6137                 this.NotifyIcon1.Icon = this.iconAssets.IconTrayOffline;
6138         }
6139
6140         private void TimerRefreshIcon_Tick(object sender, EventArgs e)
6141             => this.RefreshTasktrayIcon(); // 200ms
6142
6143         private void ContextMenuTabProperty_Opening(object sender, CancelEventArgs e)
6144         {
6145             // 右クリックの場合はタブ名が設定済。アプリケーションキーの場合は現在のタブを対象とする
6146             if (MyCommon.IsNullOrEmpty(this.rclickTabName) || sender != this.ContextMenuTabProperty)
6147                 this.rclickTabName = this.CurrentTabName;
6148
6149             if (this.statuses == null) return;
6150             if (this.statuses.Tabs == null) return;
6151
6152             if (!this.statuses.Tabs.TryGetValue(this.rclickTabName, out var tb))
6153                 return;
6154
6155             this.NotifyDispMenuItem.Checked = tb.Notify;
6156             this.NotifyTbMenuItem.Checked = tb.Notify;
6157
6158             this.soundfileListup = true;
6159             this.SoundFileComboBox.Items.Clear();
6160             this.SoundFileTbComboBox.Items.Clear();
6161             this.SoundFileComboBox.Items.Add("");
6162             this.SoundFileTbComboBox.Items.Add("");
6163             var oDir = new DirectoryInfo(Application.StartupPath + Path.DirectorySeparatorChar);
6164             if (Directory.Exists(Path.Combine(Application.StartupPath, "Sounds")))
6165             {
6166                 oDir = oDir.GetDirectories("Sounds")[0];
6167             }
6168             foreach (var oFile in oDir.GetFiles("*.wav"))
6169             {
6170                 this.SoundFileComboBox.Items.Add(oFile.Name);
6171                 this.SoundFileTbComboBox.Items.Add(oFile.Name);
6172             }
6173             var idx = this.SoundFileComboBox.Items.IndexOf(tb.SoundFile);
6174             if (idx == -1) idx = 0;
6175             this.SoundFileComboBox.SelectedIndex = idx;
6176             this.SoundFileTbComboBox.SelectedIndex = idx;
6177             this.soundfileListup = false;
6178             this.UreadManageMenuItem.Checked = tb.UnreadManage;
6179             this.UnreadMngTbMenuItem.Checked = tb.UnreadManage;
6180
6181             this.TabMenuControl(this.rclickTabName);
6182         }
6183
6184         private void TabMenuControl(string tabName)
6185         {
6186             var tabInfo = this.statuses.GetTabByName(tabName)!;
6187
6188             this.FilterEditMenuItem.Enabled = true;
6189             this.EditRuleTbMenuItem.Enabled = true;
6190
6191             if (tabInfo.IsDefaultTabType)
6192             {
6193                 this.ProtectTabMenuItem.Enabled = false;
6194                 this.ProtectTbMenuItem.Enabled = false;
6195             }
6196             else
6197             {
6198                 this.ProtectTabMenuItem.Enabled = true;
6199                 this.ProtectTbMenuItem.Enabled = true;
6200             }
6201
6202             if (tabInfo.IsDefaultTabType || tabInfo.Protected)
6203             {
6204                 this.ProtectTabMenuItem.Checked = true;
6205                 this.ProtectTbMenuItem.Checked = true;
6206                 this.DeleteTabMenuItem.Enabled = false;
6207                 this.DeleteTbMenuItem.Enabled = false;
6208             }
6209             else
6210             {
6211                 this.ProtectTabMenuItem.Checked = false;
6212                 this.ProtectTbMenuItem.Checked = false;
6213                 this.DeleteTabMenuItem.Enabled = true;
6214                 this.DeleteTbMenuItem.Enabled = true;
6215             }
6216         }
6217
6218         private void ProtectTabMenuItem_Click(object sender, EventArgs e)
6219         {
6220             var checkState = ((ToolStripMenuItem)sender).Checked;
6221
6222             // チェック状態を同期
6223             this.ProtectTbMenuItem.Checked = checkState;
6224             this.ProtectTabMenuItem.Checked = checkState;
6225
6226             // ロック中はタブの削除を無効化
6227             this.DeleteTabMenuItem.Enabled = !checkState;
6228             this.DeleteTbMenuItem.Enabled = !checkState;
6229
6230             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
6231             this.statuses.Tabs[this.rclickTabName].Protected = checkState;
6232
6233             this.SaveConfigsTabs();
6234         }
6235
6236         private void UreadManageMenuItem_Click(object sender, EventArgs e)
6237         {
6238             this.UreadManageMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
6239             this.UnreadMngTbMenuItem.Checked = this.UreadManageMenuItem.Checked;
6240
6241             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
6242             this.ChangeTabUnreadManage(this.rclickTabName, this.UreadManageMenuItem.Checked);
6243
6244             this.SaveConfigsTabs();
6245         }
6246
6247         public void ChangeTabUnreadManage(string tabName, bool isManage)
6248         {
6249             var idx = this.GetTabPageIndex(tabName);
6250             if (idx == -1)
6251                 return;
6252
6253             var tab = this.statuses.Tabs[tabName];
6254             tab.UnreadManage = isManage;
6255
6256             if (this.settings.Common.TabIconDisp)
6257             {
6258                 var tabPage = this.ListTab.TabPages[idx];
6259                 if (tab.UnreadCount > 0)
6260                     tabPage.ImageIndex = 0;
6261                 else
6262                     tabPage.ImageIndex = -1;
6263             }
6264
6265             if (this.CurrentTabName == tabName)
6266             {
6267                 this.listCache?.PurgeCache();
6268                 this.CurrentListView.Refresh();
6269             }
6270
6271             this.SetMainWindowTitle();
6272             this.SetStatusLabelUrl();
6273             if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh();
6274         }
6275
6276         private void NotifyDispMenuItem_Click(object sender, EventArgs e)
6277         {
6278             this.NotifyDispMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
6279             this.NotifyTbMenuItem.Checked = this.NotifyDispMenuItem.Checked;
6280
6281             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
6282
6283             this.statuses.Tabs[this.rclickTabName].Notify = this.NotifyDispMenuItem.Checked;
6284
6285             this.SaveConfigsTabs();
6286         }
6287
6288         private void SoundFileComboBox_SelectedIndexChanged(object sender, EventArgs e)
6289         {
6290             if (this.soundfileListup || MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
6291
6292             this.statuses.Tabs[this.rclickTabName].SoundFile = (string)((ToolStripComboBox)sender).SelectedItem;
6293
6294             this.SaveConfigsTabs();
6295         }
6296
6297         private void DeleteTabMenuItem_Click(object sender, EventArgs e)
6298         {
6299             if (MyCommon.IsNullOrEmpty(this.rclickTabName) || sender == this.DeleteTbMenuItem)
6300                 this.rclickTabName = this.CurrentTabName;
6301
6302             this.RemoveSpecifiedTab(this.rclickTabName, true);
6303             this.SaveConfigsTabs();
6304         }
6305
6306         private void FilterEditMenuItem_Click(object sender, EventArgs e)
6307         {
6308             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) this.rclickTabName = this.statuses.HomeTab.TabName;
6309
6310             using (var fltDialog = new FilterDialog())
6311             {
6312                 fltDialog.Owner = this;
6313                 fltDialog.SetCurrent(this.rclickTabName);
6314                 fltDialog.ShowDialog(this);
6315             }
6316             this.TopMost = this.settings.Common.AlwaysTop;
6317
6318             this.ApplyPostFilters();
6319             this.SaveConfigsTabs();
6320         }
6321
6322         private async void AddTabMenuItem_Click(object sender, EventArgs e)
6323         {
6324             string? tabName = null;
6325             MyCommon.TabUsageType tabUsage;
6326             using (var inputName = new InputTabName())
6327             {
6328                 inputName.TabName = this.statuses.MakeTabName("MyTab");
6329                 inputName.IsShowUsage = true;
6330                 inputName.ShowDialog();
6331                 if (inputName.DialogResult == DialogResult.Cancel) return;
6332                 tabName = inputName.TabName;
6333                 tabUsage = inputName.Usage;
6334             }
6335             this.TopMost = this.settings.Common.AlwaysTop;
6336             if (!MyCommon.IsNullOrEmpty(tabName))
6337             {
6338                 // List対応
6339                 ListElement? list = null;
6340                 if (tabUsage == MyCommon.TabUsageType.Lists)
6341                 {
6342                     using var listAvail = new ListAvailable();
6343                     if (listAvail.ShowDialog(this) == DialogResult.Cancel)
6344                         return;
6345                     if (listAvail.SelectedList == null)
6346                         return;
6347                     list = listAvail.SelectedList;
6348                 }
6349
6350                 TabModel tab;
6351                 switch (tabUsage)
6352                 {
6353                     case MyCommon.TabUsageType.UserDefined:
6354                         tab = new FilterTabModel(tabName);
6355                         break;
6356                     case MyCommon.TabUsageType.PublicSearch:
6357                         tab = new PublicSearchTabModel(tabName);
6358                         break;
6359                     case MyCommon.TabUsageType.Lists:
6360                         tab = new ListTimelineTabModel(tabName, list!);
6361                         break;
6362                     default:
6363                         return;
6364                 }
6365
6366                 if (!this.statuses.AddTab(tab) || !this.AddNewTab(tab, startup: false))
6367                 {
6368                     var tmp = string.Format(Properties.Resources.AddTabMenuItem_ClickText1, tabName);
6369                     MessageBox.Show(tmp, Properties.Resources.AddTabMenuItem_ClickText2, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
6370                 }
6371                 else
6372                 {
6373                     // 成功
6374                     this.SaveConfigsTabs();
6375
6376                     var tabIndex = this.statuses.Tabs.Count - 1;
6377
6378                     if (tabUsage == MyCommon.TabUsageType.PublicSearch)
6379                     {
6380                         this.ListTab.SelectedIndex = tabIndex;
6381                         this.CurrentTabPage.Controls["panelSearch"].Controls["comboSearch"].Focus();
6382                     }
6383                     if (tabUsage == MyCommon.TabUsageType.Lists)
6384                     {
6385                         this.ListTab.SelectedIndex = tabIndex;
6386                         await this.RefreshTabAsync(this.CurrentTab);
6387                     }
6388                 }
6389             }
6390         }
6391
6392         private void TabMenuItem_Click(object sender, EventArgs e)
6393         {
6394             // 選択発言を元にフィルタ追加
6395             foreach (var post in this.CurrentTab.SelectedPosts)
6396             {
6397                 // タブ選択(or追加)
6398                 if (!this.SelectTab(out var tab))
6399                     return;
6400
6401                 using (var fltDialog = new FilterDialog())
6402                 {
6403                     fltDialog.Owner = this;
6404                     fltDialog.SetCurrent(tab.TabName);
6405
6406                     if (post.RetweetedBy == null)
6407                     {
6408                         fltDialog.AddNewFilter(post.ScreenName, post.TextFromApi);
6409                     }
6410                     else
6411                     {
6412                         fltDialog.AddNewFilter(post.RetweetedBy, post.TextFromApi);
6413                     }
6414                     fltDialog.ShowDialog(this);
6415                 }
6416
6417                 this.TopMost = this.settings.Common.AlwaysTop;
6418             }
6419
6420             this.ApplyPostFilters();
6421             this.SaveConfigsTabs();
6422         }
6423
6424         protected override bool ProcessDialogKey(Keys keyData)
6425         {
6426             // TextBox1でEnterを押してもビープ音が鳴らないようにする
6427             if ((keyData & Keys.KeyCode) == Keys.Enter)
6428             {
6429                 if (this.StatusText.Focused)
6430                 {
6431                     var newLine = false;
6432                     var post = false;
6433
6434                     if (this.settings.Common.PostCtrlEnter) // Ctrl+Enter投稿時
6435                     {
6436                         if (this.StatusText.Multiline)
6437                         {
6438                             if ((keyData & Keys.Shift) == Keys.Shift && (keyData & Keys.Control) != Keys.Control) newLine = true;
6439
6440                             if ((keyData & Keys.Control) == Keys.Control) post = true;
6441                         }
6442                         else
6443                         {
6444                             if ((keyData & Keys.Control) == Keys.Control) post = true;
6445                         }
6446                     }
6447                     else if (this.settings.Common.PostShiftEnter) // SHift+Enter投稿時
6448                     {
6449                         if (this.StatusText.Multiline)
6450                         {
6451                             if ((keyData & Keys.Control) == Keys.Control && (keyData & Keys.Shift) != Keys.Shift) newLine = true;
6452
6453                             if ((keyData & Keys.Shift) == Keys.Shift) post = true;
6454                         }
6455                         else
6456                         {
6457                             if ((keyData & Keys.Shift) == Keys.Shift) post = true;
6458                         }
6459                     }
6460                     else // Enter投稿時
6461                     {
6462                         if (this.StatusText.Multiline)
6463                         {
6464                             if ((keyData & Keys.Shift) == Keys.Shift && (keyData & Keys.Control) != Keys.Control) newLine = true;
6465
6466                             if (((keyData & Keys.Control) != Keys.Control && (keyData & Keys.Shift) != Keys.Shift) ||
6467                                 ((keyData & Keys.Control) == Keys.Control && (keyData & Keys.Shift) == Keys.Shift)) post = true;
6468                         }
6469                         else
6470                         {
6471                             if (((keyData & Keys.Shift) == Keys.Shift) ||
6472                                 (((keyData & Keys.Control) != Keys.Control) &&
6473                                 ((keyData & Keys.Shift) != Keys.Shift))) post = true;
6474                         }
6475                     }
6476
6477                     if (newLine)
6478                     {
6479                         var pos1 = this.StatusText.SelectionStart;
6480                         if (this.StatusText.SelectionLength > 0)
6481                         {
6482                             this.StatusText.Text = this.StatusText.Text.Remove(pos1, this.StatusText.SelectionLength);  // 選択状態文字列削除
6483                         }
6484                         this.StatusText.Text = this.StatusText.Text.Insert(pos1, Environment.NewLine);  // 改行挿入
6485                         this.StatusText.SelectionStart = pos1 + Environment.NewLine.Length;    // カーソルを改行の次の文字へ移動
6486                         return true;
6487                     }
6488                     else if (post)
6489                     {
6490                         this.PostButton_Click(this.PostButton, EventArgs.Empty);
6491                         return true;
6492                     }
6493                 }
6494                 else
6495                 {
6496                     var tab = this.CurrentTab;
6497                     if (tab.TabType == MyCommon.TabUsageType.PublicSearch)
6498                     {
6499                         var tabPage = this.CurrentTabPage;
6500                         if (tabPage.Controls["panelSearch"].Controls["comboSearch"].Focused ||
6501                             tabPage.Controls["panelSearch"].Controls["comboLang"].Focused)
6502                         {
6503                             this.SearchButton_Click(tabPage.Controls["panelSearch"].Controls["comboSearch"], EventArgs.Empty);
6504                             return true;
6505                         }
6506                     }
6507                 }
6508             }
6509
6510             return base.ProcessDialogKey(keyData);
6511         }
6512
6513         private void ReplyAllStripMenuItem_Click(object sender, EventArgs e)
6514             => this.MakeReplyText(atAll: true);
6515
6516         private void IDRuleMenuItem_Click(object sender, EventArgs e)
6517         {
6518             var tab = this.CurrentTab;
6519             var selectedPosts = tab.SelectedPosts;
6520
6521             // 未選択なら処理終了
6522             if (selectedPosts.Length == 0)
6523                 return;
6524
6525             var screenNameArray = selectedPosts
6526                 .Select(x => x.RetweetedBy ?? x.ScreenName)
6527                 .ToArray();
6528
6529             this.AddFilterRuleByScreenName(screenNameArray);
6530
6531             if (screenNameArray.Length != 0)
6532             {
6533                 var atids = new List<string>();
6534                 foreach (var screenName in screenNameArray)
6535                 {
6536                     atids.Add("@" + screenName);
6537                 }
6538                 var cnt = this.AtIdSupl.ItemCount;
6539                 this.AtIdSupl.AddRangeItem(atids.ToArray());
6540                 if (this.AtIdSupl.ItemCount != cnt)
6541                     this.MarkSettingAtIdModified();
6542             }
6543         }
6544
6545         private void SourceRuleMenuItem_Click(object sender, EventArgs e)
6546         {
6547             var tab = this.CurrentTab;
6548             var selectedPosts = tab.SelectedPosts;
6549
6550             if (selectedPosts.Length == 0)
6551                 return;
6552
6553             var sourceArray = selectedPosts.Select(x => x.Source).ToArray();
6554
6555             this.AddFilterRuleBySource(sourceArray);
6556         }
6557
6558         public void AddFilterRuleByScreenName(params string[] screenNameArray)
6559         {
6560             // タブ選択(or追加)
6561             if (!this.SelectTab(out var tab)) return;
6562
6563             bool mv;
6564             bool mk;
6565             if (tab.TabType != MyCommon.TabUsageType.Mute)
6566             {
6567                 this.MoveOrCopy(out mv, out mk);
6568             }
6569             else
6570             {
6571                 // ミュートタブでは常に MoveMatches を true にする
6572                 mv = true;
6573                 mk = false;
6574             }
6575
6576             foreach (var screenName in screenNameArray)
6577             {
6578                 tab.AddFilter(new PostFilterRule
6579                 {
6580                     FilterName = screenName,
6581                     UseNameField = true,
6582                     MoveMatches = mv,
6583                     MarkMatches = mk,
6584                     UseRegex = false,
6585                     FilterByUrl = false,
6586                 });
6587             }
6588
6589             this.ApplyPostFilters();
6590             this.SaveConfigsTabs();
6591         }
6592
6593         public void AddFilterRuleBySource(params string[] sourceArray)
6594         {
6595             // タブ選択ダイアログを表示(or追加)
6596             if (!this.SelectTab(out var filterTab))
6597                 return;
6598
6599             bool mv;
6600             bool mk;
6601             if (filterTab.TabType != MyCommon.TabUsageType.Mute)
6602             {
6603                 // フィルタ動作選択ダイアログを表示(移動/コピー, マーク有無)
6604                 this.MoveOrCopy(out mv, out mk);
6605             }
6606             else
6607             {
6608                 // ミュートタブでは常に MoveMatches を true にする
6609                 mv = true;
6610                 mk = false;
6611             }
6612
6613             // 振り分けルールに追加するSource
6614             foreach (var source in sourceArray)
6615             {
6616                 filterTab.AddFilter(new PostFilterRule
6617                 {
6618                     FilterSource = source,
6619                     MoveMatches = mv,
6620                     MarkMatches = mk,
6621                     UseRegex = false,
6622                     FilterByUrl = false,
6623                 });
6624             }
6625
6626             this.ApplyPostFilters();
6627             this.SaveConfigsTabs();
6628         }
6629
6630         private bool SelectTab([NotNullWhen(true)] out FilterTabModel? tab)
6631         {
6632             do
6633             {
6634                 tab = null;
6635
6636                 // 振り分け先タブ選択
6637                 using (var dialog = new TabsDialog(this.statuses))
6638                 {
6639                     if (dialog.ShowDialog(this) == DialogResult.Cancel) return false;
6640
6641                     tab = dialog.SelectedTab;
6642                 }
6643
6644                 this.CurrentTabPage.Focus();
6645                 // 新規タブを選択→タブ作成
6646                 if (tab == null)
6647                 {
6648                     string tabName;
6649                     using (var inputName = new InputTabName())
6650                     {
6651                         inputName.TabName = this.statuses.MakeTabName("MyTab");
6652                         inputName.ShowDialog();
6653                         if (inputName.DialogResult == DialogResult.Cancel) return false;
6654                         tabName = inputName.TabName;
6655                     }
6656                     this.TopMost = this.settings.Common.AlwaysTop;
6657                     if (!MyCommon.IsNullOrEmpty(tabName))
6658                     {
6659                         var newTab = new FilterTabModel(tabName);
6660                         if (!this.statuses.AddTab(newTab) || !this.AddNewTab(newTab, startup: false))
6661                         {
6662                             var tmp = string.Format(Properties.Resources.IDRuleMenuItem_ClickText2, tabName);
6663                             MessageBox.Show(tmp, Properties.Resources.IDRuleMenuItem_ClickText3, MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
6664                             // もう一度タブ名入力
6665                         }
6666                         else
6667                         {
6668                             tab = newTab;
6669                             return true;
6670                         }
6671                     }
6672                 }
6673                 else
6674                 {
6675                     // 既存タブを選択
6676                     return true;
6677                 }
6678             }
6679             while (true);
6680         }
6681
6682         private void MoveOrCopy(out bool move, out bool mark)
6683         {
6684             {
6685                 // 移動するか?
6686                 var tmp = string.Format(Properties.Resources.IDRuleMenuItem_ClickText4, Environment.NewLine);
6687                 if (MessageBox.Show(tmp, Properties.Resources.IDRuleMenuItem_ClickText5, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
6688                     move = false;
6689                 else
6690                     move = true;
6691             }
6692             if (!move)
6693             {
6694                 // マークするか?
6695                 var tmp = string.Format(Properties.Resources.IDRuleMenuItem_ClickText6, Environment.NewLine);
6696                 if (MessageBox.Show(tmp, Properties.Resources.IDRuleMenuItem_ClickText7, MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
6697                     mark = true;
6698                 else
6699                     mark = false;
6700             }
6701             else
6702             {
6703                 mark = false;
6704             }
6705         }
6706
6707         private void CopySTOTMenuItem_Click(object sender, EventArgs e)
6708             => this.CopyStot();
6709
6710         private void CopyURLMenuItem_Click(object sender, EventArgs e)
6711             => this.CopyIdUri();
6712
6713         private void SelectAllMenuItem_Click(object sender, EventArgs e)
6714         {
6715             if (this.StatusText.Focused)
6716             {
6717                 // 発言欄でのCtrl+A
6718                 this.StatusText.SelectAll();
6719             }
6720             else
6721             {
6722                 // ListView上でのCtrl+A
6723                 NativeMethods.SelectAllItems(this.CurrentListView);
6724             }
6725         }
6726
6727         private void MoveMiddle()
6728         {
6729             ListViewItem item;
6730             int idx1;
6731             int idx2;
6732
6733             var listView = this.CurrentListView;
6734             if (listView.SelectedIndices.Count == 0) return;
6735
6736             var idx = listView.SelectedIndices[0];
6737
6738             item = listView.GetItemAt(0, 25);
6739             if (item == null)
6740                 idx1 = 0;
6741             else
6742                 idx1 = item.Index;
6743
6744             item = listView.GetItemAt(0, listView.ClientSize.Height - 1);
6745             if (item == null)
6746                 idx2 = listView.VirtualListSize - 1;
6747             else
6748                 idx2 = item.Index;
6749
6750             idx -= Math.Abs(idx1 - idx2) / 2;
6751             if (idx < 0) idx = 0;
6752
6753             listView.EnsureVisible(listView.VirtualListSize - 1);
6754             listView.EnsureVisible(idx);
6755         }
6756
6757         private async void OpenURLMenuItem_Click(object sender, EventArgs e)
6758         {
6759             var linkElements = this.tweetDetailsView.GetLinkElements();
6760
6761             if (linkElements.Length == 0)
6762                 return;
6763
6764             var links = new List<OpenUrlItem>(linkElements.Length);
6765
6766             foreach (var linkElm in linkElements)
6767             {
6768                 var displayUrl = linkElm.GetAttribute("title");
6769                 var href = linkElm.GetAttribute("href");
6770                 var linkedText = linkElm.InnerText;
6771
6772                 if (MyCommon.IsNullOrEmpty(displayUrl))
6773                     displayUrl = href;
6774
6775                 links.Add(new OpenUrlItem(linkedText, displayUrl, href));
6776             }
6777
6778             string selectedUrl;
6779             bool isReverseSettings;
6780
6781             if (links.Count == 1)
6782             {
6783                 // ツイートに含まれる URL が 1 つのみの場合
6784                 //   => OpenURL ダイアログを表示せずにリンクを開く
6785                 selectedUrl = links[0].Href;
6786
6787                 // Ctrl+E で呼ばれた場合を考慮し isReverseSettings の判定を行わない
6788                 isReverseSettings = false;
6789             }
6790             else
6791             {
6792                 // ツイートに含まれる URL が複数ある場合
6793                 //   => OpenURL を表示しユーザーが選択したリンクを開く
6794                 this.urlDialog.ClearUrl();
6795
6796                 foreach (var link in links)
6797                     this.urlDialog.AddUrl(link);
6798
6799                 if (this.urlDialog.ShowDialog(this) != DialogResult.OK)
6800                     return;
6801
6802                 this.TopMost = this.settings.Common.AlwaysTop;
6803
6804                 selectedUrl = this.urlDialog.SelectedUrl;
6805
6806                 // Ctrlを押しながらリンクを開いた場合は、設定と逆の動作をするフラグを true としておく
6807                 isReverseSettings = MyCommon.IsKeyDown(Keys.Control);
6808             }
6809
6810             await this.OpenUriAsync(new Uri(selectedUrl), isReverseSettings);
6811         }
6812
6813         private void ClearTabMenuItem_Click(object sender, EventArgs e)
6814         {
6815             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
6816             this.ClearTab(this.rclickTabName, true);
6817         }
6818
6819         private void ClearTab(string tabName, bool showWarning)
6820         {
6821             if (showWarning)
6822             {
6823                 var tmp = string.Format(Properties.Resources.ClearTabMenuItem_ClickText1, Environment.NewLine);
6824                 if (MessageBox.Show(tmp, tabName + " " + Properties.Resources.ClearTabMenuItem_ClickText2, MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.Cancel)
6825                 {
6826                     return;
6827                 }
6828             }
6829
6830             this.statuses.ClearTabIds(tabName);
6831             if (this.CurrentTabName == tabName)
6832             {
6833                 this.CurrentTab.ClearAnchor();
6834                 this.listCache?.PurgeCache();
6835                 this.listCache?.UpdateListSize();
6836             }
6837
6838             var tabIndex = this.statuses.Tabs.IndexOf(tabName);
6839             var tabPage = this.ListTab.TabPages[tabIndex];
6840             tabPage.ImageIndex = -1;
6841
6842             if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh();
6843
6844             this.SetMainWindowTitle();
6845             this.SetStatusLabelUrl();
6846         }
6847
6848         private static long followers = 0;
6849
6850         private void SetMainWindowTitle()
6851         {
6852             // メインウインドウタイトルの書き換え
6853             var ttl = new StringBuilder(256);
6854             var ur = 0;
6855             var al = 0;
6856             if (this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.None &&
6857                 this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.Post &&
6858                 this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.Ver &&
6859                 this.settings.Common.DispLatestPost != MyCommon.DispTitleEnum.OwnStatus)
6860             {
6861                 foreach (var tab in this.statuses.Tabs)
6862                 {
6863                     ur += tab.UnreadCount;
6864                     al += tab.AllCount;
6865                 }
6866             }
6867
6868             if (this.settings.Common.DispUsername) ttl.Append(this.tw.Username).Append(" - ");
6869             ttl.Append(ApplicationSettings.ApplicationName);
6870             ttl.Append("  ");
6871             switch (this.settings.Common.DispLatestPost)
6872             {
6873                 case MyCommon.DispTitleEnum.Ver:
6874                     ttl.Append("Ver:").Append(MyCommon.GetReadableVersion());
6875                     break;
6876                 case MyCommon.DispTitleEnum.Post:
6877                     if (this.history.Peek() is { } lastItem)
6878                         ttl.Append(lastItem.Status.Replace("\r\n", " "));
6879                     break;
6880                 case MyCommon.DispTitleEnum.UnreadRepCount:
6881                     ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText1, this.statuses.MentionTab.UnreadCount + this.statuses.DirectMessageTab.UnreadCount);
6882                     break;
6883                 case MyCommon.DispTitleEnum.UnreadAllCount:
6884                     ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText2, ur);
6885                     break;
6886                 case MyCommon.DispTitleEnum.UnreadAllRepCount:
6887                     ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText3, ur, this.statuses.MentionTab.UnreadCount + this.statuses.DirectMessageTab.UnreadCount);
6888                     break;
6889                 case MyCommon.DispTitleEnum.UnreadCountAllCount:
6890                     ttl.AppendFormat(Properties.Resources.SetMainWindowTitleText4, ur, al);
6891                     break;
6892                 case MyCommon.DispTitleEnum.OwnStatus:
6893                     if (followers == 0 && this.tw.FollowersCount > 0) followers = this.tw.FollowersCount;
6894                     ttl.AppendFormat(Properties.Resources.OwnStatusTitle, this.tw.StatusesCount, this.tw.FriendsCount, this.tw.FollowersCount, this.tw.FollowersCount - followers);
6895                     break;
6896             }
6897
6898             try
6899             {
6900                 this.Text = ttl.ToString();
6901             }
6902             catch (AccessViolationException)
6903             {
6904                 // 原因不明。ポスト内容に依存か?たまーに発生するが再現せず。
6905             }
6906         }
6907
6908         private string GetStatusLabelText()
6909         {
6910             // ステータス欄にカウント表示
6911             // タブ未読数/タブ発言数 全未読数/総発言数 (未読@+未読DM数)
6912             if (this.statuses == null) return "";
6913             var tbRep = this.statuses.MentionTab;
6914             var tbDm = this.statuses.DirectMessageTab;
6915             if (tbRep == null || tbDm == null) return "";
6916             var urat = tbRep.UnreadCount + tbDm.UnreadCount;
6917             var ur = 0;
6918             var al = 0;
6919             var tur = 0;
6920             var tal = 0;
6921             var slbl = new StringBuilder(256);
6922             try
6923             {
6924                 foreach (var tab in this.statuses.Tabs)
6925                 {
6926                     ur += tab.UnreadCount;
6927                     al += tab.AllCount;
6928                     if (tab.TabName == this.CurrentTabName)
6929                     {
6930                         tur = tab.UnreadCount;
6931                         tal = tab.AllCount;
6932                     }
6933                 }
6934             }
6935             catch (Exception)
6936             {
6937                 return "";
6938             }
6939
6940             this.unreadCounter = ur;
6941             this.unreadAtCounter = urat;
6942
6943             var homeTab = this.statuses.HomeTab;
6944
6945             slbl.AppendFormat(Properties.Resources.SetStatusLabelText1, tur, tal, ur, al, urat, this.postTimestamps.Count, this.favTimestamps.Count, homeTab.TweetsPerHour);
6946             if (this.settings.Common.TimelinePeriod == 0)
6947             {
6948                 slbl.Append(Properties.Resources.SetStatusLabelText2);
6949             }
6950             else
6951             {
6952                 slbl.Append(this.settings.Common.TimelinePeriod + Properties.Resources.SetStatusLabelText3);
6953             }
6954             return slbl.ToString();
6955         }
6956
6957         private async void TwitterApiStatus_AccessLimitUpdated(object sender, EventArgs e)
6958         {
6959             try
6960             {
6961                 if (this.InvokeRequired && !this.IsDisposed)
6962                 {
6963                     await this.InvokeAsync(() => this.TwitterApiStatus_AccessLimitUpdated(sender, e));
6964                 }
6965                 else
6966                 {
6967                     var endpointName = ((TwitterApiStatus.AccessLimitUpdatedEventArgs)e).EndpointName;
6968                     this.SetApiStatusLabel(endpointName);
6969                 }
6970             }
6971             catch (ObjectDisposedException)
6972             {
6973                 return;
6974             }
6975             catch (InvalidOperationException)
6976             {
6977                 return;
6978             }
6979         }
6980
6981         private void SetApiStatusLabel(string? endpointName = null)
6982         {
6983             var tabType = this.CurrentTab.TabType;
6984
6985             if (endpointName == null)
6986             {
6987                 var authByCookie = this.tw.Api.AuthType == APIAuthType.TwitterComCookie;
6988
6989                 // 表示中のタブに応じて更新
6990                 endpointName = tabType switch
6991                 {
6992                     MyCommon.TabUsageType.Home => "/statuses/home_timeline",
6993                     MyCommon.TabUsageType.UserDefined => "/statuses/home_timeline",
6994                     MyCommon.TabUsageType.Mentions => "/statuses/mentions_timeline",
6995                     MyCommon.TabUsageType.Favorites => "/favorites/list",
6996                     MyCommon.TabUsageType.DirectMessage => "/direct_messages/events/list",
6997                     MyCommon.TabUsageType.UserTimeline =>
6998                         authByCookie ? UserTweetsAndRepliesRequest.EndpointName : "/statuses/user_timeline",
6999                     MyCommon.TabUsageType.Lists =>
7000                         authByCookie ? ListLatestTweetsTimelineRequest.EndpointName : "/lists/statuses",
7001                     MyCommon.TabUsageType.PublicSearch =>
7002                         authByCookie ? SearchTimelineRequest.EndpointName : "/search/tweets",
7003                     MyCommon.TabUsageType.Related => "/statuses/show/:id",
7004                     _ => null,
7005                 };
7006                 this.toolStripApiGauge.ApiEndpoint = endpointName;
7007             }
7008             else
7009             {
7010                 var currentEndpointName = this.toolStripApiGauge.ApiEndpoint;
7011                 this.toolStripApiGauge.ApiEndpoint = currentEndpointName;
7012             }
7013         }
7014
7015         private void SetStatusLabelUrl()
7016             => this.StatusLabelUrl.Text = this.GetStatusLabelText();
7017
7018         public void SetStatusLabel(string text)
7019             => this.StatusLabel.Text = text;
7020
7021         private void SetNotifyIconText()
7022         {
7023             var ur = new StringBuilder(64);
7024
7025             // タスクトレイアイコンのツールチップテキスト書き換え
7026             // Tween [未読/@]
7027             ur.Remove(0, ur.Length);
7028             if (this.settings.Common.DispUsername)
7029             {
7030                 ur.Append(this.tw.Username);
7031                 ur.Append(" - ");
7032             }
7033             ur.Append(ApplicationSettings.ApplicationName);
7034 #if DEBUG
7035             ur.Append("(Debug Build)");
7036 #endif
7037             if (this.unreadCounter != -1 && this.unreadAtCounter != -1)
7038             {
7039                 ur.Append(" [");
7040                 ur.Append(this.unreadCounter);
7041                 ur.Append("/@");
7042                 ur.Append(this.unreadAtCounter);
7043                 ur.Append("]");
7044             }
7045             this.NotifyIcon1.Text = ur.ToString();
7046         }
7047
7048         internal void CheckReplyTo(string statusText)
7049         {
7050             MatchCollection m;
7051             // ハッシュタグの保存
7052             m = Regex.Matches(statusText, Twitter.Hashtag, RegexOptions.IgnoreCase);
7053             var hstr = "";
7054             foreach (Match hm in m)
7055             {
7056                 if (!hstr.Contains("#" + hm.Result("$3") + " "))
7057                 {
7058                     hstr += "#" + hm.Result("$3") + " ";
7059                     this.HashSupl.AddItem("#" + hm.Result("$3"));
7060                 }
7061             }
7062             if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash) && !hstr.Contains(this.HashMgr.UseHash + " "))
7063             {
7064                 hstr += this.HashMgr.UseHash;
7065             }
7066             if (!MyCommon.IsNullOrEmpty(hstr)) this.HashMgr.AddHashToHistory(hstr.Trim(), false);
7067
7068             // 本当にリプライ先指定すべきかどうかの判定
7069             m = Regex.Matches(statusText, "(^|[ -/:-@[-^`{-~])(?<id>@[a-zA-Z0-9_]+)");
7070
7071             if (this.settings.Common.UseAtIdSupplement)
7072             {
7073                 var bCnt = this.AtIdSupl.ItemCount;
7074                 foreach (Match mid in m)
7075                 {
7076                     this.AtIdSupl.AddItem(mid.Result("${id}"));
7077                 }
7078                 if (bCnt != this.AtIdSupl.ItemCount)
7079                     this.MarkSettingAtIdModified();
7080             }
7081
7082             // リプライ先ステータスIDの指定がない場合は指定しない
7083             if (this.inReplyTo == null)
7084                 return;
7085
7086             // 通常Reply
7087             // 次の条件を満たす場合に in_reply_to_status_id 指定
7088             // 1. Twitterによりリンクと判定される @idが文中に1つ含まれる (2009/5/28 リンク化される@IDのみカウントするように修正)
7089             // 2. リプライ先ステータスIDが設定されている(リストをダブルクリックで返信している)
7090             // 3. 文中に含まれた@idがリプライ先のポスト者のIDと一致する
7091
7092             if (m != null)
7093             {
7094                 var inReplyToScreenName = this.inReplyTo.Value.ScreenName;
7095                 if (statusText.StartsWith("@", StringComparison.Ordinal))
7096                 {
7097                     if (statusText.StartsWith("@" + inReplyToScreenName, StringComparison.Ordinal)) return;
7098                 }
7099                 else
7100                 {
7101                     foreach (Match mid in m)
7102                     {
7103                         if (statusText.Contains("RT " + mid.Result("${id}") + ":") && mid.Result("${id}") == "@" + inReplyToScreenName) return;
7104                     }
7105                 }
7106             }
7107
7108             this.inReplyTo = null;
7109         }
7110
7111         private void TweenMain_Resize(object sender, EventArgs e)
7112         {
7113             if (!this.initialLayout && this.settings.Common.MinimizeToTray && this.WindowState == FormWindowState.Minimized)
7114             {
7115                 this.Visible = false;
7116             }
7117             if (this.WindowState != FormWindowState.Minimized)
7118             {
7119                 this.formWindowState = this.WindowState;
7120             }
7121         }
7122
7123         private void ApplyLayoutFromSettings()
7124         {
7125             // 現在の DPI と設定保存時の DPI との比を取得する
7126             var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions);
7127
7128             this.ClientSize = ScaleBy(configScaleFactor, this.settings.Local.FormSize);
7129
7130             // Splitterの位置設定
7131             var splitterDistance = ScaleBy(configScaleFactor.Height, this.settings.Local.SplitterDistance);
7132             if (splitterDistance > this.SplitContainer1.Panel1MinSize &&
7133                 splitterDistance < this.SplitContainer1.Height - this.SplitContainer1.Panel2MinSize - this.SplitContainer1.SplitterWidth)
7134             {
7135                 this.SplitContainer1.SplitterDistance = splitterDistance;
7136             }
7137
7138             // 発言欄複数行
7139             this.StatusText.Multiline = this.settings.Local.StatusMultiline;
7140             if (this.StatusText.Multiline)
7141             {
7142                 var statusTextHeight = ScaleBy(configScaleFactor.Height, this.settings.Local.StatusTextHeight);
7143                 var dis = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth;
7144                 if (dis > this.SplitContainer2.Panel1MinSize && dis < this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth)
7145                 {
7146                     this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - statusTextHeight - this.SplitContainer2.SplitterWidth;
7147                 }
7148                 this.StatusText.Height = statusTextHeight;
7149             }
7150             else
7151             {
7152                 if (this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth > 0)
7153                 {
7154                     this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth;
7155                 }
7156             }
7157
7158             var previewDistance = ScaleBy(configScaleFactor.Width, this.settings.Local.PreviewDistance);
7159             if (previewDistance > this.SplitContainer3.Panel1MinSize && previewDistance < this.SplitContainer3.Width - this.SplitContainer3.Panel2MinSize - this.SplitContainer3.SplitterWidth)
7160             {
7161                 this.SplitContainer3.SplitterDistance = previewDistance;
7162             }
7163
7164             // Panel2Collapsed は SplitterDistance の設定を終えるまで true にしない
7165             this.SplitContainer3.Panel2Collapsed = true;
7166             this.initialLayout = false;
7167         }
7168
7169         private void PlaySoundMenuItem_CheckedChanged(object sender, EventArgs e)
7170         {
7171             this.PlaySoundMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
7172             this.PlaySoundFileMenuItem.Checked = this.PlaySoundMenuItem.Checked;
7173             if (this.PlaySoundMenuItem.Checked)
7174             {
7175                 this.settings.Common.PlaySound = true;
7176             }
7177             else
7178             {
7179                 this.settings.Common.PlaySound = false;
7180             }
7181             this.MarkSettingCommonModified();
7182         }
7183
7184         private void SplitContainer1_SplitterMoved(object sender, SplitterEventArgs e)
7185         {
7186             if (this.initialLayout)
7187                 return;
7188
7189             int splitterDistance;
7190             switch (this.WindowState)
7191             {
7192                 case FormWindowState.Normal:
7193                     splitterDistance = this.SplitContainer1.SplitterDistance;
7194                     break;
7195                 case FormWindowState.Maximized:
7196                     // 最大化時は、通常時のウィンドウサイズに換算した SplitterDistance を算出する
7197                     var normalContainerHeight = this.mySize.Height - this.ToolStripContainer1.TopToolStripPanel.Height - this.ToolStripContainer1.BottomToolStripPanel.Height;
7198                     splitterDistance = this.SplitContainer1.SplitterDistance - (this.SplitContainer1.Height - normalContainerHeight);
7199                     splitterDistance = Math.Min(splitterDistance, normalContainerHeight - this.SplitContainer1.SplitterWidth - this.SplitContainer1.Panel2MinSize);
7200                     break;
7201                 default:
7202                     return;
7203             }
7204
7205             this.mySpDis = splitterDistance;
7206             this.MarkSettingLocalModified();
7207         }
7208
7209         private async Task DoRepliedStatusOpen()
7210         {
7211             var currentPost = this.CurrentPost;
7212             if (this.ExistCurrentPost && currentPost != null && currentPost.InReplyToUser != null && currentPost.InReplyToStatusId != null)
7213             {
7214                 if (MyCommon.IsKeyDown(Keys.Shift))
7215                 {
7216                     await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.ToTwitterStatusId()));
7217                     return;
7218                 }
7219                 if (this.statuses.Posts.TryGetValue(currentPost.InReplyToStatusId, out var repPost))
7220                 {
7221                     MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname}   ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi);
7222                 }
7223                 else
7224                 {
7225                     foreach (var tb in this.statuses.GetTabsByType(MyCommon.TabUsageType.Lists | MyCommon.TabUsageType.PublicSearch))
7226                     {
7227                         if (tb == null || !tb.Contains(currentPost.InReplyToStatusId)) break;
7228                         repPost = tb.Posts[currentPost.InReplyToStatusId];
7229                         MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname}   ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi);
7230                         return;
7231                     }
7232                     await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.ToTwitterStatusId()));
7233                 }
7234             }
7235         }
7236
7237         private async void RepliedStatusOpenMenuItem_Click(object sender, EventArgs e)
7238             => await this.DoRepliedStatusOpen();
7239
7240         private void SplitContainer2_Panel2_Resize(object sender, EventArgs e)
7241         {
7242             if (this.initialLayout)
7243                 return; // SettingLocal の反映が完了するまで multiline の判定を行わない
7244
7245             var multiline = this.SplitContainer2.Panel2.Height > this.SplitContainer2.Panel2MinSize + 2;
7246             if (multiline != this.StatusText.Multiline)
7247             {
7248                 this.StatusText.Multiline = multiline;
7249                 this.settings.Local.StatusMultiline = multiline;
7250                 this.MarkSettingLocalModified();
7251             }
7252         }
7253
7254         private void StatusText_MultilineChanged(object sender, EventArgs e)
7255         {
7256             if (this.StatusText.Multiline)
7257                 this.StatusText.ScrollBars = ScrollBars.Vertical;
7258             else
7259                 this.StatusText.ScrollBars = ScrollBars.None;
7260
7261             if (!this.initialLayout)
7262                 this.MarkSettingLocalModified();
7263         }
7264
7265         private void MultiLineMenuItem_Click(object sender, EventArgs e)
7266         {
7267             // 発言欄複数行
7268             var menuItemChecked = ((ToolStripMenuItem)sender).Checked;
7269             this.StatusText.Multiline = menuItemChecked;
7270             this.settings.Local.StatusMultiline = menuItemChecked;
7271             if (menuItemChecked)
7272             {
7273                 if (this.SplitContainer2.Height - this.mySpDis2 - this.SplitContainer2.SplitterWidth < 0)
7274                     this.SplitContainer2.SplitterDistance = 0;
7275                 else
7276                     this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.mySpDis2 - this.SplitContainer2.SplitterWidth;
7277             }
7278             else
7279             {
7280                 this.SplitContainer2.SplitterDistance = this.SplitContainer2.Height - this.SplitContainer2.Panel2MinSize - this.SplitContainer2.SplitterWidth;
7281             }
7282             this.MarkSettingLocalModified();
7283         }
7284
7285         private async Task<bool> UrlConvertAsync(MyCommon.UrlConverter converterType)
7286         {
7287             if (converterType == MyCommon.UrlConverter.Bitly || converterType == MyCommon.UrlConverter.Jmp)
7288             {
7289                 // OAuth2 アクセストークンまたは API キー (旧方式) のいずれも設定されていなければ短縮しない
7290                 if (MyCommon.IsNullOrEmpty(this.settings.Common.BitlyAccessToken) &&
7291                     (MyCommon.IsNullOrEmpty(this.settings.Common.BilyUser) || MyCommon.IsNullOrEmpty(this.settings.Common.BitlyPwd)))
7292                 {
7293                     MessageBox.Show(this, Properties.Resources.UrlConvert_BitlyAuthRequired, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Warning);
7294                     return false;
7295                 }
7296             }
7297
7298             // Converter_Type=Nicomsの場合は、nicovideoのみ短縮する
7299             // 参考資料 RFC3986 Uniform Resource Identifier (URI): Generic Syntax
7300             // Appendix A.  Collected ABNF for URI
7301             // http://www.ietf.org/rfc/rfc3986.txt
7302
7303             const string nico = @"^https?://[a-z]+\.(nicovideo|niconicommons|nicolive)\.jp/[a-z]+/[a-z0-9]+$";
7304
7305             string result;
7306             if (this.StatusText.SelectionLength > 0)
7307             {
7308                 var tmp = this.StatusText.SelectedText;
7309                 // httpから始まらない場合、ExcludeStringで指定された文字列で始まる場合は対象としない
7310                 if (tmp.StartsWith("http", StringComparison.OrdinalIgnoreCase))
7311                 {
7312                     // 文字列が選択されている場合はその文字列について処理
7313
7314                     // nico.ms使用、nicovideoにマッチしたら変換
7315                     if (this.settings.Common.Nicoms && Regex.IsMatch(tmp, nico))
7316                     {
7317                         result = Nicoms.Shorten(tmp);
7318                     }
7319                     else if (converterType != MyCommon.UrlConverter.Nicoms)
7320                     {
7321                         // 短縮URL変換
7322                         try
7323                         {
7324                             var srcUri = new Uri(tmp);
7325                             var resultUri = await ShortUrl.Instance.ShortenUrlAsync(converterType, srcUri);
7326                             result = resultUri.AbsoluteUri;
7327                         }
7328                         catch (WebApiException e)
7329                         {
7330                             this.StatusLabel.Text = converterType + ":" + e.Message;
7331                             return false;
7332                         }
7333                         catch (UriFormatException e)
7334                         {
7335                             this.StatusLabel.Text = converterType + ":" + e.Message;
7336                             return false;
7337                         }
7338                     }
7339                     else
7340                     {
7341                         return true;
7342                     }
7343
7344                     if (!MyCommon.IsNullOrEmpty(result))
7345                     {
7346                         // 短縮 URL が生成されるまでの間に投稿欄から元の URL が削除されていたら中断する
7347                         var origUrlIndex = this.StatusText.Text.IndexOf(tmp, StringComparison.Ordinal);
7348                         if (origUrlIndex == -1)
7349                             return false;
7350
7351                         this.StatusText.Select(origUrlIndex, tmp.Length);
7352                         this.StatusText.SelectedText = result;
7353
7354                         // undoバッファにセット
7355                         var undo = new UrlUndo
7356                         {
7357                             Before = tmp,
7358                             After = result,
7359                         };
7360
7361                         if (this.urlUndoBuffer == null)
7362                         {
7363                             this.urlUndoBuffer = new List<UrlUndo>();
7364                             this.UrlUndoToolStripMenuItem.Enabled = true;
7365                         }
7366
7367                         this.urlUndoBuffer.Add(undo);
7368                     }
7369                 }
7370             }
7371             else
7372             {
7373                 const string url = @"(?<before>(?:[^\""':!=]|^|\:))" +
7374                                    @"(?<url>(?<protocol>https?://)" +
7375                                    @"(?<domain>(?:[\.-]|[^\p{P}\s])+\.[a-z]{2,}(?::[0-9]+)?)" +
7376                                    @"(?<path>/[a-z0-9!*//();:&=+$/%#\-_.,~@]*[a-z0-9)=#/]?)?" +
7377                                    @"(?<query>\?[a-z0-9!*//();:&=+$/%#\-_.,~@?]*[a-z0-9_&=#/])?)";
7378                 // 正規表現にマッチしたURL文字列をtinyurl化
7379                 foreach (Match mt in Regex.Matches(this.StatusText.Text, url, RegexOptions.IgnoreCase))
7380                 {
7381                     if (this.StatusText.Text.IndexOf(mt.Result("${url}"), StringComparison.Ordinal) == -1)
7382                         continue;
7383                     var tmp = mt.Result("${url}");
7384                     if (tmp.StartsWith("w", StringComparison.OrdinalIgnoreCase))
7385                         tmp = "http://" + tmp;
7386
7387                     // 選んだURLを選択(?)
7388                     this.StatusText.Select(this.StatusText.Text.IndexOf(mt.Result("${url}"), StringComparison.Ordinal), mt.Result("${url}").Length);
7389
7390                     // nico.ms使用、nicovideoにマッチしたら変換
7391                     if (this.settings.Common.Nicoms && Regex.IsMatch(tmp, nico))
7392                     {
7393                         result = Nicoms.Shorten(tmp);
7394                     }
7395                     else if (converterType != MyCommon.UrlConverter.Nicoms)
7396                     {
7397                         // 短縮URL変換
7398                         try
7399                         {
7400                             var srcUri = new Uri(tmp);
7401                             var resultUri = await ShortUrl.Instance.ShortenUrlAsync(converterType, srcUri);
7402                             result = resultUri.AbsoluteUri;
7403                         }
7404                         catch (HttpRequestException e)
7405                         {
7406                             // 例外のメッセージが「Response status code does not indicate success: 500 (Internal Server Error).」
7407                             // のように長いので「:」が含まれていればそれ以降のみを抽出する
7408                             var message = e.Message.Split(new[] { ':' }, count: 2).Last();
7409
7410                             this.StatusLabel.Text = converterType + ":" + message;
7411                             continue;
7412                         }
7413                         catch (WebApiException e)
7414                         {
7415                             this.StatusLabel.Text = converterType + ":" + e.Message;
7416                             continue;
7417                         }
7418                         catch (UriFormatException e)
7419                         {
7420                             this.StatusLabel.Text = converterType + ":" + e.Message;
7421                             continue;
7422                         }
7423                     }
7424                     else
7425                     {
7426                         continue;
7427                     }
7428
7429                     if (!MyCommon.IsNullOrEmpty(result))
7430                     {
7431                         // 短縮 URL が生成されるまでの間に投稿欄から元の URL が削除されていたら中断する
7432                         var origUrlIndex = this.StatusText.Text.IndexOf(mt.Result("${url}"), StringComparison.Ordinal);
7433                         if (origUrlIndex == -1)
7434                             return false;
7435
7436                         this.StatusText.Select(origUrlIndex, mt.Result("${url}").Length);
7437                         this.StatusText.SelectedText = result;
7438                         // undoバッファにセット
7439                         var undo = new UrlUndo
7440                         {
7441                             Before = mt.Result("${url}"),
7442                             After = result,
7443                         };
7444
7445                         if (this.urlUndoBuffer == null)
7446                         {
7447                             this.urlUndoBuffer = new List<UrlUndo>();
7448                             this.UrlUndoToolStripMenuItem.Enabled = true;
7449                         }
7450
7451                         this.urlUndoBuffer.Add(undo);
7452                     }
7453                 }
7454             }
7455
7456             return true;
7457         }
7458
7459         private void DoUrlUndo()
7460         {
7461             if (this.urlUndoBuffer != null)
7462             {
7463                 var tmp = this.StatusText.Text;
7464                 foreach (var data in this.urlUndoBuffer)
7465                 {
7466                     tmp = tmp.Replace(data.After, data.Before);
7467                 }
7468                 this.StatusText.Text = tmp;
7469                 this.urlUndoBuffer = null;
7470                 this.UrlUndoToolStripMenuItem.Enabled = false;
7471                 this.StatusText.SelectionStart = 0;
7472                 this.StatusText.SelectionLength = 0;
7473             }
7474         }
7475
7476         private async void TinyURLToolStripMenuItem_Click(object sender, EventArgs e)
7477             => await this.UrlConvertAsync(MyCommon.UrlConverter.TinyUrl);
7478
7479         private async void IsgdToolStripMenuItem_Click(object sender, EventArgs e)
7480             => await this.UrlConvertAsync(MyCommon.UrlConverter.Isgd);
7481
7482         private async void UxnuMenuItem_Click(object sender, EventArgs e)
7483             => await this.UrlConvertAsync(MyCommon.UrlConverter.Uxnu);
7484
7485         private async void UrlConvertAutoToolStripMenuItem_Click(object sender, EventArgs e)
7486         {
7487             if (!await this.UrlConvertAsync(this.settings.Common.AutoShortUrlFirst))
7488             {
7489                 var rnd = new Random();
7490
7491                 MyCommon.UrlConverter svc;
7492                 // 前回使用した短縮URLサービス以外を選択する
7493                 do
7494                 {
7495                     svc = (MyCommon.UrlConverter)rnd.Next(System.Enum.GetNames(typeof(MyCommon.UrlConverter)).Length);
7496                 }
7497                 while (svc == this.settings.Common.AutoShortUrlFirst || svc == MyCommon.UrlConverter.Nicoms || svc == MyCommon.UrlConverter.Unu);
7498                 await this.UrlConvertAsync(svc);
7499             }
7500         }
7501
7502         private void UrlUndoToolStripMenuItem_Click(object sender, EventArgs e)
7503             => this.DoUrlUndo();
7504
7505         private void NewPostPopMenuItem_CheckStateChanged(object sender, EventArgs e)
7506         {
7507             this.NotifyFileMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
7508             this.NewPostPopMenuItem.Checked = this.NotifyFileMenuItem.Checked;
7509             this.settings.Common.NewAllPop = this.NewPostPopMenuItem.Checked;
7510             this.MarkSettingCommonModified();
7511         }
7512
7513         private void ListLockMenuItem_CheckStateChanged(object sender, EventArgs e)
7514         {
7515             this.ListLockMenuItem.Checked = ((ToolStripMenuItem)sender).Checked;
7516             this.LockListFileMenuItem.Checked = this.ListLockMenuItem.Checked;
7517             this.settings.Common.ListLock = this.ListLockMenuItem.Checked;
7518             this.MarkSettingCommonModified();
7519         }
7520
7521         private void MenuStrip1_MenuActivate(object sender, EventArgs e)
7522         {
7523             // フォーカスがメニューに移る (MenuStrip1.Tag フラグを立てる)
7524             this.MenuStrip1.Tag = new object();
7525             this.MenuStrip1.Select(); // StatusText がフォーカスを持っている場合 Leave が発生
7526         }
7527
7528         private void MenuStrip1_MenuDeactivate(object sender, EventArgs e)
7529         {
7530             var currentTabPage = this.CurrentTabPage;
7531             if (this.Tag != null) // 設定された戻り先へ遷移
7532             {
7533                 if (this.Tag == currentTabPage)
7534                     ((Control)currentTabPage.Tag).Select();
7535                 else
7536                     ((Control)this.Tag).Select();
7537             }
7538             else // 戻り先が指定されていない (初期状態) 場合はタブに遷移
7539             {
7540                 this.Tag = currentTabPage.Tag;
7541                 ((Control)this.Tag).Select();
7542             }
7543             // フォーカスがメニューに遷移したかどうかを表すフラグを降ろす
7544             this.MenuStrip1.Tag = null;
7545         }
7546
7547         private void MyList_ColumnReordered(object sender, ColumnReorderedEventArgs e)
7548         {
7549             if (this.Use2ColumnsMode)
7550             {
7551                 e.Cancel = true;
7552                 return;
7553             }
7554
7555             var lst = (DetailsListView)sender;
7556             var columnsCount = lst.Columns.Count;
7557
7558             var darr = new int[columnsCount];
7559             for (var i = 0; i < columnsCount; i++)
7560                 darr[lst.Columns[i].DisplayIndex] = i;
7561
7562             MyCommon.MoveArrayItem(darr, e.OldDisplayIndex, e.NewDisplayIndex);
7563
7564             for (var i = 0; i < columnsCount; i++)
7565                 this.settings.Local.ColumnsOrder[darr[i]] = i;
7566
7567             this.MarkSettingLocalModified();
7568             this.isColumnChanged = true;
7569         }
7570
7571         private void MyList_ColumnWidthChanged(object sender, ColumnWidthChangedEventArgs e)
7572         {
7573             var lst = (DetailsListView)sender;
7574             if (this.settings.Local == null) return;
7575
7576             var modified = false;
7577             if (this.Use2ColumnsMode)
7578             {
7579                 if (this.settings.Local.ColumnsWidth[0] != lst.Columns[0].Width)
7580                 {
7581                     this.settings.Local.ColumnsWidth[0] = lst.Columns[0].Width;
7582                     modified = true;
7583                 }
7584                 if (this.settings.Local.ColumnsWidth[2] != lst.Columns[1].Width)
7585                 {
7586                     this.settings.Local.ColumnsWidth[2] = lst.Columns[1].Width;
7587                     modified = true;
7588                 }
7589             }
7590             else
7591             {
7592                 var columnsCount = lst.Columns.Count;
7593                 for (var i = 0; i < columnsCount; i++)
7594                 {
7595                     if (this.settings.Local.ColumnsWidth[i] == lst.Columns[i].Width)
7596                         continue;
7597
7598                     this.settings.Local.ColumnsWidth[i] = lst.Columns[i].Width;
7599                     modified = true;
7600                 }
7601             }
7602             if (modified)
7603             {
7604                 this.MarkSettingLocalModified();
7605                 this.isColumnChanged = true;
7606             }
7607         }
7608
7609         private void SplitContainer2_SplitterMoved(object sender, SplitterEventArgs e)
7610         {
7611             if (this.StatusText.Multiline) this.mySpDis2 = this.StatusText.Height;
7612             this.MarkSettingLocalModified();
7613         }
7614
7615         private void TweenMain_DragDrop(object sender, DragEventArgs e)
7616         {
7617             if (e.Data.GetDataPresent(DataFormats.FileDrop))
7618             {
7619                 if (!e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像Drag&Dropは弾く
7620                 {
7621                     this.SelectMedia_DragDrop(e);
7622                 }
7623             }
7624             else if (e.Data.GetDataPresent("UniformResourceLocatorW"))
7625             {
7626                 var (url, title) = GetUrlFromDataObject(e.Data);
7627
7628                 string appendText;
7629                 if (title == null)
7630                     appendText = url;
7631                 else
7632                     appendText = title + " " + url;
7633
7634                 if (this.StatusText.TextLength == 0)
7635                     this.StatusText.Text = appendText;
7636                 else
7637                     this.StatusText.Text += " " + appendText;
7638             }
7639             else if (e.Data.GetDataPresent(DataFormats.UnicodeText))
7640             {
7641                 var text = (string)e.Data.GetData(DataFormats.UnicodeText);
7642                 if (text != null)
7643                     this.StatusText.Text += text;
7644             }
7645             else if (e.Data.GetDataPresent(DataFormats.StringFormat))
7646             {
7647                 var data = (string)e.Data.GetData(DataFormats.StringFormat, true);
7648                 if (data != null) this.StatusText.Text += data;
7649             }
7650         }
7651
7652         /// <summary>
7653         /// IDataObject から URL とタイトルの対を取得します
7654         /// </summary>
7655         /// <remarks>
7656         /// タイトルのみ取得できなかった場合は Value2 が null のタプルを返すことがあります。
7657         /// </remarks>
7658         /// <exception cref="ArgumentException">不正なフォーマットが入力された場合</exception>
7659         /// <exception cref="NotSupportedException">サポートされていないデータが入力された場合</exception>
7660         internal static (string Url, string? Title) GetUrlFromDataObject(IDataObject data)
7661         {
7662             if (data.GetDataPresent("text/x-moz-url"))
7663             {
7664                 // Firefox, Google Chrome で利用可能
7665                 // 参照: https://developer.mozilla.org/ja/docs/DragDrop/Recommended_Drag_Types
7666
7667                 using var stream = (MemoryStream)data.GetData("text/x-moz-url");
7668                 var lines = Encoding.Unicode.GetString(stream.ToArray()).TrimEnd('\0').Split('\n');
7669                 if (lines.Length < 2)
7670                     throw new ArgumentException("不正な text/x-moz-url フォーマットです", nameof(data));
7671
7672                 return (lines[0], lines[1]);
7673             }
7674             else if (data.GetDataPresent("IESiteModeToUrl"))
7675             {
7676                 // Internet Exproler 用
7677                 // 保護モードが有効なデフォルトの IE では DragDrop イベントが発火しないため使えない
7678
7679                 using var stream = (MemoryStream)data.GetData("IESiteModeToUrl");
7680                 var lines = Encoding.Unicode.GetString(stream.ToArray()).TrimEnd('\0').Split('\0');
7681                 if (lines.Length < 2)
7682                     throw new ArgumentException("不正な IESiteModeToUrl フォーマットです", nameof(data));
7683
7684                 return (lines[0], lines[1]);
7685             }
7686             else if (data.GetDataPresent("UniformResourceLocatorW"))
7687             {
7688                 // それ以外のブラウザ向け
7689
7690                 using var stream = (MemoryStream)data.GetData("UniformResourceLocatorW");
7691                 var url = Encoding.Unicode.GetString(stream.ToArray()).TrimEnd('\0');
7692                 return (url, null);
7693             }
7694
7695             throw new NotSupportedException("サポートされていないデータ形式です: " + data.GetFormats()[0]);
7696         }
7697
7698         private void TweenMain_DragEnter(object sender, DragEventArgs e)
7699         {
7700             if (e.Data.GetDataPresent(DataFormats.FileDrop))
7701             {
7702                 if (!e.Data.GetDataPresent(DataFormats.Html, false)) // WebBrowserコントロールからの絵文字画像Drag&Dropは弾く
7703                 {
7704                     this.SelectMedia_DragEnter(e);
7705                     return;
7706                 }
7707             }
7708             else if (e.Data.GetDataPresent("UniformResourceLocatorW"))
7709             {
7710                 e.Effect = DragDropEffects.Copy;
7711                 return;
7712             }
7713             else if (e.Data.GetDataPresent(DataFormats.UnicodeText))
7714             {
7715                 e.Effect = DragDropEffects.Copy;
7716                 return;
7717             }
7718             else if (e.Data.GetDataPresent(DataFormats.StringFormat))
7719             {
7720                 e.Effect = DragDropEffects.Copy;
7721                 return;
7722             }
7723
7724             e.Effect = DragDropEffects.None;
7725         }
7726
7727         private void TweenMain_DragOver(object sender, DragEventArgs e)
7728         {
7729         }
7730
7731         public bool IsNetworkAvailable()
7732         {
7733             var nw = MyCommon.IsNetworkAvailable();
7734             this.myStatusOnline = nw;
7735             return nw;
7736         }
7737
7738         public async Task OpenUriAsync(Uri uri, bool isReverseSettings = false)
7739         {
7740             var uriStr = uri.AbsoluteUri;
7741
7742             // OpenTween 内部で使用する URL
7743             if (uri.Authority == "opentween")
7744             {
7745                 await this.OpenInternalUriAsync(uri);
7746                 return;
7747             }
7748
7749             // ハッシュタグを含む Twitter 検索
7750             if (uri.Host == "twitter.com" && uri.AbsolutePath == "/search" && uri.Query.Contains("q=%23"))
7751             {
7752                 // ハッシュタグの場合は、タブで開く
7753                 var unescapedQuery = Uri.UnescapeDataString(uri.Query);
7754                 var pos = unescapedQuery.IndexOf('#');
7755                 if (pos == -1) return;
7756
7757                 var hash = unescapedQuery.Substring(pos);
7758                 this.HashSupl.AddItem(hash);
7759                 this.HashMgr.AddHashToHistory(hash.Trim(), false);
7760                 this.AddNewTabForSearch(hash);
7761                 return;
7762             }
7763
7764             // ユーザープロフィールURL
7765             // フラグが立っている場合は設定と逆の動作をする
7766             if (this.settings.Common.OpenUserTimeline && !isReverseSettings ||
7767                 !this.settings.Common.OpenUserTimeline && isReverseSettings)
7768             {
7769                 var userUriMatch = Regex.Match(uriStr, "^https?://twitter.com/(#!/)?(?<ScreenName>[a-zA-Z0-9_]+)$");
7770                 if (userUriMatch.Success)
7771                 {
7772                     var screenName = userUriMatch.Groups["ScreenName"].Value;
7773                     if (this.IsTwitterId(screenName))
7774                     {
7775                         await this.AddNewTabForUserTimeline(screenName);
7776                         return;
7777                     }
7778                 }
7779             }
7780
7781             // どのパターンにも該当しないURL
7782             await MyCommon.OpenInBrowserAsync(this, uriStr);
7783         }
7784
7785         /// <summary>
7786         /// OpenTween 内部の機能を呼び出すための URL を開きます
7787         /// </summary>
7788         private async Task OpenInternalUriAsync(Uri uri)
7789         {
7790             // ツイートを開く (//opentween/status/:status_id)
7791             var match = Regex.Match(uri.AbsolutePath, @"^/status/(\d+)$");
7792             if (match.Success)
7793             {
7794                 var statusId = new TwitterStatusId(match.Groups[1].Value);
7795                 await this.OpenRelatedTab(statusId);
7796                 return;
7797             }
7798         }
7799
7800         private void ListTabSelect(TabPage tabPage)
7801         {
7802             this.SetListProperty();
7803
7804             var previousTabName = this.CurrentTabName;
7805             if (this.listViewState.TryGetValue(previousTabName, out var previousListViewState))
7806                 previousListViewState.Save(this.ListLockMenuItem.Checked);
7807
7808             this.listCache?.PurgeCache();
7809
7810             this.statuses.SelectTab(tabPage.Text);
7811
7812             this.InitializeTimelineListView();
7813
7814             var tab = this.CurrentTab;
7815             tab.ClearAnchor();
7816
7817             var listView = this.CurrentListView;
7818
7819             var currentListViewState = this.listViewState[tab.TabName];
7820             currentListViewState.Restore(forceScroll: true);
7821
7822             if (this.Use2ColumnsMode)
7823             {
7824                 listView.Columns[1].Text = this.columnText[2];
7825             }
7826             else
7827             {
7828                 for (var i = 0; i < listView.Columns.Count; i++)
7829                 {
7830                     listView.Columns[i].Text = this.columnText[i];
7831                 }
7832             }
7833         }
7834
7835         private void InitializeTimelineListView()
7836         {
7837             var listView = this.CurrentListView;
7838             var tab = this.CurrentTab;
7839
7840             var newCache = new TimelineListViewCache(listView, tab, this.settings.Common);
7841             (this.listCache, var oldCache) = (newCache, this.listCache);
7842             oldCache?.Dispose();
7843
7844             var newDrawer = new TimelineListViewDrawer(listView, tab, this.listCache, this.iconCache, this.themeManager);
7845             (this.listDrawer, var oldDrawer) = (newDrawer, this.listDrawer);
7846             oldDrawer?.Dispose();
7847
7848             newDrawer.IconSize = this.settings.Common.IconSize;
7849             newDrawer.UpdateItemHeight();
7850         }
7851
7852         private void ListTab_Selecting(object sender, TabControlCancelEventArgs e)
7853             => this.ListTabSelect(e.TabPage);
7854
7855         private void SelectListItem(DetailsListView lView, int index)
7856         {
7857             // 単一
7858             var bnd = new Rectangle();
7859             var flg = false;
7860             var item = lView.FocusedItem;
7861             if (item != null)
7862             {
7863                 bnd = item.Bounds;
7864                 flg = true;
7865             }
7866
7867             do
7868             {
7869                 lView.SelectedIndices.Clear();
7870             }
7871             while (lView.SelectedIndices.Count > 0);
7872             item = lView.Items[index];
7873             item.Selected = true;
7874             item.Focused = true;
7875
7876             if (flg) lView.Invalidate(bnd);
7877         }
7878
7879         private async void TweenMain_Shown(object sender, EventArgs e)
7880         {
7881             this.NotifyIcon1.Visible = true;
7882             this.StartTimers();
7883
7884             if (this.settings.IsFirstRun)
7885             {
7886                 // 初回起動時だけ右下のメニューを目立たせる
7887                 this.HashStripSplitButton.ShowDropDown();
7888             }
7889
7890             if (this.IsNetworkAvailable())
7891             {
7892                 var loadTasks = new TaskCollection();
7893
7894                 loadTasks.Add(new[]
7895                 {
7896                     this.RefreshMuteUserIdsAsync,
7897                     this.RefreshBlockIdsAsync,
7898                     this.RefreshNoRetweetIdsAsync,
7899                     this.RefreshTwitterConfigurationAsync,
7900                     this.RefreshTabAsync<HomeTabModel>,
7901                     this.RefreshTabAsync<MentionsTabModel>,
7902                     this.RefreshTabAsync<DirectMessagesTabModel>,
7903                     this.RefreshTabAsync<PublicSearchTabModel>,
7904                     this.RefreshTabAsync<UserTimelineTabModel>,
7905                     this.RefreshTabAsync<ListTimelineTabModel>,
7906                 });
7907
7908                 if (this.settings.Common.StartupFollowers)
7909                     loadTasks.Add(this.RefreshFollowerIdsAsync);
7910
7911                 if (this.settings.Common.GetFav)
7912                     loadTasks.Add(this.RefreshTabAsync<FavoritesTabModel>);
7913
7914                 var allTasks = loadTasks.RunAll();
7915
7916                 var i = 0;
7917                 while (true)
7918                 {
7919                     var timeout = Task.Delay(5000);
7920                     if (await Task.WhenAny(allTasks, timeout) != timeout)
7921                         break;
7922
7923                     i += 1;
7924                     if (i > 24) break; // 120秒間初期処理が終了しなかったら強制的に打ち切る
7925
7926                     if (MyCommon.EndingFlag)
7927                         return;
7928                 }
7929
7930                 if (MyCommon.EndingFlag) return;
7931
7932                 if (ApplicationSettings.VersionInfoUrl != null)
7933                 {
7934                     // バージョンチェック(引数:起動時チェックの場合はtrue・・・チェック結果のメッセージを表示しない)
7935                     if (this.settings.Common.StartupVersion)
7936                         await this.CheckNewVersion(true);
7937                 }
7938                 else
7939                 {
7940                     // ApplicationSetting.cs の設定により更新チェックが無効化されている場合
7941                     this.VerUpMenuItem.Enabled = false;
7942                     this.VerUpMenuItem.Available = false;
7943                     this.ToolStripSeparator16.Available = false; // VerUpMenuItem の一つ上にあるセパレータ
7944                 }
7945
7946                 // 権限チェック read/write権限(xAuthで取得したトークン)の場合は再認証を促す
7947                 if (MyCommon.TwitterApiInfo.AccessLevel == TwitterApiAccessLevel.ReadWrite)
7948                 {
7949                     MessageBox.Show(Properties.Resources.ReAuthorizeText);
7950                     this.SettingStripMenuItem_Click(this.SettingStripMenuItem, EventArgs.Empty);
7951                 }
7952
7953                 // 取得失敗の場合は再試行する
7954                 var reloadTasks = new TaskCollection();
7955
7956                 if (!this.tw.GetFollowersSuccess && this.settings.Common.StartupFollowers)
7957                     reloadTasks.Add(() => this.RefreshFollowerIdsAsync());
7958
7959                 if (!this.tw.GetNoRetweetSuccess)
7960                     reloadTasks.Add(() => this.RefreshNoRetweetIdsAsync());
7961
7962                 if (this.tw.Configuration.PhotoSizeLimit == 0)
7963                     reloadTasks.Add(() => this.RefreshTwitterConfigurationAsync());
7964
7965                 await reloadTasks.RunAll();
7966             }
7967
7968             this.initial = false;
7969         }
7970
7971         private void StartTimers()
7972         {
7973             if (!this.StopRefreshAllMenuItem.Checked)
7974                 this.timelineScheduler.Enabled = true;
7975
7976             this.selectionDebouncer.Enabled = true;
7977             this.saveConfigDebouncer.Enabled = true;
7978             this.thumbGenerator.ImgAzyobuziNet.AutoUpdate = true;
7979         }
7980
7981         private async Task DoGetFollowersMenu()
7982         {
7983             await this.RefreshFollowerIdsAsync();
7984             this.DispSelectedPost(true);
7985         }
7986
7987         private async void GetFollowersAllToolStripMenuItem_Click(object sender, EventArgs e)
7988             => await this.DoGetFollowersMenu();
7989
7990         private void ReTweetUnofficialStripMenuItem_Click(object sender, EventArgs e)
7991             => this.DoReTweetUnofficial();
7992
7993         private async Task DoReTweetOfficial(bool isConfirm)
7994         {
7995             // 公式RT
7996             if (this.ExistCurrentPost)
7997             {
7998                 var selectedPosts = this.CurrentTab.SelectedPosts;
7999
8000                 if (selectedPosts.Any(x => !x.CanRetweetBy(this.tw.UserId)))
8001                 {
8002                     if (selectedPosts.Any(x => x.IsProtect))
8003                         MessageBox.Show("Protected.");
8004
8005                     this.doFavRetweetFlags = false;
8006                     return;
8007                 }
8008
8009                 if (selectedPosts.Length > 15)
8010                 {
8011                     MessageBox.Show(Properties.Resources.RetweetLimitText);
8012                     this.doFavRetweetFlags = false;
8013                     return;
8014                 }
8015                 else if (selectedPosts.Length > 1)
8016                 {
8017                     var questionText = Properties.Resources.RetweetQuestion2;
8018                     if (this.doFavRetweetFlags) questionText = Properties.Resources.FavoriteRetweetQuestionText1;
8019                     switch (MessageBox.Show(questionText, "Retweet", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question))
8020                     {
8021                         case DialogResult.Cancel:
8022                         case DialogResult.No:
8023                             this.doFavRetweetFlags = false;
8024                             return;
8025                     }
8026                 }
8027                 else
8028                 {
8029                     if (!this.settings.Common.RetweetNoConfirm)
8030                     {
8031                         var questiontext = Properties.Resources.RetweetQuestion1;
8032                         if (this.doFavRetweetFlags) questiontext = Properties.Resources.FavoritesRetweetQuestionText2;
8033                         if (isConfirm && MessageBox.Show(questiontext, "Retweet", MessageBoxButtons.OKCancel, MessageBoxIcon.Question) == DialogResult.Cancel)
8034                         {
8035                             this.doFavRetweetFlags = false;
8036                             return;
8037                         }
8038                     }
8039                 }
8040
8041                 var statusIds = selectedPosts.Select(x => x.StatusId).ToList();
8042
8043                 await this.RetweetAsync(statusIds);
8044             }
8045         }
8046
8047         private async void ReTweetStripMenuItem_Click(object sender, EventArgs e)
8048             => await this.DoReTweetOfficial(true);
8049
8050         private async Task FavoritesRetweetOfficial()
8051         {
8052             if (!this.ExistCurrentPost) return;
8053             this.doFavRetweetFlags = true;
8054
8055             var tasks = new TaskCollection();
8056             tasks.Add(() => this.DoReTweetOfficial(true));
8057
8058             if (this.doFavRetweetFlags)
8059             {
8060                 this.doFavRetweetFlags = false;
8061                 tasks.Add(() => this.FavoriteChange(true, false));
8062             }
8063
8064             await tasks.RunAll();
8065         }
8066
8067         private async Task FavoritesRetweetUnofficial()
8068         {
8069             var post = this.CurrentPost;
8070             if (this.ExistCurrentPost && post != null && !post.IsDm)
8071             {
8072                 this.doFavRetweetFlags = true;
8073                 var favoriteTask = this.FavoriteChange(true);
8074                 if (!post.IsProtect && this.doFavRetweetFlags)
8075                 {
8076                     this.doFavRetweetFlags = false;
8077                     this.DoReTweetUnofficial();
8078                 }
8079
8080                 await favoriteTask;
8081             }
8082         }
8083
8084         /// <summary>
8085         /// TweetFormatterクラスによって整形された状態のHTMLを、非公式RT用に元のツイートに復元します
8086         /// </summary>
8087         /// <param name="statusHtml">TweetFormatterによって整形された状態のHTML</param>
8088         /// <param name="multiline">trueであればBRタグを改行に、falseであればスペースに変換します</param>
8089         /// <returns>復元されたツイート本文</returns>
8090         internal static string CreateRetweetUnofficial(string statusHtml, bool multiline)
8091         {
8092             // TweetFormatterクラスによって整形された状態のHTMLを元のツイートに復元します
8093
8094             // 通常の URL
8095             statusHtml = Regex.Replace(statusHtml, """<a href="(?<href>.+?)" title="(?<title>.+?)">(?<text>.+?)</a>""", "${title}");
8096             // メンション
8097             statusHtml = Regex.Replace(statusHtml, """<a class="mention" href="(?<href>.+?)">(?<text>.+?)</a>""", "${text}");
8098             // ハッシュタグ
8099             statusHtml = Regex.Replace(statusHtml, """<a class="hashtag" href="(?<href>.+?)">(?<text>.+?)</a>""", "${text}");
8100             // 絵文字
8101             statusHtml = Regex.Replace(statusHtml, """<img class="emoji" src=".+?" alt="(?<text>.+?)" />""", "${text}");
8102
8103             // <br> 除去
8104             if (multiline)
8105                 statusHtml = statusHtml.Replace("<br>", Environment.NewLine);
8106             else
8107                 statusHtml = statusHtml.Replace("<br>", " ");
8108
8109             // &nbsp; は本来であれば U+00A0 (NON-BREAK SPACE) に置換すべきですが、
8110             // 現状では半角スペースの代用として &nbsp; を使用しているため U+0020 に置換します
8111             statusHtml = statusHtml.Replace("&nbsp;", " ");
8112
8113             return WebUtility.HtmlDecode(statusHtml);
8114         }
8115
8116         private void DumpPostClassToolStripMenuItem_Click(object sender, EventArgs e)
8117         {
8118             this.tweetDetailsView.DumpPostClass = this.DumpPostClassToolStripMenuItem.Checked;
8119
8120             if (this.CurrentPost != null)
8121                 this.DispSelectedPost(true);
8122         }
8123
8124         private void MenuItemHelp_DropDownOpening(object sender, EventArgs e)
8125         {
8126             if (MyCommon.DebugBuild || MyCommon.IsKeyDown(Keys.CapsLock | Keys.Control | Keys.Shift))
8127                 this.DebugModeToolStripMenuItem.Visible = true;
8128             else
8129                 this.DebugModeToolStripMenuItem.Visible = false;
8130         }
8131
8132         private void UrlMultibyteSplitMenuItem_CheckedChanged(object sender, EventArgs e)
8133             => this.SeparateUrlAndFullwidthCharacter = ((ToolStripMenuItem)sender).Checked;
8134
8135         private void PreventSmsCommandMenuItem_CheckedChanged(object sender, EventArgs e)
8136             => this.preventSmsCommand = ((ToolStripMenuItem)sender).Checked;
8137
8138         private void UrlAutoShortenMenuItem_CheckedChanged(object sender, EventArgs e)
8139             => this.settings.Common.UrlConvertAuto = ((ToolStripMenuItem)sender).Checked;
8140
8141         private void IdeographicSpaceToSpaceMenuItem_Click(object sender, EventArgs e)
8142         {
8143             this.settings.Common.WideSpaceConvert = ((ToolStripMenuItem)sender).Checked;
8144             this.MarkSettingCommonModified();
8145         }
8146
8147         private void FocusLockMenuItem_CheckedChanged(object sender, EventArgs e)
8148         {
8149             this.settings.Common.FocusLockToStatusText = ((ToolStripMenuItem)sender).Checked;
8150             this.MarkSettingCommonModified();
8151         }
8152
8153         private void PostModeMenuItem_DropDownOpening(object sender, EventArgs e)
8154         {
8155             this.UrlMultibyteSplitMenuItem.Checked = this.SeparateUrlAndFullwidthCharacter;
8156             this.PreventSmsCommandMenuItem.Checked = this.preventSmsCommand;
8157             this.UrlAutoShortenMenuItem.Checked = this.settings.Common.UrlConvertAuto;
8158             this.IdeographicSpaceToSpaceMenuItem.Checked = this.settings.Common.WideSpaceConvert;
8159             this.MultiLineMenuItem.Checked = this.settings.Local.StatusMultiline;
8160             this.FocusLockMenuItem.Checked = this.settings.Common.FocusLockToStatusText;
8161         }
8162
8163         private void ContextMenuPostMode_Opening(object sender, CancelEventArgs e)
8164         {
8165             this.UrlMultibyteSplitPullDownMenuItem.Checked = this.SeparateUrlAndFullwidthCharacter;
8166             this.PreventSmsCommandPullDownMenuItem.Checked = this.preventSmsCommand;
8167             this.UrlAutoShortenPullDownMenuItem.Checked = this.settings.Common.UrlConvertAuto;
8168             this.IdeographicSpaceToSpacePullDownMenuItem.Checked = this.settings.Common.WideSpaceConvert;
8169             this.MultiLinePullDownMenuItem.Checked = this.settings.Local.StatusMultiline;
8170             this.FocusLockPullDownMenuItem.Checked = this.settings.Common.FocusLockToStatusText;
8171         }
8172
8173         private void TraceOutToolStripMenuItem_Click(object sender, EventArgs e)
8174         {
8175             if (this.TraceOutToolStripMenuItem.Checked)
8176                 MyCommon.TraceFlag = true;
8177             else
8178                 MyCommon.TraceFlag = false;
8179         }
8180
8181         private void TweenMain_Deactivate(object sender, EventArgs e)
8182             => this.StatusText_Leave(this.StatusText, EventArgs.Empty); // 画面が非アクティブになったら、発言欄の背景色をデフォルトへ
8183
8184         private void TabRenameMenuItem_Click(object sender, EventArgs e)
8185         {
8186             if (MyCommon.IsNullOrEmpty(this.rclickTabName)) return;
8187
8188             _ = this.TabRename(this.rclickTabName, out _);
8189         }
8190
8191         private async void BitlyToolStripMenuItem_Click(object sender, EventArgs e)
8192             => await this.UrlConvertAsync(MyCommon.UrlConverter.Bitly);
8193
8194         private async void JmpToolStripMenuItem_Click(object sender, EventArgs e)
8195             => await this.UrlConvertAsync(MyCommon.UrlConverter.Jmp);
8196
8197         private async void ApiUsageInfoMenuItem_Click(object sender, EventArgs e)
8198         {
8199             TwitterApiStatus? apiStatus;
8200
8201             using (var dialog = new WaitingDialog(Properties.Resources.ApiInfo6))
8202             {
8203                 var cancellationToken = dialog.EnableCancellation();
8204
8205                 try
8206                 {
8207                     var task = this.tw.GetInfoApi();
8208                     apiStatus = await dialog.WaitForAsync(this, task);
8209                 }
8210                 catch (WebApiException)
8211                 {
8212                     apiStatus = null;
8213                 }
8214
8215                 if (cancellationToken.IsCancellationRequested)
8216                     return;
8217
8218                 if (apiStatus == null)
8219                 {
8220                     MessageBox.Show(Properties.Resources.ApiInfo5, Properties.Resources.ApiInfo4, MessageBoxButtons.OK, MessageBoxIcon.Information);
8221                     return;
8222                 }
8223             }
8224
8225             using var apiDlg = new ApiInfoDialog();
8226             apiDlg.ShowDialog(this);
8227         }
8228
8229         private async void FollowCommandMenuItem_Click(object sender, EventArgs e)
8230         {
8231             var id = this.CurrentPost?.ScreenName ?? "";
8232
8233             await this.FollowCommand(id);
8234         }
8235
8236         internal async Task FollowCommand(string id)
8237         {
8238             using (var inputName = new InputTabName())
8239             {
8240                 inputName.FormTitle = "Follow";
8241                 inputName.FormDescription = Properties.Resources.FRMessage1;
8242                 inputName.TabName = id;
8243
8244                 if (inputName.ShowDialog(this) != DialogResult.OK)
8245                     return;
8246                 if (string.IsNullOrWhiteSpace(inputName.TabName))
8247                     return;
8248
8249                 id = inputName.TabName.Trim();
8250             }
8251
8252             using (var dialog = new WaitingDialog(Properties.Resources.FollowCommandText1))
8253             {
8254                 try
8255                 {
8256                     var task = this.tw.Api.FriendshipsCreate(id).IgnoreResponse();
8257                     await dialog.WaitForAsync(this, task);
8258                 }
8259                 catch (WebApiException ex)
8260                 {
8261                     MessageBox.Show(Properties.Resources.FRMessage2 + ex.Message);
8262                     return;
8263                 }
8264             }
8265
8266             MessageBox.Show(Properties.Resources.FRMessage3);
8267         }
8268
8269         private async void RemoveCommandMenuItem_Click(object sender, EventArgs e)
8270         {
8271             var id = this.CurrentPost?.ScreenName ?? "";
8272
8273             await this.RemoveCommand(id, false);
8274         }
8275
8276         internal async Task RemoveCommand(string id, bool skipInput)
8277         {
8278             if (!skipInput)
8279             {
8280                 using var inputName = new InputTabName();
8281                 inputName.FormTitle = "Unfollow";
8282                 inputName.FormDescription = Properties.Resources.FRMessage1;
8283                 inputName.TabName = id;
8284
8285                 if (inputName.ShowDialog(this) != DialogResult.OK)
8286                     return;
8287                 if (string.IsNullOrWhiteSpace(inputName.TabName))
8288                     return;
8289
8290                 id = inputName.TabName.Trim();
8291             }
8292
8293             using (var dialog = new WaitingDialog(Properties.Resources.RemoveCommandText1))
8294             {
8295                 try
8296                 {
8297                     var task = this.tw.Api.FriendshipsDestroy(id).IgnoreResponse();
8298                     await dialog.WaitForAsync(this, task);
8299                 }
8300                 catch (WebApiException ex)
8301                 {
8302                     MessageBox.Show(Properties.Resources.FRMessage2 + ex.Message);
8303                     return;
8304                 }
8305             }
8306
8307             MessageBox.Show(Properties.Resources.FRMessage3);
8308         }
8309
8310         private async void FriendshipMenuItem_Click(object sender, EventArgs e)
8311         {
8312             var id = this.CurrentPost?.ScreenName ?? "";
8313
8314             await this.ShowFriendship(id);
8315         }
8316
8317         internal async Task ShowFriendship(string id)
8318         {
8319             using (var inputName = new InputTabName())
8320             {
8321                 inputName.FormTitle = "Show Friendships";
8322                 inputName.FormDescription = Properties.Resources.FRMessage1;
8323                 inputName.TabName = id;
8324
8325                 if (inputName.ShowDialog(this) != DialogResult.OK)
8326                     return;
8327                 if (string.IsNullOrWhiteSpace(inputName.TabName))
8328                     return;
8329
8330                 id = inputName.TabName.Trim();
8331             }
8332
8333             bool isFollowing, isFollowed;
8334
8335             using (var dialog = new WaitingDialog(Properties.Resources.ShowFriendshipText1))
8336             {
8337                 var cancellationToken = dialog.EnableCancellation();
8338
8339                 try
8340                 {
8341                     var task = this.tw.Api.FriendshipsShow(this.tw.Username, id);
8342                     var friendship = await dialog.WaitForAsync(this, task);
8343
8344                     isFollowing = friendship.Relationship.Source.Following;
8345                     isFollowed = friendship.Relationship.Source.FollowedBy;
8346                 }
8347                 catch (WebApiException ex)
8348                 {
8349                     if (!cancellationToken.IsCancellationRequested)
8350                         MessageBox.Show($"Err:{ex.Message}(FriendshipsShow)");
8351                     return;
8352                 }
8353
8354                 if (cancellationToken.IsCancellationRequested)
8355                     return;
8356             }
8357
8358             string result;
8359             if (isFollowing)
8360             {
8361                 result = Properties.Resources.GetFriendshipInfo1 + System.Environment.NewLine;
8362             }
8363             else
8364             {
8365                 result = Properties.Resources.GetFriendshipInfo2 + System.Environment.NewLine;
8366             }
8367             if (isFollowed)
8368             {
8369                 result += Properties.Resources.GetFriendshipInfo3;
8370             }
8371             else
8372             {
8373                 result += Properties.Resources.GetFriendshipInfo4;
8374             }
8375             result = id + Properties.Resources.GetFriendshipInfo5 + System.Environment.NewLine + result;
8376             MessageBox.Show(result);
8377         }
8378
8379         internal async Task ShowFriendship(string[] ids)
8380         {
8381             foreach (var id in ids)
8382             {
8383                 bool isFollowing, isFollowed;
8384
8385                 using (var dialog = new WaitingDialog(Properties.Resources.ShowFriendshipText1))
8386                 {
8387                     var cancellationToken = dialog.EnableCancellation();
8388
8389                     try
8390                     {
8391                         var task = this.tw.Api.FriendshipsShow(this.tw.Username, id);
8392                         var friendship = await dialog.WaitForAsync(this, task);
8393
8394                         isFollowing = friendship.Relationship.Source.Following;
8395                         isFollowed = friendship.Relationship.Source.FollowedBy;
8396                     }
8397                     catch (WebApiException ex)
8398                     {
8399                         if (!cancellationToken.IsCancellationRequested)
8400                             MessageBox.Show($"Err:{ex.Message}(FriendshipsShow)");
8401                         return;
8402                     }
8403
8404                     if (cancellationToken.IsCancellationRequested)
8405                         return;
8406                 }
8407
8408                 var result = "";
8409                 var ff = "";
8410
8411                 ff = "  ";
8412                 if (isFollowing)
8413                 {
8414                     ff += Properties.Resources.GetFriendshipInfo1;
8415                 }
8416                 else
8417                 {
8418                     ff += Properties.Resources.GetFriendshipInfo2;
8419                 }
8420
8421                 ff += System.Environment.NewLine + "  ";
8422                 if (isFollowed)
8423                 {
8424                     ff += Properties.Resources.GetFriendshipInfo3;
8425                 }
8426                 else
8427                 {
8428                     ff += Properties.Resources.GetFriendshipInfo4;
8429                 }
8430                 result += id + Properties.Resources.GetFriendshipInfo5 + System.Environment.NewLine + ff;
8431                 if (isFollowing)
8432                 {
8433                     if (MessageBox.Show(
8434                         Properties.Resources.GetFriendshipInfo7 + System.Environment.NewLine + result,
8435                         Properties.Resources.GetFriendshipInfo8,
8436                         MessageBoxButtons.YesNo,
8437                         MessageBoxIcon.Question,
8438                         MessageBoxDefaultButton.Button2) == DialogResult.Yes)
8439                     {
8440                         await this.RemoveCommand(id, true);
8441                     }
8442                 }
8443                 else
8444                 {
8445                     MessageBox.Show(result);
8446                 }
8447             }
8448         }
8449
8450         private async void OwnStatusMenuItem_Click(object sender, EventArgs e)
8451             => await this.DoShowUserStatus(this.tw.Username, false);
8452
8453         // TwitterIDでない固定文字列を調べる(文字列検証のみ 実際に取得はしない)
8454         // URLから切り出した文字列を渡す
8455
8456         public bool IsTwitterId(string name)
8457         {
8458             if (this.tw.Configuration.NonUsernamePaths == null || this.tw.Configuration.NonUsernamePaths.Length == 0)
8459                 return !Regex.Match(name, @"^(about|jobs|tos|privacy|who_to_follow|download|messages)$", RegexOptions.IgnoreCase).Success;
8460             else
8461                 return !this.tw.Configuration.NonUsernamePaths.Contains(name, StringComparer.InvariantCultureIgnoreCase);
8462         }
8463
8464         private void DoQuoteOfficial()
8465         {
8466             var post = this.CurrentPost;
8467             if (this.ExistCurrentPost && post != null)
8468             {
8469                 if (post.IsDm || !this.StatusText.Enabled)
8470                     return;
8471
8472                 if (post.IsProtect)
8473                 {
8474                     MessageBox.Show("Protected.");
8475                     return;
8476                 }
8477
8478                 var selection = (this.StatusText.SelectionStart, this.StatusText.SelectionLength);
8479
8480                 this.inReplyTo = null;
8481
8482                 this.StatusText.Text += " " + MyCommon.GetStatusUrl(post);
8483
8484                 (this.StatusText.SelectionStart, this.StatusText.SelectionLength) = selection;
8485                 this.StatusText.Focus();
8486             }
8487         }
8488
8489         private void DoReTweetUnofficial()
8490         {
8491             // RT @id:内容
8492             var post = this.CurrentPost;
8493             if (this.ExistCurrentPost && post != null)
8494             {
8495                 if (post.IsDm || !this.StatusText.Enabled)
8496                     return;
8497
8498                 if (post.IsProtect)
8499                 {
8500                     MessageBox.Show("Protected.");
8501                     return;
8502                 }
8503                 var rtdata = post.Text;
8504                 rtdata = CreateRetweetUnofficial(rtdata, this.StatusText.Multiline);
8505
8506                 var selection = (this.StatusText.SelectionStart, this.StatusText.SelectionLength);
8507
8508                 // 投稿時に in_reply_to_status_id を付加する
8509                 var inReplyToStatusId = post.RetweetedId ?? post.StatusId;
8510                 var inReplyToScreenName = post.ScreenName;
8511                 this.inReplyTo = (inReplyToStatusId, inReplyToScreenName);
8512
8513                 this.StatusText.Text += " RT @" + post.ScreenName + ": " + rtdata;
8514
8515                 (this.StatusText.SelectionStart, this.StatusText.SelectionLength) = selection;
8516                 this.StatusText.Focus();
8517             }
8518         }
8519
8520         private void QuoteStripMenuItem_Click(object sender, EventArgs e)
8521             => this.DoQuoteOfficial();
8522
8523         private async void SearchButton_Click(object sender, EventArgs e)
8524         {
8525             // 公式検索
8526             var pnl = ((Control)sender).Parent;
8527             if (pnl == null) return;
8528             var tbName = pnl.Parent.Text;
8529             var tb = (PublicSearchTabModel)this.statuses.Tabs[tbName];
8530             var cmb = (ComboBox)pnl.Controls["comboSearch"];
8531             var cmbLang = (ComboBox)pnl.Controls["comboLang"];
8532             cmb.Text = cmb.Text.Trim();
8533             // 検索式演算子 OR についてのみ大文字しか認識しないので強制的に大文字とする
8534             var quote = false;
8535             var buf = new StringBuilder();
8536             var c = cmb.Text.ToCharArray();
8537             for (var cnt = 0; cnt < cmb.Text.Length; cnt++)
8538             {
8539                 if (cnt > cmb.Text.Length - 4)
8540                 {
8541                     buf.Append(cmb.Text.Substring(cnt));
8542                     break;
8543                 }
8544                 if (c[cnt] == '"')
8545                 {
8546                     quote = !quote;
8547                 }
8548                 else
8549                 {
8550                     if (!quote && cmb.Text.Substring(cnt, 4).Equals(" or ", StringComparison.OrdinalIgnoreCase))
8551                     {
8552                         buf.Append(" OR ");
8553                         cnt += 3;
8554                         continue;
8555                     }
8556                 }
8557                 buf.Append(c[cnt]);
8558             }
8559             cmb.Text = buf.ToString();
8560
8561             var listView = (DetailsListView)pnl.Parent.Tag;
8562
8563             var queryChanged = tb.SearchWords != cmb.Text || tb.SearchLang != cmbLang.Text;
8564
8565             tb.SearchWords = cmb.Text;
8566             tb.SearchLang = cmbLang.Text;
8567             if (MyCommon.IsNullOrEmpty(cmb.Text))
8568             {
8569                 listView.Focus();
8570                 this.SaveConfigsTabs();
8571                 return;
8572             }
8573             if (queryChanged)
8574             {
8575                 var idx = cmb.Items.IndexOf(tb.SearchWords);
8576                 if (idx > -1) cmb.Items.RemoveAt(idx);
8577                 cmb.Items.Insert(0, tb.SearchWords);
8578                 cmb.Text = tb.SearchWords;
8579                 cmb.SelectAll();
8580                 this.statuses.ClearTabIds(tbName);
8581                 this.listCache?.PurgeCache();
8582                 this.listCache?.UpdateListSize();
8583                 this.SaveConfigsTabs();   // 検索条件の保存
8584             }
8585
8586             listView.Focus();
8587             await this.RefreshTabAsync(tb);
8588         }
8589
8590         private async void RefreshMoreStripMenuItem_Click(object sender, EventArgs e)
8591             => await this.DoRefreshMore(); // もっと前を取得
8592
8593         /// <summary>
8594         /// 指定されたタブのListTabにおける位置を返します
8595         /// </summary>
8596         /// <remarks>
8597         /// 非表示のタブについて -1 が返ることを常に考慮して下さい
8598         /// </remarks>
8599         public int GetTabPageIndex(string tabName)
8600             => this.statuses.Tabs.IndexOf(tabName);
8601
8602         private void UndoRemoveTabMenuItem_Click(object sender, EventArgs e)
8603         {
8604             try
8605             {
8606                 var restoredTab = this.statuses.UndoRemovedTab();
8607                 this.AddNewTab(restoredTab, startup: false);
8608
8609                 var tabIndex = this.statuses.Tabs.Count - 1;
8610                 this.ListTab.SelectedIndex = tabIndex;
8611
8612                 this.SaveConfigsTabs();
8613             }
8614             catch (TabException ex)
8615             {
8616                 MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
8617             }
8618         }
8619
8620         private async Task DoMoveToRTHome()
8621         {
8622             var post = this.CurrentPost;
8623             if (post != null && post.RetweetedId != null)
8624                 await MyCommon.OpenInBrowserAsync(this, "https://twitter.com/" + post.RetweetedBy);
8625         }
8626
8627         private async void RetweetedByOpenInBrowserMenuItem_Click(object sender, EventArgs e)
8628             => await this.DoMoveToRTHome();
8629
8630         private void AuthorListManageMenuItem_Click(object sender, EventArgs e)
8631         {
8632             var screenName = this.CurrentPost?.ScreenName;
8633             if (screenName != null)
8634                 this.ListManageUserContext(screenName);
8635         }
8636
8637         private void RetweetedByListManageMenuItem_Click(object sender, EventArgs e)
8638         {
8639             var screenName = this.CurrentPost?.RetweetedBy;
8640             if (screenName != null)
8641                 this.ListManageUserContext(screenName);
8642         }
8643
8644         public void ListManageUserContext(string screenName)
8645         {
8646             using var listSelectForm = new MyLists(screenName, this.tw.Api);
8647             listSelectForm.ShowDialog(this);
8648         }
8649
8650         private void SearchControls_Enter(object sender, EventArgs e)
8651         {
8652             var pnl = (Control)sender;
8653             foreach (Control ctl in pnl.Controls)
8654             {
8655                 ctl.TabStop = true;
8656             }
8657         }
8658
8659         private void SearchControls_Leave(object sender, EventArgs e)
8660         {
8661             var pnl = (Control)sender;
8662             foreach (Control ctl in pnl.Controls)
8663             {
8664                 ctl.TabStop = false;
8665             }
8666         }
8667
8668         private void PublicSearchQueryMenuItem_Click(object sender, EventArgs e)
8669         {
8670             var tab = this.CurrentTab;
8671             if (tab.TabType != MyCommon.TabUsageType.PublicSearch) return;
8672             this.CurrentTabPage.Controls["panelSearch"].Controls["comboSearch"].Focus();
8673         }
8674
8675         private void StatusLabel_DoubleClick(object sender, EventArgs e)
8676             => MessageBox.Show(this.StatusLabel.TextHistory, "Logs", MessageBoxButtons.OK, MessageBoxIcon.None);
8677
8678         private void HashManageMenuItem_Click(object sender, EventArgs e)
8679         {
8680             DialogResult rslt;
8681             try
8682             {
8683                 rslt = this.HashMgr.ShowDialog();
8684             }
8685             catch (Exception)
8686             {
8687                 return;
8688             }
8689             this.TopMost = this.settings.Common.AlwaysTop;
8690             if (rslt == DialogResult.Cancel) return;
8691             if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash))
8692             {
8693                 this.HashStripSplitButton.Text = this.HashMgr.UseHash;
8694                 this.HashTogglePullDownMenuItem.Checked = true;
8695                 this.HashToggleMenuItem.Checked = true;
8696             }
8697             else
8698             {
8699                 this.HashStripSplitButton.Text = "#[-]";
8700                 this.HashTogglePullDownMenuItem.Checked = false;
8701                 this.HashToggleMenuItem.Checked = false;
8702             }
8703
8704             this.MarkSettingCommonModified();
8705             this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
8706         }
8707
8708         private void HashToggleMenuItem_Click(object sender, EventArgs e)
8709         {
8710             this.HashMgr.ToggleHash();
8711             if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash))
8712             {
8713                 this.HashStripSplitButton.Text = this.HashMgr.UseHash;
8714                 this.HashToggleMenuItem.Checked = true;
8715                 this.HashTogglePullDownMenuItem.Checked = true;
8716             }
8717             else
8718             {
8719                 this.HashStripSplitButton.Text = "#[-]";
8720                 this.HashToggleMenuItem.Checked = false;
8721                 this.HashTogglePullDownMenuItem.Checked = false;
8722             }
8723             this.MarkSettingCommonModified();
8724             this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
8725         }
8726
8727         private void HashStripSplitButton_ButtonClick(object sender, EventArgs e)
8728             => this.HashToggleMenuItem_Click(this.HashToggleMenuItem, EventArgs.Empty);
8729
8730         public void SetPermanentHashtag(string hashtag)
8731         {
8732             this.HashMgr.SetPermanentHash("#" + hashtag);
8733             this.HashStripSplitButton.Text = this.HashMgr.UseHash;
8734             this.HashTogglePullDownMenuItem.Checked = true;
8735             this.HashToggleMenuItem.Checked = true;
8736             // 使用ハッシュタグとして設定
8737             this.MarkSettingCommonModified();
8738         }
8739
8740         private void MenuItemOperate_DropDownOpening(object sender, EventArgs e)
8741         {
8742             var tab = this.CurrentTab;
8743             var post = this.CurrentPost;
8744             if (!this.ExistCurrentPost)
8745             {
8746                 this.ReplyOpMenuItem.Enabled = false;
8747                 this.ReplyAllOpMenuItem.Enabled = false;
8748                 this.DmOpMenuItem.Enabled = false;
8749                 this.CreateTabRuleOpMenuItem.Enabled = false;
8750                 this.CreateIdRuleOpMenuItem.Enabled = false;
8751                 this.CreateSourceRuleOpMenuItem.Enabled = false;
8752                 this.ReadOpMenuItem.Enabled = false;
8753                 this.UnreadOpMenuItem.Enabled = false;
8754                 this.AuthorMenuItem.Visible = false;
8755                 this.RetweetedByMenuItem.Visible = false;
8756             }
8757             else
8758             {
8759                 this.ReplyOpMenuItem.Enabled = true;
8760                 this.ReplyAllOpMenuItem.Enabled = true;
8761                 this.DmOpMenuItem.Enabled = true;
8762                 this.CreateTabRuleOpMenuItem.Enabled = true;
8763                 this.CreateIdRuleOpMenuItem.Enabled = true;
8764                 this.CreateSourceRuleOpMenuItem.Enabled = true;
8765                 this.ReadOpMenuItem.Enabled = true;
8766                 this.UnreadOpMenuItem.Enabled = true;
8767                 this.AuthorMenuItem.Visible = true;
8768                 this.AuthorMenuItem.Text = $"@{post!.ScreenName}";
8769                 this.RetweetedByMenuItem.Visible = post.RetweetedByUserId != null;
8770                 this.RetweetedByMenuItem.Text = $"@{post.RetweetedBy}";
8771             }
8772
8773             if (tab.TabType == MyCommon.TabUsageType.DirectMessage || !this.ExistCurrentPost || post == null || post.IsDm)
8774             {
8775                 this.FavOpMenuItem.Enabled = false;
8776                 this.UnFavOpMenuItem.Enabled = false;
8777                 this.OpenStatusOpMenuItem.Enabled = false;
8778                 this.ShowRelatedStatusesMenuItem2.Enabled = false;
8779                 this.RtOpMenuItem.Enabled = false;
8780                 this.RtUnOpMenuItem.Enabled = false;
8781                 this.QtOpMenuItem.Enabled = false;
8782                 this.FavoriteRetweetMenuItem.Enabled = false;
8783                 this.FavoriteRetweetUnofficialMenuItem.Enabled = false;
8784             }
8785             else
8786             {
8787                 this.FavOpMenuItem.Enabled = true;
8788                 this.UnFavOpMenuItem.Enabled = true;
8789                 this.OpenStatusOpMenuItem.Enabled = true;
8790                 this.ShowRelatedStatusesMenuItem2.Enabled = true;  // PublicSearchの時問題出るかも
8791
8792                 if (!post.CanRetweetBy(this.tw.UserId))
8793                 {
8794                     this.RtOpMenuItem.Enabled = false;
8795                     this.RtUnOpMenuItem.Enabled = false;
8796                     this.QtOpMenuItem.Enabled = false;
8797                     this.FavoriteRetweetMenuItem.Enabled = false;
8798                     this.FavoriteRetweetUnofficialMenuItem.Enabled = false;
8799                 }
8800                 else
8801                 {
8802                     this.RtOpMenuItem.Enabled = true;
8803                     this.RtUnOpMenuItem.Enabled = true;
8804                     this.QtOpMenuItem.Enabled = true;
8805                     this.FavoriteRetweetMenuItem.Enabled = true;
8806                     this.FavoriteRetweetUnofficialMenuItem.Enabled = true;
8807                 }
8808             }
8809
8810             if (tab.TabType != MyCommon.TabUsageType.Favorites)
8811             {
8812                 this.RefreshPrevOpMenuItem.Enabled = true;
8813             }
8814             else
8815             {
8816                 this.RefreshPrevOpMenuItem.Enabled = false;
8817             }
8818             if (!this.ExistCurrentPost || post == null || post.InReplyToStatusId == null)
8819             {
8820                 this.OpenRepSourceOpMenuItem.Enabled = false;
8821             }
8822             else
8823             {
8824                 this.OpenRepSourceOpMenuItem.Enabled = true;
8825             }
8826
8827             if (this.ExistCurrentPost && post != null)
8828             {
8829                 this.DelOpMenuItem.Enabled = post.CanDeleteBy(this.tw.UserId);
8830             }
8831         }
8832
8833         private void MenuItemTab_DropDownOpening(object sender, EventArgs e)
8834             => this.ContextMenuTabProperty_Opening(sender, null!);
8835
8836         public Twitter TwitterInstance
8837             => this.tw;
8838
8839         private void SplitContainer3_SplitterMoved(object sender, SplitterEventArgs e)
8840         {
8841             if (this.initialLayout)
8842                 return;
8843
8844             int splitterDistance;
8845             switch (this.WindowState)
8846             {
8847                 case FormWindowState.Normal:
8848                     splitterDistance = this.SplitContainer3.SplitterDistance;
8849                     break;
8850                 case FormWindowState.Maximized:
8851                     // 最大化時は、通常時のウィンドウサイズに換算した SplitterDistance を算出する
8852                     var normalContainerWidth = this.mySize.Width - SystemInformation.Border3DSize.Width * 2;
8853                     splitterDistance = this.SplitContainer3.SplitterDistance - (this.SplitContainer3.Width - normalContainerWidth);
8854                     splitterDistance = Math.Min(splitterDistance, normalContainerWidth - this.SplitContainer3.SplitterWidth - this.SplitContainer3.Panel2MinSize);
8855                     break;
8856                 default:
8857                     return;
8858             }
8859
8860             this.mySpDis3 = splitterDistance;
8861             this.MarkSettingLocalModified();
8862         }
8863
8864         private void MenuItemEdit_DropDownOpening(object sender, EventArgs e)
8865         {
8866             this.UndoRemoveTabMenuItem.Enabled = this.statuses.CanUndoRemovedTab;
8867
8868             if (this.CurrentTab.TabType == MyCommon.TabUsageType.PublicSearch)
8869                 this.PublicSearchQueryMenuItem.Enabled = true;
8870             else
8871                 this.PublicSearchQueryMenuItem.Enabled = false;
8872
8873             var post = this.CurrentPost;
8874             if (!this.ExistCurrentPost || post == null)
8875             {
8876                 this.CopySTOTMenuItem.Enabled = false;
8877                 this.CopyURLMenuItem.Enabled = false;
8878                 this.CopyUserIdStripMenuItem.Enabled = false;
8879             }
8880             else
8881             {
8882                 this.CopySTOTMenuItem.Enabled = true;
8883                 this.CopyURLMenuItem.Enabled = true;
8884                 this.CopyUserIdStripMenuItem.Enabled = true;
8885
8886                 if (post.IsDm) this.CopyURLMenuItem.Enabled = false;
8887                 if (post.IsProtect) this.CopySTOTMenuItem.Enabled = false;
8888             }
8889         }
8890
8891         private void NotifyIcon1_MouseMove(object sender, MouseEventArgs e)
8892             => this.SetNotifyIconText();
8893
8894         private async void UserStatusToolStripMenuItem_Click(object sender, EventArgs e)
8895             => await this.ShowUserStatus(this.CurrentPost?.ScreenName ?? "");
8896
8897         private async Task DoShowUserStatus(string id, bool showInputDialog)
8898         {
8899             TwitterUser? user = null;
8900
8901             if (showInputDialog)
8902             {
8903                 using var inputName = new InputTabName();
8904                 inputName.FormTitle = "Show UserStatus";
8905                 inputName.FormDescription = Properties.Resources.FRMessage1;
8906                 inputName.TabName = id;
8907
8908                 if (inputName.ShowDialog(this) != DialogResult.OK)
8909                     return;
8910                 if (string.IsNullOrWhiteSpace(inputName.TabName))
8911                     return;
8912
8913                 id = inputName.TabName.Trim();
8914             }
8915
8916             using (var dialog = new WaitingDialog(Properties.Resources.doShowUserStatusText1))
8917             {
8918                 var cancellationToken = dialog.EnableCancellation();
8919
8920                 try
8921                 {
8922                     var task = this.tw.GetUserInfo(id);
8923                     user = await dialog.WaitForAsync(this, task);
8924                 }
8925                 catch (WebApiException ex)
8926                 {
8927                     if (!cancellationToken.IsCancellationRequested)
8928                         MessageBox.Show($"Err:{ex.Message}(UsersShow)");
8929                     return;
8930                 }
8931
8932                 if (cancellationToken.IsCancellationRequested)
8933                     return;
8934             }
8935
8936             await this.DoShowUserStatus(user);
8937         }
8938
8939         private async Task DoShowUserStatus(TwitterUser user)
8940         {
8941             using var userDialog = new UserInfoDialog(this, this.tw.Api, this.detailsHtmlBuilder);
8942             var showUserTask = userDialog.ShowUserAsync(user);
8943             userDialog.ShowDialog(this);
8944
8945             this.Activate();
8946             this.BringToFront();
8947
8948             // ユーザー情報の表示が完了するまで userDialog を破棄しない
8949             await showUserTask;
8950         }
8951
8952         internal Task ShowUserStatus(string id, bool showInputDialog)
8953             => this.DoShowUserStatus(id, showInputDialog);
8954
8955         internal Task ShowUserStatus(string id)
8956             => this.DoShowUserStatus(id, true);
8957
8958         private async void AuthorShowProfileMenuItem_Click(object sender, EventArgs e)
8959         {
8960             var post = this.CurrentPost;
8961             if (post != null)
8962             {
8963                 await this.ShowUserStatus(post.ScreenName, false);
8964             }
8965         }
8966
8967         private async void RetweetedByShowProfileMenuItem_Click(object sender, EventArgs e)
8968         {
8969             var retweetedBy = this.CurrentPost?.RetweetedBy;
8970             if (retweetedBy != null)
8971             {
8972                 await this.ShowUserStatus(retweetedBy, false);
8973             }
8974         }
8975
8976         private async void RtCountMenuItem_Click(object sender, EventArgs e)
8977         {
8978             var post = this.CurrentPost;
8979             if (!this.ExistCurrentPost || post == null)
8980                 return;
8981
8982             var statusId = post.RetweetedId ?? post.StatusId;
8983             TwitterStatus status;
8984
8985             using (var dialog = new WaitingDialog(Properties.Resources.RtCountMenuItem_ClickText1))
8986             {
8987                 var cancellationToken = dialog.EnableCancellation();
8988
8989                 try
8990                 {
8991                     var task = this.tw.Api.StatusesShow(statusId.ToTwitterStatusId());
8992                     status = await dialog.WaitForAsync(this, task);
8993                 }
8994                 catch (WebApiException ex)
8995                 {
8996                     if (!cancellationToken.IsCancellationRequested)
8997                         MessageBox.Show(Properties.Resources.RtCountText2 + Environment.NewLine + "Err:" + ex.Message);
8998                     return;
8999                 }
9000
9001                 if (cancellationToken.IsCancellationRequested)
9002                     return;
9003             }
9004
9005             MessageBox.Show(status.RetweetCount + Properties.Resources.RtCountText1);
9006         }
9007
9008         private void HookGlobalHotkey_HotkeyPressed(object sender, KeyEventArgs e)
9009         {
9010             if ((this.WindowState == FormWindowState.Normal || this.WindowState == FormWindowState.Maximized) && this.Visible && Form.ActiveForm == this)
9011             {
9012                 // アイコン化
9013                 this.Visible = false;
9014             }
9015             else if (Form.ActiveForm == null)
9016             {
9017                 this.Visible = true;
9018                 if (this.WindowState == FormWindowState.Minimized) this.WindowState = FormWindowState.Normal;
9019                 this.Activate();
9020                 this.BringToFront();
9021                 this.StatusText.Focus();
9022             }
9023         }
9024
9025         private void SplitContainer2_MouseDoubleClick(object sender, MouseEventArgs e)
9026             => this.MultiLinePullDownMenuItem.PerformClick();
9027
9028 #region "画像投稿"
9029         private void ImageSelectMenuItem_Click(object sender, EventArgs e)
9030         {
9031             if (this.ImageSelector.Visible)
9032                 this.ImageSelector.EndSelection();
9033             else
9034                 this.ImageSelector.BeginSelection();
9035         }
9036
9037         private void SelectMedia_DragEnter(DragEventArgs e)
9038         {
9039             if (this.ImageSelector.Model.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true))
9040             {
9041                 e.Effect = DragDropEffects.Copy;
9042                 return;
9043             }
9044             e.Effect = DragDropEffects.None;
9045         }
9046
9047         private void SelectMedia_DragDrop(DragEventArgs e)
9048         {
9049             this.Activate();
9050             this.BringToFront();
9051
9052             var filePathArray = (string[])e.Data.GetData(DataFormats.FileDrop, false);
9053             this.ImageSelector.BeginSelection();
9054             this.ImageSelector.Model.AddMediaItemFromFilePath(filePathArray);
9055             this.StatusText.Focus();
9056         }
9057
9058         private void ImageSelector_BeginSelecting(object sender, EventArgs e)
9059         {
9060             this.TimelinePanel.Visible = false;
9061             this.TimelinePanel.Enabled = false;
9062         }
9063
9064         private void ImageSelector_EndSelecting(object sender, EventArgs e)
9065         {
9066             this.TimelinePanel.Visible = true;
9067             this.TimelinePanel.Enabled = true;
9068             this.CurrentListView.Focus();
9069         }
9070
9071         private void ImageSelector_FilePickDialogOpening(object sender, EventArgs e)
9072             => this.AllowDrop = false;
9073
9074         private void ImageSelector_FilePickDialogClosed(object sender, EventArgs e)
9075             => this.AllowDrop = true;
9076
9077         private void ImageSelector_SelectedServiceChanged(object sender, EventArgs e)
9078         {
9079             if (this.ImageSelector.Visible)
9080             {
9081                 this.MarkSettingCommonModified();
9082                 this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
9083             }
9084         }
9085
9086         private void ImageSelector_VisibleChanged(object sender, EventArgs e)
9087             => this.StatusText_TextChanged(this.StatusText, EventArgs.Empty);
9088
9089         /// <summary>
9090         /// StatusTextでCtrl+Vが押下された時の処理
9091         /// </summary>
9092         private void ProcClipboardFromStatusTextWhenCtrlPlusV()
9093         {
9094             try
9095             {
9096                 if (Clipboard.ContainsText())
9097                 {
9098                     // clipboardにテキストがある場合は貼り付け処理
9099                     this.StatusText.Paste(Clipboard.GetText());
9100                 }
9101                 else if (Clipboard.ContainsImage())
9102                 {
9103                     // clipboardから画像を取得
9104                     using var image = Clipboard.GetImage();
9105                     this.ImageSelector.BeginSelection();
9106                     this.ImageSelector.Model.AddMediaItemFromImage(image);
9107                 }
9108                 else if (Clipboard.ContainsFileDropList())
9109                 {
9110                     var files = Clipboard.GetFileDropList().Cast<string>().ToArray();
9111                     this.ImageSelector.BeginSelection();
9112                     this.ImageSelector.Model.AddMediaItemFromFilePath(files);
9113                 }
9114             }
9115             catch (ExternalException ex)
9116             {
9117                 MessageBox.Show(ex.Message);
9118             }
9119         }
9120 #endregion
9121
9122         private void ListManageToolStripMenuItem_Click(object sender, EventArgs e)
9123         {
9124             using var form = new ListManage(this.tw);
9125             form.ShowDialog(this);
9126         }
9127
9128         private bool ModifySettingCommon { get; set; }
9129
9130         private bool ModifySettingLocal { get; set; }
9131
9132         private bool ModifySettingAtId { get; set; }
9133
9134         private void MenuItemCommand_DropDownOpening(object sender, EventArgs e)
9135         {
9136             var post = this.CurrentPost;
9137             if (this.ExistCurrentPost && post != null && !post.IsDm)
9138                 this.RtCountMenuItem.Enabled = true;
9139             else
9140                 this.RtCountMenuItem.Enabled = false;
9141         }
9142
9143         private void CopyUserIdStripMenuItem_Click(object sender, EventArgs e)
9144             => this.CopyUserId();
9145
9146         private void CopyUserId()
9147         {
9148             var post = this.CurrentPost;
9149             if (post == null) return;
9150             var clstr = post.ScreenName;
9151             try
9152             {
9153                 Clipboard.SetDataObject(clstr, false, 5, 100);
9154             }
9155             catch (Exception ex)
9156             {
9157                 MessageBox.Show(ex.Message);
9158             }
9159         }
9160
9161         private async void ShowRelatedStatusesMenuItem_Click(object sender, EventArgs e)
9162         {
9163             var post = this.CurrentPost;
9164             if (this.ExistCurrentPost && post != null && !post.IsDm)
9165             {
9166                 try
9167                 {
9168                     await this.OpenRelatedTab(post);
9169                 }
9170                 catch (TabException ex)
9171                 {
9172                     MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
9173                 }
9174             }
9175         }
9176
9177         /// <summary>
9178         /// 指定されたツイートに対する関連発言タブを開きます
9179         /// </summary>
9180         /// <param name="statusId">表示するツイートのID</param>
9181         /// <exception cref="TabException">名前の重複が多すぎてタブを作成できない場合</exception>
9182         public async Task OpenRelatedTab(PostId statusId)
9183         {
9184             var post = this.statuses[statusId];
9185             if (post == null)
9186             {
9187                 try
9188                 {
9189                     post = await this.tw.GetStatusApi(false, statusId.ToTwitterStatusId());
9190                 }
9191                 catch (WebApiException ex)
9192                 {
9193                     this.StatusLabel.Text = $"Err:{ex.Message}(GetStatus)";
9194                     return;
9195                 }
9196             }
9197
9198             await this.OpenRelatedTab(post);
9199         }
9200
9201         /// <summary>
9202         /// 指定されたツイートに対する関連発言タブを開きます
9203         /// </summary>
9204         /// <param name="post">表示する対象となるツイート</param>
9205         /// <exception cref="TabException">名前の重複が多すぎてタブを作成できない場合</exception>
9206         private async Task OpenRelatedTab(PostClass post)
9207         {
9208             var tabRelated = this.statuses.GetTabByType<RelatedPostsTabModel>();
9209             if (tabRelated != null)
9210             {
9211                 this.RemoveSpecifiedTab(tabRelated.TabName, confirm: false);
9212             }
9213
9214             var tabName = this.statuses.MakeTabName("Related Tweets");
9215
9216             tabRelated = new RelatedPostsTabModel(tabName, post)
9217             {
9218                 UnreadManage = false,
9219                 Notify = false,
9220             };
9221
9222             this.statuses.AddTab(tabRelated);
9223             this.AddNewTab(tabRelated, startup: false);
9224
9225             this.ListTab.SelectedIndex = this.statuses.Tabs.IndexOf(tabName);
9226
9227             await this.RefreshTabAsync(tabRelated);
9228
9229             var tabIndex = this.statuses.Tabs.IndexOf(tabRelated.TabName);
9230
9231             if (tabIndex != -1)
9232             {
9233                 // TODO: 非同期更新中にタブが閉じられている場合を厳密に考慮したい
9234
9235                 var tabPage = this.ListTab.TabPages[tabIndex];
9236                 var listView = (DetailsListView)tabPage.Tag;
9237                 var targetPost = tabRelated.TargetPost;
9238                 var index = tabRelated.IndexOf(targetPost.RetweetedId ?? targetPost.StatusId);
9239
9240                 if (index != -1 && index < listView.Items.Count)
9241                 {
9242                     listView.SelectedIndices.Add(index);
9243                     listView.Items[index].Focused = true;
9244                 }
9245             }
9246         }
9247
9248         private void CacheInfoMenuItem_Click(object sender, EventArgs e)
9249         {
9250             var buf = new StringBuilder();
9251             buf.AppendFormat("キャッシュエントリ保持数     : {0}" + Environment.NewLine, this.iconCache.CacheCount);
9252             buf.AppendFormat("キャッシュエントリ破棄数     : {0}" + Environment.NewLine, this.iconCache.CacheRemoveCount);
9253             MessageBox.Show(buf.ToString(), "アイコンキャッシュ使用状況");
9254         }
9255
9256         private void TweenRestartMenuItem_Click(object sender, EventArgs e)
9257         {
9258             MyCommon.EndingFlag = true;
9259             try
9260             {
9261                 this.Close();
9262                 Application.Restart();
9263             }
9264             catch (Exception)
9265             {
9266                 MessageBox.Show("Failed to restart. Please run " + ApplicationSettings.ApplicationName + " manually.");
9267             }
9268         }
9269
9270         private async void OpenOwnHomeMenuItem_Click(object sender, EventArgs e)
9271             => await MyCommon.OpenInBrowserAsync(this, MyCommon.TwitterUrl + this.tw.Username);
9272
9273         private bool ExistCurrentPost
9274         {
9275             get
9276             {
9277                 var post = this.CurrentPost;
9278                 return post != null && !post.IsDeleted;
9279             }
9280         }
9281
9282         private async void AuthorShowUserTimelineMenuItem_Click(object sender, EventArgs e)
9283             => await this.ShowUserTimeline();
9284
9285         private async void RetweetedByShowUserTimelineMenuItem_Click(object sender, EventArgs e)
9286             => await this.ShowRetweeterTimeline();
9287
9288         private string GetUserIdFromCurPostOrInput(string caption)
9289         {
9290             var id = this.CurrentPost?.ScreenName ?? "";
9291
9292             using var inputName = new InputTabName();
9293             inputName.FormTitle = caption;
9294             inputName.FormDescription = Properties.Resources.FRMessage1;
9295             inputName.TabName = id;
9296
9297             if (inputName.ShowDialog() == DialogResult.OK &&
9298                 !MyCommon.IsNullOrEmpty(inputName.TabName.Trim()))
9299             {
9300                 id = inputName.TabName.Trim();
9301             }
9302             else
9303             {
9304                 id = "";
9305             }
9306             return id;
9307         }
9308
9309         private async void UserTimelineToolStripMenuItem_Click(object sender, EventArgs e)
9310         {
9311             var id = this.GetUserIdFromCurPostOrInput("Show UserTimeline");
9312             if (!MyCommon.IsNullOrEmpty(id))
9313             {
9314                 await this.AddNewTabForUserTimeline(id);
9315             }
9316         }
9317
9318         private void SystemEvents_PowerModeChanged(object sender, Microsoft.Win32.PowerModeChangedEventArgs e)
9319         {
9320             if (e.Mode == Microsoft.Win32.PowerModes.Resume)
9321                 this.timelineScheduler.SystemResumed();
9322         }
9323
9324         private void SystemEvents_TimeChanged(object sender, EventArgs e)
9325         {
9326             var prevTimeOffset = TimeZoneInfo.Local.BaseUtcOffset;
9327
9328             TimeZoneInfo.ClearCachedData();
9329
9330             var curTimeOffset = TimeZoneInfo.Local.BaseUtcOffset;
9331
9332             if (curTimeOffset != prevTimeOffset)
9333             {
9334                 // タイムゾーンの変更を反映
9335                 this.listCache?.PurgeCache();
9336                 this.CurrentListView.Refresh();
9337
9338                 this.DispSelectedPost(forceupdate: true);
9339             }
9340
9341             this.timelineScheduler.Reset();
9342         }
9343
9344         private void TimelineRefreshEnableChange(bool isEnable)
9345         {
9346             this.timelineScheduler.Enabled = isEnable;
9347         }
9348
9349         private void StopRefreshAllMenuItem_CheckedChanged(object sender, EventArgs e)
9350             => this.TimelineRefreshEnableChange(!this.StopRefreshAllMenuItem.Checked);
9351
9352         private async Task OpenUserAppointUrl()
9353         {
9354             if (!MyCommon.IsNullOrEmpty(this.settings.Common.UserAppointUrl))
9355             {
9356                 if (this.settings.Common.UserAppointUrl.Contains("{ID}") || this.settings.Common.UserAppointUrl.Contains("{STATUS}"))
9357                 {
9358                     var post = this.CurrentPost;
9359                     if (post != null)
9360                     {
9361                         var xUrl = this.settings.Common.UserAppointUrl;
9362                         xUrl = xUrl.Replace("{ID}", post.ScreenName);
9363
9364                         var statusId = post.RetweetedId ?? post.StatusId;
9365                         xUrl = xUrl.Replace("{STATUS}", statusId.Id);
9366
9367                         await MyCommon.OpenInBrowserAsync(this, xUrl);
9368                     }
9369                 }
9370                 else
9371                 {
9372                     await MyCommon.OpenInBrowserAsync(this, this.settings.Common.UserAppointUrl);
9373                 }
9374             }
9375         }
9376
9377         private async void OpenUserSpecifiedUrlMenuItem_Click(object sender, EventArgs e)
9378             => await this.OpenUserAppointUrl();
9379
9380         private async void GrowlHelper_Callback(object sender, GrowlHelper.NotifyCallbackEventArgs e)
9381         {
9382             if (Form.ActiveForm == null)
9383             {
9384                 await this.InvokeAsync(() =>
9385                 {
9386                     this.Visible = true;
9387                     if (this.WindowState == FormWindowState.Minimized) this.WindowState = FormWindowState.Normal;
9388                     this.Activate();
9389                     this.BringToFront();
9390                     if (e.NotifyType == GrowlHelper.NotifyType.DirectMessage)
9391                     {
9392                         if (!this.GoDirectMessage(new TwitterStatusId(e.StatusId))) this.StatusText.Focus();
9393                     }
9394                     else
9395                     {
9396                         if (!this.GoStatus(new TwitterStatusId(e.StatusId))) this.StatusText.Focus();
9397                     }
9398                 });
9399             }
9400         }
9401
9402         private void ReplaceAppName()
9403         {
9404             this.MatomeMenuItem.Text = MyCommon.ReplaceAppName(this.MatomeMenuItem.Text);
9405             this.AboutMenuItem.Text = MyCommon.ReplaceAppName(this.AboutMenuItem.Text);
9406         }
9407
9408         private async void TwitterApiStatusToolStripMenuItem_Click(object sender, EventArgs e)
9409             => await MyCommon.OpenInBrowserAsync(this, Twitter.ServiceAvailabilityStatusUrl);
9410
9411         private void PostButton_KeyDown(object sender, KeyEventArgs e)
9412         {
9413             if (e.KeyCode == Keys.Space)
9414             {
9415                 this.JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty);
9416
9417                 e.SuppressKeyPress = true;
9418             }
9419         }
9420
9421         private void ContextMenuColumnHeader_Opening(object sender, CancelEventArgs e)
9422         {
9423             this.IconSizeNoneToolStripMenuItem.Checked = this.settings.Common.IconSize == MyCommon.IconSizes.IconNone;
9424             this.IconSize16ToolStripMenuItem.Checked = this.settings.Common.IconSize == MyCommon.IconSizes.Icon16;
9425             this.IconSize24ToolStripMenuItem.Checked = this.settings.Common.IconSize == MyCommon.IconSizes.Icon24;
9426             this.IconSize48ToolStripMenuItem.Checked = this.settings.Common.IconSize == MyCommon.IconSizes.Icon48;
9427             this.IconSize48_2ToolStripMenuItem.Checked = this.settings.Common.IconSize == MyCommon.IconSizes.Icon48_2;
9428
9429             this.LockListSortOrderToolStripMenuItem.Checked = this.settings.Common.SortOrderLock;
9430         }
9431
9432         private void IconSizeNoneToolStripMenuItem_Click(object sender, EventArgs e)
9433             => this.ChangeListViewIconSize(MyCommon.IconSizes.IconNone);
9434
9435         private void IconSize16ToolStripMenuItem_Click(object sender, EventArgs e)
9436             => this.ChangeListViewIconSize(MyCommon.IconSizes.Icon16);
9437
9438         private void IconSize24ToolStripMenuItem_Click(object sender, EventArgs e)
9439             => this.ChangeListViewIconSize(MyCommon.IconSizes.Icon24);
9440
9441         private void IconSize48ToolStripMenuItem_Click(object sender, EventArgs e)
9442             => this.ChangeListViewIconSize(MyCommon.IconSizes.Icon48);
9443
9444         private void IconSize48_2ToolStripMenuItem_Click(object sender, EventArgs e)
9445             => this.ChangeListViewIconSize(MyCommon.IconSizes.Icon48_2);
9446
9447         private void ChangeListViewIconSize(MyCommon.IconSizes iconSize)
9448         {
9449             if (this.settings.Common.IconSize == iconSize) return;
9450
9451             var oldIconCol = this.Use2ColumnsMode;
9452
9453             this.settings.Common.IconSize = iconSize;
9454             this.ApplyListViewIconSize(iconSize);
9455
9456             if (this.Use2ColumnsMode != oldIconCol)
9457             {
9458                 foreach (TabPage tp in this.ListTab.TabPages)
9459                 {
9460                     this.ResetColumns((DetailsListView)tp.Tag);
9461                 }
9462             }
9463
9464             this.CurrentListView.Refresh();
9465             this.MarkSettingCommonModified();
9466         }
9467
9468         private void LockListSortToolStripMenuItem_Click(object sender, EventArgs e)
9469         {
9470             var state = this.LockListSortOrderToolStripMenuItem.Checked;
9471             if (this.settings.Common.SortOrderLock == state) return;
9472
9473             this.settings.Common.SortOrderLock = state;
9474             this.MarkSettingCommonModified();
9475         }
9476
9477         private void TweetDetailsView_StatusChanged(object sender, TweetDetailsViewStatusChengedEventArgs e)
9478         {
9479             if (!MyCommon.IsNullOrEmpty(e.StatusText))
9480             {
9481                 this.StatusLabelUrl.Text = e.StatusText;
9482             }
9483             else
9484             {
9485                 this.SetStatusLabelUrl();
9486             }
9487         }
9488     }
9489 }