OSDN Git Service

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