OSDN Git Service

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