using System.Windows.Forms;
using OpenTween.Api;
using OpenTween.Api.DataModel;
+using OpenTween.Api.TwitterV2;
using OpenTween.Connection;
using OpenTween.MediaUploadServices;
using OpenTween.Models;
/// <summary>プレビュー区切り位置</summary>
private int mySpDis3;
- /// <summary>アイコンサイズ</summary>
- /// <remarks>
- /// 現在は16、24、48の3種類。将来直接数字指定可能とする
- /// 注:24x24の場合に26と指定しているのはMSゴシック系フォントのための仕様
- /// </remarks>
- private int iconSz;
-
- private bool iconCol; // 1列表示の時true(48サイズのとき)
-
// 雑多なフラグ類
private bool initial; // true:起動時処理中
private bool initialLayout = true;
private readonly object syncObject = new(); // ロック用
private const string DetailHtmlFormatHead =
- "<head><meta http-equiv=\"X-UA-Compatible\" content=\"IE=8\">"
- + "<style type=\"text/css\"><!-- "
+ """<head><meta http-equiv="X-UA-Compatible" content="IE=8">"""
+ + """<style type="text/css"><!-- """
+ "body, p, pre {margin: 0;} "
- + "body {font-family: \"%FONT_FAMILY%\", sans-serif; font-size: %FONT_SIZE%pt; background-color:rgb(%BG_COLOR%); word-wrap: break-word; color:rgb(%FONT_COLOR%);} "
+ + """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%);} """
+ "pre {font-family: inherit;} "
+ "a:link, a:visited, a:active, a:hover {color:rgb(%LINK_COLOR%); } "
+ "img.emoji {width: 1em; height: 1em; margin: 0 .05em 0 .1em; vertical-align: -0.1em; border: none;} "
private readonly SettingManager settings;
// twitter解析部
- private readonly TwitterApi twitterApi = new(ApplicationSettings.TwitterConsumerKey, ApplicationSettings.TwitterConsumerSecret);
private readonly Twitter tw;
// Growl呼び出し部
public HashtagManage HashMgr = null!;
// 表示フォント、色、アイコン
-
- /// <summary>未読用フォント</summary>
- private Font fntUnread = null!;
-
- /// <summary>未読用文字色</summary>
- private Color clUnread;
-
- /// <summary>既読用フォント</summary>
- private Font fntReaded = null!;
-
- /// <summary>既読用文字色</summary>
- private Color clReaded;
-
- /// <summary>Fav用文字色</summary>
- private Color clFav;
-
- /// <summary>片思い用文字色</summary>
- private Color clOWL;
-
- /// <summary>Retweet用文字色</summary>
- private Color clRetweet;
-
- /// <summary>選択中の行用文字色</summary>
- private readonly Color clHighLight = Color.FromKnownColor(KnownColor.HighlightText);
-
- /// <summary>発言詳細部用フォント</summary>
- private Font fntDetail = null!;
-
- /// <summary>発言詳細部用色</summary>
- private Color clDetail;
-
- /// <summary>発言詳細部用リンク文字色</summary>
- private Color clDetailLink;
-
- /// <summary>発言詳細部用背景色</summary>
- private Color clDetailBackcolor;
-
- /// <summary>自分の発言用背景色</summary>
- private Color clSelf;
-
- /// <summary>自分宛返信用背景色</summary>
- private Color clAtSelf;
-
- /// <summary>選択発言者の他の発言用背景色</summary>
- private Color clTarget;
-
- /// <summary>選択発言中の返信先用背景色</summary>
- private Color clAtTarget;
-
- /// <summary>選択発言者への返信発言用背景色</summary>
- private Color clAtFromTarget;
-
- /// <summary>選択発言の唯一@先</summary>
- private Color clAtTo;
-
- /// <summary>リスト部通常発言背景色</summary>
- private Color clListBackcolor;
-
- /// <summary>入力欄背景色</summary>
- private Color clInputBackcolor;
-
- /// <summary>入力欄文字色</summary>
- private Color clInputFont;
-
- /// <summary>入力欄フォント</summary>
- private Font fntInputFont = null!;
+ private ThemeManager themeManager;
/// <summary>アイコン画像リスト</summary>
private readonly ImageCache iconCache;
- /// <summary>タスクトレイアイコン:通常時 (At.ico)</summary>
- private Icon nIconAt = null!;
-
- /// <summary>タスクトレイアイコン:通信エラー時 (AtRed.ico)</summary>
- private Icon nIconAtRed = null!;
-
- /// <summary>タスクトレイアイコン:オフライン時 (AtSmoke.ico)</summary>
- private Icon nIconAtSmoke = null!;
-
- /// <summary>タスクトレイアイコン:更新中 (Refresh.ico)</summary>
- private Icon[] nIconRefresh = new Icon[4];
-
- /// <summary>未読のあるタブ用アイコン (Tab.ico)</summary>
- private Icon tabIcon = null!;
-
- /// <summary>画面左上のアイコン (Main.ico)</summary>
- private Icon mainIcon = null!;
+ private readonly IconAssetsManager iconAssets;
- private Icon replyIcon = null!;
- private Icon replyIconBlink = null!;
-
- private readonly ImageList listViewImageList = new(); // ListViewItemの高さ変更用
-
- private PostClass? anchorPost;
- private bool anchorFlag; // true:関連発言移動中(関連移動以外のオペレーションをするとfalseへ。trueだとリスト背景色をアンカー発言選択中として描画)
+ private readonly ThumbnailGenerator thumbGenerator;
/// <summary>発言履歴</summary>
private readonly List<StatusTextHistory> history = new();
// 発言投稿時のAPI引数(発言編集時に設定。手書きreplyでは設定されない)
/// <summary>リプライ先のステータスID・スクリーン名</summary>
- private (long StatusId, string ScreenName)? inReplyTo = null;
+ private (PostId StatusId, string ScreenName)? inReplyTo = null;
// 時速表示用
private readonly List<DateTimeUtc> postTimestamps = new();
private readonly List<DateTimeUtc> favTimestamps = new();
// 以下DrawItem関連
- private readonly SolidBrush brsHighLight = new(Color.FromKnownColor(KnownColor.Highlight));
- private SolidBrush brsBackColorMine = null!;
- private SolidBrush brsBackColorAt = null!;
- private SolidBrush brsBackColorYou = null!;
- private SolidBrush brsBackColorAtYou = null!;
- private SolidBrush brsBackColorAtFromTarget = null!;
- private SolidBrush brsBackColorAtTo = null!;
- private SolidBrush brsBackColorNone = null!;
-
- /// <summary>Listにフォーカスないときの選択行の背景色</summary>
- private readonly SolidBrush brsDeactiveSelection = new(Color.FromKnownColor(KnownColor.ButtonFace));
-
private readonly StringFormat sfTab = new();
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/// <summary>発言保持クラス</summary>
private readonly TabInformations statuses;
- /// <summary>
- /// 現在表示している発言一覧の <see cref="ListView"/> に対するキャッシュ
- /// </summary>
- /// <remarks>
- /// キャッシュクリアのために null が代入されることがあるため、
- /// 使用する場合には <see cref="listItemCache"/> に対して直接メソッド等を呼び出さずに
- /// 一旦ローカル変数に代入してから参照すること。
- /// </remarks>
- private ListViewItemCache? listItemCache = null;
-
- /// <param name="TargetList">アイテムをキャッシュする対象の <see cref="ListView"/></param>
- /// <param name="StartIndex">キャッシュする範囲の開始インデックス</param>
- /// <param name="EndIndex">キャッシュする範囲の終了インデックス</param>
- /// <param name="Cache">ャッシュされた範囲に対応する <see cref="ListViewItem"/> と <see cref="PostClass"/> の組</param>
- internal record class ListViewItemCache(
- ListView TargetList,
- int StartIndex,
- int EndIndex,
- (ListViewItem, PostClass)[] Cache
- )
- {
- /// <summary>キャッシュされたアイテムの件数</summary>
- public int Count
- => this.EndIndex - this.StartIndex + 1;
-
- /// <summary>指定されたインデックスがキャッシュの範囲内であるか判定します</summary>
- /// <returns><paramref name="index"/> がキャッシュの範囲内であれば true、それ以外は false</returns>
- public bool Contains(int index)
- => index >= this.StartIndex && index <= this.EndIndex;
-
- /// <summary>指定されたインデックスの範囲が全てキャッシュの範囲内であるか判定します</summary>
- /// <returns><paramref name="rangeStart"/> から <paramref name="rangeEnd"/> の範囲が全てキャッシュの範囲内であれば true、それ以外は false</returns>
- public bool IsSupersetOf(int rangeStart, int rangeEnd)
- => rangeStart >= this.StartIndex && rangeEnd <= this.EndIndex;
-
- /// <summary>指定されたインデックスの <see cref="ListViewItem"/> と <see cref="PostClass"/> をキャッシュから取得することを試みます</summary>
- /// <returns>取得に成功すれば true、それ以外は false</returns>
- public bool TryGetValue(int index, [NotNullWhen(true)] out ListViewItem? item, [NotNullWhen(true)] out PostClass? post)
- {
- if (this.Contains(index))
- {
- (item, post) = this.Cache[index - this.StartIndex];
- return true;
- }
- else
- {
- item = null;
- post = null;
- return false;
- }
- }
- }
+ private TimelineListViewCache? listCache;
+ private TimelineListViewDrawer? listDrawer;
+ private readonly Dictionary<string, TimelineListViewState> listViewState = new();
private bool isColumnChanged = false;
private List<UrlUndo>? urlUndoBuffer = null;
private readonly record struct ReplyChain(
- long OriginalId,
- long InReplyToId,
+ PostId OriginalId,
+ PostId InReplyToId,
TabModel OriginalTab
);
public PostClass? CurrentPost
=> this.CurrentTab.SelectedPost;
+ public bool Use2ColumnsMode
+ => this.settings.Common.IconSize == MyCommon.IconSizes.Icon48_2;
+
/// <summary>検索処理タイプ</summary>
internal enum SEARCHTYPE
{
private readonly record struct StatusTextHistory(
string Status,
- (long StatusId, string ScreenName)? InReplyTo = null
+ (PostId StatusId, string ScreenName)? InReplyTo = null
);
private readonly HookGlobalHotkey hookGlobalHotkey;
// 後始末
this.SearchDialog.Dispose();
this.urlDialog.Dispose();
- this.nIconAt?.Dispose();
- this.nIconAtRed?.Dispose();
- this.nIconAtSmoke?.Dispose();
- foreach (var iconRefresh in this.nIconRefresh)
- {
- iconRefresh?.Dispose();
- }
- this.tabIcon?.Dispose();
- this.mainIcon?.Dispose();
- this.replyIcon?.Dispose();
- this.replyIconBlink?.Dispose();
- this.listViewImageList.Dispose();
- this.brsHighLight.Dispose();
- this.brsBackColorMine?.Dispose();
- this.brsBackColorAt?.Dispose();
- this.brsBackColorYou?.Dispose();
- this.brsBackColorAtYou?.Dispose();
- this.brsBackColorAtFromTarget?.Dispose();
- this.brsBackColorAtTo?.Dispose();
- this.brsBackColorNone?.Dispose();
- this.brsDeactiveSelection?.Dispose();
+ this.themeManager.Dispose();
this.sfTab.Dispose();
this.timelineScheduler.Dispose();
this.workerCts.Cancel();
-
- if (this.iconCache != null)
- {
- this.iconCache.CancelAsync();
- this.iconCache.Dispose();
- }
-
this.thumbnailTokenSource?.Dispose();
- this.twitterApi.Dispose();
this.hookGlobalHotkey.Dispose();
}
this.disposed = true;
}
- private void LoadIcons()
- {
- // Icons フォルダ以下のアイコンを読み込み(着せ替えアイコン対応)
- var iconsDir = Path.Combine(Application.StartupPath, "Icons");
-
- // ウィンドウ左上のアイコン
- var iconMain = this.LoadIcon(Path.Combine(iconsDir, "MIcon.ico"));
-
- // タブ見出し未読表示アイコン
- var iconTab = this.LoadIcon(Path.Combine(iconsDir, "Tab.ico"));
-
- // タスクトレイ: 通常時アイコン
- var iconAt = this.LoadIcon(Path.Combine(iconsDir, "At.ico"));
-
- // タスクトレイ: エラー時アイコン
- var iconAtRed = this.LoadIcon(Path.Combine(iconsDir, "AtRed.ico"));
-
- // タスクトレイ: オフライン時アイコン
- var iconAtSmoke = this.LoadIcon(Path.Combine(iconsDir, "AtSmoke.ico"));
-
- // タスクトレイ: Reply通知アイコン (最大2枚でアニメーション可能)
- var iconReply = this.LoadIcon(Path.Combine(iconsDir, "Reply.ico"));
- var iconReplyBlink = this.LoadIcon(Path.Combine(iconsDir, "ReplyBlink.ico"));
-
- // タスクトレイ: 更新中アイコン (最大4枚でアニメーション可能)
- var iconRefresh1 = this.LoadIcon(Path.Combine(iconsDir, "Refresh.ico"));
- var iconRefresh2 = this.LoadIcon(Path.Combine(iconsDir, "Refresh2.ico"));
- var iconRefresh3 = this.LoadIcon(Path.Combine(iconsDir, "Refresh3.ico"));
- var iconRefresh4 = this.LoadIcon(Path.Combine(iconsDir, "Refresh4.ico"));
-
- // 読み込んだアイコンを設定 (不足するアイコンはデフォルトのものを設定)
-
- this.mainIcon = iconMain ?? Properties.Resources.MIcon;
- this.tabIcon = iconTab ?? Properties.Resources.TabIcon;
- this.nIconAt = iconAt ?? iconMain ?? Properties.Resources.At;
- this.nIconAtRed = iconAtRed ?? Properties.Resources.AtRed;
- this.nIconAtSmoke = iconAtSmoke ?? Properties.Resources.AtSmoke;
-
- if (iconReply != null && iconReplyBlink != null)
- {
- this.replyIcon = iconReply;
- this.replyIconBlink = iconReplyBlink;
- }
- else
- {
- this.replyIcon = iconReply ?? iconReplyBlink ?? Properties.Resources.Reply;
- this.replyIconBlink = this.nIconAt;
- }
-
- if (iconRefresh1 == null)
- {
- this.nIconRefresh = new[]
- {
- Properties.Resources.Refresh, Properties.Resources.Refresh2,
- Properties.Resources.Refresh3, Properties.Resources.Refresh4,
- };
- }
- else if (iconRefresh2 == null)
- {
- this.nIconRefresh = new[] { iconRefresh1 };
- }
- else if (iconRefresh3 == null)
- {
- this.nIconRefresh = new[] { iconRefresh1, iconRefresh2 };
- }
- else if (iconRefresh4 == null)
- {
- this.nIconRefresh = new[] { iconRefresh1, iconRefresh2, iconRefresh3 };
- }
- else // iconRefresh1 から iconRefresh4 まで全て揃っている
- {
- this.nIconRefresh = new[] { iconRefresh1, iconRefresh2, iconRefresh3, iconRefresh4 };
- }
- }
-
- private Icon? LoadIcon(string filePath)
- {
- if (!File.Exists(filePath))
- return null;
-
- try
- {
- return new Icon(filePath);
- }
- catch (Exception)
- {
- return null;
- }
- }
-
private void InitColumns(ListView list, bool startup)
{
this.InitColumnText();
ColumnHeader[]? columns = null;
try
{
- if (this.iconCol)
+ if (this.Use2ColumnsMode)
{
columns = new[]
{
{
var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width;
- columns[0].Width = ScaleBy(widthScaleFactor, this.settings.Local.Width1);
- columns[1].Width = ScaleBy(widthScaleFactor, this.settings.Local.Width3);
+ columns[0].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[0]);
+ columns[1].Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[2]);
columns[0].DisplayIndex = 0;
columns[1].DisplayIndex = 1;
}
{
var widthScaleFactor = this.CurrentAutoScaleDimensions.Width / this.settings.Local.ScaleDimension.Width;
- columns[0].Width = ScaleBy(widthScaleFactor, this.settings.Local.Width1);
- columns[1].Width = ScaleBy(widthScaleFactor, this.settings.Local.Width2);
- columns[2].Width = ScaleBy(widthScaleFactor, this.settings.Local.Width3);
- columns[3].Width = ScaleBy(widthScaleFactor, this.settings.Local.Width4);
- columns[4].Width = ScaleBy(widthScaleFactor, this.settings.Local.Width5);
- columns[5].Width = ScaleBy(widthScaleFactor, this.settings.Local.Width6);
- columns[6].Width = ScaleBy(widthScaleFactor, this.settings.Local.Width7);
- columns[7].Width = ScaleBy(widthScaleFactor, this.settings.Local.Width8);
-
- var displayIndex = new[]
- {
- this.settings.Local.DisplayIndex1, this.settings.Local.DisplayIndex2,
- this.settings.Local.DisplayIndex3, this.settings.Local.DisplayIndex4,
- this.settings.Local.DisplayIndex5, this.settings.Local.DisplayIndex6,
- this.settings.Local.DisplayIndex7, this.settings.Local.DisplayIndex8,
- };
-
- foreach (var i in Enumerable.Range(0, displayIndex.Length))
+ foreach (var (column, index) in columns.WithIndex())
{
- columns[i].DisplayIndex = displayIndex[i];
+ column.Width = ScaleBy(widthScaleFactor, this.settings.Local.ColumnsWidth[index]);
+ column.DisplayIndex = this.settings.Local.ColumnsOrder[index];
}
}
else
_ => 0,
};
- if (this.iconCol)
+ if (this.Use2ColumnsMode)
{
if (this.statuses.SortOrder == SortOrder.Descending)
{
}
}
- public TweenMain(SettingManager settingManager, TabInformations tabInfo)
+ public TweenMain(
+ SettingManager settingManager,
+ TabInformations tabInfo,
+ Twitter twitter,
+ ImageCache imageCache,
+ IconAssetsManager iconAssets,
+ ThumbnailGenerator thumbGenerator
+ )
{
this.settings = settingManager;
this.statuses = tabInfo;
+ this.tw = twitter;
+ this.iconCache = imageCache;
+ this.iconAssets = iconAssets;
+ this.thumbGenerator = thumbGenerator;
this.InitializeComponent();
this.hookGlobalHotkey = new HookGlobalHotkey(this);
- this.tweetDetailsView.Owner = this;
-
this.hookGlobalHotkey.HotkeyPressed += this.HookGlobalHotkey_HotkeyPressed;
this.gh.NotifyClicked += this.GrowlHelper_Callback;
Regex.CacheSize = 100;
// アイコン設定
- this.LoadIcons();
- this.Icon = this.mainIcon; // メインフォーム(TweenMain)
- this.NotifyIcon1.Icon = this.nIconAt; // タスクトレイ
- this.TabImage.Images.Add(this.tabIcon); // タブ見出し
+ this.Icon = this.iconAssets.IconMain; // メインフォーム(TweenMain)
+ this.NotifyIcon1.Icon = this.iconAssets.IconTray; // タスクトレイ
+ this.TabImage.Images.Add(this.iconAssets.IconTab); // タブ見出し
// <<<<<<<<<設定関連>>>>>>>>>
// 設定読み出し
// 現在の DPI と設定保存時の DPI との比を取得する
var configScaleFactor = this.settings.Local.GetConfigScaleFactor(this.CurrentAutoScaleDimensions);
- // UIフォント設定
- var fontUIGlobal = this.settings.Local.FontUIGlobal;
- if (fontUIGlobal != null)
- {
- OTBaseForm.GlobalFont = fontUIGlobal;
- this.Font = fontUIGlobal;
- }
-
- TwitterApiConnection.RestApiHost = this.settings.Common.TwitterApiHost;
- this.tw = new Twitter(this.twitterApi);
-
// 認証関連
- this.tw.Initialize(this.settings.Common.Token, this.settings.Common.TokenSecret, this.settings.Common.UserName, this.settings.Common.UserId);
+ var account = this.settings.Common.SelectedAccount;
+ if (account != null)
+ this.tw.Initialize(account.GetTwitterAppToken(), account.Token, account.TokenSecret, account.Username, account.UserId);
+ else
+ this.tw.Initialize(TwitterAppToken.GetDefault(), "", "", "", 0L);
this.initial = true;
- Networking.Initialize();
-
- var saveRequired = false;
- var firstRun = false;
-
- // ユーザー名、パスワードが未設定なら設定画面を表示(初回起動時など)
- if (MyCommon.IsNullOrEmpty(this.tw.Username))
- {
- saveRequired = true;
- firstRun = true;
-
- // 設定せずにキャンセルされたか、設定されたが依然ユーザー名が未設定ならプログラム終了
- if (this.ShowSettingDialog(showTaskbarIcon: true) != DialogResult.OK ||
- MyCommon.IsNullOrEmpty(this.tw.Username))
- {
- Application.Exit(); // 強制終了
- }
- }
-
- // Twitter用通信クラス初期化
- Networking.DefaultTimeout = TimeSpan.FromSeconds(this.settings.Common.DefaultTimeOut);
- Networking.UploadImageTimeout = TimeSpan.FromSeconds(this.settings.Common.UploadImageTimeout);
- Networking.SetWebProxy(
- this.settings.Local.ProxyType,
- this.settings.Local.ProxyAddress,
- this.settings.Local.ProxyPort,
- this.settings.Local.ProxyUser,
- this.settings.Local.ProxyPassword);
- Networking.ForceIPv4 = this.settings.Common.ForceIPv4;
-
- TwitterApiConnection.RestApiHost = this.settings.Common.TwitterApiHost;
this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck;
this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost;
- ShortUrl.Instance.DisableExpanding = !this.settings.Common.TinyUrlResolve;
- ShortUrl.Instance.BitlyAccessToken = this.settings.Common.BitlyAccessToken;
- ShortUrl.Instance.BitlyId = this.settings.Common.BilyUser;
- ShortUrl.Instance.BitlyKey = this.settings.Common.BitlyPwd;
// アクセストークンが有効であるか確認する
// ここが Twitter API への最初のアクセスになるようにすること
// サムネイル関連の初期化
// プロキシ設定等の通信まわりの初期化が済んでから処理する
- ThumbnailGenerator.InitializeGenerator();
-
- var imgazyobizinet = ThumbnailGenerator.ImgAzyobuziNetInstance;
+ var imgazyobizinet = this.thumbGenerator.ImgAzyobuziNet;
imgazyobizinet.Enabled = this.settings.Common.EnableImgAzyobuziNet;
imgazyobizinet.DisabledInDM = this.settings.Common.ImgAzyobuziNetDisabledInDM;
+ imgazyobizinet.AutoUpdate = true;
- Thumbnail.Services.TonTwitterCom.GetApiConnection = () => this.twitterApi.Connection;
+ Thumbnail.Services.TonTwitterCom.GetApiConnection = () => this.tw.Api.Connection;
// 画像投稿サービス
- this.ImageSelector.Initialize(this.tw, this.tw.Configuration, this.settings.Common.UseImageServiceName, this.settings.Common.UseImageService);
+ this.ImageSelector.Model.InitializeServices(this.tw, this.tw.Configuration);
+ this.ImageSelector.Model.SelectMediaService(this.settings.Common.UseImageServiceName, this.settings.Common.UseImageService);
+
+ this.tweetThumbnail1.Model.Initialize(this.thumbGenerator);
// ハッシュタグ/@id関連
this.AtIdSupl = new AtIdSupplement(this.settings.AtIdList.AtIdList, "@");
this.settings.Common.HashIsNotAddToAtReply);
if (!MyCommon.IsNullOrEmpty(this.HashMgr.UseHash) && this.HashMgr.IsPermanent) this.HashStripSplitButton.Text = this.HashMgr.UseHash;
- // アイコンリスト作成
- this.iconCache = new ImageCache();
- this.tweetDetailsView.IconCache = this.iconCache;
-
// フォント&文字色&背景色保持
- this.fntUnread = this.settings.Local.FontUnread;
- this.clUnread = this.settings.Local.ColorUnread;
- this.fntReaded = this.settings.Local.FontRead;
- this.clReaded = this.settings.Local.ColorRead;
- this.clFav = this.settings.Local.ColorFav;
- this.clOWL = this.settings.Local.ColorOWL;
- this.clRetweet = this.settings.Local.ColorRetweet;
- this.fntDetail = this.settings.Local.FontDetail;
- this.clDetail = this.settings.Local.ColorDetail;
- this.clDetailLink = this.settings.Local.ColorDetailLink;
- this.clDetailBackcolor = this.settings.Local.ColorDetailBackcolor;
- this.clSelf = this.settings.Local.ColorSelf;
- this.clAtSelf = this.settings.Local.ColorAtSelf;
- this.clTarget = this.settings.Local.ColorTarget;
- this.clAtTarget = this.settings.Local.ColorAtTarget;
- this.clAtFromTarget = this.settings.Local.ColorAtFromTarget;
- this.clAtTo = this.settings.Local.ColorAtTo;
- this.clListBackcolor = this.settings.Local.ColorListBackcolor;
- this.clInputBackcolor = this.settings.Local.ColorInputBackcolor;
- this.clInputFont = this.settings.Local.ColorInputFont;
- this.fntInputFont = this.settings.Local.FontInputFont;
-
- this.brsBackColorMine = new SolidBrush(this.clSelf);
- this.brsBackColorAt = new SolidBrush(this.clAtSelf);
- this.brsBackColorYou = new SolidBrush(this.clTarget);
- this.brsBackColorAtYou = new SolidBrush(this.clAtTarget);
- this.brsBackColorAtFromTarget = new SolidBrush(this.clAtFromTarget);
- this.brsBackColorAtTo = new SolidBrush(this.clAtTo);
- this.brsBackColorNone = new SolidBrush(this.clListBackcolor);
+ this.themeManager = new(this.settings.Local);
+ this.tweetDetailsView.Initialize(this, this.iconCache, this.themeManager);
// StringFormatオブジェクトへの事前設定
this.sfTab.Alignment = StringAlignment.Center;
this.TopMost = this.settings.Common.AlwaysTop;
this.mySpDis = ScaleBy(configScaleFactor.Height, this.settings.Local.SplitterDistance);
this.mySpDis2 = ScaleBy(configScaleFactor.Height, this.settings.Local.StatusTextHeight);
- if (this.settings.Local.PreviewDistance == -1)
- {
- this.mySpDis3 = this.mySize.Width - ScaleBy(this.CurrentScaleFactor.Width, 150);
- if (this.mySpDis3 < 1) this.mySpDis3 = ScaleBy(this.CurrentScaleFactor.Width, 50);
- this.settings.Local.PreviewDistance = this.mySpDis3;
- }
- else
- {
- this.mySpDis3 = ScaleBy(configScaleFactor.Width, this.settings.Local.PreviewDistance);
- }
+ this.mySpDis3 = ScaleBy(configScaleFactor.Width, this.settings.Local.PreviewDistance);
+
this.PlaySoundMenuItem.Checked = this.settings.Common.PlaySound;
this.PlaySoundFileMenuItem.Checked = this.settings.Common.PlaySound;
// 入力欄
- this.StatusText.Font = this.fntInputFont;
- this.StatusText.ForeColor = this.clInputFont;
+ this.StatusText.Font = this.themeManager.FontInputFont;
+ this.StatusText.ForeColor = this.themeManager.ColorInputFont;
// SplitContainer2.Panel2MinSize を一行表示の入力欄の高さに合わせる (MS UI Gothic 12pt (96dpi) の場合は 19px)
this.StatusText.Multiline = false; // this.settings.Local.StatusMultiline の設定は後で反映される
throw new TabException(Properties.Resources.TweenMain_LoadText1);
}
- this.statuses.SelectTab(this.ListTab.SelectedTab.Text);
+ this.ListTabSelect(this.ListTab.SelectedTab);
// タブの位置を調整する
this.SetTabAlignment();
this.ignoreConfigSave = false;
this.TweenMain_Resize(this, EventArgs.Empty);
- if (saveRequired) this.SaveConfigsAll(false);
- if (firstRun)
+ if (this.settings.IsFirstRun)
{
// 初回起動時だけ右下のメニューを目立たせる
this.HashStripSplitButton.ShowDropDown();
{
var htmlTemplate = this.settings.Common.IsMonospace ? DetailHtmlFormatTemplateMono : DetailHtmlFormatTemplateNormal;
+ static string ColorToRGBString(Color color)
+ => $"{color.R},{color.G},{color.B}";
+
this.detailHtmlFormatPreparedTemplate = htmlTemplate
- .Replace("%FONT_FAMILY%", this.fntDetail.Name)
- .Replace("%FONT_SIZE%", this.fntDetail.Size.ToString())
- .Replace("%FONT_COLOR%", $"{this.clDetail.R},{this.clDetail.G},{this.clDetail.B}")
- .Replace("%LINK_COLOR%", $"{this.clDetailLink.R},{this.clDetailLink.G},{this.clDetailLink.B}")
- .Replace("%BG_COLOR%", $"{this.clDetailBackcolor.R},{this.clDetailBackcolor.G},{this.clDetailBackcolor.B}")
- .Replace("%BG_REPLY_COLOR%", $"{this.clAtTo.R}, {this.clAtTo.G}, {this.clAtTo.B}");
+ .Replace("%FONT_FAMILY%", this.themeManager.FontDetail.Name)
+ .Replace("%FONT_SIZE%", this.themeManager.FontDetail.Size.ToString())
+ .Replace("%FONT_COLOR%", ColorToRGBString(this.themeManager.ColorDetail))
+ .Replace("%LINK_COLOR%", ColorToRGBString(this.themeManager.ColorDetailLink))
+ .Replace("%BG_COLOR%", ColorToRGBString(this.themeManager.ColorDetailBackcolor))
+ .Replace("%BG_REPLY_COLOR%", ColorToRGBString(this.themeManager.ColorAtTo));
}
private void ListTab_DrawItem(object sender, DrawItemEventArgs e)
private void LoadConfig()
{
- // v1.2.4 以前の設定には ScaleDimension の項目がないため、現在の DPI と同じとして扱う
- if (this.settings.Local.ScaleDimension.IsEmpty)
- this.settings.Local.ScaleDimension = this.CurrentAutoScaleDimensions;
-
- var tabSettings = this.settings.Tabs;
- foreach (var tabSetting in tabSettings.Tabs)
- {
- TabModel tab;
- switch (tabSetting.TabType)
- {
- case MyCommon.TabUsageType.Home:
- tab = new HomeTabModel(tabSetting.TabName);
- break;
- case MyCommon.TabUsageType.Mentions:
- tab = new MentionsTabModel(tabSetting.TabName);
- break;
- case MyCommon.TabUsageType.DirectMessage:
- tab = new DirectMessagesTabModel(tabSetting.TabName);
- break;
- case MyCommon.TabUsageType.Favorites:
- tab = new FavoritesTabModel(tabSetting.TabName);
- break;
- case MyCommon.TabUsageType.UserDefined:
- tab = new FilterTabModel(tabSetting.TabName);
- break;
- case MyCommon.TabUsageType.UserTimeline:
- tab = new UserTimelineTabModel(tabSetting.TabName, tabSetting.User!);
- break;
- case MyCommon.TabUsageType.PublicSearch:
- tab = new PublicSearchTabModel(tabSetting.TabName)
- {
- SearchWords = tabSetting.SearchWords,
- SearchLang = tabSetting.SearchLang,
- };
- break;
- case MyCommon.TabUsageType.Lists:
- tab = new ListTimelineTabModel(tabSetting.TabName, tabSetting.ListInfo!);
- break;
- case MyCommon.TabUsageType.Mute:
- tab = new MuteTabModel(tabSetting.TabName);
- break;
- default:
- continue;
- }
-
- tab.UnreadManage = tabSetting.UnreadManage;
- tab.Protected = tabSetting.Protected;
- tab.Notify = tabSetting.Notify;
- tab.SoundFile = tabSetting.SoundFile;
-
- if (tab.IsDistributableTabType)
- {
- var filterTab = (FilterTabModel)tab;
- filterTab.FilterArray = tabSetting.FilterArray;
- filterTab.FilterModified = false;
- }
-
- if (this.statuses.ContainsTab(tab.TabName))
- tab.TabName = this.statuses.MakeTabName("MyTab");
-
- this.statuses.AddTab(tab);
- }
-
+ this.statuses.LoadTabsFromSettings(this.settings.Tabs);
this.statuses.AddDefaultTabs();
}
private void RefreshTimeline()
{
- var curTabModel = this.CurrentTab;
var curListView = this.CurrentListView;
// 現在表示中のタブのスクロール位置を退避
- var curListScroll = this.SaveListViewScroll(curListView, curTabModel);
-
- // 各タブのリスト上の選択位置などを退避
- var listSelections = this.SaveListViewSelection();
+ var currentListViewState = this.listViewState[this.CurrentTabName];
+ currentListViewState.Save(this.ListLockMenuItem.Checked);
// 更新確定
int addCount;
if (MyCommon.EndingFlag) return;
// リストに反映&選択状態復元
- foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
+ if (this.listCache != null && (this.listCache.IsListSizeMismatched || isDelete))
{
- var tabPage = this.ListTab.TabPages[index];
- var listView = (DetailsListView)tabPage.Tag;
-
- if (listView.VirtualListSize != tab.AllCount || isDelete)
+ using (ControlTransaction.Update(curListView))
{
- using (ControlTransaction.Update(listView))
- {
- if (listView == curListView)
- this.PurgeListViewItemCache();
+ this.listCache.PurgeCache();
+ this.listCache.UpdateListSize();
- try
- {
- // リスト件数更新
- listView.VirtualListSize = tab.AllCount;
- }
- catch (NullReferenceException ex)
- {
- // WinForms 内部で ListView.set_TopItem が発生させている例外
- // https://ja.osdn.net/ticket/browse.php?group_id=6526&tid=36588
- MyCommon.TraceOut(ex, $"TabType: {tab.TabType}, Count: {tab.AllCount}, ListSize: {listView.VirtualListSize}");
- }
-
- // 選択位置などを復元
- this.RestoreListViewSelection(listView, tab, listSelections[tab.TabName]);
- }
+ // 選択位置などを復元
+ currentListViewState.RestoreSelection();
}
}
}
// スクロール位置を復元
- this.RestoreListViewScroll(curListView, curTabModel, curListScroll);
+ currentListViewState.RestoreScroll();
// 新着通知
this.NotifyNewPosts(notifyPosts, soundFile, addCount, newMentionOrDm);
this.HashSupl.AddRangeItem(this.tw.GetHashList());
}
- internal readonly record struct ListViewScroll(
- ScrollLockMode ScrollLockMode,
- long? TopItemStatusId
- );
-
- internal enum ScrollLockMode
- {
- /// <summary>固定しない</summary>
- None,
-
- /// <summary>最上部に固定する</summary>
- FixedToTop,
-
- /// <summary>最下部に固定する</summary>
- FixedToBottom,
-
- /// <summary><see cref="ListViewScroll.TopItemStatusId"/> の位置に固定する</summary>
- FixedToItem,
- }
-
- /// <summary>
- /// <see cref="ListView"/> のスクロール位置に関する情報を <see cref="ListViewScroll"/> として返します
- /// </summary>
- private ListViewScroll SaveListViewScroll(DetailsListView listView, TabModel tab)
- {
- var lockMode = this.GetScrollLockMode(listView);
- long? topItemStatusId = null;
-
- if (lockMode == ScrollLockMode.FixedToItem)
- {
- var topItemIndex = listView.TopItem?.Index ?? -1;
- if (topItemIndex != -1 && topItemIndex < tab.AllCount)
- topItemStatusId = tab.GetStatusIdAt(topItemIndex);
- }
-
- return new ListViewScroll
- {
- ScrollLockMode = lockMode,
- TopItemStatusId = topItemStatusId,
- };
- }
-
- private ScrollLockMode GetScrollLockMode(DetailsListView listView)
- {
- if (this.statuses.SortMode == ComparerMode.Id)
- {
- if (this.statuses.SortOrder == SortOrder.Ascending)
- {
- // Id昇順
- if (this.ListLockMenuItem.Checked)
- return ScrollLockMode.None;
-
- // 最下行が表示されていたら、最下行へ強制スクロール。最下行が表示されていなかったら制御しない
-
- // 一番下に表示されているアイテム
- var bottomItem = listView.GetItemAt(0, listView.ClientSize.Height - 1);
- if (bottomItem == null || bottomItem.Index == listView.VirtualListSize - 1)
- return ScrollLockMode.FixedToBottom;
- else
- return ScrollLockMode.None;
- }
- else
- {
- // Id降順
- if (this.ListLockMenuItem.Checked)
- return ScrollLockMode.FixedToItem;
-
- // 最上行が表示されていたら、制御しない。最上行が表示されていなかったら、現在表示位置へ強制スクロール
- var topItem = listView.TopItem;
- if (topItem == null || topItem.Index == 0)
- return ScrollLockMode.FixedToTop;
- else
- return ScrollLockMode.FixedToItem;
- }
- }
- else
- {
- return ScrollLockMode.FixedToItem;
- }
- }
-
- internal readonly record struct ListViewSelection(
- long[]? SelectedStatusIds,
- long? SelectionMarkStatusId,
- long? FocusedStatusId
- );
-
- /// <summary>
- /// <see cref="ListView"/> の選択状態を <see cref="ListViewSelection"/> として返します
- /// </summary>
- private IReadOnlyDictionary<string, ListViewSelection> SaveListViewSelection()
- {
- var listsDict = new Dictionary<string, ListViewSelection>();
-
- foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
- {
- var listView = (DetailsListView)this.ListTab.TabPages[index].Tag;
- listsDict[tab.TabName] = this.SaveListViewSelection(listView, tab);
- }
-
- return listsDict;
- }
-
- /// <summary>
- /// <see cref="ListView"/> の選択状態を <see cref="ListViewSelection"/> として返します
- /// </summary>
- private ListViewSelection SaveListViewSelection(DetailsListView listView, TabModel tab)
- {
- if (listView.VirtualListSize == 0)
- {
- return new ListViewSelection
- {
- SelectedStatusIds = Array.Empty<long>(),
- SelectionMarkStatusId = null,
- FocusedStatusId = null,
- };
- }
-
- return new ListViewSelection
- {
- SelectedStatusIds = tab.SelectedStatusIds,
- FocusedStatusId = this.GetFocusedStatusId(listView, tab),
- SelectionMarkStatusId = this.GetSelectionMarkStatusId(listView, tab),
- };
- }
-
- private long? GetFocusedStatusId(DetailsListView listView, TabModel tab)
- {
- var index = listView.FocusedItem?.Index ?? -1;
-
- return index != -1 && index < tab.AllCount ? tab.GetStatusIdAt(index) : (long?)null;
- }
-
- private long? GetSelectionMarkStatusId(DetailsListView listView, TabModel tab)
- {
- var index = listView.SelectionMark;
-
- return index != -1 && index < tab.AllCount ? tab.GetStatusIdAt(index) : (long?)null;
- }
-
- /// <summary>
- /// <see cref="SaveListViewScroll"/> によって保存されたスクロール位置を復元します
- /// </summary>
- private void RestoreListViewScroll(DetailsListView listView, TabModel tab, ListViewScroll listScroll)
- {
- if (listView.VirtualListSize == 0)
- return;
-
- switch (listScroll.ScrollLockMode)
- {
- case ScrollLockMode.FixedToTop:
- listView.EnsureVisible(0);
- break;
- case ScrollLockMode.FixedToBottom:
- listView.EnsureVisible(listView.VirtualListSize - 1);
- break;
- case ScrollLockMode.FixedToItem:
- var topIndex = listScroll.TopItemStatusId != null ? tab.IndexOf(listScroll.TopItemStatusId.Value) : -1;
- if (topIndex != -1)
- {
- var topItem = listView.Items[topIndex];
- try
- {
- listView.TopItem = topItem;
- }
- catch (NullReferenceException)
- {
- listView.EnsureVisible(listView.VirtualListSize - 1);
- listView.EnsureVisible(topIndex);
- }
- }
- break;
- case ScrollLockMode.None:
- default:
- break;
- }
- }
-
- /// <summary>
- /// <see cref="SaveListViewSelection"/> によって保存された選択状態を復元します
- /// </summary>
- private void RestoreListViewSelection(DetailsListView listView, TabModel tab, ListViewSelection listSelection)
- {
- // status_id から ListView 上のインデックスに変換
- int[]? selectedIndices = null;
- if (listSelection.SelectedStatusIds != null)
- selectedIndices = tab.IndexOf(listSelection.SelectedStatusIds).Where(x => x != -1).ToArray();
-
- var focusedIndex = -1;
- if (listSelection.FocusedStatusId != null)
- focusedIndex = tab.IndexOf(listSelection.FocusedStatusId.Value);
-
- var selectionMarkIndex = -1;
- if (listSelection.SelectionMarkStatusId != null)
- selectionMarkIndex = tab.IndexOf(listSelection.SelectionMarkStatusId.Value);
-
- this.SelectListItem(listView, selectedIndices, focusedIndex, selectionMarkIndex);
- }
-
private bool BalloonRequired()
{
if (this.initial)
if (MyCommon.IsNullOrEmpty(bText)) return;
var image = this.iconCache.TryGetFromCache(post.ImageUrl);
- this.gh.Notify(nt, post.StatusId.ToString(), title.ToString(), bText, image?.Image, post.ImageUrl);
+ this.gh.Notify(nt, post.StatusId.Id, title.ToString(), bText, image?.Image, post.ImageUrl);
}
}
else
}
}
- private async void MyList_SelectedIndexChanged(object sender, EventArgs e)
- {
- var listView = this.CurrentListView;
- if (listView != sender)
- return;
-
- var indices = listView.SelectedIndices.Cast<int>().ToArray();
- this.CurrentTab.SelectPosts(indices);
-
- if (indices.Length != 1)
- return;
-
- var index = indices[0];
- if (index > listView.VirtualListSize - 1) return;
-
- this.PushSelectPostChain();
-
- var post = this.CurrentPost!;
- this.statuses.SetReadAllTab(post.StatusId, read: true);
-
- // キャッシュの書き換え
- this.ChangeCacheStyleRead(true, index); // 既読へ(フォント、文字色)
-
- this.ColorizeList();
- await this.selectionDebouncer.Call();
- }
-
- private void ChangeCacheStyleRead(bool read, int index)
- {
- var tabInfo = this.CurrentTab;
- // Read:true=既読 false=未読
- // 未読管理していなかったら既読として扱う
- if (!tabInfo.UnreadManage ||
- !this.settings.Common.UnreadManage) read = true;
-
- var listCache = this.listItemCache;
- if (listCache == null)
- return;
-
- // キャッシュに含まれていないアイテムは対象外
- if (!listCache.TryGetValue(index, out var itm, out var post))
- return;
-
- this.ChangeItemStyleRead(read, itm, post, (DetailsListView)listCache.TargetList);
- }
-
- private void ChangeItemStyleRead(bool read, ListViewItem item, PostClass post, DetailsListView? dList)
- {
- Font fnt;
- string star;
- // フォント
- if (read)
- {
- fnt = this.fntReaded;
- star = "";
- }
- else
- {
- fnt = this.fntUnread;
- star = "★";
- }
- if (item.SubItems[5].Text != star)
- item.SubItems[5].Text = star;
-
- // 文字色
- Color cl;
- if (post.IsFav)
- cl = this.clFav;
- else if (post.RetweetedId != null)
- cl = this.clRetweet;
- else if (post.IsOwl && (post.IsDm || this.settings.Common.OneWayLove))
- cl = this.clOWL;
- else if (read || !this.settings.Common.UseUnreadStyle)
- cl = this.clReaded;
- else
- cl = this.clUnread;
-
- if (dList == null || item.Index == -1)
- {
- item.ForeColor = cl;
- if (this.settings.Common.UseUnreadStyle)
- item.Font = fnt;
- }
- else
- {
- dList.Update();
- if (this.settings.Common.UseUnreadStyle)
- dList.ChangeItemFontAndColor(item, cl, fnt);
- else
- dList.ChangeItemForeColor(item, cl);
- }
- }
-
- private void ColorizeList()
+ private async void MyList_SelectedIndexChanged(object sender, EventArgs e)
{
- // Index:更新対象のListviewItem.Index。Colorを返す。
- // -1は全キャッシュ。Colorは返さない(ダミーを戻す)
- PostClass? post;
- if (this.anchorFlag)
- post = this.anchorPost;
- else
- post = this.CurrentPost;
-
- if (post == null) return;
-
- var listCache = this.listItemCache;
- if (listCache == null)
+ var listView = this.CurrentListView;
+ if (listView != sender)
return;
- var listView = (DetailsListView)listCache.TargetList;
-
- // ValidateRectが呼ばれる前に選択色などの描画を済ませておく
- listView.Update();
+ var indices = listView.SelectedIndices.Cast<int>().ToArray();
+ this.CurrentTab.SelectPosts(indices);
- foreach (var (listViewItem, cachedPost) in listCache.Cache)
- {
- var backColor = this.JudgeColor(post, cachedPost);
- listView.ChangeItemBackColor(listViewItem, backColor);
- }
- }
+ if (indices.Length != 1)
+ return;
- private void ColorizeList(ListViewItem item, PostClass post)
- {
- // Index:更新対象のListviewItem.Index。Colorを返す。
- // -1は全キャッシュ。Colorは返さない(ダミーを戻す)
- PostClass? basePost;
- if (this.anchorFlag)
- basePost = this.anchorPost;
- else
- basePost = this.CurrentPost;
+ var index = indices[0];
+ if (index > listView.VirtualListSize - 1) return;
- if (basePost == null) return;
+ this.PushSelectPostChain();
- if (item.Index == -1)
- item.BackColor = this.JudgeColor(basePost, post);
- else
- this.CurrentListView.ChangeItemBackColor(item, this.JudgeColor(basePost, post));
- }
-
- private Color JudgeColor(PostClass basePost, PostClass targetPost)
- {
- Color cl;
- if (targetPost.StatusId == basePost.InReplyToStatusId)
- // @先
- cl = this.clAtTo;
- else if (targetPost.IsMe)
- // 自分=発言者
- cl = this.clSelf;
- else if (targetPost.IsReply)
- // 自分宛返信
- cl = this.clAtSelf;
- else if (basePost.ReplyToList.Any(x => x.UserId == targetPost.UserId))
- // 返信先
- cl = this.clAtFromTarget;
- else if (targetPost.ReplyToList.Any(x => x.UserId == basePost.UserId))
- // その人への返信
- cl = this.clAtTarget;
- else if (targetPost.UserId == basePost.UserId)
- // 発言者
- cl = this.clTarget;
- else
- // その他
- cl = this.clListBackcolor;
+ var post = this.CurrentPost!;
+ this.statuses.SetReadAllTab(post.StatusId, read: true);
- return cl;
+ this.listCache?.RefreshStyle();
+ await this.selectionDebouncer.Call();
}
private void StatusTextHistoryBack()
var status = new PostStatusParams();
var statusTextCompat = this.FormatStatusText(this.StatusText.Text);
- if (this.GetRestStatusCount(statusTextCompat) >= 0)
+ if (this.GetRestStatusCount(statusTextCompat) >= 0 && this.tw.Api.AppToken.AuthType == APIAuthType.OAuth1)
{
// auto_populate_reply_metadata や attachment_url を使用しなくても 140 字以内に
// 収まる場合はこれらのオプションを使用せずに投稿する
if (!this.ImageSelector.TryGetSelectedMedia(out var serviceName, out uploadItems))
return;
- uploadService = this.ImageSelector.GetService(serviceName);
+ this.ImageSelector.EndSelection();
+ uploadService = this.ImageSelector.Model.GetService(serviceName);
}
this.inReplyTo = null;
}
}
- private async Task FavAddAsync(long statusId, TabModel tab)
+ private async Task FavAddAsync(PostId statusId, TabModel tab)
{
await this.workerSemaphore.WaitAsync();
}
}
- private async Task FavAddAsyncInternal(IProgress<string> p, CancellationToken ct, long statusId, TabModel tab)
+ private async Task FavAddAsyncInternal(IProgress<string> p, CancellationToken ct, PostId statusId, TabModel tab)
{
if (ct.IsCancellationRequested)
return;
try
{
+ var twitterStatusId = (post.RetweetedId ?? post.StatusId).ToTwitterStatusId();
try
{
- await this.twitterApi.FavoritesCreate(post.RetweetedId ?? post.StatusId)
+ await this.tw.Api.FavoritesCreate(twitterStatusId)
.IgnoreResponse()
.ConfigureAwait(false);
}
if (this.settings.Common.RestrictFavCheck)
{
- var status = await this.twitterApi.StatusesShow(post.RetweetedId ?? post.StatusId)
+ var status = await this.tw.Api.StatusesShow(twitterStatusId)
.ConfigureAwait(false);
if (status.Favorited != true)
{
var idx = tab.IndexOf(statusId);
if (idx != -1)
- this.ChangeCacheStyleRead(post.IsRead, idx);
+ this.listCache?.RefreshStyle(idx);
}
var currentPost = this.CurrentPost;
}
}
- private async Task FavRemoveAsync(IReadOnlyList<long> statusIds, TabModel tab)
+ private async Task FavRemoveAsync(IReadOnlyList<PostId> statusIds, TabModel tab)
{
await this.workerSemaphore.WaitAsync();
}
}
- private async Task FavRemoveAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<long> statusIds, TabModel tab)
+ private async Task FavRemoveAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<PostId> statusIds, TabModel tab)
{
if (ct.IsCancellationRequested)
return;
if (!CheckAccountValid())
throw new WebApiException("Auth error. Check your account");
- var successIds = new List<long>();
+ var successIds = new List<PostId>();
await Task.Run(async () =>
{
if (!post.IsFav)
continue;
+ var twitterStatusId = (post.RetweetedId ?? post.StatusId).ToTwitterStatusId();
+
try
{
- await this.twitterApi.FavoritesDestroy(post.RetweetedId ?? post.StatusId)
+ await this.tw.Api.FavoritesDestroy(twitterStatusId)
.IgnoreResponse()
.ConfigureAwait(false);
}
foreach (var statusId in successIds)
{
var idx = tab.IndexOf(statusId);
- if (idx == -1)
- continue;
-
- var post = tab.Posts[statusId];
- this.ChangeCacheStyleRead(post.IsRead, idx);
+ if (idx != -1)
+ this.listCache?.RefreshStyle(idx);
}
}
this.SetMainWindowTitle();
// TLに反映
- if (this.settings.Common.PostAndGet)
- {
- await this.RefreshTabAsync<HomeTabModel>();
- }
- else
+ if (post != null)
{
- if (post != null)
- {
- this.statuses.AddPost(post);
- this.statuses.DistributePosts();
- }
+ this.statuses.AddPost(post);
+ this.statuses.DistributePosts();
this.RefreshTimeline();
}
+
+ if (this.settings.Common.PostAndGet)
+ await this.RefreshTabAsync<HomeTabModel>();
}
- private async Task RetweetAsync(IReadOnlyList<long> statusIds)
+ private async Task RetweetAsync(IReadOnlyList<PostId> statusIds)
{
await this.workerSemaphore.WaitAsync();
}
}
- private async Task RetweetAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<long> statusIds)
+ private async Task RetweetAsyncInternal(IProgress<string> p, CancellationToken ct, IReadOnlyList<PostId> statusIds)
{
if (ct.IsCancellationRequested)
return;
this.StatusLabel.Text = Properties.Resources.UpdateFollowersMenuItem1_ClickText3;
this.RefreshTimeline();
- this.PurgeListViewItemCache();
+ this.listCache?.PurgeCache();
this.CurrentListView.Refresh();
}
catch (WebApiException ex)
if (this.tw.Configuration.PhotoSizeLimit != 0)
{
- foreach (var service in this.ImageSelector.GetServices())
+ foreach (var (_, service) in this.ImageSelector.Model.MediaServices)
{
service.UpdateTwitterConfiguration(this.tw.Configuration);
}
}
- this.PurgeListViewItemCache();
+ this.listCache?.PurgeCache();
this.CurrentListView.Refresh();
}
catch (WebApiException ex)
}
}
- private PostClass GetCurTabPost(int index)
- {
- var listCache = this.listItemCache;
- if (listCache != null)
- {
- if (listCache.TryGetValue(index, out _, out var post))
- return post;
- }
-
- return this.CurrentTab[index];
- }
-
private async void AuthorOpenInBrowserMenuItem_Click(object sender, EventArgs e)
{
var post = this.CurrentPost;
/// <returns>ソートを行う ComparerMode。null であればソートを行わない</returns>
private ComparerMode? GetComparerModeByColumnIndex(int columnIndex)
{
- if (this.iconCol)
+ if (this.Use2ColumnsMode)
return ComparerMode.Id;
return columnIndex switch
this.InitColumnText();
var list = this.CurrentListView;
- if (this.iconCol)
+ if (this.Use2ColumnsMode)
{
list.Columns[0].Text = this.columnText[0];
list.Columns[1].Text = this.columnText[2];
}
}
- this.PurgeListViewItemCache();
+ this.listCache?.PurgeCache();
var tab = this.CurrentTab;
var post = this.CurrentPost;
this.StatusOpenMenuItem.Enabled = true;
this.ShowRelatedStatusesMenuItem.Enabled = true; // PublicSearchの時問題出るかも
- if (!post.CanRetweetBy(this.twitterApi.CurrentUserId))
+ if (!post.CanRetweetBy(this.tw.UserId))
{
this.ReTweetStripMenuItem.Enabled = false;
this.ReTweetUnofficialStripMenuItem.Enabled = false;
try
{
- if (post.IsDm)
+ if (post.StatusId is TwitterDirectMessageId dmId)
{
- await this.twitterApi.DirectMessagesEventsDestroy(post.StatusId.ToString(CultureInfo.InvariantCulture));
+ await this.tw.Api.DirectMessagesEventsDestroy(dmId);
}
else
{
{
// 自分が RT したツイート (自分が RT した自分のツイートも含む)
// => RT を取り消し
- await this.twitterApi.StatusesDestroy(post.StatusId)
- .IgnoreResponse();
+ await this.tw.DeleteRetweet(post);
}
else
{
{
// 他人に RT された自分のツイート
// => RT 元の自分のツイートを削除
- await this.twitterApi.StatusesDestroy(post.RetweetedId.Value)
- .IgnoreResponse();
+ await this.tw.DeleteTweet(post.RetweetedId.ToTwitterStatusId());
}
else
{
// 自分のツイート
// => ツイートを削除
- await this.twitterApi.StatusesDestroy(post.StatusId)
- .IgnoreResponse();
+ await this.tw.DeleteTweet(post.StatusId.ToTwitterStatusId());
}
}
}
else
this.StatusLabel.Text = Properties.Resources.DeleteStripMenuItem_ClickText3; // 失敗
- this.PurgeListViewItemCache();
-
- foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
+ using (ControlTransaction.Update(currentListView))
{
- var tabPage = this.ListTab.TabPages[index];
- var listView = (DetailsListView)tabPage.Tag;
-
- using (ControlTransaction.Update(listView))
- {
- listView.VirtualListSize = tab.AllCount;
+ this.listCache?.PurgeCache();
+ this.listCache?.UpdateListSize();
- if (tab.TabName == this.CurrentTabName)
- {
- listView.SelectedIndices.Clear();
+ currentListView.SelectedIndices.Clear();
- if (tab.AllCount != 0)
- {
- int selectedIndex;
- if (tab.AllCount - 1 > focusedIndex && focusedIndex > -1)
- selectedIndex = focusedIndex;
- else
- selectedIndex = tab.AllCount - 1;
+ var currentTab = this.CurrentTab;
+ if (currentTab.AllCount != 0)
+ {
+ int selectedIndex;
+ if (currentTab.AllCount - 1 > focusedIndex && focusedIndex > -1)
+ selectedIndex = focusedIndex;
+ else
+ selectedIndex = currentTab.AllCount - 1;
- listView.SelectedIndices.Add(selectedIndex);
- listView.EnsureVisible(selectedIndex);
- listView.FocusedItem = listView.Items[selectedIndex];
- }
- }
+ currentListView.SelectedIndices.Add(selectedIndex);
+ currentListView.EnsureVisible(selectedIndex);
+ currentListView.FocusedItem = currentListView.Items[selectedIndex];
}
+ }
+ foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
+ {
+ var tabPage = this.ListTab.TabPages[index];
if (this.settings.Common.TabIconDisp && tab.UnreadCount == 0)
{
if (tabPage.ImageIndex == 0)
{
this.statuses.SetReadAllTab(statusId, read: true);
var idx = tab.IndexOf(statusId);
- this.ChangeCacheStyleRead(true, idx);
+ this.listCache?.RefreshStyle(idx);
}
- this.ColorizeList();
}
if (this.settings.Common.TabIconDisp)
{
{
this.statuses.SetReadAllTab(statusId, read: false);
var idx = tab.IndexOf(statusId);
- this.ChangeCacheStyleRead(false, idx);
+ this.listCache?.RefreshStyle(idx);
}
- this.ColorizeList();
}
if (this.settings.Common.TabIconDisp)
{
private async Task DoRefreshMore()
=> await this.RefreshTabAsync(this.CurrentTab, backward: true);
- private DialogResult ShowSettingDialog(bool showTaskbarIcon = false)
+ private DialogResult ShowSettingDialog()
{
- var result = DialogResult.Abort;
-
using var settingDialog = new AppendSettingDialog();
- settingDialog.Icon = this.mainIcon;
- settingDialog.Owner = this;
- settingDialog.ShowInTaskbar = showTaskbarIcon;
+ settingDialog.Icon = this.iconAssets.IconMain;
settingDialog.IntervalChanged += this.TimerInterval_Changed;
- settingDialog.Tw = this.tw;
- settingDialog.TwitterApi = this.twitterApi;
-
settingDialog.LoadConfig(this.settings.Common, this.settings.Local);
+ DialogResult result;
try
{
result = settingDialog.ShowDialog(this);
private async void SettingStripMenuItem_Click(object sender, EventArgs e)
{
// 設定画面表示前のユーザー情報
- var oldUser = new { this.tw.AccessToken, this.tw.AccessTokenSecret, this.tw.Username, this.tw.UserId };
-
- var oldIconSz = this.settings.Common.IconSize;
+ var previousUserId = this.settings.Common.UserId;
+ var oldIconCol = this.Use2ColumnsMode;
if (this.ShowSettingDialog() == DialogResult.OK)
{
lock (this.syncObject)
{
+ this.settings.ApplySettings();
+
+ if (MyCommon.IsNullOrEmpty(this.settings.Common.Token))
+ this.tw.ClearAuthInfo();
+
+ var account = this.settings.Common.SelectedAccount;
+ if (account != null)
+ this.tw.Initialize(account.GetTwitterAppToken(), account.Token, account.TokenSecret, account.Username, account.UserId);
+ else
+ this.tw.Initialize(TwitterAppToken.GetDefault(), "", "", "", 0L);
+
this.tw.RestrictFavCheck = this.settings.Common.RestrictFavCheck;
this.tw.ReadOwnPost = this.settings.Common.ReadOwnPost;
- ShortUrl.Instance.DisableExpanding = !this.settings.Common.TinyUrlResolve;
- ShortUrl.Instance.BitlyAccessToken = this.settings.Common.BitlyAccessToken;
- ShortUrl.Instance.BitlyId = this.settings.Common.BilyUser;
- ShortUrl.Instance.BitlyKey = this.settings.Common.BitlyPwd;
- TwitterApiConnection.RestApiHost = this.settings.Common.TwitterApiHost;
-
- Networking.DefaultTimeout = TimeSpan.FromSeconds(this.settings.Common.DefaultTimeOut);
- Networking.UploadImageTimeout = TimeSpan.FromSeconds(this.settings.Common.UploadImageTimeout);
- Networking.SetWebProxy(
- this.settings.Local.ProxyType,
- this.settings.Local.ProxyAddress,
- this.settings.Local.ProxyPort,
- this.settings.Local.ProxyUser,
- this.settings.Local.ProxyPassword);
- Networking.ForceIPv4 = this.settings.Common.ForceIPv4;
-
- this.ImageSelector.Reset(this.tw, this.tw.Configuration);
+
+ this.ImageSelector.Model.InitializeServices(this.tw, this.tw.Configuration);
try
{
this.SplitContainer1.IsPanelInverted = !this.settings.Common.StatusAreaAtBottom;
- var imgazyobizinet = ThumbnailGenerator.ImgAzyobuziNetInstance;
+ var imgazyobizinet = this.thumbGenerator.ImgAzyobuziNet;
imgazyobizinet.Enabled = this.settings.Common.EnableImgAzyobuziNet;
imgazyobizinet.DisabledInDM = this.settings.Common.ImgAzyobuziNetDisabledInDM;
this.NotifyFileMenuItem.Checked = this.settings.Common.NewAllPop;
this.PlaySoundMenuItem.Checked = this.settings.Common.PlaySound;
this.PlaySoundFileMenuItem.Checked = this.settings.Common.PlaySound;
- this.fntUnread = this.settings.Local.FontUnread;
- this.clUnread = this.settings.Local.ColorUnread;
- this.fntReaded = this.settings.Local.FontRead;
- this.clReaded = this.settings.Local.ColorRead;
- this.clFav = this.settings.Local.ColorFav;
- this.clOWL = this.settings.Local.ColorOWL;
- this.clRetweet = this.settings.Local.ColorRetweet;
- this.fntDetail = this.settings.Local.FontDetail;
- this.clDetail = this.settings.Local.ColorDetail;
- this.clDetailLink = this.settings.Local.ColorDetailLink;
- this.clDetailBackcolor = this.settings.Local.ColorDetailBackcolor;
- this.clSelf = this.settings.Local.ColorSelf;
- this.clAtSelf = this.settings.Local.ColorAtSelf;
- this.clTarget = this.settings.Local.ColorTarget;
- this.clAtTarget = this.settings.Local.ColorAtTarget;
- this.clAtFromTarget = this.settings.Local.ColorAtFromTarget;
- this.clAtTo = this.settings.Local.ColorAtTo;
- this.clListBackcolor = this.settings.Local.ColorListBackcolor;
- this.clInputBackcolor = this.settings.Local.ColorInputBackcolor;
- this.clInputFont = this.settings.Local.ColorInputFont;
- this.fntInputFont = this.settings.Local.FontInputFont;
- this.brsBackColorMine.Dispose();
- this.brsBackColorAt.Dispose();
- this.brsBackColorYou.Dispose();
- this.brsBackColorAtYou.Dispose();
- this.brsBackColorAtFromTarget.Dispose();
- this.brsBackColorAtTo.Dispose();
- this.brsBackColorNone.Dispose();
- this.brsBackColorMine = new SolidBrush(this.clSelf);
- this.brsBackColorAt = new SolidBrush(this.clAtSelf);
- this.brsBackColorYou = new SolidBrush(this.clTarget);
- this.brsBackColorAtYou = new SolidBrush(this.clAtTarget);
- this.brsBackColorAtFromTarget = new SolidBrush(this.clAtFromTarget);
- this.brsBackColorAtTo = new SolidBrush(this.clAtTo);
- this.brsBackColorNone = new SolidBrush(this.clListBackcolor);
+
+ var newTheme = new ThemeManager(this.settings.Local);
+ (var oldTheme, this.themeManager) = (this.themeManager, newTheme);
+ this.tweetDetailsView.Theme = this.themeManager;
+ if (this.listDrawer != null)
+ this.listDrawer.Theme = this.themeManager;
+ oldTheme.Dispose();
try
{
- if (this.StatusText.Focused) this.StatusText.BackColor = this.clInputBackcolor;
- this.StatusText.Font = this.fntInputFont;
- this.StatusText.ForeColor = this.clInputFont;
+ if (this.StatusText.Focused)
+ this.StatusText.BackColor = this.themeManager.ColorInputBackcolor;
+
+ this.StatusText.Font = this.themeManager.FontInputFont;
+ this.StatusText.ForeColor = this.themeManager.ColorInputFont;
}
catch (Exception ex)
{
try
{
- var oldIconCol = this.iconCol;
-
- if (this.settings.Common.IconSize != oldIconSz)
- this.ApplyListViewIconSize(this.settings.Common.IconSize);
+ this.ApplyListViewIconSize(this.settings.Common.IconSize);
foreach (TabPage tp in this.ListTab.TabPages)
{
using (ControlTransaction.Update(lst))
{
lst.GridLines = this.settings.Common.ShowGrid;
- lst.Font = this.fntReaded;
- lst.BackColor = this.clListBackcolor;
- if (this.iconCol != oldIconCol)
+ if (this.Use2ColumnsMode != oldIconCol)
this.ResetColumns(lst);
}
}
this.SetMainWindowTitle();
this.SetNotifyIconText();
- this.PurgeListViewItemCache();
+ this.listCache?.PurgeCache();
this.CurrentListView.Refresh();
this.ListTab.Refresh();
}
}
}
- else
- {
- // キャンセル時は Twitter クラスの認証情報を画面表示前の状態に戻す
- this.tw.Initialize(oldUser.AccessToken, oldUser.AccessTokenSecret, oldUser.Username, oldUser.UserId);
- }
Twitter.AccountState = MyCommon.ACCOUNT_STATE.Valid;
this.TopMost = this.settings.Common.AlwaysTop;
this.SaveConfigsAll(false);
- if (this.tw.UserId != oldUser.UserId)
+ if (this.tw.UserId != previousUserId)
await this.DoGetFollowersMenu();
}
var newAlignment = this.settings.Common.ViewTabBottom ? TabAlignment.Bottom : TabAlignment.Top;
if (this.ListTab.Alignment == newAlignment) return;
- // 各タブのリスト上の選択位置などを退避
- var listSelections = this.SaveListViewSelection();
+ // リスト上の選択位置などを退避
+ var currentListViewState = this.listViewState[this.CurrentTabName];
+ currentListViewState.Save(this.ListLockMenuItem.Checked);
this.ListTab.Alignment = newAlignment;
- foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
- {
- var lst = (DetailsListView)this.ListTab.TabPages[index].Tag;
- using (ControlTransaction.Update(lst))
- {
- // 選択位置などを復元
- this.RestoreListViewSelection(lst, tab, listSelections[tab.TabName]);
- }
- }
+ currentListViewState.Restore(forceScroll: true);
}
private void ApplyListViewIconSize(MyCommon.IconSizes iconSz)
{
// アイコンサイズの再設定
- this.iconSz = iconSz switch
- {
- MyCommon.IconSizes.IconNone => 0,
- MyCommon.IconSizes.Icon16 => 16,
- MyCommon.IconSizes.Icon24 => 26,
- MyCommon.IconSizes.Icon48 => 48,
- MyCommon.IconSizes.Icon48_2 => 48,
- _ => throw new InvalidEnumArgumentException(nameof(iconSz), (int)iconSz, typeof(MyCommon.IconSizes)),
- };
- this.iconCol = iconSz == MyCommon.IconSizes.Icon48_2;
-
- this.PurgeListViewItemCache();
-
- if (this.iconSz > 0)
- {
- // ディスプレイの DPI 設定を考慮したサイズを設定する
- this.listViewImageList.ImageSize = new Size(
- 1,
- (int)Math.Ceiling(this.iconSz * this.CurrentScaleFactor.Height));
- }
- else
+ if (this.listDrawer != null)
{
- this.listViewImageList.ImageSize = new Size(1, 1);
+ this.listDrawer.IconSize = iconSz;
+ this.listDrawer.UpdateItemHeight();
}
+
+ this.listCache?.PurgeCache();
}
private void ResetColumns(DetailsListView list)
listCustom.View = View.Details;
listCustom.OwnerDraw = true;
listCustom.VirtualMode = true;
- listCustom.Font = this.fntReaded;
- listCustom.BackColor = this.clListBackcolor;
listCustom.GridLines = this.settings.Common.ShowGrid;
listCustom.AllowDrop = true;
- listCustom.SmallImageList = this.listViewImageList;
-
this.InitColumns(listCustom, startup);
listCustom.SelectedIndexChanged += this.MyList_SelectedIndexChanged;
listCustom.DragDrop += this.TweenMain_DragDrop;
listCustom.DragEnter += this.TweenMain_DragEnter;
listCustom.DragOver += this.TweenMain_DragOver;
- listCustom.DrawItem += this.MyList_DrawItem;
listCustom.MouseClick += this.MyList_MouseClick;
listCustom.ColumnReordered += this.MyList_ColumnReordered;
listCustom.ColumnWidthChanged += this.MyList_ColumnWidthChanged;
- listCustom.CacheVirtualItems += this.MyList_CacheVirtualItems;
- listCustom.RetrieveVirtualItem += this.MyList_RetrieveVirtualItem;
- listCustom.DrawSubItem += this.MyList_DrawSubItem;
listCustom.HScrolled += this.MyList_HScrolled;
}
+ var state = new TimelineListViewState(listCustom, tab);
+ this.listViewState[tab.TabName] = state;
+
return true;
}
this.SetListProperty(); // 他のタブに列幅等を反映
+ this.listViewState.Remove(tabName);
+
// オブジェクトインスタンスの削除
var listCustom = (DetailsListView)tabPage.Tag;
tabPage.Tag = null;
listCustom.DragDrop -= this.TweenMain_DragDrop;
listCustom.DragEnter -= this.TweenMain_DragEnter;
listCustom.DragOver -= this.TweenMain_DragOver;
- listCustom.DrawItem -= this.MyList_DrawItem;
listCustom.MouseClick -= this.MyList_MouseClick;
listCustom.ColumnReordered -= this.MyList_ColumnReordered;
listCustom.ColumnWidthChanged -= this.MyList_ColumnWidthChanged;
- listCustom.CacheVirtualItems -= this.MyList_CacheVirtualItems;
- listCustom.RetrieveVirtualItem -= this.MyList_RetrieveVirtualItem;
- listCustom.DrawSubItem -= this.MyList_DrawSubItem;
listCustom.HScrolled -= this.MyList_HScrolled;
var cols = listCustom.Columns.Cast<ColumnHeader>().ToList<ColumnHeader>();
listCustom.ListViewItemSorter = null;
// キャッシュのクリア
- this.PurgeListViewItemCache();
+ this.listCache?.PurgeCache();
}
tabPage.Dispose();
listCustom.Dispose();
this.statuses.RemoveTab(tabName);
- foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
- {
- var lst = (DetailsListView)this.ListTab.TabPages[index].Tag;
- lst.VirtualListSize = tab.AllCount;
- }
-
return true;
}
private void ListTab_Deselected(object sender, TabControlEventArgs e)
{
- this.PurgeListViewItemCache();
+ this.listCache?.PurgeCache();
this.beforeSelectedTab = e.TabPage;
}
}
else
{
- this.StatusText.ForeColor = this.clInputFont;
+ this.StatusText.ForeColor = this.themeManager.ColorInputFont;
}
this.StatusText.AccessibleDescription = string.Format(Properties.Resources.StatusText_AccessibleDescription, pLen);
attachmentUrl = null;
// attachment_url は media_id と同時に使用できない
- if (this.ImageSelector.Visible && this.ImageSelector.SelectedService is TwitterPhoto)
+ if (this.ImageSelector.Visible && this.ImageSelector.Model.SelectedMediaService is TwitterPhoto)
return statusText;
var match = Twitter.AttachmentUrlRegex.Match(statusText);
statusText = '\u200b' + statusText;
}
}
-
- return statusText;
- }
-
- /// <summary>
- /// 投稿欄に表示する入力可能な文字数を計算します
- /// </summary>
- private int GetRestStatusCount(string statusText)
- {
- var remainCount = this.tw.GetTextLengthRemain(statusText);
-
- var uploadService = this.GetSelectedImageService();
- if (uploadService != null)
- {
- // TODO: ImageSelector で選択中の画像の枚数が mediaCount 引数に渡るようにする
- remainCount -= uploadService.GetReservedTextLength(1);
- }
-
- return remainCount;
- }
-
- private IMediaUploadService? GetSelectedImageService()
- => this.ImageSelector.Visible ? this.ImageSelector.SelectedService : null;
-
- private void MyList_CacheVirtualItems(object sender, CacheVirtualItemsEventArgs e)
- {
- if (sender != this.CurrentListView)
- return;
-
- var listCache = this.listItemCache;
- if (listCache?.TargetList == sender && listCache.IsSupersetOf(e.StartIndex, e.EndIndex))
- {
- // If the newly requested cache is a subset of the old cache,
- // no need to rebuild everything, so do nothing.
- return;
- }
-
- // Now we need to rebuild the cache.
- this.CreateCache(e.StartIndex, e.EndIndex);
- }
-
- private void MyList_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
- {
- var listCache = this.listItemCache;
- if (listCache?.TargetList == sender)
- {
- if (listCache.TryGetValue(e.ItemIndex, out var item, out _))
- {
- e.Item = item;
- return;
- }
- }
-
- // A cache miss, so create a new ListViewItem and pass it back.
- var tabPage = (TabPage)((DetailsListView)sender).Parent;
- var tab = this.statuses.Tabs[tabPage.Text];
- try
- {
- e.Item = this.CreateItem(tab, tab[e.ItemIndex]);
- }
- catch (Exception)
- {
- // 不正な要求に対する間に合わせの応答
- string[] sitem = { "", "", "", "", "", "", "", "" };
- e.Item = new ImageListViewItem(sitem);
- }
- }
-
- private void CreateCache(int startIndex, int endIndex)
- {
- var tabInfo = this.CurrentTab;
-
- if (tabInfo.AllCount == 0)
- return;
-
- // インデックスを 0...(tabInfo.AllCount - 1) の範囲内にする
- int FilterRange(int index)
- => Math.Max(Math.Min(index, tabInfo.AllCount - 1), 0);
-
- // キャッシュ要求(要求範囲±30を作成)
- startIndex = FilterRange(startIndex - 30);
- endIndex = FilterRange(endIndex + 30);
-
- var cacheLength = endIndex - startIndex + 1;
-
- var tab = this.CurrentTab;
- var posts = tabInfo[startIndex, endIndex]; // 配列で取得
- var listItems = Enumerable.Range(0, cacheLength)
- .Select(x => this.CreateItem(tab, posts[x]))
- .ToArray();
-
- var listCache = new ListViewItemCache(
- TargetList: this.CurrentListView,
- StartIndex: startIndex,
- EndIndex: endIndex,
- Cache: Enumerable.Zip(listItems, posts, (x, y) => (x, y)).ToArray()
- );
-
- Interlocked.Exchange(ref this.listItemCache, listCache);
- }
-
- /// <summary>
- /// DetailsListView のための ListViewItem のキャッシュを消去する
- /// </summary>
- private void PurgeListViewItemCache()
- => Interlocked.Exchange(ref this.listItemCache, null);
-
- private ListViewItem CreateItem(TabModel tab, PostClass post)
- {
- var mk = new StringBuilder();
-
- if (post.FavoritedCount > 0) mk.Append("+" + post.FavoritedCount);
-
- var scaledIconSz = (int)Math.Ceiling(this.iconSz * this.CurrentScaleFactor.Width);
-
- ImageListViewItem itm;
- if (post.RetweetedId == null)
- {
- string[] sitem =
- {
- "",
- post.Nickname,
- post.IsDeleted ? "(DELETED)" : post.AccessibleText.Replace('\n', ' '),
- post.CreatedAt.ToLocalTimeString(this.settings.Common.DateTimeFormat),
- post.ScreenName,
- "",
- mk.ToString(),
- post.Source,
- };
- itm = new ImageListViewItem(sitem, this.iconCache, post.ImageUrl, scaledIconSz);
- }
- else
- {
- string[] sitem =
- {
- "",
- post.Nickname,
- post.IsDeleted ? "(DELETED)" : post.AccessibleText.Replace('\n', ' '),
- post.CreatedAt.ToLocalTimeString(this.settings.Common.DateTimeFormat),
- post.ScreenName + Environment.NewLine + "(RT:" + post.RetweetedBy + ")",
- "",
- mk.ToString(),
- post.Source,
- };
- itm = new ImageListViewItem(sitem, this.iconCache, post.ImageUrl, scaledIconSz);
- }
- itm.StateIndex = post.StateIndex;
- itm.Tag = post;
-
- var read = post.IsRead;
- // 未読管理していなかったら既読として扱う
- if (!tab.UnreadManage || !this.settings.Common.UnreadManage)
- read = true;
-
- this.ChangeItemStyleRead(read, itm, post, null);
-
- if (tab.TabName == this.CurrentTabName)
- this.ColorizeList(itm, post);
-
- return itm;
- }
-
- /// <summary>
- /// 全てのタブの振り分けルールを反映し直します
- /// </summary>
- private void ApplyPostFilters()
- {
- using (ControlTransaction.Cursor(this, Cursors.WaitCursor))
- {
- this.PurgeListViewItemCache();
- this.statuses.FilterAll();
-
- foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
- {
- var tabPage = this.ListTab.TabPages[index];
- var listview = (DetailsListView)tabPage.Tag;
- using (ControlTransaction.Update(listview))
- {
- listview.VirtualListSize = tab.AllCount;
- }
-
- if (this.settings.Common.TabIconDisp)
- {
- if (tab.UnreadCount > 0)
- tabPage.ImageIndex = 0;
- else
- tabPage.ImageIndex = -1;
- }
- }
-
- if (!this.settings.Common.TabIconDisp)
- this.ListTab.Refresh();
-
- this.SetMainWindowTitle();
- this.SetStatusLabelUrl();
- }
- }
-
- private void MyList_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
- => e.DrawDefault = true;
-
- private void MyList_HScrolled(object sender, EventArgs e)
- => ((DetailsListView)sender).Refresh();
-
- private void MyList_DrawItem(object sender, DrawListViewItemEventArgs e)
- {
- if (e.State == 0) return;
- e.DrawDefault = false;
-
- SolidBrush brs2;
- if (!e.Item.Selected) // e.ItemStateでうまく判定できない???
- {
- if (e.Item.BackColor == this.clSelf)
- brs2 = this.brsBackColorMine;
- else if (e.Item.BackColor == this.clAtSelf)
- brs2 = this.brsBackColorAt;
- else if (e.Item.BackColor == this.clTarget)
- brs2 = this.brsBackColorYou;
- else if (e.Item.BackColor == this.clAtTarget)
- brs2 = this.brsBackColorAtYou;
- else if (e.Item.BackColor == this.clAtFromTarget)
- brs2 = this.brsBackColorAtFromTarget;
- else if (e.Item.BackColor == this.clAtTo)
- brs2 = this.brsBackColorAtTo;
- else
- brs2 = this.brsBackColorNone;
- }
- else
- {
- // 選択中の行
- if (((Control)sender).Focused)
- brs2 = this.brsHighLight;
- else
- brs2 = this.brsDeactiveSelection;
- }
- e.Graphics.FillRectangle(brs2, e.Bounds);
- e.DrawFocusRectangle();
- this.DrawListViewItemIcon(e);
- }
-
- private void MyList_DrawSubItem(object sender, DrawListViewSubItemEventArgs e)
- {
- if (e.ItemState == 0) return;
-
- if (e.ColumnIndex > 0)
- {
- // アイコン以外の列
- var post = (PostClass)e.Item.Tag;
-
- RectangleF rct = e.Bounds;
- rct.Width = e.Header.Width;
- var fontHeight = e.Item.Font.Height;
- if (this.iconCol)
- {
- rct.Y += fontHeight;
- rct.Height -= fontHeight;
- }
-
- var drawLineCount = Math.Max(1, Math.DivRem((int)rct.Height, fontHeight, out var heightDiff));
-
- // フォントの高さの半分を足してるのは保険。無くてもいいかも。
- if (this.iconCol || drawLineCount > 1)
- {
- if (heightDiff < fontHeight * 0.7)
- {
- // 最終行が70%以上欠けていたら、最終行は表示しない
- rct.Height = (fontHeight * drawLineCount) - 1;
- }
- else
- {
- drawLineCount += 1;
- }
- }
-
- if (rct.Width > 0)
- {
- var color = (!e.Item.Selected) ? e.Item.ForeColor : // 選択されていない行
- ((Control)sender).Focused ? this.clHighLight : // 選択中の行
- this.clUnread;
-
- if (this.iconCol)
- {
- var rctB = e.Bounds;
- rctB.Width = e.Header.Width;
- rctB.Height = fontHeight;
-
- using var fnt = new Font(e.Item.Font, FontStyle.Bold);
- var formatFlags1 = TextFormatFlags.WordBreak |
- TextFormatFlags.EndEllipsis |
- TextFormatFlags.GlyphOverhangPadding |
- TextFormatFlags.NoPrefix;
-
- TextRenderer.DrawText(
- e.Graphics,
- post.IsDeleted ? "(DELETED)" : post.TextSingleLine,
- e.Item.Font,
- Rectangle.Round(rct),
- color,
- formatFlags1);
-
- var formatFlags2 = TextFormatFlags.SingleLine |
- TextFormatFlags.EndEllipsis |
- TextFormatFlags.GlyphOverhangPadding |
- TextFormatFlags.NoPrefix;
-
- TextRenderer.DrawText(
- e.Graphics,
- e.Item.SubItems[4].Text + " / " + e.Item.SubItems[1].Text + " (" + e.Item.SubItems[3].Text + ") " + e.Item.SubItems[5].Text + e.Item.SubItems[6].Text + " [" + e.Item.SubItems[7].Text + "]",
- fnt,
- rctB,
- color,
- formatFlags2);
- }
- else
- {
- string text;
- if (e.ColumnIndex != 2)
- text = e.SubItem.Text;
- else
- text = post.IsDeleted ? "(DELETED)" : post.TextSingleLine;
-
- if (drawLineCount == 1)
- {
- var formatFlags = TextFormatFlags.SingleLine |
- TextFormatFlags.EndEllipsis |
- TextFormatFlags.GlyphOverhangPadding |
- TextFormatFlags.NoPrefix |
- TextFormatFlags.VerticalCenter;
-
- TextRenderer.DrawText(
- e.Graphics,
- text,
- e.Item.Font,
- Rectangle.Round(rct),
- color,
- formatFlags);
- }
- else
- {
- var formatFlags = TextFormatFlags.WordBreak |
- TextFormatFlags.EndEllipsis |
- TextFormatFlags.GlyphOverhangPadding |
- TextFormatFlags.NoPrefix;
-
- TextRenderer.DrawText(
- e.Graphics,
- text,
- e.Item.Font,
- Rectangle.Round(rct),
- color,
- formatFlags);
- }
- }
- }
- }
+
+ return statusText;
}
- private void DrawListViewItemIcon(DrawListViewItemEventArgs e)
+ /// <summary>
+ /// 投稿欄に表示する入力可能な文字数を計算します
+ /// </summary>
+ private int GetRestStatusCount(string statusText)
{
- if (this.iconSz == 0) return;
-
- var item = (ImageListViewItem)e.Item;
-
- // e.Bounds.Leftが常に0を指すから自前で計算
- var itemRect = item.Bounds;
- var col0 = e.Item.ListView.Columns[0];
- itemRect.Width = col0.Width;
+ var remainCount = this.tw.GetTextLengthRemain(statusText);
- if (col0.DisplayIndex > 0)
+ var uploadService = this.GetSelectedImageService();
+ if (uploadService != null)
{
- foreach (ColumnHeader clm in e.Item.ListView.Columns)
- {
- if (clm.DisplayIndex < col0.DisplayIndex)
- itemRect.X += clm.Width;
- }
+ // TODO: ImageSelector で選択中の画像の枚数が mediaCount 引数に渡るようにする
+ remainCount -= uploadService.GetReservedTextLength(1);
}
- // ディスプレイの DPI 設定を考慮したアイコンサイズ
- var realIconSize = new SizeF(this.iconSz * this.CurrentScaleFactor.Width, this.iconSz * this.CurrentScaleFactor.Height).ToSize();
- var realStateSize = new SizeF(16 * this.CurrentScaleFactor.Width, 16 * this.CurrentScaleFactor.Height).ToSize();
+ return remainCount;
+ }
+
+ private IMediaUploadService? GetSelectedImageService()
+ => this.ImageSelector.Visible ? this.ImageSelector.Model.SelectedMediaService : null;
- Rectangle iconRect;
- var img = item.Image;
- if (img != null)
+ /// <summary>
+ /// 全てのタブの振り分けルールを反映し直します
+ /// </summary>
+ private void ApplyPostFilters()
+ {
+ using (ControlTransaction.Cursor(this, Cursors.WaitCursor))
{
- iconRect = Rectangle.Intersect(new Rectangle(e.Item.GetBounds(ItemBoundsPortion.Icon).Location, realIconSize), itemRect);
- iconRect.Offset(0, Math.Max(0, (itemRect.Height - realIconSize.Height) / 2));
+ this.statuses.FilterAll();
- if (iconRect.Width > 0)
+ var listView = this.CurrentListView;
+ using (ControlTransaction.Update(listView))
{
- e.Graphics.FillRectangle(Brushes.White, iconRect);
- e.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
- try
- {
- e.Graphics.DrawImage(img.Image, iconRect);
- }
- catch (ArgumentException)
+ this.listCache?.PurgeCache();
+ this.listCache?.UpdateListSize();
+ }
+
+ foreach (var (tab, index) in this.statuses.Tabs.WithIndex())
+ {
+ var tabPage = this.ListTab.TabPages[index];
+
+ if (this.settings.Common.TabIconDisp)
{
+ if (tab.UnreadCount > 0)
+ tabPage.ImageIndex = 0;
+ else
+ tabPage.ImageIndex = -1;
}
}
- }
- else
- {
- iconRect = Rectangle.Intersect(new Rectangle(e.Item.GetBounds(ItemBoundsPortion.Icon).Location, new Size(1, 1)), itemRect);
- item.GetImageAsync();
- }
+ if (!this.settings.Common.TabIconDisp)
+ this.ListTab.Refresh();
- if (item.StateIndex > -1)
- {
- var stateRect = Rectangle.Intersect(new Rectangle(new Point(iconRect.X + realIconSize.Width + 2, iconRect.Y), realStateSize), itemRect);
- if (stateRect.Width > 0)
- e.Graphics.DrawImage(this.PostStateImageList.Images[item.StateIndex], stateRect);
+ this.SetMainWindowTitle();
+ this.SetStatusLabelUrl();
}
}
+ private void MyList_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e)
+ => e.DrawDefault = true;
+
+ private void MyList_HScrolled(object sender, EventArgs e)
+ => ((DetailsListView)sender).Refresh();
+
protected override void ScaleControl(SizeF factor, BoundsSpecified specified)
{
base.ScaleControl(factor, specified);
if (this.statuses.SortMode == ComparerMode.Id)
{
- if (this.statuses.SortOrder == SortOrder.Ascending && lst.Items[foundIndex].Position.Y > lst.ClientSize.Height - this.iconSz - 10 ||
- this.statuses.SortOrder == SortOrder.Descending && lst.Items[foundIndex].Position.Y < this.iconSz + 10)
+ var rowHeight = lst.SmallImageList.ImageSize.Height;
+ if (this.statuses.SortOrder == SortOrder.Ascending && lst.Items[foundIndex].Position.Y > lst.ClientSize.Height - rowHeight - 10 ||
+ this.statuses.SortOrder == SortOrder.Descending && lst.Items[foundIndex].Position.Y < rowHeight + 10)
{
this.MoveTop();
}
var pinfo = new ProcessStartInfo
{
UseShellExecute = true,
- WorkingDirectory = MyCommon.SettingPath,
- FileName = Path.Combine(MyCommon.SettingPath, "TweenUp3.exe"),
+ WorkingDirectory = this.settings.SettingsPath,
+ FileName = Path.Combine(this.settings.SettingsPath, "TweenUp3.exe"),
Arguments = "\"" + Application.StartupPath + "\"",
};
if (dialog.ShowDialog(this) == DialogResult.Yes)
{
- await MyCommon.OpenInBrowserAsync(this, versionInfo.DownloadUri.OriginalString);
+ await MyCommon.OpenInBrowserAsync(this, versionInfo.DownloadUri);
}
else if (dialog.SkipButtonPressed)
{
this.tweetDetailsView.ShowPostDetails(currentPost),
};
- this.SplitContainer3.Panel2Collapsed = true;
-
if (this.settings.Common.PreviewEnable)
{
var oldTokenSource = Interlocked.Exchange(ref this.thumbnailTokenSource, new CancellationTokenSource());
oldTokenSource?.Cancel();
var token = this.thumbnailTokenSource!.Token;
- loadTasks.Add(this.tweetThumbnail1.ShowThumbnailAsync(currentPost, token));
+ loadTasks.Add(this.PrepareThumbnailControl(currentPost, token));
+ }
+ else
+ {
+ this.SplitContainer3.Panel2Collapsed = true;
}
async Task DelayedTasks()
_ = DelayedTasks();
}
+ private async Task PrepareThumbnailControl(PostClass post, CancellationToken token)
+ {
+ var prepareTask = this.tweetThumbnail1.Model.PrepareThumbnails(post, token);
+
+ var timeout = Task.Delay(100);
+ if ((await Task.WhenAny(prepareTask, timeout)) == timeout)
+ {
+ token.ThrowIfCancellationRequested();
+
+ // サムネイル情報の読み込みに時間が掛かっている場合は一旦サムネイル領域を非表示にする
+ this.SplitContainer3.Panel2Collapsed = true;
+ }
+
+ await prepareTask;
+ token.ThrowIfCancellationRequested();
+
+ this.SplitContainer3.Panel2Collapsed = !this.tweetThumbnail1.Model.ThumbnailAvailable;
+ }
+
private async void MatomeMenuItem_Click(object sender, EventArgs e)
=> await this.OpenApplicationWebsite();
}
if (e.Control || e.Shift || e.Alt)
- this.anchorFlag = false;
+ tab.ClearAnchor();
if (this.CommonKeyDown(e.KeyData, FocusedControl.ListTab, out var asyncTask))
{
.NotFocusedOn(FocusedControl.StatusText)
.Do(() =>
{
- this.anchorFlag = false;
+ this.CurrentTab.ClearAnchor();
this.JumpUnreadMenuItem_Click(this.JumpUnreadMenuItem, EventArgs.Empty);
}),
.NotFocusedOn(FocusedControl.StatusText)
.Do(() =>
{
- this.anchorFlag = false;
+ this.CurrentTab.ClearAnchor();
this.ShowRelatedStatusesMenuItem_Click(this.ShowRelatedStatusesMenuItem, EventArgs.Empty);
}),
.FocusedOn(FocusedControl.ListTab)
.Do(() =>
{
- this.anchorFlag = false;
+ this.CurrentTab.ClearAnchor();
this.GoPost(forward: true);
}),
.FocusedOn(FocusedControl.ListTab)
.Do(() =>
{
- this.anchorFlag = false;
+ this.CurrentTab.ClearAnchor();
this.GoPost(forward: false);
}),
.FocusedOn(FocusedControl.ListTab)
.Do(() =>
{
- this.anchorFlag = false;
+ this.CurrentTab.ClearAnchor();
this.MoveTop();
}),
.FocusedOn(FocusedControl.ListTab)
.Do(() =>
{
- this.anchorFlag = false;
+ this.CurrentTab.ClearAnchor();
this.GoNextTab(forward: true);
}),
.FocusedOn(FocusedControl.ListTab)
.Do(() =>
{
- this.anchorFlag = false;
+ this.CurrentTab.ClearAnchor();
this.GoNextTab(forward: false);
}),
.FocusedOn(FocusedControl.ListTab)
.Do(() =>
{
- this.anchorFlag = false;
+ this.CurrentTab.ClearAnchor();
return this.GoInReplyToPostTree();
}),
.FocusedOn(FocusedControl.ListTab)
.Do(() =>
{
- this.anchorFlag = false;
+ this.CurrentTab.ClearAnchor();
this.GoBackInReplyToPostTree();
}),
.FocusedOn(FocusedControl.ListTab)
.Do(() =>
{
- this.anchorFlag = false;
+ this.CurrentTab.ClearAnchor();
var tab = this.CurrentTab;
var tabtype = tab.TabType;
if (tabtype == MyCommon.TabUsageType.Related || tabtype == MyCommon.TabUsageType.UserTimeline || tabtype == MyCommon.TabUsageType.PublicSearch || tabtype == MyCommon.TabUsageType.SearchResults)
// 上下キー, PageUp/Downキー, Home/Endキー は既定の動作を残しつつアンカー初期化
ShortcutCommand.Create(Keys.Up, Keys.Down, Keys.PageUp, Keys.PageDown, Keys.Home, Keys.End)
.FocusedOn(FocusedControl.ListTab)
- .Do(() => this.anchorFlag = false, preventDefault: false),
+ .Do(() => this.CurrentTab.ClearAnchor(), preventDefault: false),
// PreviewKeyDownEventArgs.IsInputKey を true にしてスクロールを発生させる
ShortcutCommand.Create(Keys.Up, Keys.Down)
.OnlyWhen(() => this.CurrentTab.TabType == MyCommon.TabUsageType.PublicSearch)
.Do(() => this.CurrentTabPage.Controls["panelSearch"].Controls["comboSearch"].Focus()),
+ ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.L)
+ .Do(() => this.DoQuoteOfficial()),
+
ShortcutCommand.Create(Keys.Control | Keys.Shift | Keys.S)
.Do(() => this.FavoriteChange(favAdd: false)),
.Do(() => this.CopyUserId()),
ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Up)
- .Do(() => this.tweetThumbnail1.ScrollUp()),
+ .Do(() => this.tweetThumbnail1.Model.ScrollUp()),
ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Down)
- .Do(() => this.tweetThumbnail1.ScrollDown()),
+ .Do(() => this.tweetThumbnail1.Model.ScrollDown()),
ShortcutCommand.Create(Keys.Alt | Keys.Shift | Keys.Enter)
.FocusedOn(FocusedControl.ListTab)
.OnlyWhen(() => !this.SplitContainer3.Panel2Collapsed)
- .Do(() => this.OpenThumbnailPicture(this.tweetThumbnail1.Thumbnail)),
+ .Do(() => this.tweetThumbnail1.OpenImageInBrowser()),
};
}
return;
var selectedStatusId = tab.SelectedStatusId;
- if (selectedStatusId == -1)
+ if (selectedStatusId == null)
return;
int fIdx, toIdx, stp;
stp = -1;
}
- if (!this.anchorFlag)
+ var anchorPost = tab.AnchorPost;
+ if (anchorPost == null)
{
var currentPost = this.CurrentPost;
- if (currentPost == null) return;
- this.anchorPost = currentPost;
- this.anchorFlag = true;
- }
- else
- {
- if (this.anchorPost == null) return;
+ if (currentPost == null)
+ return;
+
+ anchorPost = currentPost;
+ tab.AnchorPost = currentPost;
}
for (var idx = fIdx; idx != toIdx; idx += stp)
{
var post = tab[idx];
- if (post.ScreenName == this.anchorPost.ScreenName ||
- post.RetweetedBy == this.anchorPost.ScreenName ||
- post.ScreenName == this.anchorPost.RetweetedBy ||
- (!MyCommon.IsNullOrEmpty(post.RetweetedBy) && post.RetweetedBy == this.anchorPost.RetweetedBy) ||
- this.anchorPost.ReplyToList.Any(x => x.UserId == post.UserId) ||
- this.anchorPost.ReplyToList.Any(x => x.UserId == post.RetweetedByUserId) ||
- post.ReplyToList.Any(x => x.UserId == this.anchorPost.UserId) ||
- post.ReplyToList.Any(x => x.UserId == this.anchorPost.RetweetedByUserId))
+ if (post.ScreenName == anchorPost.ScreenName ||
+ post.RetweetedBy == anchorPost.ScreenName ||
+ post.ScreenName == anchorPost.RetweetedBy ||
+ (!MyCommon.IsNullOrEmpty(post.RetweetedBy) && post.RetweetedBy == anchorPost.RetweetedBy) ||
+ anchorPost.ReplyToList.Any(x => x.UserId == post.UserId) ||
+ anchorPost.ReplyToList.Any(x => x.UserId == post.RetweetedByUserId) ||
+ post.ReplyToList.Any(x => x.UserId == anchorPost.UserId) ||
+ post.ReplyToList.Any(x => x.UserId == anchorPost.RetweetedByUserId))
{
var listView = this.CurrentListView;
this.SelectListItem(listView, idx);
private void GoAnchor()
{
- if (this.anchorPost == null) return;
- var idx = this.CurrentTab.IndexOf(this.anchorPost.StatusId);
- if (idx == -1) return;
+ var anchorStatusId = this.CurrentTab.AnchorStatusId;
+ if (anchorStatusId == null)
+ return;
+
+ var idx = this.CurrentTab.IndexOf(anchorStatusId);
+ if (idx == -1)
+ return;
var listView = this.CurrentListView;
this.SelectListItem(listView, idx);
{
try
{
- var post = await this.tw.GetStatusApi(false, currentPost.StatusId);
+ var post = await this.tw.GetStatusApi(false, currentPost.StatusId.ToTwitterStatusId());
- currentPost.InReplyToStatusId = post.InReplyToStatusId;
- currentPost.InReplyToUser = post.InReplyToUser;
- currentPost.IsReply = post.IsReply;
- this.PurgeListViewItemCache();
+ currentPost = currentPost with
+ {
+ InReplyToStatusId = post.InReplyToStatusId,
+ InReplyToUser = post.InReplyToUser,
+ IsReply = post.IsReply,
+ };
+ curTabClass.ReplacePost(currentPost);
+ this.listCache?.PurgeCache();
var index = curTabClass.SelectedIndex;
this.CurrentListView.RedrawItems(index, index, false);
{
this.replyChains = new Stack<ReplyChain>();
}
- this.replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId.Value, curTabClass));
+ this.replyChains.Push(new ReplyChain(currentPost.StatusId, currentPost.InReplyToStatusId, curTabClass));
int inReplyToIndex;
string inReplyToTabName;
- var inReplyToId = currentPost.InReplyToStatusId.Value;
+ var inReplyToId = currentPost.InReplyToStatusId;
var inReplyToUser = currentPost.InReplyToUser;
var inReplyToPosts = from tab in this.statuses.Tabs
{
await Task.Run(async () =>
{
- var post = await this.tw.GetStatusApi(false, currentPost.InReplyToStatusId.Value)
+ var post = await this.tw.GetStatusApi(false, currentPost.InReplyToStatusId.ToTwitterStatusId())
.ConfigureAwait(false);
post.IsRead = true;
catch (WebApiException ex)
{
this.StatusLabel.Text = $"Err:{ex.Message}(GetStatus)";
- await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId));
+ await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId.ToTwitterStatusId()));
return;
}
inReplyPost = inReplyToPosts.FirstOrDefault();
if (inReplyPost == null)
{
- await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId));
+ await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(inReplyToUser, inReplyToId.ToTwitterStatusId()));
return;
}
}
}
}
- private bool GoStatus(long statusId)
+ private bool GoStatus(PostId statusId)
{
- if (statusId == 0) return false;
-
var tab = this.statuses.Tabs
.Where(x => x.TabType != MyCommon.TabUsageType.DirectMessage)
.Where(x => x.Contains(statusId))
return true;
}
- private bool GoDirectMessage(long statusId)
+ private bool GoDirectMessage(PostId statusId)
{
- if (statusId == 0) return false;
-
var tab = this.statuses.DirectMessageTab;
var index = tab.IndexOf(statusId);
}
private void MyList_MouseClick(object sender, MouseEventArgs e)
- => this.anchorFlag = false;
+ => this.CurrentTab.ClearAnchor();
private void StatusText_Enter(object sender, EventArgs e)
{
// フォーカスの戻り先を StatusText に設定
this.Tag = this.StatusText;
- this.StatusText.BackColor = this.clInputBackcolor;
+ this.StatusText.BackColor = this.themeManager.ColorInputBackcolor;
}
public Color InputBackColor
- {
- get => this.clInputBackcolor;
- set => this.clInputBackcolor = value;
- }
+ => this.themeManager.ColorInputBackcolor;
private void StatusText_Leave(object sender, EventArgs e)
{
this.settings.Common.HashIsHead = this.HashMgr.IsHead;
this.settings.Common.HashIsPermanent = this.HashMgr.IsPermanent;
this.settings.Common.HashIsNotAddToAtReply = this.HashMgr.IsNotAddToAtReply;
- this.settings.Common.UseImageService = this.ImageSelector.ServiceIndex;
- this.settings.Common.UseImageServiceName = this.ImageSelector.ServiceName;
+ this.settings.Common.UseImageService = this.ImageSelector.Model.SelectedMediaServiceIndex;
+ this.settings.Common.UseImageServiceName = this.ImageSelector.Model.SelectedMediaServiceName;
this.settings.SaveCommon();
}
this.settings.Local.StatusMultiline = this.StatusText.Multiline;
this.settings.Local.StatusTextHeight = this.mySpDis2;
- this.settings.Local.FontUnread = this.fntUnread;
- this.settings.Local.ColorUnread = this.clUnread;
- this.settings.Local.FontRead = this.fntReaded;
- this.settings.Local.ColorRead = this.clReaded;
- this.settings.Local.FontDetail = this.fntDetail;
- this.settings.Local.ColorDetail = this.clDetail;
- this.settings.Local.ColorDetailBackcolor = this.clDetailBackcolor;
- this.settings.Local.ColorDetailLink = this.clDetailLink;
- this.settings.Local.ColorFav = this.clFav;
- this.settings.Local.ColorOWL = this.clOWL;
- this.settings.Local.ColorRetweet = this.clRetweet;
- this.settings.Local.ColorSelf = this.clSelf;
- this.settings.Local.ColorAtSelf = this.clAtSelf;
- this.settings.Local.ColorTarget = this.clTarget;
- this.settings.Local.ColorAtTarget = this.clAtTarget;
- this.settings.Local.ColorAtFromTarget = this.clAtFromTarget;
- this.settings.Local.ColorAtTo = this.clAtTo;
- this.settings.Local.ColorListBackcolor = this.clListBackcolor;
- this.settings.Local.ColorInputBackcolor = this.clInputBackcolor;
- this.settings.Local.ColorInputFont = this.clInputFont;
- this.settings.Local.FontInputFont = this.fntInputFont;
-
if (this.ignoreConfigSave) return;
this.settings.SaveLocal();
}
private async void OpenURLFileMenuItem_Click(object sender, EventArgs e)
{
+ static void ShowFormatErrorDialog(IWin32Window owner)
+ {
+ MessageBox.Show(
+ owner,
+ Properties.Resources.OpenURL_InvalidFormat,
+ Properties.Resources.OpenURL_Caption,
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Error
+ );
+ }
+
var ret = InputDialog.Show(this, Properties.Resources.OpenURL_InputText, Properties.Resources.OpenURL_Caption, out var inputText);
if (ret != DialogResult.OK)
return;
var match = Twitter.StatusUrlRegex.Match(inputText);
if (!match.Success)
{
- MessageBox.Show(
- this,
- Properties.Resources.OpenURL_InvalidFormat,
- Properties.Resources.OpenURL_Caption,
- MessageBoxButtons.OK,
- MessageBoxIcon.Error);
+ ShowFormatErrorDialog(this);
return;
}
try
{
- var statusId = long.Parse(match.Groups["StatusId"].Value);
+ var statusId = new TwitterStatusId(match.Groups["StatusId"].Value);
await this.OpenRelatedTab(statusId);
}
+ catch (OverflowException)
+ {
+ ShowFormatErrorDialog(this);
+ }
catch (TabException ex)
{
MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
"\"" + post.TextFromApi.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
post.CreatedAt.ToLocalTimeString() + "\t" +
post.ScreenName + "\t" +
- post.StatusId + "\t" +
+ post.StatusId.Id + "\t" +
post.ImageUrl + "\t" +
"\"" + post.Text.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
protect);
"\"" + post.TextFromApi.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
post.CreatedAt.ToLocalTimeString() + "\t" +
post.ScreenName + "\t" +
- post.StatusId + "\t" +
+ post.StatusId.Id + "\t" +
post.ImageUrl + "\t" +
"\"" + post.Text.Replace("\n", "").Replace("\"", "\"\"") + "\"" + "\t" +
protect);
this.statuses.RenameTab(origTabName, newTabName);
+ var state = this.listViewState[origTabName];
+ this.listViewState.Remove(origTabName);
+ this.listViewState[newTabName] = state;
+
this.SaveConfigsCommon();
this.SaveConfigsTabs();
this.rclickTabName = newTabName;
using (ControlTransaction.Layout(this.ListTab))
{
+ // 選択中のタブを Remove メソッドで取り外すと選択状態が変化して Selecting イベントが発生するが、
+ // この時 TabInformations と TabControl の並び順が不一致なままで ListTabSelect メソッドが呼ばれてしまう。
+ // これを防ぐために、Remove メソッドを呼ぶ前に選択中のタブを切り替えておく必要がある
+ this.ListTab.SelectedIndex = targetIndex == 0 ? 1 : 0;
+
var tab = this.statuses.Tabs[targetIndex];
var tabPage = this.ListTab.TabPages[targetIndex];
if (busyTasks)
{
this.iconCnt += 1;
- if (this.iconCnt >= this.nIconRefresh.Length)
+ if (this.iconCnt >= this.iconAssets.IconTrayRefresh.Length)
this.iconCnt = 0;
- this.NotifyIcon1.Icon = this.nIconRefresh[this.iconCnt];
+ this.NotifyIcon1.Icon = this.iconAssets.IconTrayRefresh[this.iconCnt];
this.myStatusError = false;
EnableTasktrayAnimation();
return;
if (this.blinkCnt == 0)
this.blink = !this.blink;
- this.NotifyIcon1.Icon = this.blink ? this.replyIconBlink : this.replyIcon;
+ this.NotifyIcon1.Icon = this.blink ? this.iconAssets.IconTrayReplyBlink : this.iconAssets.IconTrayReply;
EnableTasktrayAnimation();
return;
}
// 優先度:リプライ→エラー→オフライン→アイドル
// エラーは更新アイコンでクリアされる
if (replyIconType == MyCommon.REPLY_ICONSTATE.StaticIcon && reply)
- this.NotifyIcon1.Icon = this.replyIcon;
+ this.NotifyIcon1.Icon = this.iconAssets.IconTrayReply;
else if (this.myStatusError)
- this.NotifyIcon1.Icon = this.nIconAtRed;
+ this.NotifyIcon1.Icon = this.iconAssets.IconTrayError;
else if (this.myStatusOnline)
- this.NotifyIcon1.Icon = this.nIconAt;
+ this.NotifyIcon1.Icon = this.iconAssets.IconTray;
else
- this.NotifyIcon1.Icon = this.nIconAtSmoke;
+ this.NotifyIcon1.Icon = this.iconAssets.IconTrayOffline;
}
private void TimerRefreshIcon_Tick(object sender, EventArgs e)
if (this.CurrentTabName == tabName)
{
- this.PurgeListViewItemCache();
+ this.listCache?.PurgeCache();
this.CurrentListView.Refresh();
}
this.statuses.ClearTabIds(tabName);
if (this.CurrentTabName == tabName)
{
- this.anchorPost = null;
- this.anchorFlag = false;
- this.PurgeListViewItemCache();
+ this.CurrentTab.ClearAnchor();
+ this.listCache?.PurgeCache();
+ this.listCache?.UpdateListSize();
}
var tabIndex = this.statuses.Tabs.IndexOf(tabName);
var tabPage = this.ListTab.TabPages[tabIndex];
tabPage.ImageIndex = -1;
- var listView = (DetailsListView)tabPage.Tag;
- listView.VirtualListSize = 0;
-
if (!this.settings.Common.TabIconDisp) this.ListTab.Refresh();
this.SetMainWindowTitle();
// 表示中のタブに応じて更新
endpointName = tabType switch
{
- MyCommon.TabUsageType.Home => "/statuses/home_timeline",
+ MyCommon.TabUsageType.Home => GetTimelineRequest.EndpointName,
MyCommon.TabUsageType.UserDefined => "/statuses/home_timeline",
MyCommon.TabUsageType.Mentions => "/statuses/mentions_timeline",
MyCommon.TabUsageType.Favorites => "/favorites/list",
else
{
// 表示中のタブに関連する endpoint であれば更新
- var update = endpointName switch
- {
- "/statuses/home_timeline" => tabType == MyCommon.TabUsageType.Home || tabType == MyCommon.TabUsageType.UserDefined,
- "/statuses/mentions_timeline" => tabType == MyCommon.TabUsageType.Mentions,
- "/favorites/list" => tabType == MyCommon.TabUsageType.Favorites,
- "/direct_messages/events/list" => tabType == MyCommon.TabUsageType.DirectMessage,
- "/statuses/user_timeline" => tabType == MyCommon.TabUsageType.UserTimeline,
- "/lists/statuses" => tabType == MyCommon.TabUsageType.Lists,
- "/search/tweets" => tabType == MyCommon.TabUsageType.PublicSearch,
- "/statuses/show/:id" => tabType == MyCommon.TabUsageType.Related,
- _ => false,
- };
+ bool update;
+ if (endpointName == GetTimelineRequest.EndpointName)
+ {
+ update = tabType == MyCommon.TabUsageType.Home || tabType == MyCommon.TabUsageType.UserDefined;
+ }
+ else
+ {
+ update = endpointName switch
+ {
+ "/statuses/mentions_timeline" => tabType == MyCommon.TabUsageType.Mentions,
+ "/favorites/list" => tabType == MyCommon.TabUsageType.Favorites,
+ "/direct_messages/events/list" => tabType == MyCommon.TabUsageType.DirectMessage,
+ "/statuses/user_timeline" => tabType == MyCommon.TabUsageType.UserTimeline,
+ "/lists/statuses" => tabType == MyCommon.TabUsageType.Lists,
+ "/search/tweets" => tabType == MyCommon.TabUsageType.PublicSearch,
+ "/statuses/show/:id" => tabType == MyCommon.TabUsageType.Related,
+ _ => false,
+ };
+ }
+
if (update)
{
this.toolStripApiGauge.ApiEndpoint = endpointName;
{
if (MyCommon.IsKeyDown(Keys.Shift))
{
- await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.Value));
+ await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.ToTwitterStatusId()));
return;
}
- if (this.statuses.Posts.TryGetValue(currentPost.InReplyToStatusId.Value, out var repPost))
+ if (this.statuses.Posts.TryGetValue(currentPost.InReplyToStatusId, out var repPost))
{
MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname} ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi);
}
{
foreach (var tb in this.statuses.GetTabsByType(MyCommon.TabUsageType.Lists | MyCommon.TabUsageType.PublicSearch))
{
- if (tb == null || !tb.Contains(currentPost.InReplyToStatusId.Value)) break;
- repPost = tb.Posts[currentPost.InReplyToStatusId.Value];
+ if (tb == null || !tb.Contains(currentPost.InReplyToStatusId)) break;
+ repPost = tb.Posts[currentPost.InReplyToStatusId];
MessageBox.Show($"{repPost.ScreenName} / {repPost.Nickname} ({repPost.CreatedAt.ToLocalTimeString()})" + Environment.NewLine + repPost.TextFromApi);
return;
}
- await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.Value));
+ await MyCommon.OpenInBrowserAsync(this, MyCommon.GetStatusUrl(currentPost.InReplyToUser, currentPost.InReplyToStatusId.ToTwitterStatusId()));
}
}
}
private void MyList_ColumnReordered(object sender, ColumnReorderedEventArgs e)
{
- var lst = (DetailsListView)sender;
- if (this.settings.Local == null) return;
-
- if (this.iconCol)
+ if (this.Use2ColumnsMode)
{
- this.settings.Local.Width1 = lst.Columns[0].Width;
- this.settings.Local.Width3 = lst.Columns[1].Width;
+ e.Cancel = true;
+ return;
}
- else
- {
- var darr = new int[lst.Columns.Count];
- for (var i = 0; i < lst.Columns.Count; i++)
- {
- darr[lst.Columns[i].DisplayIndex] = i;
- }
- MyCommon.MoveArrayItem(darr, e.OldDisplayIndex, e.NewDisplayIndex);
- for (var i = 0; i < lst.Columns.Count; i++)
- {
- switch (darr[i])
- {
- case 0:
- this.settings.Local.DisplayIndex1 = i;
- break;
- case 1:
- this.settings.Local.DisplayIndex2 = i;
- break;
- case 2:
- this.settings.Local.DisplayIndex3 = i;
- break;
- case 3:
- this.settings.Local.DisplayIndex4 = i;
- break;
- case 4:
- this.settings.Local.DisplayIndex5 = i;
- break;
- case 5:
- this.settings.Local.DisplayIndex6 = i;
- break;
- case 6:
- this.settings.Local.DisplayIndex7 = i;
- break;
- case 7:
- this.settings.Local.DisplayIndex8 = i;
- break;
- }
- }
- this.settings.Local.Width1 = lst.Columns[0].Width;
- this.settings.Local.Width2 = lst.Columns[1].Width;
- this.settings.Local.Width3 = lst.Columns[2].Width;
- this.settings.Local.Width4 = lst.Columns[3].Width;
- this.settings.Local.Width5 = lst.Columns[4].Width;
- this.settings.Local.Width6 = lst.Columns[5].Width;
- this.settings.Local.Width7 = lst.Columns[6].Width;
- this.settings.Local.Width8 = lst.Columns[7].Width;
- }
+ var lst = (DetailsListView)sender;
+ var columnsCount = lst.Columns.Count;
+
+ var darr = new int[columnsCount];
+ for (var i = 0; i < columnsCount; i++)
+ darr[lst.Columns[i].DisplayIndex] = i;
+
+ MyCommon.MoveArrayItem(darr, e.OldDisplayIndex, e.NewDisplayIndex);
+
+ for (var i = 0; i < columnsCount; i++)
+ this.settings.Local.ColumnsOrder[darr[i]] = i;
+
this.MarkSettingLocalModified();
this.isColumnChanged = true;
}
if (this.settings.Local == null) return;
var modified = false;
- if (this.iconCol)
+ if (this.Use2ColumnsMode)
{
- if (this.settings.Local.Width1 != lst.Columns[0].Width)
+ if (this.settings.Local.ColumnsWidth[0] != lst.Columns[0].Width)
{
- this.settings.Local.Width1 = lst.Columns[0].Width;
+ this.settings.Local.ColumnsWidth[0] = lst.Columns[0].Width;
modified = true;
}
- if (this.settings.Local.Width3 != lst.Columns[1].Width)
+ if (this.settings.Local.ColumnsWidth[2] != lst.Columns[1].Width)
{
- this.settings.Local.Width3 = lst.Columns[1].Width;
+ this.settings.Local.ColumnsWidth[2] = lst.Columns[1].Width;
modified = true;
}
}
else
{
- if (this.settings.Local.Width1 != lst.Columns[0].Width)
- {
- this.settings.Local.Width1 = lst.Columns[0].Width;
- modified = true;
- }
- if (this.settings.Local.Width2 != lst.Columns[1].Width)
- {
- this.settings.Local.Width2 = lst.Columns[1].Width;
- modified = true;
- }
- if (this.settings.Local.Width3 != lst.Columns[2].Width)
- {
- this.settings.Local.Width3 = lst.Columns[2].Width;
- modified = true;
- }
- if (this.settings.Local.Width4 != lst.Columns[3].Width)
- {
- this.settings.Local.Width4 = lst.Columns[3].Width;
- modified = true;
- }
- if (this.settings.Local.Width5 != lst.Columns[4].Width)
- {
- this.settings.Local.Width5 = lst.Columns[4].Width;
- modified = true;
- }
- if (this.settings.Local.Width6 != lst.Columns[5].Width)
- {
- this.settings.Local.Width6 = lst.Columns[5].Width;
- modified = true;
- }
- if (this.settings.Local.Width7 != lst.Columns[6].Width)
- {
- this.settings.Local.Width7 = lst.Columns[6].Width;
- modified = true;
- }
- if (this.settings.Local.Width8 != lst.Columns[7].Width)
+ var columnsCount = lst.Columns.Count;
+ for (var i = 0; i < columnsCount; i++)
{
- this.settings.Local.Width8 = lst.Columns[7].Width;
+ if (this.settings.Local.ColumnsWidth[i] == lst.Columns[i].Width)
+ continue;
+
+ this.settings.Local.ColumnsWidth[i] = lst.Columns[i].Width;
modified = true;
}
}
var match = Regex.Match(uri.AbsolutePath, @"^/status/(\d+)$");
if (match.Success)
{
- var statusId = long.Parse(match.Groups[1].Value);
+ var statusId = new TwitterStatusId(match.Groups[1].Value);
await this.OpenRelatedTab(statusId);
return;
}
}
- private void ListTabSelect(TabPage tab)
+ private void ListTabSelect(TabPage tabPage)
{
this.SetListProperty();
- this.PurgeListViewItemCache();
+ var previousTabName = this.CurrentTabName;
+ if (this.listViewState.TryGetValue(previousTabName, out var previousListViewState))
+ previousListViewState.Save(this.ListLockMenuItem.Checked);
+
+ this.listCache?.PurgeCache();
+
+ this.statuses.SelectTab(tabPage.Text);
- this.statuses.SelectTab(tab.Text);
+ this.InitializeTimelineListView();
+
+ var tab = this.CurrentTab;
+ tab.ClearAnchor();
var listView = this.CurrentListView;
- this.anchorPost = null;
- this.anchorFlag = false;
+ var currentListViewState = this.listViewState[tab.TabName];
+ currentListViewState.Restore(forceScroll: true);
- if (this.iconCol)
+ if (this.Use2ColumnsMode)
{
listView.Columns[1].Text = this.columnText[2];
}
}
}
+ private void InitializeTimelineListView()
+ {
+ var listView = this.CurrentListView;
+ var tab = this.CurrentTab;
+
+ var newCache = new TimelineListViewCache(listView, tab, this.settings.Common);
+ (this.listCache, var oldCache) = (newCache, this.listCache);
+ oldCache?.Dispose();
+
+ var newDrawer = new TimelineListViewDrawer(listView, tab, this.listCache, this.iconCache, this.themeManager);
+ (this.listDrawer, var oldDrawer) = (newDrawer, this.listDrawer);
+ oldDrawer?.Dispose();
+
+ newDrawer.IconSize = this.settings.Common.IconSize;
+ newDrawer.UpdateItemHeight();
+ }
+
private void ListTab_Selecting(object sender, TabControlCancelEventArgs e)
=> this.ListTabSelect(e.TabPage);
if (flg) lView.Invalidate(bnd);
}
- private void SelectListItem(DetailsListView lView, int[]? index, int focusedIndex, int selectionMarkIndex)
- {
- // 複数
- var bnd = new Rectangle();
- var flg = false;
- var item = lView.FocusedItem;
- if (item != null)
- {
- bnd = item.Bounds;
- flg = true;
- }
-
- if (index != null)
- {
- lView.SelectItems(index);
- }
- if (selectionMarkIndex > -1 && lView.VirtualListSize > selectionMarkIndex)
- {
- lView.SelectionMark = selectionMarkIndex;
- }
- if (focusedIndex > -1 && lView.VirtualListSize > focusedIndex)
- {
- lView.Items[focusedIndex].Focused = true;
- }
- else if (index != null && index.Length != 0)
- {
- lView.Items[index.Last()].Focused = true;
- }
-
- if (flg) lView.Invalidate(bnd);
- }
-
private async void TweenMain_Shown(object sender, EventArgs e)
{
this.NotifyIcon1.Visible = true;
{
var selectedPosts = this.CurrentTab.SelectedPosts;
- if (selectedPosts.Any(x => !x.CanRetweetBy(this.twitterApi.CurrentUserId)))
+ if (selectedPosts.Any(x => !x.CanRetweetBy(this.tw.UserId)))
{
if (selectedPosts.Any(x => x.IsProtect))
MessageBox.Show("Protected.");
// TweetFormatterクラスによって整形された状態のHTMLを元のツイートに復元します
// 通常の URL
- statusHtml = Regex.Replace(statusHtml, "<a href=\"(?<href>.+?)\" title=\"(?<title>.+?)\">(?<text>.+?)</a>", "${title}");
+ statusHtml = Regex.Replace(statusHtml, """<a href="(?<href>.+?)" title="(?<title>.+?)">(?<text>.+?)</a>""", "${title}");
// メンション
- statusHtml = Regex.Replace(statusHtml, "<a class=\"mention\" href=\"(?<href>.+?)\">(?<text>.+?)</a>", "${text}");
+ statusHtml = Regex.Replace(statusHtml, """<a class="mention" href="(?<href>.+?)">(?<text>.+?)</a>""", "${text}");
// ハッシュタグ
- statusHtml = Regex.Replace(statusHtml, "<a class=\"hashtag\" href=\"(?<href>.+?)\">(?<text>.+?)</a>", "${text}");
+ statusHtml = Regex.Replace(statusHtml, """<a class="hashtag" href="(?<href>.+?)">(?<text>.+?)</a>""", "${text}");
// 絵文字
- statusHtml = Regex.Replace(statusHtml, "<img class=\"emoji\" src=\".+?\" alt=\"(?<text>.+?)\" />", "${text}");
+ statusHtml = Regex.Replace(statusHtml, """<img class="emoji" src=".+?" alt="(?<text>.+?)" />""", "${text}");
// <br> 除去
if (multiline)
{
try
{
- var task = this.twitterApi.FriendshipsCreate(id).IgnoreResponse();
+ var task = this.tw.Api.FriendshipsCreate(id).IgnoreResponse();
await dialog.WaitForAsync(this, task);
}
catch (WebApiException ex)
{
try
{
- var task = this.twitterApi.FriendshipsDestroy(id).IgnoreResponse();
+ var task = this.tw.Api.FriendshipsDestroy(id).IgnoreResponse();
await dialog.WaitForAsync(this, task);
}
catch (WebApiException ex)
try
{
- var task = this.twitterApi.FriendshipsShow(this.twitterApi.CurrentScreenName, id);
+ var task = this.tw.Api.FriendshipsShow(this.tw.Username, id);
var friendship = await dialog.WaitForAsync(this, task);
isFollowing = friendship.Relationship.Source.Following;
try
{
- var task = this.twitterApi.FriendshipsShow(this.twitterApi.CurrentScreenName, id);
+ var task = this.tw.Api.FriendshipsShow(this.tw.Username, id);
var friendship = await dialog.WaitForAsync(this, task);
isFollowing = friendship.Relationship.Source.Following;
cmb.Items.Insert(0, tb.SearchWords);
cmb.Text = tb.SearchWords;
cmb.SelectAll();
- this.PurgeListViewItemCache();
- listView.VirtualListSize = 0;
this.statuses.ClearTabIds(tbName);
+ this.listCache?.PurgeCache();
+ this.listCache?.UpdateListSize();
this.SaveConfigsTabs(); // 検索条件の保存
}
private void UndoRemoveTabMenuItem_Click(object sender, EventArgs e)
{
- if (this.statuses.RemovedTab.Count == 0)
- {
- MessageBox.Show("There isn't removed tab.", "Undo", MessageBoxButtons.OK, MessageBoxIcon.Information);
- return;
- }
- else
+ try
{
- DetailsListView? listView;
-
- var tb = this.statuses.RemovedTab.Pop();
- if (tb.TabType == MyCommon.TabUsageType.Related)
- {
- var relatedTab = this.statuses.GetTabByType(MyCommon.TabUsageType.Related);
- if (relatedTab != null)
- {
- // 関連発言なら既存のタブを置き換える
- tb.TabName = relatedTab.TabName;
- this.ClearTab(tb.TabName, false);
-
- this.statuses.ReplaceTab(tb);
-
- var tabIndex = this.statuses.Tabs.IndexOf(tb);
- var tabPage = this.ListTab.TabPages[tabIndex];
- listView = (DetailsListView)tabPage.Tag;
- this.ListTab.SelectedIndex = tabIndex;
- }
- else
- {
- const string TabName = "Related Tweets";
- var renamed = TabName;
- for (var i = 2; i <= 100; i++)
- {
- if (!this.statuses.ContainsTab(renamed))
- break;
- renamed = TabName + i;
- }
- tb.TabName = renamed;
-
- this.statuses.AddTab(tb);
- this.AddNewTab(tb, startup: false);
-
- var tabIndex = this.statuses.Tabs.Count - 1;
- var tabPage = this.ListTab.TabPages[tabIndex];
-
- listView = (DetailsListView)tabPage.Tag;
- this.ListTab.SelectedIndex = tabIndex;
- }
- }
- else
- {
- var renamed = tb.TabName;
- for (var i = 1; i < int.MaxValue; i++)
- {
- if (!this.statuses.ContainsTab(renamed))
- break;
- renamed = tb.TabName + "(" + i + ")";
- }
- tb.TabName = renamed;
-
- this.statuses.AddTab(tb);
- this.AddNewTab(tb, startup: false);
+ var restoredTab = this.statuses.UndoRemovedTab();
+ this.AddNewTab(restoredTab, startup: false);
- var tabIndex = this.statuses.Tabs.Count - 1;
- var tabPage = this.ListTab.TabPages[tabIndex];
+ var tabIndex = this.statuses.Tabs.Count - 1;
+ this.ListTab.SelectedIndex = tabIndex;
- listView = (DetailsListView)tabPage.Tag;
- this.ListTab.SelectedIndex = tabIndex;
- }
this.SaveConfigsTabs();
-
- if (listView != null)
- {
- using (ControlTransaction.Update(listView))
- {
- listView.VirtualListSize = tb.AllCount;
- }
- }
+ }
+ catch (TabException ex)
+ {
+ MessageBox.Show(this, ex.Message, ApplicationSettings.ApplicationName, MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
public void ListManageUserContext(string screenName)
{
- using var listSelectForm = new MyLists(screenName, this.twitterApi);
+ using var listSelectForm = new MyLists(screenName, this.tw.Api);
listSelectForm.ShowDialog(this);
}
this.OpenStatusOpMenuItem.Enabled = true;
this.ShowRelatedStatusesMenuItem2.Enabled = true; // PublicSearchの時問題出るかも
- if (!post.CanRetweetBy(this.twitterApi.CurrentUserId))
+ if (!post.CanRetweetBy(this.tw.UserId))
{
this.RtOpMenuItem.Enabled = false;
this.RtUnOpMenuItem.Enabled = false;
private void MenuItemEdit_DropDownOpening(object sender, EventArgs e)
{
- if (this.statuses.RemovedTab.Count == 0)
- {
- this.UndoRemoveTabMenuItem.Enabled = false;
- }
- else
- {
- this.UndoRemoveTabMenuItem.Enabled = true;
- }
+ this.UndoRemoveTabMenuItem.Enabled = this.statuses.CanUndoRemovedTab;
if (this.CurrentTab.TabType == MyCommon.TabUsageType.PublicSearch)
this.PublicSearchQueryMenuItem.Enabled = true;
try
{
- var task = this.twitterApi.UsersShow(id);
+ var task = this.tw.Api.UsersShow(id);
user = await dialog.WaitForAsync(this, task);
}
catch (WebApiException ex)
private async Task DoShowUserStatus(TwitterUser user)
{
- using var userDialog = new UserInfoDialog(this, this.twitterApi);
+ using var userDialog = new UserInfoDialog(this, this.tw.Api);
var showUserTask = userDialog.ShowUserAsync(user);
userDialog.ShowDialog(this);
try
{
- var task = this.twitterApi.StatusesShow(statusId);
+ var task = this.tw.Api.StatusesShow(statusId.ToTwitterStatusId());
status = await dialog.WaitForAsync(this, task);
}
catch (WebApiException ex)
private void SelectMedia_DragEnter(DragEventArgs e)
{
- if (this.ImageSelector.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true))
+ if (this.ImageSelector.Model.HasUploadableService(((string[])e.Data.GetData(DataFormats.FileDrop, false))[0], true))
{
e.Effect = DragDropEffects.Copy;
return;
{
this.Activate();
this.BringToFront();
- this.ImageSelector.BeginSelection((string[])e.Data.GetData(DataFormats.FileDrop, false));
+
+ var filePathArray = (string[])e.Data.GetData(DataFormats.FileDrop, false);
+ this.ImageSelector.BeginSelection();
+ this.ImageSelector.Model.AddMediaItemFromFilePath(filePathArray);
this.StatusText.Focus();
}
}
else if (Clipboard.ContainsImage())
{
- // 画像があるので投稿処理を行う
- if (MessageBox.Show(Properties.Resources.PostPictureConfirm3,
- Properties.Resources.PostPictureWarn4,
- MessageBoxButtons.OKCancel,
- MessageBoxIcon.Question,
- MessageBoxDefaultButton.Button2)
- == DialogResult.OK)
- {
- // clipboardから画像を取得
- using var image = Clipboard.GetImage();
- this.ImageSelector.BeginSelection(image);
- }
+ // clipboardから画像を取得
+ using var image = Clipboard.GetImage();
+ this.ImageSelector.BeginSelection();
+ this.ImageSelector.Model.AddMediaItemFromImage(image);
}
else if (Clipboard.ContainsFileDropList())
{
var files = Clipboard.GetFileDropList().Cast<string>().ToArray();
- this.ImageSelector.BeginSelection(files);
+ this.ImageSelector.BeginSelection();
+ this.ImageSelector.Model.AddMediaItemFromFilePath(files);
}
}
catch (ExternalException ex)
/// </summary>
/// <param name="statusId">表示するツイートのID</param>
/// <exception cref="TabException">名前の重複が多すぎてタブを作成できない場合</exception>
- public async Task OpenRelatedTab(long statusId)
+ public async Task OpenRelatedTab(PostId statusId)
{
var post = this.statuses[statusId];
if (post == null)
{
try
{
- post = await this.tw.GetStatusApi(false, statusId);
+ post = await this.tw.GetStatusApi(false, statusId.ToTwitterStatusId());
}
catch (WebApiException ex)
{
if (curTimeOffset != prevTimeOffset)
{
// タイムゾーンの変更を反映
- this.PurgeListViewItemCache();
+ this.listCache?.PurgeCache();
this.CurrentListView.Refresh();
this.DispSelectedPost(forceupdate: true);
private async Task OpenUserAppointUrl()
{
- if (this.settings.Common.UserAppointUrl != null)
+ if (!MyCommon.IsNullOrEmpty(this.settings.Common.UserAppointUrl))
{
if (this.settings.Common.UserAppointUrl.Contains("{ID}") || this.settings.Common.UserAppointUrl.Contains("{STATUS}"))
{
xUrl = xUrl.Replace("{ID}", post.ScreenName);
var statusId = post.RetweetedId ?? post.StatusId;
- xUrl = xUrl.Replace("{STATUS}", statusId.ToString());
+ xUrl = xUrl.Replace("{STATUS}", statusId.Id);
await MyCommon.OpenInBrowserAsync(this, xUrl);
}
this.BringToFront();
if (e.NotifyType == GrowlHelper.NotifyType.DirectMessage)
{
- if (!this.GoDirectMessage(e.StatusId)) this.StatusText.Focus();
+ if (!this.GoDirectMessage(new TwitterStatusId(e.StatusId))) this.StatusText.Focus();
}
else
{
- if (!this.GoStatus(e.StatusId)) this.StatusText.Focus();
+ if (!this.GoStatus(new TwitterStatusId(e.StatusId))) this.StatusText.Focus();
}
});
}
this.AboutMenuItem.Text = MyCommon.ReplaceAppName(this.AboutMenuItem.Text);
}
- private void TweetThumbnail_ThumbnailLoading(object sender, EventArgs e)
- => this.SplitContainer3.Panel2Collapsed = false;
-
- private async void TweetThumbnail_ThumbnailDoubleClick(object sender, ThumbnailDoubleClickEventArgs e)
- => await this.OpenThumbnailPicture(e.Thumbnail);
-
- private async void TweetThumbnail_ThumbnailImageSearchClick(object sender, ThumbnailImageSearchEventArgs e)
- => await MyCommon.OpenInBrowserAsync(this, e.ImageUrl);
-
- private async Task OpenThumbnailPicture(ThumbnailInfo thumbnail)
- {
- var url = thumbnail.FullSizeImageUrl ?? thumbnail.MediaPageUrl;
-
- await MyCommon.OpenInBrowserAsync(this, url);
- }
-
private async void TwitterApiStatusToolStripMenuItem_Click(object sender, EventArgs e)
=> await MyCommon.OpenInBrowserAsync(this, Twitter.ServiceAvailabilityStatusUrl);
{
if (this.settings.Common.IconSize == iconSize) return;
- var oldIconCol = this.iconCol;
+ var oldIconCol = this.Use2ColumnsMode;
this.settings.Common.IconSize = iconSize;
this.ApplyListViewIconSize(iconSize);
- if (this.iconCol != oldIconCol)
+ if (this.Use2ColumnsMode != oldIconCol)
{
foreach (TabPage tp in this.ListTab.TabPages)
{