OSDN Git Service

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