add about window (#52)

Co-authored-by: N3xoX1 <n3xox1@nxemu.com>
This commit is contained in:
N3xoX1 2026-06-02 14:41:32 +09:30 committed by GitHub
parent d2f63e1308
commit 77ab6d7612
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 755 additions and 438 deletions

2
external/sciterui vendored

@ -1 +1 @@
Subproject commit 1dc05d97e7b624548f29d8c4db7ea5433eef8a25
Subproject commit 008571130310f2159286560205b2e31dea022846

View File

@ -0,0 +1,157 @@
html
{
margin: 0;
padding: 0;
overflow: hidden;
height: max-content;
}
body
{
flow: vertical;
width: 100%%;
height: max-content;
}
.about-body
{
flow: vertical;
width: 100%%;
padding: 1em 1.25em 0.25em 1.25em;
}
body>.footer
{
padding: 0.35em 1.25em 0.5em 1.25em;
}
.about-row-top
{
flow: horizontal;
vertical-align: middle;
width: 100%%;
margin-bottom: 0.85em;
}
.about-logo-wrap
{
width: 40%%;
margin-right: 1em;
horizontal-align: center;
}
.about-logo
{
display: block;
width: 100%%;
height: width(100%%);
margin: 0 auto;
image-rendering: auto;
}
.about-product
{
width: *;
flow: vertical;
text-align: center;
horizontal-align: center;
vertical-align: middle;
}
.about-name
{
font-size: 1.5rem;
font-weight: 600;
margin-top: *;
margin-bottom: 0.2em;
color: var(--color-ui-text-primary);
}
.about-version,
.about-copyright
{
color: var(--color-ui-text-muted);
}
.about-version
{
margin-bottom: 0.15em;
}
.about-copyright
{
font-size: 0.85rem;
margin-bottom: *;
}
.about-row-content
{
width: 100%%;
}
.about-description
{
line-height: 1.45;
margin-bottom: 0.85em;
color: var(--color-ui-text-primary);
}
.about-description p
{
margin: 0 0 0.65em 0;
}
.about-disclaimer
{
color: var(--color-ui-text-muted);
font-size: 0.9rem;
}
.about-credits
{
margin-bottom: 0.85em;
line-height: 1.45;
}
.about-credits-title
{
color: var(--color-ui-text-primary);
font-weight: 600;
margin-bottom: 0.25em;
}
.about-credits-list
{
color: var(--color-ui-text-muted);
font-size: 0.9rem;
}
.about-links
{
margin-bottom: 0.75em;
line-height: 1.5;
}
.about-link-sep
{
color: var(--color-ui-text-muted);
}
a.about-link
{
color: #3cafe9;
text-decoration: underline;
cursor: pointer;
}
a.about-link:hover
{
color: #7ec8f7;
}
.about-trademark
{
color: var(--color-ui-text-muted);
font-size: 0.75rem;
line-height: 1.35;
}

View File

@ -139,274 +139,3 @@ html > header > window-buttons > window-button[role="window-minimize"]
{
foreground-image: url(icon:window-minimize);
}
ul#menu-bar
{
margin: 0;
padding: 0 0.5em 0.3em 0.5em;
background: var(--color-menubar-background);
color: var(--color-ui-text-primary);
font-family: var(--font-ui);
font-size: 0.9em;
font-weight: 100;
behavior:menu-bar;
flow: horizontal;
}
ul#menu-bar > li
{
padding: 0.28em 0.55em;
margin: 0;
white-space: nowrap;
width: max-content;
display: block;
flow: horizontal;
border-radius: 5px;
}
ul#menu-bar > li:hover
{
background: var(--color-menubar-background-hover);
}
ul#menu-bar:focus > li:current,
ul#menu-bar > li:owns-popup {
background-color: var(--color-menubar-background-active);
border-radius: 5px 5px 0;
}
ul#menu-bar caption u,
menu li .menu-item-label u
{
text-decoration: none;
}
[data-menu-mnemonics="1"] ul#menu-bar caption u,
[data-menu-mnemonics="1"] menu li .menu-item-label u,
ul#menu-bar > li:owns-popup caption u,
menu[window-state="shown"] li .menu-item-label u,
menu[window-state="shown"] caption u
{
text-decoration: underline;
}
menu
{
behavior:menu;
flow: vertical;
display:none;
margin:0;
padding: 0.35em 0;
min-width: 14em;
max-width:max-content;
background: var(--color-menu-popup-background);
color: #f0f0f0;
border: 1px solid var(--color-menu-popup-border);
border-top: none;
box-shadow: 0 0.25em 0.75em var(--color-menu-popup-shadow);
}
@media (menu-animation-supported) {
menu {
popup-animation: popup-animation(type:blend,duration:100ms);
}
}
menu menu
{
max-width: max-content;
}
menu li
{
width:*;
display:block;
flow:horizontal;
vertical-align:middle;
foreground-repeat: no-repeat;
foreground-position: left 2px top 50%;
padding: 0.4em 1.75em 0.4em 1.1em;
margin: 0;
}
menu li .menu-item-label
{
width: *;
text-align: left;
}
menu li:has-child-of-type(menu) caption
{
width: *;
text-align: left;
}
menu li:current caption,
menu li:hover:owns-popup caption
{
color: #f0f0f0;
}
menu li .menu-item-glyph
{
width: 1.15em;
min-width: 1.15em;
max-width: 1.15em;
min-height: 1.15em;
max-height: 1.15em;
margin: 0 0.55em 0 0;
box-sizing: border-box;
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
foreground-image: none;
}
menu li .menu-item-glyph.menu-item-glyph-spacer
{
border: 1.5px solid transparent;
border-radius: 3px;
position: relative;
}
menu li .menu-item-glyph.menu-item-checkbox
{
border: 1.5px solid var(--color-ui-text-muted, #8c8c8c);
border-radius: 3px;
position: relative;
foreground-image: none;
}
menu li .menu-item-glyph.menu-item-checkbox.checked
{
border-color: var(--color-ui-text-primary, #f0f0f0);
}
menu li .menu-item-glyph.menu-item-icon-svg
{
border: none;
width: 1.15em;
height: 1.15em;
min-width: 1.15em;
max-width: 1.15em;
padding: 0;
}
menu li .menu-item-glyph.menu-item-icon-svg svg
{
width: 1.05em;
height: 1.05em;
display: block;
vertical-align: middle;
}
menu li .menu-item-glyph.menu-item-icon-svg path,
menu li .menu-item-glyph.menu-item-icon-svg circle
{
fill: currentColor;
}
menu li .menu-item-glyph.menu-item-icon-svg path[fill="none"],
menu li .menu-item-glyph.menu-item-icon-svg circle[fill="none"]
{
fill: none;
stroke: currentColor;
}
menu li .menu-item-glyph .menu-item-checkmark
{
position: absolute;
left: 0;
top: 0;
width: *;
height: *;
text-align: center;
vertical-align: middle;
font-size: 0.72em;
line-height: 1.15em;
color: var(--color-ui-text-primary, #f0f0f0);
}
menu li:current .menu-item-glyph.menu-item-checkbox,
menu li:hover:owns-popup .menu-item-glyph.menu-item-checkbox
{
border-color: #f0f0f0;
}
menu li:current .menu-item-glyph .menu-item-checkmark,
menu li:hover:owns-popup .menu-item-glyph .menu-item-checkmark
{
color: #f0f0f0;
}
menu li .menu-accelerator
{
width: max-content;
margin-left: 2.5em;
white-space: nowrap;
text-align: right;
color: var(--color-menu-shortcut-text);
}
menu li .menu-accelerator:empty
{
display: none;
margin: 0;
width: 0;
}
menu li:hover
{
background: var(--color-menuitem-background-hover);
border-radius: 2px;
}
menu li:current,
menu li:hover:owns-popup
{
background-color: #094771;
border-radius: 2px;
}
menu li:current .menu-item-label,
menu li:current .menu-accelerator,
menu li:hover:owns-popup .menu-item-label,
menu li:hover:owns-popup .menu-accelerator
{
color: #f0f0f0;
}
menu li:disabled
{
color:graytext;
foreground-image: none;
}
menu li:has-child-of-type(menu)
{
foreground-image:url(stock:arrow-right);
foreground-repeat: no-repeat;
foreground-position: top 50% right 2px;
}
menu li:has-child-of-type(menu):rtl
{
foreground-image:url(stock:arrow-left);
foreground-repeat: no-repeat;
foreground-position: top 50% left 2px;
}
hr
{
padding: 0;
margin: 0.2em 0;
}
menu hr
{
display: block;
height: 1px;
margin: 0.4em 0.65em;
border: none;
background: var(--color-menu-separator);
}

View File

@ -1,28 +1,11 @@
:root {
--color-config-background: #181818;
--color-config-page: #31363B;
--color-config-page-border: #505050;
}
html
{
background: var(--color-config-background);
}
button
{
color: #eff0f1;
background: #232728;
border: 1px solid #55575a;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 0.8rem;
padding: 0.2rem;
min-width: 5rem;
}
div.ConfigPage
{
background: var(--color-config-background);
background: var(--color-dialog-background);
flow: horizontal;
width: 100%%;
height: 100%%;
@ -44,16 +27,6 @@ popup.tooltip
margin: 0 0 0 0.3rem;
}
button:hover
{
border: 1px solid #3cafe9;
}
button:active
{
background: #3cafe9;
}
.tabs {
display: flex;
font-size: 0;
@ -147,12 +120,6 @@ button:active
height: 1.4rem;
}
.footer
{
padding: 0.5rem 0 0;
text-align: right;
}
#ConfigContents {
width: 100%%;
height: 100%%;

View File

@ -0,0 +1,30 @@
:root {
--color-dialog-background: #181818;
}
html {
background: var(--color-dialog-background);
}
button {
color: #eff0f1;
background: #232728;
border: 1px solid #55575a;
font-family: var(--font-ui);
font-size: 0.8rem;
padding: 0.2rem;
min-width: 5rem;
}
button:hover {
border: 1px solid #3cafe9;
}
button:active {
background: #3cafe9;
}
.footer {
padding: 0.5rem 0 0;
text-align: right;
}

View File

@ -358,3 +358,268 @@ rombrowser .games-grid[data-size="4"] .rom-card
{
display: none !important;
}
ul#menu-bar
{
margin: 0;
padding: 0 0.5em 0.3em 0.5em;
background: var(--color-menubar-background);
color: var(--color-ui-text-primary);
font-family: var(--font-ui);
font-size: 0.9em;
font-weight: 100;
behavior:menu-bar;
flow: horizontal;
}
ul#menu-bar > li
{
padding: 0.28em 0.55em;
margin: 0;
white-space: nowrap;
width: max-content;
display: block;
flow: horizontal;
border-radius: 5px;
}
ul#menu-bar > li:hover
{
background: var(--color-menubar-background-hover);
}
ul#menu-bar:focus > li:current,
ul#menu-bar > li:owns-popup {
background-color: var(--color-menubar-background-active);
border-radius: 5px 5px 0;
}
ul#menu-bar caption u,
menu li .menu-item-label u
{
text-decoration: none;
}
[data-menu-mnemonics="1"] ul#menu-bar caption u,
[data-menu-mnemonics="1"] menu li .menu-item-label u,
ul#menu-bar > li:owns-popup caption u,
menu[window-state="shown"] li .menu-item-label u,
menu[window-state="shown"] caption u
{
text-decoration: underline;
}
menu
{
behavior:menu;
flow: vertical;
display:none;
margin:0;
padding: 0.35em 0;
min-width: 14em;
max-width:max-content;
background: var(--color-menu-popup-background);
color: #f0f0f0;
border: 1px solid var(--color-menu-popup-border);
border-top: none;
box-shadow: 0 0.25em 0.75em var(--color-menu-popup-shadow);
}
@media (menu-animation-supported) {
menu {
popup-animation: popup-animation(type:blend,duration:100ms);
}
}
menu menu
{
max-width: max-content;
}
menu li
{
width:*;
display:block;
flow:horizontal;
vertical-align:middle;
foreground-repeat: no-repeat;
foreground-position: left 2px top 50%;
padding: 0.4em 1.75em 0.4em 1.1em;
margin: 0;
}
menu li .menu-item-label
{
width: *;
text-align: left;
}
menu li:has-child-of-type(menu) caption
{
width: *;
text-align: left;
}
menu li:current caption,
menu li:hover:owns-popup caption
{
color: #f0f0f0;
}
menu li .menu-item-glyph
{
width: 1.15em;
min-width: 1.15em;
max-width: 1.15em;
min-height: 1.15em;
max-height: 1.15em;
margin: 0 0.55em 0 0;
box-sizing: border-box;
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
foreground-image: none;
}
menu li .menu-item-glyph.menu-item-glyph-spacer
{
border: 1.5px solid transparent;
border-radius: 3px;
position: relative;
}
menu li .menu-item-glyph.menu-item-checkbox
{
border: 1.5px solid var(--color-ui-text-muted, #8c8c8c);
border-radius: 3px;
position: relative;
foreground-image: none;
}
menu li .menu-item-glyph.menu-item-checkbox.checked
{
border-color: var(--color-ui-text-primary, #f0f0f0);
}
menu li .menu-item-glyph.menu-item-icon-svg
{
border: none;
width: 1.15em;
height: 1.15em;
min-width: 1.15em;
max-width: 1.15em;
padding: 0;
}
menu li .menu-item-glyph.menu-item-icon-svg svg
{
width: 1.05em;
height: 1.05em;
display: block;
vertical-align: middle;
}
menu li .menu-item-glyph.menu-item-icon-svg path,
menu li .menu-item-glyph.menu-item-icon-svg circle
{
fill: currentColor;
}
menu li .menu-item-glyph.menu-item-icon-svg path[fill="none"],
menu li .menu-item-glyph.menu-item-icon-svg circle[fill="none"]
{
fill: none;
stroke: currentColor;
}
menu li .menu-item-glyph .menu-item-checkmark
{
position: absolute;
left: 0;
top: 0;
width: *;
height: *;
text-align: center;
vertical-align: middle;
font-size: 0.72em;
line-height: 1.15em;
color: var(--color-ui-text-primary, #f0f0f0);
}
menu li:current .menu-item-glyph.menu-item-checkbox,
menu li:hover:owns-popup .menu-item-glyph.menu-item-checkbox
{
border-color: #f0f0f0;
}
menu li:current .menu-item-glyph .menu-item-checkmark,
menu li:hover:owns-popup .menu-item-glyph .menu-item-checkmark
{
color: #f0f0f0;
}
menu li .menu-accelerator
{
width: max-content;
margin-left: 2.5em;
white-space: nowrap;
text-align: right;
color: var(--color-menu-shortcut-text);
}
menu li .menu-accelerator:empty
{
display: none;
margin: 0;
width: 0;
}
menu li:hover
{
background: var(--color-menuitem-background-hover);
border-radius: 2px;
}
menu li:current,
menu li:hover:owns-popup
{
background-color: #094771;
border-radius: 2px;
}
menu li:current .menu-item-label,
menu li:current .menu-accelerator,
menu li:hover:owns-popup .menu-item-label,
menu li:hover:owns-popup .menu-accelerator
{
color: #f0f0f0;
}
menu li:disabled
{
color:graytext;
foreground-image: none;
}
menu li:has-child-of-type(menu)
{
foreground-image:url(stock:arrow-right);
foreground-repeat: no-repeat;
foreground-position: top 50% right 2px;
}
menu li:has-child-of-type(menu):rtl
{
foreground-image:url(stock:arrow-left);
foreground-repeat: no-repeat;
foreground-position: top 50% left 2px;
}
menu hr
{
display: block;
height: 1px;
margin: 0.4em 0.65em;
border: none;
background: var(--color-menu-separator);
}

View File

@ -0,0 +1,53 @@
<html window-frame="solid-with-shadow" window-icon="window-icon.png">
<head>
<link href="base.css" rel="stylesheet" type="text/css" />
<link href="dialog.css" rel="stylesheet" type="text/css" />
<link href="about_dialog.css" rel="stylesheet" type="text/css" />
<title>About NXEmu</title>
</head>
<header>
<window-caption role="window-caption"><img src="window-icon-32.png" /><span>About NXEmu</span></window-caption>
<window-buttons>
<window-button role="window-close"></window-button>
</window-buttons>
</header>
<body>
<div class="about-body">
<div class="about-row-top">
<div class="about-logo-wrap">
<img class="about-logo" src="nxemu-logo.png" />
</div>
<div class="about-product">
<div class="about-name">NXEmu</div>
<div id="AboutVersion" class="about-version"></div>
<div id="AboutCopyright" class="about-copyright"></div>
</div>
</div>
<div class="about-row-content">
<div class="about-description">
<p>NXEmu is an experimental open-source Nintendo Switch emulator.</p>
<p class="about-disclaimer">You are responsible for ensuring your use of this software complies with the laws in your jurisdiction.</p>
</div>
<div class="about-credits">
<div class="about-credits-title">With thanks to</div>
<div class="about-credits-list">
the yuzu project, Stardust, and NullVector
</div>
</div>
<div class="about-links">
<a id="AboutLinkWebsite" class="about-link" href="https://www.nxemu.com/">Website</a>
<span class="about-link-sep"> | </span>
<a id="AboutLinkSource" class="about-link" href="https://github.com/N3xoX1/nxemu">Source Code</a>
<span class="about-link-sep"> | </span>
<a id="AboutLinkDiscord" class="about-link" href="https://discord.gg/hEa4hNyFWU">Discord</a>
</div>
<div class="about-trademark">
&quot;Nintendo Switch&quot; is a trademark of Nintendo. NXEmu is not affiliated with Nintendo in any way.
</div>
</div>
</div>
<div class="footer">
<button role="window-ok">OK</button>
</div>
</body>
</html>

View File

@ -1,6 +1,7 @@
<html window-frame="solid-with-shadow" window-icon="window-icon.png">
<head>
<link href="base.css" rel="stylesheet" type="text/css" />
<link href="dialog.css" rel="stylesheet" type="text/css" />
<link href="config.css" rel="stylesheet" type="text/css" />
<title>Configure Input</title>
</head>

View File

@ -1,6 +1,7 @@
<html window-frame="solid-with-shadow" window-icon="window-icon.png">
<head>
<link href="base.css" rel="stylesheet" type="text/css" />
<link href="dialog.css" rel="stylesheet" type="text/css" />
<link href="config.css" rel="stylesheet" type="text/css" />
<title>NxEmu Configuration</title>
<style>

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"/>
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 16v-4M12 8h.01"/>
</svg>

After

Width:  |  Height:  |  Size: 384 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.59 34.173A49 49 0 0 1 14.68 38C7.3 37.79 4.5 33 4.5 33a44.8 44.8 0 0 1 4.81-19.52A16.47 16.47 0 0 1 18.69 10l1 2.31A33 33 0 0 1 24 12a33 33 0 0 1 4.33.3l1-2.31a16.47 16.47 0 0 1 9.38 3.51A44.8 44.8 0 0 1 43.5 33s-2.8 4.79-10.18 5a47 47 0 0 1-2.86-3.81m6.46-2.9c-3.84 1.945-7.555 3.89-12.92 3.89s-9.08-1.945-12.92-3.89"/>
<circle fill="none" stroke="currentColor" stroke-width="2" cx="17.847" cy="26.23" r="3.35"/>
<circle fill="none" stroke="currentColor" stroke-width="2" cx="30.153" cy="26.23" r="3.35"/>
</svg>

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@ -7,6 +7,7 @@ add_library(common STATIC
path.cpp
path_finder.cpp
platform.cpp
shell_open_url.cpp
std_string.cpp
)

View File

@ -34,6 +34,7 @@
<ClInclude Include="path.h" />
<ClInclude Include="path_finder.h" />
<ClInclude Include="sha256.h" />
<ClInclude Include="shell_open_url.h" />
<ClInclude Include="std_string.h" />
</ItemGroup>
<ItemGroup>
@ -46,6 +47,7 @@
<ClCompile Include="path.cpp" />
<ClCompile Include="path_finder.cpp" />
<ClCompile Include="sha256.cpp" />
<ClCompile Include="shell_open_url.cpp" />
<ClCompile Include="std_string.cpp" />
</ItemGroup>
</Project>

View File

@ -48,6 +48,9 @@
<ClInclude Include="path_finder.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="shell_open_url.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="file.cpp">
@ -80,5 +83,8 @@
<ClCompile Include="path_finder.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="shell_open_url.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>

View File

@ -0,0 +1,31 @@
#include "shell_open_url.h"
#include <common/std_string.h>
#ifdef _WIN32
#include <Windows.h>
#include <shellapi.h>
#endif
bool ShellOpenUrl(const char * url_utf8, const void * owner_window)
{
if (url_utf8 == nullptr || url_utf8[0] == '\0')
{
return false;
}
#ifdef _WIN32
bool converted = false;
const std::wstring wide_url = stdstr(url_utf8).ToUTF16(stdstr::CODEPAGE_UTF8, &converted);
if (!converted || wide_url.empty())
{
return false;
}
const HWND owner = (HWND)owner_window;
const HINSTANCE result = ShellExecuteW(owner, L"open", wide_url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
return reinterpret_cast<INT_PTR>(result) > 32;
#else
(void)owner_window;
return false;
#endif
}

View File

@ -0,0 +1,5 @@
#pragma once
// Opens a URL or file URI with the system default handler (browser, etc.).
// owner_window is optional; on Windows this is an HWND passed to ShellExecuteW.
bool ShellOpenUrl(const char * url_utf8, const void * owner_window = nullptr);

View File

@ -1,30 +1,13 @@
#include "web_browser.h"
#include <common/shell_open_url.h>
#include <string>
#include <string_view>
#include <windows.h>
#include <shellapi.h>
namespace
{
std::wstring Utf8ToWide(std::string_view utf8)
{
if (utf8.empty())
{
return {};
}
const int n = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), nullptr, 0);
if (n <= 0)
{
return {};
}
std::wstring wide(static_cast<std::size_t>(n), L'\0');
MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast<int>(utf8.size()), wide.data(), n);
return wide;
}
std::string FileUriFromLocalPath(std::string_view local_url_utf8)
{
std::string uri{"file:///"};
@ -52,17 +35,6 @@ void InvokeOpenResult(OpenWebPageFn callback, void * user_data, bool success, co
callback(user_data, static_cast<uint32_t>(success ? WebExitReasonHost::EndButtonPressed : WebExitReasonHost::WindowClosed), last_url_utf8 != nullptr ? last_url_utf8 : "");
}
bool ShellOpen(HWND owner, const std::string & url_utf8)
{
const std::wstring wide_url = Utf8ToWide(url_utf8);
if (wide_url.empty())
{
return false;
}
const HINSTANCE result = ShellExecuteW(owner, L"open", wide_url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
return reinterpret_cast<INT_PTR>(result) > 32;
}
} // namespace
WebBrowserApplet::WebBrowserApplet() :
@ -99,7 +71,7 @@ void WebBrowserApplet::OpenLocalWebPage(const char * local_url_utf8, void * extr
}
const std::string file_uri = FileUriFromLocalPath(local_path);
const bool success = ShellOpen(static_cast<HWND>(m_hwnd), file_uri);
const bool success = ShellOpenUrl(file_uri.c_str(), m_hwnd);
InvokeOpenResult(open_callback, open_user_data, success, success ? file_uri.c_str() : "");
}
@ -112,6 +84,6 @@ void WebBrowserApplet::OpenExternalWebPage(const char * external_url_utf8, void
return;
}
const bool success = ShellOpen(static_cast<HWND>(m_hwnd), external_url);
const bool success = ShellOpenUrl(external_url.c_str(), m_hwnd);
InvokeOpenResult(callback, user_data, success, success ? external_url.c_str() : "");
}

View File

@ -44,6 +44,7 @@
<ClCompile Include="user_interface\game_list_worker.cpp" />
<ClCompile Include="user_interface\hotkey_map.cpp" />
<ClCompile Include="user_interface\key_mappings.cpp" />
<ClCompile Include="user_interface\about_dialog.cpp" />
<ClCompile Include="user_interface\notification.cpp" />
<ClCompile Include="user_interface\sciter_main_window.cpp" />
<ClCompile Include="user_interface\settings\config_setting .cpp" />
@ -65,10 +66,10 @@
<ClInclude Include="settings\ui_settings.h" />
<ClInclude Include="settings\ui_settings_identifiers.h" />
<ClInclude Include="startup_checks.h" />
<ClInclude Include="user_interface\dpi_scaling.h" />
<ClInclude Include="user_interface\game_list_worker.h" />
<ClInclude Include="user_interface\hotkey_map.h" />
<ClInclude Include="user_interface\key_mappings.h" />
<ClInclude Include="user_interface\about_dialog.h" />
<ClInclude Include="user_interface\notification.h" />
<ClInclude Include="user_interface\resource.h" />
<ClInclude Include="user_interface\sciter_main_window.h" />

View File

@ -51,6 +51,9 @@
<ClCompile Include="settings\ui_settings.cpp">
<Filter>Source Files\settings</Filter>
</ClCompile>
<ClCompile Include="user_interface\about_dialog.cpp">
<Filter>Source Files\user_interface</Filter>
</ClCompile>
<ClCompile Include="user_interface\notification.cpp">
<Filter>Source Files\user_interface</Filter>
</ClCompile>
@ -116,6 +119,9 @@
<ClInclude Include="settings\ui_settings.h">
<Filter>Header Files\settings</Filter>
</ClInclude>
<ClInclude Include="user_interface\about_dialog.h">
<Filter>Header Files\user_interface</Filter>
</ClInclude>
<ClInclude Include="user_interface\notification.h">
<Filter>Header Files\user_interface</Filter>
</ClInclude>
@ -161,9 +167,6 @@
<ClInclude Include="user_interface\game_list_worker.h">
<Filter>Header Files\user_interface</Filter>
</ClInclude>
<ClInclude Include="user_interface\dpi_scaling.h">
<Filter>Header Files\user_interface</Filter>
</ClInclude>
<ClInclude Include="user_interface\widgets\rom_browser.h">
<Filter>Header Files\user_interface\widgets</Filter>
</ClInclude>

View File

@ -0,0 +1,107 @@
#include "about_dialog.h"
#include <common/shell_open_url.h>
#include <nxemu-core/version.h>
#include <sciter_element.h>
#include <sciter_handler.h>
#include <sciter_ui.h>
#include <string>
namespace
{
void AttachClickHandler(ISciterUI & sciterUI, const SciterElement & element, IClickSink * sink)
{
if (element.IsValid())
{
sciterUI.AttachHandler(element, IID_ICLICKSINK, sink);
}
}
} // namespace
AboutDialog::AboutDialog(ISciterUI & sciterUI) :
m_sciterUI(sciterUI),
m_window(nullptr)
{
}
AboutDialog::~AboutDialog() = default;
void AboutDialog::Display(void * parentWindow)
{
enum
{
WINDOW_WIDTH = 480,
};
m_window = nullptr;
if (!m_sciterUI.WindowCreate(parentWindow, "about_dialog.html", 0, 0, WINDOW_WIDTH, 0, SUIW_CHILD, m_window))
{
return;
}
m_window->OnDestroySinkAdd(this);
SciterElement root(m_window->GetRootElement());
if (root.IsValid())
{
SciterElement versionEl(root.GetElementByID("AboutVersion"));
if (versionEl.IsValid())
{
versionEl.SetText(VER_FILE_VERSION_STR);
}
SciterElement copyrightEl(root.GetElementByID("AboutCopyright"));
if (copyrightEl.IsValid())
{
copyrightEl.SetText(VER_COPYRIGHT_STR);
}
AttachClickHandler(m_sciterUI, root.FindFirst("button[role=\"window-ok\"]"), this);
AttachClickHandler(m_sciterUI, root.FindFirst("window-button[role=\"window-close\"]"), this);
AttachClickHandler(m_sciterUI, root.GetElementByID("AboutLinkWebsite"), this);
AttachClickHandler(m_sciterUI, root.GetElementByID("AboutLinkSource"), this);
AttachClickHandler(m_sciterUI, root.GetElementByID("AboutLinkDiscord"), this);
}
m_window->FixMinSize();
m_window->CenterWindow();
}
void AboutDialog::Close()
{
if (m_window == nullptr || m_window->IsClosed())
{
return;
}
m_window->Destroy();
}
bool AboutDialog::OnClick(SCITER_ELEMENT element, SCITER_ELEMENT /*source*/, uint32_t /*reason*/)
{
SciterElement clickElem(element);
const std::string role = clickElem.GetAttribute("role");
if (role == "window-ok" || role == "window-close")
{
Close();
return true;
}
const std::string href = clickElem.GetAttribute("href");
if (!href.empty() && (href.compare(0, 7, "http://") == 0 || href.compare(0, 8, "https://") == 0))
{
const void * owner = m_window != nullptr ? m_window->GetHandle() : nullptr;
ShellOpenUrl(href.c_str(), owner);
return true;
}
return false;
}
void AboutDialog::OnWindowDestroy(HWINDOW /*hWnd*/)
{
m_window = nullptr;
}

View File

@ -0,0 +1,29 @@
#pragma once
#include <sciter_handler.h>
__interface ISciterUI;
__interface ISciterWindow;
class AboutDialog :
public IClickSink,
public IWindowDestroySink
{
public:
explicit AboutDialog(ISciterUI & sciterUI);
~AboutDialog();
void Display(void * parentWindow);
bool OnClick(SCITER_ELEMENT element, SCITER_ELEMENT source, uint32_t reason) override;
void OnWindowDestroy(HWINDOW hWnd) override;
private:
AboutDialog(const AboutDialog &) = delete;
AboutDialog & operator=(const AboutDialog &) = delete;
void Close();
ISciterUI & m_sciterUI;
ISciterWindow * m_window;
};

View File

@ -1,86 +0,0 @@
#pragma once
#ifdef _WIN32
#include <Windows.h>
#endif
#include <algorithm>
#include <cstdint>
inline void ScaleWindowSizeForDpi(void * parentWindow, int & width, int & height)
{
#ifdef _WIN32
HWND parent = static_cast<HWND>(parentWindow);
UINT dpi = USER_DEFAULT_SCREEN_DPI;
typedef UINT (WINAPI * PFN_GetDpiForWindow)(HWND);
typedef UINT (WINAPI * PFN_GetDpiForSystem)();
static PFN_GetDpiForWindow pGetDpiForWindow = reinterpret_cast<PFN_GetDpiForWindow>(
::GetProcAddress(::GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"));
static PFN_GetDpiForSystem pGetDpiForSystem = reinterpret_cast<PFN_GetDpiForSystem>(
::GetProcAddress(::GetModuleHandleW(L"user32.dll"), "GetDpiForSystem"));
if (parent != nullptr && pGetDpiForWindow != nullptr)
{
UINT v = pGetDpiForWindow(parent);
if (v != 0)
{
dpi = v;
}
}
else if (pGetDpiForSystem != nullptr)
{
UINT v = pGetDpiForSystem();
if (v != 0)
{
dpi = v;
}
}
else
{
HDC hdc = ::GetDC(nullptr);
if (hdc != nullptr)
{
int v = ::GetDeviceCaps(hdc, LOGPIXELSX);
if (v > 0)
{
dpi = static_cast<UINT>(v);
}
::ReleaseDC(nullptr, hdc);
}
}
width = ::MulDiv(width, static_cast<int>(dpi), USER_DEFAULT_SCREEN_DPI);
height = ::MulDiv(height, static_cast<int>(dpi), USER_DEFAULT_SCREEN_DPI);
RECT workArea{};
HMONITOR monitor = parent != nullptr
? ::MonitorFromWindow(parent, MONITOR_DEFAULTTONEAREST)
: ::MonitorFromPoint(POINT{0, 0}, MONITOR_DEFAULTTOPRIMARY);
MONITORINFO mi{sizeof(MONITORINFO)};
if (monitor != nullptr && ::GetMonitorInfoW(monitor, &mi))
{
workArea = mi.rcWork;
}
else if (!::SystemParametersInfoW(SPI_GETWORKAREA, 0, &workArea, 0))
{
workArea.right = ::GetSystemMetrics(SM_CXSCREEN);
workArea.bottom = ::GetSystemMetrics(SM_CYSCREEN);
}
const int maxWidth = std::max<int>(320, workArea.right - workArea.left);
const int maxHeight = std::max<int>(240, workArea.bottom - workArea.top);
if (width > maxWidth)
{
width = maxWidth;
}
if (height > maxHeight)
{
height = maxHeight;
}
#else
(void)parentWindow;
(void)width;
(void)height;
#endif
}

View File

@ -1,11 +1,12 @@
#include "sciter_main_window.h"
#include "user_interface/about_dialog.h"
#include "user_interface/notification.h"
#include "settings/input_config.h"
#include "settings/system_config.h"
#include "settings/ui_settings.h"
#include "user_interface/dpi_scaling.h"
#include "user_interface/key_mappings.h"
#include <common/base64.h>
#include <common/shell_open_url.h>
#include <common/std_string.h>
#include <nxemu-core/notification.h>
#include <nxemu-core/settings/identifiers.h>
@ -30,6 +31,13 @@
#include <string>
#include <vector>
namespace
{
constexpr const char * kNxEmuDiscordUrl = "https://discord.gg/hEa4hNyFWU";
} // namespace
struct Win32FullscreenState
{
bool active = false;
@ -272,6 +280,10 @@ const char * SciterMainWindow::MenuIconResource(GuiAction action) const
case GuiAction::ResetWindowSize900p:
case GuiAction::ResetWindowSize1080p:
return "resize.svg";
case GuiAction::OpenAboutDialog:
return "about.svg";
case GuiAction::OpenDiscord:
return "discord.svg";
default:
return nullptr;
}
@ -431,6 +443,12 @@ void SciterMainWindow::ResetMenu()
}
mainTitleMenu.push_back(MenuBarItem(MenuBarItem::SUB_MENU, "&Options", &optionsMenu));
MenuBarItemList helpMenu;
helpMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::OpenDiscord), "&Discord", nullptr, nullptr, MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::OpenDiscord)));
helpMenu.push_back(MenuBarItem(MenuBarItem::SPLITER));
helpMenu.push_back(MenuBarItem(static_cast<int32_t>(GuiAction::OpenAboutDialog), "&About NXEmu...", nullptr, nullptr, MenuBarItem::CheckState::None, MenuIconSvg(GuiAction::OpenAboutDialog)));
mainTitleMenu.push_back(MenuBarItem(MenuBarItem::SUB_MENU, "&Help", &helpMenu));
m_menuBar->AddSink(this);
m_menuBar->SetMenuContent(mainTitleMenu);
}
@ -443,10 +461,7 @@ bool SciterMainWindow::Show()
WINDOW_WIDTH = 760,
};
int width = WINDOW_WIDTH;
int height = WINDOW_HEIGHT;
ScaleWindowSizeForDpi(nullptr, width, height);
if (!m_sciterUI.WindowCreate(nullptr, "main_window.html", 0, 0, width, height, SUIW_MAIN | SUIW_HIDDEN, m_window))
if (!m_sciterUI.WindowCreate(nullptr, "main_window.html", 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, SUIW_MAIN | SUIW_HIDDEN, m_window))
{
return false;
}
@ -1137,6 +1152,17 @@ void SciterMainWindow::OnInputConfig()
m_inputConfig->Display((void *)m_window->GetHandle());
}
void SciterMainWindow::OnAbout()
{
m_aboutDialog.reset(new AboutDialog(m_sciterUI));
m_aboutDialog->Display((void *)m_window->GetHandle());
}
void SciterMainWindow::OnOpenDiscord()
{
ShellOpenUrl(kNxEmuDiscordUrl);
}
void SciterMainWindow::UpdateEmulationStatusText()
{
SciterElement statusTextEl(m_rootElement.GetElementByID("StatusText"));
@ -1352,6 +1378,12 @@ void SciterMainWindow::OnGuiAction(GuiAction action)
case GuiAction::OpenSystemConfiguration:
OnSystemConfig();
break;
case GuiAction::OpenAboutDialog:
OnAbout();
break;
case GuiAction::OpenDiscord:
OnOpenDiscord();
break;
case GuiAction::ToggleFullscreen:
ToggleFullscreen();
break;

View File

@ -20,6 +20,7 @@ struct Win32FullscreenState;
class SystemConfig;
class InputConfig;
class AboutDialog;
class SciterMainWindow :
public IWindowDestroySink,
@ -51,6 +52,8 @@ class SciterMainWindow :
ResetWindowSize720p,
ResetWindowSize900p,
ResetWindowSize1080p,
OpenAboutDialog,
OpenDiscord,
RecentFileMenuFirst,
RecentFileMenuLast = RecentFileMenuFirst + 20,
};
@ -121,6 +124,8 @@ private:
void OnRecetGame(uint32_t fileIndex);
void OnToggleDockedMode();
void OnToggleStartGamesInFullscreen();
void OnAbout();
void OnOpenDiscord();
void UpdateEmulationStatusText();
const MenuBarAccelerator * HotkeyAccelerator(const char * name);
const char * IsMenuBarAccelerator(uint32_t keyCode, uint32_t keyboardState);
@ -187,6 +192,7 @@ private:
std::string m_windowTitle;
std::unique_ptr<SystemConfig> m_systemConfig;
std::unique_ptr<InputConfig> m_inputConfig;
std::unique_ptr<AboutDialog> m_aboutDialog;
WebBrowserApplet m_WebBrowser;
float m_resolutionUpFactor;
bool m_useMultiCore;

View File

@ -5,7 +5,6 @@
#include "system_config_general.h"
#include "system_config_graphics.h"
#include "system_config_system.h"
#include "user_interface/dpi_scaling.h"
#include <common/std_string.h>
#include <nxemu-core/settings/identifiers.h>
#include <nxemu-core/settings/settings.h>
@ -48,10 +47,7 @@ void SystemConfig::Display(void * parentWindow, const char * startPage)
subPage = initialPage.substr(separator + 1);
initialPage = initialPage.substr(0, separator);
}
int width = WINDOW_WIDTH;
int height = WINDOW_HEIGHT;
ScaleWindowSizeForDpi(parentWindow, width, height);
if (!m_sciterUI.WindowCreate(parentWindow, "system_config.html", 0, 0, width, height, SUIW_CHILD, m_window))
if (!m_sciterUI.WindowCreate(parentWindow, "system_config.html", 0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, SUIW_CHILD, m_window))
{
return;
}