OSDN Git Service

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