OSDN Git Service

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