OSDN Git Service

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