diff --git a/README.md b/README.md index 122171e..04fb36e 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,5 @@ utility that lets you quickly force any program to be always on top should work on any version of windows that supports .net framework 4.0 + +[Click here to download!](https://github.com/flashwave/topmostfriend/releases/latest) diff --git a/TopMostFriend/AboutWindow.cs b/TopMostFriend/AboutWindow.cs index f50ab16..a14a4f7 100644 --- a/TopMostFriend/AboutWindow.cs +++ b/TopMostFriend/AboutWindow.cs @@ -15,7 +15,7 @@ namespace TopMostFriend { } public AboutWindow() { - Text = @"About Top Most Friend"; + Text = Locale.String(@"AboutTitle"); Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); BackgroundImage = Properties.Resources.about; StartPosition = FormStartPosition.CenterScreen; @@ -24,11 +24,12 @@ namespace TopMostFriend { ClientSize = Properties.Resources.about.Size; MaximizeBox = MinimizeBox = false; MaximumSize = MinimumSize = Size; + TopMost = true; int tabIndex = 0; Button closeButton = new Button { - Text = @"Close", + Text = Locale.String(@"AboutClose"), Size = new Size(BUTTON_WIDTH, BUTTON_HEIGHT), TabIndex = ++tabIndex, }; @@ -37,7 +38,7 @@ namespace TopMostFriend { Controls.Add(closeButton); Button websiteButton = new Button { - Text = @"Website", + Text = Locale.String(@"AboutWebsite"), Size = new Size(BUTTON_WIDTH, BUTTON_HEIGHT), TabIndex = ++tabIndex, }; @@ -46,7 +47,7 @@ namespace TopMostFriend { Controls.Add(websiteButton); Button donateButton = new Button { - Text = @"Donate", + Text = Locale.String(@"AboutDonate"), Size = new Size(BUTTON_WIDTH, BUTTON_HEIGHT), TabIndex = ++tabIndex, }; diff --git a/TopMostFriend/ActionTimeout.cs b/TopMostFriend/ActionTimeout.cs new file mode 100644 index 0000000..cf2b542 --- /dev/null +++ b/TopMostFriend/ActionTimeout.cs @@ -0,0 +1,35 @@ +using System; +using System.Threading; + +namespace TopMostFriend { + public class ActionTimeout { + private readonly Action Action; + private bool Continue = true; + private int Remaining = 0; + private const int STEP = 500; + + public ActionTimeout(Action action, int timeout) { + Action = action ?? throw new ArgumentNullException(nameof(action)); + if(timeout < 1) + throw new ArgumentException(@"Timeout must be a positive integer.", nameof(timeout)); + Remaining = timeout; + new Thread(ThreadBody) { IsBackground = true }.Start(); + } + + private void ThreadBody() { + do { + Thread.Sleep(STEP); + Remaining -= STEP; + + if(!Continue) + return; + } while(Remaining > 0); + + Action.Invoke(); + } + + public void Cancel() { + Continue = false; + } + } +} diff --git a/TopMostFriend/BlacklistWindow.cs b/TopMostFriend/BlacklistWindow.cs index 91dcc16..605900e 100644 --- a/TopMostFriend/BlacklistWindow.cs +++ b/TopMostFriend/BlacklistWindow.cs @@ -35,26 +35,27 @@ namespace TopMostFriend { MinimizeBox = MaximizeBox = false; MinimumSize = Size; DialogResult = DialogResult.Cancel; + TopMost = true; BlacklistView = new ListBox { TabIndex = 101, IntegralHeight = false, Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, - Location = new Point(BUTTON_WIDTH + (SPACING * 2), SPACING), + Location = new Point(SPACING, SPACING), ClientSize = new Size(ClientSize.Width - BUTTON_WIDTH - (int)(SPACING * 3.5), ClientSize.Height - (int)(SPACING * 2.5)), }; BlacklistView.SelectedIndexChanged += BlacklistView_SelectedIndexChanged; BlacklistView.MouseDoubleClick += BlacklistView_MouseDoubleClick; AddButton = new Button { - Anchor = AnchorStyles.Top | AnchorStyles.Left, - Text = @"Add", + Anchor = AnchorStyles.Top | AnchorStyles.Right, + Text = Locale.String(@"BlacklistAdd"), ClientSize = new Size(BUTTON_WIDTH, BUTTON_HEIGHT), - Location = new Point(SPACING, SPACING), + Location = new Point(BlacklistView.Width + (SPACING * 2), SPACING), TabIndex = 201, }; EditButton = new Button { - Text = @"Edit", + Text = Locale.String(@"BlacklistEdit"), Location = new Point(AddButton.Location.X, AddButton.Location.Y + AddButton.Height + SPACING), Enabled = false, Anchor = AddButton.Anchor, @@ -62,7 +63,7 @@ namespace TopMostFriend { TabIndex = 202, }; RemoveButton = new Button { - Text = @"Remove", + Text = Locale.String(@"BlacklistRemove"), Location = new Point(AddButton.Location.X, EditButton.Location.Y + AddButton.Height + SPACING), Enabled = false, Anchor = AddButton.Anchor, @@ -75,16 +76,16 @@ namespace TopMostFriend { RemoveButton.Click += RemoveButton_Click; CancelButton = new Button { - Anchor = AnchorStyles.Bottom | AnchorStyles.Left, - Text = @"Cancel", - Location = new Point(SPACING, ClientSize.Height - AddButton.ClientSize.Height - SPACING), + Anchor = AnchorStyles.Bottom | AnchorStyles.Right, + Text = Locale.String(@"BlacklistCancel"), + Location = new Point(AddButton.Location.X, ClientSize.Height - AddButton.ClientSize.Height - SPACING), ClientSize = AddButton.ClientSize, TabIndex = 10001, }; Button acceptButton = new Button { - Anchor = AnchorStyles.Bottom | AnchorStyles.Left, - Text = @"Done", - Location = new Point(SPACING, ClientSize.Height - ((AddButton.ClientSize.Height + SPACING) * 2)), + Anchor = AnchorStyles.Bottom | AnchorStyles.Right, + Text = Locale.String(@"BlacklistDone"), + Location = new Point(AddButton.Location.X, ClientSize.Height - ((AddButton.ClientSize.Height + SPACING) * 2)), ClientSize = AddButton.ClientSize, TabIndex = 10002, }; @@ -106,16 +107,18 @@ namespace TopMostFriend { public string Original { get; } public string String { get => TextBox.Text; } - private TextBox TextBox; + private readonly TextBox TextBox; public BlacklistEditorWindow(string original = null) { Original = original ?? string.Empty; - Text = original == null ? @"Adding new entry..." : $@"Editing {original}..."; + Text = Locale.String(original == null ? @"BlacklistEditorAdding" : @"BlacklistEditorEditing", original); + Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); StartPosition = FormStartPosition.CenterParent; - FormBorderStyle = FormBorderStyle.FixedToolWindow; + FormBorderStyle = FormBorderStyle.FixedSingle; ClientSize = new Size(500, 39); MaximizeBox = MinimizeBox = false; MaximumSize = MinimumSize = Size; + TopMost = true; Button cancelButton = new Button { Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Right, @@ -123,7 +126,7 @@ namespace TopMostFriend { Name = @"cancelButton", Size = new Size(75, 23), TabIndex = 102, - Text = @"Cancel", + Text = Locale.String(@"BlacklistEditorCancel"), }; cancelButton.Click += (s, e) => { DialogResult = DialogResult.Cancel; Close(); }; @@ -133,14 +136,14 @@ namespace TopMostFriend { Name = @"saveButton", Size = new Size(75, 23), TabIndex = 101, - Text = @"Save", + Text = Locale.String(@"BlacklistEditorSave"), }; saveButton.Click += (s, e) => { DialogResult = DialogResult.OK; Close(); }; TextBox = new TextBox { Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right, Location = new Point(8, 9), - Name = @"deviceSelection", + Name = @"deviceSelection", // ?????? Size = new Size(ClientSize.Width - 8 - cancelButton.Width - saveButton.Width - 19, 23), TabIndex = 100, Text = Original, diff --git a/TopMostFriend/FirstRunWindow.cs b/TopMostFriend/FirstRunWindow.cs new file mode 100644 index 0000000..1648281 --- /dev/null +++ b/TopMostFriend/FirstRunWindow.cs @@ -0,0 +1,357 @@ +using System; +using System.Diagnostics; +using System.Drawing; +using System.Threading; +using System.Windows.Forms; + +namespace TopMostFriend { + public class FirstRunWindow : Form { + public static void Display() { + using(FirstRunWindow firstRun = new FirstRunWindow()) + firstRun.ShowDialog(); + } + + private bool CanClose = false; + private bool IsClosing = false; + private bool IsSizing = false; + + private Button NextBtn { get; } + private Button PrevBtn { get; } + + private Action NextAct = null; + private Action PrevAct = null; + + private bool NextVisible { get => NextBtn.Visible; set => NextBtn.Visible = value; } + private bool PrevVisible { get => PrevBtn.Visible; set => PrevBtn.Visible = value; } + + private Panel WorkArea { get; } + + public FirstRunWindow() { + Text = Program.TITLE + @" v" + Application.ProductVersion.Substring(0, Application.ProductVersion.Length - 2); + Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); + StartPosition = FormStartPosition.CenterScreen; + FormBorderStyle = FormBorderStyle.FixedSingle; + AutoScaleMode = AutoScaleMode.Dpi; + MaximizeBox = MinimizeBox = false; + TopMost = true; + ClientSize = new Size(410, 80); + + Controls.Add(new PictureBox { + Image = Properties.Resources.firstrun, + Size = Properties.Resources.firstrun.Size, + Location = new Point(0, 0), + }); + + NextBtn = new Button { + Text = Locale.String(@"FirstRunNext"), + Anchor = AnchorStyles.Right | AnchorStyles.Bottom, + Visible = false, + Location = new Point(ClientSize.Width - 81, ClientSize.Height - 29), + }; + NextBtn.Click += NextBtn_Click; + Controls.Add(NextBtn); + + PrevBtn = new Button { + Text = Locale.String(@"FirstRunPrev"), + Anchor = AnchorStyles.Left | AnchorStyles.Bottom, + Visible = false, + Location = new Point(6, ClientSize.Height - 29), + }; + PrevBtn.Click += PrevBtn_Click; + Controls.Add(PrevBtn); + + WorkArea = new Panel { + Dock = DockStyle.Fill, + }; + Controls.Add(WorkArea); + } + + private void PrevBtn_Click(object sender, EventArgs e) { + if(!PrevVisible) + return; + WorkArea.Controls.Clear(); + if(PrevAct == null) + Close(); + else + Invoke(PrevAct); + } + + private void NextBtn_Click(object sender, EventArgs e) { + if(!NextVisible) + return; + WorkArea.Controls.Clear(); + if(NextAct == null) + Close(); + else + Invoke(NextAct); + } + + protected override void OnShown(EventArgs e) { + base.OnShown(e); + Update(); + Thread.Sleep(500); + ShowPageIntro(); + } + + protected override void OnFormClosing(FormClosingEventArgs e) { + if(e.CloseReason == CloseReason.UserClosing && !CanClose) { + e.Cancel = true; + + if(!IsClosing) { + IsClosing = true; + + SetHeight(80, new Action(() => { + CanClose = true; + Thread.Sleep(100); + Close(); + })); + } + return; + } + + base.OnFormClosing(e); + } + + public void SetHeight(int height, Action onFinish = null) { + if(height < 80) + throw new ArgumentException(@"target height must be more than or equal to 80.", nameof(height)); + + if(IsSizing) + return; + IsSizing = true; + + const int timeout = 1000 / 60; + double time = 0; + double period = timeout / 400d; + + int currentHeight = ClientSize.Height; + int currentY = Location.Y; + int diffHeight = height - currentHeight; + int diffY = diffHeight / 2; + + Action setHeight = new Action(() => { + int newHeight = currentHeight + (int)Math.Ceiling(time * diffHeight); + int newY = currentY - (int)Math.Ceiling(time * diffY); + ClientSize = new Size(ClientSize.Width, newHeight); + Location = new Point(Location.X, newY); + }); + + new Thread(() => { + Stopwatch sw = new Stopwatch(); + + try { + do { + sw.Restart(); + + Invoke(setHeight); + time += period; + + int delay = timeout - (int)sw.ElapsedMilliseconds; + if(delay > 1) + Thread.Sleep(delay); + } while(time < 1d + period); + } finally { + sw.Stop(); + if(onFinish != null) + Invoke(onFinish); + IsSizing = false; + } + }) { + IsBackground = true, + Priority = ThreadPriority.AboveNormal, + }.Start(); + } + + public void ShowPageIntro() { + Text = Locale.String(@"FirstRunWelcomeTitle"); + PrevVisible = false; + NextVisible = true; + NextAct = ShowPageHotKey; + + SetHeight(190, new Action(() => { + WorkArea.Controls.Add(new Label { + Text = Locale.String(@"FirstRunWelcomeIntro"), + Location = new Point(10, 90), + Size = new Size(ClientSize.Width - 20, 200), + }); + })); + } + + public void ShowPageHotKey() { + Text = Locale.String(@"FirstRunHotKeyTitle"); + PrevVisible = NextVisible = true; + PrevAct = ShowPageIntro; + NextAct = () => { + Program.SetForegroundHotKey(Settings.Get(Program.FOREGROUND_HOTKEY_SETTING, 0)); + ShowPageElevation(); + }; + + SetHeight(230, new Action(() => { + WorkArea.Controls.Add(new Label { + Text = Locale.String(@"FirstRunHotKeyExplain"), + Location = new Point(10, 90), + Size = new Size(ClientSize.Width - 20, 40), + }); + + SettingsWindow.CreateHotKeyInput( + WorkArea, + () => Settings.Get(Program.FOREGROUND_HOTKEY_SETTING, 0), + keyCode => Settings.Set(Program.FOREGROUND_HOTKEY_SETTING, keyCode), + 0, + 110 + ); + + CheckBox flShowNotification = new CheckBox { + Text = Locale.String(@"FirstRunHotKeyNotify"), + Location = new Point(12, 170), + Checked = Settings.Get(Program.TOGGLE_BALLOON_SETTING, Program.ToggleBalloonDefault), + AutoSize = true, + TabIndex = 201, + }; + flShowNotification.CheckedChanged += (s, e) => { + Settings.Set(Program.TOGGLE_BALLOON_SETTING, flShowNotification.Checked); + }; + WorkArea.Controls.Add(flShowNotification); + })); + } + + public void ShowPageElevation() { + Text = Locale.String(@"FirstRunAdminTitle"); + PrevVisible = NextVisible = true; + PrevAct = ShowPageHotKey; + NextAct = ShowPageThanks; + + SetHeight(280, () => { + WorkArea.Controls.Add(new Label { + Text = Locale.String(@"FirstRunAdminExplain"), + Location = new Point(10, 90), + Size = new Size(ClientSize.Width - 20, 40), + }); + + bool alwaysAdmin = Settings.Get(Program.ALWAYS_ADMIN_SETTING, false); + bool implicitAdmin = Settings.Get(Program.ALWAYS_RETRY_ELEVATED, false); + + RadioButton rdAsk = new RadioButton { + Text = Locale.String(@"FirstRunAdminOptionAsk"), + Location = new Point(10, 140), + Size = new Size(ClientSize.Width - 20, 30), + Appearance = Appearance.Button, + Checked = !alwaysAdmin && !implicitAdmin, + }; + rdAsk.CheckedChanged += (s, e) => { + if(rdAsk.Checked) { + Settings.Set(Program.ALWAYS_ADMIN_SETTING, false); + Settings.Set(Program.ALWAYS_RETRY_ELEVATED, false); + } + }; + WorkArea.Controls.Add(rdAsk); + + RadioButton rdImplicit = new RadioButton { + Text = Locale.String(@"FirstRunAdminOptionImplicit"), + Location = new Point(rdAsk.Location.X, rdAsk.Location.Y + 36), + Size = rdAsk.Size, + Appearance = rdAsk.Appearance, + Checked = implicitAdmin && !alwaysAdmin, + }; + rdImplicit.CheckedChanged += (s, e) => { + if(rdImplicit.Checked) { + Settings.Set(Program.ALWAYS_ADMIN_SETTING, false); + Settings.Set(Program.ALWAYS_RETRY_ELEVATED, true); + } + }; + WorkArea.Controls.Add(rdImplicit); + + RadioButton rdAlways = new RadioButton { + Text = Locale.String(@"FirstRunAdminOptionAlways"), + Location = new Point(rdAsk.Location.X, rdImplicit.Location.Y + 36), + Size = rdAsk.Size, + Appearance = rdAsk.Appearance, + Checked = alwaysAdmin, + }; + rdAlways.CheckedChanged += (s, e) => { + if(rdAlways.Checked) { + Settings.Set(Program.ALWAYS_ADMIN_SETTING, true); + Settings.Set(Program.ALWAYS_RETRY_ELEVATED, false); + } + }; + WorkArea.Controls.Add(rdAlways); + }); + } + + public void ShowPageThanks() { + Text = Locale.String(@"FirstRunThanksTitle"); + PrevVisible = NextVisible = true; + PrevAct = ShowPageElevation; + NextAct = CheckRestartNeeded; + + SetHeight(270, () => { + Label thankYou = new Label { + Text = Locale.String(@"FirstRunThanksThank"), + Location = new Point(10, 90), + Size = new Size(ClientSize.Width - 20, 20), + }; + WorkArea.Controls.Add(thankYou); + + string updateLinkString = Locale.String(@"FirstRunThanksUpdate"); + + int websiteStart = updateLinkString.IndexOf(@"[WEB]"); + updateLinkString = updateLinkString.Substring(0, websiteStart) + updateLinkString.Substring(websiteStart + 5); + int websiteEnd = updateLinkString.IndexOf(@"[/WEB]"); + updateLinkString = updateLinkString.Substring(0, websiteEnd) + updateLinkString.Substring(websiteEnd + 6); + + int changelogStart = updateLinkString.IndexOf(@"[CHANGELOG]"); + updateLinkString = updateLinkString.Substring(0, changelogStart) + updateLinkString.Substring(changelogStart + 11); + int changelogEnd = updateLinkString.IndexOf(@"[/CHANGELOG]"); + updateLinkString = updateLinkString.Substring(0, changelogEnd) + updateLinkString.Substring(changelogEnd + 12); + + LinkLabel updateLink; + WorkArea.Controls.Add(updateLink = new LinkLabel { + Text = updateLinkString, + Location = new Point(10, 120), + Size = new Size(ClientSize.Width - 20, 34), + Font = thankYou.Font, + Links = { + new LinkLabel.Link(websiteStart, websiteEnd - websiteStart, @"https://flash.moe/topmostfriend"), + new LinkLabel.Link(changelogStart, changelogEnd - changelogStart, @"https://flash.moe/topmostfriend/changelog.php"), + }, + }); + updateLink.LinkClicked += (s, e) => { + Process.Start((string)e.Link.LinkData); + }; + + string settingsLinkString = Locale.String(@"FirstRunThanksSettings"); + + int settingsStart = settingsLinkString.IndexOf(@"[SETTINGS]"); + settingsLinkString = settingsLinkString.Substring(0, settingsStart) + settingsLinkString.Substring(settingsStart + 10); + int settingsEnd = settingsLinkString.IndexOf(@"[/SETTINGS]"); + settingsLinkString = settingsLinkString.Substring(0, settingsEnd) + settingsLinkString.Substring(settingsEnd + 11); + + LinkLabel settingsLink; + WorkArea.Controls.Add(settingsLink = new LinkLabel { + Text = settingsLinkString, + Location = new Point(10, 160), + Size = new Size(ClientSize.Width - 20, 30), + Font = thankYou.Font, + Links = { + new LinkLabel.Link(settingsStart, settingsEnd - settingsStart), + }, + }); + settingsLink.LinkClicked += (s, e) => { + SettingsWindow.Display(); + }; + + WorkArea.Controls.Add(new Label { + Text = Locale.String(@"FirstRunThanksAdmin"), + Location = new Point(10, 200), + Size = new Size(ClientSize.Width - 20, 80), + }); + }); + } + + public void CheckRestartNeeded() { + if(Settings.Get(Program.ALWAYS_ADMIN_SETTING, false)) + UAC.RestartElevated(); + Close(); + } + } +} diff --git a/TopMostFriend/Languages/Language.cs b/TopMostFriend/Languages/Language.cs new file mode 100644 index 0000000..0c89bfd --- /dev/null +++ b/TopMostFriend/Languages/Language.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using System.Xml; +using System.Xml.Serialization; + +namespace TopMostFriend.Languages { + [XmlRoot(@"Language")] + public class Language { + [XmlElement(@"Info")] + public LanguageInfo Info { get; set; } + + [XmlArray(@"Strings")] + [XmlArrayItem(@"String", Type = typeof(LanguageString))] + public LanguageString[] Strings { get; set; } + + public LanguageString GetString(string name) { + if(name == null) + throw new ArgumentNullException(nameof(name)); + return Strings.FirstOrDefault(s => name.Equals(s.Name)); + } + } +} diff --git a/TopMostFriend/Languages/LanguageInfo.cs b/TopMostFriend/Languages/LanguageInfo.cs new file mode 100644 index 0000000..e1bc766 --- /dev/null +++ b/TopMostFriend/Languages/LanguageInfo.cs @@ -0,0 +1,21 @@ +using System.Xml.Serialization; + +namespace TopMostFriend.Languages { + public class LanguageInfo { + [XmlElement(@"Id")] + public string Id { get; set; } + + [XmlElement(@"NameNative")] + public string NameNative { get; set; } + + [XmlElement(@"NameEnglish")] + public string NameEnglish { get; set; } + + [XmlElement(@"TargetVersion")] + public string TargetVersion { get; set; } + + public override string ToString() { + return $@"{NameNative} / {NameEnglish} ({Id})"; + } + } +} diff --git a/TopMostFriend/Languages/LanguageString.cs b/TopMostFriend/Languages/LanguageString.cs new file mode 100644 index 0000000..8511cd9 --- /dev/null +++ b/TopMostFriend/Languages/LanguageString.cs @@ -0,0 +1,19 @@ +using System.Xml.Serialization; + +namespace TopMostFriend.Languages { + public class LanguageString { + [XmlAttribute(@"name")] + public string Name { get; set; } + + [XmlText] + public string Value { get; set; } + + public string Format(params object[] args) { + return string.Format(Value, args); + } + + public override string ToString() { + return $@"{Name}: {Value}"; + } + } +} diff --git a/TopMostFriend/Languages/en-GB.xml b/TopMostFriend/Languages/en-GB.xml new file mode 100644 index 0000000..781735f --- /dev/null +++ b/TopMostFriend/Languages/en-GB.xml @@ -0,0 +1,109 @@ + + + + en-GB + British English + British English + 1.6.0 + + + An instance of {0} is already running. + + &Refresh + &Settings + &About + &Quit + + Now always on top + No longer always on top + Window has no title. + + Wasn't able to change always on top status on this window. + Do you want to try again as an Administrator? A User Account Control dialog will pop up after selecting Yes. + {0} was still unable to change always on top status for the window. It might be protected by Windows. + + About {0} + Close + Website + Donate + + {0} Settings + Apply + Cancel + OK + + Hot Keys + Reset + Toggle always on top status on active window + + Options + Show notification when using toggle hot key + Show icon of window affected by hot key + Always retry changing always on top status as Administrator on first failure + SHIFT+CLICK items in the tray area list to add to the title blacklist + Revert status to before {0} altered them on exit + Show list of open windows in the tray area menu + Always run as Administrator + + Language + You should restart {0} for the language change to fully take effect. A lot of text will still appear in the previously selected language. + + Other + Manage Blacklist... + Title Blacklist + Start with Windows... + +Should {0} start with Windows? +Clicking Yes will create a shortcut to the {0} in your Start-up folder, clicking No will delete the shortcut. +Make sure you've placed your {0} executable in a permanent location before clicking Yes. + + Reset All Settings... + +This will reset all {0} settings and restart the application. +Are you absolutely sure? + + + Add + Edit + Remove + Cancel + Done + + Adding new entry... + Editing {0}... + Cancel + Save + + Next + Previous + + Welcome to {0}! + +{0} is a utility that lets you manage the always-on-top state of windows from programs you have open. + +You will now be taken through a few steps to configure {0} to your liking! + + + Selecting a hot key + +Select a global hot key for toggling the always on top status of whatever window is currently in the foreground. +If you don't wish to specify one just hit Reset followed by Next. + + Show a notification when the hot key has been used + + Administrator actions + +Sometimes {0} is not able to change the always on top state for certain windows without having administrative privileges. +{0} provides multiple options for this situation: + + Ask me what to do when it's required. + Ask for administrative privileges automatically when required. + Always run {0} as Administrator (not recommended). + + Thank you! + Thank you for using {0}! + Be sure to check the {0} [WEB]website[/WEB] and [CHANGELOG]changelog[/CHANGELOG] for updates from time to time. + There are more options you may be interested in taking a look at in the [SETTINGS]Settings[/SETTINGS] window. + If you have chosen to always run {0} as Administrator, you will receive a User Account Control prompt after hitting next. + + diff --git a/TopMostFriend/Languages/nl-NL.xml b/TopMostFriend/Languages/nl-NL.xml new file mode 100644 index 0000000..c1fa96d --- /dev/null +++ b/TopMostFriend/Languages/nl-NL.xml @@ -0,0 +1,109 @@ + + + + nl-NL + Nederlands + Dutch + 1.6.0 + + + {0} is al gestart. + + &Verversen + &Instellingen + &Over + &Sluiten + + Nu altijd op voorgrond + Niet langer altijd op voorgrond + Venster heeft geen titel. + + Het was niet mogelijk om de altijd op voorgrond status van dit venster te veranderen. + Wil je het opnieuw proberen als Administrator? Een Gebruikersaccountbeheer dialoog zal verschijnen als je op Ja klikt. + {0} kon nog steeds niet de altijd op voorgrond status van dit venster veranderen. Het wordt mogelijk beschermd door Windows. + + Over {0} + Sluiten + Website + Doneer + + {0} Instellingen + Toepassen + Annuleren + OK + + Sneltoetsen + Herstel + Schakel altijd op voorgrond status op actief venster + + Opties + Toon notificatie als de sneltoets gebruikt is + Toon icoon van venster dat beïnvloed was door de sneltoets + Probeer altijd opnieuw als Administrator bij de eerste mislukking van het veranderen van de status + SHIFT+CLICK items in de systeemvak lijst om ze toe te voegen aan de titel blacklist + Herstel status naar voordat {0} ze heeft aangepast tijdens het sluiten + Geef lijst met open venster weer in het systeemvak menu + Start altijd als Administrator + + Taal (Language) + Je moet {0} opnieuw starten om de verandering van de taal volledig effect te laten nemen. Veel tekst zal nog steeds verschijnen in de voorheen geselecteerde taal. + + Overig + Beheer Blacklist... + Titel Blacklist + Start met Windows... + +Moet {0} tegelijkertijd met Windows starten? +Als je op Ja klikt zal een een snelkoppeling aangemaakt worden in de Opstarten map van het start menu, als je op Nee klikt wordt de snelkoppeling verwijderd. +Zorg ervoor dat je het EXE bestand voor {0} in een permanente locatie hebt voordat je op Ja klikt. + + Herstel alle instellingen... + +Dit zal alle instellingen verwijderen en het programma opnieuw opstarten. +Weet je het absoluut zeker? + + + Toevoegen + Bewerken + Verwijderen + Annuleren + Klaar + + Nieuw item toevoegen... + {0} bewerken... + Annuleren + Opslaan + + Volgende + Vorige + + Welkom bij {0}! + +{0} is een hulpprogramma dat jou controle geeft over de altijd-op-voorgrond status van programma's die je open hebt. + +Je zal nu door een aantal stappen genomen worden om {0} naar jouw wens te configureren! + + + Sneltoets selecteren + +Selecteer een globale sneltoets voor het wijzigen van de altijd op voorgrond status van het venster dat op dat moment in de voorgrond staat. +Als je geen sneltoets toe wil wijzen, druk op Herstel en vervolgens op Volgende. + + Toon een melding wanneer de sneltoets is gebruikt + + Administrator acties + +Soms kan {0} niet de altijd op voorgrond status van bepaalde vensters aanpassen omdat daar administratieve rechten voor nodig zijn. +{0} geeft je de volgende opties voor deze situatie: + + Vraag mij wat te doen wanneer het nodig is. + Vraag mij automatisch om administratieve rechten wanneer het nodig is. + Start {0} altijd als Administrator (niet aanbevolen). + + Bedankt! + Bedankt voor het gebruiken van {0}! + Controlleer de {0} [WEB]website[/WEB] en [CHANGELOG]changelog[/CHANGELOG] eens in de zoveel tijd om te kijken of er updates zijn. + Naast de opties die je al gekregen hebt, zijn er nog meer beschikbaar in het [SETTINGS]Instellingen[/SETTINGS] venster als je daarin geïnteresseerd bent. + Als je gekozen hebt om {0} altijd als Administrator te starten, zal je een Gebruikersaccountbeheer dialoog zien nadat je op volgende klikt. + + diff --git a/TopMostFriend/Locale.cs b/TopMostFriend/Locale.cs new file mode 100644 index 0000000..9c50742 --- /dev/null +++ b/TopMostFriend/Locale.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml; +using System.Xml.Serialization; +using TopMostFriend.Languages; + +namespace TopMostFriend { + public static class Locale { + public const string DEFAULT = @"en-GB"; + private static XmlSerializer Serializer { get; } + private static Dictionary Languages { get; } + private static Language ActiveLanguage { get; set; } + + static Locale() { + Serializer = new XmlSerializer(typeof(Language)); + Languages = new Dictionary(); + Assembly currentAsm = Assembly.GetExecutingAssembly(); + string[] resources = currentAsm.GetManifestResourceNames(); + + foreach(string resource in resources) + if(resource.StartsWith(@"TopMostFriend.Languages.") && resource.EndsWith(@".xml")) + using(Stream resourceStream = currentAsm.GetManifestResourceStream(resource)) + LoadLanguage(resourceStream); + } + + public static string LoadLanguage(Stream stream) { + Language lang = (Language)Serializer.Deserialize(stream); + foreach(LanguageString ls in lang.Strings) + ls.Value = ls.Value.Trim(); + + Languages.Add(lang.Info.Id, lang); + if(ActiveLanguage == null && DEFAULT.Equals(lang.Info.Id)) + ActiveLanguage = lang; + +#if DEBUG + Debug.WriteLine(@" =========="); + Debug.WriteLine(lang.Info); + foreach(LanguageString str in lang.Strings) + Debug.WriteLine(str); + Debug.WriteLine(string.Empty); +#endif + + return lang.Info.Id; + } + + public static LanguageInfo GetCurrentLanguage() { + return ActiveLanguage.Info; + } + + public static LanguageInfo[] GetAvailableLanguages() { + return Languages.Values.Select(l => l.Info).ToArray(); + } + + public static string GetPreferredLanguage() { + return Settings.Has(Program.LANGUAGE) + ? Settings.Get(Program.LANGUAGE, DEFAULT) + : CultureInfo.InstalledUICulture.Name; + } + + public static void SetLanguage(string langId) { + if(!Languages.ContainsKey(langId)) + langId = DEFAULT; + ActiveLanguage = Languages[langId]; + } + + public static void SetLanguage(LanguageInfo langInfo) { + SetLanguage(langInfo.Id); + } + + public static string String(string name, params object[] args) { + LanguageString str = ActiveLanguage.GetString(name); + if(str == null) + return name; + + List rargs = new List { Program.TITLE }; + rargs.AddRange(args); + + return str.Format(rargs.ToArray()); + } + + public static void Meow() { + Debug.WriteLine(@"meow"); + } + } +} diff --git a/TopMostFriend/Program.cs b/TopMostFriend/Program.cs index 996c64c..d9fbb36 100644 --- a/TopMostFriend/Program.cs +++ b/TopMostFriend/Program.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Drawing; using System.IO; using System.Linq; -using System.Security.Principal; using System.Text; using System.Threading; using System.Windows.Forms; @@ -17,6 +16,8 @@ namespace TopMostFriend { private static HotKeyWindow HotKeys; private static Icon OriginalIcon; + public const string TITLE = @"Top Most Friend"; + private const string GUID = #if DEBUG @"{1A22D9CA-2AA9-48F2-B007-3A48CF205CDD}"; @@ -25,6 +26,8 @@ namespace TopMostFriend { #endif private static readonly Mutex GlobalMutex = new Mutex(true, GUID); + private const string CUSTOM_LANGUAGE = @"TopMostFriendLanguage.xml"; + public const string FOREGROUND_HOTKEY_ATOM = @"{86795D64-770D-4BD6-AA26-FA638FBAABCF}"; #if DEBUG public const string FOREGROUND_HOTKEY_SETTING = @"ForegroundHotKey_DEBUG"; @@ -44,6 +47,10 @@ namespace TopMostFriend { public const string TITLE_BLACKLIST = @"TitleBlacklist"; public const string SHOW_HOTKEY_ICON = @"ShowHotkeyIcon"; public const string SHOW_WINDOW_LIST = @"ShowWindowList"; + public const string LAST_VERSION = @"LastVersion"; + public const string ALWAYS_RETRY_ELEVATED = @"AlwaysRetryElevated"; + public const string REVERT_ON_EXIT = @"RevertOnExit"; + public const string LANGUAGE = @"Language"; private static ToolStripItem RefreshButton; private static ToolStripItem LastSelectedItem = null; @@ -53,8 +60,39 @@ namespace TopMostFriend { private static ToolStripItem[] ListActionItems; private static ToolStripItem[] AppActionItems; + private static readonly Dictionary OriginalStates = new Dictionary(); + [STAThread] - public static void Main(string[] args) { + public static int Main(string[] args) { + Settings.Set(LAST_VERSION, Application.ProductVersion); + + IEnumerable cliToggleNew = args.Where(a => a.StartsWith(@"--toggle=")).Select(a => a.Substring(9)); + if(cliToggleNew.Any()) { + bool is32bit = IntPtr.Size == 4; + + foreach(string hwndStr in cliToggleNew) { + IntPtr hwnd; + if(is32bit) { + if(int.TryParse(hwndStr, out int hwnd32)) + hwnd = new IntPtr(hwnd32); + else + return 1; + } else { + if(long.TryParse(hwndStr, out long hwnd64)) + hwnd = new IntPtr(hwnd64); + else + return 1; + } + + // pass 0 to skip implicit FindOwnerId call + WindowInfo wi = new WindowInfo(hwnd, 0); + if(!wi.ToggleTopMost(!args.Contains($@"--background={hwndStr}"))) + return 2; + } + + return 0; + } + if(Environment.OSVersion.Version.Major >= 6) Win32.SetProcessDPIAware(); @@ -64,26 +102,53 @@ namespace TopMostFriend { if(args.Contains(@"--reset-admin")) Settings.Remove(ALWAYS_ADMIN_SETTING); - string cliToggle = args.FirstOrDefault(x => x.StartsWith(@"--hwnd=")); - if(!string.IsNullOrEmpty(cliToggle) && int.TryParse(cliToggle.Substring(7), out int cliToggleHWnd)) { - WindowInfo cliWindow = new WindowInfo(cliToggleHWnd); - if(!cliWindow.ToggleTopMost()) - TopMostFailed(cliWindow); + string cliToggleOld = args.FirstOrDefault(a => a.StartsWith(@"--hwnd="))?.Substring(7); + if(!string.IsNullOrEmpty(cliToggleOld)) { + IntPtr cliToggleHWnd = IntPtr.Zero; + if(IntPtr.Size == 4) { + if(int.TryParse(cliToggleOld, out int hwnd32)) + cliToggleHWnd = new IntPtr(hwnd32); + } else { + if(long.TryParse(cliToggleOld, out long hwnd64)) + cliToggleHWnd = new IntPtr(hwnd64); + } + + if(cliToggleHWnd != IntPtr.Zero) { + WindowInfo cliWindow = new WindowInfo(cliToggleHWnd); + if(!cliWindow.ToggleTopMost()) + TopMostFailed(cliWindow); + } } if(args.Contains(@"--stop")) - return; + return 0; + + if(File.Exists(CUSTOM_LANGUAGE)) { + string customLanguage; + using(Stream s = File.OpenRead(CUSTOM_LANGUAGE)) + try { + customLanguage = Locale.LoadLanguage(s); + } catch { + customLanguage = Locale.DEFAULT; + } + Locale.SetLanguage(customLanguage); + } else + Locale.SetLanguage(Locale.GetPreferredLanguage()); if(!GlobalMutex.WaitOne(0, true)) { - MessageBox.Show(@"An instance of Top Most Friend is already running.", @"Top Most Friend"); - return; + MessageBox.Show(Locale.String(@"AlreadyRunning"), TITLE, MessageBoxButtons.OK, MessageBoxIcon.Information); + return -1; } + bool isFirstRun = !Settings.Has(FOREGROUND_HOTKEY_SETTING); + Settings.SetDefault(FOREGROUND_HOTKEY_SETTING, 0); Settings.SetDefault(ALWAYS_ADMIN_SETTING, false); Settings.SetDefault(SHIFT_CLICK_BLACKLIST, true); Settings.SetDefault(SHOW_HOTKEY_ICON, true); Settings.SetDefault(SHOW_WINDOW_LIST, true); + Settings.SetDefault(ALWAYS_RETRY_ELEVATED, false); + Settings.SetDefault(REVERT_ON_EXIT, false); // Defaulting to false on Windows 10 because it uses the stupid, annoying and intrusive new Android style notification system // This would fucking piledrive the notification history and also just be annoying in general because intrusive Settings.SetDefault(TOGGLE_BALLOON_SETTING, ToggleBalloonDefault); @@ -102,9 +167,9 @@ namespace TopMostFriend { Settings.Set(TITLE_BLACKLIST, titles.ToArray()); } - if(Settings.Get(ALWAYS_ADMIN_SETTING) && !IsElevated()) { - Elevate(); - return; + if(Settings.Get(ALWAYS_ADMIN_SETTING) && !UAC.IsElevated) { + UAC.RestartElevated(); + return -2; } TitleBlacklist.Clear(); @@ -134,19 +199,19 @@ namespace TopMostFriend { CtxMenu.ItemClicked += CtxMenu_ItemClicked; ListActionItems = new ToolStripItem[] { new ToolStripSeparator(), - RefreshButton = new ToolStripMenuItem(@"&Refresh", Properties.Resources.arrow_refresh, new EventHandler((s, e) => RefreshWindowList())), + RefreshButton = new ToolStripMenuItem(Locale.String(@"TrayRefresh"), Properties.Resources.arrow_refresh, new EventHandler((s, e) => RefreshWindowList())), }; AppActionItems = new ToolStripItem[] { - new ToolStripMenuItem(@"&Settings", Properties.Resources.cog, new EventHandler((s, e) => SettingsWindow.Display())), - new ToolStripMenuItem(@"&About", Properties.Resources.help, new EventHandler((s, e) => AboutWindow.Display())), - new ToolStripMenuItem(@"&Quit", Properties.Resources.door_in, new EventHandler((s, e) => Application.Exit())), + new ToolStripMenuItem(Locale.String(@"TraySettings"), Properties.Resources.cog, new EventHandler((s, e) => SettingsWindow.Display())), + new ToolStripMenuItem(Locale.String(@"TrayAbout"), Properties.Resources.help, new EventHandler((s, e) => AboutWindow.Display())), + new ToolStripMenuItem(Locale.String(@"TrayQuit"), Properties.Resources.door_in, new EventHandler((s, e) => Application.Exit())), }; CtxMenu.Items.AddRange(AppActionItems); SysIcon = new NotifyIcon { Visible = true, Icon = OriginalIcon, - Text = @"Top Most Application Manager", + Text = TITLE, }; SysIcon.ContextMenuStrip = CtxMenu; SysIcon.MouseDown += SysIcon_MouseDown; @@ -154,9 +219,26 @@ namespace TopMostFriend { HotKeys = new HotKeyWindow(); SetForegroundHotKey(Settings.Get(FOREGROUND_HOTKEY_SETTING)); + if(isFirstRun) + FirstRunWindow.Display(); + Application.Run(); + if(Settings.Get(REVERT_ON_EXIT, false)) + RevertTopMostStatus(); + Shutdown(); + + return 0; + } + + public static void RevertTopMostStatus() { + foreach(KeyValuePair originalState in OriginalStates) { + WindowInfo wi = new WindowInfo(originalState.Key); + if(wi.IsTopMost != originalState.Value) + if(!wi.ToggleTopMost(false)) + TopMostFailed(wi, false); + } } public static void AddBlacklistedTitle(string title) { @@ -192,33 +274,6 @@ namespace TopMostFriend { GlobalMutex.ReleaseMutex(); } - private static bool? IsElevatedValue; - - public static bool IsElevated() { - if(!IsElevatedValue.HasValue) { - using(WindowsIdentity identity = WindowsIdentity.GetCurrent()) - IsElevatedValue = identity != null && new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); - } - - return IsElevatedValue.Value; - } - - public static void Elevate(string args = null) { - if(IsElevated()) - return; - - Shutdown(); - - Process.Start(new ProcessStartInfo { - UseShellExecute = true, - FileName = Application.ExecutablePath, - WorkingDirectory = Environment.CurrentDirectory, - Arguments = args ?? string.Empty, - Verb = @"runas", - }); - Application.Exit(); - } - public static void SetForegroundHotKey(int keyCode) { SetForegroundHotKey((Win32ModKeys)(keyCode & 0xFFFF), (Keys)((keyCode & 0xFFFF0000) >> 16)); } @@ -270,8 +325,13 @@ namespace TopMostFriend { if(Settings.Get(SHIFT_CLICK_BLACKLIST, true) && Control.ModifierKeys.HasFlag(Keys.Shift)) { AddBlacklistedTitle(title); SaveBlacklistedTitles(); - } else if(!window.ToggleTopMost()) - TopMostFailed(window); + } else { + if(window.ToggleTopMost()) { + if(!OriginalStates.ContainsKey(window.Handle)) + OriginalStates[window.Handle] = !window.IsTopMost; + } else + TopMostFailed(window); + } })) { CheckOnClick = true, Checked = window.IsTopMost, @@ -287,50 +347,29 @@ namespace TopMostFriend { CtxMenu.Items.AddRange(items.ToArray()); } - private static void TopMostFailed(WindowInfo window) { - MessageBoxButtons buttons = MessageBoxButtons.OK; - StringBuilder sb = new StringBuilder(); - sb.AppendLine(@"Wasn't able to change topmost status on this window."); + private static void TopMostFailed(WindowInfo window, bool switchWindow = true) { + bool retryElevated = Settings.Get(ALWAYS_RETRY_ELEVATED, false), + isElevated = UAC.IsElevated; - if(!IsElevated()) { - sb.AppendLine(@"Do you want to restart Top Most Friend as administrator and try again?"); - buttons = MessageBoxButtons.YesNo; + if(!retryElevated) { + MessageBoxButtons buttons = MessageBoxButtons.OK; + StringBuilder sb = new StringBuilder(); + sb.AppendLine(Locale.String(@"ErrUnableAlterStatus1")); + + if(!isElevated) { + sb.AppendLine(Locale.String(@"ErrUnableAlterStatus2")); + buttons = MessageBoxButtons.YesNo; + } + + retryElevated = MessageBox.Show(sb.ToString(), TITLE, buttons, MessageBoxIcon.Error) == DialogResult.Yes; } - DialogResult result = MessageBox.Show(sb.ToString(), @"Top Most Friend", buttons, MessageBoxIcon.Error); - - if(result == DialogResult.Yes) - Elevate($@"--hwnd={window.Handle}"); - } - - private class ActionTimeout { - private readonly Action Action; - private bool Continue = true; - private int Remaining = 0; - private const int STEP = 500; - - public ActionTimeout(Action action, int timeout) { - Action = action ?? throw new ArgumentNullException(nameof(action)); - if(timeout < 1) - throw new ArgumentException(@"Timeout must be a positive integer.", nameof(timeout)); - Remaining = timeout; - new Thread(ThreadBody) { IsBackground = true }.Start(); - } - - private void ThreadBody() { - do { - Thread.Sleep(STEP); - Remaining -= STEP; - - if(!Continue) - return; - } while(Remaining > 0); - - Action.Invoke(); - } - - public void Cancel() { - Continue = false; + if(retryElevated) { + if(window.ToggleTopMostElevated(switchWindow)) { + if(!OriginalStates.ContainsKey(window.Handle)) + OriginalStates[window.Handle] = !window.IsTopMost; + } else + MessageBox.Show(Locale.String(@"ErrUnableAlterStatusProtected"), TITLE, MessageBoxButtons.OK, MessageBoxIcon.Error); } } @@ -343,8 +382,8 @@ namespace TopMostFriend { if(Settings.Get(TOGGLE_BALLOON_SETTING, false)) { string title = window.Title; SysIcon?.ShowBalloonTip( - 2000, window.IsTopMost ? @"Always on top" : @"No longer always on top", - string.IsNullOrEmpty(title) ? @"Window has no title." : title, + 2000, Locale.String(window.IsTopMost ? @"NotifyOnTop" : @"NotifyNoLonger"), + string.IsNullOrEmpty(title) ? Locale.String(@"NotifyNoTitle") : title, ToolTipIcon.Info ); } diff --git a/TopMostFriend/Properties/AssemblyInfo.cs b/TopMostFriend/Properties/AssemblyInfo.cs index a2c6bb1..e19f006 100644 --- a/TopMostFriend/Properties/AssemblyInfo.cs +++ b/TopMostFriend/Properties/AssemblyInfo.cs @@ -29,5 +29,5 @@ using System.Runtime.InteropServices; // Build Number // Revision // -[assembly: AssemblyVersion("1.5.1.0")] -[assembly: AssemblyFileVersion("1.5.1.0")] +[assembly: AssemblyVersion("1.6.0.0")] +[assembly: AssemblyFileVersion("1.6.0.0")] diff --git a/TopMostFriend/Properties/Resources.Designer.cs b/TopMostFriend/Properties/Resources.Designer.cs index fb8717c..ab9867c 100644 --- a/TopMostFriend/Properties/Resources.Designer.cs +++ b/TopMostFriend/Properties/Resources.Designer.cs @@ -100,6 +100,16 @@ namespace TopMostFriend.Properties { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap firstrun { + get { + object obj = ResourceManager.GetObject("firstrun", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/TopMostFriend/Properties/Resources.resx b/TopMostFriend/Properties/Resources.resx index 5aecbd4..6ba3e5a 100644 --- a/TopMostFriend/Properties/Resources.resx +++ b/TopMostFriend/Properties/Resources.resx @@ -130,6 +130,9 @@ ..\Resources\door_in.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\firstrun.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\help.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/TopMostFriend/Resources/firstrun.png b/TopMostFriend/Resources/firstrun.png new file mode 100644 index 0000000..f83fdef Binary files /dev/null and b/TopMostFriend/Resources/firstrun.png differ diff --git a/TopMostFriend/Settings.cs b/TopMostFriend/Settings.cs index 2921408..b1ab304 100644 --- a/TopMostFriend/Settings.cs +++ b/TopMostFriend/Settings.cs @@ -26,9 +26,7 @@ namespace TopMostFriend { } public static string[] Get(string name, string[] fallback = null) { - byte[] buffer = GetRoot().GetValue(name, null) as byte[]; - - if (buffer == null) + if(!(GetRoot().GetValue(name, null) is byte[] buffer)) return fallback; List strings = new List(); diff --git a/TopMostFriend/SettingsWindow.cs b/TopMostFriend/SettingsWindow.cs index 600cf92..062c42d 100644 --- a/TopMostFriend/SettingsWindow.cs +++ b/TopMostFriend/SettingsWindow.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics; using System.Drawing; +using System.IO; using System.Windows.Forms; +using TopMostFriend.Languages; namespace TopMostFriend { public class SettingsWindow : Form { @@ -29,184 +32,172 @@ namespace TopMostFriend { public readonly CheckBox FlShiftClickBlacklist; public readonly CheckBox FlShowHotkeyIcon; public readonly CheckBox FlShowWindowList; + public readonly CheckBox FlAlwaysRetryAsAdmin; + public readonly CheckBox FlRevertOnExit; + + public readonly ComboBox LangSelect; public SettingsWindow() { - Text = @"Top Most Friend Settings"; + Text = Locale.String(@"SettingsTitle"); Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); StartPosition = FormStartPosition.CenterScreen; FormBorderStyle = FormBorderStyle.FixedSingle; AutoScaleMode = AutoScaleMode.Dpi; - ClientSize = new Size(430, 298); + ClientSize = new Size(430, 407); MinimizeBox = MaximizeBox = false; - MinimumSize = MaximumSize = Size; + TopMost = true; KeyCode = Settings.Get(Program.FOREGROUND_HOTKEY_SETTING, 0); Button applyButton = new Button { - Text = @"Apply", + Text = Locale.String(@"SettingsApply"), Size = new Size(75, 23), Location = new Point(ClientSize.Width - 81, ClientSize.Height - 30), TabIndex = 10003, + Anchor = AnchorStyles.Bottom | AnchorStyles.Right, }; applyButton.Click += ApplyButton_Click; Button cancelButton = new Button { - Text = @"Cancel", + Text = Locale.String(@"SettingsCancel"), Size = applyButton.Size, Location = new Point(ClientSize.Width - 162, applyButton.Location.Y), TabIndex = 10002, + Anchor = applyButton.Anchor, }; cancelButton.Click += CancelButton_Click; Button okButton = new Button { - Text = @"OK", + Text = Locale.String(@"SettingsOk"), Size = applyButton.Size, Location = new Point(ClientSize.Width - 243, applyButton.Location.Y), TabIndex = 10001, + Anchor = applyButton.Anchor, }; okButton.Click += OkButton_Click; GroupBox hotKeyGroup = new GroupBox { - Text = @"Hotkeys", + Text = Locale.String(@"SettingsHotKeysTitle"), Location = new Point(6, 6), Size = new Size(Width - 18, 70), + Anchor = AnchorStyles.Left | AnchorStyles.Right, }; - GroupBox flagsGroup = new GroupBox { - Text = @"Flags", - Location = new Point(6, 76), - Size = new Size(Width - 18, 130), + Text = Locale.String(@"SettingsOptionsTitle"), + Location = new Point(6, 80), + Size = new Size(Width - 18, 170), + Anchor = hotKeyGroup.Anchor, }; - - GroupBox blackListGroup = new GroupBox { - Text = @"Blacklist", - Location = new Point(6, 206), + GroupBox langGroup = new GroupBox { + Text = Locale.String(@"SettingsLanguageTitle"), + Location = new Point(6, 254), Size = new Size(Width - 18, 55), + Anchor = hotKeyGroup.Anchor, + }; + GroupBox otherGroup = new GroupBox { + Text = Locale.String(@"SettingsOtherTitle"), + Location = new Point(6, 313), + Size = new Size(Width - 18, 55), + Anchor = hotKeyGroup.Anchor, }; Controls.AddRange(new Control[] { applyButton, cancelButton, okButton, - hotKeyGroup, flagsGroup, blackListGroup, + hotKeyGroup, flagsGroup, langGroup, otherGroup, }); - Label toggleForegroundLabel = new Label { + hotKeyGroup.Controls.Add(new Label { AutoSize = true, - Text = @"Toggle always on top status on active window", + Text = Locale.String(@"SettingsHotKeysToggle"), Location = new Point(8, 17), - }; - - const int mod_x = 120; - const int mod_y = 34; - - Button fgReset = new Button { - Text = @"Reset", - Location = new Point(hotKeyGroup.Width - 85, mod_y), - TabIndex = 105, - }; - fgReset.Click += FgReset_Click; - - FgKey = new TextBox { - Text = ((Keys)(KeyCode >> 16)).ToString(), - Location = new Point(12, mod_y + 2), - TabIndex = 101, - }; - FgKey.KeyDown += FgKey_KeyDown; - - FgModCtrl = new CheckBox { - Text = @"CTRL", - Location = new Point(mod_x, mod_y), - Checked = (KeyCode & (int)Win32ModKeys.MOD_CONTROL) > 0, - Appearance = Appearance.Button, - Size = new Size(50, 23), - TextAlign = ContentAlignment.MiddleCenter, - TabIndex = 102, - }; - FgModCtrl.Click += FgModCtrl_Click; - - FgModAlt = new CheckBox { - Text = @"ALT", - Location = new Point(mod_x + 50, mod_y), - Checked = (KeyCode & (int)Win32ModKeys.MOD_ALT) > 0, - Appearance = FgModCtrl.Appearance, - Size = FgModCtrl.Size, - TextAlign = FgModCtrl.TextAlign, - TabIndex = 103, - }; - FgModAlt.Click += FgModAlt_Click; - - FgModShift = new CheckBox { - Text = @"SHIFT", - Location = new Point(mod_x + 100, mod_y), - Checked = (KeyCode & (int)Win32ModKeys.MOD_SHIFT) > 0, - Appearance = FgModCtrl.Appearance, - Size = FgModCtrl.Size, - TextAlign = FgModCtrl.TextAlign, - TabIndex = 104, - }; - FgModShift.Click += FgModShift_Click; - - FgModWindows = new CheckBox { - Text = @"WIN", - Location = new Point(mod_x + 150, mod_y), - Checked = (KeyCode & (int)Win32ModKeys.MOD_WIN) > 0, - Appearance = FgModCtrl.Appearance, - Size = FgModCtrl.Size, - TextAlign = FgModCtrl.TextAlign, - TabIndex = 105, - }; - FgModWindows.Click += FgModWindows_Click; - - hotKeyGroup.Controls.AddRange(new Control[] { - toggleForegroundLabel, FgModCtrl, FgModAlt, FgModShift, FgModWindows, fgReset, FgKey, }); - FlAlwaysAdmin = new CheckBox { - Text = @"Always run as administrator", + CreateHotKeyInput( + hotKeyGroup, + () => KeyCode, + keyCode => KeyCode = keyCode + ); + + FlToggleNotification = new CheckBox { + Text = Locale.String(@"SettingsOptionsToggleNotify"), Location = new Point(10, 20), - Checked = Settings.Get(Program.ALWAYS_ADMIN_SETTING, false), + Checked = Settings.Get(Program.TOGGLE_BALLOON_SETTING, Program.ToggleBalloonDefault), AutoSize = true, TabIndex = 201, }; - FlToggleNotification = new CheckBox { - Text = @"Show notification when using toggle hotkey", + FlShowHotkeyIcon = new CheckBox { + Text = Locale.String(@"SettingsOptionsToggleNotifyIcon"), Location = new Point(10, 40), - Checked = Settings.Get(Program.TOGGLE_BALLOON_SETTING, Program.ToggleBalloonDefault), + Checked = Settings.Get(Program.SHOW_HOTKEY_ICON, true), AutoSize = true, TabIndex = 202, }; - FlShiftClickBlacklist = new CheckBox { - Text = @"SHIFT+CLICK items in the list to add to the title blacklist", + FlAlwaysRetryAsAdmin = new CheckBox { + Text = Locale.String(@"SettingsOptionsElevatedRetry"), Location = new Point(10, 60), - Checked = Settings.Get(Program.SHIFT_CLICK_BLACKLIST, true), + Checked = Settings.Get(Program.ALWAYS_RETRY_ELEVATED, false), AutoSize = true, TabIndex = 203, }; - FlShowHotkeyIcon = new CheckBox { - Text = @"Show icon of window affected by hotkey", + FlShiftClickBlacklist = new CheckBox { + Text = Locale.String(@"SettingsOptionsShiftBlacklist"), Location = new Point(10, 80), - Checked = Settings.Get(Program.SHOW_HOTKEY_ICON, true), + Checked = Settings.Get(Program.SHIFT_CLICK_BLACKLIST, true), AutoSize = true, TabIndex = 204, }; - FlShowWindowList = new CheckBox { - Text = @"Show list of open windows in the task bar context menu", + FlRevertOnExit = new CheckBox { + Text = Locale.String(@"SettingsOptionsRevertOnExit"), Location = new Point(10, 100), - Checked = Settings.Get(Program.SHOW_WINDOW_LIST, true), + Checked = Settings.Get(Program.REVERT_ON_EXIT, false), AutoSize = true, TabIndex = 205, }; + FlShowWindowList = new CheckBox { + Text = Locale.String(@"SettingsOptionsShowTrayList"), + Location = new Point(10, 120), + Checked = Settings.Get(Program.SHOW_WINDOW_LIST, true), + AutoSize = true, + TabIndex = 206, + }; + FlAlwaysAdmin = new CheckBox { + Text = Locale.String(@"SettingsOptionsAlwaysAdmin"), + Location = new Point(10, 140), + Checked = Settings.Get(Program.ALWAYS_ADMIN_SETTING, false), + AutoSize = true, + TabIndex = 207, + }; - flagsGroup.Controls.AddRange(new[] { + CheckBox[] options = new[] { FlAlwaysAdmin, FlToggleNotification, FlShiftClickBlacklist, - FlShowHotkeyIcon, FlShowWindowList, - }); + FlShowHotkeyIcon, FlShowWindowList, FlAlwaysRetryAsAdmin, + FlRevertOnExit, + }; + + flagsGroup.Controls.AddRange(options); + + foreach(CheckBox option in options) + if((option.Width + (option.Left * 2)) > flagsGroup.ClientSize.Width) + ClientSize = new Size(option.Width + 30, ClientSize.Height); + + LangSelect = new ComboBox { + Anchor = AnchorStyles.Left | AnchorStyles.Right, + DropDownStyle = ComboBoxStyle.DropDownList, + Size = new Size(langGroup.Width - 24, 21), + Location = new Point(12, 22), + }; + LangSelect.Items.AddRange(Locale.GetAvailableLanguages()); + LangSelect.SelectedItem = Locale.GetCurrentLanguage(); + langGroup.Controls.Add(LangSelect); Button titleBlacklist = new Button { Size = new Size(120, 23), Location = new Point(10, 20), - Text = @"Manage...", + Text = Locale.String(@"SettingsOtherBlacklistButton"), TabIndex = 301, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowOnly, }; titleBlacklist.Click += (s, e) => { - string[] newList = BlacklistWindow.Display(@"Title Blacklist", Program.GetBlacklistedTitles()); + string[] newList = BlacklistWindow.Display(Locale.String(@"SettingsOtherBlacklistWindowTitle"), Program.GetBlacklistedTitles()); if(newList != null) { Program.ApplyBlacklistedTitles(newList); @@ -214,22 +205,207 @@ namespace TopMostFriend { } }; - blackListGroup.Controls.AddRange(new[] { titleBlacklist }); + Button startWithWindows = new Button { + Size = new Size(120, 23), + Location = new Point(134, 20), + Text = Locale.String(@"SettingsOtherStartupButton"), + TabIndex = 302, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowOnly, + }; + startWithWindows.Click += (s, e) => { + DialogResult dr = MessageBox.Show(Locale.String(@"SettingsOtherStartupConfirm"), Program.TITLE, MessageBoxButtons.YesNo, MessageBoxIcon.Question); + + string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), @"TopMostFriend.lnk"); + + if(File.Exists(path)) + File.Delete(path); + + if(dr == DialogResult.Yes) { + IWshRuntimeLibrary.WshShell shell = new IWshRuntimeLibrary.WshShell(); + IWshRuntimeLibrary.IWshShortcut shortcut = (IWshRuntimeLibrary.IWshShortcut)shell.CreateShortcut(path); + shortcut.TargetPath = Application.ExecutablePath; + shortcut.Save(); + } + }; + + Button resetSettings = new Button { + Size = new Size(120, 23), + Location = new Point(258, 20), + Text = Locale.String(@"SettingsOtherResetButton"), + TabIndex = 303, + AutoSize = true, + AutoSizeMode = AutoSizeMode.GrowOnly, + }; + resetSettings.Click += (s, e) => { + DialogResult dr = MessageBox.Show(Locale.String(@"SettingsOtherResetConfirm"), Program.TITLE, MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation); + + if(dr == DialogResult.Yes) { + Settings.Remove(Program.FOREGROUND_HOTKEY_SETTING); + Settings.Remove(Program.PROCESS_SEPARATOR_SETTING); + Settings.Remove(Program.LIST_SELF_SETTING); + Settings.Remove(Program.LIST_BACKGROUND_PATH_SETTING); + Settings.Remove(Program.LIST_BACKGROUND_LAYOUT_SETTING); + Settings.Remove(Program.ALWAYS_ADMIN_SETTING); + Settings.Remove(Program.TOGGLE_BALLOON_SETTING); + Settings.Remove(Program.SHIFT_CLICK_BLACKLIST); + Settings.Remove(Program.TITLE_BLACKLIST); + Settings.Remove(Program.SHOW_HOTKEY_ICON); + Settings.Remove(Program.SHOW_WINDOW_LIST); + Settings.Remove(Program.LAST_VERSION); + Settings.Remove(Program.ALWAYS_RETRY_ELEVATED); + Settings.Remove(Program.REVERT_ON_EXIT); + Program.Shutdown(); + Process.Start(Application.ExecutablePath); + Application.Exit(); + } + }; + + Button[] otherButtons = new[] { titleBlacklist, startWithWindows, resetSettings }; + + otherGroup.Controls.AddRange(otherButtons); } - private void FgReset_Click(object sender, EventArgs e) { - FgModCtrl.Checked = FgModAlt.Checked = FgModShift.Checked = false; - FgKey.Text = string.Empty; - KeyCode = 0; + public static void CreateHotKeyInput( + Control target, + Func getKeyCode, + Action setKeyCode, + int offsetX = 0, + int offsetY = 0 + ) { + int modX = 120 + offsetX; + int modY = 34 + offsetY; + int keyCode = getKeyCode(); + + Button fgReset = new Button { + Text = Locale.String(@"SettingsHotKeysReset"), + Location = new Point(target.Width - 85 + offsetX, modY), + TabIndex = 105, + }; + + TextBox fgKey = new TextBox { + Text = ((Keys)(keyCode >> 16)).ToString(), + Location = new Point(12 + offsetX, modY + 2), + TabIndex = 101, + }; + + CheckBox fgModCtrl = new CheckBox { + Text = @"CTRL", + Location = new Point(modX, modY), + Checked = (keyCode & (int)Win32ModKeys.MOD_CONTROL) > 0, + Appearance = Appearance.Button, + Size = new Size(50, 23), + TextAlign = ContentAlignment.MiddleCenter, + TabIndex = 102, + }; + + CheckBox fgModWindows = new CheckBox { + Text = @"WIN", + Location = new Point(modX + 50, modY), + Checked = (keyCode & (int)Win32ModKeys.MOD_WIN) > 0, + Appearance = fgModCtrl.Appearance, + Size = fgModCtrl.Size, + TextAlign = fgModCtrl.TextAlign, + TabIndex = 103, + }; + + CheckBox fgModAlt = new CheckBox { + Text = @"ALT", + Location = new Point(modX + 100, modY), + Checked = (keyCode & (int)Win32ModKeys.MOD_ALT) > 0, + Appearance = fgModCtrl.Appearance, + Size = fgModCtrl.Size, + TextAlign = fgModCtrl.TextAlign, + TabIndex = 104, + }; + + CheckBox fgModShift = new CheckBox { + Text = @"SHIFT", + Location = new Point(modX + 150, modY), + Checked = (keyCode & (int)Win32ModKeys.MOD_SHIFT) > 0, + Appearance = fgModCtrl.Appearance, + Size = fgModCtrl.Size, + TextAlign = fgModCtrl.TextAlign, + TabIndex = 105, + }; + + fgReset.Click += (s, e) => { + fgModCtrl.Checked = fgModAlt.Checked = fgModShift.Checked = false; + fgKey.Text = ((Keys)0).ToString(); + setKeyCode(0); + }; + fgModCtrl.Click += (s, e) => { + if(s is CheckBox cb) { + if(cb.Checked) + keyCode |= (int)Win32ModKeys.MOD_CONTROL; + else + keyCode &= ~(int)Win32ModKeys.MOD_CONTROL; + setKeyCode(keyCode); + } + }; + fgModAlt.Click += (s, e) => { + if(s is CheckBox cb) { + if(cb.Checked) + keyCode |= (int)Win32ModKeys.MOD_ALT; + else + keyCode &= ~(int)Win32ModKeys.MOD_ALT; + setKeyCode(keyCode); + } + }; + fgModShift.Click += (s, e) => { + if(s is CheckBox cb) { + if(cb.Checked) + keyCode |= (int)Win32ModKeys.MOD_SHIFT; + else + keyCode &= ~(int)Win32ModKeys.MOD_SHIFT; + setKeyCode(keyCode); + } + }; + fgModWindows.Click += (s, e) => { + if(s is CheckBox cb) { + if(cb.Checked) + keyCode |= (int)Win32ModKeys.MOD_WIN; + else + keyCode &= ~(int)Win32ModKeys.MOD_WIN; + setKeyCode(keyCode); + } + }; + fgKey.KeyDown += (s, e) => { + if(!(s is TextBox textBox)) + return; + e.Handled = e.SuppressKeyPress = true; + textBox.Text = e.KeyCode.ToString(); + keyCode &= 0xFFFF; + keyCode |= (int)e.KeyCode << 16; + setKeyCode(keyCode); + }; + + target.Controls.AddRange(new Control[] { + fgModCtrl, fgModAlt, fgModShift, fgModWindows, fgReset, fgKey, + }); } public void Apply() { + if(LangSelect.SelectedItem is LanguageInfo li) { + if(li != Locale.GetCurrentLanguage()) { + if(Locale.GetPreferredLanguage() == li.Id) + Settings.Remove(Program.LANGUAGE); + else + Settings.Set(Program.LANGUAGE, li.Id); + + Locale.SetLanguage(li); + MessageBox.Show(Locale.String(@"SettingsLanguageChangedInfo"), Program.TITLE, MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + Settings.Set(Program.FOREGROUND_HOTKEY_SETTING, KeyCode); Settings.Set(Program.ALWAYS_ADMIN_SETTING, FlAlwaysAdmin.Checked); Settings.Set(Program.TOGGLE_BALLOON_SETTING, FlToggleNotification.Checked); Settings.Set(Program.SHIFT_CLICK_BLACKLIST, FlShiftClickBlacklist.Checked); Settings.Set(Program.SHOW_HOTKEY_ICON, FlShowHotkeyIcon.Checked); Settings.Set(Program.SHOW_WINDOW_LIST, FlShowWindowList.Checked); + Settings.Set(Program.ALWAYS_RETRY_ELEVATED, FlAlwaysRetryAsAdmin.Checked); + Settings.Set(Program.REVERT_ON_EXIT, FlRevertOnExit.Checked); Program.SetForegroundHotKey(KeyCode); } @@ -244,49 +420,6 @@ namespace TopMostFriend { Apply(); } - private void FgModCtrl_Click(object sender, EventArgs e) { - if(sender is CheckBox cb) { - if (cb.Checked) - KeyCode |= (int)Win32ModKeys.MOD_CONTROL; - else - KeyCode &= ~(int)Win32ModKeys.MOD_CONTROL; - } - } - private void FgModAlt_Click(object sender, EventArgs e) { - if (sender is CheckBox cb) { - if (cb.Checked) - KeyCode |= (int)Win32ModKeys.MOD_ALT; - else - KeyCode &= ~(int)Win32ModKeys.MOD_ALT; - } - } - private void FgModShift_Click(object sender, EventArgs e) { - if (sender is CheckBox cb) { - if (cb.Checked) - KeyCode |= (int)Win32ModKeys.MOD_SHIFT; - else - KeyCode &= ~(int)Win32ModKeys.MOD_SHIFT; - } - } - - private void FgModWindows_Click(object sender, EventArgs e) { - if(sender is CheckBox cb) { - if(cb.Checked) - KeyCode |= (int)Win32ModKeys.MOD_WIN; - else - KeyCode &= ~(int)Win32ModKeys.MOD_WIN; - } - } - - private void FgKey_KeyDown(object sender, KeyEventArgs e) { - if (!(sender is TextBox textBox)) - return; - e.Handled = e.SuppressKeyPress = true; - textBox.Text = e.KeyCode.ToString(); - KeyCode &= 0xFFFF; - KeyCode |= (int)e.KeyCode << 16; - } - protected override void OnFormClosed(FormClosedEventArgs e) { base.OnFormClosed(e); Instance.Dispose(); diff --git a/TopMostFriend/TopMostFriend.csproj b/TopMostFriend/TopMostFriend.csproj index 3201bbf..aeb5065 100644 --- a/TopMostFriend/TopMostFriend.csproj +++ b/TopMostFriend/TopMostFriend.csproj @@ -44,12 +44,20 @@ Form + Form + + Form + Form + + + + @@ -61,10 +69,12 @@ Form + + @@ -80,6 +90,9 @@ + + + @@ -88,5 +101,16 @@ Resources.Designer.cs + + + {F935DC20-1CF0-11D0-ADB9-00C04FD58A0B} + 1 + 0 + 0 + tlbimp + False + True + + \ No newline at end of file diff --git a/TopMostFriend/UAC.cs b/TopMostFriend/UAC.cs new file mode 100644 index 0000000..9c98a55 --- /dev/null +++ b/TopMostFriend/UAC.cs @@ -0,0 +1,81 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Security.Principal; +using System.Windows.Forms; + +namespace TopMostFriend { + public static class UAC { + private static bool? IsElevatedValue; + private static string ExecutablePathValue; + + static UAC() { + ExecutablePath = null; + } + + public static bool IsElevated { + get { + if(!IsElevatedValue.HasValue) + using(WindowsIdentity identity = WindowsIdentity.GetCurrent()) + IsElevatedValue = identity != null && new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator); + + return IsElevatedValue.Value; + } + } + + public static string ExecutablePath { + get => ExecutablePathValue; + set { + ExecutablePathValue = string.IsNullOrWhiteSpace(value) || !File.Exists(value) + ? Assembly.GetEntryAssembly().Location + : value; + } + } + + public static int RunElevatedTask(string args) { + if(string.IsNullOrWhiteSpace(args)) + throw new ArgumentException(@"No arguments provided.", nameof(args)); + + try { + Process process = Process.Start(new ProcessStartInfo { + UseShellExecute = true, + FileName = ExecutablePath, + WorkingDirectory = Environment.CurrentDirectory, + Arguments = args, + Verb = @"runas", + }); + + process.WaitForExit(); + + return process.ExitCode; + } catch(Win32Exception ex) { + return ex.ErrorCode; + } + } + + public static int ToggleWindowTopMost(WindowInfo window, bool switchWindow) + => ToggleWindowTopMost(window.Handle, switchWindow); + + public static int ToggleWindowTopMost(IntPtr handle, bool switchWindow) + => RunElevatedTask(switchWindow ? $@"--toggle={handle}" : $@"--toggle={handle} --background={handle}"); + + public static void RestartElevated() { + if(IsElevated) + return; + + Program.Shutdown(); + + Process.Start(new ProcessStartInfo { + UseShellExecute = true, + FileName = ExecutablePath, + WorkingDirectory = Environment.CurrentDirectory, + Arguments = string.Join(@" ", Environment.GetCommandLineArgs()), + Verb = @"runas", + }); + + Application.Exit(); + } + } +} diff --git a/TopMostFriend/Win32.cs b/TopMostFriend/Win32.cs index 3cb94f0..fb516ad 100644 --- a/TopMostFriend/Win32.cs +++ b/TopMostFriend/Win32.cs @@ -52,12 +52,10 @@ namespace TopMostFriend { public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow); [DllImport(@"user32", SetLastError = true)] - public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId); public static IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex) { - if (IntPtr.Size == 8) - return GetWindowLongPtr64(hWnd, nIndex); - return new IntPtr(GetWindowLong32(hWnd, nIndex)); + return IntPtr.Size == 8 ? GetWindowLongPtr64(hWnd, nIndex) : new IntPtr(GetWindowLong32(hWnd, nIndex)); } [DllImport(@"user32", EntryPoint = "GetWindowLong")] @@ -84,9 +82,7 @@ namespace TopMostFriend { } public static IntPtr GetClassLongPtr(IntPtr hWnd, int nIndex) { - if (IntPtr.Size > 4) - return GetClassLongPtr64(hWnd, nIndex); - return new IntPtr(GetClassLongPtr32(hWnd, nIndex)); + return IntPtr.Size > 4 ? GetClassLongPtr64(hWnd, nIndex) : new IntPtr(GetClassLongPtr32(hWnd, nIndex)); } [DllImport(@"user32", EntryPoint = "GetClassLong")] diff --git a/TopMostFriend/WindowInfo.cs b/TopMostFriend/WindowInfo.cs index 8d86c6c..0e01873 100644 --- a/TopMostFriend/WindowInfo.cs +++ b/TopMostFriend/WindowInfo.cs @@ -3,18 +3,18 @@ using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Linq; -using System.Text; namespace TopMostFriend { public class WindowInfo { public IntPtr Handle { get; } - public Process Owner { get; } + public int ProcessId { get; } + + public Process Owner => Process.GetProcessById(ProcessId); public string Title => Win32.GetWindowTextString(Handle); - public long Flags => Win32.GetWindowLongPtr(Handle, Win32.GWL_EXSTYLE).ToInt32(); public bool IsTopMost { - get => (Flags & Win32.WS_EX_TOPMOST) > 0; + get => (Win32.GetWindowLongPtr(Handle, Win32.GWL_EXSTYLE).ToInt32() & Win32.WS_EX_TOPMOST) > 0; set { Win32.SetWindowPos( Handle, new IntPtr(value ? Win32.HWND_TOPMOST : Win32.HWND_NOTOPMOST), @@ -52,33 +52,34 @@ namespace TopMostFriend { public Image IconBitmap => Icon?.ToBitmap(); - public WindowInfo(int handle) - : this(new IntPtr(handle)) {} - public WindowInfo(IntPtr handle) - : this(handle, FindOwner(handle)) {} + : this(handle, FindOwnerId(handle)) { } - public WindowInfo(IntPtr handle, Process owner) { + public WindowInfo(IntPtr handle, int processId) { Handle = handle; - Owner = owner ?? throw new ArgumentNullException(nameof(owner)); + ProcessId = processId; } public void SwitchTo() { Win32.SwitchToThisWindow(Handle, false); } - public bool ToggleTopMost() { + public bool ToggleTopMost(bool switchWindow = true) { bool expected = !IsTopMost; IsTopMost = expected; bool success = IsTopMost == expected; - if(expected && success) + if(switchWindow && expected && success) SwitchTo(); return success; } - public static Process FindOwner(IntPtr hWnd) { - Win32.GetWindowThreadProcessId(hWnd, out uint procId); - return Process.GetProcessById((int)procId); + public bool ToggleTopMostElevated(bool switchWindow = true) { + return UAC.ToggleWindowTopMost(this, switchWindow) == 0; + } + + public static int FindOwnerId(IntPtr hWnd) { + Win32.GetWindowThreadProcessId(hWnd, out int procId); + return procId; } public static WindowInfo GetForegroundWindow() { diff --git a/firstrun.psd b/firstrun.psd new file mode 100644 index 0000000..e8089b9 Binary files /dev/null and b/firstrun.psd differ