Repository: elvissteinjr/DesktopPlus
Branch: master
Commit: 78a63d79b184
Files: 211
Total size: 7.6 MB
Directory structure:
gitextract_9ffo_z3l/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ └── nightly.yml
├── CONTRIBUTING
├── LICENSE
├── README.md
├── assets/
│ ├── action_manifest.json
│ ├── actions_default.ini
│ ├── config_default.ini
│ ├── input/
│ │ ├── action_bindings_frame_controller.json
│ │ ├── action_bindings_gamepad.json
│ │ ├── action_bindings_generic.json
│ │ ├── action_bindings_holographic.json
│ │ ├── action_bindings_hpmotioncontroller.json
│ │ ├── action_bindings_knuckles.json
│ │ ├── action_bindings_knuckles_ev1.json
│ │ ├── action_bindings_playstation_vr2_sense_controller.json
│ │ ├── action_bindings_svl_hand_interaction_augmented.json
│ │ ├── action_bindings_touch.json
│ │ ├── action_bindings_vive_controller.json
│ │ ├── action_bindings_vive_cosmos.json
│ │ ├── action_manifest_de.json
│ │ ├── action_manifest_en.json
│ │ ├── action_manifest_ja.json
│ │ ├── action_manifest_ko.json
│ │ └── action_manifest_zh_CN.json
│ ├── keyboards/
│ │ ├── azerty_be.ini
│ │ ├── azerty_fr.ini
│ │ ├── qwerty_dk.ini
│ │ ├── qwerty_es.ini
│ │ ├── qwerty_ja.ini
│ │ ├── qwerty_ko_2-set.ini
│ │ ├── qwerty_latam.ini
│ │ ├── qwerty_th_kedmanee.ini
│ │ ├── qwerty_uk.ini
│ │ ├── qwerty_usa.ini
│ │ ├── qwertz_ger.ini
│ │ └── qwertz_hu.ini
│ ├── lang/
│ │ ├── de.ini
│ │ ├── en.ini
│ │ ├── ja.ini
│ │ ├── ko.ini
│ │ └── zh_CN.ini
│ ├── license.txt
│ ├── manifest.vrmanifest
│ ├── misc/
│ │ ├── !About this folder.txt
│ │ ├── CreateElevatedTask.bat
│ │ ├── CreateElevatedTask.ps1
│ │ ├── EnableUIAccess.bat
│ │ ├── EnableUIAccess.ps1
│ │ ├── EnableUIAccessDesktopPlus.manifest
│ │ ├── UndoSystemChanges.bat
│ │ └── UndoSystemChanges.ps1
│ ├── profiles/
│ │ └── Sample Profile.ini
│ ├── readme.txt
│ └── third-party_licenses.txt
├── docs/
│ └── user_guide.md
└── src/
├── DesktopPlus/
│ ├── BackgroundOverlay.cpp
│ ├── BackgroundOverlay.h
│ ├── CommonTypes.h
│ ├── DesktopPlus.cpp
│ ├── DesktopPlus.manifest
│ ├── DesktopPlus.rc
│ ├── DesktopPlus.ruleset
│ ├── DesktopPlus.vcxproj
│ ├── DesktopPlus.vcxproj.filters
│ ├── DesktopPlus.vcxproj.user
│ ├── DisplayManager.cpp
│ ├── DisplayManager.h
│ ├── DuplicationManager.cpp
│ ├── DuplicationManager.h
│ ├── ElevatedMode.cpp
│ ├── ElevatedMode.h
│ ├── InputSimulator.cpp
│ ├── InputSimulator.h
│ ├── LaserPointer.cpp
│ ├── LaserPointer.h
│ ├── OutputManager.cpp
│ ├── OutputManager.h
│ ├── Overlays.cpp
│ ├── Overlays.h
│ ├── PixelShader.hlsl
│ ├── PixelShaderCursor.hlsl
│ ├── RadialFollowSmoothing.cpp
│ ├── RadialFollowSmoothing.h
│ ├── SoftwareCursorGrabber.cpp
│ ├── SoftwareCursorGrabber.h
│ ├── ThreadManager.cpp
│ ├── ThreadManager.h
│ ├── VRInput.cpp
│ ├── VRInput.h
│ ├── VertexShader.hlsl
│ └── resource.h
├── DesktopPlus.sln
├── DesktopPlusUI/
│ ├── AuxUI.cpp
│ ├── AuxUI.h
│ ├── DesktopPlusUI.cpp
│ ├── DesktopPlusUI.rc
│ ├── DesktopPlusUI.vcxproj
│ ├── DesktopPlusUI.vcxproj.filters
│ ├── DesktopPlusUI.vcxproj.user
│ ├── FloatingUI.cpp
│ ├── FloatingUI.h
│ ├── FloatingWindow.cpp
│ ├── FloatingWindow.h
│ ├── ImGuiExt.cpp
│ ├── ImGuiExt.h
│ ├── NotificationIcon.cpp
│ ├── NotificationIcon.h
│ ├── TextureManager.cpp
│ ├── TextureManager.h
│ ├── TranslationManager.cpp
│ ├── TranslationManager.h
│ ├── UIManager.cpp
│ ├── UIManager.h
│ ├── VRKeyboard.cpp
│ ├── VRKeyboard.h
│ ├── VRKeyboardCommon.h
│ ├── Win32PerformanceData.cpp
│ ├── Win32PerformanceData.h
│ ├── WindowDesktopMode.cpp
│ ├── WindowDesktopMode.h
│ ├── WindowFloatingUIBar.cpp
│ ├── WindowFloatingUIBar.h
│ ├── WindowKeyboard.cpp
│ ├── WindowKeyboard.h
│ ├── WindowKeyboardEditor.cpp
│ ├── WindowKeyboardEditor.h
│ ├── WindowOverlayBar.cpp
│ ├── WindowOverlayBar.h
│ ├── WindowOverlayProperties.cpp
│ ├── WindowOverlayProperties.h
│ ├── WindowPerformance.cpp
│ ├── WindowPerformance.h
│ ├── WindowSettings.cpp
│ ├── WindowSettings.h
│ ├── imgui/
│ │ ├── imconfig.h
│ │ ├── imgui.cpp
│ │ ├── imgui.h
│ │ ├── imgui_demo.cpp
│ │ ├── imgui_draw.cpp
│ │ ├── imgui_internal.h
│ │ ├── imgui_tables.cpp
│ │ ├── imgui_widgets.cpp
│ │ ├── imstb_rectpack.h
│ │ ├── imstb_textedit.h
│ │ └── imstb_truetype.h
│ ├── imgui_win32_dx11_openvr/
│ │ ├── PixelShaderImGui.hlsl
│ │ ├── VertexShaderImGui.hlsl
│ │ ├── imgui_impl_dx11_openvr.cpp
│ │ ├── imgui_impl_dx11_openvr.h
│ │ ├── imgui_impl_win32_openvr.cpp
│ │ └── imgui_impl_win32_openvr.h
│ ├── implot/
│ │ ├── implot.cpp
│ │ ├── implot.h
│ │ ├── implot_internal.h
│ │ └── implot_items.cpp
│ └── resource.h
├── DesktopPlusWinRT/
│ ├── CaptureManager.cpp
│ ├── CaptureManager.h
│ ├── CommonHeaders.h
│ ├── DesktopPlusWinRT.cpp
│ ├── DesktopPlusWinRT.h
│ ├── DesktopPlusWinRT.rc
│ ├── DesktopPlusWinRT.vcxproj
│ ├── DesktopPlusWinRT.vcxproj.filters
│ ├── DesktopPlusWinRT.vcxproj.user
│ ├── OverlayCapture.cpp
│ ├── OverlayCapture.h
│ ├── ThreadData.h
│ ├── packages.config
│ ├── resource.h
│ └── util/
│ ├── DesktopWindow.h
│ ├── capture.desktop.interop.h
│ ├── direct3d11.interop.h
│ ├── dispatcherqueue.desktop.interop.h
│ └── hwnd.interop.h
└── Shared/
├── Actions.cpp
├── Actions.h
├── AppProfiles.cpp
├── AppProfiles.h
├── ConfigManager.cpp
├── ConfigManager.h
├── DPBrowserAPI.h
├── DPBrowserAPIClient.cpp
├── DPBrowserAPIClient.h
├── DPRect.h
├── Ini.cpp
├── Ini.h
├── InterprocessMessaging.cpp
├── InterprocessMessaging.h
├── Logging.cpp
├── Logging.h
├── Matrices.cpp
├── Matrices.h
├── OUtoSBSConverter.cpp
├── OUtoSBSConverter.h
├── OpenVRExt.cpp
├── OpenVRExt.h
├── OverlayDragger.cpp
├── OverlayDragger.h
├── OverlayManager.cpp
├── OverlayManager.h
├── Util.cpp
├── Util.h
├── Vectors.h
├── WindowManager.cpp
├── WindowManager.h
├── loguru.cpp
├── loguru.hpp
├── openvr.h
└── openvr_api.lib
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
ko_fi: elvissteinjr
================================================
FILE: .github/workflows/nightly.yml
================================================
name: Nightly Build
on:
push:
paths-ignore:
- 'README.md'
- 'CONTRIBUTING'
- 'LICENSE'
- 'docs/**'
env:
SOLUTION_FILE_PATH: ./src/DesktopPlus.sln
OUTPUT_PATH: ./src/x64/Release
BUILD_CONFIGURATION: Release
permissions:
contents: read
jobs:
build:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup MSBuild
uses: microsoft/setup-msbuild@v2
- name: Restore NuGet Packages
working-directory: ${{env.GITHUB_WORKSPACE}}
run: nuget restore ${{env.SOLUTION_FILE_PATH}}
- name: Build
working-directory: ${{env.GITHUB_WORKSPACE}}
run: msbuild /m /p:Configuration=${{env.BUILD_CONFIGURATION}} /p:DPLUS_SHA='"${{github.sha}}"' ${{env.SOLUTION_FILE_PATH}}
- name: Rename Output Folder for Archive
working-directory: ${{env.GITHUB_WORKSPACE}}
run: ren ${{env.OUTPUT_PATH}} DesktopPlus
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: DesktopPlus-${{github.sha}}
path: |
./src/x64
!./**/*.h
!./**/*.lib
!./**/*.exp
================================================
FILE: CONTRIBUTING
================================================
Short version:
Any contribution is welcomed with open arms. In the rare case something would be off, it'll be taken care of.
If you contribute code, you allow me to use it in a Steam build that may contain additional code not present in this repo.
Long version:
I want to preface this with stating that the Desktop+ code is no way trying to be professional. Just read the comments.
It's provided as-is, in hope somebody finds a use in it.
The code grew with time, overall structure may not be up to best practice. If you want to re-organize it to be "better", feel free.
In terms of actually adding code, there are no strict style guidelines. Just don't make the code look foreign in the middle of the existing mess.
If you plan to add something big, consider opening an issue for it first.
I take some needless pride in the low memory footprint, both runtime and size on disk. Try to not add bloat without reason.
It's fine for code to be a tiny bit awkward in order to use system libraries instead of pulling megabytes of additional dependencies.
Perhaps also avoid stacks of abstraction. The code may run on typically beefy machines, but it's running in the background most of time. Make sane decisions.
Just felt like I should mention this stuff somewhere. Not like anyone reads this anyways.
If you contribute code to this repository, you are also granting me (GitHub user elvissteinjr) permission to distribute it on other platforms such as Steam in binary form, possibly linking to non-free third-party libraries such as the Steamworks API library. Such a build may also contain additional code not present in this repo to support these libraries.
Please do not contribute if you do not agree to this.
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
================================================
FILE: README.md
================================================
# Desktop+ VR Overlay
Advanced desktop access for OpenVR.

## Features
- User interface with real-time adjustments accessible in VR or on desktop
- Smooth, low-latency mirroring of desktops and windows
- Low memory footprint and performance impact
- Support for creating as many overlays as SteamVR allows at once
- Customizable overlay settings (width, position, curvature, opacity, cropping), with switchable profiles
- 3D support (SBS, HSBS, OU, HOU)
- Overlay visibility and origin settings: Display a desktop/window during gameplay or attach it to a different origin (play space, dashboard, HMD, controllers, tracker)
- Actions: User-definable functions (input simulation, running applications) which can be bound to controller inputs, hotkeys or UI buttons
- Custom laser pointer implementation, allowing for non-blocking overlay interaction during gameplay
- Configurable PC-style VR keyboard with various layouts and a keyboard layout editor for deeper customization, appearing automatically when detecting text input widget focus
- Elevated access toggle, making it possible to deal with UAC prompts and other UIP-restricted UI in VR without using full admin-access at all times
- Gaze Fade: Fade-out overlay when not looking at it
- Window management: Change window focus depending on overlay/dashboard state or drag overlays when dragging the title bar of a mirrored window
- Performance Monitor: View system performance in real time
- Application profiles: Automatically switch overlay profiles when a specific VR application is being run
- Browser overlays: View web pages independent from your desktop (CEF-based)
## Usage
### Steam
Install Desktop+ from its [Steam store page](https://store.steampowered.com/app/1494460) and run it.
### Release Archive
Download and extract the latest archive from the [releases page](https://github.com/elvissteinjr/DesktopPlus/releases). Follow instructions in the included [readme file](assets/readme.txt).
Make sure to also download the [Desktop+ Browser component](https://github.com/elvissteinjr/DesktopPlusBrowser/releases) if you want browser overlay support.
### Nightly Build
An automated build based on the latest code changes can be downloaded from [here](https://nightly.link/elvissteinjr/DesktopPlus/workflows/nightly/master).
Keep in mind that nightly builds are unstable and mostly untested. Prefer the latest release if possible.
### Building from Source
The Visual Studio 2019 Solution builds out of the box with no further external dependencies.
Building with Graphics Capture support requires Windows SDK 10.0.19041 or newer, and will download C++/WinRT packages automatically.
Graphics Capture support can be disabled entirely if desired. Windows 8 SDK or newer is sufficient in that case. See DesktopPlusWinRT.h for details.
See the [Desktop+ Browser repository](https://github.com/elvissteinjr/DesktopPlusBrowser) for building the browser component.
Other compilers likely work as well, but are neither tested nor have a build configuration. Building for 32-bit is not supported.
## Demonstration
The [Steam announcements](https://store.steampowered.com/news/app/1494460) for typically feature short video clips showing off new additions.
The trailer on the [Steam store page](https://store.steampowered.com/app/1494460) also shows off some functionality.
## Documentation
For basic usage, installation and troubleshooting see the included [readme file](assets/readme.txt).
For more detailed information on each setting, step-by-step examples for a few common usage scenarios and more check out the [User Guide](docs/user_guide.md).
## Notes
Desktop+ only runs on Windows 8.1 or newer, as it uses the DXGI Desktop Duplication API which is not available on older versions of Windows.
Window mirroring through Graphics Capture requires at least Windows 10 1803 for basic support, Windows 10 2004 or newer for full support (some additional non-essential features require Windows 11 or Windows 11 24H2).
## License
This software is licensed under the GPL 3.0.
Desktop+ includes work of third-party projects. For their licenses, see [here](assets/third-party_licenses.txt).
================================================
FILE: assets/action_manifest.json
================================================
{
"actions": [
{
"name": "/actions/shortcuts/in/EnableGlobalLaserPointer",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut01",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut02",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut03",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut04",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut05",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut06",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut07",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut08",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut09",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut10",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut11",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut12",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut13",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut14",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut15",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut16",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut17",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut18",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut19",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/shortcuts/in/GlobalShortcut20",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/laserpointer/in/LeftClick",
"requirement": "suggested",
"type": "boolean"
},
{
"name": "/actions/laserpointer/in/RightClick",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/laserpointer/in/MiddleClick",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/laserpointer/in/Aux01Click",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/laserpointer/in/Aux02Click",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/laserpointer/in/Drag",
"requirement": "optional",
"type": "boolean"
},
{
"name": "/actions/scroll_discrete/in/ScrollDiscrete",
"requirement": "suggested",
"type": "vector2"
},
{
"name": "/actions/scroll_smooth/in/ScrollSmooth",
"requirement": "suggested",
"type": "vector2"
},
{
"name": "/actions/laserpointer/out/Haptic",
"requirement": "suggested",
"type": "vibration"
}
],
"action_sets": [
{
"name": "/actions/shortcuts",
"usage": "leftright"
},
{
"name": "/actions/laserpointer",
"usage": "single"
},
{
"name": "/actions/scroll_discrete",
"display_with": "/actions/laserpointer",
"usage": "single"
},
{
"name": "/actions/scroll_smooth",
"display_with": "/actions/laserpointer",
"usage": "single"
}
],
"localization_files": {
"en_US" : "input/action_manifest_en.json",
"de_DE" : "input/action_manifest_de.json",
"ja_JP" : "input/action_manifest_ja.json",
"ko_KR" : "input/action_manifest_ko.json",
"zh_CN" : "input/action_manifest_zh_CN.json"
},
"default_bindings": [
{
"controller_type": "frame_controller",
"binding_url": "input/action_bindings_frame_controller.json"
},
{
"controller_type": "knuckles",
"binding_url": "input/action_bindings_knuckles.json"
},
{
"controller_type": "playstation_vr2_sense",
"binding_url": "input/action_bindings_playstation_vr2_sense_controller.json"
},
{
"controller_type": "hpmotioncontroller",
"binding_url": "input/action_bindings_hpmotioncontroller.json"
},
{
"controller_type": "holographic",
"binding_url": "input/action_bindings_holographic.json"
},
{
"controller_type": "oculus_touch",
"binding_url": "input/action_bindings_touch.json"
},
{
"controller_type": "vive_cosmos_controller",
"binding_url": "input/action_bindings_vive_cosmos.json"
},
{
"controller_type": "knuckles_ev1",
"binding_url": "input/action_bindings_knuckles_ev1.json"
},
{
"controller_type": "vive_controller",
"binding_url": "input/action_bindings_vive_controller.json"
},
{
"controller_type": "svl_hand_interaction_augmented",
"binding_url": "input/action_bindings_svl_hand_interaction_augmented.json"
},
{
"controller_type": "gamepad",
"binding_url": "input/action_bindings_gamepad.json"
},
{
"controller_type": "generic",
"binding_url": "input/action_bindings_generic.json"
}
],
"version": 5,
"minimum_required_version": 2
}
================================================
FILE: assets/actions_default.ini
================================================
[1]
Name=tstr_DefActionShowKeyboard
CommandCount=1
Command0Type=ShowKeyboard
Command0UIntArg=0
IconFilename=keyboard.png
[2]
Name=tstr_DefActionActiveWindowCrop
Label=tstr_DefActionActiveWindowCropLabel
CommandCount=1
Command0Type=CropActiveWindow
[3]
Name=tstr_DefActionSwitchTask
CommandCount=1
Command0Type=SwitchTask
IconFilename=task_switch.png
[4]
Name=tstr_DefActionToggleOverlays
Label=tstr_DefActionToggleOverlaysLabel
CommandCount=1
Command0Type=ShowOverlay
TargetUseTags=true
TargetTags=Ovrl_All
[5]
Name=tstr_DefActionMiddleMouse
Label=tstr_DefActionMiddleMouseLabel
CommandCount=1
Command0Type=Key
Command0UIntID=4
Command0UIntArg=0
[6]
Name=tstr_DefActionBackMouse
Label=tstr_DefActionBackMouseLabel
CommandCount=1
Command0Type=Key
Command0UIntID=5
Command0UIntArg=0
[7]
Name=tstr_DefActionReadMe
Label=tstr_DefActionReadMeLabel
CommandCount=1
Command0Type=LaunchApp
Command0StrMain=readme.txt
[8]
Name=tstr_DefActionDashboardToggle
Label=tstr_DefActionDashboardToggleLabel
CommandCount=1
Command0Type=LaunchApp
Command0StrMain=vrmonitor://debugcommands/system_dashboard_toggle
================================================
FILE: assets/config_default.ini
================================================
;Lines starting with ; are comments
;Boolean settings allow use of "true"/"false" or 1/0
;Do not include the quotation marks for string values
;Only change the values after the =
;
;It shouldn't be necessary to mess with most of these by hand, but feel free to do so
;Hidden settings are preceded by a comment line
;Save changes to config.ini, not config_default.ini, and make sure Desktop+ is not running when doing so
[Overlay0]
Name=
NameIsCustom=false
Enabled=true
DesktopID=0
CaptureSource=0
WinRTDesktopID=-2
WinRTLastWindowTitle=
WinRTLastWindowClassName=
WinRTLastWindowExeName=
BrowserURL=
BrowserURLUserLast=
BrowserTitle=
BrowserAllowTransparency=false
Width=165
Curvature=17
Opacity=100
Brightness=100
OffsetRight=0
OffsetUp=0
OffsetForward=0
DisplayMode=3
Origin=Dashboard
OriginHMDFloorTurning=false
OriginSmoothingLevel=0
TransformLocked=true
CroppingEnabled=false
CroppingX=0
CroppingY=0
CroppingWidth=-1
CroppingHeight=-1
ShowBackside=true
3DEnabled=false
3DMode=0
3DSwapped=false
GazeFade=false
GazeFadeDistance=0
GazeFadeRate=100
GazeFadeOpacity=0
UpdateLimitModeOverride=0
UpdateLimitMS=0
UpdateLimitFPS=7
InputEnabled=true
InputDPlusLPEnabled=true
UpdateInvisible=false
ShowFloatingUI=true
ShowDesktopButtons=true
ShowActionBar=true
ShowExtraButtons=true
ActionBarOrderUseGlobal=true
Transform=[2.12766 0 0 0 0 2.12766 0 0 0 0 2.12766 0 0 0 0 1]
[Interface]
;Don't want DesktopPlusUI.exe to run automatically? Set this to true
NoUIAutoLaunch=false
;Don't want the notification/tray icon? Set this to true
NoNotificationIcon=false
;Want to override desktop mode DPI? Set your custom scale here (100 is 100%)
DesktopUIScaleOverride=0
;LanguageFile is not included here to trigger auto-detection
ShowAdvancedSettings=false
DisplaySizeLarge=false
OverlayCurrentID=0
DesktopButtonCyclingMode=1
DesktopButtonIncludeAll=false
EnvironmentBackgroundColor=00000080
EnvironmentBackgroundColorDisplayMode=0
DimUI=false
BlankSpaceDragEnabled=true
LastVRUIScale=100
WarningCompositorResolutionHidden=false
WarningCompositorQualityHidden=false
WarningProcessElevationHidden=false
WarningElevatedModeHidden=false
WarningBrowserMissingHidden=false
WarningBrowserVersionMismatchHidden=false
WarningAppProfileActiveHidden=false
ActionOrder=1;2;3;4;5;6;7;8;
ActionOrderBarDefault=1;3;7;
WindowSettingsRestoreState=false
WindowSettingsRoomVisible=false
WindowSettingsRoomPinned=false
WindowSettingsRoomSize=100
WindowSettingsRoomTransform=[0.965926 0 0.258819 0 0 1 0 0 -0.258819 0 0.965926 0 0.846609 0.7 0.38214 1]
WindowSettingsDashboardTabVisible=false
WindowSettingsDashboardTabPinned=false
WindowSettingsDashboardTabSize=100
WindowSettingsDashboardTabTransform=[0.965926 0 0.258819 0 0 1 0 0 -0.258819 0 0.965926 0 0.846609 0.7 0.38214 1]
WindowPropertiesRestoreState=false
WindowPropertiesRoomVisible=false
WindowPropertiesRoomPinned=false
WindowPropertiesRoomSize=100
WindowPropertiesRoomTransform=[0.965926 0 -0.258819 0 0 1 0 0 0.258819 0 0.965926 0 -0.846609 0.7 0.38214 1]
WindowPropertiesDashboardTabVisible=false
WindowPropertiesDashboardTabPinned=false
WindowPropertiesDashboardTabSize=100
WindowPropertiesDashboardTabTransform=[0.965926 0 -0.258819 0 0 1 0 0 0.258819 0 0.965926 0 -0.846609 0.7 0.38214 1]
WindowPropertiesLastOverlayID=-1
WindowPropertiesRestoreState=false
WindowPropertiesRoomVisible=false
WindowPropertiesDashboardTabVisible=false
WindowKeyboardRestoreState=true
WindowKeyboardRoomVisible=false
WindowKeyboardRoomPinned=false
WindowKeyboardRoomSize=100
WindowKeyboardRoomTransform=[1 0 0 0 0 0.707107 -0.707107 0 0 0.707107 0.707107 0 0 -0.782608 0.782608 1]
WindowKeyboardDashboardTabVisible=false
WindowKeyboardDashboardTabPinned=false
WindowKeyboardDashboardTabSize=100
WindowKeyboardDashboardTabTransform=[1 0 0 0 0 0.707107 -0.707107 0 0 0.707107 0.707107 0 0 -0.767465 0.767466 1]
WindowKeyboardLastAssignedOverlayID=-1
QuickStartGuideHidden=false
[Input]
GoHomeButtonActionUID=6
GoBackButtonActionUID=5
;You can change this to allow for more global shortcuts, but the action manifest needs to adjusted as well and the app can not be installed in Steam (its internal manifest has priority)
GlobalShortcutsMaxCount=20
GlobalShortcut01ActionUID=0
GlobalHotkey01Modifiers=0
GlobalHotkey01KeyCode=0
GlobalHotkey01ActionUID=0
DetachedInteractionMaxDistance=200
LaserPointerBlockInput=false
GlobalHMDPointer=false
LaserPointerHMDKeyCodeToggle=0
LaserPointerHMDKeyCodeLeft=0
LaserPointerHMDKeyCodeRight=0
LaserPointerHMDKeyCodeMiddle=0
LaserPointerHMDKeyCodeDrag=0
DragAutoDocking=true
DragFixedDistance=false
DragFixedDistanceCM=200
DragFixedDistanceShape=0
DragFixedDistanceAutoCurve=true
DragFixedDistanceAutoTilt=true
DragSnapPosition=false
DragSnapPositionSize=10
DragSnapRotation=false
DragSnapRotationX=true
DragSnapRotationY=true
DragSnapRotationZ=true
DragSnapRotationAngle=45
[Mouse]
RenderCursor=true
RenderIntersectionBlob=false
ScrollSmooth=false
SimulatePenInput=false
AllowPointerOverride=true
DoubleClickAssistDuration=-1
InputSmoothingLevel=0
[Keyboard]
LayoutFile=qwerty_usa.ini
LayoutClusterFunction=true
LayoutClusterNavigation=true
LayoutClusterNumpad=false
LayoutClusterExtra=false
StickyModifiers=true
KeyRepeat=true
[Windows]
AutoFocusSceneAppDashboard=false
WinRTAutoFocus=true
WinRTKeepOnScreen=true
WinRTAutoSizeOverlay=false
WinRTAutoFocusSceneApp=false
WinRTWindowMatchingStrict=false
WinRTDraggingMode=2
WinRTOnCaptureLost=1
[Browser]
;These arguments are passed to the Desktop+ Browser executable and parsed by CEF. Things may break, use at your own risk.
CommandLineArguments=
BrowserMaxFPS=60
BrowserContentBlocker=false
[Performance]
UpdateLimitMode=0
UpdateLimitMS=0
UpdateLimitFPS=7
RapidLaserPointerUpdates=false
SingleDesktopMirroring=false
AlternativeCursorRendering=false
ShowFPS=false
;Experience issues with the UI getting stuck? Set this to false to let it render unconditionally
UIAutoThrottle=true
;Want to lower the render rate of the UI? Set this to the amount of VR frame syncs to skip for each rendered UI frame
UIFrameSkip=0
PerformanceMonitorStyleMinimal=false
PerformanceMonitorStyleLarge=true
PerformanceMonitorStyleShowWindow=true
PerformanceMonitorStyleShowTextOutline=false
PerformanceMonitorStyleMinimalShowMore=false
PerformanceMonitorShowGraphs=true
PerformanceMonitorShowTime=false
PerformanceMonitorShowCPU=true
PerformanceMonitorShowGPU=true
PerformanceMonitorShowFPS=true
PerformanceMonitorShowBattery=true
PerformanceMonitorShowTrackers=true
PerformanceMonitorShowViveWireless=false
;Disables display of GPU load % and VRAM usage. This prevents GPU hardware monitoring related stutter with certain older NVIDIA drivers
PerformanceMonitorDisableGPUCounters=false
[Misc]
NoSteam=false
UIAccessWasEnabled=false
;Need to force a specific GPU to be used? Set the DeviceID here (get the IDs from logs)
ForceGPUDeviceID=-1
;Same as above but for the GPU SteamVR is using if different from the desktops. Normally SteamVR reports it to the application with no need to change anything here
ForceGPUVRDeviceID=-1
================================================
FILE: assets/input/action_bindings_frame_controller.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"haptics" : [
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/left/output/haptic"
},
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/right/output/haptic"
}
],
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6",
"haptic_amplitude" : "0"
},
"path" : "/user/hand/right/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6",
"haptic_amplitude" : "0"
},
"path" : "/user/hand/left/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/bumper"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/bumper"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/dpad_down"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/b"
}
]
},
"/actions/scroll_discrete" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/left/input/thumbstick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/right/input/thumbstick"
}
]
},
"/actions/scroll_smooth" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/left/input/thumbstick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/right/input/thumbstick"
}
]
}
},
"category" : "steamvr_input",
"controller_type" : "frame_controller",
"description" : "",
"name" : "Default Desktop+ bindings for Steam Frame Controllers",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_bindings_gamepad.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"path" : "/user/gamepad/input/trigger_right"
},
{
"inputs" : {
"east" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/gamepad/input/trigger_left"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/gamepad/input/b"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/gamepad/input/x"
}
]
},
"/actions/scroll_discrete" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/gamepad/input/joystick_right"
}
]
},
"/actions/scroll_smooth" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/gamepad/input/joystick_right"
}
]
}
},
"category" : "steamvr_input",
"controller_type" : "gamepad",
"description" : "",
"name" : "Default Desktop+ bindings for Gamepads",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_bindings_generic.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"haptics" : [
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/left/output/haptic"
},
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/right/output/haptic"
}
],
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/trigger"
},
{
"inputs" : {
"east" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "dpad_click",
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"east" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "dpad_click",
"path" : "/user/hand/right/input/trackpad"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/grip"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/grip"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/application_menu"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/application_menu"
}
]
},
"/actions/scroll_discrete" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"discrete_scroll_trackpad_accumthreshold_onmove" : "0.224",
"discrete_scroll_trackpad_accumthreshold_onreversal" : "0.672",
"discrete_scroll_trackpad_accumthreshold_ontouch" : "0.624",
"discrete_scroll_trackpad_noisethreshold_onmove" : "0.008",
"discrete_scroll_trackpad_noisethreshold_onreversal" : "0.095",
"discrete_scroll_trackpad_noisethreshold_ontouch" : "0.16",
"discrete_scroll_trackpad_slideandhold_borderbottom" : "-0.60",
"discrete_scroll_trackpad_slideandhold_bordertop" : "0.65",
"scroll_mode" : "discrete"
},
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"discrete_scroll_trackpad_accumthreshold_onmove" : "0.224",
"discrete_scroll_trackpad_accumthreshold_onreversal" : "0.672",
"discrete_scroll_trackpad_accumthreshold_ontouch" : "0.624",
"discrete_scroll_trackpad_noisethreshold_onmove" : "0.008",
"discrete_scroll_trackpad_noisethreshold_onreversal" : "0.095",
"discrete_scroll_trackpad_noisethreshold_ontouch" : "0.16",
"discrete_scroll_trackpad_slideandhold_borderbottom" : "-0.60",
"discrete_scroll_trackpad_slideandhold_bordertop" : "0.65",
"scroll_mode" : "discrete"
},
"path" : "/user/hand/right/input/trackpad"
}
]
},
"/actions/scroll_smooth" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/right/input/trackpad"
}
]
}
},
"category" : "steamvr_input",
"controller_type" : "generic",
"description" : "",
"name" : "Default Desktop+ bindings for Generic Controllers",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_bindings_holographic.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"haptics" : [
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/left/output/haptic"
},
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/right/output/haptic"
}
],
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/trigger"
},
{
"inputs" : {
"east" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "dpad_click",
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"east" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "dpad_click",
"path" : "/user/hand/right/input/trackpad"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/grip"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/grip"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/application_menu"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/application_menu"
}
]
},
"/actions/scroll_discrete" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "discrete"
},
"path" : "/user/hand/left/input/joystick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "discrete"
},
"path" : "/user/hand/right/input/joystick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/right/input/trackpad"
}
]
},
"/actions/scroll_smooth" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters": {
"scroll_mode": "smooth",
"smooth_scroll_multiplier": "11.8"
},
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters": {
"scroll_mode": "smooth",
"smooth_scroll_multiplier": "11.8"
},
"path" : "/user/hand/right/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters": {
"scroll_mode": "smooth",
"smooth_scroll_joystick_min_input_magnitude": "0.3"
},
"path" : "/user/hand/left/input/joystick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters": {
"scroll_mode": "smooth",
"smooth_scroll_joystick_min_input_magnitude": "0.3"
},
"path" : "/user/hand/right/input/joystick"
}
]
}
},
"category" : "steamvr_input",
"controller_type" : "holographic_controller",
"description" : "",
"name" : "Default Desktop+ bindings for Windows Mixed Reality Controllers",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_bindings_hpmotioncontroller.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"haptics" : [
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/left/output/haptic"
},
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/right/output/haptic"
}
],
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/x"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/a"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/grip"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/grip"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/y"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/b"
}
]
},
"/actions/scroll_discrete" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/left/input/joystick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/right/input/joystick"
}
]
},
"/actions/scroll_smooth" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"path" : "/user/hand/left/input/joystick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"path" : "/user/hand/right/input/joystick"
}
]
}
},
"category" : "steamvr_input",
"controller_type" : "hpmotioncontroller",
"description" : "",
"name" : "Default Desktop+ bindings for HP Motion Controllers",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_bindings_knuckles.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"haptics" : [
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/left/output/haptic"
},
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/right/output/haptic"
}
],
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/right/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/left/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/trackpad"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/MiddleClick"
}
},
"mode" : "joystick",
"path" : "/user/hand/left/input/thumbstick"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/MiddleClick"
}
},
"mode" : "joystick",
"path" : "/user/hand/right/input/thumbstick"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/b"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/b"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/a"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/a"
}
]
},
"/actions/scroll_discrete" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "discrete"
},
"path" : "/user/hand/left/input/thumbstick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "discrete"
},
"path" : "/user/hand/right/input/thumbstick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"discrete_scroll_trackpad_accumthreshold_onmove" : "0.28",
"discrete_scroll_trackpad_accumthreshold_onreversal" : "0.84",
"discrete_scroll_trackpad_accumthreshold_ontouch" : "0.78",
"discrete_scroll_trackpad_noisethreshold_onmove" : "0.01",
"discrete_scroll_trackpad_noisethreshold_onreversal" : "0.045",
"discrete_scroll_trackpad_noisethreshold_ontouch" : "0.15",
"discrete_scroll_trackpad_slideandhold_borderbottom" : "-0.65",
"discrete_scroll_trackpad_slideandhold_bordertop" : "0.55",
"scroll_mode" : "discrete"
},
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"discrete_scroll_trackpad_accumthreshold_onmove" : "0.28",
"discrete_scroll_trackpad_accumthreshold_onreversal" : "0.84",
"discrete_scroll_trackpad_accumthreshold_ontouch" : "0.78",
"discrete_scroll_trackpad_noisethreshold_onmove" : "0.01",
"discrete_scroll_trackpad_noisethreshold_onreversal" : "0.045",
"discrete_scroll_trackpad_noisethreshold_ontouch" : "0.15",
"discrete_scroll_trackpad_slideandhold_borderbottom" : "-0.65",
"discrete_scroll_trackpad_slideandhold_bordertop" : "0.55",
"scroll_mode" : "discrete"
},
"path" : "/user/hand/right/input/trackpad"
}
]
},
"/actions/scroll_smooth" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth",
"smooth_scroll_edge_min_swipe" : "0.75",
"smooth_scroll_edge_scroll_threshold" : "0.55",
"smooth_scroll_edge_scroll_threshold_vertical_bias" : "0.1",
"smooth_scroll_trackpad_aspect_ratio" : "0.5"
},
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth",
"smooth_scroll_edge_min_swipe" : "0.75",
"smooth_scroll_edge_scroll_threshold" : "0.55",
"smooth_scroll_edge_scroll_threshold_vertical_bias" : "0.1",
"smooth_scroll_trackpad_aspect_ratio" : "0.5"
},
"path" : "/user/hand/right/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/left/input/thumbstick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/right/input/thumbstick"
}
]
}
},
"category" : "steamvr_input",
"controller_type" : "knuckles",
"description" : "",
"name" : "Default Desktop+ bindings for Index Controllers",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_bindings_knuckles_ev1.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"haptics" : [
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/left/output/haptic"
},
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/right/output/haptic"
}
],
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/trigger"
},
{
"inputs" : {
"east" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "dpad_click",
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"east" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "dpad_click",
"path" : "/user/hand/right/input/trackpad"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/b"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/a"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/a"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/b"
}
]
},
"/actions/scroll_discrete" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"discrete_scroll_trackpad_accumthreshold_onmove" : "0.224",
"discrete_scroll_trackpad_accumthreshold_onreversal" : "0.672",
"discrete_scroll_trackpad_accumthreshold_ontouch" : "0.624",
"discrete_scroll_trackpad_noisethreshold_onmove" : "0.008",
"discrete_scroll_trackpad_noisethreshold_onreversal" : "0.095",
"discrete_scroll_trackpad_noisethreshold_ontouch" : "0.16",
"discrete_scroll_trackpad_slideandhold_borderbottom" : "-0.60",
"discrete_scroll_trackpad_slideandhold_bordertop" : "0.65",
"scroll_mode" : "discrete"
},
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"discrete_scroll_trackpad_accumthreshold_onmove" : "0.224",
"discrete_scroll_trackpad_accumthreshold_onreversal" : "0.672",
"discrete_scroll_trackpad_accumthreshold_ontouch" : "0.624",
"discrete_scroll_trackpad_noisethreshold_onmove" : "0.008",
"discrete_scroll_trackpad_noisethreshold_onreversal" : "0.095",
"discrete_scroll_trackpad_noisethreshold_ontouch" : "0.16",
"discrete_scroll_trackpad_slideandhold_borderbottom" : "-0.60",
"discrete_scroll_trackpad_slideandhold_bordertop" : "0.65",
"scroll_mode" : "discrete"
},
"path" : "/user/hand/right/input/trackpad"
}
]
},
"/actions/scroll_smooth" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/right/input/trackpad"
}
]
}
},
"category" : "steamvr_input",
"controller_type" : "knuckles_ev1",
"description" : "",
"name" : "Default Desktop+ bindings for Knuckles EV1 Controllers",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_bindings_playstation_vr2_sense_controller.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"haptics" : [
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/left/output/haptic"
},
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/right/output/haptic"
}
],
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/left/input/l2"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/right/input/r2"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/square"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/cross"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/triangle"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/circle"
}
]
},
"/actions/scroll_discrete" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/left/input/left_stick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/right/input/right_stick"
}
]
},
"/actions/scroll_smooth" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/left/input/left_stick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/right/input/right_stick"
}
]
}
},
"category" : "steamvr_input",
"controller_type" : "playstation_vr2_sense",
"description" : "",
"name" : "Default Desktop+ bindings for PS VR2 Sense Controllers",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_bindings_svl_hand_interaction_augmented.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/right/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/left/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/index_pinch"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/index_pinch"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/pinky_pinch"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/pinky_pinch"
}
]
}
},
"category" : "steamvr_input",
"controller_type": "svl_hand_interaction_augmented",
"description": "",
"name": "Default Desktop+ bindings for Steam Link Hand Tracking",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_bindings_touch.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"haptics" : [
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/left/output/haptic"
},
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/right/output/haptic"
}
],
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/right/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/left/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/x"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/a"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/MiddleClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/grip"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/MiddleClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/grip"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/y"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/b"
}
]
},
"/actions/scroll_discrete" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/left/input/joystick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/right/input/joystick"
}
]
},
"/actions/scroll_smooth" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/left/input/joystick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/right/input/joystick"
}
]
}
},
"category" : "steamvr_input",
"controller_type" : "oculus_touch",
"description" : "",
"name" : "Default Desktop+ bindings for Oculus Touch",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_bindings_vive_controller.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"haptics" : [
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/left/output/haptic"
},
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/right/output/haptic"
}
],
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/right/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/left/input/trigger"
},
{
"inputs" : {
"east" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "dpad_click",
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"east" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "dpad_click",
"path" : "/user/hand/right/input/trackpad"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/grip"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux01Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/grip"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/application_menu"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/application_menu"
}
]
},
"/actions/scroll_discrete" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"discrete_scroll_trackpad_accumthreshold_onmove" : "0.224",
"discrete_scroll_trackpad_accumthreshold_onreversal" : "0.672",
"discrete_scroll_trackpad_accumthreshold_ontouch" : "0.624",
"discrete_scroll_trackpad_noisethreshold_onmove" : "0.008",
"discrete_scroll_trackpad_noisethreshold_onreversal" : "0.095",
"discrete_scroll_trackpad_noisethreshold_ontouch" : "0.16",
"discrete_scroll_trackpad_slideandhold_borderbottom" : "-0.60",
"discrete_scroll_trackpad_slideandhold_bordertop" : "0.65",
"scroll_mode" : "discrete"
},
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"parameters" : {
"discrete_scroll_trackpad_accumthreshold_onmove" : "0.224",
"discrete_scroll_trackpad_accumthreshold_onreversal" : "0.672",
"discrete_scroll_trackpad_accumthreshold_ontouch" : "0.624",
"discrete_scroll_trackpad_noisethreshold_onmove" : "0.008",
"discrete_scroll_trackpad_noisethreshold_onreversal" : "0.095",
"discrete_scroll_trackpad_noisethreshold_ontouch" : "0.16",
"discrete_scroll_trackpad_slideandhold_borderbottom" : "-0.60",
"discrete_scroll_trackpad_slideandhold_bordertop" : "0.65",
"scroll_mode" : "discrete"
},
"path" : "/user/hand/right/input/trackpad"
}
]
},
"/actions/scroll_smooth" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/left/input/trackpad"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/right/input/trackpad"
}
]
}
},
"category" : "steamvr_input",
"controller_type" : "vive_controller",
"description" : "",
"name" : "Default Desktop+ bindings for Vive Controllers",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_bindings_vive_cosmos.json
================================================
{
"action_manifest_version" : 5,
"alias_info" : {},
"app_key" : "steam.overlay.1494460",
"bindings" : {
"/actions/laserpointer" : {
"haptics" : [
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/left/output/haptic"
},
{
"output" : "/actions/laserpointer/out/Haptic",
"path" : "/user/hand/right/output/haptic"
}
],
"sources" : [
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/right/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/LeftClick"
}
},
"mode" : "button",
"parameters" : {
"click_activate_threshold" : "0.65",
"click_deactivate_threshold" : "0.6"
},
"path" : "/user/hand/left/input/trigger"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/x"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/RightClick"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/a"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/left/input/y"
},
{
"inputs" : {
"click" : {
"output" : "/actions/laserpointer/in/Aux02Click"
}
},
"mode" : "button",
"path" : "/user/hand/right/input/b"
}
]
},
"/actions/scroll_discrete" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/left/input/joystick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_discrete/in/ScrollDiscrete"
}
},
"mode" : "scroll",
"path" : "/user/hand/right/input/joystick"
}
]
},
"/actions/scroll_smooth" : {
"sources" : [
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/left/input/joystick"
},
{
"inputs" : {
"scroll" : {
"output" : "/actions/scroll_smooth/in/ScrollSmooth"
}
},
"mode" : "scroll",
"parameters" : {
"scroll_mode" : "smooth"
},
"path" : "/user/hand/right/input/joystick"
}
]
}
},
"category" : "steamvr_input",
"controller_type" : "vive_cosmos_controller",
"description" : "",
"name" : "Default Desktop+ bindings for Vive Cosmos Controllers",
"options" : {},
"simulated_actions" : []
}
================================================
FILE: assets/input/action_manifest_de.json
================================================
{
"language_tag": "de_DE",
"/actions/shortcuts" : "Globale Kürzel",
"/actions/shortcuts/in/EnableGlobalLaserPointer" : "Aktiviere globalen Laser-Pointer",
"/actions/shortcuts/in/GlobalShortcut01" : "Globales Kürzel #1 ausführen",
"/actions/shortcuts/in/GlobalShortcut02" : "Globales Kürzel #2 ausführen",
"/actions/shortcuts/in/GlobalShortcut03" : "Globales Kürzel #3 ausführen",
"/actions/shortcuts/in/GlobalShortcut04" : "Globales Kürzel #4 ausführen",
"/actions/shortcuts/in/GlobalShortcut05" : "Globales Kürzel #5 ausführen",
"/actions/shortcuts/in/GlobalShortcut06" : "Globales Kürzel #6 ausführen",
"/actions/shortcuts/in/GlobalShortcut07" : "Globales Kürzel #7 ausführen",
"/actions/shortcuts/in/GlobalShortcut08" : "Globales Kürzel #8 ausführen",
"/actions/shortcuts/in/GlobalShortcut09" : "Globales Kürzel #9 ausführen",
"/actions/shortcuts/in/GlobalShortcut10" : "Globales Kürzel #10 ausführen",
"/actions/shortcuts/in/GlobalShortcut11" : "Globales Kürzel #11 ausführen",
"/actions/shortcuts/in/GlobalShortcut12" : "Globales Kürzel #12 ausführen",
"/actions/shortcuts/in/GlobalShortcut13" : "Globales Kürzel #13 ausführen",
"/actions/shortcuts/in/GlobalShortcut14" : "Globales Kürzel #14 ausführen",
"/actions/shortcuts/in/GlobalShortcut15" : "Globales Kürzel #15 ausführen",
"/actions/shortcuts/in/GlobalShortcut16" : "Globales Kürzel #16 ausführen",
"/actions/shortcuts/in/GlobalShortcut17" : "Globales Kürzel #17 ausführen",
"/actions/shortcuts/in/GlobalShortcut18" : "Globales Kürzel #18 ausführen",
"/actions/shortcuts/in/GlobalShortcut19" : "Globales Kürzel #19 ausführen",
"/actions/shortcuts/in/GlobalShortcut20" : "Globales Kürzel #20 ausführen",
"/actions/laserpointer" : "Laserpointer",
"/actions/laserpointer/in/LeftClick" : "Linksklick",
"/actions/laserpointer/in/RightClick" : "Rechtsklick",
"/actions/laserpointer/in/MiddleClick" : "Mittelklick",
"/actions/laserpointer/in/Aux01Click" : "Extra 1-Klick",
"/actions/laserpointer/in/Aux02Click" : "Extra 2-Klick",
"/actions/laserpointer/in/Drag" : "Overlay ziehen",
"/actions/scroll_discrete" : "Mausrad-Bildlauf",
"/actions/scroll_discrete/in/ScrollDiscrete" : "Overlay scrollen",
"/actions/scroll_smooth" : "Weicher Bildlauf",
"/actions/scroll_smooth/in/ScrollSmooth" : "Overlay scrollen"
}
================================================
FILE: assets/input/action_manifest_en.json
================================================
{
"language_tag": "en_US",
"/actions/shortcuts" : "Global Shortcuts",
"/actions/shortcuts/in/EnableGlobalLaserPointer" : "Enable Global Laser Pointer",
"/actions/shortcuts/in/GlobalShortcut01" : "Do Global Shortcut #1",
"/actions/shortcuts/in/GlobalShortcut02" : "Do Global Shortcut #2",
"/actions/shortcuts/in/GlobalShortcut03" : "Do Global Shortcut #3",
"/actions/shortcuts/in/GlobalShortcut04" : "Do Global Shortcut #4",
"/actions/shortcuts/in/GlobalShortcut05" : "Do Global Shortcut #5",
"/actions/shortcuts/in/GlobalShortcut06" : "Do Global Shortcut #6",
"/actions/shortcuts/in/GlobalShortcut07" : "Do Global Shortcut #7",
"/actions/shortcuts/in/GlobalShortcut08" : "Do Global Shortcut #8",
"/actions/shortcuts/in/GlobalShortcut09" : "Do Global Shortcut #9",
"/actions/shortcuts/in/GlobalShortcut10" : "Do Global Shortcut #10",
"/actions/shortcuts/in/GlobalShortcut11" : "Do Global Shortcut #11",
"/actions/shortcuts/in/GlobalShortcut12" : "Do Global Shortcut #12",
"/actions/shortcuts/in/GlobalShortcut13" : "Do Global Shortcut #13",
"/actions/shortcuts/in/GlobalShortcut14" : "Do Global Shortcut #14",
"/actions/shortcuts/in/GlobalShortcut15" : "Do Global Shortcut #15",
"/actions/shortcuts/in/GlobalShortcut16" : "Do Global Shortcut #16",
"/actions/shortcuts/in/GlobalShortcut17" : "Do Global Shortcut #17",
"/actions/shortcuts/in/GlobalShortcut18" : "Do Global Shortcut #18",
"/actions/shortcuts/in/GlobalShortcut19" : "Do Global Shortcut #19",
"/actions/shortcuts/in/GlobalShortcut20" : "Do Global Shortcut #20",
"/actions/laserpointer" : "Laser Pointer",
"/actions/laserpointer/in/LeftClick" : "Left Click",
"/actions/laserpointer/in/RightClick" : "Right Click",
"/actions/laserpointer/in/MiddleClick" : "Middle Click",
"/actions/laserpointer/in/Aux01Click" : "Auxiliary 1 Click",
"/actions/laserpointer/in/Aux02Click" : "Auxiliary 2 Click",
"/actions/laserpointer/in/Drag" : "Drag Overlay",
"/actions/scroll_discrete" : "Mousewheel Scrolling",
"/actions/scroll_discrete/in/ScrollDiscrete" : "Scroll Overlay",
"/actions/scroll_smooth" : "Smooth Scrolling",
"/actions/scroll_smooth/in/ScrollSmooth" : "Scroll Overlay"
}
================================================
FILE: assets/input/action_manifest_ja.json
================================================
{
"language_tag": "ja_JP",
"/actions/shortcuts" : "グローバルショートカット",
"/actions/shortcuts/in/EnableGlobalLaserPointer" : "グローバルレーザーポインターを有効化",
"/actions/shortcuts/in/GlobalShortcut01" : "グローバルショートカットを実行 #1",
"/actions/shortcuts/in/GlobalShortcut02" : "グローバルショートカットを実行 #2",
"/actions/shortcuts/in/GlobalShortcut03" : "グローバルショートカットを実行 #3",
"/actions/shortcuts/in/GlobalShortcut04" : "グローバルショートカットを実行 #4",
"/actions/shortcuts/in/GlobalShortcut05" : "グローバルショートカットを実行 #5",
"/actions/shortcuts/in/GlobalShortcut06" : "グローバルショートカットを実行 #6",
"/actions/shortcuts/in/GlobalShortcut07" : "グローバルショートカットを実行 #7",
"/actions/shortcuts/in/GlobalShortcut08" : "グローバルショートカットを実行 #8",
"/actions/shortcuts/in/GlobalShortcut09" : "グローバルショートカットを実行 #9",
"/actions/shortcuts/in/GlobalShortcut10" : "グローバルショートカットを実行 #10",
"/actions/shortcuts/in/GlobalShortcut11" : "グローバルショートカットを実行 #11",
"/actions/shortcuts/in/GlobalShortcut12" : "グローバルショートカットを実行 #12",
"/actions/shortcuts/in/GlobalShortcut13" : "グローバルショートカットを実行 #13",
"/actions/shortcuts/in/GlobalShortcut14" : "グローバルショートカットを実行 #14",
"/actions/shortcuts/in/GlobalShortcut15" : "グローバルショートカットを実行 #15",
"/actions/shortcuts/in/GlobalShortcut16" : "グローバルショートカットを実行 #16",
"/actions/shortcuts/in/GlobalShortcut17" : "グローバルショートカットを実行 #17",
"/actions/shortcuts/in/GlobalShortcut18" : "グローバルショートカットを実行 #18",
"/actions/shortcuts/in/GlobalShortcut19" : "グローバルショートカットを実行 #19",
"/actions/shortcuts/in/GlobalShortcut20" : "グローバルショートカットを実行 #20",
"/actions/laserpointer" : "レーザーポインター",
"/actions/laserpointer/in/LeftClick" : "左クリック",
"/actions/laserpointer/in/RightClick" : "右クリック",
"/actions/laserpointer/in/MiddleClick" : "ホイールクリック",
"/actions/laserpointer/in/Aux01Click" : "補助 1 クリック",
"/actions/laserpointer/in/Aux02Click" : "補助 2 クリック",
"/actions/scroll_discrete" : "マウスホイール スクロール",
"/actions/scroll_discrete/in/ScrollDiscrete" : "スクロールオーバーレイ",
"/actions/scroll_smooth" : "スムーズ スクロール",
"/actions/scroll_smooth/in/ScrollSmooth" : "スクロールオーバーレイ"
}
================================================
FILE: assets/input/action_manifest_ko.json
================================================
{
"language_tag": "ko_KR",
"/actions/shortcuts" : "전역 단축키",
"/actions/shortcuts/in/EnableGlobalLaserPointer" : "전역 레이저 포인터 활성화",
"/actions/shortcuts/in/GlobalShortcut01" : "전역 단축키 실행 #1",
"/actions/shortcuts/in/GlobalShortcut02" : "전역 단축키 실행 #2",
"/actions/shortcuts/in/GlobalShortcut03" : "전역 단축키 실행 #3",
"/actions/shortcuts/in/GlobalShortcut04" : "전역 단축키 실행 #4",
"/actions/shortcuts/in/GlobalShortcut05" : "전역 단축키 실행 #5",
"/actions/shortcuts/in/GlobalShortcut06" : "전역 단축키 실행 #6",
"/actions/shortcuts/in/GlobalShortcut07" : "전역 단축키 실행 #7",
"/actions/shortcuts/in/GlobalShortcut08" : "전역 단축키 실행 #8",
"/actions/shortcuts/in/GlobalShortcut09" : "전역 단축키 실행 #9",
"/actions/shortcuts/in/GlobalShortcut10" : "전역 단축키 실행 #10",
"/actions/shortcuts/in/GlobalShortcut11" : "전역 단축키 실행 #11",
"/actions/shortcuts/in/GlobalShortcut12" : "전역 단축키 실행 #12",
"/actions/shortcuts/in/GlobalShortcut13" : "전역 단축키 실행 #13",
"/actions/shortcuts/in/GlobalShortcut14" : "전역 단축키 실행 #14",
"/actions/shortcuts/in/GlobalShortcut15" : "전역 단축키 실행 #15",
"/actions/shortcuts/in/GlobalShortcut16" : "전역 단축키 실행 #16",
"/actions/shortcuts/in/GlobalShortcut17" : "전역 단축키 실행 #17",
"/actions/shortcuts/in/GlobalShortcut18" : "전역 단축키 실행 #18",
"/actions/shortcuts/in/GlobalShortcut19" : "전역 단축키 실행 #19",
"/actions/shortcuts/in/GlobalShortcut20" : "전역 단축키 실행 #20",
"/actions/laserpointer" : "레이저 포인터",
"/actions/laserpointer/in/LeftClick" : "왼쪽 클릭",
"/actions/laserpointer/in/RightClick" : "오른쪽 클릭",
"/actions/laserpointer/in/MiddleClick" : "휠 클릭",
"/actions/laserpointer/in/Aux01Click" : "측면 1 클릭",
"/actions/laserpointer/in/Aux02Click" : "측면 2 클릭",
"/actions/laserpointer/in/Drag" : "오버레이 드래그",
"/actions/scroll_discrete" : "마우스 휠 스크롤",
"/actions/scroll_discrete/in/ScrollDiscrete" : "오버레이 스크롤",
"/actions/scroll_smooth" : "부드러운 스크롤",
"/actions/scroll_smooth/in/ScrollSmooth" : "오버레이 스크롤"
}
================================================
FILE: assets/input/action_manifest_zh_CN.json
================================================
{
"language_tag": "zh_CN",
"/actions/shortcuts" : "全局快捷键",
"/actions/shortcuts/in/EnableGlobalLaserPointer" : "启用全局激光指针",
"/actions/shortcuts/in/GlobalShortcut01" : "执行全局快捷键 #1",
"/actions/shortcuts/in/GlobalShortcut02" : "执行全局快捷键 #2",
"/actions/shortcuts/in/GlobalShortcut03" : "执行全局快捷键 #3",
"/actions/shortcuts/in/GlobalShortcut04" : "执行全局快捷键 #4",
"/actions/shortcuts/in/GlobalShortcut05" : "执行全局快捷键 #5",
"/actions/shortcuts/in/GlobalShortcut06" : "执行全局快捷键 #6",
"/actions/shortcuts/in/GlobalShortcut07" : "执行全局快捷键 #7",
"/actions/shortcuts/in/GlobalShortcut08" : "执行全局快捷键 #8",
"/actions/shortcuts/in/GlobalShortcut09" : "执行全局快捷键 #9",
"/actions/shortcuts/in/GlobalShortcut10" : "执行全局快捷键 #10",
"/actions/shortcuts/in/GlobalShortcut11" : "执行全局快捷键 #11",
"/actions/shortcuts/in/GlobalShortcut12" : "执行全局快捷键 #12",
"/actions/shortcuts/in/GlobalShortcut13" : "执行全局快捷键 #13",
"/actions/shortcuts/in/GlobalShortcut14" : "执行全局快捷键 #14",
"/actions/shortcuts/in/GlobalShortcut15" : "执行全局快捷键 #15",
"/actions/shortcuts/in/GlobalShortcut16" : "执行全局快捷键 #16",
"/actions/shortcuts/in/GlobalShortcut17" : "执行全局快捷键 #17",
"/actions/shortcuts/in/GlobalShortcut18" : "执行全局快捷键 #18",
"/actions/shortcuts/in/GlobalShortcut19" : "执行全局快捷键 #19",
"/actions/shortcuts/in/GlobalShortcut20" : "执行全局快捷键 #20",
"/actions/laserpointer" : "激光指针",
"/actions/laserpointer/in/LeftClick" : "左键点击",
"/actions/laserpointer/in/RightClick" : "右键点击",
"/actions/laserpointer/in/MiddleClick" : "中键点击",
"/actions/laserpointer/in/Aux01Click" : "辅助1点击",
"/actions/laserpointer/in/Aux02Click" : "辅助2点击",
"/actions/laserpointer/in/Drag" : "拖动叠加",
"/actions/scroll_discrete" : "鼠标滚轮滚动",
"/actions/scroll_discrete/in/ScrollDiscrete" : "滚动叠加",
"/actions/scroll_smooth" : "平滑滚动",
"/actions/scroll_smooth/in/ScrollSmooth" : "滚动叠加"
}
================================================
FILE: assets/keyboards/azerty_be.ini
================================================
[LayoutInfo]
Name=AZERTY (Belgium)
Author=
HasAltGr=true
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_Base_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Base_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Base_Row_2_ID_0]
Type=String
Label=²
String=²
[Key_Base_Row_2_ID_1]
Type=String
Label=&
String=&
[Key_Base_Row_2_ID_2]
Type=String
Label=é
String=é
[Key_Base_Row_2_ID_3]
Type=String
Label="
String="
[Key_Base_Row_2_ID_4]
Type=String
Label='
String='
[Key_Base_Row_2_ID_5]
Type=String
Label=(
String=(
[Key_Base_Row_2_ID_6]
Type=String
Label=§
String=§
[Key_Base_Row_2_ID_7]
Type=String
Label=è
String=è
[Key_Base_Row_2_ID_8]
Type=String
Label=!
String=!
[Key_Base_Row_2_ID_9]
Type=String
Label=ç
String=ç
[Key_Base_Row_2_ID_10]
Type=String
Label=à
String=à
[Key_Base_Row_2_ID_11]
Type=String
Label=)
String=)
[Key_Base_Row_2_ID_12]
Type=String
Label=-
String=-
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Base_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=a
KeyCode=65
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=z
KeyCode=90
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=y
KeyCode=89
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=String
Label=^
String=^
[Key_Base_Row_3_ID_12]
Type=String
Label=$
String=$
[Key_Base_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=q
KeyCode=81
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=VirtualKey
Label=m
KeyCode=77
[Key_Base_Row_4_ID_11]
Type=String
Label=ù
String=ù
[Key_Base_Row_4_ID_12]
Type=String
Label=µ
String=µ
[Key_Base_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Base_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Base_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=String
Label=<
String=<
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=w
KeyCode=87
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=x
KeyCode=88
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=c
KeyCode=67
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=v
KeyCode=86
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=b
KeyCode=66
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=n
KeyCode=78
[Key_Base_Row_5_ID_8]
Type=String
Label=,
String=,
[Key_Base_Row_5_ID_9]
Type=String
Label=;
String=;
[Key_Base_Row_5_ID_10]
Type=String
Label=:
String=:
[Key_Base_Row_5_ID_11]
Type=String
Label==
String==
[Key_Base_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Base_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Base_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Base_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=🡰
Cluster=Extra
KeyCode=166
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=🡲
Cluster=Extra
KeyCode=167
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=🔇
Cluster=Extra
KeyCode=173
NoRepeat=true
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Shift_Row_2_ID_0]
Type=String
Label=³
String=³
NoRepeat=true
[Key_Shift_Row_2_ID_1]
Type=VirtualKey
Label=1
KeyCode=49
[Key_Shift_Row_2_ID_2]
Type=VirtualKey
Label=2
KeyCode=50
[Key_Shift_Row_2_ID_3]
Type=VirtualKey
Label=3
KeyCode=51
[Key_Shift_Row_2_ID_4]
Type=VirtualKey
Label=4
KeyCode=52
[Key_Shift_Row_2_ID_5]
Type=VirtualKey
Label=5
KeyCode=53
[Key_Shift_Row_2_ID_6]
Type=VirtualKey
Label=6
KeyCode=54
[Key_Shift_Row_2_ID_7]
Type=VirtualKey
Label=7
KeyCode=55
[Key_Shift_Row_2_ID_8]
Type=VirtualKey
Label=8
KeyCode=56
[Key_Shift_Row_2_ID_9]
Type=VirtualKey
Label=9
KeyCode=57
[Key_Shift_Row_2_ID_10]
Type=VirtualKey
Label=0
KeyCode=48
[Key_Shift_Row_2_ID_11]
Type=String
Label=°
String=°
[Key_Shift_Row_2_ID_12]
Type=String
Label=_
String=_
[Key_Shift_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Shift_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=A
KeyCode=65
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=Z
KeyCode=90
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Y
KeyCode=89
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=String
Label=¨
String=¨
[Key_Shift_Row_3_ID_12]
Type=String
Label=*
String=*
[Key_Shift_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=Q
KeyCode=81
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=VirtualKey
Label=M
KeyCode=77
[Key_Shift_Row_4_ID_11]
Type=String
Label=%
String=%
[Key_Shift_Row_4_ID_12]
Type=String
Label=£
String=£
[Key_Shift_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=String
Label=>
String=>
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=W
KeyCode=87
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=X
KeyCode=88
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=C
KeyCode=67
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=V
KeyCode=86
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=B
KeyCode=66
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=N
KeyCode=78
[Key_Shift_Row_5_ID_8]
Type=String
Label=?
String=?
[Key_Shift_Row_5_ID_9]
Type=String
Label=.
String=.
[Key_Shift_Row_5_ID_10]
Type=String
Label=/
String=/
[Key_Shift_Row_5_ID_11]
Type=String
Label=+
String=+
[Key_Shift_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Shift_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Shift_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Shift_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
[Key_AltGr_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_AltGr_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_AltGr_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_AltGr_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_AltGr_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_AltGr_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_AltGr_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_AltGr_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_AltGr_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_AltGr_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_AltGr_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_AltGr_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_AltGr_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_AltGr_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_AltGr_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_AltGr_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_AltGr_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_AltGr_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_AltGr_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_AltGr_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_AltGr_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_AltGr_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_AltGr_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_AltGr_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_AltGr_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_AltGr_Row_2_ID_0]
Type=Blank
[Key_AltGr_Row_2_ID_1]
Type=String
Label=|
String=|
[Key_AltGr_Row_2_ID_2]
Type=String
Label=@
String=@
[Key_AltGr_Row_2_ID_3]
Type=String
Label=#
String=#
[Key_AltGr_Row_2_ID_4]
Type=Blank
Width=200
[Key_AltGr_Row_2_ID_5]
Type=String
Label=^
String=^
[Key_AltGr_Row_2_ID_6]
Type=Blank
Width=200
[Key_AltGr_Row_2_ID_7]
Type=String
Label={
String={
[Key_AltGr_Row_2_ID_8]
Type=String
Label=}
String=}
[Key_AltGr_Row_2_ID_9]
Type=Blank
Width=200
[Key_AltGr_Row_2_ID_10]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_AltGr_Row_2_ID_11]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_2_ID_12]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_AltGr_Row_2_ID_13]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_AltGr_Row_2_ID_14]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_AltGr_Row_2_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_2_ID_16]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_AltGr_Row_2_ID_17]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_AltGr_Row_2_ID_18]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_AltGr_Row_2_ID_19]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_AltGr_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_AltGr_Row_3_ID_1]
Type=Blank
Width=200
[Key_AltGr_Row_3_ID_2]
Type=String
Label=€
String=€
[Key_AltGr_Row_3_ID_3]
Type=Blank
Width=700
[Key_AltGr_Row_3_ID_4]
Type=String
Label=[
String=[
[Key_AltGr_Row_3_ID_5]
Type=String
Label=]
String=]
[Key_AltGr_Row_3_ID_6]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_AltGr_Row_3_ID_7]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_3_ID_8]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_AltGr_Row_3_ID_9]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_AltGr_Row_3_ID_10]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_AltGr_Row_3_ID_11]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_3_ID_12]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_AltGr_Row_3_ID_13]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_AltGr_Row_3_ID_14]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_AltGr_Row_3_ID_15]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_AltGr_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_AltGr_Row_4_ID_1]
Type=Blank
Width=1000
[Key_AltGr_Row_4_ID_2]
Type=String
Label=´
String=´
[Key_AltGr_Row_4_ID_3]
Type=String
Label=`
String=`
[Key_AltGr_Row_4_ID_4]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_AltGr_Row_4_ID_5]
Type=Blank
Width=325
Cluster=Navigation
[Key_AltGr_Row_4_ID_6]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_4_ID_7]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_AltGr_Row_4_ID_8]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_AltGr_Row_4_ID_9]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_AltGr_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_AltGr_Row_5_ID_1]
Type=String
Label=\
String=\
[Key_AltGr_Row_5_ID_2]
Type=Blank
Width=900
[Key_AltGr_Row_5_ID_3]
Type=String
Label=~
String=~
[Key_AltGr_Row_5_ID_4]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_AltGr_Row_5_ID_5]
Type=Blank
Width=125
Cluster=Navigation
[Key_AltGr_Row_5_ID_6]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_AltGr_Row_5_ID_7]
Type=Blank
Cluster=Navigation
[Key_AltGr_Row_5_ID_8]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_5_ID_9]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_AltGr_Row_5_ID_10]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_AltGr_Row_5_ID_11]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_AltGr_Row_5_ID_12]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_AltGr_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_AltGr_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_AltGr_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_AltGr_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_AltGr_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_AltGr_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_AltGr_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_AltGr_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_AltGr_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_AltGr_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_AltGr_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_AltGr_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_AltGr_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/keyboards/azerty_fr.ini
================================================
[LayoutInfo]
Name=AZERTY (France)
Author=
HasAltGr=true
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Échap
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=Impr
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=Défil
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_Base_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Base_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Base_Row_2_ID_0]
Type=String
Label=²
String=²
[Key_Base_Row_2_ID_1]
Type=String
Label=&
String=&
[Key_Base_Row_2_ID_2]
Type=String
Label=é
String=é
[Key_Base_Row_2_ID_3]
Type=String
Label="
String="
[Key_Base_Row_2_ID_4]
Type=String
Label='
String='
[Key_Base_Row_2_ID_5]
Type=String
Label=(
String=(
[Key_Base_Row_2_ID_6]
Type=String
Label=-
String=-
[Key_Base_Row_2_ID_7]
Type=String
Label=è
String=è
[Key_Base_Row_2_ID_8]
Type=String
Label=_
String=_
[Key_Base_Row_2_ID_9]
Type=String
Label=ç
String=ç
[Key_Base_Row_2_ID_10]
Type=String
Label=à
String=à
[Key_Base_Row_2_ID_11]
Type=String
Label=)
String=)
[Key_Base_Row_2_ID_12]
Type=String
Label==
String==
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Base_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_15]
Type=VirtualKey
Label=Inser
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Début
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=Page🠹
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_19]
Type=VirtualKey
Label=Verr\nNum
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=a
KeyCode=65
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=z
KeyCode=90
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=y
KeyCode=89
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=String
Label=^
String=^
[Key_Base_Row_3_ID_12]
Type=String
Label=$
String=$
[Key_Base_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Suppr
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=Fin
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=Page🠻
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=q
KeyCode=81
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=VirtualKey
Label=m
KeyCode=77
[Key_Base_Row_4_ID_11]
Type=String
Label=ù
String=ù
[Key_Base_Row_4_ID_12]
Type=String
Label=*
String=*
[Key_Base_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Base_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Base_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=String
Label=<
String=<
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=w
KeyCode=87
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=x
KeyCode=88
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=c
KeyCode=67
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=v
KeyCode=86
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=b
KeyCode=66
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=n
KeyCode=78
[Key_Base_Row_5_ID_8]
Type=String
Label=,
String=,
[Key_Base_Row_5_ID_9]
Type=String
Label=;
String=;
[Key_Base_Row_5_ID_10]
Type=String
Label=:
String=:
[Key_Base_Row_5_ID_11]
Type=String
Label=!
String=!
[Key_Base_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Base_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Entrée
Cluster=Numpad
KeyCode=13
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Base_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Base_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Échap
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=Impr
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=Défil
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=🡰
Cluster=Extra
KeyCode=166
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=🡲
Cluster=Extra
KeyCode=167
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=🔇
Cluster=Extra
KeyCode=173
NoRepeat=true
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Shift_Row_2_ID_0]
Type=String
Label=²
String=²
[Key_Shift_Row_2_ID_1]
Type=VirtualKey
Label=1
KeyCode=49
[Key_Shift_Row_2_ID_2]
Type=VirtualKey
Label=2
KeyCode=50
[Key_Shift_Row_2_ID_3]
Type=VirtualKey
Label=3
KeyCode=51
[Key_Shift_Row_2_ID_4]
Type=VirtualKey
Label=4
KeyCode=52
[Key_Shift_Row_2_ID_5]
Type=VirtualKey
Label=5
KeyCode=53
[Key_Shift_Row_2_ID_6]
Type=VirtualKey
Label=6
KeyCode=54
[Key_Shift_Row_2_ID_7]
Type=VirtualKey
Label=7
KeyCode=55
[Key_Shift_Row_2_ID_8]
Type=VirtualKey
Label=8
KeyCode=56
[Key_Shift_Row_2_ID_9]
Type=VirtualKey
Label=9
KeyCode=57
[Key_Shift_Row_2_ID_10]
Type=VirtualKey
Label=0
KeyCode=48
[Key_Shift_Row_2_ID_11]
Type=String
Label=°
String=°
[Key_Shift_Row_2_ID_12]
Type=String
Label=+
String=+
[Key_Shift_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Shift_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_15]
Type=VirtualKey
Label=Inser
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Début
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=Page🠹
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_19]
Type=VirtualKey
Label=Verr\nNum
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=A
KeyCode=65
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=Z
KeyCode=90
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Y
KeyCode=89
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=String
Label=¨
String=¨
[Key_Shift_Row_3_ID_12]
Type=String
Label=£
String=£
[Key_Shift_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Suppr
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=Fin
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=Page🠻
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=Q
KeyCode=81
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=VirtualKey
Label=M
KeyCode=77
[Key_Shift_Row_4_ID_11]
Type=String
Label=%
String=%
[Key_Shift_Row_4_ID_12]
Type=String
Label=µ
String=µ
[Key_Shift_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=String
Label=>
String=>
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=W
KeyCode=87
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=X
KeyCode=88
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=C
KeyCode=67
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=V
KeyCode=86
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=B
KeyCode=66
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=N
KeyCode=78
[Key_Shift_Row_5_ID_8]
Type=String
Label=?
String=?
[Key_Shift_Row_5_ID_9]
Type=String
Label=.
String=.
[Key_Shift_Row_5_ID_10]
Type=String
Label=/
String=/
[Key_Shift_Row_5_ID_11]
Type=String
Label=§
String=§
[Key_Shift_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Shift_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Entrée
Cluster=Numpad
KeyCode=13
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Shift_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Shift_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
[Key_AltGr_Row_0_ID_0]
Type=VirtualKey
Label=Échap
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_AltGr_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_AltGr_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_AltGr_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_AltGr_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_AltGr_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_AltGr_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_AltGr_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_AltGr_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_AltGr_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_AltGr_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_AltGr_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_AltGr_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_AltGr_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_AltGr_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_AltGr_Row_0_ID_17]
Type=VirtualKey
Label=Impr
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_AltGr_Row_0_ID_18]
Type=VirtualKey
Label=Défil
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_AltGr_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_AltGr_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_AltGr_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_AltGr_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_AltGr_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_AltGr_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_AltGr_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_AltGr_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_AltGr_Row_2_ID_0]
Type=Blank
Width=200
[Key_AltGr_Row_2_ID_1]
Type=String
Label=~
String=~
[Key_AltGr_Row_2_ID_2]
Type=String
Label=#
String=#
[Key_AltGr_Row_2_ID_3]
Type=String
Label={
String={
[Key_AltGr_Row_2_ID_4]
Type=String
Label=[
String=[
[Key_AltGr_Row_2_ID_5]
Type=String
Label=|
String=|
[Key_AltGr_Row_2_ID_6]
Type=String
Label=`
String=`
[Key_AltGr_Row_2_ID_7]
Type=String
Label=\
String=\
[Key_AltGr_Row_2_ID_8]
Type=String
Label=^
String=^
[Key_AltGr_Row_2_ID_9]
Type=String
Label=@
String=@
[Key_AltGr_Row_2_ID_10]
Type=String
Label=]
String=]
[Key_AltGr_Row_2_ID_11]
Type=String
Label=}
String=}
[Key_AltGr_Row_2_ID_12]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_AltGr_Row_2_ID_13]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_2_ID_14]
Type=VirtualKey
Label=Inser
Cluster=Navigation
KeyCode=45
[Key_AltGr_Row_2_ID_15]
Type=VirtualKey
Label=Début
Cluster=Navigation
KeyCode=36
[Key_AltGr_Row_2_ID_16]
Type=VirtualKey
Label=Page🠹
Cluster=Navigation
KeyCode=33
[Key_AltGr_Row_2_ID_17]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_2_ID_18]
Type=VirtualKey
Label=Verr\nNum
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_AltGr_Row_2_ID_19]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_AltGr_Row_2_ID_20]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_AltGr_Row_2_ID_21]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_AltGr_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_AltGr_Row_3_ID_1]
Type=Blank
Width=200
[Key_AltGr_Row_3_ID_2]
Type=String
Label=€
String=€
[Key_AltGr_Row_3_ID_3]
Type=Blank
Width=800
[Key_AltGr_Row_3_ID_4]
Type=String
Label=¤
String=¤
[Key_AltGr_Row_3_ID_5]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_AltGr_Row_3_ID_6]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_3_ID_7]
Type=VirtualKey
Label=Suppr
Cluster=Navigation
KeyCode=46
[Key_AltGr_Row_3_ID_8]
Type=VirtualKey
Label=Fin
Cluster=Navigation
KeyCode=35
[Key_AltGr_Row_3_ID_9]
Type=VirtualKey
Label=Page🠻
Cluster=Navigation
KeyCode=34
[Key_AltGr_Row_3_ID_10]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_3_ID_11]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_AltGr_Row_3_ID_12]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_AltGr_Row_3_ID_13]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_AltGr_Row_3_ID_14]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_AltGr_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_AltGr_Row_4_ID_1]
Type=Blank
Width=1200
[Key_AltGr_Row_4_ID_2]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_AltGr_Row_4_ID_3]
Type=Blank
Width=325
Cluster=Navigation
[Key_AltGr_Row_4_ID_4]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_4_ID_5]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_AltGr_Row_4_ID_6]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_AltGr_Row_4_ID_7]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_AltGr_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_AltGr_Row_5_ID_1]
Type=Blank
Width=1100
[Key_AltGr_Row_5_ID_2]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_AltGr_Row_5_ID_3]
Type=Blank
Width=125
Cluster=Navigation
[Key_AltGr_Row_5_ID_4]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_AltGr_Row_5_ID_5]
Type=Blank
Cluster=Navigation
[Key_AltGr_Row_5_ID_6]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_5_ID_7]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_AltGr_Row_5_ID_8]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_AltGr_Row_5_ID_9]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_AltGr_Row_5_ID_10]
Type=VirtualKey
Height=200
Label=Entrée
Cluster=Numpad
KeyCode=13
[Key_AltGr_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_AltGr_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_AltGr_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_AltGr_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_AltGr_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_AltGr_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_AltGr_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_AltGr_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_AltGr_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_AltGr_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_AltGr_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_AltGr_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_AltGr_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/keyboards/qwerty_dk.ini
================================================
[LayoutInfo]
Name=QWERTY (Denmark)
Author=BOTAlex
HasAltGr=true
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_Base_Row_1_ID_0]
Type=Blank
Width=1500
Height=25
Cluster=Function
[Key_Base_Row_2_ID_0]
Type=String
Label=½
String=½
[Key_Base_Row_2_ID_1]
Type=VirtualKey
Label=1
KeyCode=49
[Key_Base_Row_2_ID_2]
Type=VirtualKey
Label=2
KeyCode=50
[Key_Base_Row_2_ID_3]
Type=VirtualKey
Label=3
KeyCode=51
[Key_Base_Row_2_ID_4]
Type=VirtualKey
Label=4
KeyCode=52
[Key_Base_Row_2_ID_5]
Type=VirtualKey
Label=5
KeyCode=53
[Key_Base_Row_2_ID_6]
Type=VirtualKey
Label=6
KeyCode=54
[Key_Base_Row_2_ID_7]
Type=VirtualKey
Label=7
KeyCode=55
[Key_Base_Row_2_ID_8]
Type=VirtualKey
Label=8
KeyCode=56
[Key_Base_Row_2_ID_9]
Type=VirtualKey
Label=9
KeyCode=57
[Key_Base_Row_2_ID_10]
Type=VirtualKey
Label=0
KeyCode=48
[Key_Base_Row_2_ID_11]
Type=VirtualKey
Label=+
KeyCode=187
[Key_Base_Row_2_ID_12]
Type=String
Label=´
String=´
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=Backspace
KeyCode=8
[Key_Base_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=Tab
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=q
KeyCode=81
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=w
KeyCode=87
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=y
KeyCode=89
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=String
Label=å
String=å
[Key_Base_Row_3_ID_12]
Type=String
Label=¨
String=¨
[Key_Base_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=Enter
KeyCode=13
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Caps Lock
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=a
KeyCode=65
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=String
Label=æ
String=æ
[Key_Base_Row_4_ID_11]
Type=String
Label=ø
String=ø
[Key_Base_Row_4_ID_12]
Type=String
Label='
String='
[Key_Base_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=Enter
KeyCode=13
[Key_Base_Row_4_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_4_ID_15]
Type=Blank
Width=300
Cluster=Navigation
[Key_Base_Row_4_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_18]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_19]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Shift
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=VirtualKey
Label=<
KeyCode=226
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=z
KeyCode=90
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=x
KeyCode=88
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=c
KeyCode=67
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=v
KeyCode=86
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=b
KeyCode=66
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=n
KeyCode=78
[Key_Base_Row_5_ID_8]
Type=VirtualKey
Label=m
KeyCode=77
[Key_Base_Row_5_ID_9]
Type=String
Label=,
String=,
[Key_Base_Row_5_ID_10]
Type=String
Label=.
String=.
[Key_Base_Row_5_ID_11]
Type=String
Label=-
String=-
[Key_Base_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=Shift
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Base_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Base_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Base_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=,
Cluster=Numpad
KeyCode=110
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=🡰
Cluster=Extra
KeyCode=166
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=🡲
Cluster=Extra
KeyCode=167
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=🔇
Cluster=Extra
KeyCode=173
NoRepeat=true
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Shift_Row_2_ID_0]
Type=String
Label=§
String=§
[Key_Shift_Row_2_ID_1]
Type=String
Label=!
String=!
[Key_Shift_Row_2_ID_2]
Type=String
Label="
String="
[Key_Shift_Row_2_ID_3]
Type=String
Label=#
String=#
[Key_Shift_Row_2_ID_4]
Type=String
Label=¤
String=¤
[Key_Shift_Row_2_ID_5]
Type=String
Label=%
String=%
[Key_Shift_Row_2_ID_6]
Type=String
Label=&
String=&
[Key_Shift_Row_2_ID_7]
Type=String
Label=/
String=/
[Key_Shift_Row_2_ID_8]
Type=String
Label=(
String=(
[Key_Shift_Row_2_ID_9]
Type=String
Label=)
String=)
[Key_Shift_Row_2_ID_10]
Type=String
Label==
String==
[Key_Shift_Row_2_ID_11]
Type=String
Label=?
String=?
[Key_Shift_Row_2_ID_12]
Type=String
Label=`
String=`
[Key_Shift_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=Backspace
KeyCode=8
[Key_Shift_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=Tab
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=Q
KeyCode=81
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=W
KeyCode=87
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Y
KeyCode=89
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=String
Label=Å
String=Å
[Key_Shift_Row_3_ID_12]
Type=String
Label=^
String=^
[Key_Shift_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=Enter
KeyCode=13
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Caps Lock
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=A
KeyCode=65
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=String
Label=Æ
String=Æ
[Key_Shift_Row_4_ID_11]
Type=String
Label=Ø
String=Ø
[Key_Shift_Row_4_ID_12]
Type=String
Label=*
String=*
[Key_Shift_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=Enter
KeyCode=13
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Shift
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=VirtualKey
Label=>
KeyCode=226
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=Z
KeyCode=90
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=X
KeyCode=88
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=C
KeyCode=67
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=V
KeyCode=86
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=B
KeyCode=66
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=N
KeyCode=78
[Key_Shift_Row_5_ID_8]
Type=VirtualKey
Label=M
KeyCode=77
[Key_Shift_Row_5_ID_9]
Type=String
Label=;
String=;
[Key_Shift_Row_5_ID_10]
Type=String
Label=:
String=:
[Key_Shift_Row_5_ID_11]
Type=String
Label=_
String=_
[Key_Shift_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=Shift
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Shift_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Shift_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Shift_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=,
Cluster=Numpad
KeyCode=110
[Key_AltGr_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_AltGr_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_AltGr_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_AltGr_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_AltGr_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_AltGr_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_AltGr_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_AltGr_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_AltGr_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_AltGr_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_AltGr_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_AltGr_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_AltGr_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_AltGr_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_AltGr_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_AltGr_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_AltGr_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_AltGr_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_AltGr_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_AltGr_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_AltGr_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_AltGr_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_AltGr_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_AltGr_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_AltGr_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_AltGr_Row_2_ID_0]
Type=String
Label=
String=
[Key_AltGr_Row_2_ID_1]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_2_ID_2]
Type=String
Label=@
String=@
[Key_AltGr_Row_2_ID_3]
Type=String
Label=£
String=£
[Key_AltGr_Row_2_ID_4]
Type=String
Label=$
String=$
[Key_AltGr_Row_2_ID_5]
Type=String
Label=€
String=€
[Key_AltGr_Row_2_ID_6]
Type=String
Label=
String=
[Key_AltGr_Row_2_ID_7]
Type=String
Label={
String={
[Key_AltGr_Row_2_ID_8]
Type=String
Label=[
String=[
[Key_AltGr_Row_2_ID_9]
Type=String
Label=]
String=]
[Key_AltGr_Row_2_ID_10]
Type=String
Label=}
String=}
[Key_AltGr_Row_2_ID_11]
Type=String
Label=
String=
[Key_AltGr_Row_2_ID_12]
Type=String
Label=|
String=|
[Key_AltGr_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=Backspace
KeyCode=8
[Key_AltGr_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_AltGr_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_AltGr_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_AltGr_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_AltGr_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_AltGr_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_AltGr_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_AltGr_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=Tab
KeyCode=9
[Key_AltGr_Row_3_ID_1]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_3_ID_2]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_3_ID_3]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_3_ID_4]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_3_ID_5]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_3_ID_6]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_3_ID_7]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_3_ID_8]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_3_ID_9]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_3_ID_10]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_3_ID_11]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_3_ID_12]
Type=String
Label=~
String=~
[Key_AltGr_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=Enter
KeyCode=13
[Key_AltGr_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_AltGr_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_AltGr_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_AltGr_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_AltGr_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=56
[Key_AltGr_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_AltGr_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_AltGr_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Caps Lock
KeyCode=20
NoRepeat=true
[Key_AltGr_Row_4_ID_1]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_4_ID_2]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_4_ID_3]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_4_ID_4]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_4_ID_5]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_4_ID_6]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_4_ID_7]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_4_ID_8]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_4_ID_9]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_4_ID_10]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_4_ID_11]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_4_ID_12]
Type=String
Label=
String=
[Key_AltGr_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=Enter
KeyCode=13
[Key_AltGr_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_AltGr_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_AltGr_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_AltGr_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_AltGr_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Shift
KeyCode=160
NoRepeat=true
[Key_AltGr_Row_5_ID_1]
Type=String
Label=\
String=\
[Key_AltGr_Row_5_ID_2]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_5_ID_3]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_5_ID_4]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_5_ID_5]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_5_ID_6]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_5_ID_7]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_5_ID_8]
Type=VirtualKey
Label=
KeyCode=0
[Key_AltGr_Row_5_ID_9]
Type=String
Label=
String=
[Key_AltGr_Row_5_ID_10]
Type=String
Label=
String=
[Key_AltGr_Row_5_ID_11]
Type=String
Label=
String=
[Key_AltGr_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=Shift
KeyCode=161
NoRepeat=true
[Key_AltGr_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_AltGr_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_AltGr_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_AltGr_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_AltGr_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_AltGr_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_AltGr_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_AltGr_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_AltGr_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_AltGr_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_AltGr_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_AltGr_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_AltGr_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_AltGr_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_AltGr_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_AltGr_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_AltGr_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_AltGr_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_AltGr_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_AltGr_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/keyboards/qwerty_es.ini
================================================
[LayoutInfo]
Name=QWERTY (Spain)
Author=
HasAltGr=true
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=Impr\nPant
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=Bloq\nDespl
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pausa
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_Base_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Base_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Base_Row_2_ID_0]
Type=String
Label=º
String=º
[Key_Base_Row_2_ID_1]
Type=VirtualKey
Label=1
KeyCode=49
[Key_Base_Row_2_ID_2]
Type=VirtualKey
Label=2
KeyCode=50
[Key_Base_Row_2_ID_3]
Type=VirtualKey
Label=3
KeyCode=51
[Key_Base_Row_2_ID_4]
Type=VirtualKey
Label=4
KeyCode=52
[Key_Base_Row_2_ID_5]
Type=VirtualKey
Label=5
KeyCode=53
[Key_Base_Row_2_ID_6]
Type=VirtualKey
Label=6
KeyCode=54
[Key_Base_Row_2_ID_7]
Type=VirtualKey
Label=7
KeyCode=55
[Key_Base_Row_2_ID_8]
Type=VirtualKey
Label=8
KeyCode=56
[Key_Base_Row_2_ID_9]
Type=VirtualKey
Label=9
KeyCode=57
[Key_Base_Row_2_ID_10]
Type=VirtualKey
Label=0
KeyCode=48
[Key_Base_Row_2_ID_11]
Type=String
Label='
String='
[Key_Base_Row_2_ID_12]
Type=String
Label=¡
String=¡
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Base_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Incio
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=RePág
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_19]
Type=VirtualKey
Label=Bloq\nNum
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=q
KeyCode=81
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=w
KeyCode=87
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=y
KeyCode=89
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=String
Label=`
String=`
[Key_Base_Row_3_ID_12]
Type=String
Label=+
String=+
[Key_Base_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Supr
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=Fin
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=AvPág
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Bloq Mayús
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=a
KeyCode=65
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=String
Label=ñ
String=ñ
[Key_Base_Row_4_ID_11]
Type=String
Label=´
String=´
[Key_Base_Row_4_ID_12]
Type=String
Label=ç
String=ç
[Key_Base_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Base_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Base_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=String
Label=<
String=<
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=z
KeyCode=90
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=x
KeyCode=88
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=c
KeyCode=67
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=v
KeyCode=86
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=b
KeyCode=66
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=n
KeyCode=78
[Key_Base_Row_5_ID_8]
Type=VirtualKey
Label=m
KeyCode=77
[Key_Base_Row_5_ID_9]
Type=String
Label=,
String=,
[Key_Base_Row_5_ID_10]
Type=String
Label=.
String=.
[Key_Base_Row_5_ID_11]
Type=String
Label=-
String=-
[Key_Base_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Base_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Intro
Cluster=Numpad
KeyCode=13
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Base_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Base_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=Impr\nPant
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=Bloq\nDespl
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pausa
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=🡰
Cluster=Extra
KeyCode=166
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=🡲
Cluster=Extra
KeyCode=167
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=🔇
Cluster=Extra
KeyCode=173
NoRepeat=true
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Shift_Row_2_ID_0]
Type=String
Label=ª
String=ª
[Key_Shift_Row_2_ID_1]
Type=String
Label=!
String=!
[Key_Shift_Row_2_ID_2]
Type=String
Label="
String="
[Key_Shift_Row_2_ID_3]
Type=String
Label=·
String=·
[Key_Shift_Row_2_ID_4]
Type=String
Label=$
String=$
[Key_Shift_Row_2_ID_5]
Type=String
Label=%
String=%
[Key_Shift_Row_2_ID_6]
Type=String
Label=&
String=&
[Key_Shift_Row_2_ID_7]
Type=String
Label=/
String=/
[Key_Shift_Row_2_ID_8]
Type=String
Label=(
String=(
[Key_Shift_Row_2_ID_9]
Type=String
Label=)
String=)
[Key_Shift_Row_2_ID_10]
Type=String
Label==
String==
[Key_Shift_Row_2_ID_11]
Type=String
Label=?
String=?
[Key_Shift_Row_2_ID_12]
Type=String
Label=¿
String=¿
[Key_Shift_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Shift_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Incio
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=RePág
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_19]
Type=VirtualKey
Label=Bloq\nNum
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=Q
KeyCode=81
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=W
KeyCode=87
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Y
KeyCode=89
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=String
Label=^
String=^
[Key_Shift_Row_3_ID_12]
Type=String
Label=*
String=*
[Key_Shift_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Supr
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=Fin
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=AvPág
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Bloq Mayús
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=A
KeyCode=65
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=String
Label=Ñ
String=Ñ
[Key_Shift_Row_4_ID_11]
Type=String
Label=¨
String=¨
[Key_Shift_Row_4_ID_12]
Type=String
Label=Ç
String=Ç
[Key_Shift_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=String
Label=>
String=>
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=Z
KeyCode=90
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=X
KeyCode=88
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=C
KeyCode=67
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=V
KeyCode=86
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=B
KeyCode=66
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=N
KeyCode=78
[Key_Shift_Row_5_ID_8]
Type=VirtualKey
Label=M
KeyCode=77
[Key_Shift_Row_5_ID_9]
Type=String
Label=;
String=;
[Key_Shift_Row_5_ID_10]
Type=String
Label=:
String=:
[Key_Shift_Row_5_ID_11]
Type=String
Label=_
String=_
[Key_Shift_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Shift_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Intro
Cluster=Numpad
KeyCode=13
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Shift_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Shift_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
[Key_AltGr_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_AltGr_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_AltGr_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_AltGr_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_AltGr_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_AltGr_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_AltGr_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_AltGr_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_AltGr_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_AltGr_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_AltGr_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_AltGr_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_AltGr_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_AltGr_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_AltGr_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_AltGr_Row_0_ID_17]
Type=VirtualKey
Label=Impr\nPant
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_AltGr_Row_0_ID_18]
Type=VirtualKey
Label=Bloq\nDespl
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_AltGr_Row_0_ID_19]
Type=VirtualKey
Label=Pausa
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_AltGr_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_AltGr_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_AltGr_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_AltGr_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_AltGr_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_AltGr_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_AltGr_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_AltGr_Row_2_ID_0]
Type=String
Label=\
String=\
[Key_AltGr_Row_2_ID_1]
Type=String
Label=|
String=|
[Key_AltGr_Row_2_ID_2]
Type=String
Label=@
String=@
[Key_AltGr_Row_2_ID_3]
Type=String
Label=#
String=#
[Key_AltGr_Row_2_ID_4]
Type=String
Label=~
String=~
[Key_AltGr_Row_2_ID_5]
Type=Blank
[Key_AltGr_Row_2_ID_6]
Type=String
Label=¬
String=¬
[Key_AltGr_Row_2_ID_7]
Type=Blank
Width=600
[Key_AltGr_Row_2_ID_8]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_AltGr_Row_2_ID_9]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_2_ID_10]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_AltGr_Row_2_ID_11]
Type=VirtualKey
Label=Incio
Cluster=Navigation
KeyCode=36
[Key_AltGr_Row_2_ID_12]
Type=VirtualKey
Label=RePág
Cluster=Navigation
KeyCode=33
[Key_AltGr_Row_2_ID_13]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_2_ID_14]
Type=VirtualKey
Label=Bloq\nNum
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_AltGr_Row_2_ID_15]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_AltGr_Row_2_ID_16]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_AltGr_Row_2_ID_17]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_AltGr_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_AltGr_Row_3_ID_1]
Type=Blank
Width=200
[Key_AltGr_Row_3_ID_2]
Type=String
Label=€
String=€
[Key_AltGr_Row_3_ID_3]
Type=Blank
Width=700
[Key_AltGr_Row_3_ID_4]
Type=String
Label=[
String=[
[Key_AltGr_Row_3_ID_5]
Type=String
Label=]
String=]
[Key_AltGr_Row_3_ID_6]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_AltGr_Row_3_ID_7]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_3_ID_8]
Type=VirtualKey
Label=Supr
Cluster=Navigation
KeyCode=46
[Key_AltGr_Row_3_ID_9]
Type=VirtualKey
Label=Fin
Cluster=Navigation
KeyCode=35
[Key_AltGr_Row_3_ID_10]
Type=VirtualKey
Label=AvPág
Cluster=Navigation
KeyCode=34
[Key_AltGr_Row_3_ID_11]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_3_ID_12]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_AltGr_Row_3_ID_13]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_AltGr_Row_3_ID_14]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_AltGr_Row_3_ID_15]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_AltGr_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Bloq Mayús
KeyCode=20
NoRepeat=true
[Key_AltGr_Row_4_ID_1]
Type=Blank
Width=1000
[Key_AltGr_Row_4_ID_2]
Type=String
Label={
String={
[Key_AltGr_Row_4_ID_3]
Type=String
Label=}
String=}
[Key_AltGr_Row_4_ID_4]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_AltGr_Row_4_ID_5]
Type=Blank
Width=325
Cluster=Navigation
[Key_AltGr_Row_4_ID_6]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_4_ID_7]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_AltGr_Row_4_ID_8]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_AltGr_Row_4_ID_9]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_AltGr_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_AltGr_Row_5_ID_1]
Type=Blank
Width=1100
[Key_AltGr_Row_5_ID_2]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_AltGr_Row_5_ID_3]
Type=Blank
Width=125
Cluster=Navigation
[Key_AltGr_Row_5_ID_4]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_AltGr_Row_5_ID_5]
Type=Blank
Cluster=Navigation
[Key_AltGr_Row_5_ID_6]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_5_ID_7]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_AltGr_Row_5_ID_8]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_AltGr_Row_5_ID_9]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_AltGr_Row_5_ID_10]
Type=VirtualKey
Height=200
Label=Intro
Cluster=Numpad
KeyCode=13
[Key_AltGr_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_AltGr_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_AltGr_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_AltGr_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_AltGr_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_AltGr_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_AltGr_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_AltGr_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_AltGr_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_AltGr_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_AltGr_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_AltGr_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_AltGr_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/keyboards/qwerty_ja.ini
================================================
[LayoutInfo]
Name=QWERTY (Japanese JIS-set)
Author=Shimotuki Rieru
HasAltGr=false
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_Base_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Base_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Base_Row_2_ID_0]
Type=VirtualKey
Label=半角/##L\n全角##L\n漢字##L
KeyCode=25
[Key_Base_Row_2_ID_1]
Type=VirtualKey
Label=1##L\nぬ##R
KeyCode=49
[Key_Base_Row_2_ID_2]
Type=VirtualKey
Label=2##L\nふ##R
KeyCode=50
[Key_Base_Row_2_ID_3]
Type=VirtualKey
Label=3##L\nあ##R
KeyCode=51
[Key_Base_Row_2_ID_4]
Type=VirtualKey
Label=4##L\nう##R
KeyCode=52
[Key_Base_Row_2_ID_5]
Type=VirtualKey
Label=5##L\nえ##R
KeyCode=53
[Key_Base_Row_2_ID_6]
Type=VirtualKey
Label=6##L\nお##R
KeyCode=54
[Key_Base_Row_2_ID_7]
Type=VirtualKey
Label=7##L\nや##R
KeyCode=55
[Key_Base_Row_2_ID_8]
Type=VirtualKey
Label=8##L\nゆ##R
KeyCode=56
[Key_Base_Row_2_ID_9]
Type=VirtualKey
Label=9##L\nよ##R
KeyCode=57
[Key_Base_Row_2_ID_10]
Type=VirtualKey
Label=0##L\nわ##R
KeyCode=48
[Key_Base_Row_2_ID_11]
Type=String
Label=-##L\nほ##R
String=-
[Key_Base_Row_2_ID_12]
Type=String
Label=^##L\nへ##R
String=^
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Label=¥##L\n
KeyCode=220
[Key_Base_Row_2_ID_14]
Type=VirtualKey
Label=Back\nspace
KeyCode=8
[Key_Base_Row_2_ID_15]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_18]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_19]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_23]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=Tab
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=q##L\nた##R
KeyCode=81
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=w##L\nて##R
KeyCode=87
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e##L\nい##R
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r##L\nす##R
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t##L\nか##R
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=y##L\nん##R
KeyCode=89
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u##L\nな##R
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i##L\nに##R
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o##L\nら##R
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p##L\nせ##R
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=String
Label=@##L\n
String=@
[Key_Base_Row_3_ID_12]
Type=String
Label=[ 「##L\n
String=[
[Key_Base_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7\nHome
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8\n↑
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9\nPgUp
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Caps Lock
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=a##L\nち##R
KeyCode=65
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s##L\nと##R
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d##L\nし##R
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f##L\nは##R
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g##L\nき##R
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h##L\nく##R
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j##L\nま##R
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k##L\nの##R
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l##L\nり##R
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=String
Label=;##L\nれ##R
String=;
[Key_Base_Row_4_ID_11]
Type=String
Label=:##L\nけ##R
String=:
[Key_Base_Row_4_ID_12]
Type=String
Label=] 」##L\nむ##R
String=]
[Key_Base_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=Enter
KeyCode=13
[Key_Base_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Base_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_16]
Type=VirtualKey
Label=4\n←
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=5\n
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_18]
Type=VirtualKey
Label=6\n→
Cluster=Numpad
KeyCode=102
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=225
Label=Shift
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=VirtualKey
Label=z##L\nつ##R
KeyCode=90
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=x##L\nさ##R
KeyCode=88
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=c##L\nそ##R
KeyCode=67
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=v##L\nひ##R
KeyCode=86
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=b##L\nこ##R
KeyCode=66
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=n##L\nみ##R
KeyCode=78
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=m##L\nも##R
KeyCode=77
[Key_Base_Row_5_ID_8]
Type=String
Label=, 、##L\nね##R
String=,
[Key_Base_Row_5_ID_9]
Type=String
Label=. 。##L\nる##R
String=.
[Key_Base_Row_5_ID_10]
Type=String
Label=/ ・##L\nめ##R
String=/
[Key_Base_Row_5_ID_11]
Type=String
Label=\##L\nろ##R
String=\
[Key_Base_Row_5_ID_12]
Type=VirtualKeyToggle
Width=175
Label=Shift
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Base_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=1\nEnd
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=2\n↓
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Label=3\nPgDn
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
Type=VirtualKey
Label=無変換
KeyCode=29
[Key_Base_Row_6_ID_4]
Type=VirtualKey
Width=300
Label=
KeyCode=32
[Key_Base_Row_6_ID_5]
Type=VirtualKey
Label=変換
KeyCode=28
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=カタカナ\nひらがな
KeyCode=240
[Key_Base_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=165
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_10]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Base_Row_6_ID_11]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_12]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_16]
Type=VirtualKey
Width=200
Label=0\nInsert
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_17]
Type=VirtualKey
Label=.\nDel
Cluster=Numpad
KeyCode=110
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=🡰
Cluster=Extra
KeyCode=166
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=🡲
Cluster=Extra
KeyCode=167
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=🔇
Cluster=Extra
KeyCode=173
NoRepeat=true
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Shift_Row_2_ID_0]
Type=VirtualKey
Label=半角/##L\n全角##L\n漢字##L
KeyCode=25
[Key_Shift_Row_2_ID_1]
Type=String
Label=!##L\nぬ##R
String=!
[Key_Shift_Row_2_ID_2]
Type=String
Label="##L\nふ##R
String="
[Key_Shift_Row_2_ID_3]
Type=String
Label=# ##L\nあ##R
String=#
[Key_Shift_Row_2_ID_4]
Type=String
Label=$##L\nう##R
String=$
[Key_Shift_Row_2_ID_5]
Type=String
Label=%##L\nえ##R
String=%
[Key_Shift_Row_2_ID_6]
Type=String
Label=#L\nお##R
String=&
[Key_Shift_Row_2_ID_7]
Type=String
Label='##L\nや##R
String='
[Key_Shift_Row_2_ID_8]
Type=String
Label=(##L\nゆ##R
String=(
[Key_Shift_Row_2_ID_9]
Type=String
Label=)##L\nよ##R
String=)
[Key_Shift_Row_2_ID_10]
Type=VirtualKey
Label=##L\nわ##R
KeyCode=48
[Key_Shift_Row_2_ID_11]
Type=String
Label==##L\nほ##R
String==
[Key_Shift_Row_2_ID_12]
Type=String
Label=~##L\nへ##R
String=~
[Key_Shift_Row_2_ID_13]
Type=String
Label=|##L\n
String=|
[Key_Shift_Row_2_ID_14]
Type=VirtualKey
Label=Back\nspace
KeyCode=8
[Key_Shift_Row_2_ID_15]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_18]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_19]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_23]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=Tab
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=Q##L\nた##R
KeyCode=81
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=W##L\nて##R
KeyCode=87
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E##L\nい##R
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R##L\nす##R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T##L\nか##R
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Y##L\nん##R
KeyCode=89
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U##L\nな##R
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I##L\nに##R
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O##L\nら##R
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P##L\nせ##R
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=String
Label=`##L\n
String=`
[Key_Shift_Row_3_ID_12]
Type=String
Label={##L\n
String={
[Key_Shift_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=7\nHome
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=8\n↑
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=9\nPgUp
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Caps Lock
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=A##L\nち##R
KeyCode=65
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S##L\nと##R
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D##L\nし##R
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F##L\nは##R
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G##L\nき##R
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H##L\nく##R
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J##L\nま##R
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K##L\nの##R
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L##L\nり##R
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=String
Label=+##L\nれ##R
String=+
[Key_Shift_Row_4_ID_11]
Type=String
Label=*##L\nけ##R
String=*
[Key_Shift_Row_4_ID_12]
Type=String
Label=}##L\nむ##R
String=}
[Key_Shift_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=Enter
KeyCode=13
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=4\n←
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=5\n
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_18]
Type=VirtualKey
Label=6\n→
Cluster=Numpad
KeyCode=102
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=225
Label=Shift
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=VirtualKey
Label=Z##L\nつ##R
KeyCode=90
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=X##L\nさ##R
KeyCode=88
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=C##L\nそ##R
KeyCode=67
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=V##L\nひ##R
KeyCode=86
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=B##L\nこ##R
KeyCode=66
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=N##L\nみ##R
KeyCode=78
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=M##L\nも##R
KeyCode=77
[Key_Shift_Row_5_ID_8]
Type=String
Label=<##L\nね##R
String=<
[Key_Shift_Row_5_ID_9]
Type=String
Label=>##L\nる##R
String=>
[Key_Shift_Row_5_ID_10]
Type=String
Label=?##L\nめ##R
String=?
[Key_Shift_Row_5_ID_11]
Type=String
Label=_##L\nろ##R
String=_
[Key_Shift_Row_5_ID_12]
Type=VirtualKeyToggle
Width=175
Label=Shift
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Shift_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=1\nEnd
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=2\n↓
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Label=3\nPgDn
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
Type=VirtualKey
Label=無変換
KeyCode=29
[Key_Shift_Row_6_ID_4]
Type=VirtualKey
Width=300
Label=
KeyCode=32
[Key_Shift_Row_6_ID_5]
Type=VirtualKey
Label=変換
KeyCode=28
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=カタカナ\nひらがな
KeyCode=21
[Key_Shift_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=165
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_10]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Shift_Row_6_ID_11]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_12]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_16]
Type=VirtualKey
Width=200
Label=0\nInsert
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_17]
Type=VirtualKey
Label=.\nDel
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/keyboards/qwerty_ko_2-set.ini
================================================
[LayoutInfo]
Name=QWERTY (Korean 2-set)
Author=HisaCat
HasAltGr=false
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_Base_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Base_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Base_Row_2_ID_0]
Type=VirtualKey
Label=`##L\n
KeyCode=192
[Key_Base_Row_2_ID_1]
Type=VirtualKey
Label=1##L\n
KeyCode=49
[Key_Base_Row_2_ID_2]
Type=VirtualKey
Label=2##L\n
KeyCode=50
[Key_Base_Row_2_ID_3]
Type=VirtualKey
Label=3##L\n
KeyCode=51
[Key_Base_Row_2_ID_4]
Type=VirtualKey
Label=4##L\n
KeyCode=52
[Key_Base_Row_2_ID_5]
Type=VirtualKey
Label=5##L\n
KeyCode=53
[Key_Base_Row_2_ID_6]
Type=VirtualKey
Label=6##L\n
KeyCode=54
[Key_Base_Row_2_ID_7]
Type=VirtualKey
Label=7##L\n
KeyCode=55
[Key_Base_Row_2_ID_8]
Type=VirtualKey
Label=8##L\n
KeyCode=56
[Key_Base_Row_2_ID_9]
Type=VirtualKey
Label=9##L\n
KeyCode=57
[Key_Base_Row_2_ID_10]
Type=VirtualKey
Label=0##L\n
KeyCode=48
[Key_Base_Row_2_ID_11]
Type=VirtualKey
Label=-##L\n
KeyCode=189
[Key_Base_Row_2_ID_12]
Type=VirtualKey
Label==##L\n
KeyCode=187
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=Backspace
KeyCode=8
[Key_Base_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_19]
Type=VirtualKey
Label=Num##L\nLock##L
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=/##L\n
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=*##L\n
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=-##L\n
Cluster=Numpad
KeyCode=109
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=Tab
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=q##L\nㅂ##R
KeyCode=81
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=w ##L\nㅈ##R
KeyCode=87
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e##L\nㄷ##R
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r##L\nㄱ##R
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t##L\nㅅ##R
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=y##L\nㅛ##R
KeyCode=89
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u##L\nㅕ##R
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i##L\nㅑ##R
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o##L\nㅐ##R
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p##L\nㅔ##R
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=VirtualKey
Label=[##L\n
KeyCode=219
[Key_Base_Row_3_ID_12]
Type=VirtualKey
Label=]##L\n
KeyCode=221
[Key_Base_Row_3_ID_13]
Type=VirtualKey
Width=150
Label=₩##L\n
KeyCode=220
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7##L\nHome##L
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8##L\n↑##L
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9##L\nPgUp##L
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Caps Lock
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=a##L\nㅁ##R
KeyCode=65
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s##L\nㄴ##R
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d##L\nㅇ##R
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f##L\nㄹ##R
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g##L\nㅎ##R
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h##L\nㅗ##R
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j##L\nㅓ##R
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k##L\nㅏ##R
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l##L\nㅣ##R
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=VirtualKey
Label=;##L\n
KeyCode=186
[Key_Base_Row_4_ID_11]
Type=VirtualKey
Label='##L\n
KeyCode=222
[Key_Base_Row_4_ID_12]
Type=VirtualKey
Width=225
Label=Enter
KeyCode=13
[Key_Base_Row_4_ID_13]
Type=Blank
Width=325
Cluster=Navigation
[Key_Base_Row_4_ID_14]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_15]
Type=VirtualKey
Label=4##L\n←##L
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_16]
Type=VirtualKey
Label=5##L\n
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=6##L\n→##L
Cluster=Numpad
KeyCode=102
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=225
Label=Shift
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=VirtualKey
Label=z##L\nㅋ##R
KeyCode=90
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=x##L\nㅌ##R
KeyCode=88
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=c##L\nㅊ##R
KeyCode=67
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=v##L\nㅍ##R
KeyCode=86
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=b##L\nㅠ##R
KeyCode=66
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=n##L\nㅜ##R
KeyCode=78
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=m##L\nㅡ##R
KeyCode=77
[Key_Base_Row_5_ID_8]
Type=VirtualKey
Label=,##L\n
KeyCode=188
[Key_Base_Row_5_ID_9]
Type=VirtualKey
Label=.##L\n
KeyCode=190
[Key_Base_Row_5_ID_10]
Type=VirtualKey
Label=/##L\n
KeyCode=191
[Key_Base_Row_5_ID_11]
Type=VirtualKeyToggle
Width=275
Label=Shift
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_12]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_13]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_14]
Type=Blank
Cluster=Navigation
[Key_Base_Row_5_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_16]
Type=VirtualKey
Label=1##L\nEnd##L
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=2##L\n↓##L
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=3##L\nPgDn##L
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Base_Row_6_ID_4]
Type=VirtualKey
Width=125
Label=Alt##L\n한/영##L
KeyCode=21
NoRepeat=true
[Key_Base_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_7]
Type=VirtualKey
Width=125
Label=Ctrl##L\n한자##L
KeyCode=25
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0##L\nInsert##L
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=.##L\nDel##L
Cluster=Numpad
KeyCode=110
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Shift_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Shift_Row_2_ID_0]
Type=VirtualKey
Label=~##L\n
KeyCode=192
[Key_Shift_Row_2_ID_1]
Type=VirtualKey
Label=!##L\n
KeyCode=49
[Key_Shift_Row_2_ID_2]
Type=VirtualKey
Label=@##L\n
KeyCode=50
[Key_Shift_Row_2_ID_3]
Type=VirtualKey
Label=# ##L\n
KeyCode=51
[Key_Shift_Row_2_ID_4]
Type=VirtualKey
Label=$##L\n
KeyCode=52
[Key_Shift_Row_2_ID_5]
Type=VirtualKey
Label=%##L\n
KeyCode=53
[Key_Shift_Row_2_ID_6]
Type=VirtualKey
Label=^##L\n
KeyCode=54
[Key_Shift_Row_2_ID_7]
Type=VirtualKey
Label=#L\n
KeyCode=55
[Key_Shift_Row_2_ID_8]
Type=VirtualKey
Label=*##L\n
KeyCode=56
[Key_Shift_Row_2_ID_9]
Type=VirtualKey
Label=(##L\n
KeyCode=57
[Key_Shift_Row_2_ID_10]
Type=VirtualKey
Label=)##L\n
KeyCode=48
[Key_Shift_Row_2_ID_11]
Type=VirtualKey
Label=_##L\n
KeyCode=189
[Key_Shift_Row_2_ID_12]
Type=VirtualKey
Label=+##L\n
KeyCode=187
[Key_Shift_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=Backspace
KeyCode=8
[Key_Shift_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_19]
Type=VirtualKey
Label=Num##L\nLock##L
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=/##L\n
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=*##L\n
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=-##L\n
Cluster=Numpad
KeyCode=109
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=Tab
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=Q##L\nㅃ##R
KeyCode=81
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=W##L\nㅉ##R
KeyCode=87
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E##L\nㄸ##R
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R##L\nㄲ##R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T##L\nㅆ##R
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Y##L\nㅛ##R
KeyCode=89
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U##L\nㅕ##R
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I##L\nㅑ##R
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O##L\nㅒ##R
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P##L\nㅖ##R
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=VirtualKey
Label={##L\n
KeyCode=219
[Key_Shift_Row_3_ID_12]
Type=VirtualKey
Label=}##L\n
KeyCode=221
[Key_Shift_Row_3_ID_13]
Type=VirtualKey
Width=150
Label=|##L\n
KeyCode=220
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=7##L\nHome##L
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=8##L\n↑##L
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=9##L\nPgUp##L
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Caps Lock
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=A##L\nㅁ##R
KeyCode=65
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S##L\nㄴ##R
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D##L\nㅇ##R
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F##L\nㄹ##R
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G##L\nㅎ##R
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H##L\nㅗ##R
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J##L\nㅓ##R
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K##L\nㅏ##R
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L##L\nㅣ##R
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=VirtualKey
Label=:##L\n
KeyCode=186
[Key_Shift_Row_4_ID_11]
Type=VirtualKey
Label="##L\n
KeyCode=222
[Key_Shift_Row_4_ID_12]
Type=VirtualKey
Width=225
Label=Enter
KeyCode=13
[Key_Shift_Row_4_ID_13]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_15]
Type=VirtualKey
Label=4##L\n←##L
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=5##L\n
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=6##L\n→##L
Cluster=Numpad
KeyCode=102
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=225
Label=Shift
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=VirtualKey
Label=Z##L\nㅋ##R
KeyCode=90
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=X##L\nㅌ##R
KeyCode=88
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=C##L\nㅊ##R
KeyCode=67
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=V##L\nㅍ##R
KeyCode=86
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=B##L\nㅠ##R
KeyCode=66
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=N##L\nㅜ##R
KeyCode=78
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=M##L\nㅡ##R
KeyCode=77
[Key_Shift_Row_5_ID_8]
Type=VirtualKey
Label=<##L\n
KeyCode=188
[Key_Shift_Row_5_ID_9]
Type=VirtualKey
Label=>##L\n
KeyCode=190
[Key_Shift_Row_5_ID_10]
Type=VirtualKey
Label=?##L\n
KeyCode=191
[Key_Shift_Row_5_ID_11]
Type=VirtualKeyToggle
Width=275
Label=Shift
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_12]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_13]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_14]
Type=Blank
Cluster=Navigation
[Key_Shift_Row_5_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_16]
Type=VirtualKey
Label=1##L\nEnd##L
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=2##L\n↓##L
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=3##L\nPgDn##L
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Shift_Row_6_ID_4]
Type=VirtualKey
Width=125
Label=Alt##L\n한/영##L
KeyCode=21
NoRepeat=true
[Key_Shift_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_7]
Type=VirtualKey
Width=125
Label=Ctrl##L\n한자##L
KeyCode=25
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0##L\nInsert##L
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=.##L\nDel##L
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/keyboards/qwerty_latam.ini
================================================
[LayoutInfo]
Name=QWERTY (Latin America)
Author=
HasAltGr=true
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=Impr\nPant
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=Bloq\nDespl
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pausa
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_Base_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Base_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Base_Row_2_ID_0]
Type=String
Label=|
String=|
[Key_Base_Row_2_ID_1]
Type=VirtualKey
Label=1
KeyCode=49
[Key_Base_Row_2_ID_2]
Type=VirtualKey
Label=2
KeyCode=50
[Key_Base_Row_2_ID_3]
Type=VirtualKey
Label=3
KeyCode=51
[Key_Base_Row_2_ID_4]
Type=VirtualKey
Label=4
KeyCode=52
[Key_Base_Row_2_ID_5]
Type=VirtualKey
Label=5
KeyCode=53
[Key_Base_Row_2_ID_6]
Type=VirtualKey
Label=6
KeyCode=54
[Key_Base_Row_2_ID_7]
Type=VirtualKey
Label=7
KeyCode=55
[Key_Base_Row_2_ID_8]
Type=VirtualKey
Label=8
KeyCode=56
[Key_Base_Row_2_ID_9]
Type=VirtualKey
Label=9
KeyCode=57
[Key_Base_Row_2_ID_10]
Type=VirtualKey
Label=0
KeyCode=48
[Key_Base_Row_2_ID_11]
Type=String
Label='
String='
[Key_Base_Row_2_ID_12]
Type=String
Label=¿
String=¿
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Base_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Incio
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=RePág
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_19]
Type=VirtualKey
Label=Bloq\nNum
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=q
KeyCode=81
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=w
KeyCode=87
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=y
KeyCode=89
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=String
Label=´
String=´
[Key_Base_Row_3_ID_12]
Type=String
Label=+
String=+
[Key_Base_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Supr
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=Fin
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=AvPág
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Bloq Mayús
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=a
KeyCode=65
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=String
Label=ñ
String=ñ
[Key_Base_Row_4_ID_11]
Type=String
Label={
String={
[Key_Base_Row_4_ID_12]
Type=String
Label=}
String=}
[Key_Base_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Base_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Base_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=String
Label=<
String=<
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=z
KeyCode=90
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=x
KeyCode=88
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=c
KeyCode=67
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=v
KeyCode=86
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=b
KeyCode=66
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=n
KeyCode=78
[Key_Base_Row_5_ID_8]
Type=VirtualKey
Label=m
KeyCode=77
[Key_Base_Row_5_ID_9]
Type=String
Label=,
String=,
[Key_Base_Row_5_ID_10]
Type=String
Label=.
String=.
[Key_Base_Row_5_ID_11]
Type=String
Label=-
String=-
[Key_Base_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Base_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Intro
Cluster=Numpad
KeyCode=13
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Base_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Base_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=Impr\nPant
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=Bloq\nDespl
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pausa
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=🡰
Cluster=Extra
KeyCode=166
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=🡲
Cluster=Extra
KeyCode=167
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=🔇
Cluster=Extra
KeyCode=173
NoRepeat=true
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Shift_Row_2_ID_0]
Type=String
Label=°
String=°
[Key_Shift_Row_2_ID_1]
Type=String
Label=!
String=!
[Key_Shift_Row_2_ID_2]
Type=String
Label="
String="
[Key_Shift_Row_2_ID_3]
Type=String
Label=#
String=#
[Key_Shift_Row_2_ID_4]
Type=String
Label=$
String=$
[Key_Shift_Row_2_ID_5]
Type=String
Label=%
String=%
[Key_Shift_Row_2_ID_6]
Type=String
Label=&
String=&
[Key_Shift_Row_2_ID_7]
Type=String
Label=/
String=/
[Key_Shift_Row_2_ID_8]
Type=String
Label=(
String=(
[Key_Shift_Row_2_ID_9]
Type=String
Label=)
String=)
[Key_Shift_Row_2_ID_10]
Type=String
Label==
String==
[Key_Shift_Row_2_ID_11]
Type=String
Label=?
String=?
[Key_Shift_Row_2_ID_12]
Type=String
Label=¡
String=¡
[Key_Shift_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Shift_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Incio
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=RePág
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_19]
Type=VirtualKey
Label=Bloq\nNum
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=Q
KeyCode=81
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=W
KeyCode=87
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Y
KeyCode=89
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=String
Label=¨
String=¨
[Key_Shift_Row_3_ID_12]
Type=String
Label=*
String=*
[Key_Shift_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Supr
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=Fin
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=AvPág
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Bloq Mayús
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=A
KeyCode=65
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=String
Label=Ñ
String=Ñ
[Key_Shift_Row_4_ID_11]
Type=String
Label=[
String=[
[Key_Shift_Row_4_ID_12]
Type=String
Label=]
String=]
[Key_Shift_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=String
Label=>
String=>
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=Z
KeyCode=90
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=X
KeyCode=88
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=C
KeyCode=67
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=V
KeyCode=86
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=B
KeyCode=66
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=N
KeyCode=78
[Key_Shift_Row_5_ID_8]
Type=VirtualKey
Label=M
KeyCode=77
[Key_Shift_Row_5_ID_9]
Type=String
Label=;
String=;
[Key_Shift_Row_5_ID_10]
Type=String
Label=:
String=:
[Key_Shift_Row_5_ID_11]
Type=String
Label=_
String=_
[Key_Shift_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Shift_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Intro
Cluster=Numpad
KeyCode=13
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Shift_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Shift_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
[Key_AltGr_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_AltGr_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_AltGr_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_AltGr_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_AltGr_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_AltGr_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_AltGr_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_AltGr_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_AltGr_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_AltGr_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_AltGr_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_AltGr_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_AltGr_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_AltGr_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_AltGr_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_AltGr_Row_0_ID_17]
Type=VirtualKey
Label=Impr\nPant
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_AltGr_Row_0_ID_18]
Type=VirtualKey
Label=Bloq\nDespl
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_AltGr_Row_0_ID_19]
Type=VirtualKey
Label=Pausa
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_AltGr_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_AltGr_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_AltGr_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_AltGr_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_AltGr_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_AltGr_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_AltGr_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_AltGr_Row_2_ID_0]
Type=String
Label=¬
String=¬
[Key_AltGr_Row_2_ID_1]
Type=Blank
Width=1000
[Key_AltGr_Row_2_ID_2]
Type=String
Label=\
String=\
[Key_AltGr_Row_2_ID_3]
Type=Blank
[Key_AltGr_Row_2_ID_4]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_AltGr_Row_2_ID_5]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_2_ID_6]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_AltGr_Row_2_ID_7]
Type=VirtualKey
Label=Incio
Cluster=Navigation
KeyCode=36
[Key_AltGr_Row_2_ID_8]
Type=VirtualKey
Label=RePág
Cluster=Navigation
KeyCode=33
[Key_AltGr_Row_2_ID_9]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_2_ID_10]
Type=VirtualKey
Label=Bloq\nNum
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_AltGr_Row_2_ID_11]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_AltGr_Row_2_ID_12]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_AltGr_Row_2_ID_13]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_AltGr_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_AltGr_Row_3_ID_1]
Type=String
Label=@
String=@
[Key_AltGr_Row_3_ID_2]
Type=Blank
Width=1000
[Key_AltGr_Row_3_ID_3]
Type=String
Label=~
String=~
[Key_AltGr_Row_3_ID_4]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_AltGr_Row_3_ID_5]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_3_ID_6]
Type=VirtualKey
Label=Supr
Cluster=Navigation
KeyCode=46
[Key_AltGr_Row_3_ID_7]
Type=VirtualKey
Label=Fin
Cluster=Navigation
KeyCode=35
[Key_AltGr_Row_3_ID_8]
Type=VirtualKey
Label=AvPág
Cluster=Navigation
KeyCode=34
[Key_AltGr_Row_3_ID_9]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_3_ID_10]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_AltGr_Row_3_ID_11]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_AltGr_Row_3_ID_12]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_AltGr_Row_3_ID_13]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_AltGr_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Bloq Mayús
KeyCode=20
NoRepeat=true
[Key_AltGr_Row_4_ID_1]
Type=Blank
Width=1000
[Key_AltGr_Row_4_ID_2]
Type=String
Label=^
String=^
[Key_AltGr_Row_4_ID_3]
Type=String
Label=`
String=`
[Key_AltGr_Row_4_ID_4]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_AltGr_Row_4_ID_5]
Type=Blank
Width=325
Cluster=Navigation
[Key_AltGr_Row_4_ID_6]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_4_ID_7]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_AltGr_Row_4_ID_8]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_AltGr_Row_4_ID_9]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_AltGr_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_AltGr_Row_5_ID_1]
Type=Blank
Width=1100
[Key_AltGr_Row_5_ID_2]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_AltGr_Row_5_ID_3]
Type=Blank
Width=125
Cluster=Navigation
[Key_AltGr_Row_5_ID_4]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_AltGr_Row_5_ID_5]
Type=Blank
Cluster=Navigation
[Key_AltGr_Row_5_ID_6]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_5_ID_7]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_AltGr_Row_5_ID_8]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_AltGr_Row_5_ID_9]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_AltGr_Row_5_ID_10]
Type=VirtualKey
Height=200
Label=Intro
Cluster=Numpad
KeyCode=13
[Key_AltGr_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_AltGr_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_AltGr_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_AltGr_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_AltGr_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_AltGr_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_AltGr_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_AltGr_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_AltGr_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_AltGr_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_AltGr_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_AltGr_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_AltGr_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/keyboards/qwerty_th_kedmanee.ini
================================================
[LayoutInfo]
Name=QWERTY (Thai Kedmanee)
Author=TangMo
HasAltGr=false
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_Base_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Base_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Base_Row_2_ID_0]
Type=VirtualKey
Label=`##L\n_##R
KeyCode=192
[Key_Base_Row_2_ID_1]
Type=VirtualKey
Label=1##L\nๅ##R
KeyCode=49
[Key_Base_Row_2_ID_2]
Type=VirtualKey
Label=2##L\n/##R
KeyCode=50
[Key_Base_Row_2_ID_3]
Type=VirtualKey
Label=3##L\n-##R
KeyCode=51
[Key_Base_Row_2_ID_4]
Type=VirtualKey
Label=4##L\nภ##R
KeyCode=52
[Key_Base_Row_2_ID_5]
Type=VirtualKey
Label=5##L\nถ##R
KeyCode=53
[Key_Base_Row_2_ID_6]
Type=VirtualKey
Label=6##L\nุ##R
KeyCode=54
[Key_Base_Row_2_ID_7]
Type=VirtualKey
Label=7##L\nึ##R
KeyCode=55
[Key_Base_Row_2_ID_8]
Type=VirtualKey
Label=8##L\nค##R
KeyCode=56
[Key_Base_Row_2_ID_9]
Type=VirtualKey
Label=9##L\nต##R
KeyCode=57
[Key_Base_Row_2_ID_10]
Type=VirtualKey
Label=0##L\nจ##R
KeyCode=48
[Key_Base_Row_2_ID_11]
Type=VirtualKey
Label=-##L\nข##R
KeyCode=189
[Key_Base_Row_2_ID_12]
Type=VirtualKey
Label==##L\nช##R
KeyCode=187
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=Backspace
KeyCode=8
[Key_Base_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_19]
Type=VirtualKey
Label=Num##L\nLock##L
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=/##L\n
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=*##L\n
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=-##L\n
Cluster=Numpad
KeyCode=109
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=Tab
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=q##L\nๆ##R
KeyCode=81
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=w ##L\nไ##R
KeyCode=87
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e##L\nำ##R
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r##L\nพ##R
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t##L\nะ##R
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=y##L\nั##R
KeyCode=89
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u##L\nี##R
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i##L\nร##R
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o##L\nน##R
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p##L\nย##R
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=VirtualKey
Label=[##L\nบ##R
KeyCode=219
[Key_Base_Row_3_ID_12]
Type=VirtualKey
Label=]##L\nล##R
KeyCode=221
[Key_Base_Row_3_ID_13]
Type=VirtualKey
Width=150
Label=\##L\nฃ##R
KeyCode=220
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7##L\nHome##L
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8##L\n↑##L
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9##L\nPgUp##L
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Caps Lock
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=a##L\nฟ##R
KeyCode=65
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s##L\nห##R
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d##L\nก##R
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f##L\nด##R
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g##L\nเ##R
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h##L\n้##R
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j##L\n่##R
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k##L\nา##R
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l##L\nส##R
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=VirtualKey
Label=;##L\nว##R
KeyCode=186
[Key_Base_Row_4_ID_11]
Type=VirtualKey
Label='##L\nง##R
KeyCode=222
[Key_Base_Row_4_ID_12]
Type=VirtualKey
Width=225
Label=Enter
KeyCode=13
[Key_Base_Row_4_ID_13]
Type=Blank
Width=325
Cluster=Navigation
[Key_Base_Row_4_ID_14]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_15]
Type=VirtualKey
Label=4##L\n←##L
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_16]
Type=VirtualKey
Label=5##L\n
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=6##L\n→##L
Cluster=Numpad
KeyCode=102
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=225
Label=Shift
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=VirtualKey
Label=z##L\nผ##R
KeyCode=90
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=x##L\nป##R
KeyCode=88
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=c##L\nแ##R
KeyCode=67
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=v##L\nอ##R
KeyCode=86
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=b##L\nิ##R
KeyCode=66
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=n##L\nื##R
KeyCode=78
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=m##L\nท##R
KeyCode=77
[Key_Base_Row_5_ID_8]
Type=VirtualKey
Label=,##L\nม##R
KeyCode=188
[Key_Base_Row_5_ID_9]
Type=VirtualKey
Label=.##L\nใ##R
KeyCode=190
[Key_Base_Row_5_ID_10]
Type=VirtualKey
Label=/##L\nฝ##R
KeyCode=191
[Key_Base_Row_5_ID_11]
Type=VirtualKeyToggle
Width=275
Label=Shift
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_12]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_13]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_14]
Type=Blank
Cluster=Navigation
[Key_Base_Row_5_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_16]
Type=VirtualKey
Label=1##L\nEnd##L
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=2##L\n↓##L
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=3##L\nPgDn##L
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Base_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=165
NoRepeat=true
[Key_Base_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0##L\nInsert##L
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=.##L\nDel##L
Cluster=Numpad
KeyCode=110
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=🡰
Cluster=Extra
KeyCode=166
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=🡲
Cluster=Extra
KeyCode=167
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=🔇
Cluster=Extra
KeyCode=173
NoRepeat=true
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Shift_Row_2_ID_0]
Type=VirtualKey
Label=~##L\n%##R
KeyCode=192
[Key_Shift_Row_2_ID_1]
Type=VirtualKey
Label=!##L\n+##R
KeyCode=49
[Key_Shift_Row_2_ID_2]
Type=VirtualKey
Label=@##L\n๑##R
KeyCode=50
[Key_Shift_Row_2_ID_3]
Type=VirtualKey
Label=# ##L\n๒##R
KeyCode=51
[Key_Shift_Row_2_ID_4]
Type=VirtualKey
Label=$##L\n๓##R
KeyCode=52
[Key_Shift_Row_2_ID_5]
Type=VirtualKey
Label=%##L\n๔##R
KeyCode=53
[Key_Shift_Row_2_ID_6]
Type=VirtualKey
Label=^##L\nู##R
KeyCode=54
[Key_Shift_Row_2_ID_7]
Type=VirtualKey
Label=#L\n฿##R
KeyCode=55
[Key_Shift_Row_2_ID_8]
Type=VirtualKey
Label=*##L\n๕##R
KeyCode=56
[Key_Shift_Row_2_ID_9]
Type=VirtualKey
Label=(##L\n๖##R
KeyCode=57
[Key_Shift_Row_2_ID_10]
Type=VirtualKey
Label=)##L\n๗##R
KeyCode=48
[Key_Shift_Row_2_ID_11]
Type=VirtualKey
Label=_##L\n๘##R
KeyCode=189
[Key_Shift_Row_2_ID_12]
Type=VirtualKey
Label=+##L\n๙##R
KeyCode=187
[Key_Shift_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=Backspace
KeyCode=8
[Key_Shift_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_19]
Type=VirtualKey
Label=Num##L\nLock##L
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=/##L\n
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=*##L\n
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=-##L\n
Cluster=Numpad
KeyCode=109
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=Tab
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=Q##L\n๐##R
KeyCode=81
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=W##L\n"##R
KeyCode=87
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E##L\nฎ##R
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R##L\nฑ##R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T##L\nธ##R
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Y##L\nํ##R
KeyCode=89
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U##L\n๊##R
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I##L\nณ##R
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O##L\nฯ##R
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P##L\nญ##R
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=VirtualKey
Label={##L\nฐ##R
KeyCode=219
[Key_Shift_Row_3_ID_12]
Type=VirtualKey
Label=}##L\n,##R
KeyCode=221
[Key_Shift_Row_3_ID_13]
Type=VirtualKey
Width=150
Label=|##L\nฅ##R
KeyCode=220
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=7##L\nHome##L
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=8##L\n↑##L
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=9##L\nPgUp##L
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Caps Lock
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=A##L\nฤ##R
KeyCode=65
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S##L\nฆ##R
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D##L\nฏ##R
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F##L\nโ##R
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G##L\nฌ##R
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H##L\n็##R
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J##L\n๋##R
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K##L\nษ##R
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L##L\nศ##R
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=VirtualKey
Label=:##L\nซ##R
KeyCode=186
[Key_Shift_Row_4_ID_11]
Type=VirtualKey
Label="##L\n.##R
KeyCode=222
[Key_Shift_Row_4_ID_12]
Type=VirtualKey
Width=225
Label=Enter
KeyCode=13
[Key_Shift_Row_4_ID_13]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_15]
Type=VirtualKey
Label=4##L\n←##L
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=5##L\n
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=6##L\n→##L
Cluster=Numpad
KeyCode=102
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=225
Label=Shift
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=VirtualKey
Label=Z##L\n(##R
KeyCode=90
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=X##L\n)##R
KeyCode=88
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=C##L\nฉ##R
KeyCode=67
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=V##L\nฮ##R
KeyCode=86
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=B##L\nฺ##R
KeyCode=66
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=N##L\n์##R
KeyCode=78
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=M##L\n?##R
KeyCode=77
[Key_Shift_Row_5_ID_8]
Type=VirtualKey
Label=<##L\nฒ##R
KeyCode=188
[Key_Shift_Row_5_ID_9]
Type=VirtualKey
Label=>##L\nฬ##R
KeyCode=190
[Key_Shift_Row_5_ID_10]
Type=VirtualKey
Label=?##L\nฦ##R
KeyCode=191
[Key_Shift_Row_5_ID_11]
Type=VirtualKeyToggle
Width=275
Label=Shift
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_12]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_13]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_14]
Type=Blank
Cluster=Navigation
[Key_Shift_Row_5_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_16]
Type=VirtualKey
Label=1##L\nEnd##L
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=2##L\n↓##L
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=3##L\nPgDn##L
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Shift_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=165
NoRepeat=true
[Key_Shift_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0##L\nInsert##L
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=.##L\nDel##L
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/keyboards/qwerty_uk.ini
================================================
[LayoutInfo]
Name=QWERTY (United Kingdom)
Author=
HasAltGr=true
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_Base_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Base_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Base_Row_2_ID_0]
Type=String
Label=`
String=`
[Key_Base_Row_2_ID_1]
Type=VirtualKey
Label=1
KeyCode=49
[Key_Base_Row_2_ID_2]
Type=VirtualKey
Label=2
KeyCode=50
[Key_Base_Row_2_ID_3]
Type=VirtualKey
Label=3
KeyCode=51
[Key_Base_Row_2_ID_4]
Type=VirtualKey
Label=4
KeyCode=52
[Key_Base_Row_2_ID_5]
Type=VirtualKey
Label=5
KeyCode=53
[Key_Base_Row_2_ID_6]
Type=VirtualKey
Label=6
KeyCode=54
[Key_Base_Row_2_ID_7]
Type=VirtualKey
Label=7
KeyCode=55
[Key_Base_Row_2_ID_8]
Type=VirtualKey
Label=8
KeyCode=56
[Key_Base_Row_2_ID_9]
Type=VirtualKey
Label=9
KeyCode=57
[Key_Base_Row_2_ID_10]
Type=VirtualKey
Label=0
KeyCode=48
[Key_Base_Row_2_ID_11]
Type=String
Label=-
String=-
[Key_Base_Row_2_ID_12]
Type=String
Label==
String==
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Base_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=q
KeyCode=81
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=w
KeyCode=87
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=y
KeyCode=89
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=String
Label=[
String=[
[Key_Base_Row_3_ID_12]
Type=String
Label=]
String=]
[Key_Base_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=a
KeyCode=65
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=String
Label=;
String=;
[Key_Base_Row_4_ID_11]
Type=String
Label='
String='
[Key_Base_Row_4_ID_12]
Type=String
Label=#
String=#
[Key_Base_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Base_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Base_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=String
Label=\
String=\
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=z
KeyCode=90
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=x
KeyCode=88
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=c
KeyCode=67
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=v
KeyCode=86
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=b
KeyCode=66
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=n
KeyCode=78
[Key_Base_Row_5_ID_8]
Type=VirtualKey
Label=m
KeyCode=77
[Key_Base_Row_5_ID_9]
Type=String
Label=,
String=,
[Key_Base_Row_5_ID_10]
Type=String
Label=.
String=.
[Key_Base_Row_5_ID_11]
Type=String
Label=/
String=/
[Key_Base_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Base_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Base_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Base_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=🡰
Cluster=Extra
KeyCode=166
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=🡲
Cluster=Extra
KeyCode=167
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=🔇
Cluster=Extra
KeyCode=173
NoRepeat=true
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Shift_Row_2_ID_0]
Type=String
Label=¬
String=¬
[Key_Shift_Row_2_ID_1]
Type=String
Label=!
String=!
[Key_Shift_Row_2_ID_2]
Type=String
Label="
String="
[Key_Shift_Row_2_ID_3]
Type=String
Label=£
String=£
[Key_Shift_Row_2_ID_4]
Type=String
Label=$
String=$
[Key_Shift_Row_2_ID_5]
Type=String
Label=%
String=%
[Key_Shift_Row_2_ID_6]
Type=String
Label=^
String=^
[Key_Shift_Row_2_ID_7]
Type=String
Label=&
String=&
[Key_Shift_Row_2_ID_8]
Type=String
Label=*
String=*
[Key_Shift_Row_2_ID_9]
Type=String
Label=(
String=(
[Key_Shift_Row_2_ID_10]
Type=String
Label=)
String=)
[Key_Shift_Row_2_ID_11]
Type=String
Label=_
String=_
[Key_Shift_Row_2_ID_12]
Type=String
Label=+
String=+
[Key_Shift_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Shift_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=Q
KeyCode=81
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=W
KeyCode=87
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Y
KeyCode=89
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=String
Label={
String={
[Key_Shift_Row_3_ID_12]
Type=String
Label=}
String=}
[Key_Shift_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=A
KeyCode=65
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=String
Label=:
String=:
[Key_Shift_Row_4_ID_11]
Type=String
Label=@
String=@
[Key_Shift_Row_4_ID_12]
Type=String
Label=~
String=~
[Key_Shift_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=String
Label=|
String=|
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=Z
KeyCode=90
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=X
KeyCode=88
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=C
KeyCode=67
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=V
KeyCode=86
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=B
KeyCode=66
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=N
KeyCode=78
[Key_Shift_Row_5_ID_8]
Type=VirtualKey
Label=M
KeyCode=77
[Key_Shift_Row_5_ID_9]
Type=String
Label=<
String=<
[Key_Shift_Row_5_ID_10]
Type=String
Label=>
String=>
[Key_Shift_Row_5_ID_11]
Type=String
Label=?
String=?
[Key_Shift_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Shift_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Shift_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Shift_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
[Key_AltGr_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_AltGr_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_AltGr_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_AltGr_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_AltGr_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_AltGr_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_AltGr_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_AltGr_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_AltGr_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_AltGr_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_AltGr_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_AltGr_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_AltGr_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_AltGr_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_AltGr_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_AltGr_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_AltGr_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_AltGr_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_AltGr_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_AltGr_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_AltGr_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_AltGr_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_AltGr_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_AltGr_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_AltGr_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_AltGr_Row_2_ID_0]
Type=String
Label=¦
String=¦
[Key_AltGr_Row_2_ID_1]
Type=Blank
Width=300
[Key_AltGr_Row_2_ID_2]
Type=String
Label=€
String=€
[Key_AltGr_Row_2_ID_3]
Type=Blank
Width=800
[Key_AltGr_Row_2_ID_4]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_AltGr_Row_2_ID_5]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_2_ID_6]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_AltGr_Row_2_ID_7]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_AltGr_Row_2_ID_8]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_AltGr_Row_2_ID_9]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_2_ID_10]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_AltGr_Row_2_ID_11]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_AltGr_Row_2_ID_12]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_AltGr_Row_2_ID_13]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_AltGr_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_AltGr_Row_3_ID_1]
Type=Blank
Width=1200
[Key_AltGr_Row_3_ID_2]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_AltGr_Row_3_ID_3]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_3_ID_4]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_AltGr_Row_3_ID_5]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_AltGr_Row_3_ID_6]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_AltGr_Row_3_ID_7]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_3_ID_8]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_AltGr_Row_3_ID_9]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_AltGr_Row_3_ID_10]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_AltGr_Row_3_ID_11]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_AltGr_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_AltGr_Row_4_ID_1]
Type=Blank
Width=1200
[Key_AltGr_Row_4_ID_2]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_AltGr_Row_4_ID_3]
Type=Blank
Width=325
Cluster=Navigation
[Key_AltGr_Row_4_ID_4]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_4_ID_5]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_AltGr_Row_4_ID_6]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_AltGr_Row_4_ID_7]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
[Key_AltGr_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_AltGr_Row_5_ID_1]
Type=Blank
Width=1100
[Key_AltGr_Row_5_ID_2]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_AltGr_Row_5_ID_3]
Type=Blank
Width=125
Cluster=Navigation
[Key_AltGr_Row_5_ID_4]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_AltGr_Row_5_ID_5]
Type=Blank
Cluster=Navigation
[Key_AltGr_Row_5_ID_6]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_5_ID_7]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_AltGr_Row_5_ID_8]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_AltGr_Row_5_ID_9]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_AltGr_Row_5_ID_10]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_AltGr_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_AltGr_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_AltGr_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_AltGr_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_AltGr_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_AltGr_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_AltGr_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_AltGr_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_AltGr_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_AltGr_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_AltGr_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_AltGr_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_AltGr_Row_6_ID_14]
Type=VirtualKey
Label=.
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/keyboards/qwerty_usa.ini
================================================
[LayoutInfo]
Name=QWERTY (USA)
HasAltGr=false
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
;[Key_{SubLayout}_Row_{Row}_ID_{ID}]
;Type=Blank / VirtualKey / VirtualKeyToggle / VirtualKeyIsoEnter / String / SubLayoutToggle / Action
;Width=Value in % | Optional, defaults to 100
;Height=Value in % | Optional, defaults to 100
;Label=Key Label | Not used with type Blank, use \n for a second line
;Cluster=Base / Function / Navigation / Numpad / Extra | Optional, defaults to Base
;KeyCode=Virtual key code (decimal) | Use with type VirtualKey / VirtualKeyToggle / VirtualKeyIsoEnter
;BlockModifiers=true/false | Optional, defaults to false. Use with type VirtualKey
;NoRepeat=true/false | Optional, defaults to false
;String=String typed by the key (UTF-8) | Use with type String
;SubLayout=SubLayout Name | Use with type SubLayoutToggle
;ActionID=Global Action ID | Use with type Action
;
;Possible sublayouts: Base / Shift / AltGr / Aux
;
;Use String type for non-basic character keys. This allows Desktop+ to figure out the correct key combination on the real keyboard layout for maximum application compatibility.
;Use two VirtualKeyIsoEnter in adjacent rows to build an ISO-Enter shaped key. One max per sublayout.
;Use BlockModifiers to have all modifier keys be released while the key is pressed. Not recommended in general, but can come handy when having auxiliary keys in modifier sublayouts.
;Use NoRepeat to block key repeat from holding down even when key repeat is enabled. Recommend for keys that typically do not repeat on real keyboards.
;
;Cluster assignment is used to selectively disable loading of keys. Function cluster should also include the keys next to the actual function keys so the whole row can be toggled.
;Surrounding blank space is not removed. Carefully assign blank space type keys to clusters in order to have them toggle correctly... or don't use clusters at all.
;
;Global Action ID's are not really displayed anywhere, but basically anything below ID 1000 is reserved for built-in actions and custom action are their ID + 1000.
;Refer to the source code or values of ActionBarOrderCustom in config.ini for built-in action IDs for now.
;----------------------Row 0
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
;----------------------Row 1 (25% height blank space)
[Key_Base_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Base_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
;----------------------Row 2
[Key_Base_Row_2_ID_0]
Type=String
Label=`
String=`
[Key_Base_Row_2_ID_1]
Type=VirtualKey
Label=1
KeyCode=49
[Key_Base_Row_2_ID_2]
Type=VirtualKey
Label=2
KeyCode=50
[Key_Base_Row_2_ID_3]
Type=VirtualKey
Label=3
KeyCode=51
[Key_Base_Row_2_ID_4]
Type=VirtualKey
Label=4
KeyCode=52
[Key_Base_Row_2_ID_5]
Type=VirtualKey
Label=5
KeyCode=53
[Key_Base_Row_2_ID_6]
Type=VirtualKey
Label=6
KeyCode=54
[Key_Base_Row_2_ID_7]
Type=VirtualKey
Label=7
KeyCode=55
[Key_Base_Row_2_ID_8]
Type=VirtualKey
Label=8
KeyCode=56
[Key_Base_Row_2_ID_9]
Type=VirtualKey
Label=9
KeyCode=57
[Key_Base_Row_2_ID_10]
Type=VirtualKey
Label=0
KeyCode=48
[Key_Base_Row_2_ID_11]
Type=String
Label=-
String=-
[Key_Base_Row_2_ID_12]
Type=String
Label==
String==
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=Backspace
KeyCode=8
[Key_Base_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
;----------------------Row 3
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=Tab
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=q
KeyCode=81
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=w
KeyCode=87
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=y
KeyCode=89
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=String
Label=[
String=[
[Key_Base_Row_3_ID_12]
Type=String
Label=]
String=]
[Key_Base_Row_3_ID_13]
Type=String
Width=150
Label=\
String=\
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
;----------------------Row 4
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Caps Lock
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=a
KeyCode=65
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=String
Label=;
String=;
[Key_Base_Row_4_ID_11]
Type=String
Label='
String='
[Key_Base_Row_4_ID_12]
Type=VirtualKey
Width=225
Label=Enter
KeyCode=13
[Key_Base_Row_4_ID_13]
Type=Blank
Width=325
Cluster=Navigation
[Key_Base_Row_4_ID_14]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_15]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_16]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
;----------------------Row 5
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=225
Label=Shift
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=VirtualKey
Label=z
KeyCode=90
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=x
KeyCode=88
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=c
KeyCode=67
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=v
KeyCode=86
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=b
KeyCode=66
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=n
KeyCode=78
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=m
KeyCode=77
[Key_Base_Row_5_ID_8]
Type=String
Label=,
String=,
[Key_Base_Row_5_ID_9]
Type=String
Label=.
String=.
[Key_Base_Row_5_ID_10]
Type=String
Label=/
String=/
[Key_Base_Row_5_ID_11]
Type=VirtualKeyToggle
Width=275
Label=Shift
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_12]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_13]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_14]
Type=Blank
Width=100
Cluster=Navigation
[Key_Base_Row_5_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_16]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
;----------------------Row 6
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
;Space Bar
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Base_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=165
NoRepeat=true
[Key_Base_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=,
Cluster=Numpad
KeyCode=110
;----------------------Shift SubLayout
;
;----------------------Row 0
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=Print\nScreen
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=Scroll\nLock
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=🡰
Cluster=Extra
KeyCode=166
BlockModifiers=true
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=🡲
Cluster=Extra
KeyCode=167
BlockModifiers=true
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=🔇
Cluster=Extra
KeyCode=173
NoRepeat=true
;----------------------Row 1 (25% height blank space)
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
;----------------------Row 2
[Key_Shift_Row_2_ID_0]
Type=String
Label=~
String=~
[Key_Shift_Row_2_ID_1]
Type=String
Label=!
String=!
[Key_Shift_Row_2_ID_2]
Type=String
Label=@
String=@
[Key_Shift_Row_2_ID_3]
Type=String
Label=#
String=#
[Key_Shift_Row_2_ID_4]
Type=String
Label=$
String=$
[Key_Shift_Row_2_ID_5]
Type=String
Label=%
String=%
[Key_Shift_Row_2_ID_6]
Type=String
Label=^
String=^
[Key_Shift_Row_2_ID_7]
Type=String
Label=&
String=&
[Key_Shift_Row_2_ID_8]
Type=String
Label=*
String=*
[Key_Shift_Row_2_ID_9]
Type=String
Label=(
String=(
[Key_Shift_Row_2_ID_10]
Type=String
Label=)
String=)
[Key_Shift_Row_2_ID_11]
Type=String
Label=_
String=_
[Key_Shift_Row_2_ID_12]
Type=String
Label=+
String=+
[Key_Shift_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=Backspace
KeyCode=8
[Key_Shift_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
;----------------------Row 3
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=Tab
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=Q
KeyCode=81
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=W
KeyCode=87
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Y
KeyCode=89
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=String
Label={
String={
[Key_Shift_Row_3_ID_12]
Type=String
Label=}
String=}
[Key_Shift_Row_3_ID_13]
Type=String
Width=150
Label=|
String=|
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
;----------------------Row 4
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=Caps Lock
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=A
KeyCode=65
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=String
Label=:
String=:
[Key_Shift_Row_4_ID_11]
Type=String
Label="
String="
[Key_Shift_Row_4_ID_12]
Type=VirtualKey
Width=225
Label=Enter
KeyCode=13
[Key_Shift_Row_4_ID_13]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_15]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
;----------------------Row 5
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=225
Label=Shift
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=VirtualKey
Label=Z
KeyCode=90
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=X
KeyCode=88
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=C
KeyCode=67
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=V
KeyCode=86
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=B
KeyCode=66
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=N
KeyCode=78
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=M
KeyCode=77
[Key_Shift_Row_5_ID_8]
Type=String
Label=<
String=<
[Key_Shift_Row_5_ID_9]
Type=String
Label=>
String=>
[Key_Shift_Row_5_ID_10]
Type=String
Label=?
String=?
[Key_Shift_Row_5_ID_11]
Type=VirtualKeyToggle
Width=275
Label=Shift
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_12]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_13]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_14]
Type=Blank
Width=100
Cluster=Navigation
[Key_Shift_Row_5_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_16]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
;----------------------Row 6
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
;Space Bar
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Shift_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=165
NoRepeat=true
[Key_Shift_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menu
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=,
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/keyboards/qwertz_ger.ini
================================================
[LayoutInfo]
Name=QWERTZ (Germany)
HasAltGr=true
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
;See qwerty_usa.ini for format documentation
;----------------------Row 0
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=Druck
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=Rollen
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
;----------------------Row 1 (25% height blank space)
[Key_Base_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Base_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
;----------------------Row 2
[Key_Base_Row_2_ID_0]
Type=String
Label=^
String=^
[Key_Base_Row_2_ID_1]
Type=VirtualKey
Label=1
KeyCode=49
[Key_Base_Row_2_ID_2]
Type=VirtualKey
Label=2
KeyCode=50
[Key_Base_Row_2_ID_3]
Type=VirtualKey
Label=3
KeyCode=51
[Key_Base_Row_2_ID_4]
Type=VirtualKey
Label=4
KeyCode=52
[Key_Base_Row_2_ID_5]
Type=VirtualKey
Label=5
KeyCode=53
[Key_Base_Row_2_ID_6]
Type=VirtualKey
Label=6
KeyCode=54
[Key_Base_Row_2_ID_7]
Type=VirtualKey
Label=7
KeyCode=55
[Key_Base_Row_2_ID_8]
Type=VirtualKey
Label=8
KeyCode=56
[Key_Base_Row_2_ID_9]
Type=VirtualKey
Label=9
KeyCode=57
[Key_Base_Row_2_ID_10]
Type=VirtualKey
Label=0
KeyCode=48
[Key_Base_Row_2_ID_11]
Type=String
Label=ß
String=ß
[Key_Base_Row_2_ID_12]
Type=String
Label=´
String=´
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Base_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_15]
Type=VirtualKey
Label=Einfg
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Pos1
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=Bild🠹
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_19]
Type=VirtualKey
Label=Num
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
;----------------------Row 3
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=q
KeyCode=81
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=w
KeyCode=87
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=z
KeyCode=90
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=String
Label=ü
String=ü
[Key_Base_Row_3_ID_12]
Type=String
Label=+
String=+
[Key_Base_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Entf
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=Ende
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=Bild🠻
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
;----------------------Row 4
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=a
KeyCode=65
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=String
Label=ö
String=ö
[Key_Base_Row_4_ID_11]
Type=String
Label=ä
String=ä
[Key_Base_Row_4_ID_12]
Type=String
Label=#
String=#
[Key_Base_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Base_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Base_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
;----------------------Row 5
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=String
Label=<
String=<
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=y
KeyCode=89
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=x
KeyCode=88
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=c
KeyCode=67
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=v
KeyCode=86
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=b
KeyCode=66
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=n
KeyCode=78
[Key_Base_Row_5_ID_8]
Type=VirtualKey
Label=m
KeyCode=77
[Key_Base_Row_5_ID_9]
Type=String
Label=,
String=,
[Key_Base_Row_5_ID_10]
Type=String
Label=.
String=.
[Key_Base_Row_5_ID_11]
Type=String
Label=-
String=-
[Key_Base_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_15]
Type=Blank
Width=100
Cluster=Navigation
[Key_Base_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
;----------------------Row 6
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Strg
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
;Space Bar
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Base_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Base_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menü
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Strg
KeyCode=163
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=,
Cluster=Numpad
KeyCode=110
;----------------------Shift SubLayout
;
;----------------------Row 0
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=Druck
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=Rollen
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=🡰
Cluster=Extra
KeyCode=166
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=🡲
Cluster=Extra
KeyCode=167
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=🔇
Cluster=Extra
KeyCode=173
NoRepeat=true
;----------------------Row 1 (25% height blank space)
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
;----------------------Row 2
[Key_Shift_Row_2_ID_0]
Type=String
Label=°
String=°
[Key_Shift_Row_2_ID_1]
Type=String
Label=!
String=!
[Key_Shift_Row_2_ID_2]
Type=String
Label="
String="
[Key_Shift_Row_2_ID_3]
Type=String
Label=§
String=§
[Key_Shift_Row_2_ID_4]
Type=String
Label=$
String=$
[Key_Shift_Row_2_ID_5]
Type=String
Label=%
String=%
[Key_Shift_Row_2_ID_6]
Type=String
Label=&
String=&
[Key_Shift_Row_2_ID_7]
Type=String
Label=/
String=/
[Key_Shift_Row_2_ID_8]
Type=String
Label=(
String=(
[Key_Shift_Row_2_ID_9]
Type=String
Label=)
String=)
[Key_Shift_Row_2_ID_10]
Type=String
Label==
String==
[Key_Shift_Row_2_ID_11]
Type=String
Label=?
String=?
[Key_Shift_Row_2_ID_12]
Type=String
Label=`
String=`
[Key_Shift_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Shift_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_15]
Type=VirtualKey
Label=Einfg
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Pos1
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=Bild🠹
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_19]
Type=VirtualKey
Label=Num
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
;----------------------Row 3
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=Q
KeyCode=81
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=W
KeyCode=87
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Z
KeyCode=90
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=String
Label=Ü
String=Ü
[Key_Shift_Row_3_ID_12]
Type=String
Label=*
String=*
[Key_Shift_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Entf
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=Ende
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=Bild🠻
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
;----------------------Row 4
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=A
KeyCode=65
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=String
Label=Ö
String=Ö
[Key_Shift_Row_4_ID_11]
Type=String
Label=Ä
String=Ä
[Key_Shift_Row_4_ID_12]
Type=String
Label='
String='
[Key_Shift_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_18]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
;----------------------Row 5
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=String
Label=>
String=>
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=Y
KeyCode=89
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=X
KeyCode=88
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=C
KeyCode=67
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=V
KeyCode=86
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=B
KeyCode=66
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=N
KeyCode=78
[Key_Shift_Row_5_ID_8]
Type=VirtualKey
Label=M
KeyCode=77
[Key_Shift_Row_5_ID_9]
Type=String
Label=;
String=;
[Key_Shift_Row_5_ID_10]
Type=String
Label=:
String=:
[Key_Shift_Row_5_ID_11]
Type=String
Label=_
String=_
[Key_Shift_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_15]
Type=Blank
Width=100
Cluster=Navigation
[Key_Shift_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
;----------------------Row 6
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Strg
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
;Space Bar
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Shift_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Shift_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menü
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Strg
KeyCode=163
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=,
Cluster=Numpad
KeyCode=110
;----------------------AltGr SubLayout
;
;----------------------Row 0
[Key_AltGr_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_AltGr_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_AltGr_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_AltGr_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_AltGr_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_AltGr_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_AltGr_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_AltGr_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_AltGr_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_AltGr_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_AltGr_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_AltGr_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_AltGr_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_AltGr_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_AltGr_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_AltGr_Row_0_ID_17]
Type=VirtualKey
Label=Druck
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_AltGr_Row_0_ID_18]
Type=VirtualKey
Label=Rollen
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_AltGr_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_AltGr_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_AltGr_Row_0_ID_21]
Type=VirtualKey
Label=⏯
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_AltGr_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_AltGr_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_AltGr_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
;----------------------Row 1 (25% height blank space)
[Key_AltGr_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_AltGr_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
;----------------------Row 2
[Key_AltGr_Row_2_ID_0]
Type=Blank
Width=700
[Key_AltGr_Row_2_ID_1]
Type=String
Label={
String={
[Key_AltGr_Row_2_ID_2]
Type=String
Label=[
String=[
[Key_AltGr_Row_2_ID_3]
Type=String
Label=]
String=]
[Key_AltGr_Row_2_ID_4]
Type=String
Label=}
String=}
[Key_AltGr_Row_2_ID_5]
Type=String
Label=\
String=\
[Key_AltGr_Row_2_ID_6]
Type=Blank
[Key_AltGr_Row_2_ID_7]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_AltGr_Row_2_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_2_ID_9]
Type=VirtualKey
Label=Einfg
Cluster=Navigation
KeyCode=45
[Key_AltGr_Row_2_ID_10]
Type=VirtualKey
Label=Pos1
Cluster=Navigation
KeyCode=36
[Key_AltGr_Row_2_ID_11]
Type=VirtualKey
Label=Bild🠹
Cluster=Navigation
KeyCode=33
[Key_AltGr_Row_2_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_2_ID_13]
Type=VirtualKey
Label=Num
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_AltGr_Row_2_ID_14]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_AltGr_Row_2_ID_15]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_AltGr_Row_2_ID_16]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
;----------------------Row 3
[Key_AltGr_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_AltGr_Row_3_ID_1]
Type=String
Label=@
String=@
[Key_AltGr_Row_3_ID_2]
Type=Blank
[Key_AltGr_Row_3_ID_3]
Type=String
Label=€
String=€
[Key_AltGr_Row_3_ID_4]
Type=Blank
Width=800
[Key_AltGr_Row_3_ID_5]
Type=String
Label=~
String=~
[Key_AltGr_Row_3_ID_6]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_AltGr_Row_3_ID_7]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_3_ID_8]
Type=VirtualKey
Label=Entf
Cluster=Navigation
KeyCode=46
[Key_AltGr_Row_3_ID_9]
Type=VirtualKey
Label=Ende
Cluster=Navigation
KeyCode=35
[Key_AltGr_Row_3_ID_10]
Type=VirtualKey
Label=Bild🠻
Cluster=Navigation
KeyCode=34
[Key_AltGr_Row_3_ID_11]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_3_ID_12]
Type=VirtualKey
Label=7
Cluster=Numpad
KeyCode=103
[Key_AltGr_Row_3_ID_13]
Type=VirtualKey
Label=8
Cluster=Numpad
KeyCode=104
[Key_AltGr_Row_3_ID_14]
Type=VirtualKey
Label=9
Cluster=Numpad
KeyCode=105
[Key_AltGr_Row_3_ID_15]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
;----------------------Row 4
[Key_AltGr_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_AltGr_Row_4_ID_1]
Type=Blank
Width=1200
[Key_AltGr_Row_4_ID_2]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_AltGr_Row_4_ID_3]
Type=Blank
Width=325
Cluster=Navigation
[Key_AltGr_Row_4_ID_4]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_4_ID_5]
Type=VirtualKey
Label=4
Cluster=Numpad
KeyCode=100
[Key_AltGr_Row_4_ID_6]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_AltGr_Row_4_ID_7]
Type=VirtualKey
Label=6
Cluster=Numpad
KeyCode=102
;----------------------Row 5
[Key_AltGr_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_AltGr_Row_5_ID_1]
Type=String
Label=|
String=|
[Key_AltGr_Row_5_ID_2]
Type=Blank
Width=600
[Key_AltGr_Row_5_ID_3]
Type=String
Label=µ
String=µ
[Key_AltGr_Row_5_ID_4]
Type=Blank
Width=300
[Key_AltGr_Row_5_ID_5]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_AltGr_Row_5_ID_6]
Type=Blank
Width=125
Cluster=Navigation
[Key_AltGr_Row_5_ID_7]
Type=VirtualKey
Label=🠅
KeyCode=38
Cluster=Navigation
[Key_AltGr_Row_5_ID_8]
Type=Blank
Width=100
Cluster=Navigation
[Key_AltGr_Row_5_ID_9]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_5_ID_10]
Type=VirtualKey
Label=1
Cluster=Numpad
KeyCode=97
[Key_AltGr_Row_5_ID_11]
Type=VirtualKey
Label=2
Cluster=Numpad
KeyCode=98
[Key_AltGr_Row_5_ID_12]
Type=VirtualKey
Label=3
Cluster=Numpad
KeyCode=99
[Key_AltGr_Row_5_ID_13]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
;----------------------Row 6
[Key_AltGr_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Strg
KeyCode=162
NoRepeat=true
[Key_AltGr_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_AltGr_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_AltGr_Row_6_ID_3]
;Space Bar
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_AltGr_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_AltGr_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_AltGr_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menü
KeyCode=93
NoRepeat=true
[Key_AltGr_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Strg
KeyCode=163
NoRepeat=true
[Key_AltGr_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_AltGr_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_AltGr_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_AltGr_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0
Cluster=Numpad
KeyCode=96
[Key_AltGr_Row_6_ID_14]
Type=VirtualKey
Label=,
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/keyboards/qwertz_hu.ini
================================================
[LayoutInfo]
Name=QWERTZ (Hungary)
Author=Lenr
HasAltGr=true
HasClusterFunction=true
HasClusterNavigation=true
HasClusterNumpad=true
HasClusterExtra=true
[Key_Base_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Base_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Base_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Base_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Base_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Base_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Base_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Base_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Base_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Base_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Base_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Base_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Base_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Base_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Base_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Base_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Base_Row_0_ID_17]
Type=VirtualKey
Label=PrtSc
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Base_Row_0_ID_18]
Type=VirtualKey
Label=ScrLk
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Base_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Base_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Base_Row_0_ID_21]
Type=VirtualKey
Label=⏵/⏸
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_Base_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_Base_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_Base_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_Base_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Base_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Base_Row_2_ID_0]
Type=String
Label=0
String=0
[Key_Base_Row_2_ID_1]
Type=VirtualKey
Label=1
KeyCode=49
[Key_Base_Row_2_ID_2]
Type=VirtualKey
Label=2
KeyCode=50
[Key_Base_Row_2_ID_3]
Type=VirtualKey
Label=3
KeyCode=51
[Key_Base_Row_2_ID_4]
Type=VirtualKey
Label=4
KeyCode=52
[Key_Base_Row_2_ID_5]
Type=VirtualKey
Label=5
KeyCode=53
[Key_Base_Row_2_ID_6]
Type=VirtualKey
Label=6
KeyCode=54
[Key_Base_Row_2_ID_7]
Type=VirtualKey
Label=7
KeyCode=55
[Key_Base_Row_2_ID_8]
Type=VirtualKey
Label=8
KeyCode=56
[Key_Base_Row_2_ID_9]
Type=VirtualKey
Label=9
KeyCode=57
[Key_Base_Row_2_ID_10]
Type=VirtualKey
Label=ö
KeyCode=192
[Key_Base_Row_2_ID_11]
Type=String
Label=ü
String=ü
[Key_Base_Row_2_ID_12]
Type=String
Label=ó
String=ó
[Key_Base_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Base_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Base_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Base_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Base_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Base_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Base_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Base_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Base_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Base_Row_3_ID_1]
Type=VirtualKey
Label=q
KeyCode=81
[Key_Base_Row_3_ID_2]
Type=VirtualKey
Label=w
KeyCode=87
[Key_Base_Row_3_ID_3]
Type=VirtualKey
Label=e
KeyCode=69
[Key_Base_Row_3_ID_4]
Type=VirtualKey
Label=r
KeyCode=82
[Key_Base_Row_3_ID_5]
Type=VirtualKey
Label=t
KeyCode=84
[Key_Base_Row_3_ID_6]
Type=VirtualKey
Label=z
KeyCode=90
[Key_Base_Row_3_ID_7]
Type=VirtualKey
Label=u
KeyCode=85
[Key_Base_Row_3_ID_8]
Type=VirtualKey
Label=i
KeyCode=73
[Key_Base_Row_3_ID_9]
Type=VirtualKey
Label=o
KeyCode=79
[Key_Base_Row_3_ID_10]
Type=VirtualKey
Label=p
KeyCode=80
[Key_Base_Row_3_ID_11]
Type=String
Label=ő
String=ő
[Key_Base_Row_3_ID_12]
Type=String
Label=ú
String=ú
[Key_Base_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Base_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Base_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Base_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Base_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_3_ID_19]
Type=VirtualKey
Label=7\nHome
Cluster=Numpad
KeyCode=103
[Key_Base_Row_3_ID_20]
Type=VirtualKey
Label=8\n🠅
Cluster=Numpad
KeyCode=104
[Key_Base_Row_3_ID_21]
Type=VirtualKey
Label=9\nPgUp
Cluster=Numpad
KeyCode=105
[Key_Base_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Base_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_Base_Row_4_ID_1]
Type=VirtualKey
Label=a
KeyCode=65
[Key_Base_Row_4_ID_2]
Type=VirtualKey
Label=s
KeyCode=83
[Key_Base_Row_4_ID_3]
Type=VirtualKey
Label=d
KeyCode=68
[Key_Base_Row_4_ID_4]
Type=VirtualKey
Label=f
KeyCode=70
[Key_Base_Row_4_ID_5]
Type=VirtualKey
Label=g
KeyCode=71
[Key_Base_Row_4_ID_6]
Type=VirtualKey
Label=h
KeyCode=72
[Key_Base_Row_4_ID_7]
Type=VirtualKey
Label=j
KeyCode=74
[Key_Base_Row_4_ID_8]
Type=VirtualKey
Label=k
KeyCode=75
[Key_Base_Row_4_ID_9]
Type=VirtualKey
Label=l
KeyCode=76
[Key_Base_Row_4_ID_10]
Type=String
Label=é
String=é
[Key_Base_Row_4_ID_11]
Type=String
Label=á
String=á
[Key_Base_Row_4_ID_12]
Type=String
Label=ű
String=ű
[Key_Base_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Base_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Base_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_4_ID_16]
Type=VirtualKey
Label=4\n🠄
Cluster=Numpad
KeyCode=100
[Key_Base_Row_4_ID_17]
Type=VirtualKey
Label=5\n
Cluster=Numpad
KeyCode=101
[Key_Base_Row_4_ID_18]
Type=VirtualKey
Label=6\n🠆
Cluster=Numpad
KeyCode=102
[Key_Base_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Base_Row_5_ID_1]
Type=String
Label=í
String=í
[Key_Base_Row_5_ID_2]
Type=VirtualKey
Label=y
KeyCode=89
[Key_Base_Row_5_ID_3]
Type=VirtualKey
Label=x
KeyCode=88
[Key_Base_Row_5_ID_4]
Type=VirtualKey
Label=c
KeyCode=67
[Key_Base_Row_5_ID_5]
Type=VirtualKey
Label=v
KeyCode=86
[Key_Base_Row_5_ID_6]
Type=VirtualKey
Label=b
KeyCode=66
[Key_Base_Row_5_ID_7]
Type=VirtualKey
Label=n
KeyCode=78
[Key_Base_Row_5_ID_8]
Type=VirtualKey
Label=m
KeyCode=77
[Key_Base_Row_5_ID_9]
Type=String
Label=,
String=,
[Key_Base_Row_5_ID_10]
Type=String
Label=.
String=.
[Key_Base_Row_5_ID_11]
Type=String
Label=-
String=-
[Key_Base_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Base_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Base_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Base_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Base_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_5_ID_17]
Type=VirtualKey
Label=1\nEnd
Cluster=Numpad
KeyCode=97
[Key_Base_Row_5_ID_18]
Type=VirtualKey
Label=2\n🠇
Cluster=Numpad
KeyCode=98
[Key_Base_Row_5_ID_19]
Type=VirtualKey
Label=3\nPgDn
Cluster=Numpad
KeyCode=99
[Key_Base_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Base_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Base_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Base_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Base_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Base_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Base_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Base_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menü
KeyCode=93
NoRepeat=true
[Key_Base_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Base_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Base_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Base_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Base_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Base_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Base_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0\nInsert
Cluster=Numpad
KeyCode=96
[Key_Base_Row_6_ID_14]
Type=VirtualKey
Label=,\nDelete
Cluster=Numpad
KeyCode=110
[Key_Shift_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_Shift_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_Shift_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_Shift_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_Shift_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_Shift_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_Shift_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_Shift_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_Shift_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_Shift_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_Shift_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_Shift_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_Shift_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_Shift_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_Shift_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_Shift_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_Shift_Row_0_ID_17]
Type=VirtualKey
Label=PrtSc
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_Shift_Row_0_ID_18]
Type=VirtualKey
Label=ScrLk
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_Shift_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_Shift_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_Shift_Row_0_ID_21]
Type=VirtualKey
Label=🡰
Cluster=Extra
KeyCode=166
NoRepeat=true
[Key_Shift_Row_0_ID_22]
Type=VirtualKey
Label=🡲
Cluster=Extra
KeyCode=167
NoRepeat=true
[Key_Shift_Row_0_ID_23]
Type=VirtualKey
Label=🔇
Cluster=Extra
KeyCode=173
NoRepeat=true
[Key_Shift_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_Shift_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_Shift_Row_2_ID_0]
Type=String
Label=§
String=§
[Key_Shift_Row_2_ID_1]
Type=String
Label='
String='
[Key_Shift_Row_2_ID_2]
Type=String
Label="
String="
[Key_Shift_Row_2_ID_3]
Type=String
Label=+
String=+
[Key_Shift_Row_2_ID_4]
Type=String
Label=!
String=!
[Key_Shift_Row_2_ID_5]
Type=String
Label=%
String=%
[Key_Shift_Row_2_ID_6]
Type=String
Label=/
String=/
[Key_Shift_Row_2_ID_7]
Type=String
Label==
String==
[Key_Shift_Row_2_ID_8]
Type=String
Label=(
String=(
[Key_Shift_Row_2_ID_9]
Type=String
Label=)
String=)
[Key_Shift_Row_2_ID_10]
Type=String
Label=Ö
String=Ö
[Key_Shift_Row_2_ID_11]
Type=String
Label=Ü
String=Ü
[Key_Shift_Row_2_ID_12]
Type=String
Label=Ó
String=Ó
[Key_Shift_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_Shift_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_2_ID_15]
Type=VirtualKey
Label=Insert
Cluster=Navigation
KeyCode=45
[Key_Shift_Row_2_ID_16]
Type=VirtualKey
Label=Home
Cluster=Navigation
KeyCode=36
[Key_Shift_Row_2_ID_17]
Type=VirtualKey
Label=PgUp
Cluster=Navigation
KeyCode=33
[Key_Shift_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_Shift_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_Shift_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_Shift_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_Shift_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_Shift_Row_3_ID_1]
Type=VirtualKey
Label=Q
KeyCode=81
[Key_Shift_Row_3_ID_2]
Type=VirtualKey
Label=W
KeyCode=87
[Key_Shift_Row_3_ID_3]
Type=VirtualKey
Label=E
KeyCode=69
[Key_Shift_Row_3_ID_4]
Type=VirtualKey
Label=R
KeyCode=82
[Key_Shift_Row_3_ID_5]
Type=VirtualKey
Label=T
KeyCode=84
[Key_Shift_Row_3_ID_6]
Type=VirtualKey
Label=Z
KeyCode=90
[Key_Shift_Row_3_ID_7]
Type=VirtualKey
Label=U
KeyCode=85
[Key_Shift_Row_3_ID_8]
Type=VirtualKey
Label=I
KeyCode=73
[Key_Shift_Row_3_ID_9]
Type=VirtualKey
Label=O
KeyCode=79
[Key_Shift_Row_3_ID_10]
Type=VirtualKey
Label=P
KeyCode=80
[Key_Shift_Row_3_ID_11]
Type=String
Label=Ő
String=Ő
[Key_Shift_Row_3_ID_12]
Type=String
Label=Ú
String=Ú
[Key_Shift_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_Shift_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_3_ID_15]
Type=VirtualKey
Label=Delete
Cluster=Navigation
KeyCode=46
[Key_Shift_Row_3_ID_16]
Type=VirtualKey
Label=End
Cluster=Navigation
KeyCode=35
[Key_Shift_Row_3_ID_17]
Type=VirtualKey
Label=PgDn
Cluster=Navigation
KeyCode=34
[Key_Shift_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_3_ID_19]
Type=VirtualKey
Label=Home
Cluster=Numpad
KeyCode=103
[Key_Shift_Row_3_ID_20]
Type=VirtualKey
Label=🠅
Cluster=Numpad
KeyCode=104
[Key_Shift_Row_3_ID_21]
Type=VirtualKey
Label=PgUp
Cluster=Numpad
KeyCode=105
[Key_Shift_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_Shift_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_Shift_Row_4_ID_1]
Type=VirtualKey
Label=A
KeyCode=65
[Key_Shift_Row_4_ID_2]
Type=VirtualKey
Label=S
KeyCode=83
[Key_Shift_Row_4_ID_3]
Type=VirtualKey
Label=D
KeyCode=68
[Key_Shift_Row_4_ID_4]
Type=VirtualKey
Label=F
KeyCode=70
[Key_Shift_Row_4_ID_5]
Type=VirtualKey
Label=G
KeyCode=71
[Key_Shift_Row_4_ID_6]
Type=VirtualKey
Label=H
KeyCode=72
[Key_Shift_Row_4_ID_7]
Type=VirtualKey
Label=J
KeyCode=74
[Key_Shift_Row_4_ID_8]
Type=VirtualKey
Label=K
KeyCode=75
[Key_Shift_Row_4_ID_9]
Type=VirtualKey
Label=L
KeyCode=76
[Key_Shift_Row_4_ID_10]
Type=String
Label=É
String=É
[Key_Shift_Row_4_ID_11]
Type=String
Label=Á
String=Á
[Key_Shift_Row_4_ID_12]
Type=String
Label=Ű
String=Ű
[Key_Shift_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_Shift_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_Shift_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_4_ID_16]
Type=VirtualKey
Label=🠄
Cluster=Numpad
KeyCode=100
[Key_Shift_Row_4_ID_17]
Type=VirtualKey
Label=5
Cluster=Numpad
KeyCode=101
[Key_Shift_Row_4_ID_18]
Type=VirtualKey
Label=🠆
Cluster=Numpad
KeyCode=102
[Key_Shift_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_Shift_Row_5_ID_1]
Type=String
Label=Í
String=Í
[Key_Shift_Row_5_ID_2]
Type=VirtualKey
Label=Y
KeyCode=89
[Key_Shift_Row_5_ID_3]
Type=VirtualKey
Label=X
KeyCode=88
[Key_Shift_Row_5_ID_4]
Type=VirtualKey
Label=C
KeyCode=67
[Key_Shift_Row_5_ID_5]
Type=VirtualKey
Label=V
KeyCode=86
[Key_Shift_Row_5_ID_6]
Type=VirtualKey
Label=B
KeyCode=66
[Key_Shift_Row_5_ID_7]
Type=VirtualKey
Label=N
KeyCode=78
[Key_Shift_Row_5_ID_8]
Type=VirtualKey
Label=M
KeyCode=77
[Key_Shift_Row_5_ID_9]
Type=String
Label=?
String=?
[Key_Shift_Row_5_ID_10]
Type=String
Label=:
String=:
[Key_Shift_Row_5_ID_11]
Type=String
Label=_
String=_
[Key_Shift_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_Shift_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_Shift_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_Shift_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_Shift_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_5_ID_17]
Type=VirtualKey
Label=End
Cluster=Numpad
KeyCode=97
[Key_Shift_Row_5_ID_18]
Type=VirtualKey
Label=🠇
Cluster=Numpad
KeyCode=98
[Key_Shift_Row_5_ID_19]
Type=VirtualKey
Label=PgDn
Cluster=Numpad
KeyCode=99
[Key_Shift_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_Shift_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_Shift_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_Shift_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_Shift_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_Shift_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_Shift_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_Shift_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menü
KeyCode=93
NoRepeat=true
[Key_Shift_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_Shift_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_Shift_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_Shift_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_Shift_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_Shift_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_Shift_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=Insert
Cluster=Numpad
KeyCode=96
[Key_Shift_Row_6_ID_14]
Type=VirtualKey
Label=Delete
Cluster=Numpad
KeyCode=110
[Key_AltGr_Row_0_ID_0]
Type=VirtualKey
Label=Esc
Cluster=Function
KeyCode=27
NoRepeat=true
[Key_AltGr_Row_0_ID_1]
Type=Blank
Cluster=Function
[Key_AltGr_Row_0_ID_2]
Type=VirtualKey
Label=F1
Cluster=Function
KeyCode=112
[Key_AltGr_Row_0_ID_3]
Type=VirtualKey
Label=F2
Cluster=Function
KeyCode=113
[Key_AltGr_Row_0_ID_4]
Type=VirtualKey
Label=F3
Cluster=Function
KeyCode=114
[Key_AltGr_Row_0_ID_5]
Type=VirtualKey
Label=F4
Cluster=Function
KeyCode=115
[Key_AltGr_Row_0_ID_6]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_7]
Type=VirtualKey
Label=F5
Cluster=Function
KeyCode=116
[Key_AltGr_Row_0_ID_8]
Type=VirtualKey
Label=F6
Cluster=Function
KeyCode=117
[Key_AltGr_Row_0_ID_9]
Type=VirtualKey
Label=F7
Cluster=Function
KeyCode=118
[Key_AltGr_Row_0_ID_10]
Type=VirtualKey
Label=F8
Cluster=Function
KeyCode=119
[Key_AltGr_Row_0_ID_11]
Type=Blank
Width=50
Cluster=Function
[Key_AltGr_Row_0_ID_12]
Type=VirtualKey
Label=F9
Cluster=Function
KeyCode=120
[Key_AltGr_Row_0_ID_13]
Type=VirtualKey
Label=F10
Cluster=Function
KeyCode=121
[Key_AltGr_Row_0_ID_14]
Type=VirtualKey
Label=F11
Cluster=Function
KeyCode=122
[Key_AltGr_Row_0_ID_15]
Type=VirtualKey
Label=F12
Cluster=Function
KeyCode=123
[Key_AltGr_Row_0_ID_16]
Type=Blank
Width=25
Cluster=Function
[Key_AltGr_Row_0_ID_17]
Type=VirtualKey
Label=PrtSc
Cluster=Function
KeyCode=44
NoRepeat=true
[Key_AltGr_Row_0_ID_18]
Type=VirtualKey
Label=ScrLk
Cluster=Function
KeyCode=145
NoRepeat=true
[Key_AltGr_Row_0_ID_19]
Type=VirtualKey
Label=Pause
Cluster=Function
KeyCode=19
NoRepeat=true
[Key_AltGr_Row_0_ID_20]
Type=Blank
Width=25
Cluster=Extra
[Key_AltGr_Row_0_ID_21]
Type=VirtualKey
Label=⏵/⏸
Cluster=Extra
KeyCode=179
NoRepeat=true
[Key_AltGr_Row_0_ID_22]
Type=VirtualKey
Label=◼
Cluster=Extra
KeyCode=178
NoRepeat=true
[Key_AltGr_Row_0_ID_23]
Type=VirtualKey
Label=⏮
Cluster=Extra
KeyCode=177
NoRepeat=true
[Key_AltGr_Row_0_ID_24]
Type=VirtualKey
Label=⏭
Cluster=Extra
KeyCode=176
NoRepeat=true
[Key_AltGr_Row_1_ID_0]
Type=Blank
Height=25
Cluster=Function
[Key_AltGr_Row_1_ID_1]
Type=Blank
Height=25
Cluster=Extra
[Key_AltGr_Row_2_ID_0]
Type=Blank
[Key_AltGr_Row_2_ID_1]
Type=String
Label=~
String=~
[Key_AltGr_Row_2_ID_2]
Type=String
Label=ˇ
String=ˇ
[Key_AltGr_Row_2_ID_3]
Type=String
Label=^
String=^
[Key_AltGr_Row_2_ID_4]
Type=String
Label=˘
String=˘
[Key_AltGr_Row_2_ID_5]
Type=String
Label=°
String=°
[Key_AltGr_Row_2_ID_6]
Type=String
Label=˛
String=˛
[Key_AltGr_Row_2_ID_7]
Type=String
Label=`
String=`
[Key_AltGr_Row_2_ID_8]
Type=String
Label=˙
String=˙
[Key_AltGr_Row_2_ID_9]
Type=String
Label=´
String=´
[Key_AltGr_Row_2_ID_10]
Type=String
Label=˝
String=˝
[Key_AltGr_Row_2_ID_11]
Type=String
Label=¨
String=¨
[Key_AltGr_Row_2_ID_12]
Type=String
Label=¸
String=¸
[Key_AltGr_Row_2_ID_13]
Type=VirtualKey
Width=200
Label=⟵
KeyCode=8
[Key_AltGr_Row_2_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_2_ID_15]
Type=VirtualKey
Label=⏵/⏸
Cluster=Navigation
KeyCode=179
[Key_AltGr_Row_2_ID_16]
Type=VirtualKey
Label=⏮
Cluster=Navigation
KeyCode=177
[Key_AltGr_Row_2_ID_17]
Type=VirtualKey
Label=⏭
Cluster=Navigation
KeyCode=176
[Key_AltGr_Row_2_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_2_ID_19]
Type=VirtualKey
Label=Num\nLock
Cluster=Numpad
KeyCode=144
NoRepeat=true
[Key_AltGr_Row_2_ID_20]
Type=VirtualKey
Label=/
Cluster=Numpad
KeyCode=111
[Key_AltGr_Row_2_ID_21]
Type=VirtualKey
Label=*
Cluster=Numpad
KeyCode=106
[Key_AltGr_Row_2_ID_22]
Type=VirtualKey
Label=-
Cluster=Numpad
KeyCode=109
[Key_AltGr_Row_3_ID_0]
Type=VirtualKey
Width=150
Label=⭾
KeyCode=9
[Key_AltGr_Row_3_ID_1]
Type=String
Label=\
String=\
[Key_AltGr_Row_3_ID_2]
Type=String
Label=|
String=|
[Key_AltGr_Row_3_ID_3]
Type=String
Label=Ä
String=Ä
[Key_AltGr_Row_3_ID_4]
Type=Blank
[Key_AltGr_Row_3_ID_5]
Type=Blank
[Key_AltGr_Row_3_ID_6]
Type=Blank
[Key_AltGr_Row_3_ID_7]
Type=String
Label=€
String=€
[Key_AltGr_Row_3_ID_8]
Type=String
Label=Í
String=Í
[Key_AltGr_Row_3_ID_9]
Type=Blank
[Key_AltGr_Row_3_ID_10]
Type=Blank
[Key_AltGr_Row_3_ID_11]
Type=String
Label=÷
String=÷
[Key_AltGr_Row_3_ID_12]
Type=String
Label=×
String=×
[Key_AltGr_Row_3_ID_13]
Type=VirtualKeyIsoEnter
Width=150
Label=
KeyCode=13
[Key_AltGr_Row_3_ID_14]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_3_ID_15]
Type=VirtualKey
Label=🔇 X
Cluster=Navigation
KeyCode=173
[Key_AltGr_Row_3_ID_16]
Type=VirtualKey
Label=🔈--
Cluster=Navigation
KeyCode=174
[Key_AltGr_Row_3_ID_17]
Type=VirtualKey
Label=🔈++
Cluster=Navigation
KeyCode=175
[Key_AltGr_Row_3_ID_18]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_3_ID_19]
Type=VirtualKey
Label=7\nHome
Cluster=Numpad
KeyCode=103
[Key_AltGr_Row_3_ID_20]
Type=VirtualKey
Label=8\n🠅
Cluster=Numpad
KeyCode=104
[Key_AltGr_Row_3_ID_21]
Type=VirtualKey
Label=9\nPgUp
Cluster=Numpad
KeyCode=105
[Key_AltGr_Row_3_ID_22]
Type=VirtualKey
Height=200
Label=+
Cluster=Numpad
KeyCode=107
[Key_AltGr_Row_4_ID_0]
Type=VirtualKey
Width=175
Label=🡇
KeyCode=20
NoRepeat=true
[Key_AltGr_Row_4_ID_1]
Type=String
Label=ä
String=ä
[Key_AltGr_Row_4_ID_2]
Type=String
Label=đ
String=đ
[Key_AltGr_Row_4_ID_3]
Type=String
Label=Đ
String=Đ
[Key_AltGr_Row_4_ID_4]
Type=String
Label=[
String=[
[Key_AltGr_Row_4_ID_5]
Type=String
Label=]
String=]
[Key_AltGr_Row_4_ID_6]
Type=Blank
[Key_AltGr_Row_4_ID_7]
Type=String
Label=í
String=í
[Key_AltGr_Row_4_ID_8]
Type=String
Label=ł
String=ł
[Key_AltGr_Row_4_ID_9]
Type=String
Label=Ł
String=Ł
[Key_AltGr_Row_4_ID_10]
Type=String
Label=$
String=$
[Key_AltGr_Row_4_ID_11]
Type=String
Label=ß
String=ß
[Key_AltGr_Row_4_ID_12]
Type=String
Label=¤
String=¤
[Key_AltGr_Row_4_ID_13]
Type=VirtualKeyIsoEnter
Width=125
Label=↵
KeyCode=13
[Key_AltGr_Row_4_ID_14]
Type=Blank
Width=325
Cluster=Navigation
[Key_AltGr_Row_4_ID_15]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_4_ID_16]
Type=VirtualKey
Label=4\n🠄
Cluster=Numpad
KeyCode=100
[Key_AltGr_Row_4_ID_17]
Type=VirtualKey
Label=5\n
Cluster=Numpad
KeyCode=101
[Key_AltGr_Row_4_ID_18]
Type=VirtualKey
Label=6\n🠆
Cluster=Numpad
KeyCode=102
[Key_AltGr_Row_5_ID_0]
Type=VirtualKeyToggle
Width=125
Label=🡅
KeyCode=160
NoRepeat=true
[Key_AltGr_Row_5_ID_1]
Type=String
Label=<
String=<
[Key_AltGr_Row_5_ID_2]
Type=String
Label=>
String=>
[Key_AltGr_Row_5_ID_3]
Type=String
Label=#
String=#
[Key_AltGr_Row_5_ID_4]
Type=String
Label=&
String=&
[Key_AltGr_Row_5_ID_5]
Type=String
Label=@
String=@
[Key_AltGr_Row_5_ID_6]
Type=String
Label={
String={
[Key_AltGr_Row_5_ID_7]
Type=String
Label=}
String=}
[Key_AltGr_Row_5_ID_8]
Type=String
Label=<
String=<
[Key_AltGr_Row_5_ID_9]
Type=String
Label=;
String=;
[Key_AltGr_Row_5_ID_10]
Type=String
Label=>
String=>
[Key_AltGr_Row_5_ID_11]
Type=String
Label=*
String=*
[Key_AltGr_Row_5_ID_12]
Type=VirtualKeyToggle
Width=275
Label=🡅
KeyCode=161
NoRepeat=true
[Key_AltGr_Row_5_ID_13]
Type=Blank
Width=125
Cluster=Navigation
[Key_AltGr_Row_5_ID_14]
Type=VirtualKey
Label=🠅
Cluster=Navigation
KeyCode=38
[Key_AltGr_Row_5_ID_15]
Type=Blank
Cluster=Navigation
[Key_AltGr_Row_5_ID_16]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_5_ID_17]
Type=VirtualKey
Label=1\nEnd
Cluster=Numpad
KeyCode=97
[Key_AltGr_Row_5_ID_18]
Type=VirtualKey
Label=2\n🠇
Cluster=Numpad
KeyCode=98
[Key_AltGr_Row_5_ID_19]
Type=VirtualKey
Label=3\nPgDn
Cluster=Numpad
KeyCode=99
[Key_AltGr_Row_5_ID_20]
Type=VirtualKey
Height=200
Label=Enter
Cluster=Numpad
KeyCode=13
[Key_AltGr_Row_6_ID_0]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=162
NoRepeat=true
[Key_AltGr_Row_6_ID_1]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=91
NoRepeat=true
[Key_AltGr_Row_6_ID_2]
Type=VirtualKeyToggle
Width=125
Label=Alt
KeyCode=164
NoRepeat=true
[Key_AltGr_Row_6_ID_3]
Type=VirtualKey
Width=625
Label=
KeyCode=32
[Key_AltGr_Row_6_ID_4]
Type=VirtualKeyToggle
Width=125
Label=AltGr
KeyCode=165
NoRepeat=true
[Key_AltGr_Row_6_ID_5]
Type=VirtualKeyToggle
Width=125
Label=Win
KeyCode=92
NoRepeat=true
[Key_AltGr_Row_6_ID_6]
Type=VirtualKey
Width=125
Label=Menü
KeyCode=93
NoRepeat=true
[Key_AltGr_Row_6_ID_7]
Type=VirtualKeyToggle
Width=125
Label=Ctrl
KeyCode=163
NoRepeat=true
[Key_AltGr_Row_6_ID_8]
Type=Blank
Width=25
Cluster=Navigation
[Key_AltGr_Row_6_ID_9]
Type=VirtualKey
Label=🠄
Cluster=Navigation
KeyCode=37
[Key_AltGr_Row_6_ID_10]
Type=VirtualKey
Label=🠇
Cluster=Navigation
KeyCode=40
[Key_AltGr_Row_6_ID_11]
Type=VirtualKey
Label=🠆
Cluster=Navigation
KeyCode=39
[Key_AltGr_Row_6_ID_12]
Type=Blank
Width=25
Cluster=Numpad
[Key_AltGr_Row_6_ID_13]
Type=VirtualKey
Width=200
Label=0\nInsert
Cluster=Numpad
KeyCode=96
[Key_AltGr_Row_6_ID_14]
Type=VirtualKey
Label=,\nDelete
Cluster=Numpad
KeyCode=110
================================================
FILE: assets/lang/de.ini
================================================
[TranslationInfo]
Name=Deutsch (German)
Locale=de-DE
[Strings]
;Settings Window
tstr_SettingsWindowTitle=Desktop+ Einstellungen
tstr_SettingsCatInterface=Oberfläche
tstr_SettingsCatEnvironment=Umgebung
tstr_SettingsCatProfiles=Profile
tstr_SettingsCatActions=Aktionen
tstr_SettingsCatKeyboard=Tastatur
tstr_SettingsCatMouse=Maus
tstr_SettingsCatLaserPointer=Laserpointer
tstr_SettingsCatWindowOverlays=Fenster-Overlays
tstr_SettingsCatBrowser=Browser
tstr_SettingsCatPerformance=Leistung
tstr_SettingsCatVersionInfo=Versionsdetails
tstr_SettingsCatWarnings=Warnungen
tstr_SettingsCatStartup=Startvorgang
tstr_SettingsCatTroubleshooting=Problembehandlung
tstr_SettingsWarningPrefix=Warnung:
tstr_SettingsWarningCompositorResolution=Auflösung des SteamVR-Compositors ist unter 100%! Dies beeinträchtigt Overlay-Renderqualität.
tstr_SettingsWarningCompositorQuality=SteamVR Overlay-Renderqualität ist nicht auf hoch eingestellt!
tstr_SettingsWarningProcessElevated=Desktop+ wird mit administrativen Rechten ausgeführt!
tstr_SettingsWarningElevatedMode=Erhöhter Modus ist aktiv!
tstr_SettingsWarningElevatedProcessFocus=Ein Prozess mit erhöhten Rechten hat Fokus!\nDesktop+ ist derzeit nicht in der Lage Eingaben zu simulieren.
tstr_SettingsWarningBrowserMissing=Es werden Browser-Overlays verwendet, aber die Desktop+ Browser-Komponente ist derzeit nicht verfügbar.
tstr_SettingsWarningBrowserMismatch=Die installierte Desktop+ Browser-Komponente ist nicht mit dieser Version von Desktop+ kompatibel!
tstr_SettingsWarningUIAccessLost=Desktop+ wird nicht mehr mit UIAccess Rechten ausgeführt!
tstr_SettingsWarningOverlayCreationErrorLimit=Eine Overlayerstellung ist fehlgeschlagen! (Maximales Overlaylimit wurde überschritten)
tstr_SettingsWarningOverlayCreationErrorOther=Eine Overlayerstellung ist fehlgeschlagen! (%ERRORNAME%)
tstr_SettingsWarningGraphicsCaptureError=Ein unerwarteter Fehler ist in einem Graphics Capture-Thread aufgetreten! (%ERRORCODE%)
tstr_SettingsWarningAppProfileActive=Das Anwendungs-Profil für %APPNAME% hat das bestehende Overlaylayout überschrieben.\nÄnderungen an den Overlays werden nicht automatisch gespeichert während es aktiv ist.
tstr_SettingsWarningConfigMigrated=Einstellungen und Profile der vorherigen Desktop+ Version wurden in neue Formate migriert.\nDie ursprünglichen Dateien wurden nicht gelöscht und können weiterhin mit einer älteren Version der Anwendung verwendet werden.\nUm stattdessen von Null zu beginnen, siehe [Auf Standardeinstellungen zurücksetzen].
tstr_SettingsWarningMenuDontShowAgain=Nicht mehr zeigen
tstr_SettingsWarningMenuDismiss=Ausblenden
tstr_SettingsInterfaceLanguage=Sprache
tstr_SettingsInterfaceLanguageCommunity=Community-Übersetzung von %AUTHOR%
tstr_SettingsInterfaceLanguageIncompleteWarning=Übersetzung ist möglicherweise nicht vollständig
tstr_SettingsInterfaceAdvancedSettings=Zeige erweiterte Einstellungen
tstr_SettingsInterfaceAdvancedSettingsTip=Zeige erweiterte und seltener verwendete Einstellungen
tstr_SettingsInterfaceBlankSpaceDrag=Verschiebe Fenster, wenn auf leeren Bereich geklickt wird
tstr_SettingsInterfacePersistentUI=Persistente UI
tstr_SettingsInterfacePersistentUIManage=Verwalten
tstr_SettingsInterfaceDesktopButtons=Desktop-Schaltflächenstil
tstr_SettingsInterfaceDesktopButtonsNone=Keine
tstr_SettingsInterfaceDesktopButtonsIndividual=Individuelle Desktops
tstr_SettingsInterfaceDesktopButtonsCycle=Zyklische Schaltflächen
tstr_SettingsInterfaceDesktopButtonsAddCombined=Gesamten Desktop hinzufügen
tstr_SettingsInterfacePersistentUIHelp=Desktop+ merkt sich die Zustände der UI-Fenster getrennt für die allgemeine Nutzung (Global) und der Nutzung innerhalb des Desktop+ SteamVR Dashboard-Tabs (Desktop+ Tab).
tstr_SettingsInterfacePersistentUIHelp2=Die folgenden Bedienelemente können zur direkten Manipulation der Fensterzustände verwendet werden.\nBeachte, dass einige Zustände das Zurücksetzen der Position erfordern um das Fenster an eine sichtbare Stelle zu bringen.
tstr_SettingsInterfacePersistentUIWindowsHeader=Fenster
tstr_SettingsInterfacePersistentUIWindowsSettings=Einstellungen
tstr_SettingsInterfacePersistentUIWindowsProperties=Overlay-Eigenschaften
tstr_SettingsInterfacePersistentUIWindowsKeyboard=Desktop+ Tastatur
tstr_SettingsInterfacePersistentUIWindowsStateGlobal=Global
tstr_SettingsInterfacePersistentUIWindowsStateDashboardTab=Desktop+ Tab
tstr_SettingsInterfacePersistentUIWindowsStateVisible=Sichtbar
tstr_SettingsInterfacePersistentUIWindowsStatePinned=Angepinnt
tstr_SettingsInterfacePersistentUIWindowsStatePosition=Position
tstr_SettingsInterfacePersistentUIWindowsStatePositionReset=Zurücksetzen
tstr_SettingsInterfacePersistentUIWindowsStateSize=Größe
tstr_SettingsInterfacePersistentUIWindowsStateLaunchRestore=Zustand bei Desktop+-Start wiederherstellen
tstr_SettingsEnvironmentBackgroundColor=Hintergrundfarbe
tstr_SettingsEnvironmentBackgroundColorDispModeNever=Nie sichtbar
tstr_SettingsEnvironmentBackgroundColorDispModeDPlusTab=Nur im Desktop+ Tab sichtbar
tstr_SettingsEnvironmentBackgroundColorDispModeAlways=Immer sichtbar
tstr_SettingsEnvironmentDimInterface=Oberfläche dimmen
tstr_SettingsEnvironmentDimInterfaceTip=Dimmt das SteamVR Dashboard und die Desktop+ Oberfläche während der Desktop+ Dashboard-Tab offen ist
tstr_SettingsProfilesOverlays=Overlay-Profile
tstr_SettingsProfilesApps=Anwendungs-Profile
tstr_SettingsProfilesManage=Verwalten
tstr_SettingsProfilesOverlaysHeader=Overlay-Profile verwalten
tstr_SettingsProfilesOverlaysNameDefault=Standardprofil
tstr_SettingsProfilesOverlaysNameNew=[Neues Profil]
tstr_SettingsProfilesOverlaysNameNewBase=Profil %ID%
tstr_SettingsProfilesOverlaysProfileLoad=Profil laden
tstr_SettingsProfilesOverlaysProfileAdd=Aus Profil hinzufügen
tstr_SettingsProfilesOverlaysProfileSave=Aktuelle Overlays speichern
tstr_SettingsProfilesOverlaysProfileDelete=Profil löschen
tstr_SettingsProfilesOverlaysProfileDeleteConfirm=Wirklich?
tstr_SettingsProfilesOverlaysProfileFailedLoad=Laden des Profils fehlgeschlagen
tstr_SettingsProfilesOverlaysProfileFailedDelete=Löschen des Profils fehlgeschlagen
tstr_SettingsProfilesOverlaysProfileAddSelectHeader=Profil zum Hinzufügen auswählen
tstr_SettingsProfilesOverlaysProfileAddSelectEmpty=Dieses Profil enthält keine Overlays
tstr_SettingsProfilesOverlaysProfileAddSelectDo=Ausgewählte Overlays hinzufügen
tstr_SettingsProfilesOverlaysProfileAddSelectAll=Alle auswählen
tstr_SettingsProfilesOverlaysProfileAddSelectNone=Keine auswählen
tstr_SettingsProfilesOverlaysProfileSaveSelectHeader=Aktuelle Overlays speichern
tstr_SettingsProfilesOverlaysProfileSaveSelectName=Profilname
tstr_SettingsProfilesOverlaysProfileSaveSelectNameErrorBlank=Name darf nicht leer sein
tstr_SettingsProfilesOverlaysProfileSaveSelectNameErrorTaken=Name ist bereits in Verwendung
tstr_SettingsProfilesOverlaysProfileSaveSelectHeaderList=Overlays zum Speichern im Profil auswählen
tstr_SettingsProfilesOverlaysProfileSaveSelectDo=Ausgewählte Overlays speichern
tstr_SettingsProfilesOverlaysProfileSaveSelectDoFailed=Speichern des Profils fehlgeschlagen
tstr_SettingsProfilesAppsHeader=Anwendungs-Profile verwalten
tstr_SettingsProfilesAppsHeaderNoVRTip=Während Desktop+ nicht läuft werden nur existierende Anwendungs-Profile gelistet
tstr_SettingsProfilesAppsListEmpty=Keine Anwendungen verfügbar
tstr_SettingsProfilesAppsProfileHeaderActive=(derzeit aktiv)
tstr_SettingsProfilesAppsProfileEnabled=Aktiviere, wenn Anwendung läuft
tstr_SettingsProfilesAppsProfileOverlayProfile=Overlay-Profil
tstr_SettingsProfilesAppsProfileActionEnter=Start-Aktion
tstr_SettingsProfilesAppsProfileActionLeave=End-Aktion
tstr_SettingsActionsManage=Aktionen
tstr_SettingsActionsManageButton=Verwalten
tstr_SettingsActionsButtonsOrderDefault=Aktions-Schaltflächen (Standard)
tstr_SettingsActionsButtonsOrderOverlayBar=Aktions-Schaltflächen (Overlay-Bar)
tstr_SettingsActionsShowBindings=Zeige Controllerzuordnungen
tstr_SettingsActionsActiveShortcuts=Aktive Controllertasten
tstr_SettingsActionsActiveShortcutsTip=Controllerzuordnungen während auf ein Overlay gezeigt wird.\nBearbeite die VR Dashboard Controllerzuordnungen um zu ändern welche Tasten diese sind.
tstr_SettingsActionsActiveShortuctsHome="Zur Startseite"
tstr_SettingsActionsActiveShortuctsBack="Zurück"
tstr_SettingsActionsGlobalShortcuts=Globale Controllertasten
tstr_SettingsActionsGlobalShortcutsTip=Controllerzuordnungen während das Dashboard geschlossen ist und auf kein Overlay gezeigt wird.\nBearbeite die Desktop+ Controllerzuordnungnen um zu ändern welche Tasten diese sind.
tstr_SettingsActionsGlobalShortcutsEntry=Globales Kürzel #%ID%
tstr_SettingsActionsGlobalShortcutsAdd=Kürzel hinzufügen
tstr_SettingsActionsGlobalShortcutsRemove=Letztes Kürzel entfernen
tstr_SettingsActionsHotkeys=Hotkeys
tstr_SettingsActionsHotkeysTip=Systemweite Tastenkürzel.\nHotkey-Eingaben werden von anderen Anwendungen nicht erfasst und funktionieren möglicherweise nicht falls die selbe Tastenkombination schon woanders registriert wurde.
tstr_SettingsActionsHotkeysAdd=Hotkey hinzufügen
tstr_SettingsActionsHotkeysRemove=Entfernen
tstr_SettingsActionsTableHeaderAction=Aktion
tstr_SettingsActionsTableHeaderShortcut=Kürzel
tstr_SettingsActionsTableHeaderHotkey=Hotkey
tstr_SettingsActionsManageHeader=Aktionen verwalten
tstr_SettingsActionsManageCopyUID=UID kopieren
tstr_SettingsActionsManageNew=Neue Aktion...
tstr_SettingsActionsManageEdit=Aktion bearbeiten
tstr_SettingsActionsManageDuplicate=Aktion duplizieren
tstr_SettingsActionsManageDelete=Aktion löschen
tstr_SettingsActionsManageDeleteConfirm=Wirklich?
tstr_SettingsActionsManageDuplicatedName=%NAME% (Kopie)
tstr_SettingsActionsEditHeader=Aktion bearbeiten
tstr_SettingsActionsEditName=Name
tstr_SettingsActionsEditNameTranslatedTip=Diese Aktion verwendet momentan eine Übersetzungstext-ID als Name, um sich automatisch der gewählten Sprache anzupassen
tstr_SettingsActionsEditTarget=Ziel
tstr_SettingsActionsEditTargetDefault=Standard
tstr_SettingsActionsEditTargetDefaultTip=Verwendet das Overlay als Ziel, auf dem die Aktion ausgelöst wurde.\nFalls letzteres nicht existiert, wird das fokussierte Overlay verwendet.
tstr_SettingsActionsEditTargetUseTags=Verwende Tags
tstr_SettingsActionsEditTargetActionTarget=Aktions-Ziel
tstr_SettingsActionsEditHeaderAppearance=Schaltflächen-Erscheinungsbild
tstr_SettingsActionsEditIcon=Icon
tstr_SettingsActionsEditLabel=Beschriftung
tstr_SettingsActionsEditLabelTranslatedTip=Diese Aktion verwendet momentan eine Übersetzungstext-ID als Beschriftung, um sich automatisch der gewählten Sprache anzupassen
tstr_SettingsActionsEditHeaderCommands=Befehle
tstr_SettingsActionsEditNameNew=Neue Aktion
tstr_SettingsActionsEditCommandAdd=Befehl hinzufügen
tstr_SettingsActionsEditCommandDelete=Befehl löschen
tstr_SettingsActionsEditCommandDeleteConfirm=Wirklich?
tstr_SettingsActionsEditCommandType=Befehlstyp
tstr_SettingsActionsEditCommandTypeNone=Kein
tstr_SettingsActionsEditCommandTypeKey=Drücke Taste
tstr_SettingsActionsEditCommandTypeMousePos=Setze Mausposition
tstr_SettingsActionsEditCommandTypeString=Tippe Text
tstr_SettingsActionsEditCommandTypeLaunchApp=Starte Anwendung
tstr_SettingsActionsEditCommandTypeShowKeyboard=Zeige Tastatur
tstr_SettingsActionsEditCommandTypeCropActiveWindow=Schneide zum aktiven Fenster zu
tstr_SettingsActionsEditCommandTypeShowOverlay=Zeige Overlay
tstr_SettingsActionsEditCommandTypeSwitchTask=Wechsle Task
tstr_SettingsActionsEditCommandTypeLoadOverlayProfile=Lade Overlay-Profil
tstr_SettingsActionsEditCommandTypeUnknown=Unbekannt
tstr_SettingsActionsEditCommandVisibilityToggle=Umschalten
tstr_SettingsActionsEditCommandVisibilityShow=Immer zeigen
tstr_SettingsActionsEditCommandVisibilityHide=Immer verstecken
tstr_SettingsActionsEditCommandUndo=Bei Loslassen umkehren
tstr_SettingsActionsEditCommandKeyCode=Tastencode
tstr_SettingsActionsEditCommandKeyToggle=Taste umschalten
tstr_SettingsActionsEditCommandMouseX=X
tstr_SettingsActionsEditCommandMouseY=Y
tstr_SettingsActionsEditCommandMouseUseCurrent=Verwende momentane Mausposition
tstr_SettingsActionsEditCommandString=Text
tstr_SettingsActionsEditCommandPath=Anwendungspfad
tstr_SettingsActionsEditCommandPathTip=Dies kann auch eine normale Datei oder URL sein
tstr_SettingsActionsEditCommandArgs=Anwendungsparameter
tstr_SettingsActionsEditCommandArgsTip=Diese werden zur Anwendung weitergeleitet.\nFalls unsicher, leer lassen.
tstr_SettingsActionsEditCommandVisibility=Sichtbarkeit
tstr_SettingsActionsEditCommandSwitchingMethod=Wechselmethode
tstr_SettingsActionsEditCommandSwitchingMethodSwitcher=Zeige Task-Wechsler
tstr_SettingsActionsEditCommandSwitchingMethodFocus=Fokussiere Fenster
tstr_SettingsActionsEditCommandWindow=Fenster
tstr_SettingsActionsEditCommandWindowNone=[Kein Fenster]
tstr_SettingsActionsEditCommandWindowStrictMatchingTip=Erlaube nur exakte Fenstertitel-Übereinstimmungen, wenn ein Fenster zum fokussieren gesucht wird
tstr_SettingsActionsEditCommandCursorWarp=Bewege Mauszeiger in das Fenster
tstr_SettingsActionsEditCommandProfile=Profil
tstr_SettingsActionsEditCommandProfileClear=Entferne bestehende Overlays
tstr_SettingsActionsEditCommandDescNone=Kein Befehl
tstr_SettingsActionsEditCommandDescKey=Drücke Taste "%KEYNAME%"
tstr_SettingsActionsEditCommandDescKeyToggle=Schalte Taste "%KEYNAME%" um
tstr_SettingsActionsEditCommandDescMousePos=Setze Mausposition zu %X%, %Y%
tstr_SettingsActionsEditCommandDescString=Tippe Text "%STRING%"
tstr_SettingsActionsEditCommandDescLaunchApp=Starte Anwendung "%APP%" %ARGSOPT%
tstr_SettingsActionsEditCommandDescLaunchAppArgsOpt="%ARGS%"
tstr_SettingsActionsEditCommandDescKeyboardToggle=Schalte Tastatur um
tstr_SettingsActionsEditCommandDescKeyboardShow=Zeige Tastatur
tstr_SettingsActionsEditCommandDescKeyboardHide=Verstecke Tastatur
tstr_SettingsActionsEditCommandDescCropWindow=Schneide zum aktiven Fenster zu
tstr_SettingsActionsEditCommandDescOverlayToggle=Schalte Overlay um: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayShow=Zeige Overlay: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayHide=Verstecke Overlay: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayTargetDefault=[Aktions-Ziel]
tstr_SettingsActionsEditCommandDescSwitchTask=Wechsle Task
tstr_SettingsActionsEditCommandDescSwitchTaskWindow=Wechsle Task zu "%WINDOW%"
tstr_SettingsActionsEditCommandDescLoadOverlayProfile=Lade Overlay-Profil "%PROFILE%"
tstr_SettingsActionsEditCommandDescLoadOverlayProfileAdd=Füge Overlay-Profil "%PROFILE%" hinzu
tstr_SettingsActionsEditCommandDescUnknown=Unbekannter Befehl
tstr_SettingsActionsOrderHeader=Ändere Aktions-Anordnung
tstr_SettingsActionsOrderButtonLabel=%COUNT% Aktionen ausgewählt
tstr_SettingsActionsOrderButtonLabelSingular=%COUNT% Aktion ausgewählt
tstr_SettingsActionsOrderNoActions=Keine Aktionen ausgewählt
tstr_SettingsActionsOrderAdd=Aktionen hinzufügen...
tstr_SettingsActionsOrderRemove=Aktion entfernen
tstr_SettingsActionsAddSelectorHeader=Wähle Aktionen zum Hinzufügen
tstr_SettingsActionsAddSelectorAdd=Ausgewählte Aktionen hinzufügen
tstr_SettingsKeyboardLayout=Tastaturlayout
tstr_SettingsKeyboardSize=Größe
tstr_SettingsKeyboardBehavior=Verhalten
tstr_SettingsKeyboardStickyMod=Umschalttasten einrasten
tstr_SettingsKeyboardKeyRepeat=Tastenwiederholung
tstr_SettingsKeyboardAutoShow=Automatisch einblenden
tstr_SettingsKeyboardAutoShowDesktopOnly=Automatisch einblenden
tstr_SettingsKeyboardAutoShowDesktop=Für Desktop- & Fenster-Overlays
tstr_SettingsKeyboardAutoShowDesktopTip=Experimentell. Funktioniert nicht mit allen Anwendungen.
tstr_SettingsKeyboardAutoShowBrowser=Für Browser-Overlays
tstr_SettingsKeyboardLayoutAuthor=Erstellt von %AUTHOR%
tstr_SettingsKeyboardKeyClusters=Tastengruppen
tstr_SettingsKeyboardKeyClusterBase=Basis
tstr_SettingsKeyboardKeyClusterFunction=Funktion
tstr_SettingsKeyboardKeyClusterNavigation=Navigation
tstr_SettingsKeyboardKeyClusterNumpad=Ziffernblock
tstr_SettingsKeyboardKeyClusterExtra=Extra
tstr_SettingsKeyboardSwitchToEditor=Zu Tastaturlayout-Editor wechseln
tstr_SettingsMouseShowCursor=Zeige Mauszeiger
tstr_SettingsMouseShowCursorGCUnsupported=Das Deaktivieren des Mauszeigers für Graphics Capture-Overlays wird von diesem System nicht unterstützt
tstr_SettingsMouseShowCursorGCActiveWarning=Aktive Graphics Capture-Bilderfassungen verhindern möglicherweise, dass der Mauszeiger in Desktop Duplication-Overlays ausgeblendet werden kann
tstr_SettingsMouseScrollSmooth=Verwende weiches Scrollen
tstr_SettingsMouseSimulatePen=Simuliere als Stifteingabe
tstr_SettingsMouseSimulatePenUnsupported=Simulation von Stifteingaben wird von diesem System nicht unterstützt
tstr_SettingsMouseAllowLaserPointerOverride=Erlaube Laserpointer-Überbrückung
tstr_SettingsMouseAllowLaserPointerOverrideTip=Deaktiviert den Laserpointer, wenn die physische Maus ruckartig bewegt wird.\nKlicke auf ein Overlay um den Laserpointer wieder zu aktivieren.
tstr_SettingsMouseDoubleClickAssist=Doppelklick-Assistent
tstr_SettingsMouseDoubleClickAssistTip=Friert den Mauszeiger für die angegebene Zeit ein, um Eingaben von Doppelklicks zu erleichtern.
tstr_SettingsMouseDoubleClickAssistTipValueOff=Aus
tstr_SettingsMouseDoubleClickAssistTipValueAuto=Auto
tstr_SettingsMouseSmoothing=Eingabeglättung
tstr_SettingsMouseSmoothingLevelNone=Keine
tstr_SettingsMouseSmoothingLevelVeryLow=Sehr Niedrig
tstr_SettingsMouseSmoothingLevelLow=Niedrig
tstr_SettingsMouseSmoothingLevelMedium=Mittel
tstr_SettingsMouseSmoothingLevelHigh=Hoch
tstr_SettingsMouseSmoothingLevelVeryHigh=Sehr Hoch
tstr_SettingsLaserPointerTip=Diese Einstellungen gelten für Desktop+'s Laserpointer, welcher verwendet wird, wenn das SteamVR Dashboard geschlossen ist
tstr_SettingsLaserPointerBlockInput=Blockiere Spiel-Eingabe, wenn aktiv
tstr_SettingsLaserPointerAutoToggleDistance=Maximale Auto-Aktivierungsdistanz
tstr_SettingsLaserPointerAutoToggleDistanceValueOff=Aus
tstr_SettingsLaserPointerHMDPointer=Blick-basierter HMD-Pointer
tstr_SettingsLaserPointerHMDPointerTableHeaderInputAction=Eingabeaktion
tstr_SettingsLaserPointerHMDPointerTableHeaderBinding=Tastatureingabe
tstr_SettingsLaserPointerHMDPointerTableBindingToggle=Laserpointer umschalten
tstr_SettingsLaserPointerHMDPointerTableBindingLeft=Linke Maustaste
tstr_SettingsLaserPointerHMDPointerTableBindingRight=Rechte Maustaste
tstr_SettingsLaserPointerHMDPointerTableBindingMiddle=Mittlere Maustaste
tstr_SettingsLaserPointerHMDPointerTableBindingDrag=Overlay ziehen
tstr_SettingsWindowOverlaysAutoFocus=Fokussiere Fenster, wenn auf ein Overlay gezeigt wird
tstr_SettingsWindowOverlaysKeepOnScreen=Halte Fenster auf dem Bildschirm
tstr_SettingsWindowOverlaysKeepOnScreenTip=Bewegt das Quellfenster in den Arbeitsbereich des Bildschirms, wenn es außerhalb dessen liegt
tstr_SettingsWindowOverlaysAutoSizeOverlay=Passe Overlaygröße an, wenn die Fenstergröße sich ändert
tstr_SettingsWindowOverlaysFocusSceneApp=Fokussiere Spiel, wenn der Laserpointer das Overlay verlässt
tstr_SettingsWindowOverlaysFocusSceneAppDashboard=Fokussiere Spiel, nachdem das Dashboard geschlossen wurde
tstr_SettingsWindowOverlaysOnWindowDrag=Bei Fenster-Verschiebung
tstr_SettingsWindowOverlaysOnWindowDragDoNothing=Nichts tun
tstr_SettingsWindowOverlaysOnWindowDragBlock=Blockiere Verschiebung
tstr_SettingsWindowOverlaysOnWindowDragOverlay=Verschiebe Overlay
tstr_SettingsWindowOverlaysOnCaptureLoss=Bei Bilderfassungs-Verlust
tstr_SettingsWindowOverlaysOnCaptureLossTip=Verhalten, wenn eine Fenster-Bilderfassung endet, in der Regel aufgrund vom Schließen des Zielfensters.\n"Verstecke Overlay" zeigt das Overlay auch wieder, wenn die Erfassung wiederhergestellt wird.
tstr_SettingsWindowOverlaysOnCaptureLossDoNothing=Nichts tun
tstr_SettingsWindowOverlaysOnCaptureLossHide=Verstecke Overlay
tstr_SettingsWindowOverlaysOnCaptureLossRemove=Entferne Overlay
tstr_SettingsBrowserMaxFrameRate=Maximale Frame-Rate
tstr_SettingsBrowserMaxFrameRateOverrideOff=Globale Einstellung
tstr_SettingsBrowserContentBlocker=Inhaltsblockierung
tstr_SettingsBrowserContentBlockerTip=Kopiere Blocklisten im Adblock Plus-Syntax in das "DesktopPlusBrowser\content_block" Verzeichnis.\nEs werden alle Listen im Verzeichnis geladen.
tstr_SettingsBrowserContentBlockerListCount=(%LISTCOUNT% Listen aktiv)
tstr_SettingsBrowserContentBlockerListCountSingular=(%LISTCOUNT% Liste aktiv)
tstr_SettingsPerformanceUpdateLimiter=Limitiere Updates
tstr_SettingsPerformanceUpdateLimiterMode=Updatelimiter-Modus
tstr_SettingsPerformanceUpdateLimiterModeOff=Aus
tstr_SettingsPerformanceUpdateLimiterModeMS=Frame-Zeit
tstr_SettingsPerformanceUpdateLimiterModeFPS=Frame-Rate
tstr_SettingsPerformanceUpdateLimiterModeOffOverride=Verwende globale Einstellung
tstr_SettingsPerformanceUpdateLimiterModeMSTip="Frame-Zeit" erzwingt ein Mindestintervall zwischen Overlayupdates
tstr_SettingsPerformanceUpdateLimiterFPSValue=%FPS% fps
tstr_SettingsPerformanceUpdateLimiterOverride=Überschreibe Updatelimit
tstr_SettingsPerformanceUpdateLimiterOverrideTip=Wenn mehrere Überschreibungen aktiv sind, wird diejenige verwendet, die in der höchsten Update-Rate resultiert
tstr_SettingsPerformanceUpdateLimiterModeOverride=Überschreibe Updatelimiter
tstr_SettingsPerformanceRapidUpdates=Verringere Laserpointer-Latenz
tstr_SettingsPerformanceRapidUpdatesTip=Verringere die Latenz von Laserpointer-Eingaben auf Kosten zusätzlicher CPU-Last, solange auf ein Overlay gezeigt wird
tstr_SettingsPerformanceSingleDesktopMirror=Einzelne Desktopspiegelung
tstr_SettingsPerformanceSingleDesktopMirrorTip=Spiegele individuelle Desktops, wenn zu ihnen gewechselt wird, anstatt vom gesamten Desktop zuzuschneiden.\nSolange diese Einstellung aktiv ist, werden alle Overlays den selben Desktop zeigen.
tstr_SettingsPerformanceAlternativeCursorRendering=Alternative Mauszeigerdarstellung
tstr_SettingsPerformanceAlternativeCursorRenderingTip=Verwende eine andere Methode zum Erfassen und Darstellen des Mauszeigers.\nKann helfen, wenn mit der Standardmethode Probleme bei der Darstellung des Hardware-Mauszeigers auftreten.
tstr_SettingsPerformanceUseHDR=HDR-Spiegelung
tstr_SettingsPerformanceUseHDRTip=Verwende beim Spiegeln von Desktops und Fenstern Texturen mit höherer Bittiefe, zur HDR-Unterstützung. Experimentell.\nKann die Leistung beeinträchtigen, falls nicht erforderlich, und erhöht VRAM-Verbrauch.
tstr_SettingsPerformanceShowFPS=Zeige FPS in Floating UI
tstr_SettingsWarningsHidden=Versteckte Warnungen:
tstr_SettingsWarningsReset=Versteckte Warnungen zurücksetzen
tstr_SettingsStartupAutoLaunch=Starte automatisch bei SteamVR-Start
tstr_SettingsStartupSteamDisable=Deaktiviere Steam-Integration
tstr_SettingsStartupSteamDisableTip=Startet Desktop+ ohne Steam neu, wenn es davon gestartet wurde.\nDies deaktiviert den permanenten In-App-Status, Nutzungszeit-Statistik und andere Steam-Funktionen.
tstr_SettingsTroubleshootingRestart=Neustarten
tstr_SettingsTroubleshootingRestartSteam=Mit Steam neu starten
tstr_SettingsTroubleshootingRestartDesktop=Im Desktopmodus neu starten
tstr_SettingsTroubleshootingElevatedModeEnter=Starte erhöhten Modus
tstr_SettingsTroubleshootingElevatedModeLeave=Stoppe erhöhten Modus
tstr_SettingsTroubleshootingSettingsReset=Auf Standardeinstellungen zurücksetzen
tstr_SettingsTroubleshootingSettingsResetConfirmDescription=Die ausgewählte Elemente werden auf ihre Standardwerte zurückgesetzt.\nDieser Vorgang kann nicht rückgängig gemacht werden.\n\nSetze folgendes zurück:
tstr_SettingsTroubleshootingSettingsResetConfirmButton=Ausgewählte Elemente zurücksetzen
tstr_SettingsTroubleshootingSettingsResetConfirmElementOverlays=Aktive Overlaykonfiguration
tstr_SettingsTroubleshootingSettingsResetConfirmElementLegacyFiles=Lösche alte unbenutzte Konfigurations- und Profildateien
tstr_SettingsTroubleshootingSettingsResetShowQuickStart=Kurzanleitung anzeigen
;Keyboard Window
tstr_KeyboardWindowTitle=Desktop+ Tastatur
tstr_KeyboardWindowTitleSettings=Desktop+ Tastatur (Einstellungen)
tstr_KeyboardWindowTitleOverlay=Desktop+ Tastatur (%OVERLAYNAME%)
tstr_KeyboardWindowTitleOverlayUnknown=[Unbekanntes Overlay]
;Keyboard Shortcuts Window
tstr_KeyboardShortcutsCut=Ausschneiden
tstr_KeyboardShortcutsCopy=Kopieren
tstr_KeyboardShortcutsPaste=Einfügen
;Overlay Properties Window
tstr_OvrlPropsCatPosition=Position
tstr_OvrlPropsCatAppearance=Aussehen
tstr_OvrlPropsCatCapture=Bilderfassung
tstr_OvrlPropsCatPerformanceMonitor=Leistungsanzeige
tstr_OvrlPropsCatBrowser=Browser
tstr_OvrlPropsCatAdvanced=Erweitert
tstr_OvrlPropsCatPerformance=Leistung
tstr_OvrlPropsCatInterface=Oberfläche
tstr_OvrlPropsPositionOrigin=Ursprung
tstr_OvrlPropsPositionOriginRoom=Spielbereich
tstr_OvrlPropsPositionOriginHMDXY=HMD-Bodenposition
tstr_OvrlPropsPositionOriginDashboard=Dashboard
tstr_OvrlPropsPositionOriginHMD=HMD
tstr_OvrlPropsPositionOriginSeatedSpace=Sitzposition
tstr_OvrlPropsPositionOriginControllerR=Rechter Controller
tstr_OvrlPropsPositionOriginControllerL=Linker Controller
tstr_OvrlPropsPositionOriginTheaterScreen=Kinoleinwand
tstr_OvrlPropsPositionOriginTracker1=Tracker #1
tstr_OvrlPropsPositionOriginConfigHMDXYTurning=Mit HMD drehen
tstr_OvrlPropsPositionOriginConfigTheaterScreenEnter=Starte Kinomodus
tstr_OvrlPropsPositionOriginConfigTheaterScreenLeave=Beende Kinomodus
tstr_OvrlPropsPositionOriginConfigSmoothing=Ursprungsglättung
tstr_OvrlPropsPositionOriginTheaterScreenTip=Einige Eigenschaften werden von der SteamVR Kinoleinwand gesteuert
tstr_OvrlPropsPositionDispMode=Anzeigemodus
tstr_OvrlPropsPositionDispModeAlways=Immer
tstr_OvrlPropsPositionDispModeDashboard=Nur im Dashboard
tstr_OvrlPropsPositionDispModeScene=Nur im Spiel
tstr_OvrlPropsPositionDispModeDPlus=Nur im Desktop+ Tab
tstr_OvrlPropsPositionPos=Position
tstr_OvrlPropsPositionPosTip=Die Position kann nur verändert oder zurückgesetzt werden wenn Desktop+ läuft
tstr_OvrlPropsPositionChange=Ändern
tstr_OvrlPropsPositionReset=Zurücksetzen
tstr_OvrlPropsPositionLock=Sperren
tstr_OvrlPropsPositionChangeHeader=Overlayposition ändern
tstr_OvrlPropsPositionChangeHelp=Verschiebe ein beliebiges Overlay, um dessen Position zu ändern.\nHalte Rechtsklick gedrückt für eine zweihändige Gestenmanipulation.
tstr_OvrlPropsPositionChangeHelpDesktop=Halte die Zieh-Schaltflächen ("Z") gedrückt um das Overlay mit der Maus zu verschieben oder drehen.
tstr_OvrlPropsPositionChangeManualAdjustment=Manuelle Anpassung
tstr_OvrlPropsPositionChangeMove=Bewegen
tstr_OvrlPropsPositionChangeRotate=Drehen
tstr_OvrlPropsPositionChangeForward=Vorwärts
tstr_OvrlPropsPositionChangeBackward=Rückwärts
tstr_OvrlPropsPositionChangeRollCW=Rolle ⟳
tstr_OvrlPropsPositionChangeRollCCW=Rolle ⟲
tstr_OvrlPropsPositionChangeLookAt=Zum HMD
tstr_OvrlPropsPositionChangeDragButton=Z
tstr_OvrlPropsPositionChangeOffset=Zusätzliches Offset
tstr_OvrlPropsPositionChangeOffsetUpDown=Hoch/Runter-Offset
tstr_OvrlPropsPositionChangeOffsetRightLeft=Rechts/Links-Offset
tstr_OvrlPropsPositionChangeOffsetForwardBackward=Vorwärts/Rückwärts-Offset
tstr_OvrlPropsPositionChangeDragSettings=Zieh-Einstellungen
tstr_OvrlPropsPositionChangeDragSettingsAutoDocking=Docke an Controller, wenn in Nähe
tstr_OvrlPropsPositionChangeDragSettingsForceUpright=Erzwinge aufrechte Ausrichtung
tstr_OvrlPropsPositionChangeDragSettingsForceDistance=Erzwinge festen Abstand
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShape=Form
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShapeSphere=Kugel
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShapeCylinder=Zylinder
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceAutoCurve=Auto-Krümmung
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceAutoTilt=Auto-Neigung
tstr_OvrlPropsPositionChangeDragSettingsSnapPosition=Position einrasten
tstr_OvrlPropsPositionChangeDragSettingsSnapRotation=Drehung einrasten
tstr_OvrlPropsPositionChangeDragSettingsSnapRotationPitch=Nickachse
tstr_OvrlPropsPositionChangeDragSettingsSnapRotationYaw=Gierachse
tstr_OvrlPropsPositionChangeDragSettingsSnapRotationRoll=Rollachse
tstr_OvrlPropsAppearanceWidth=Breite
tstr_OvrlPropsAppearanceCurve=Krümmung
tstr_OvrlPropsAppearanceOpacity=Deckkraft
tstr_OvrlPropsAppearanceBrightness=Helligkeit
tstr_OvrlPropsAppearanceCrop=Zuschneidung
tstr_OvrlPropsAppearanceCropValueMax=Max
tstr_OvrlPropsAppearanceShowBackside=Zeige Rückseite
tstr_OvrlPropsCrop=Zuschneide-Bereich
tstr_OvrlPropsCropHelp=Ziehe das Rechteck um die Zuschneidung zu verändern. Verwende Scroll-Eingaben oder ziehe die Seiten um die Größe des Zuschneidungs-Rechtecks anzupassen.
tstr_OvrlPropsCropManualAdjust=Manuelle Anpassung
tstr_OvrlPropsCropInvalidTip=Das aktuelle Zuschneide-Rechteck ist ungültig. Das Overlay ist möglicherweise nicht sichtbar.
tstr_OvrlPropsCropX=X
tstr_OvrlPropsCropY=Y
tstr_OvrlPropsCropWidth=Breite
tstr_OvrlPropsCropHeight=Höhe
tstr_OvrlPropsCropToWindow=Zum aktiven Fenster zuschneiden
tstr_OvrlPropsCaptureMethod=Erfassungsmethode
tstr_OvrlPropsCaptureMethodDup=Desktop Duplication
tstr_OvrlPropsCaptureMethodGC=Graphics Capture
tstr_OvrlPropsCaptureMethodGCUnsupportedTip=Graphics Capture wird von diesem System nicht unterstützt
tstr_OvrlPropsCaptureMethodGCUnsupportedPartialTip=Einige Graphics Capture-Funktionen werden von diesem System nicht unterstützt
tstr_OvrlPropsCaptureSource=Quelle
tstr_OvrlPropsCaptureGCSource=Graphics Capture-Quelle
tstr_OvrlPropsCaptureSourceUnknownWarning=Dieses Overlay verwendet eine unbekannte Erfassungsmethode und funktioniert möglicherweise nicht korrekt
tstr_OvrlPropsCaptureGCStrictMatching=Verwende strikten Fensterabgleich
tstr_OvrlPropsCaptureGCStrictMatchingTip=Erlaube nur exakte Fenstertitel-Übereinstimmungen, wenn dieses Overlay wiederhergestellt wird
tstr_OvrlPropsPerfMonDesktopModeTip=Leistungsanzeige-Overlays werden im Desktopmodus nicht aktualisiert
tstr_OvrlPropsPerfMonGlobalTip=Diese Einstellungen gelten für alle Leistungsanzeige-Overlays
tstr_OvrlPropsPerfMonStyle=Stil
tstr_OvrlPropsPerfMonItems=Elemente
tstr_OvrlPropsPerfMonStyleMinimal=Minimal
tstr_OvrlPropsPerfMonStyleCompact=Kompakt
tstr_OvrlPropsPerfMonStyleLarge=Groß
tstr_OvrlPropsPerfMonStyleShowWindow=Zeige Fenster
tstr_OvrlPropsPerfMonStyleShowTextOutline=Zeige Textkontur
tstr_OvrlPropsPerfMonStyleMinimalShowMore=Zeige mehr Elemente
tstr_OvrlPropsPerfMonItemCPU=CPU Statistiken
tstr_OvrlPropsPerfMonItemGPU=GPU Statistiken
tstr_OvrlPropsPerfMonItemGraphs=Kurven
tstr_OvrlPropsPerfMonItemFrameStats=Frame-Statistiken
tstr_OvrlPropsPerfMonItemTime=Zeit
tstr_OvrlPropsPerfMonItemBattery=Akku-Statistiken
tstr_OvrlPropsPerfMonItemTrackerBattery=Tracker-Akkulevel
tstr_OvrlPropsPerfMonItemViveWirelessTemp=Vive Wireless-Temperatur
tstr_OvrlPropsPerfMonResetValues=Kumulative Werte zurücksetzen
tstr_OvrlPropsBrowserNotAvailableTip=Die Desktop+ Browser-Komponente ist nicht installiert
tstr_OvrlPropsBrowserCloned=Geklonte Ausgabe
tstr_OvrlPropsBrowserClonedTip=Dieses Overlay ist ein Klon von "%OVERLAYNAME%".\nÄnderungen an den Browser-Eigenschaften werden auf das Original und alle Overlays welche es klonen angewendet.
tstr_OvrlPropsBrowserClonedConvert=Konvertiere zu eigenständigen Overlay
tstr_OvrlPropsBrowserURL=URL
tstr_OvrlPropsBrowserURLHint=Gebe eine Adresse ein
tstr_OvrlPropsBrowserGo=Los
tstr_OvrlPropsBrowserRestore=Letzte Eingabe wiederherstellen
tstr_OvrlPropsBrowserWidth=Breite
tstr_OvrlPropsBrowserHeight=Höhe
tstr_OvrlPropsBrowserZoom=Zoom
tstr_OvrlPropsBrowserAllowTransparency=Erlaube Transparenz
tstr_OvrlPropsBrowserAllowTransparencyTip=Erlaubt Webseiten Transparenz zu nutzen.\nSeiten, die keine Hintergrundfarbe definieren, werden möglicherweise nicht korrekt dargestellt.
tstr_OvrlPropsBrowserRecreateContext=Erneuere Browser-Kontext
tstr_OvrlPropsBrowserRecreateContextTip=Der Browser-Kontext muss neu erstellt werden um die Änderung anzuwenden.\nDadurch wird die Seite neu geladen und der Navigationsverlauf zurückgesetzt.
tstr_OvrlPropsAdvanced3D=3D
tstr_OvrlPropsAdvancedHSBS=Halbes Nebeneinander (HSBS)
tstr_OvrlPropsAdvancedSBS=Nebeneinander (SBS)
tstr_OvrlPropsAdvancedHOU=Halbes Übereinander (HOU)
tstr_OvrlPropsAdvancedOU=Übereinander (OU)
tstr_OvrlPropsAdvanced3DSwap=Tausche Augen
tstr_OvrlPropsAdvancedGazeFade=Gaze Fade
tstr_OvrlPropsAdvancedGazeFadeAuto=Automatisch einstellen
tstr_OvrlPropsAdvancedGazeFadeDistance=Distanz
tstr_OvrlPropsAdvancedGazeFadeDistanceValueInf=Unendlich
tstr_OvrlPropsAdvancedGazeFadeSensitivity=Empfindlichkeit
tstr_OvrlPropsAdvancedGazeFadeOpacity=Ziel-Deckkraft
tstr_OvrlPropsAdvancedInput=Laserpointer-Eingabe
tstr_OvrlPropsAdvancedInputInGame=Aktiviere im Spiel
tstr_OvrlPropsAdvancedInputFloatingUI=Zeige Floating UI
tstr_OvrlPropsAdvancedOverlayTags=Overlay-Tags
tstr_OvrlPropsAdvancedOverlayTagsTip=Overlay-Tags können verwendet werden um Overlays als Ziel in Aktionen festzulegen
tstr_OvrlPropsPerformanceInvisibleUpdate=Aktualisiere wenn unsichtbar
tstr_OvrlPropsPerformanceInvisibleUpdateTip=Aktualisiert das Overlay, auch wenn es aufgrund von Deckkrafts- oder Gaze Fade-Einstellung nicht sichtbar ist.\nHilft Anwendungen von Drittanbietern auf den Overlayinhalt zuzugreifen.\nIn anderen Fällen nicht empfohlen.\nWenn das Overlay manuell oder durch den Anzeigemodus versteckt ist, sind Overlayupdates dennoch pausiert.
tstr_OvrlPropsInterfaceOverlayName=Overlayname
tstr_OvrlPropsInterfaceOverlayNameAuto=[Automatisch benennen]
tstr_OvrlPropsInterfaceActionOrderCustom=Eigene Aktions-Schaltflächen
tstr_OvrlPropsInterfaceDesktopButtons=Zeige Desktop-Schaltflächen
tstr_OvrlPropsInterfaceExtraButtons=Zeige Zusatz-Schaltflächen
;Overlay Bar
tstr_OverlayBarOvrlHide=Verstecken
tstr_OverlayBarOvrlShow=Zeigen
tstr_OverlayBarOvrlClone=Klonen
tstr_OverlayBarOvrlRemove=Entfernen
tstr_OverlayBarOvrlRemoveConfirm=Wirklich?
tstr_OverlayBarOvrlProperties=Eigenschaften...
tstr_OverlayBarOvrlAddWindow=Fenster...
tstr_OverlayBarTooltipOvrlAdd=Overlay hinzufügen
tstr_OverlayBarTooltipSettings=Einstellungen
tstr_OverlayBarTooltipResetHold=Halte um Fensterposition zurückzusetzen...
;Floating UI
tstr_FloatingUIHideOverlayTip=Verstecke Overlay
tstr_FloatingUIHideOverlayHoldTip=Halte um Overlay zu entfernen...
tstr_FloatingUIDragModeEnableTip=Aktiviere Verschiebemodus
tstr_FloatingUIDragModeDisableTip=Deaktiviere Verschiebemodus
tstr_FloatingUIDragModeHoldLockTip=Halte um Overlayposition zu sperren...
tstr_FloatingUIDragModeHoldUnlockTip=Halte um Overlayposition zu entsperren...
tstr_FloatingUIWindowAddTip=Füge aktives Fenster als Overlay hinzu
tstr_FloatingUIActionBarShowTip=Zeige Aktions-Leiste
tstr_FloatingUIActionBarHideTip=Verstecke Aktions-Leiste
tstr_FloatingUIBrowserGoBackTip=Zurück zur vorherigen Seite
tstr_FloatingUIBrowserGoForwardTip=Weiter zur nächsten Seite
tstr_FloatingUIBrowserRefreshTip=Seite aktualisieren
tstr_FloatingUIBrowserStopTip=Stoppe Seiten-Ladevorgang
tstr_FloatingUIActionBarDesktopNext=Nächster Desktop
tstr_FloatingUIActionBarDesktopPrev=Vorheriger Desktop
tstr_FloatingUIActionBarEmpty=Keine Aktionen ausgewählt
;Special Actions
tstr_ActionNone=[Keine Aktion]
tstr_ActionKeyboardShow=Zeige Tastatur
tstr_ActionKeyboardHide=Verstecke Tastatur
;Default Actions (translation IDs are matched to Action name string)
tstr_DefActionShowKeyboard=Zeige Tastatur
tstr_DefActionActiveWindowCrop=Zum aktiven Fenster zuschneiden
tstr_DefActionActiveWindowCropLabel=Zum aktiven\nFenster\nzuschneiden
tstr_DefActionSwitchTask=Wechsle Task
tstr_DefActionToggleOverlays=Overlays umschalten
tstr_DefActionToggleOverlaysLabel=Overlays\numschalten
tstr_DefActionMiddleMouse=Mittlere Maustaste
tstr_DefActionMiddleMouseLabel=Mittlere\nMaustaste
tstr_DefActionBackMouse=Maus Zurücktaste
tstr_DefActionBackMouseLabel=Maus\nZurücktaste
tstr_DefActionReadMe=Öffne LiesMich
tstr_DefActionReadMeLabel=Öffne\nLiesMich
tstr_DefActionDashboardToggle=SteamVR Dashboard umschalten (Debug Befehl)
tstr_DefActionDashboardToggleLabel=Dashboard\numschalten
;Performance Monitor (text space is very limited here, so keep it short or untranslated)
tstr_PerformanceMonitorCPU=CPU
tstr_PerformanceMonitorGPU=GPU
tstr_PerformanceMonitorRAM=RAM:
tstr_PerformanceMonitorVRAM=VRAM:
tstr_PerformanceMonitorFrameTime=Frame-Zeit:
tstr_PerformanceMonitorLoad=Auslastung:
tstr_PerformanceMonitorFPS=FPS:
tstr_PerformanceMonitorFPSAverage=Durchschnittliche FPS:
tstr_PerformanceMonitorReprojectionRatio=Reprojektions-Ratio:
tstr_PerformanceMonitorDroppedFrames=Verworfene Frames:
tstr_PerformanceMonitorBatteryLeft=Linker Controller:
tstr_PerformanceMonitorBatteryRight=Rechter Controller:
tstr_PerformanceMonitorBatteryHMD=Headset:
tstr_PerformanceMonitorBatteryTracker=Tracker %ID%:
tstr_PerformanceMonitorBatteryDisconnected=N/V
tstr_PerformanceMonitorViveWirelessTempNotAvailable=N/V
tstr_PerformanceMonitorCompactCPU=CPU
tstr_PerformanceMonitorCompactGPU=GPU
tstr_PerformanceMonitorCompactFPS=FPS
tstr_PerformanceMonitorCompactFPSAverage=AVG
tstr_PerformanceMonitorCompactReprojectionRatio=% RPR
tstr_PerformanceMonitorCompactDroppedFrames=VRW
tstr_PerformanceMonitorCompactBattery=BAT
tstr_PerformanceMonitorCompactBatteryLeft=L
tstr_PerformanceMonitorCompactBatteryRight=R
tstr_PerformanceMonitorCompactBatteryHMD=H
tstr_PerformanceMonitorCompactBatteryTracker=T%ID%
tstr_PerformanceMonitorCompactBatteryDisconnected=N/V
tstr_PerformanceMonitorCompactViveWirelessTempNotAvailable=N/V
tstr_PerformanceMonitorEmpty=Es sind keine Leistungsanzeige-Elemente aktiviert.
;Aux UI
tstr_AuxUIDragHintDocking=Loslassen um anzudocken
tstr_AuxUIDragHintUndocking=Loslassen um abzudocken
tstr_AuxUIDragHintOvrlLocked=Entsperre die Overlayposition um dieses Overlay zu bewegen
tstr_AuxUIDragHintOvrlTheaterScreenBlocked=Die Overlayposition wird von der SteamVR-Kinoleinwand gesteuert
tstr_AuxUIGazeFadeAutoHint=Schaue in die Mitte des Overlays und warte für %SECONDS% Sekunden...
tstr_AuxUIGazeFadeAutoHintSingular=Schaue in die Mitte des Overlays und warte für %SECONDS% Sekunde...
tstr_AuxUIQuickStartWelcomeHeader=Willkommen zu Desktop+!
tstr_AuxUIQuickStartWelcomeBody=Diese kurze Einführung wird dich mit den grundlegenden Funktionen der Anwendung bekannt machen.\nAusführlichere Informationen kannst du in der LiesMich und im Benutzerhandbuch finden.\n\nFalls du das hier überspringen möchtest, drücke einfach [Schließen].
tstr_AuxUIQuickStartOverlaysHeader=Overlays
tstr_AuxUIQuickStartOverlaysBody=Desktop+ ermöglicht es dir sogenannte Overlays zu erstellen, welche deine Desktops, einzelne Fenster, und mehr spiegeln.\nAlle von dir erstellen Overlays werden unten in der Overlay-Leiste gelistet.\n\nOverlay-Eigenschaften können geändert werden, indem du auf eines der Overlay-Icons klickst und [Eigenschaften...] auswählst.
tstr_AuxUIQuickStartOverlaysBody2=Neue Overlays können durch Drücken von [+] und Wahl einer Quelle/Overlay-Art aus der Liste erstellt werden.\nEinige Arten, wie zum Beispiel Webbrowser-Overlays, sind nur verfügbar wenn die entsprechende Anwendungskomponente installiert ist.\n\nEinzelne Overlays oder komplette Layouts können in Profilen gespeichert werden.\nDie aktive Overlaykonfiguration wird automatisch aus der letzten Sitzung wiederhergestellt.
tstr_AuxUIQuickStartOverlayPropertiesHeader=Overlay-Eigenschaften
tstr_AuxUIQuickStartOverlayPropertiesBody=Overlays haben eine Reihe von Anpassungsmöglichkeiten.\nUrsprung und Einzeigemodus bestimmen wo und wann ein Overlay angezeigt wird.\nBehalte diese Eigenschaften im Auge falls du das Gefühl hast dass ein Overlay sichtbar sein sollte, aber du es nicht finden kannst.\nFalls letzteres nicht ausreicht, könnte ein Zurücksetzen der Position helfen.\n\nDu kannst ein Overlay an einen Bewegungscontroller docken, indem du es in die Nähe des Controllers ziehst.
tstr_AuxUIQuickStartOverlayPropertiesBody2=Einige Eigenschaften sind standardmäßig ausgeblendet.\nAktiviere "Zeige erweiterte Einstellungen" im Einstellungsfenster um alle Optionen zu sehen.\n\nDie Positionen von UI-Fenstern können durch längeres Gedrückthalten der zugehörigen Schaltflächen zurückgesetzt werden.\nDoppel- und Rechtsklick können auch auf Overlay-Schaltflächen angewendet werden um Kürzel zu aktivieren.
tstr_AuxUIQuickStartSettingsHeader=Einstellungen
tstr_AuxUIQuickStartSettingsBody=Das Einstellungsfenster enthält alle globalen Einstellungen für Desktop+.\nEs kann durch Drücken der Zahnrad-Schaltfläche am rechten Ende der Overlay-Leiste geöffnet werden.\n\nWie auch bei Overlays werden Änderungen an den Einstellungen automatisch gespeichert.
tstr_AuxUIQuickStartProfilesHeader=Profile
tstr_AuxUIQuickStartProfilesBody=Es gibt zwei Arten von Profilen in Desktop+.\n\nOverlay-Profile:\nSpeichern ein oder mehrere Overlays mitsamt ihren Eigenschaften.\nSie können manuell oder durch Aktionen und Anwendungs-Profilen geladen werden.\n\nAnwendungs-Profile:\nWeisen ein Overlay-Profil und/oder Aktionen zu einer SteamVR-Anwendung zu, welche automatisch geladen werden wenn sie gestartet wird.
tstr_AuxUIQuickStartActionsHeader=Aktionen
tstr_AuxUIQuickStartActionsBody=Aktionen in Desktop+ sind eine Aneinanderreihung von Befehlen, die Controllereingaben und Anwendungs-Profile zugewiesen oder zur Aktions-Leiste von Overlays hinzugefügt werden können.
tstr_AuxUIQuickStartActionsBody2=Standardmäßig werden Aktionen auf das Overlay angewendet, auf dem sie aktiviert wurden (für das Overlay gezeigter Aktions-Schaltfläche, Controllereingabe auf dem Overlay), oder auf dem fokussierten Overlay, falls letzteres nicht anwendbar ist.\n\nDas fokussierte Overlay ist das zuletzt geklickte Overlay.\nIn vielen Fällen ist das Ziel-Overlay nicht wichtig, beispielsweise für simulierte Desktop-Eingaben.\nIn anderen Fällen kann es nützlich sein ein anderes Overlay oder auch mehrere als Ziel zu verwenden.
tstr_AuxUIQuickStartOverlayTagsHeader=Overlay-Tags
tstr_AuxUIQuickStartOverlayTagsBody=Hierfür können Overlay-Tags verwendet werden.\nEs gibt automatische Tags (grün hervorgehoben) und benutzerdefinierte Tags.\nAutomatische Tags werden automatisch basierend auf den Eigenschaften welche sie repräsentieren zugewiesen.\nBenutzerdefinierte Tags können manuell Overlays in den Overlay-Eigenschaften zugewiesen werden.\n\nBeachte dass Aktionen nur indirekt Controllertasten zugewiesen werden können. Die nummerierten Kürzel müssen auch mit einer Controllereingabe in den SteamVR-Controllerzuordnungen verbunden werden.
tstr_AuxUIQuickStartSettingsEndBody=Es gibt noch viele weitere Einstellungen.\nHabe keine Angst mit ihnen herumzuexperimentieren.\n\nFalls du doch steckenbleiben solltest, kannst du [Auf Standardeinstellungen zurücksetzen] am unteren Ende des Fensters drücken.\nDu hast die Wahl nur bestimmte Elemente zurückzusetzen.
tstr_AuxUIQuickStartFloatingUIHeader=Floating UI
tstr_AuxUIQuickStartFloatingUIBody=Die als "Floating UI" bezeichnete Oberfläche ist sichtbar wenn auf ein Overlay gezeigt wird.\nIm Dashboard ist sie permanent sichtbar, solange auf kein anderes Overlay gezeigt wird.\n\nDie Floating UI enthält grundlegende Elemente zum Anpassen des Overlays, wie das Umschalten des Zieh-Modus um das Overlay zu bewegen, sowie einen anpassbaren Bereich für Aktions-Schaltflächen.
tstr_AuxUIQuickStartDesktopModeHeader=Desktopmodus
tstr_AuxUIQuickStartDesktopModeBody=Desktop+ kann auch in einem Fenster auf dem Desktop konfiguriert werden.\nDies kann für Aufgaben welche häufige Tastatureingaben benötigen oder um Änderungen ohne verbundene Bewegungscontroller vorzunehmen nützlich sein.\nWährend fast alle Funktionen in beiden Modi verfügbar sind, ist der Tastaturlayout-Editor, mit dem die Layouts für die Desktop+ VR-Tastatur anpassen werden können, nur im Desktopmodus zugänglich.\n\nDu kannst ihn im Problembehandlungsabschnitt des Einstellungsfensters oder über das Desktop+-Symbol im Systemtray/Benachrichtigungsbereich aufrufen.
tstr_AuxUIQuickStartEndHeader=LiesMich & Benutzerhandbuch
tstr_AuxUIQuickStartEndBody=Dies sollte schon ausreichen um mit Desktop+ loszulegen.\n\nFalls du auf Probleme stößt oder nicht weiter kommst, schaue dir auch die LiesMich an. Die Schaltfläche in der Aktions-Leiste wird sie für dich öffnen.\nDetaillierte Erklärungen zu jeder Option und Schritt-für-Schritt-Anleitungen für häufige Nutzungsszenarien findest du im Benutzerhandbuch.\n\nUm diese Einführung erneut anzuzeigen, drücke auf [Kurzanleitung anzeigen] unten rechts auf der "Auf Standardeinstellungen zurücksetzen" Seite.
tstr_AuxUIQuickStartButtonNext=Nächste Seite
tstr_AuxUIQuickStartButtonPrev=Vorherige Seite
tstr_AuxUIQuickStartButtonClose=Schließen
;Desktop Mode
tstr_DesktopModeCatTools=Werkzeuge
tstr_DesktopModeCatOverlays=Overlays
tstr_DesktopModeToolSettings=Einstellungen
tstr_DesktopModeToolActions=Aktionen
tstr_DesktopModeOverlayListAdd=Overlay hinzufügen
tstr_DesktopModePageAddWindowOverlayTitle=Fenster-Overlay hinzufügen
tstr_DesktopModePageAddWindowOverlayHeader=Wähle ein Fenster
;Keyboard Editor
tstr_KeyboardEditorKeyListTitle=Tastenliste
tstr_KeyboardEditorKeyListTabContextReplace=Ersetze Inhalt mit...
tstr_KeyboardEditorKeyListTabContextClear=Leere Unterlayout
tstr_KeyboardEditorKeyListRow=Reihe %ID%
tstr_KeyboardEditorKeyListSpacing=[Abstand]
tstr_KeyboardEditorKeyListKeyAdd=Neu
tstr_KeyboardEditorKeyListKeyDuplicate=Dupl.
tstr_KeyboardEditorKeyListKeyRemove=Entf.
tstr_KeyboardEditorKeyPropertiesTitle=Tasten-Eigenschaften
tstr_KeyboardEditorKeyPropertiesNoSelection=Keine Taste ausgewählt
tstr_KeyboardEditorKeyPropertiesType=Typ
tstr_KeyboardEditorKeyPropertiesTypeBlank=Leere Fläche
tstr_KeyboardEditorKeyPropertiesTypeVirtualKey=Virtuelle Taste
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyToggle=Virtuelle Taste (Umschalten)
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyIsoEnter=Virtuelle Taste (ISO-Enter)
tstr_KeyboardEditorKeyPropertiesTypeString=Zeichenkette
tstr_KeyboardEditorKeyPropertiesTypeSublayoutToggle=Unterlayout wechseln
tstr_KeyboardEditorKeyPropertiesTypeAction=Aktion
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyIsoEnterTip=Platziere zwei "Virtuelle Taste (ISO-Enter)"-Tasten in aufeinanderfolgenden Reihen um eine ISO-Enter geformte Taste zu erstellen.\nNur eine kombinierte Taste pro Unterlayout.
tstr_KeyboardEditorKeyPropertiesTypeStringTip=Verwende den Zeichenketten-Tastentyp für Sonderzeichen und nicht-grundlegende Buchstaben.\nDies erlaubt Desktop+ die korrekte Tastenkombination auf dem echten Tastaturlayout herauszufinden und Anwendungskompatibilität zu erhöhen.
tstr_KeyboardEditorKeyPropertiesSize=Größe
tstr_KeyboardEditorKeyPropertiesLabel=Beschriftung
tstr_KeyboardEditorKeyPropertiesKeyCode=Tastencode
tstr_KeyboardEditorKeyPropertiesString=Zeichenkette
tstr_KeyboardEditorKeyPropertiesSublayout=Unterlayout
tstr_KeyboardEditorKeyPropertiesAction=Aktion
tstr_KeyboardEditorKeyPropertiesCluster=Tastengruppe
tstr_KeyboardEditorKeyPropertiesClusterTip=Tastengruppen-Zuweisung wird verwendet um ausgewählte Tasten vom Laden auszuschließen.\nUmschließende Leerfläche wird nicht entfernt. Weise Leere Flächen-Tasten den passenden Tastengruppen hinzu um sie korrekt auszuschließen.
tstr_KeyboardEditorKeyPropertiesBlockModifiers=Blockiere Modifikatoren
tstr_KeyboardEditorKeyPropertiesBlockModifiersTip=Lässt alle Modifikatoren-Tasten los, während diese Taste gedrückt wird
tstr_KeyboardEditorKeyPropertiesNoRepeat=Nie wiederholen
tstr_KeyboardEditorKeyPropertiesNoRepeatTip=Blockiert Wiederholung von Tasteneingabe, selbst wenn Tastenwiederholung eingeschaltet ist
tstr_KeyboardEditorMetadataTitle=Layout-Metadaten
tstr_KeyboardEditorMetadataName=Name
tstr_KeyboardEditorMetadataAuthor=Autor
tstr_KeyboardEditorMetadataHasAltGr=Hat AltGr
tstr_KeyboardEditorMetadataHasAltGrTip=Gedrückte rechte Alt-Taste wechselt zum AltGr Unterlayout
tstr_KeyboardEditorMetadataClusterPreview=Tastengruppen in der Vorschau:
tstr_KeyboardEditorMetadataSave=Speichern...
tstr_KeyboardEditorMetadataLoad=Laden...
tstr_KeyboardEditorMetadataSavePopupTitle=Speichere Tastaturlayout
tstr_KeyboardEditorMetadataSavePopupFilename=Dateiname
tstr_KeyboardEditorMetadataSavePopupFilenameBlankTip=Dateiname darf nicht leer sein
tstr_KeyboardEditorMetadataSavePopupConfirm=Speichere Layout
tstr_KeyboardEditorMetadataSavePopupConfirmError=Speichern des Layouts ist fehlgeschlagen
tstr_KeyboardEditorMetadataLoadPopupTitle=Lade Tastaturlayout
tstr_KeyboardEditorMetadataLoadPopupConfirm=Lade Layout
tstr_KeyboardEditorPreviewTitle=Tastaturvorschau
tstr_KeyboardEditorSublayoutBase=Basis
tstr_KeyboardEditorSublayoutShift=Shift
tstr_KeyboardEditorSublayoutAltGr=AltGr
tstr_KeyboardEditorSublayoutAux=Zusatz
;General Dialog Strings
tstr_DialogOk=Ok
tstr_DialogCancel=Abbrechen
tstr_DialogDone=Fertig
tstr_DialogUndo=Rückgängig
tstr_DialogRedo=Wiederholen
tstr_DialogColorPickerHeader=Wähle eine Farbe
tstr_DialogColorPickerCurrent=Aktuell
tstr_DialogColorPickerOriginal=Original
tstr_DialogProfilePickerHeader=Wähle ein Profil
tstr_DialogProfilePickerNone=[Kein Profil]
tstr_DialogActionPickerHeader=Wähle eine Aktion
tstr_DialogActionPickerEmpty=Keine Aktionen verfügbar
tstr_DialogIconPickerHeader=Wähle ein Icon
tstr_DialogIconPickerHeaderTip=Eigene Icons können als PNG-Dateien zum "images\icons\" Ordner hinzugefügt werden
tstr_DialogIconPickerNone=[Kein Icon]
tstr_DialogKeyCodePickerHeader=Wähle einen Tastencode
tstr_DialogKeyCodePickerHeaderHotkey=Wähle einen Hotkey
tstr_DialogKeyCodePickerModifiers=Modifikator
tstr_DialogKeyCodePickerKeyCode=Tastencode
tstr_DialogKeyCodePickerKeyCodeHint=Filtere Liste
tstr_DialogKeyCodePickerKeyCodeNone=[Keine]
tstr_DialogKeyCodePickerFromInput=Von Eingabe...
tstr_DialogKeyCodePickerFromInputPopup=Drücke eine beliebige Taste (auch Maustasten)...
tstr_DialogKeyCodePickerFromInputPopupNoMouse=Drücke eine beliebige Taste...
tstr_DialogWindowPickerHeader=Wähle ein Fenster
tstr_DialogInputTagsHint=Filtere oder füge neuen Tag hinzu
;Source Strings
tstr_SourceDesktopAll=Gesamter Desktop
tstr_SourceDesktopID=Desktop %ID%
tstr_SourceWinRTNone=[Kein Erfassungsziel]
tstr_SourceWinRTUnknown=[Unbekanntes Fenster]
tstr_SourceWinRTClosed=[Geschlossen]:
tstr_SourcePerformanceMonitor=Leistungsanzeige
tstr_SourceBrowser=Browser
tstr_SourceBrowserNoPage=[Keine Seite geladen]
;Notification Icon
tstr_NotificationIconRestoreVR=VR-Oberfläche wiederherstellen
tstr_NotificationIconOpenOnDesktop=Einstellungen auf dem Desktop öffnen
tstr_NotificationIconQuit=Beenden
;Notifications
tstr_NotificationInitialStartupTitleVR=Ersteinrichtung
tstr_NotificationInitialStartupTitleDesktop=Desktop+ Ersteinrichtung
tstr_NotificationInitialStartupMessage=Desktop+ wurde zu SteamVR hinzugefügt und startet ab jetzt automatisch wenn SteamVR ausgeführt wird.
;Browser
tstr_BrowserErrorPageTitle=Seitenladefehler - Desktop+
tstr_BrowserErrorPageHeading=Die Seite konnte nicht geladen werden
tstr_BrowserErrorPageMessage=Beim Laden von %URL% ist ein Fehler aufgetreten: %ERROR%
================================================
FILE: assets/lang/en.ini
================================================
[TranslationInfo]
Name=English
Locale=en-US
;If this language has issues with the default fonts or their loading order, a different font can be defined here which is then loaded first
;This should be the file name with extension, and is loaded either from the OS fonts folder or the "lang" folder if it's missing from the former
PreferredFont=
;Set file name to ISO 639-1 language code for auto-detection support, with ISO 3166-1 region code after an underscore if necessary
;To translate the SteamVR input bindings, add a new localization file to the "input" folder and adjust action_manifest.json
[Strings]
;Settings Window
tstr_SettingsWindowTitle=Desktop+ Settings
tstr_SettingsCatInterface=Interface
tstr_SettingsCatEnvironment=Environment
tstr_SettingsCatProfiles=Profiles
tstr_SettingsCatActions=Actions
tstr_SettingsCatKeyboard=Keyboard
tstr_SettingsCatMouse=Mouse
tstr_SettingsCatLaserPointer=Laser Pointer
tstr_SettingsCatWindowOverlays=Window Overlays
tstr_SettingsCatBrowser=Browser
tstr_SettingsCatPerformance=Performance
tstr_SettingsCatVersionInfo=Version Info
tstr_SettingsCatWarnings=Warnings
tstr_SettingsCatStartup=Startup
tstr_SettingsCatTroubleshooting=Troubleshooting
tstr_SettingsWarningPrefix=Warning:
tstr_SettingsWarningCompositorResolution=SteamVR Compositor resolution is below 100%! This affects overlay rendering quality.
tstr_SettingsWarningCompositorQuality=SteamVR Overlay render quality is not set to high!
tstr_SettingsWarningProcessElevated=Desktop+ is running with administrative privileges!
tstr_SettingsWarningElevatedMode=Elevated mode is active!
tstr_SettingsWarningBrowserMissing=Browser overlays are being used, but the Desktop+ Browser component is currently not available.
tstr_SettingsWarningBrowserMismatch=The installed Desktop+ Browser component is incompatible with this version of Desktop+!
tstr_SettingsWarningElevatedProcessFocus=An elevated process has focus!\nDesktop+ is unable to simulate input right now.
tstr_SettingsWarningUIAccessLost=Desktop+ is no longer running with UIAccess privileges!
tstr_SettingsWarningOverlayCreationErrorLimit=An overlay creation failed! (Maximum Overlay limit exceeded)
tstr_SettingsWarningOverlayCreationErrorOther=An overlay creation failed! (%ERRORNAME%)
tstr_SettingsWarningGraphicsCaptureError=An unexpected error occurred in a Graphics Capture thread! (%ERRORCODE%)
tstr_SettingsWarningAppProfileActive=The application profile for %APPNAME% has overridden the current overlay layout.\nChanges made to overlays are not saved automatically while it is active.
tstr_SettingsWarningConfigMigrated=Configuration and profiles from the previous version of Desktop+ have been migrated into new formats.\nThe original files were not deleted and can still be used with an older version of the application.\nSee [Restore Default Settings] if you wish to start fresh instead.
tstr_SettingsWarningMenuDontShowAgain=Don't show this again
tstr_SettingsWarningMenuDismiss=Dismiss
tstr_SettingsInterfaceLanguage=Language
tstr_SettingsInterfaceLanguageCommunity=Community Translation by %AUTHOR%
tstr_SettingsInterfaceLanguageIncompleteWarning=Translation may be incomplete
tstr_SettingsInterfaceAdvancedSettings=Show Advanced Settings
tstr_SettingsInterfaceAdvancedSettingsTip=Show advanced and less commonly used settings
tstr_SettingsInterfaceBlankSpaceDrag=Drag Windows when Clicking on Blank Space
tstr_SettingsInterfacePersistentUI=Persistent UI
tstr_SettingsInterfacePersistentUIManage=Manage
tstr_SettingsInterfaceDesktopButtons=Desktop Buttons Listing Style
tstr_SettingsInterfaceDesktopButtonsNone=None
tstr_SettingsInterfaceDesktopButtonsIndividual=Individual Desktops
tstr_SettingsInterfaceDesktopButtonsCycle=Cycle Buttons
tstr_SettingsInterfaceDesktopButtonsAddCombined=Add Combined Desktop Button
tstr_SettingsInterfacePersistentUIHelp=Desktop+ remembers the state of the interface windows separately for general use (Global) and use within the Desktop+ SteamVR dashboard tab (Desktop+ Tab).
tstr_SettingsInterfacePersistentUIHelp2=The following controls can be used to directly manipulate the state of those windows.\nKeep in mind some states may require resetting the position in order to bring the window into a visible spot.
tstr_SettingsInterfacePersistentUIWindowsHeader=Windows
tstr_SettingsInterfacePersistentUIWindowsSettings=Settings
tstr_SettingsInterfacePersistentUIWindowsProperties=Overlay Properties
tstr_SettingsInterfacePersistentUIWindowsKeyboard=Desktop+ Keyboard
tstr_SettingsInterfacePersistentUIWindowsStateGlobal=Global
tstr_SettingsInterfacePersistentUIWindowsStateDashboardTab=Desktop+ Tab
tstr_SettingsInterfacePersistentUIWindowsStateVisible=Visible
tstr_SettingsInterfacePersistentUIWindowsStatePinned=Pinned
tstr_SettingsInterfacePersistentUIWindowsStatePosition=Position
tstr_SettingsInterfacePersistentUIWindowsStatePositionReset=Reset
tstr_SettingsInterfacePersistentUIWindowsStateSize=Size
tstr_SettingsInterfacePersistentUIWindowsStateLaunchRestore=Restore State on Desktop+ Launch
tstr_SettingsEnvironmentBackgroundColor=Background Color
tstr_SettingsEnvironmentBackgroundColorDispModeNever=Never Visible
tstr_SettingsEnvironmentBackgroundColorDispModeDPlusTab=Only Visible in Desktop+ Tab
tstr_SettingsEnvironmentBackgroundColorDispModeAlways=Always Visible
tstr_SettingsEnvironmentDimInterface=Dim Interface
tstr_SettingsEnvironmentDimInterfaceTip=Dims the SteamVR dashboard and Desktop+ interface while the Desktop+ dashboard tab is open
tstr_SettingsProfilesOverlays=Overlay Profiles
tstr_SettingsProfilesApps=Application Profiles
tstr_SettingsProfilesManage=Manage
tstr_SettingsProfilesOverlaysHeader=Manage Overlay Profiles
tstr_SettingsProfilesOverlaysNameDefault=Default
tstr_SettingsProfilesOverlaysNameNew=[New Profile]
tstr_SettingsProfilesOverlaysNameNewBase=Profile %ID%
tstr_SettingsProfilesOverlaysProfileLoad=Load Profile
tstr_SettingsProfilesOverlaysProfileAdd=Add Overlays from Profile
tstr_SettingsProfilesOverlaysProfileSave=Save Current Overlays
tstr_SettingsProfilesOverlaysProfileDelete=Delete Profile
tstr_SettingsProfilesOverlaysProfileDeleteConfirm=Really?
tstr_SettingsProfilesOverlaysProfileFailedLoad=Failed to Load Profile
tstr_SettingsProfilesOverlaysProfileFailedDelete=Failed to Delete Profile
tstr_SettingsProfilesOverlaysProfileAddSelectHeader=Choose Overlays to Add from Profile
tstr_SettingsProfilesOverlaysProfileAddSelectEmpty=This profile contains no overlays.
tstr_SettingsProfilesOverlaysProfileAddSelectDo=Add Selected Overlays
tstr_SettingsProfilesOverlaysProfileAddSelectAll=Select All
tstr_SettingsProfilesOverlaysProfileAddSelectNone=Select None
tstr_SettingsProfilesOverlaysProfileSaveSelectHeader=Save Current Overlays
tstr_SettingsProfilesOverlaysProfileSaveSelectName=Profile Name
tstr_SettingsProfilesOverlaysProfileSaveSelectNameErrorBlank=Name cannot be blank
tstr_SettingsProfilesOverlaysProfileSaveSelectNameErrorTaken=Name is already in use
tstr_SettingsProfilesOverlaysProfileSaveSelectHeaderList=Choose Overlays to Save in Profile
tstr_SettingsProfilesOverlaysProfileSaveSelectDo=Save Selected Overlays
tstr_SettingsProfilesOverlaysProfileSaveSelectDoFailed=Failed to Save Profile
tstr_SettingsProfilesAppsHeader=Manage App Profiles
tstr_SettingsProfilesAppsHeaderNoVRTip=Only existing application profiles are listed when Desktop+ isn't running
tstr_SettingsProfilesAppsListEmpty=No Applications available
tstr_SettingsProfilesAppsProfileHeaderActive=(Currently Active)
tstr_SettingsProfilesAppsProfileEnabled=Activate when Application is Running
tstr_SettingsProfilesAppsProfileOverlayProfile=Overlay Profile
tstr_SettingsProfilesAppsProfileActionEnter=Entry Action
tstr_SettingsProfilesAppsProfileActionLeave=Exit Action
tstr_SettingsActionsManage=Actions
tstr_SettingsActionsManageButton=Manage
tstr_SettingsActionsButtonsOrderDefault=Action Buttons (Default)
tstr_SettingsActionsButtonsOrderOverlayBar=Action Buttons (Overlay Bar)
tstr_SettingsActionsShowBindings=Show Controller Bindings
tstr_SettingsActionsActiveShortcuts=Active Controller Buttons
tstr_SettingsActionsActiveShortcutsTip=Controller bindings when pointing at an overlay.\nConfigure the VR Dashboard controller bindings to change which buttons these are.
tstr_SettingsActionsActiveShortuctsHome="Go Home"
tstr_SettingsActionsActiveShortuctsBack="Go Back"
tstr_SettingsActionsGlobalShortcuts=Global Controller Buttons
tstr_SettingsActionsGlobalShortcutsTip=Controller bindings when the dashboard is closed and not pointing at an overlay.\nConfigure the Desktop+ controller bindings to change which buttons these are.
tstr_SettingsActionsGlobalShortcutsEntry=Global Shortcut #%ID%
tstr_SettingsActionsGlobalShortcutsAdd=Add Shortcut
tstr_SettingsActionsGlobalShortcutsRemove=Remove Last Shortcut
tstr_SettingsActionsHotkeys=Hotkeys
tstr_SettingsActionsHotkeysTip=System-wide keyboard shortcuts.\nHotkeys block other applications from receiving that input and may not work if the same combination has already been registered elsewhere.
tstr_SettingsActionsHotkeysAdd=Add Hotkey
tstr_SettingsActionsHotkeysRemove=Remove
tstr_SettingsActionsTableHeaderAction=Action
tstr_SettingsActionsTableHeaderShortcut=Shortcut
tstr_SettingsActionsTableHeaderHotkey=Hotkey
tstr_SettingsActionsManageHeader=Manage Actions
tstr_SettingsActionsManageCopyUID=Copy UID to Clipboard
tstr_SettingsActionsManageNew=New Action...
tstr_SettingsActionsManageEdit=Edit Action
tstr_SettingsActionsManageDuplicate=Duplicate Action
tstr_SettingsActionsManageDelete=Delete Action
tstr_SettingsActionsManageDeleteConfirm=Really?
tstr_SettingsActionsManageDuplicatedName=%NAME% (Copy)
tstr_SettingsActionsEditHeader=Edit Action
tstr_SettingsActionsEditName=Name
tstr_SettingsActionsEditNameTranslatedTip=This action currently uses a translation string ID as a name to automatically match the chosen application language
tstr_SettingsActionsEditTarget=Target
tstr_SettingsActionsEditTargetDefault=Default
tstr_SettingsActionsEditTargetDefaultTip=Targets the overlay the action was activated on or the focused overlay if the former is not applicable
tstr_SettingsActionsEditTargetUseTags=Use Tags
tstr_SettingsActionsEditTargetActionTarget=Action Target
tstr_SettingsActionsEditHeaderAppearance=Button Appearance
tstr_SettingsActionsEditIcon=Icon
tstr_SettingsActionsEditLabel=Label
tstr_SettingsActionsEditLabelTranslatedTip=This action currently uses a translation string ID as a label to automatically match the chosen application language
tstr_SettingsActionsEditHeaderCommands=Commands
tstr_SettingsActionsEditNameNew=New Action
tstr_SettingsActionsEditCommandAdd=Add Command
tstr_SettingsActionsEditCommandDelete=Delete Command
tstr_SettingsActionsEditCommandDeleteConfirm=Really?
tstr_SettingsActionsEditCommandType=Command Type
tstr_SettingsActionsEditCommandTypeNone=None
tstr_SettingsActionsEditCommandTypeKey=Press Key
tstr_SettingsActionsEditCommandTypeMousePos=Set Mouse Position
tstr_SettingsActionsEditCommandTypeString=Type String
tstr_SettingsActionsEditCommandTypeLaunchApp=Launch Application
tstr_SettingsActionsEditCommandTypeShowKeyboard=Show Keyboard
tstr_SettingsActionsEditCommandTypeCropActiveWindow=Crop to Active Window
tstr_SettingsActionsEditCommandTypeShowOverlay=Show Overlay
tstr_SettingsActionsEditCommandTypeSwitchTask=Switch Task
tstr_SettingsActionsEditCommandTypeLoadOverlayProfile=Load Overlay Profile
tstr_SettingsActionsEditCommandTypeUnknown=Unknown
tstr_SettingsActionsEditCommandVisibilityToggle=Toggle
tstr_SettingsActionsEditCommandVisibilityShow=Always Show
tstr_SettingsActionsEditCommandVisibilityHide=Always Hide
tstr_SettingsActionsEditCommandUndo=Undo on Release
tstr_SettingsActionsEditCommandKeyCode=Key Code
tstr_SettingsActionsEditCommandKeyToggle=Toggle Key
tstr_SettingsActionsEditCommandMouseX=X
tstr_SettingsActionsEditCommandMouseY=Y
tstr_SettingsActionsEditCommandMouseUseCurrent=Use Current Mouse Position
tstr_SettingsActionsEditCommandString=String
tstr_SettingsActionsEditCommandPath=Executable Path
tstr_SettingsActionsEditCommandPathTip=This can also be a normal file or URL
tstr_SettingsActionsEditCommandArgs=Application Arguments
tstr_SettingsActionsEditCommandArgsTip=These are passed to the application.\nIf unsure, leave this blank.
tstr_SettingsActionsEditCommandVisibility=Visibility
tstr_SettingsActionsEditCommandSwitchingMethod=Switching Method
tstr_SettingsActionsEditCommandSwitchingMethodSwitcher=Show Task Switcher
tstr_SettingsActionsEditCommandSwitchingMethodFocus=Focus Window
tstr_SettingsActionsEditCommandWindow=Window
tstr_SettingsActionsEditCommandWindowNone=[No Window]
tstr_SettingsActionsEditCommandWindowStrictMatchingTip=Only allow exact window title matches when looking for a window to switch to
tstr_SettingsActionsEditCommandCursorWarp=Warp Cursor into Window
tstr_SettingsActionsEditCommandProfile=Profile
tstr_SettingsActionsEditCommandProfileClear=Remove Existing Overlays
tstr_SettingsActionsEditCommandDescNone=No Command
tstr_SettingsActionsEditCommandDescKey=Press Key "%KEYNAME%"
tstr_SettingsActionsEditCommandDescKeyToggle=Toggle Key "%KEYNAME%"
tstr_SettingsActionsEditCommandDescMousePos=Set Mouse Position to %X%, %Y%
tstr_SettingsActionsEditCommandDescString=Type String "%STRING%"
tstr_SettingsActionsEditCommandDescLaunchApp=Launch Application "%APP%" %ARGSOPT%
tstr_SettingsActionsEditCommandDescLaunchAppArgsOpt="%ARGS%"
tstr_SettingsActionsEditCommandDescKeyboardToggle=Toggle Keyboard
tstr_SettingsActionsEditCommandDescKeyboardShow=Show Keyboard
tstr_SettingsActionsEditCommandDescKeyboardHide=Hide Keyboard
tstr_SettingsActionsEditCommandDescCropWindow=Crop to Active Window
tstr_SettingsActionsEditCommandDescOverlayToggle=Toggle Overlay: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayShow=Show Overlay: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayHide=Hide Overlay: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayTargetDefault=[Action Target]
tstr_SettingsActionsEditCommandDescSwitchTask=Switch Task
tstr_SettingsActionsEditCommandDescSwitchTaskWindow=Switch Task to "%WINDOW%"
tstr_SettingsActionsEditCommandDescLoadOverlayProfile=Load Overlay Profile "%PROFILE%"
tstr_SettingsActionsEditCommandDescLoadOverlayProfileAdd=Add Overlay Profile "%PROFILE%"
tstr_SettingsActionsEditCommandDescUnknown=Unknown Command
tstr_SettingsActionsOrderHeader=Change Action Order
tstr_SettingsActionsOrderButtonLabel=%COUNT% Actions Selected
tstr_SettingsActionsOrderButtonLabelSingular=%COUNT% Action Selected
tstr_SettingsActionsOrderNoActions=No Actions Selected
tstr_SettingsActionsOrderAdd=Add Actions...
tstr_SettingsActionsOrderRemove=Remove Action
tstr_SettingsActionsAddSelectorHeader=Choose Actions to Add
tstr_SettingsActionsAddSelectorAdd=Add Selected Actions
tstr_SettingsKeyboardLayout=Keyboard Layout
tstr_SettingsKeyboardSize=Size
tstr_SettingsKeyboardBehavior=Behavior
tstr_SettingsKeyboardStickyMod=Sticky Modifier Keys
tstr_SettingsKeyboardKeyRepeat=Key Repeat
tstr_SettingsKeyboardAutoShow=Show Automatically
tstr_SettingsKeyboardAutoShowDesktopOnly=Show Automatically
tstr_SettingsKeyboardAutoShowDesktop=For Desktop & Window Overlays
tstr_SettingsKeyboardAutoShowDesktopTip=Experimental. Does not work with all applications.
tstr_SettingsKeyboardAutoShowBrowser=For Browser Overlays
tstr_SettingsKeyboardLayoutAuthor=Created by %AUTHOR%
tstr_SettingsKeyboardKeyClusters=Key Clusters
tstr_SettingsKeyboardKeyClusterBase=Base
tstr_SettingsKeyboardKeyClusterFunction=Function
tstr_SettingsKeyboardKeyClusterNavigation=Navigation
tstr_SettingsKeyboardKeyClusterNumpad=Numpad
tstr_SettingsKeyboardKeyClusterExtra=Extra
tstr_SettingsKeyboardSwitchToEditor=Switch to Keyboard Layout Editor
tstr_SettingsMouseShowCursor=Show Cursor
tstr_SettingsMouseShowCursorGCUnsupported=Disabling the cursor for Graphics Capture overlays is not supported on this system
tstr_SettingsMouseShowCursorGCActiveWarning=Active Graphics Capture mirrors may stop the cursor from being hidden in Desktop Duplication overlays
tstr_SettingsMouseScrollSmooth=Use Smooth Scrolling
tstr_SettingsMouseSimulatePen=Simulate as Pen Input
tstr_SettingsMouseSimulatePenUnsupported=Pen input simulation is not supported on this system
tstr_SettingsMouseAllowLaserPointerOverride=Allow Laser Pointer Override
tstr_SettingsMouseAllowLaserPointerOverrideTip=Disables the laser pointer when the physical mouse is moved rapidly.\nClick on an overlay to get the laser pointer back.
tstr_SettingsMouseDoubleClickAssist=Double-Click Assistant
tstr_SettingsMouseDoubleClickAssistTip=Freezes the mouse cursor for the set duration to ease the input of double-clicks
tstr_SettingsMouseDoubleClickAssistTipValueOff=Off
tstr_SettingsMouseDoubleClickAssistTipValueAuto=Auto
tstr_SettingsMouseSmoothing=Input Smoothing
tstr_SettingsMouseSmoothingLevelNone=None
tstr_SettingsMouseSmoothingLevelVeryLow=Very Low
tstr_SettingsMouseSmoothingLevelLow=Low
tstr_SettingsMouseSmoothingLevelMedium=Medium
tstr_SettingsMouseSmoothingLevelHigh=High
tstr_SettingsMouseSmoothingLevelVeryHigh=Very High
tstr_SettingsLaserPointerTip=These settings apply to Desktop+'s laser pointer used when the SteamVR dashboard is closed
tstr_SettingsLaserPointerBlockInput=Block Game Input while Active
tstr_SettingsLaserPointerAutoToggleDistance=Auto-Activation Max Distance
tstr_SettingsLaserPointerAutoToggleDistanceValueOff=Off
tstr_SettingsLaserPointerHMDPointer=Gaze-based HMD Pointer
tstr_SettingsLaserPointerHMDPointerTableHeaderInputAction=Input Action
tstr_SettingsLaserPointerHMDPointerTableHeaderBinding=Keyboard Key
tstr_SettingsLaserPointerHMDPointerTableBindingToggle=Toggle Laser Pointer
tstr_SettingsLaserPointerHMDPointerTableBindingLeft=Left Mouse Button
tstr_SettingsLaserPointerHMDPointerTableBindingRight=Right Mouse Button
tstr_SettingsLaserPointerHMDPointerTableBindingMiddle=Middle Mouse Button
tstr_SettingsLaserPointerHMDPointerTableBindingDrag=Drag Overlay
tstr_SettingsWindowOverlaysAutoFocus=Focus Window when Pointing at Overlay
tstr_SettingsWindowOverlaysKeepOnScreen=Keep Window on Screen
tstr_SettingsWindowOverlaysKeepOnScreenTip=Automatically move source window inside the screen's work area if its bounds lie outside of it
tstr_SettingsWindowOverlaysAutoSizeOverlay=Adjust Overlay Size when Window Resizes
tstr_SettingsWindowOverlaysFocusSceneApp=Focus Game when Laser Pointer Leaves Overlay
tstr_SettingsWindowOverlaysFocusSceneAppDashboard=Focus Game after Closing Dashboard
tstr_SettingsWindowOverlaysOnWindowDrag=On Window Drag
tstr_SettingsWindowOverlaysOnWindowDragDoNothing=Do Nothing
tstr_SettingsWindowOverlaysOnWindowDragBlock=Block Drag
tstr_SettingsWindowOverlaysOnWindowDragOverlay=Drag Overlay
tstr_SettingsWindowOverlaysOnCaptureLoss=On Capture Loss
tstr_SettingsWindowOverlaysOnCaptureLossTip=Behavior when a window capture gets lost, usually from the target window being closed.\n"Hide Overlay" also shows the overlay again when the capture gets restored.
tstr_SettingsWindowOverlaysOnCaptureLossDoNothing=Do Nothing
tstr_SettingsWindowOverlaysOnCaptureLossHide=Hide Overlay
tstr_SettingsWindowOverlaysOnCaptureLossRemove=Remove Overlay
tstr_SettingsBrowserMaxFrameRate=Maximum Frame Rate
tstr_SettingsBrowserMaxFrameRateOverrideOff=Global Setting
tstr_SettingsBrowserContentBlocker=Content Blocker
tstr_SettingsBrowserContentBlockerTip=Add block lists in the Adblock Plus syntax to the "DesktopPlusBrowser\content_block" directory.\nAll lists in the directory will be loaded.
tstr_SettingsBrowserContentBlockerListCount=(%LISTCOUNT% Lists Active)
tstr_SettingsBrowserContentBlockerListCountSingular=(%LISTCOUNT% List Active)
tstr_SettingsPerformanceUpdateLimiter=Limit Updates
tstr_SettingsPerformanceUpdateLimiterMode=Update Limiter Mode
tstr_SettingsPerformanceUpdateLimiterModeOff=Off
tstr_SettingsPerformanceUpdateLimiterModeMS=Frame Time
tstr_SettingsPerformanceUpdateLimiterModeFPS=Frame Rate
tstr_SettingsPerformanceUpdateLimiterModeOffOverride=Use Global Setting
tstr_SettingsPerformanceUpdateLimiterModeMSTip="Frame Time" enforces a minimum interval between overlay updates
tstr_SettingsPerformanceUpdateLimiterFPSValue=%FPS% fps
tstr_SettingsPerformanceUpdateLimiterOverride=Override Update Limit
tstr_SettingsPerformanceUpdateLimiterOverrideTip=When multiple overrides are active, the one resulting in the highest update rate is used
tstr_SettingsPerformanceUpdateLimiterModeOverride=Override Update Limiter Mode
tstr_SettingsPerformanceRapidUpdates=Reduce Laser Pointer Latency
tstr_SettingsPerformanceRapidUpdatesTip=Reduce latency of laser pointer input at the cost of additional CPU load while pointing at an overlay
tstr_SettingsPerformanceSingleDesktopMirror=Single Desktop Mirroring
tstr_SettingsPerformanceSingleDesktopMirrorTip=Mirror individual desktops when switching to them instead of cropping from the combined desktop.\nWhen this is active, all overlays will be showing the same desktop.
tstr_SettingsPerformanceAlternativeCursorRendering=Alternative Cursor Rendering
tstr_SettingsPerformanceAlternativeCursorRenderingTip=Use a different method to capture and render the cursor.\nThis can help if there are problems with the default method of capturing the hardware cursor.
tstr_SettingsPerformanceUseHDR=HDR Mirroring
tstr_SettingsPerformanceUseHDRTip=Mirror desktops and windows using higher bit-depth textures, supporting HDR output. Experimental.\nMay negatively impact performance when not required and increases VRAM usage.
tstr_SettingsPerformanceShowFPS=Show FPS in Floating UI
tstr_SettingsWarningsHidden=Warnings Hidden:
tstr_SettingsWarningsReset=Reset Hidden Warnings
tstr_SettingsStartupAutoLaunch=Launch Automatically on SteamVR Startup
tstr_SettingsStartupSteamDisable=Disable Steam Integration
tstr_SettingsStartupSteamDisableTip=Restarts Desktop+ without Steam when it was launched by it.\nThis disables the permanent in-app status, usage time statistics and other Steam features.
tstr_SettingsTroubleshootingRestart=Restart
tstr_SettingsTroubleshootingRestartSteam=Restart with Steam
tstr_SettingsTroubleshootingRestartDesktop=Restart in Desktop Mode
tstr_SettingsTroubleshootingElevatedModeEnter=Enter Elevated Mode
tstr_SettingsTroubleshootingElevatedModeLeave=Leave Elevated Mode
tstr_SettingsTroubleshootingSettingsReset=Restore Default Settings
tstr_SettingsTroubleshootingSettingsResetConfirmDescription=Restoring default settings will reset the selected elements to their default values.\nThis cannot be undone.\n\nReset the following:
tstr_SettingsTroubleshootingSettingsResetConfirmButton=Reset Selected Elements
tstr_SettingsTroubleshootingSettingsResetConfirmElementOverlays=Current Overlay Setup
tstr_SettingsTroubleshootingSettingsResetConfirmElementLegacyFiles=Delete Unused Legacy Configuration & Profile Files
tstr_SettingsTroubleshootingSettingsResetShowQuickStart=Show Quick-Start Guide
;Keyboard Window
tstr_KeyboardWindowTitle=Desktop+ Keyboard
tstr_KeyboardWindowTitleSettings=Desktop+ Keyboard (Settings)
tstr_KeyboardWindowTitleOverlay=Desktop+ Keyboard (%OVERLAYNAME%)
tstr_KeyboardWindowTitleOverlayUnknown=[Unknown Overlay]
;Keyboard Shortcuts Window
tstr_KeyboardShortcutsCut=Cut
tstr_KeyboardShortcutsCopy=Copy
tstr_KeyboardShortcutsPaste=Paste
;Overlay Properties Window
tstr_OvrlPropsCatPosition=Position
tstr_OvrlPropsCatAppearance=Appearance
tstr_OvrlPropsCatCapture=Capture
tstr_OvrlPropsCatPerformanceMonitor=Performance Monitor
tstr_OvrlPropsCatBrowser=Browser
tstr_OvrlPropsCatAdvanced=Advanced
tstr_OvrlPropsCatPerformance=Performance
tstr_OvrlPropsCatInterface=Interface
tstr_OvrlPropsPositionOrigin=Origin
tstr_OvrlPropsPositionOriginRoom=Play Area
tstr_OvrlPropsPositionOriginHMDXY=HMD Floor Position
tstr_OvrlPropsPositionOriginDashboard=Dashboard
tstr_OvrlPropsPositionOriginHMD=HMD
tstr_OvrlPropsPositionOriginSeatedSpace=Seated Position
tstr_OvrlPropsPositionOriginControllerR=Right Controller
tstr_OvrlPropsPositionOriginControllerL=Left Controller
tstr_OvrlPropsPositionOriginTheaterScreen=Theater Screen
tstr_OvrlPropsPositionOriginTracker1=Tracker #1
tstr_OvrlPropsPositionOriginConfigHMDXYTurning=Turn with HMD
tstr_OvrlPropsPositionOriginConfigTheaterScreenEnter=Enter Theater Mode
tstr_OvrlPropsPositionOriginConfigTheaterScreenLeave=Leave Theater Mode
tstr_OvrlPropsPositionOriginConfigSmoothing=Origin Smoothing
tstr_OvrlPropsPositionOriginTheaterScreenTip=Some properties are controlled by the SteamVR Theater Screen
tstr_OvrlPropsPositionDispMode=Display Mode
tstr_OvrlPropsPositionDispModeAlways=Always
tstr_OvrlPropsPositionDispModeDashboard=Only in Dashboard
tstr_OvrlPropsPositionDispModeScene=Only In-Game
tstr_OvrlPropsPositionDispModeDPlus=Only in Desktop+ Tab
tstr_OvrlPropsPositionPos=Position
tstr_OvrlPropsPositionPosTip=Position can only be changed or reset while Desktop+ is running
tstr_OvrlPropsPositionChange=Change
tstr_OvrlPropsPositionReset=Reset
tstr_OvrlPropsPositionLock=Lock
tstr_OvrlPropsPositionChangeHeader=Change Overlay Position
tstr_OvrlPropsPositionChangeHelp=Drag any overlay around to change its position.\nHold right-click for two-handed gesture transform.
tstr_OvrlPropsPositionChangeHelpDesktop=Hold down the drag buttons ("D") to move or rotate the overlay with the mouse.
tstr_OvrlPropsPositionChangeManualAdjustment=Manual Adjustment
tstr_OvrlPropsPositionChangeMove=Move
tstr_OvrlPropsPositionChangeRotate=Rotate
tstr_OvrlPropsPositionChangeForward=Forward
tstr_OvrlPropsPositionChangeBackward=Backward
tstr_OvrlPropsPositionChangeRollCW=Roll ⟳
tstr_OvrlPropsPositionChangeRollCCW=Roll ⟲
tstr_OvrlPropsPositionChangeLookAt=To HMD
tstr_OvrlPropsPositionChangeDragButton=D
tstr_OvrlPropsPositionChangeOffset=Additional Offset
tstr_OvrlPropsPositionChangeOffsetUpDown=Up/Down Offset
tstr_OvrlPropsPositionChangeOffsetRightLeft=Right/Left Offset
tstr_OvrlPropsPositionChangeOffsetForwardBackward=Forward/Backward Offset
tstr_OvrlPropsPositionChangeDragSettings=Drag Settings
tstr_OvrlPropsPositionChangeDragSettingsAutoDocking=Dock to Controller when Near
tstr_OvrlPropsPositionChangeDragSettingsForceDistance=Force Fixed Distance
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShape=Shape
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShapeSphere=Sphere
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShapeCylinder=Cylinder
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceAutoCurve=Auto-Curve
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceAutoTilt=Auto-Tilt
tstr_OvrlPropsPositionChangeDragSettingsSnapPosition=Snap Position
tstr_OvrlPropsPositionChangeDragSettingsSnapRotation=Snap Rotation
tstr_OvrlPropsPositionChangeDragSettingsSnapRotationPitch=Pitch
tstr_OvrlPropsPositionChangeDragSettingsSnapRotationYaw=Yaw
tstr_OvrlPropsPositionChangeDragSettingsSnapRotationRoll=Roll
tstr_OvrlPropsAppearanceWidth=Width
tstr_OvrlPropsAppearanceCurve=Curvature
tstr_OvrlPropsAppearanceOpacity=Opacity
tstr_OvrlPropsAppearanceBrightness=Brightness
tstr_OvrlPropsAppearanceCrop=Crop
tstr_OvrlPropsAppearanceCropValueMax=Max
tstr_OvrlPropsAppearanceShowBackside=Show Backside
tstr_OvrlPropsCrop=Cropping Area
tstr_OvrlPropsCropHelp=Drag the rectangle to change the crop.\nUse scroll input or drag the edges to adjust the size of the cropping rectangle.
tstr_OvrlPropsCropManualAdjust=Manual Adjustment
tstr_OvrlPropsCropInvalidTip=The current cropping rectangle is invalid. The overlay may not be visible as a result.
tstr_OvrlPropsCropX=X
tstr_OvrlPropsCropY=Y
tstr_OvrlPropsCropWidth=Width
tstr_OvrlPropsCropHeight=Height
tstr_OvrlPropsCropToWindow=Crop to Active Window
tstr_OvrlPropsCaptureMethod=Capture Method
tstr_OvrlPropsCaptureMethodDup=Desktop Duplication
tstr_OvrlPropsCaptureMethodGC=Graphics Capture
tstr_OvrlPropsCaptureMethodGCUnsupportedTip=Graphics Capture is not supported on this system
tstr_OvrlPropsCaptureMethodGCUnsupportedPartialTip=Some Graphics Capture features are not supported on this system
tstr_OvrlPropsCaptureSource=Source
tstr_OvrlPropsCaptureGCSource=Graphics Capture Source
tstr_OvrlPropsCaptureSourceUnknownWarning=This overlay uses an unknown capture source and may not work correctly
tstr_OvrlPropsCaptureGCStrictMatching=Use Strict Window Matching
tstr_OvrlPropsCaptureGCStrictMatchingTip=Only allow exact window title matches when restoring this overlay's capture
tstr_OvrlPropsPerfMonDesktopModeTip=Performance Monitor overlays do not update in desktop mode
tstr_OvrlPropsPerfMonGlobalTip=These settings apply to all Performance Monitor overlays
tstr_OvrlPropsPerfMonStyle=Style
tstr_OvrlPropsPerfMonItems=Items
tstr_OvrlPropsPerfMonStyleMinimal=Minimal
tstr_OvrlPropsPerfMonStyleCompact=Compact
tstr_OvrlPropsPerfMonStyleLarge=Large
tstr_OvrlPropsPerfMonStyleShowWindow=Show Window
tstr_OvrlPropsPerfMonStyleShowTextOutline=Show Text Outline
tstr_OvrlPropsPerfMonStyleMinimalShowMore=Show More Items
tstr_OvrlPropsPerfMonItemCPU=CPU Stats
tstr_OvrlPropsPerfMonItemGPU=GPU Stats
tstr_OvrlPropsPerfMonItemGraphs=Graphs
tstr_OvrlPropsPerfMonItemFrameStats=Frame Stats
tstr_OvrlPropsPerfMonItemTime=Time
tstr_OvrlPropsPerfMonItemBattery=Battery Stats
tstr_OvrlPropsPerfMonItemTrackerBattery=Tracker Battery Levels
tstr_OvrlPropsPerfMonItemViveWirelessTemp=Vive Wireless Temperature
tstr_OvrlPropsPerfMonResetValues=Reset Cumulative Values
tstr_OvrlPropsBrowserNotAvailableTip=Desktop+ Browser component is not installed
tstr_OvrlPropsBrowserCloned=Cloned Output
tstr_OvrlPropsBrowserClonedTip=This overlay is a clone of "%OVERLAYNAME%".\nChanges done to the Browser properties will apply to the original and all overlays cloning from it.
tstr_OvrlPropsBrowserClonedConvert=Convert to Standalone
tstr_OvrlPropsBrowserURL=URL
tstr_OvrlPropsBrowserURLHint=Enter an Address
tstr_OvrlPropsBrowserGo=Go
tstr_OvrlPropsBrowserRestore=Restore Last Input
tstr_OvrlPropsBrowserWidth=Width
tstr_OvrlPropsBrowserHeight=Height
tstr_OvrlPropsBrowserZoom=Zoom
tstr_OvrlPropsBrowserAllowTransparency=Allow Transparency
tstr_OvrlPropsBrowserAllowTransparencyTip=Allows web pages to use transparency.\nSites not defining any background color may not display correctly.
tstr_OvrlPropsBrowserRecreateContext=Recreate Browser Context
tstr_OvrlPropsBrowserRecreateContextTip=The browser context needs to be recreated to apply the change.\nDoing so will reload the page and clear the navigation history.
tstr_OvrlPropsAdvanced3D=3D
tstr_OvrlPropsAdvancedHSBS=Half Side-by-Side
tstr_OvrlPropsAdvancedSBS=Side-by-Side
tstr_OvrlPropsAdvancedHOU=Half Over-Under
tstr_OvrlPropsAdvancedOU=Over-Under
tstr_OvrlPropsAdvanced3DSwap=Swap Eyes
tstr_OvrlPropsAdvancedGazeFade=Gaze Fade
tstr_OvrlPropsAdvancedGazeFadeAuto=Auto-Configure
tstr_OvrlPropsAdvancedGazeFadeDistance=Distance
tstr_OvrlPropsAdvancedGazeFadeDistanceValueInf=Infinite
tstr_OvrlPropsAdvancedGazeFadeSensitivity=Sensitivity
tstr_OvrlPropsAdvancedGazeFadeOpacity=Target Opacity
tstr_OvrlPropsAdvancedInput=Laser Pointer Input
tstr_OvrlPropsAdvancedInputInGame=Enable In-Game
tstr_OvrlPropsAdvancedInputFloatingUI=Show Floating UI
tstr_OvrlPropsAdvancedOverlayTags=Overlay Tags
tstr_OvrlPropsAdvancedOverlayTagsTip=Overlay tags are used to target overlays in actions
tstr_OvrlPropsPerformanceInvisibleUpdate=Update while Invisible
tstr_OvrlPropsPerformanceInvisibleUpdateTip=Update overlay even while invisible from opacity setting or Gaze Fade.\nHelps with third-party applications accessing the overlay's contents.\nNot recommended otherwise.\nUpdates are still suspended if the overlay is hidden manually or by display mode setting.
tstr_OvrlPropsInterfaceOverlayName=Overlay Name
tstr_OvrlPropsInterfaceOverlayNameAuto=[Name Automatically]
tstr_OvrlPropsInterfaceActionOrderCustom=Override Action Buttons
tstr_OvrlPropsInterfaceDesktopButtons=Show Desktop Buttons
tstr_OvrlPropsInterfaceExtraButtons=Show Extra Buttons
;Overlay Bar
tstr_OverlayBarOvrlHide=Hide
tstr_OverlayBarOvrlShow=Show
tstr_OverlayBarOvrlClone=Clone
tstr_OverlayBarOvrlRemove=Remove
tstr_OverlayBarOvrlRemoveConfirm=Really?
tstr_OverlayBarOvrlProperties=Properties...
tstr_OverlayBarOvrlAddWindow=Window...
tstr_OverlayBarTooltipOvrlAdd=Add Overlay
tstr_OverlayBarTooltipSettings=Settings
tstr_OverlayBarTooltipResetHold=Hold to reset window position...
;Floating UI
tstr_FloatingUIHideOverlayTip=Hide Overlay
tstr_FloatingUIHideOverlayHoldTip=Hold to remove overlay...
tstr_FloatingUIDragModeEnableTip=Enable Drag-Mode
tstr_FloatingUIDragModeDisableTip=Disable Drag-Mode
tstr_FloatingUIDragModeHoldLockTip=Hold to lock overlay position...
tstr_FloatingUIDragModeHoldUnlockTip=Hold to unlock overlay position...
tstr_FloatingUIWindowAddTip=Add Active Window as Overlay
tstr_FloatingUIActionBarShowTip=Show Action-Bar
tstr_FloatingUIActionBarHideTip=Hide Action-Bar
tstr_FloatingUIBrowserGoBackTip=Go to Previous Page
tstr_FloatingUIBrowserGoForwardTip=Go to Next Page
tstr_FloatingUIBrowserRefreshTip=Refresh Current Page
tstr_FloatingUIBrowserStopTip=Stop Page Load
tstr_FloatingUIActionBarDesktopPrev=Previous Desktop
tstr_FloatingUIActionBarDesktopNext=Next Desktop
tstr_FloatingUIActionBarEmpty=No actions enabled
;Special Actions
tstr_ActionNone=[None]
tstr_ActionKeyboardShow=Show Keyboard
tstr_ActionKeyboardHide=Hide Keyboard
;Default Actions (translation IDs are matched to Action name string)
tstr_DefActionShowKeyboard=Show Keyboard
tstr_DefActionActiveWindowCrop=Crop to Active Window
tstr_DefActionActiveWindowCropLabel=Crop to\nActive\nWindow
tstr_DefActionSwitchTask=Switch Task
tstr_DefActionToggleOverlays=Toggle Overlays
tstr_DefActionToggleOverlaysLabel=Toggle\nOverlays
tstr_DefActionMiddleMouse=Middle Mouse Button
tstr_DefActionMiddleMouseLabel=Middle\nMouse\nButton
tstr_DefActionBackMouse=Back Mouse Button
tstr_DefActionBackMouseLabel=Back\nMouse\nButton
tstr_DefActionReadMe=Open ReadMe
tstr_DefActionReadMeLabel=Open\nReadMe
tstr_DefActionDashboardToggle=Toggle SteamVR Dashboard (Debug Command)
tstr_DefActionDashboardToggleLabel=Toggle\nDashboard
;Performance Monitor (text space is very limited here, so keep it short or untranslated)
tstr_PerformanceMonitorCPU=CPU
tstr_PerformanceMonitorGPU=GPU
tstr_PerformanceMonitorRAM=RAM:
tstr_PerformanceMonitorVRAM=VRAM:
tstr_PerformanceMonitorFrameTime=Frame Time:
tstr_PerformanceMonitorLoad=Load:
tstr_PerformanceMonitorFPS=FPS:
tstr_PerformanceMonitorFPSAverage=Average FPS:
tstr_PerformanceMonitorReprojectionRatio=Reprojection Ratio:
tstr_PerformanceMonitorDroppedFrames=Dropped Frames:
tstr_PerformanceMonitorBatteryLeft=Left Controller:
tstr_PerformanceMonitorBatteryRight=Right Controller:
tstr_PerformanceMonitorBatteryHMD=Headset:
tstr_PerformanceMonitorBatteryTracker=Tracker %ID%:
tstr_PerformanceMonitorBatteryDisconnected=N/A
tstr_PerformanceMonitorViveWirelessTempNotAvailable=N/A
tstr_PerformanceMonitorCompactCPU=CPU
tstr_PerformanceMonitorCompactGPU=GPU
tstr_PerformanceMonitorCompactFPS=FPS
tstr_PerformanceMonitorCompactFPSAverage=AVG
tstr_PerformanceMonitorCompactReprojectionRatio=% RPR
tstr_PerformanceMonitorCompactDroppedFrames=DRP
tstr_PerformanceMonitorCompactBattery=BAT
tstr_PerformanceMonitorCompactBatteryLeft=L
tstr_PerformanceMonitorCompactBatteryRight=R
tstr_PerformanceMonitorCompactBatteryHMD=H
tstr_PerformanceMonitorCompactBatteryTracker=T%ID%
tstr_PerformanceMonitorCompactBatteryDisconnected=N/A
tstr_PerformanceMonitorCompactViveWirelessTempNotAvailable=N/A
tstr_PerformanceMonitorEmpty=No Performance Monitor Items enabled.
;Aux UI
tstr_AuxUIDragHintDocking=Release to dock
tstr_AuxUIDragHintUndocking=Release to undock
tstr_AuxUIDragHintOvrlLocked=Unlock overlay position to drag this overlay
tstr_AuxUIDragHintOvrlTheaterScreenBlocked=The overlay's position is controlled by the SteamVR Theater Screen
tstr_AuxUIGazeFadeAutoHint=Look at the center of the overlay and wait for %SECONDS% seconds...
tstr_AuxUIGazeFadeAutoHintSingular=Look at the center of the overlay and wait for %SECONDS% second...
tstr_AuxUIQuickStartWelcomeHeader=Welcome to Desktop+!
tstr_AuxUIQuickStartWelcomeBody=This short guide will introduce you to the very basics of the application.\nFor more detailed information see the ReadMe and User Guide.\n\nIf you wish to skip this, simply press [Close].
tstr_AuxUIQuickStartOverlaysHeader=Overlays
tstr_AuxUIQuickStartOverlaysBody=Desktop+ allows you to create overlays mirroring your desktops, individual windows and more.\nAll overlays you've created will be listed in the Overlay Bar down below.\n\nOverlay properties can be modified by clicking on one of the overlay icons and choosing [Properties...].
tstr_AuxUIQuickStartOverlaysBody2=New overlays can be created by pressing [+] and choosing a capture source/overlay type from the list.\nSome types, such as web browser overlays, are only available if the respective application component is installed.\n\nIndividual overlays or complete layouts can be saved in profiles.\nThe current overlay setup will automatically be remembered between sessions.
tstr_AuxUIQuickStartOverlayPropertiesHeader=Overlay Properties
tstr_AuxUIQuickStartOverlayPropertiesBody=Overlays have a variety of customization options.\nOrigin and display mode set where and when an overlay is displayed.\nRemember to review these properties when you feel an overlay should be visible but you can't find it.\nResetting its position may also help if the former doesn't.\n\nYou can also dock an overlay to motion controllers by dragging it close to one.
tstr_AuxUIQuickStartOverlayPropertiesBody2=Some properties are hidden by default.\nToggle "Show Advanced Settings" in the Settings window to show all options.\n\nThe position of UI windows can be reset by long-pressing their respective button.\nDouble- and right-click works on overlay buttons as well, as quick shortcut toggles.
tstr_AuxUIQuickStartSettingsHeader=Settings
tstr_AuxUIQuickStartSettingsBody=The Settings window contains all global settings for Desktop+.\nIt can be opened by pressing the gear button at the right end of the Overlay Bar.\n\nJust like for overlays, changes to settings are applied and saved automatically.
tstr_AuxUIQuickStartProfilesHeader=Profiles
tstr_AuxUIQuickStartProfilesBody=There two kinds of profiles in Desktop+.\n\nOverlay Profiles:\nStore one or multiple overlays with their properties.\nThey can be loaded manually or as a result of actions and application profiles.\n\nApplication Profiles:\nAssign an overlay profile and/or actions to be loaded automatically when a SteamVR application is launched.
tstr_AuxUIQuickStartActionsHeader=Actions
tstr_AuxUIQuickStartActionsBody=Actions in Desktop+ are a series of commands that can be assigned to controller inputs and application profiles, or added to the Action Bar of overlays.\nAction commands range from simulating inputs on the desktop to making changes to the overlay layout.\n
External applications can also be launched for advanced use-cases.\n\nDesktop+ comes with a few example actions that you can check out.
tstr_AuxUIQuickStartActionsBody2=Actions by default target the overlay that it was activated on (action button displayed for the overlay, controller input on the overlay) or the focused overlay if the former isn't applicable.\n\nThe focused overlay is the overlay that was last clicked into.\nIn many cases the target overlay doesn't matter, such as input simulated on the desktop. In others it may be useful to specify a different target overlay or even multiple.
tstr_AuxUIQuickStartOverlayTagsHeader=Overlay Tags
tstr_AuxUIQuickStartOverlayTagsBody=Overlay tags can be used for this purpose.\nThere are automatic tags (green label) and user-defined tags.\nAutomatic tags are assigned automatically based on the property they represent.\nUser-defined tags can be assigned manually to overlays in the Overlay Properties window.\n\nNote that actions are only indirectly bound to controller buttons for global shortcuts. The numbered global shortcut also has to be assigned to a controller input via the SteamVR's input binding interface.
tstr_AuxUIQuickStartSettingsEndBody=There are many more settings available.\nDon't be afraid to play with the toggles to check them out.\n\nIf you do get stuck, you can press [Restore Default Settings] at the bottom of the window.\nYou get the option to only restore specific elements.
tstr_AuxUIQuickStartFloatingUIHeader=Floating UI
tstr_AuxUIQuickStartFloatingUIBody=The interface referred to as "Floating UI" is displayed whenever you point at an overlay.\nIn the dashboard it is always visible if no other Overlay is being pointed at.\n\nThe Floating UI contains basic controls for modifying the overlay, such as toggling Drag Mode to allow moving it, as well as a customizable section for action buttons.\n\nDepending on the overlay type there may also be other controls.\nDisplay of those can be toggled for each overlay in the respective overlay properties.
tstr_AuxUIQuickStartDesktopModeHeader=Desktop Mode
tstr_AuxUIQuickStartDesktopModeBody=Desktop+ can also be configured in a window displayed on the desktop.\nThis can be useful for tasks that require frequent keyboard input or to make changes without motion controllers connected.\n\nWhile almost all functionality is the same in both modes, the Keyboard Layout Editor used to customize layouts for the Desktop+ VR keyboard is only accessible in desktop mode.\n\nYou can switch to it in the Troubleshooting section of the Settings window, or from the Desktop+ icon in the system tray/notification area.
tstr_AuxUIQuickStartEndHeader=ReadMe & User Guide
tstr_AuxUIQuickStartEndBody=This should already be enough to get you started with Desktop+.\n\nIf you run into any trouble or get stuck, make sure to check out the ReadMe as well. The button in the Action Bar will open it for you.\nDetailed explanations of each option and step-by-step guides for common usage scenarios can be found the in the user guide.\n\nTo see this guide again, press [Show Quick-Start Guide] at the bottom right of the "Restore Default Settings" page.
tstr_AuxUIQuickStartButtonNext=Next Page
tstr_AuxUIQuickStartButtonPrev=Previous Page
tstr_AuxUIQuickStartButtonClose=Close
;Desktop Mode
tstr_DesktopModeCatTools=Tools
tstr_DesktopModeCatOverlays=Overlays
tstr_DesktopModeToolSettings=Settings
tstr_DesktopModeToolActions=Actions
tstr_DesktopModeOverlayListAdd=Add Overlay
tstr_DesktopModePageAddWindowOverlayTitle=Add Window Overlay
tstr_DesktopModePageAddWindowOverlayHeader=Choose a Window
;Keyboard Editor
tstr_KeyboardEditorKeyListTitle=Key List
tstr_KeyboardEditorKeyListTabContextReplace=Replace Contents With...
tstr_KeyboardEditorKeyListTabContextClear=Clear Sub-Layout
tstr_KeyboardEditorKeyListRow=Row %ID%
tstr_KeyboardEditorKeyListSpacing=[Spacing]
tstr_KeyboardEditorKeyListKeyAdd=Add
tstr_KeyboardEditorKeyListKeyDuplicate=Duplicate
tstr_KeyboardEditorKeyListKeyRemove=Remove
tstr_KeyboardEditorKeyPropertiesTitle=Key Properties
tstr_KeyboardEditorKeyPropertiesNoSelection=No Key Selected
tstr_KeyboardEditorKeyPropertiesType=Type
tstr_KeyboardEditorKeyPropertiesTypeBlank=Blank Space
tstr_KeyboardEditorKeyPropertiesTypeVirtualKey=Virtual Key
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyToggle=Virtual Key (Toggle)
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyIsoEnter=Virtual Key (ISO-Enter)
tstr_KeyboardEditorKeyPropertiesTypeString=String
tstr_KeyboardEditorKeyPropertiesTypeSublayoutToggle=Toggle Sub-Layout
tstr_KeyboardEditorKeyPropertiesTypeAction=Action
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyIsoEnterTip=Use two "Virtual Key (ISO-Enter)" keys in adjacent rows to build an ISO-Enter shaped key.\nOnly one combined key in each sublayout.
tstr_KeyboardEditorKeyPropertiesTypeStringTip=Use string type for non-basic character keys.\nThis allows Desktop+ to figure out the correct key combination on the real keyboard layout to increase application compatibility.
tstr_KeyboardEditorKeyPropertiesSize=Size
tstr_KeyboardEditorKeyPropertiesLabel=Label
tstr_KeyboardEditorKeyPropertiesKeyCode=Key Code
tstr_KeyboardEditorKeyPropertiesString=String
tstr_KeyboardEditorKeyPropertiesSublayout=Sub-Layout
tstr_KeyboardEditorKeyPropertiesAction=Action
tstr_KeyboardEditorKeyPropertiesCluster=Cluster
tstr_KeyboardEditorKeyPropertiesClusterTip=Cluster assignment is used to selectively disable loading of keys.\nSurrounding blank space is not removed. Carefully assign blank space type keys to clusters in order to have them toggle correctly.
tstr_KeyboardEditorKeyPropertiesBlockModifiers=Block Modifiers
tstr_KeyboardEditorKeyPropertiesBlockModifiersTip=Releases all modifier keys while the key is pressed
tstr_KeyboardEditorKeyPropertiesNoRepeat=Never Repeat
tstr_KeyboardEditorKeyPropertiesNoRepeatTip=Block key input from repeating while holding down, even if key repeat is enabled
tstr_KeyboardEditorMetadataTitle=Layout Metadata
tstr_KeyboardEditorMetadataName=Name
tstr_KeyboardEditorMetadataAuthor=Author
tstr_KeyboardEditorMetadataHasAltGr=Has AltGr
tstr_KeyboardEditorMetadataHasAltGrTip=Right alt key switches to AltGr sub-layout when down
tstr_KeyboardEditorMetadataClusterPreview=Preview Clusters:
tstr_KeyboardEditorMetadataSave=Save...
tstr_KeyboardEditorMetadataLoad=Load...
tstr_KeyboardEditorMetadataSavePopupTitle=Save Keyboard Layout
tstr_KeyboardEditorMetadataSavePopupFilename=File Name
tstr_KeyboardEditorMetadataSavePopupFilenameBlankTip=File name must not be blank
tstr_KeyboardEditorMetadataSavePopupConfirm=Save Layout
tstr_KeyboardEditorMetadataSavePopupConfirmError=Failed to save the layout
tstr_KeyboardEditorMetadataLoadPopupTitle=Load Keyboard Layout
tstr_KeyboardEditorMetadataLoadPopupConfirm=Load Layout
tstr_KeyboardEditorPreviewTitle=Keyboard Preview
tstr_KeyboardEditorSublayoutBase=Base
tstr_KeyboardEditorSublayoutShift=Shift
tstr_KeyboardEditorSublayoutAltGr=AltGr
tstr_KeyboardEditorSublayoutAux=Aux
;General Dialog Strings
tstr_DialogOk=Ok
tstr_DialogCancel=Cancel
tstr_DialogDone=Done
tstr_DialogUndo=Undo
tstr_DialogRedo=Redo
tstr_DialogColorPickerHeader=Pick a Color
tstr_DialogColorPickerCurrent=Current
tstr_DialogColorPickerOriginal=Original
tstr_DialogProfilePickerHeader=Pick a Profile
tstr_DialogProfilePickerNone=[None]
tstr_DialogActionPickerHeader=Pick an Action
tstr_DialogActionPickerEmpty=No actions available
tstr_DialogIconPickerHeader=Pick an Icon
tstr_DialogIconPickerHeaderTip=Custom icons can be added as PNG files in the "images\icons\" directory
tstr_DialogIconPickerNone=[No Icon]
tstr_DialogKeyCodePickerHeader=Pick a Key Code
tstr_DialogKeyCodePickerHeaderHotkey=Pick a Hotkey
tstr_DialogKeyCodePickerModifiers=Modifiers
tstr_DialogKeyCodePickerKeyCode=Key Code
tstr_DialogKeyCodePickerKeyCodeHint=Filter List
tstr_DialogKeyCodePickerKeyCodeNone=[None]
tstr_DialogKeyCodePickerFromInput=From Input...
tstr_DialogKeyCodePickerFromInputPopup=Press any key or mouse button...
tstr_DialogKeyCodePickerFromInputPopupNoMouse=Press any key...
tstr_DialogWindowPickerHeader=Pick a Window
tstr_DialogInputTagsHint=Filter or add new tag
;Source Strings
tstr_SourceDesktopAll=Combined Desktop
tstr_SourceDesktopID=Desktop %ID%
tstr_SourceWinRTNone=[No Capture Target]
tstr_SourceWinRTUnknown=[Unknown Window]
tstr_SourceWinRTClosed=[Closed]:
tstr_SourcePerformanceMonitor=Performance Monitor
tstr_SourceBrowser=Browser
tstr_SourceBrowserNoPage=[No Page Loaded]
;Notification Icon
tstr_NotificationIconRestoreVR=Restore VR Interface
tstr_NotificationIconOpenOnDesktop=Open Settings on Desktop
tstr_NotificationIconQuit=Quit
;Notifications
tstr_NotificationInitialStartupTitleVR=Initial Setup
tstr_NotificationInitialStartupTitleDesktop=Desktop+ Initial Setup
tstr_NotificationInitialStartupMessage=Desktop+ has been successfully added to SteamVR and will now automatically launch when SteamVR is run.
;Browser
tstr_BrowserErrorPageTitle=Page Load Error - Desktop+
tstr_BrowserErrorPageHeading=The page could not be loaded
tstr_BrowserErrorPageMessage=There was an error trying to load %URL%: %ERROR%
================================================
FILE: assets/lang/ja.ini
================================================
[TranslationInfo]
Name=日本語 (Japanese)
Author=まるまさ
Locale=ja-JP
[Strings]
;Settings Window
tstr_SettingsWindowTitle=Desktop+ 設定
tstr_SettingsCatInterface=インターフェイス
tstr_SettingsCatEnvironment=システム構成
tstr_SettingsCatProfiles=プロファイル
tstr_SettingsCatActions=アクション
tstr_SettingsCatKeyboard=キーボード
tstr_SettingsCatMouse=マウス
tstr_SettingsCatLaserPointer=レーザーポインター
tstr_SettingsCatWindowOverlays=画面のオーバーレイ
tstr_SettingsCatBrowser=ブラウザ
tstr_SettingsCatPerformance=パフォーマンス
tstr_SettingsCatVersionInfo=バージョン情報
tstr_SettingsCatWarnings=警告
tstr_SettingsCatStartup=スタートアップ
tstr_SettingsCatTroubleshooting=トラブルシューティング
tstr_SettingsWarningPrefix=警告:
tstr_SettingsWarningCompositorResolution=SteamVR の内部解像度が100%を下回っています! これはオーバーレイのレンダリング品質に影響します。
tstr_SettingsWarningCompositorQuality=SteamVR オーバーレイのレンダリング品質が高くありません!
tstr_SettingsWarningProcessElevated=Desktop+ は管理者権限で実行されています!
tstr_SettingsWarningElevatedMode=管理者モードが有効です!
tstr_SettingsWarningBrowserMissing=ブラウザオーバーレイが使用されていますが、Desktop+ ブラウザコンポーネントは現在使用できません。
tstr_SettingsWarningBrowserMismatch=インストールされている Desktop+ ブラウザは、このバージョンの Desktop+ と互換性がありません!
tstr_SettingsWarningElevatedProcessFocus=管理者プロセスがフォーカスされています!\nDesktop+ は現在、入力のシミュレーションができません。
tstr_SettingsWarningUIAccessLost=Desktop+ がUIアクセス権限で実行されなくなりました!
tstr_SettingsWarningOverlayCreationErrorLimit=オーバーレイの作成に失敗しました! (オーバーレイの上限を超えました)
tstr_SettingsWarningOverlayCreationErrorOther=オーバーレイの作成に失敗しました! (%ERRORNAME%)
tstr_SettingsWarningGraphicsCaptureError=グラフィックキャプチャースレッドで予期しないエラーが発生しました! (%ERRORCODE%)
tstr_SettingsWarningAppProfileActive=%APPNAME% のアプリケーションプロファイルが、現在のオーバーレイのレイアウトを上書きしました。\nオーバーレイに加えられた内容は、それが無効になるまで自動では保存されません。
tstr_SettingsWarningConfigMigrated=以前のバージョンの Desktop+ の設定とプロファイルは新しい形式に移行されました。\n元のファイルは削除されていないため、アプリケーションの古いバージョンでも引き続き使用できます。\n最初からやり直す場合は、[初期設定に戻す] を参照してください。
tstr_SettingsWarningMenuDontShowAgain=次回から表示しない
tstr_SettingsWarningMenuDismiss=閉じる
tstr_SettingsInterfaceLanguage=言語
tstr_SettingsInterfaceLanguageCommunity=%AUTHOR% によるコミュニティ翻訳
tstr_SettingsInterfaceLanguageIncompleteWarning=翻訳が不正確な場合があります
tstr_SettingsInterfaceAdvancedSettings=詳細設定を表示
tstr_SettingsInterfaceAdvancedSettingsTip=高度な設定とあまり使用されない設定
tstr_SettingsInterfaceBlankSpaceDrag=空白スペースをクリックするとウィンドウがドラッグ
tstr_SettingsInterfacePersistentUI=永続的なUI
tstr_SettingsInterfacePersistentUIManage=管理
tstr_SettingsInterfaceDesktopButtons=デスクトップボタンのスタイル
tstr_SettingsInterfaceDesktopButtonsNone=なし
tstr_SettingsInterfaceDesktopButtonsIndividual=個別
tstr_SettingsInterfaceDesktopButtonsCycle=サイクルボタン
tstr_SettingsInterfaceDesktopButtonsAddCombined=複合デスクトップ追加ボタン
tstr_SettingsInterfacePersistentUIHelp=Desktop+ は、一般用 (グローバル) と、Desktop+ SteamVR ダッシュボードタブ (Desktop+ タブ) 内での使用に分けて、インターフェイスのウィンドウの状態を記憶します。
tstr_SettingsInterfacePersistentUIHelp2=コントローラーを使って、ウィンドウの状態を直接操作することができます。\n場合によっては、ウィンドウを表示可能な場所まで移動させるために、位置をリセットする必要があることに注意してください。
tstr_SettingsInterfacePersistentUIWindowsHeader=ウィンドウズ
tstr_SettingsInterfacePersistentUIWindowsSettings=設定
tstr_SettingsInterfacePersistentUIWindowsProperties=オーバーレイのプロパティ
tstr_SettingsInterfacePersistentUIWindowsKeyboard=Desktop+ キーボード
tstr_SettingsInterfacePersistentUIWindowsStateGlobal=グローバル
tstr_SettingsInterfacePersistentUIWindowsStateDashboardTab=Desktop+ タブ
tstr_SettingsInterfacePersistentUIWindowsStateVisible=表示
tstr_SettingsInterfacePersistentUIWindowsStatePinned=ピン留め
tstr_SettingsInterfacePersistentUIWindowsStatePosition=位置
tstr_SettingsInterfacePersistentUIWindowsStatePositionReset=リセット
tstr_SettingsInterfacePersistentUIWindowsStateSize=大きさ
tstr_SettingsInterfacePersistentUIWindowsStateLaunchRestore=Desktop+ の起動時の状態を復元
tstr_SettingsEnvironmentBackgroundColor=背景の色
tstr_SettingsEnvironmentBackgroundColorDispModeNever=常に非表示
tstr_SettingsEnvironmentBackgroundColorDispModeDPlusTab=Desktop+ タブでのみ表示
tstr_SettingsEnvironmentBackgroundColorDispModeAlways=常に表示
tstr_SettingsEnvironmentDimInterface=インターフェイスを暗くする
tstr_SettingsEnvironmentDimInterfaceTip=Desktop+ のダッシュボードタブを開いている間、SteamVR のダッシュボードと Desktop+ のインターフェイスを暗くします。
tstr_SettingsProfilesOverlays=オーバーレイのプロファイル
tstr_SettingsProfilesApps=アプリケーションプロファイル
tstr_SettingsProfilesManage=管理
tstr_SettingsProfilesOverlaysHeader=オーバーレイのプロファイルの管理
tstr_SettingsProfilesOverlaysNameDefault=初期設定
tstr_SettingsProfilesOverlaysNameNew=[新規プロファイル]
tstr_SettingsProfilesOverlaysNameNewBase=プロファイル %ID%
tstr_SettingsProfilesOverlaysProfileLoad=プロファイル読み込み
tstr_SettingsProfilesOverlaysProfileAdd=プロファイル追加
tstr_SettingsProfilesOverlaysProfileSave=現在のオーバーレイを保存
tstr_SettingsProfilesOverlaysProfileDelete=プロファイル削除
tstr_SettingsProfilesOverlaysProfileDeleteConfirm=本当にいい?
tstr_SettingsProfilesOverlaysProfileFailedLoad=プロファイルの読み込みに失敗しました
tstr_SettingsProfilesOverlaysProfileFailedDelete=プロファイルの削除に失敗しました
tstr_SettingsProfilesOverlaysProfileAddSelectHeader=プロファイル
tstr_SettingsProfilesOverlaysProfileAddSelectEmpty=このプロファイルにはオーバーレイが含まれていません。
tstr_SettingsProfilesOverlaysProfileAddSelectDo=選択したオーバーレイを追加する
tstr_SettingsProfilesOverlaysProfileAddSelectAll=すべて選択
tstr_SettingsProfilesOverlaysProfileAddSelectNone=選択解除
tstr_SettingsProfilesOverlaysProfileSaveSelectHeader=現在のオーバーレイを保存する
tstr_SettingsProfilesOverlaysProfileSaveSelectName=プロファイル名
tstr_SettingsProfilesOverlaysProfileSaveSelectNameErrorBlank=名前を空白にすることはできません
tstr_SettingsProfilesOverlaysProfileSaveSelectNameErrorTaken=その名前は既に使用されています
tstr_SettingsProfilesOverlaysProfileSaveSelectHeaderList=プロファイルに保存するオーバーレイを選択してください
tstr_SettingsProfilesOverlaysProfileSaveSelectDo=選択したオーバーレイを保存
tstr_SettingsProfilesOverlaysProfileSaveSelectDoFailed=プロファイルを保存できませんでした
tstr_SettingsProfilesAppsHeader=アプリケーションプロファイルの管理
tstr_SettingsProfilesAppsHeaderNoVRTip=Desktop+ が実行されていない場合は、既存のアプリケーションプロファイルのみがリストされます。
tstr_SettingsProfilesAppsListEmpty=利用可能なアプリケーションはありません
tstr_SettingsProfilesAppsProfileHeaderActive=(実行中)
tstr_SettingsProfilesAppsProfileEnabled=アプリケーションの実行中にアクティブ化する
tstr_SettingsProfilesAppsProfileOverlayProfile=オーバーレイプロファイル
tstr_SettingsProfilesAppsProfileActionEnter=アクションを開始
tstr_SettingsProfilesAppsProfileActionLeave=アクションを終了
tstr_SettingsActionsManage=アクション
tstr_SettingsActionsManageButton=管理
tstr_SettingsActionsButtonsOrderDefault=アクションボタン (初期設定)
tstr_SettingsActionsButtonsOrderOverlayBar=アクションボタン (オーバーレイのバー)
tstr_SettingsActionsShowBindings=コントローラーのボタン設定を表示
tstr_SettingsActionsActiveShortcuts=アクティブコントローラーボタン
tstr_SettingsActionsActiveShortcutsTip=オーバーレイを選択しているときのコントローラーバインディング。\nコントローラーのバインドを設定して VR ダッシュボードで、どのボタンになるかを変更します。
tstr_SettingsActionsActiveShortuctsHome="ホームに行く"
tstr_SettingsActionsActiveShortuctsBack="戻る"
tstr_SettingsActionsGlobalShortcuts=グローバル コントローラーボタン
tstr_SettingsActionsGlobalShortcutsTip=ダッシュボードが閉じられ、オーバーレイを選択していないときのコントローラーバインディング。\nコントローラーのバインドを設定して Desktop+ が、どのボタンになるかを変更します。
tstr_SettingsActionsGlobalShortcutsEntry=グローバル ショートカット #%ID%
tstr_SettingsActionsGlobalShortcutsAdd=ショートカット追加
tstr_SettingsActionsGlobalShortcutsRemove=最後の ショートカット 削除
tstr_SettingsActionsHotkeys=ホットキー
tstr_SettingsActionsHotkeysTip=システム全体のキーボード ショートカット。\nホットキーは、他のアプリケーションがその入力をブロックするため、同じ組み合わせがすでに別の場所に登録されている場合は機能しない場合があります。
tstr_SettingsActionsHotkeysAdd=ホットキー追加
tstr_SettingsActionsHotkeysRemove=削除
tstr_SettingsActionsTableHeaderAction=アクション
tstr_SettingsActionsTableHeaderShortcut=ショートカット
tstr_SettingsActionsTableHeaderHotkey=ホットキー
tstr_SettingsActionsManageHeader=アクションの管理
tstr_SettingsActionsManageCopyUID=UIDをコピー
tstr_SettingsActionsManageNew=新規アクション...
tstr_SettingsActionsManageEdit=アクションを編集
tstr_SettingsActionsManageDuplicate=アクションを複製
tstr_SettingsActionsManageDelete=アクションを削除
tstr_SettingsActionsManageDeleteConfirm=本当にいいですか?
tstr_SettingsActionsManageDuplicatedName=%NAME% (コピー)
tstr_SettingsActionsEditHeader=アクションを編集
tstr_SettingsActionsEditName=名前
tstr_SettingsActionsEditNameTranslatedTip=このアクションは現在、翻訳文字列IDを名前として使い、選択されたアプリケーション言語に自動的にマッチします。
tstr_SettingsActionsEditTarget=ターゲット
tstr_SettingsActionsEditTargetDefault=初期設定
tstr_SettingsActionsEditTargetDefaultTip=アクションが有効化されたオーバレイ、または前者が適用されない場合はフォーカスされたオーバレイを対象とする
tstr_SettingsActionsEditTargetUseTags=タグを使用する
tstr_SettingsActionsEditTargetActionTarget=アクション ターゲット
tstr_SettingsActionsEditHeaderAppearance=ボタンの外観
tstr_SettingsActionsEditIcon=アイコン
tstr_SettingsActionsEditLabel=ラベル
tstr_SettingsActionsEditLabelTranslatedTip=このアクションは現在、翻訳文字列IDをラベルとして使用し、選択されたアプリケーション言語に自動的にマッチします。
tstr_SettingsActionsEditHeaderCommands=コマンド
tstr_SettingsActionsEditNameNew=新規アクション
tstr_SettingsActionsEditCommandAdd=コマンド追加
tstr_SettingsActionsEditCommandDelete=コマンド削除
tstr_SettingsActionsEditCommandDeleteConfirm=本当にいいですか?
tstr_SettingsActionsEditCommandType=コマンド タイプ
tstr_SettingsActionsEditCommandTypeNone=なし
tstr_SettingsActionsEditCommandTypeKey=キーを押す
tstr_SettingsActionsEditCommandTypeMousePos=マウスの位置を設定
tstr_SettingsActionsEditCommandTypeString=文字を入力
tstr_SettingsActionsEditCommandTypeLaunchApp=アプリを起動
tstr_SettingsActionsEditCommandTypeShowKeyboard=キーボード 表示
tstr_SettingsActionsEditCommandTypeCropActiveWindow=アクティブなウィンドウをクロップ
tstr_SettingsActionsEditCommandTypeShowOverlay=オーバーレイ 表示
tstr_SettingsActionsEditCommandTypeSwitchTask=タスク切り替え
tstr_SettingsActionsEditCommandTypeLoadOverlayProfile=オーバーレイプロファイルを読み込む
tstr_SettingsActionsEditCommandTypeUnknown=不明
tstr_SettingsActionsEditCommandVisibilityToggle=切り替え
tstr_SettingsActionsEditCommandVisibilityShow=常に表示
tstr_SettingsActionsEditCommandVisibilityHide=常に非表示
tstr_SettingsActionsEditCommandUndo=元に戻す
tstr_SettingsActionsEditCommandKeyCode=コピーボタン
tstr_SettingsActionsEditCommandKeyToggle=キー 切り替え
tstr_SettingsActionsEditCommandMouseX=X
tstr_SettingsActionsEditCommandMouseY=Y
tstr_SettingsActionsEditCommandMouseUseCurrent=マウスカーソルの位置を使う
tstr_SettingsActionsEditCommandString=文字
tstr_SettingsActionsEditCommandPath=実行可能なパス
tstr_SettingsActionsEditCommandPathTip=これは通常のファイルやURLでも可能です。
tstr_SettingsActionsEditCommandArgs=アプリケーションの引数
tstr_SettingsActionsEditCommandArgsTip=これらの引数はアプリケーションに渡されます。\n分からない場合は空欄のままにしてください。
tstr_SettingsActionsEditCommandVisibility=表示する
tstr_SettingsActionsEditCommandSwitchingMethod=スイッチング方式
tstr_SettingsActionsEditCommandSwitchingMethodSwitcher=タスクスイッチャーを表示
tstr_SettingsActionsEditCommandSwitchingMethodFocus=フォーカスウィンドウ
tstr_SettingsActionsEditCommandWindow=ウィンドウ
tstr_SettingsActionsEditCommandWindowNone=[ウィンドウなし]
tstr_SettingsActionsEditCommandWindowStrictMatchingTip=切り替え先のウィンドウを探すときに、ウィンドウのタイトルの正確な一致のみを許可する
tstr_SettingsActionsEditCommandCursorWarp=カーソルをウィンドウ内にワープ
tstr_SettingsActionsEditCommandProfile=プロフィール
tstr_SettingsActionsEditCommandProfileClear=既存のオーバーレイを削除する
tstr_SettingsActionsEditCommandDescNone=No コマンド
tstr_SettingsActionsEditCommandDescKey=キーを押す "%KEYNAME%"
tstr_SettingsActionsEditCommandDescKeyToggle=キー 切り替え "%KEYNAME%"
tstr_SettingsActionsEditCommandDescMousePos=マウスの位置を %X%, %Y% に設定
tstr_SettingsActionsEditCommandDescString=文字を入力 "%STRING%"
tstr_SettingsActionsEditCommandDescLaunchApp=アプリを起動 "%APP%" %ARGSOPT%
tstr_SettingsActionsEditCommandDescLaunchAppArgsOpt="%ARGS%"
tstr_SettingsActionsEditCommandDescKeyboardToggle=切り替え キーボード
tstr_SettingsActionsEditCommandDescKeyboardShow=キーボード表示
tstr_SettingsActionsEditCommandDescKeyboardHide=キーボード非表示
tstr_SettingsActionsEditCommandDescCropWindow=アクティブなウィンドウをクロップ
tstr_SettingsActionsEditCommandDescOverlayToggle=オーバーレイ切り替え: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayShow=オーバーレイ表示: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayHide=オーバーレイ非表示: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayTargetDefault=[アクション ターゲット]
tstr_SettingsActionsEditCommandDescSwitchTask=タスク切り替え
tstr_SettingsActionsEditCommandDescSwitchTaskWindow=タスクを "%WINDOW%" に切り替える
tstr_SettingsActionsEditCommandDescLoadOverlayProfile=オーバーレイプロファイル "%PROFILE%" を読み込む
tstr_SettingsActionsEditCommandDescLoadOverlayProfileAdd=オーバーレイプロファイル "%PROFILE%" を追加する
tstr_SettingsActionsEditCommandDescUnknown=コマンドが不明です
tstr_SettingsActionsOrderHeader=アクション変更オーダー
tstr_SettingsActionsOrderButtonLabel=%COUNT% アクション選択中
tstr_SettingsActionsOrderButtonLabelSingular=%COUNT% 選択中
tstr_SettingsActionsOrderNoActions=アクションが選択されていません
tstr_SettingsActionsOrderAdd=アクション追加...
tstr_SettingsActionsOrderRemove=アクション削除
tstr_SettingsActionsAddSelectorHeader=追加するアクションを選んでください
tstr_SettingsActionsAddSelectorAdd=選択したアクションを追加
tstr_SettingsKeyboardLayout=キーボード レイアウト
tstr_SettingsKeyboardSize=大きさ
tstr_SettingsKeyboardBehavior=動作
tstr_SettingsKeyboardStickyMod=スティキー修飾キー
tstr_SettingsKeyboardKeyRepeat=キー置き換え
tstr_SettingsKeyboardAutoShow=自動で表示
tstr_SettingsKeyboardAutoShowDesktopOnly=デスクトップのみ自動で表示
tstr_SettingsKeyboardAutoShowDesktop=デスクトップ と ウィンドウオーバーレイ
tstr_SettingsKeyboardAutoShowDesktopTip=実験的。すべてのアプリケーションで動作するわけではありません。
tstr_SettingsKeyboardAutoShowBrowser=ブラウザオーバーレイ
tstr_SettingsKeyboardLayoutAuthor=%AUTHOR% によって作成されました
tstr_SettingsKeyboardKeyClusters=キー
tstr_SettingsKeyboardKeyClusterBase=Base
tstr_SettingsKeyboardKeyClusterFunction=Function
tstr_SettingsKeyboardKeyClusterNavigation=Navigation
tstr_SettingsKeyboardKeyClusterNumpad=Numpad
tstr_SettingsKeyboardKeyClusterExtra=Extra
tstr_SettingsKeyboardSwitchToEditor=キーボードレイアウトエディターに切り替える
tstr_SettingsMouseShowCursor=マウスカーソル表示
tstr_SettingsMouseShowCursorGCUnsupported=このシステムでは、グラフィックスキャプチャのオーバーレイのカーソルを無効にすることはサポートされていません
tstr_SettingsMouseShowCursorGCActiveWarning=アクティブ グラフィックス キャプチャ ミラーにより、デスクトップ複製オーバーレイでカーソルが非表示にならなくなる場合があります
tstr_SettingsMouseScrollSmooth=スムーズスクロール有効
tstr_SettingsMouseSimulatePen=ペン入力としてシミュレート
tstr_SettingsMouseSimulatePenUnsupported=このシステムではペン入力シミュレーションはサポートされていません
tstr_SettingsMouseAllowLaserPointerOverride=レーザーポインター オーバーライドを有効
tstr_SettingsMouseAllowLaserPointerOverrideTip=オーバーレイをクリックするとレーザーポインターが戻ります。
tstr_SettingsMouseDoubleClickAssist=ダブルクリック アシスタント
tstr_SettingsMouseDoubleClickAssistTip=ダブルクリックの入力を容易にするために、マウスカーソルを設定した時間だけ停止させる。
tstr_SettingsMouseDoubleClickAssistTipValueOff=オフ
tstr_SettingsMouseDoubleClickAssistTipValueAuto=自動
tstr_SettingsMouseSmoothing=スムーズな入力
tstr_SettingsMouseSmoothingLevelNone=なし
tstr_SettingsMouseSmoothingLevelVeryLow=最小
tstr_SettingsMouseSmoothingLevelLow=小
tstr_SettingsMouseSmoothingLevelMedium=中
tstr_SettingsMouseSmoothingLevelHigh=高
tstr_SettingsMouseSmoothingLevelVeryHigh=最高
tstr_SettingsLaserPointerTip=これらの設定は、SteamVR ダッシュボードが閉じているときに使用される Desktop+ のレーザー ポインターに適用されます。
tstr_SettingsLaserPointerBlockInput=アクティブ中にゲームの入力をブロックする
tstr_SettingsLaserPointerAutoToggleDistance=自動アクティベーション 最大距離
tstr_SettingsLaserPointerAutoToggleDistanceValueOff=オフ
tstr_SettingsLaserPointerHMDPointer=視線ベースのHMDのポインター
tstr_SettingsLaserPointerHMDPointerTableHeaderInputAction=入力アクション
tstr_SettingsLaserPointerHMDPointerTableHeaderBinding=キーボードのキー
tstr_SettingsLaserPointerHMDPointerTableBindingToggle=レーザーポインターを切り替える
tstr_SettingsLaserPointerHMDPointerTableBindingLeft=左クリック
tstr_SettingsLaserPointerHMDPointerTableBindingRight=右クリック
tstr_SettingsLaserPointerHMDPointerTableBindingMiddle=ホイールクリック
tstr_SettingsWindowOverlaysAutoFocus=オーバーレイをポイントしたときのフォーカスウィンドウ
tstr_SettingsWindowOverlaysKeepOnScreen=ウィンドウをスクリーンのままにする
tstr_SettingsWindowOverlaysKeepOnScreenTip=ソースウィンドウの境界が画面の描画領域の外にある場合、ソースウィンドウを自動的に移動します。
tstr_SettingsWindowOverlaysAutoSizeOverlay=ウィンドウのサイズ変更時にオーバーレイのサイズを調整する
tstr_SettingsWindowOverlaysFocusSceneApp=レーザーポインターがオーバーレイから離れると、ゲームに焦点を合わせる
tstr_SettingsWindowOverlaysFocusSceneAppDashboard=ゲームを終了したらダッシュボードを閉じる
tstr_SettingsWindowOverlaysOnWindowDrag=ウィンドウドラッグ時
tstr_SettingsWindowOverlaysOnWindowDragDoNothing=何もしない
tstr_SettingsWindowOverlaysOnWindowDragBlock=ドラッグをブロック
tstr_SettingsWindowOverlaysOnWindowDragOverlay=ドラッグオーバーレイ
tstr_SettingsWindowOverlaysOnCaptureLoss=キャプチャーのロスについて
tstr_SettingsWindowOverlaysOnCaptureLossTip=ウィンドウキャプチャが失われたときの動作。通常はターゲットウィンドウが閉じられることによって失われる。\nまた、"オーバーレイ非表示" は、キャプチャが見つかったときにオーバーレイを再び表示します。
tstr_SettingsWindowOverlaysOnCaptureLossDoNothing=何もしない
tstr_SettingsWindowOverlaysOnCaptureLossHide=オーバーレイ非表示
tstr_SettingsWindowOverlaysOnCaptureLossRemove=オーバーレイ削除
tstr_SettingsBrowserMaxFrameRate=最大フレームレート
tstr_SettingsBrowserMaxFrameRateOverrideOff=グローバル設定
tstr_SettingsBrowserContentBlocker=コンテンツブロッカー
tstr_SettingsBrowserContentBlockerTip=Adblock Plus構文のブロックリストを "DesktopPlusBrowser\content_block" ディレクトリに追加します。\nディレクトリ内のすべてのリストが読み込まれます。
tstr_SettingsBrowserContentBlockerListCount=(%LISTCOUNT% 実行中リスト)
tstr_SettingsBrowserContentBlockerListCountSingular=(%LISTCOUNT% 実行中リスト)
tstr_SettingsPerformanceUpdateLimiter=フレームリミッター
tstr_SettingsPerformanceUpdateLimiterMode=フレームリミッターモード
tstr_SettingsPerformanceUpdateLimiterModeOff=オフ
tstr_SettingsPerformanceUpdateLimiterModeMS=フレームタイム
tstr_SettingsPerformanceUpdateLimiterModeFPS=フレームレート
tstr_SettingsPerformanceUpdateLimiterModeOffOverride=グローバル設定を使う
tstr_SettingsPerformanceUpdateLimiterModeMSTip=強制的にオーバーレイの毎フレームの最小間隔を制御します
tstr_SettingsPerformanceUpdateLimiterFPSValue=%FPS% fps
tstr_SettingsPerformanceUpdateLimiterOverride=オーバーレイフレーム上限
tstr_SettingsPerformanceUpdateLimiterOverrideTip=複数のオーバーライドが有効な場合、更新レートが最も高いものが使用されます。
tstr_SettingsPerformanceUpdateLimiterModeOverride=オーバーレイ更新上限モード
tstr_SettingsPerformanceRapidUpdates=レーザー ポインターの遅延を短縮する
tstr_SettingsPerformanceRapidUpdatesTip=オーバーレイをポイントするときに CPU 負荷を上げて、レーザー ポインター入力の遅延を短縮します。
tstr_SettingsPerformanceSingleDesktopMirror=単一デスクトップのミラーリング
tstr_SettingsPerformanceSingleDesktopMirrorTip=デスクトップ切り替え時に、結合されたデスクトップから切り取るのではなく、別々にデスクトップをミラーリングします。\nこれが有効な場合、すべてのオーバーレイは同じデスクトップを表示します。
tstr_SettingsPerformanceShowFPS=フローティング UI で FPS を表示
tstr_SettingsWarningsHidden=警告非表示:
tstr_SettingsWarningsReset=警告非表示リセット
tstr_SettingsStartupAutoLaunch=自動で起動する
tstr_SettingsStartupSteamDisable=Steamの起動 無効
tstr_SettingsStartupSteamDisableTip=Steamで起動した場合、SteamなしでDesktop+を起動する。\nこれにより、アプリ内のステータス、使用時間の統計、その他のSteamの機能が無効になります。
tstr_SettingsTroubleshootingRestart=再起動
tstr_SettingsTroubleshootingRestartSteam=Steam再起動
tstr_SettingsTroubleshootingRestartDesktop=デスクトップモードで再起動
tstr_SettingsTroubleshootingElevatedModeEnter=管理者モード 有効
tstr_SettingsTroubleshootingElevatedModeLeave=管理者モード 無効
tstr_SettingsTroubleshootingSettingsReset=初期設定に戻す
tstr_SettingsTroubleshootingSettingsResetConfirmDescription=デフォルト設定を復元すると、選択した要素がデフォルト値にリセットされます。\nこれは、元に戻すことはできません。\n\nリセットする値:
tstr_SettingsTroubleshootingSettingsResetConfirmButton=選択している項目をリセット
tstr_SettingsTroubleshootingSettingsResetConfirmElementOverlays=現在のオーバーレイのセットアップ
tstr_SettingsTroubleshootingSettingsResetConfirmElementLegacyFiles=使用されていない古い構成とプロファイルファイルを削除する
tstr_SettingsTroubleshootingSettingsResetShowQuickStart=クイックスタートガイドを表示
;Keyboard Window
tstr_KeyboardWindowTitle=Desktop+ キーボード
tstr_KeyboardWindowTitleSettings=Desktop+ キーボード (設定)
tstr_KeyboardWindowTitleOverlay=Desktop+ キーボード (%OVERLAYNAME%)
tstr_KeyboardWindowTitleOverlayUnknown=[不明なオーバーレイ]
;Keyboard Shortcuts Window
tstr_KeyboardShortcutsCut=切り取り
tstr_KeyboardShortcutsCopy=コピー
tstr_KeyboardShortcutsPaste=貼り付け
;Overlay Properties Window
tstr_OvrlPropsCatPosition=位置
tstr_OvrlPropsCatAppearance=外観
tstr_OvrlPropsCatCapture=キャプチャー
tstr_OvrlPropsCatPerformanceMonitor=プリファレンス モニター
tstr_OvrlPropsCatBrowser=ブラウザ
tstr_OvrlPropsCatAdvanced=実績
tstr_OvrlPropsCatPerformance=パフォーマンス
tstr_OvrlPropsCatInterface=インターフェイス
tstr_OvrlPropsPositionOrigin=原点
tstr_OvrlPropsPositionOriginRoom=プレイエリア
tstr_OvrlPropsPositionOriginHMDXY=HMDの床の位置
tstr_OvrlPropsPositionOriginDashboard=ダッシュボード
tstr_OvrlPropsPositionOriginHMD=HMD
tstr_OvrlPropsPositionOriginSeatedSpace=座っている位置
tstr_OvrlPropsPositionOriginControllerR=右コントローラー
tstr_OvrlPropsPositionOriginControllerL=左コントローラー
tstr_OvrlPropsPositionOriginTheaterScreen=シアタースクリーン
tstr_OvrlPropsPositionOriginTracker1=トラッカー #1
tstr_OvrlPropsPositionOriginConfigHMDXYTurning=HMDの向きに追従
tstr_OvrlPropsPositionOriginConfigTheaterScreenEnter=シアターモード オン
tstr_OvrlPropsPositionOriginConfigTheaterScreenLeave=シアターモード オフ
tstr_OvrlPropsPositionOriginTheaterScreenTip=一部のプロパティは SteamVR シアタースクリーン によって制御されます
tstr_OvrlPropsPositionDispMode=ディスプレイモード
tstr_OvrlPropsPositionDispModeAlways=常に表示
tstr_OvrlPropsPositionDispModeDashboard=ダッシュボード中のみ
tstr_OvrlPropsPositionDispModeScene=ゲーム中のみ
tstr_OvrlPropsPositionDispModeDPlus=Desktop+ タブ内のみ
tstr_OvrlPropsPositionPos=位置
tstr_OvrlPropsPositionPosTip=位置は、Desktop+ の実行中にのみ変更またはリセットできます。
tstr_OvrlPropsPositionChange=変更
tstr_OvrlPropsPositionReset=リセット
tstr_OvrlPropsPositionLock=ロック
tstr_OvrlPropsPositionChangeHeader=オーバーレイの位置を変更
tstr_OvrlPropsPositionChangeHelp=任意のオーバーレイをドラッグして位置を変更します。\n両手で位置を変更するには、右クリックを押したままにします。
tstr_OvrlPropsPositionChangeHelpDesktop=ドラッグボタン("D")を押しながらマウスでオーバーレイを移動または回転させる。
tstr_OvrlPropsPositionChangeManualAdjustment=手動で調整
tstr_OvrlPropsPositionChangeMove=移動
tstr_OvrlPropsPositionChangeRotate=回転
tstr_OvrlPropsPositionChangeForward=手前に移動
tstr_OvrlPropsPositionChangeBackward=奥に移動
tstr_OvrlPropsPositionChangeRollCW=ロール ⟳
tstr_OvrlPropsPositionChangeRollCCW=ロール ⟲
tstr_OvrlPropsPositionChangeLookAt=HMDを向く
tstr_OvrlPropsPositionChangeDragButton=D
tstr_OvrlPropsPositionChangeOffset=追加のオフセット
tstr_OvrlPropsPositionChangeOffsetUpDown=上/下 オフセット
tstr_OvrlPropsPositionChangeOffsetRightLeft=右/左 オフセット
tstr_OvrlPropsPositionChangeOffsetForwardBackward=前/後 オフセット
tstr_OvrlPropsPositionChangeDragSettings=ドラッグ設定
tstr_OvrlPropsPositionChangeDragSettingsAutoDocking=近くのコントローラーにドッキング
tstr_OvrlPropsPositionChangeDragSettingsForceDistance=フォースの距離 固定
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShape=シャープ
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShapeSphere=球体
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShapeCylinder=シリンダー
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceAutoCurve=自動-湾曲
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceAutoTilt=自動-角度
tstr_OvrlPropsPositionChangeDragSettingsSnapPosition=スナップ位置
tstr_OvrlPropsAppearanceWidth=横
tstr_OvrlPropsAppearanceCurve=曲率
tstr_OvrlPropsAppearanceOpacity=不透明度
tstr_OvrlPropsAppearanceBrightness=明るさ
tstr_OvrlPropsAppearanceCrop=クロップ
tstr_OvrlPropsAppearanceCropValueMax=最大
tstr_OvrlPropsCrop=エリアをクロップ中
tstr_OvrlPropsCropHelp=長方形をドラッグしてクロップを変更します。スクロールするか、端をドラッグして、長方形のサイズを調整します。
tstr_OvrlPropsCropManualAdjust=手動で調整
tstr_OvrlPropsCropInvalidTip=現在の四角形のクロップは無効です。オーバーレイが見えなくなる場合があります。
tstr_OvrlPropsCropX=X
tstr_OvrlPropsCropY=Y
tstr_OvrlPropsCropWidth=横
tstr_OvrlPropsCropHeight=縦
tstr_OvrlPropsCropToWindow=選択しているウィンドウをクロップ
tstr_OvrlPropsCaptureMethod=キャプチャー方法
tstr_OvrlPropsCaptureMethodDup=デスクトップ
tstr_OvrlPropsCaptureMethodGC=グラフィック
tstr_OvrlPropsCaptureMethodGCUnsupportedTip=このシステムではグラフィックキャプチャーはサポートされていません
tstr_OvrlPropsCaptureMethodGCUnsupportedPartialTip=一部のグラフィックキャプチャー機能はこのシステムではサポートされていません
tstr_OvrlPropsCaptureSource=ソース
tstr_OvrlPropsCaptureGCSource=グラフィックキャプチャーソース
tstr_OvrlPropsCaptureSourceUnknownWarning=このオーバーレイは不明なキャプチャ ソースを使用しているため、正しく動作しない可能性があります
tstr_OvrlPropsCaptureGCStrictMatching=厳密なウィンドウ マッチングを使用する
tstr_OvrlPropsCaptureGCStrictMatchingTip=このオーバーレイのキャプチャを復元するとき、ウィンドウのタイトル名が完全に一致したウィンドウのみキャプチャーします
tstr_OvrlPropsPerfMonDesktopModeTip=パフォーマンス モニターのオーバーレイがデスクトップ モードでは更新されません
tstr_OvrlPropsPerfMonGlobalTip=これらの設定は、すべてのパフォーマンス モニター オーバーレイに適用されます
tstr_OvrlPropsPerfMonStyle=スタイル
tstr_OvrlPropsPerfMonStyleCompact=コンパクト
tstr_OvrlPropsPerfMonStyleLarge=ラージ
tstr_OvrlPropsPerfMonShowCPU=CPUの状態を表示
tstr_OvrlPropsPerfMonShowGPU=GPUの状態を表示
tstr_OvrlPropsPerfMonShowGraphs=グラフィックを表示
tstr_OvrlPropsPerfMonShowFrameStats=FPSを表示
tstr_OvrlPropsPerfMonShowTime=時計を表示
tstr_OvrlPropsPerfMonShowBattery=バッテリーの状態を表示
tstr_OvrlPropsPerfMonShowTrackerBattery=トラッカーのバッテリーを表示
tstr_OvrlPropsPerfMonShowViveWirelessTemp=Vive ワイヤレス温度を表示
tstr_OvrlPropsPerfMonResetValues=累積値 復元
tstr_OvrlPropsBrowserNotAvailableTip=Desktop+ ブラウザコンポーネントがインストールされていません
tstr_OvrlPropsBrowserCloned=クローン化された出力
tstr_OvrlPropsBrowserClonedTip=このオーバーレイは "%OVERLAYNAME%" のクローンです。\nブラウザのプロパティの変更内容は、元のブラウザとそこから複製されたすべてのオーバーレイに適用されます。
tstr_OvrlPropsBrowserClonedConvert=スタンドアローンに変換中
tstr_OvrlPropsBrowserURL=URL
tstr_OvrlPropsBrowserURLHint=URLを入力してください...
tstr_OvrlPropsBrowserGo=決定
tstr_OvrlPropsBrowserRestore=戻る
tstr_OvrlPropsBrowserWidth=横
tstr_OvrlPropsBrowserHeight=縦
tstr_OvrlPropsBrowserZoom=ズーム
tstr_OvrlPropsBrowserAllowTransparency=透過を有効にする
tstr_OvrlPropsBrowserAllowTransparencyTip=Web ページで透明度の変更を許可します。\n背景色が設定されていないサイトでは正しく表示されない可能性があります。
tstr_OvrlPropsBrowserRecreateContext=ブラウザの環境を再生成する
tstr_OvrlPropsBrowserRecreateContextTip=変更を適用するには、ブラウザの環境を再生成する必要があります。\nこれを実行すると、ページがリロードされ、履歴がリセットされます。
tstr_OvrlPropsAdvanced3D=3D
tstr_OvrlPropsAdvancedHSBS=半分に並べる
tstr_OvrlPropsAdvancedSBS=並べる
tstr_OvrlPropsAdvancedHOU=半分以下
tstr_OvrlPropsAdvancedOU=以下
tstr_OvrlPropsAdvanced3DSwap=左右を交換
tstr_OvrlPropsAdvancedGazeFade=注視フェード
tstr_OvrlPropsAdvancedGazeFadeAuto=自動構成
tstr_OvrlPropsAdvancedGazeFadeDistance=距離
tstr_OvrlPropsAdvancedGazeFadeDistanceValueInf=無制限
tstr_OvrlPropsAdvancedGazeFadeSensitivity=感度
tstr_OvrlPropsAdvancedGazeFadeOpacity=ターゲットの不透明度
tstr_OvrlPropsAdvancedInput=レーザーポインターの入力
tstr_OvrlPropsAdvancedInputInGame=ゲーム中 有効
tstr_OvrlPropsAdvancedInputFloatingUI=フローティング UI を表示中
tstr_OvrlPropsAdvancedOverlayTags=オーバーレイタグ
tstr_OvrlPropsAdvancedOverlayTagsTip=オーバーレイタグは、アクション内のオーバーレイをターゲットにするために使用されます
tstr_OvrlPropsPerformanceInvisibleUpdate=常に更新
tstr_OvrlPropsPerformanceInvisibleUpdateTip=不透明度設定や視線フェードで非表示になっている場合でも、オーバーレイを更新します。\nサードパーティ製のアプリがオーバーレイの内容を取得するのに役立ちます。\nそれ以外の場合は推奨しません。\nオーバーレイが手動または表示モード設定で非表示の場合でも、更新は一時停止しています。
tstr_OvrlPropsInterfaceOverlayName=オーバーレイの名前
tstr_OvrlPropsInterfaceOverlayNameAuto=[自動]
tstr_OvrlPropsInterfaceActionOrderCustom=アクションボタンを上書きする
tstr_OvrlPropsInterfaceDesktopButtons=デスクトップボタン表示
tstr_OvrlPropsInterfaceExtraButtons=追加ボタンを表示
;Overlay Bar
tstr_OverlayBarOvrlHide=非表示
tstr_OverlayBarOvrlShow=表示
tstr_OverlayBarOvrlClone=複製
tstr_OverlayBarOvrlRemove=削除
tstr_OverlayBarOvrlRemoveConfirm=本当にいい?
tstr_OverlayBarOvrlProperties=プロパティ...
tstr_OverlayBarOvrlAddWindow=ウィンドウ...
tstr_OverlayBarTooltipOvrlAdd=オーバーレイ追加
tstr_OverlayBarTooltipSettings=設定
tstr_OverlayBarTooltipResetHold=ホールドしたウィンドウの位置をリセット...
;Floating UI
tstr_FloatingUIHideOverlayTip=オーバーレイ 非表示
tstr_FloatingUIHideOverlayHoldTip=長押しでオーバーレイを削除...
tstr_FloatingUIDragModeEnableTip=ドラッグモード 有効
tstr_FloatingUIDragModeDisableTip=ドラッグモード 無効
tstr_FloatingUIDragModeHoldLockTip=長押しで位置を固定する...
tstr_FloatingUIDragModeHoldUnlockTip=長押しで位置の固定を解除する...
tstr_FloatingUIWindowAddTip=アクティブウィンドウをオーバーレイに追加
tstr_FloatingUIActionBarShowTip=アクションバー 表示
tstr_FloatingUIActionBarHideTip=アクションバー 非表示
tstr_FloatingUIBrowserGoBackTip=Previous ページに行く
tstr_FloatingUIBrowserGoForwardTip=次のページに行く
tstr_FloatingUIBrowserRefreshTip=現在のページをリフレッシュ
tstr_FloatingUIBrowserStopTip=ページの読み込みを停止
tstr_FloatingUIActionBarDesktopPrev=前のデスクトップ
tstr_FloatingUIActionBarDesktopNext=次のデスクトップ
tstr_FloatingUIActionBarEmpty=有効なアクションはありません
;Special Actions
tstr_ActionNone=[なし]
tstr_ActionKeyboardShow=キーボード 表示
tstr_ActionKeyboardHide=キーボード 非表示
;Default Actions (translation IDs are matched to Action name string)
tstr_DefActionShowKeyboard=キーボード 表示
tstr_DefActionActiveWindowCrop=選択しているウィンドウをトリミング
tstr_DefActionActiveWindowCropLabel=トリミング\n選択している\nウィンドウ
tstr_DefActionSwitchTask=タスク切り替え
tstr_DefActionToggleOverlays=オーバーレイ 切り替え
tstr_DefActionToggleOverlaysLabel=オーバーレイ\n切り替え
tstr_DefActionMiddleMouse=マウスボタン 中心
tstr_DefActionMiddleMouseLabel=中心\nマウス\nボタン
tstr_DefActionBackMouse=マウスボタン 戻る
tstr_DefActionBackMouseLabel=戻る\nマウス\nボタン
tstr_DefActionReadMe=説明書を開く
tstr_DefActionReadMeLabel=説明書\n開く
tstr_DefActionDashboardToggle=SteamVR ダッシュボードの切り替え (デバッグ コマンド)
tstr_DefActionDashboardToggleLabel=ダッシュボード\n切り替え
;Performance Monitor (text space is very limited here, so keep it short or untranslated)
tstr_PerformanceMonitorCPU=CPU
tstr_PerformanceMonitorGPU=GPU
tstr_PerformanceMonitorRAM=RAM:
tstr_PerformanceMonitorVRAM=VRAM:
tstr_PerformanceMonitorFrameTime=フレームタイム:
tstr_PerformanceMonitorLoad=負荷:
tstr_PerformanceMonitorFPS=FPS:
tstr_PerformanceMonitorFPSAverage=平均 FPS:
tstr_PerformanceMonitorReprojectionRatio=再投影率:
tstr_PerformanceMonitorDroppedFrames=ドロップされたフレーム:
tstr_PerformanceMonitorBatteryLeft=左コントローラー:
tstr_PerformanceMonitorBatteryRight=右コントローラー:
tstr_PerformanceMonitorBatteryHMD=ヘッドセット:
tstr_PerformanceMonitorBatteryTracker=トラッカー %ID%:
tstr_PerformanceMonitorBatteryDisconnected=N/A
tstr_PerformanceMonitorViveWirelessTempNotAvailable=N/A
tstr_PerformanceMonitorCompactCPU=CPU
tstr_PerformanceMonitorCompactGPU=GPU
tstr_PerformanceMonitorCompactFPS=FPS
tstr_PerformanceMonitorCompactFPSAverage=AVG
tstr_PerformanceMonitorCompactReprojectionRatio=% RPR
tstr_PerformanceMonitorCompactDroppedFrames=DRP
tstr_PerformanceMonitorCompactBattery=BAT
tstr_PerformanceMonitorCompactBatteryLeft=L
tstr_PerformanceMonitorCompactBatteryRight=R
tstr_PerformanceMonitorCompactBatteryHMD=H
tstr_PerformanceMonitorCompactBatteryTracker=T%ID%
tstr_PerformanceMonitorCompactBatteryDisconnected=N/A
tstr_PerformanceMonitorCompactViveWirelessTempNotAvailable=N/A
tstr_PerformanceMonitorEmpty=パフォーマンスモニターの項目が空です。
;Aux UI
tstr_AuxUIDragHintDocking=追従させる
tstr_AuxUIDragHintUndocking=追従を解除
tstr_AuxUIDragHintOvrlLocked=オーバーレイの位置のロックを解除して、このオーバーレイをドラッグします
tstr_AuxUIDragHintOvrlTheaterScreenBlocked=オーバーレイの位置は SteamVR シアタースクリーンによって制御されます
tstr_AuxUIGazeFadeAutoHint=オーバーレイの中心を見て、%SECONDS% 秒間待ちます...
tstr_AuxUIGazeFadeAutoHintSingular=オーバーレイの中心を見て、%SECONDS% 秒間待ちます...
tstr_AuxUIQuickStartWelcomeHeader=Desktop+ へようこそ!
tstr_AuxUIQuickStartWelcomeBody=この短いガイドでは、アプリケーションの基本について説明します。\n詳細については、説明書及びユーザーガイドを参照してください。\n\nこれをスキップしたい場合は、[閉じる]を押してください。
tstr_AuxUIQuickStartOverlaysHeader=オーバーレイ
tstr_AuxUIQuickStartOverlaysBody=Desktop+ を使用すると、デスクトップやウィンドウなどをミラーリングするオーバーレイを作成できます。\n作成したすべてのオーバーレイは、下のオーバーレイバーにリストされます。\n\nオーバーレイのプロパティは、オーバーレイ アイコンの 1つをクリックして [プロパティ...] を選択することで変更できます。
tstr_AuxUIQuickStartOverlaysBody2=[+] を押して、リストからキャプチャ ソース/オーバーレイタイプを選択すると、新しいオーバーレイを作成できます。\nWeb ブラウザ オーバーレイなどの一部のタイプは、それぞれのアプリケーション コンポーネントがインストールされている場合にのみ使用できます。\n\n個々のオーバーレイまたは完全なレイアウトをプロファイルに保存できます。\n現在のオーバーレイ設定は、セッション間で自動的に記憶されます。
tstr_AuxUIQuickStartOverlayPropertiesHeader=オーバーレイプロパティ
tstr_AuxUIQuickStartOverlayPropertiesBody=オーバーレイにはさまざまなカスタマイズオプションがあります。\n原点と表示モードは、オーバーレイが表示される場所とタイミングを設定します。\nオーバーレイが表示されるはずなのに見つからない場合は、これらのプロパティを確認してください。\nそれでも見つからない場合は、その位置をリセットすると役立つこともあります。\n\nまた、オーバーレイをモーションコントローラーの近くにドラッグして、モーションコントローラーにドッキングすることもできます。
tstr_AuxUIQuickStartOverlayPropertiesBody2=一部のプロパティはデフォルトで非表示になっています。\nすべてのオプションを表示するには、設定ウィンドウで[詳細設定を表示]を切り替えます。\n\nUIウィンドウの位置は、それぞれのボタンを長押しすることでリセットできます。\nダブルクリックや右クリックは、クイックショートカットトグルと同様にオーバーレイボタンでも機能します。
tstr_AuxUIQuickStartSettingsHeader=設定
tstr_AuxUIQuickStartSettingsBody=設定ウィンドウには、Desktop+ のすべてのグローバル設定が含まれています。\nオーバーレイバーの右端にある歯車のボタンから開けます。\n\nオーバーレイの場合と同様に、設定の変更は自動的に適用され、保存されます。
tstr_AuxUIQuickStartProfilesHeader=プロファイル
tstr_AuxUIQuickStartProfilesBody=Desktop+ には、2種類のプロファイルがあります。\n\nオーバーレイプロファイル:\n1つまたは複数のオーバーレイをそのプロパティとともに保存します。\nこれらは手動で読み込んだり、アクションやアプリケーション、プロファイルのイベントとして読み込むこともできます。\n\nアプリケーションのプロファイル:\nSteamVR アプリケーションの起動時に自動的に読み込まれるオーバーレイプロファイルやアクションを割り当てます。
tstr_AuxUIQuickStartActionsHeader=アクション
tstr_AuxUIQuickStartActionsBody=Desktop+ のアクションは、コントローラー入力やアプリケーションプロファイルに割り当てたり、オーバーレイのアクションバーに追加したりできる一連のコマンドです。\nアクションコマンドには、デスクトップ上の入力をシミュレートするものから、オーバーレイレイアウトを変更するものまであります。\n使い方次第では、外部アプリケーションを起動することもできます。\n\nDesktop+ には、いくつかのアクション例が用意されています。
tstr_AuxUIQuickStartActionsBody2=初期設定では、アクションは有効化されたオーバレイ(オーバレイに表示されたアクションボタン、オーバレイ上のコントローラ入力)、またはフォーカスされたオーバレイをターゲットにします。\n\nフォーカスされたオーバーレイは、最後にクリックされたオーバーレイです。\n多くの場合、デスクトップ上でシミュレートされた入力のように、対象となるオーバーレイは重要ではありません。他のケースでは、異なる対象のオーバーレイを指定したり、複数のオーバーレイを指定することが有用な場合もあります。
tstr_AuxUIQuickStartOverlayTagsHeader=オーバーレイタグ
tstr_AuxUIQuickStartOverlayTagsBody=オーバーレイタグは以下の目的で使用されます。\nオーバーレイタグには自動タグ(緑色のラベル)とユーザー定義タグがあります。\n自動タグは、それが表すプロパティに基づいて自動的に割り当てられます。\nユーザー定義タグは、オーバーレイのプロパティウィンドウでオーバーレイに手動で割り当てることができます。\n\nアクションは、グローバルショートカットのコントローラーボタンに間接的にのみバインドされることに注意してください。番号付けされたグローバルショートカットは、SteamVRの入力バインディングインターフェイスを介してコントローラの入力にも割り当てる必要があります。
tstr_AuxUIQuickStartSettingsEndBody=他にも多くの設定ができます。\nチェックボックスなど操作して試してみてください。\n\n行き詰まった場合は、ウィンドウの下部にある [初期設定に戻す] を押してください。\n特定の要素のみを復元するオプションもあります。
tstr_AuxUIQuickStartFloatingUIHeader=フローティングUI
tstr_AuxUIQuickStartFloatingUIBody=[フローティングUI]と呼ばれるインターフェースは、オーバーレイをポイントするたびに表示されます。\nダッシュボードでは、他のオーバーレイがポイントされていない場合に常に表示されます。\n\nフローティングUIには、ドラッグモードを切り替えてオーバーレイを移動できるようにするなど、オーバーレイを変更するための基本的なコントロールと、アクションボタンのカスタマイズ可能なセクションが含まれています。\n\nオーバーレイの種類によっては、他のコントロールも存在する場合があります。\nそれらの表示は、それぞれのオーバーレイプロパティでオーバーレイごとに切り替えることができます。
tstr_AuxUIQuickStartDesktopModeHeader=デスクトップモード
tstr_AuxUIQuickStartDesktopModeBody=Desktop+ は、デスクトップに表示されるウィンドウで設定することもできます。\nこれは、頻繁なキーボード入力が必要なタスクや、モーションコントローラーを接続せずにせってを変更したいときに便利です。\n\nほぼすべての機能は両方のモードで同じですが、Desktop+ VRキーボードのレイアウトをカスタマイズするために使用されるキーボードレイアウトエディターは、デスクトップモードでのみアクセスできます。\n\n[設定] ウィンドウの [トラブルシューティング] セクション、またはシステムトレイ/通知領域の Desktop+ アイコンから切り替えることができます。
tstr_AuxUIQuickStartEndHeader=説明書 & ユーザーガイド
tstr_AuxUIQuickStartEndBody=これだけで、Desktop+ を使い始めるのに十分なはずです。\n\n何か問題が起こったり、行き詰まったりした場合は、必ず説明書も確認してください。アクションバーのボタンをクリックすると、説明書を開けます。\n各オプションの詳細な説明と一般的な使用シナリオのステップバイステップガイドは、ユーザーガイドに記載されています。\n\nこのガイドを再度表示するには、[初期設定に戻す]ページの右下にある [クイックスタートガイドを表示] を押します。
tstr_AuxUIQuickStartButtonNext=次のページ
tstr_AuxUIQuickStartButtonPrev=前のページ
tstr_AuxUIQuickStartButtonClose=閉じる
;Desktop Mode
tstr_DesktopModeCatTools=ツール
tstr_DesktopModeCatOverlays=オーバーレイ
tstr_DesktopModeToolSettings=設定
tstr_DesktopModeToolActions=アクション
tstr_DesktopModeOverlayListAdd=オーバーレイ追加
tstr_DesktopModePageAddWindowOverlayTitle=ウィンドウオーバーレイ追加
tstr_DesktopModePageAddWindowOverlayHeader=ウィンドウを選択
;Keyboard Editor
tstr_KeyboardEditorKeyListTitle=キーリスト
tstr_KeyboardEditorKeyListTabContextReplace=内容を置き換える...
tstr_KeyboardEditorKeyListTabContextClear=サブレイアウトをクリア
tstr_KeyboardEditorKeyListRow=行 %ID%
tstr_KeyboardEditorKeyListSpacing=[間隔]
tstr_KeyboardEditorKeyListKeyAdd=追加
tstr_KeyboardEditorKeyListKeyDuplicate=重複
tstr_KeyboardEditorKeyListKeyRemove=削除
tstr_KeyboardEditorKeyPropertiesTitle=キーのプロパティ
tstr_KeyboardEditorKeyPropertiesNoSelection=キーが選択されていません
tstr_KeyboardEditorKeyPropertiesType=タイプ
tstr_KeyboardEditorKeyPropertiesTypeBlank=空白スペース
tstr_KeyboardEditorKeyPropertiesTypeVirtualKey=仮想キー
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyToggle=仮想キー(トグル)
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyIsoEnter=仮想キー(ISO 入力)
tstr_KeyboardEditorKeyPropertiesTypeString=文字列
tstr_KeyboardEditorKeyPropertiesTypeSublayoutToggle=サブレイアウトを切り替える
tstr_KeyboardEditorKeyPropertiesTypeAction=アクション
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyIsoEnterTip=隣接する行にある 2つの[仮想キー(ISO 入力)]キーを使用して、ISO 入力 形状のキーを構築します。\n各サブレイアウトには結合キーが 1つだけあります。
tstr_KeyboardEditorKeyPropertiesTypeStringTip=基本以外の文字キーには文字列型を使用します。\nこれにより、Desktop+ は実際のキーボードレイアウト上の正しいキーの組み合わせを判断し、アプリケーションの互換性を高めることができます。
tstr_KeyboardEditorKeyPropertiesSize=サイズ
tstr_KeyboardEditorKeyPropertiesLabel=ラベル
tstr_KeyboardEditorKeyPropertiesKeyCode=キーコード
tstr_KeyboardEditorKeyPropertiesString=文字列
tstr_KeyboardEditorKeyPropertiesSublayout=サブレイアウト
tstr_KeyboardEditorKeyPropertiesAction=アクション
tstr_KeyboardEditorKeyPropertiesCluster=クラスター
tstr_KeyboardEditorKeyPropertiesClusterTip=クラスター割り当ては、選択したキーの読み込みを無効にするために使用されます。\n周辺のスペースは削除されません。正常に切り替えられるように、空白スペースタイプのキーをクラスターに慎重に割り当ててください。
tstr_KeyboardEditorKeyPropertiesBlockModifiers=修飾キーをブロック
tstr_KeyboardEditorKeyPropertiesBlockModifiersTip=キーを押している間、すべての修飾キーを解除します。
tstr_KeyboardEditorKeyPropertiesNoRepeat=繰り返さない
tstr_KeyboardEditorKeyPropertiesNoRepeatTip=キーリピートが有効になっている場合でも、押し続けている間はキーが繰り返し入力されないようにします。
tstr_KeyboardEditorMetadataTitle=レイアウトメタデータ
tstr_KeyboardEditorMetadataName=名前
tstr_KeyboardEditorMetadataAuthor=作成者
tstr_KeyboardEditorMetadataHasAltGr=AltGrキー あり
tstr_KeyboardEditorMetadataHasAltGrTip=右Altキーを押すと、AltGrサブレイアウトに切り替わります。
tstr_KeyboardEditorMetadataClusterPreview=プレビュー クラスター:
tstr_KeyboardEditorMetadataSave=保存中...
tstr_KeyboardEditorMetadataLoad=読み込み中...
tstr_KeyboardEditorMetadataSavePopupTitle=キーボードレイアウトを保存
tstr_KeyboardEditorMetadataSavePopupFilename=ファイル名
tstr_KeyboardEditorMetadataSavePopupFilenameBlankTip=ファイル名は空白にできません
tstr_KeyboardEditorMetadataSavePopupConfirm=レイアウトを保存
tstr_KeyboardEditorMetadataSavePopupConfirmError=レイアウトを保存できませんでした
tstr_KeyboardEditorMetadataLoadPopupTitle=キーボードレイアウトを読み込む
tstr_KeyboardEditorMetadataLoadPopupConfirm=レイアウトを読み込む
tstr_KeyboardEditorPreviewTitle=キーボードプレビュー
tstr_KeyboardEditorSublayoutBase=Base
tstr_KeyboardEditorSublayoutShift=Shift
tstr_KeyboardEditorSublayoutAltGr=AltGr
tstr_KeyboardEditorSublayoutAux=Aux
;General Dialog Strings
tstr_DialogOk=適用
tstr_DialogCancel=キャンセル
tstr_DialogDone=閉じる
tstr_DialogUndo=元に戻す
tstr_DialogRedo=やり直し
tstr_DialogColorPickerHeader=色を選択
tstr_DialogColorPickerCurrent=現在
tstr_DialogColorPickerOriginal=オリジナル
tstr_DialogProfilePickerHeader=プロフィールを選択
tstr_DialogProfilePickerNone=[なし]
tstr_DialogActionPickerHeader=アクションを選択
tstr_DialogActionPickerEmpty=利用可能なアクションはありません
tstr_DialogIconPickerHeader=アイコンを選択
tstr_DialogIconPickerHeaderTip=カスタムアイコンは "images\icons\" ディレクトリに PNG ファイルとして追加できます。
tstr_DialogIconPickerNone=[アイコンがありません]
tstr_DialogKeyCodePickerHeader=キーコードを選択
tstr_DialogKeyCodePickerHeaderHotkey=ホットキーを選択
tstr_DialogKeyCodePickerModifiers=修飾子
tstr_DialogKeyCodePickerKeyCode=キーコード
tstr_DialogKeyCodePickerKeyCodeHint=フィルターリスト
tstr_DialogKeyCodePickerKeyCodeNone=[なし]
tstr_DialogKeyCodePickerFromInput=入力から...
tstr_DialogKeyCodePickerFromInputPopup=任意のキーまたはマウスのボタンを押します...
tstr_DialogKeyCodePickerFromInputPopupNoMouse=どれかキーを押してください...
tstr_DialogWindowPickerHeader=ウィンドウを選択
tstr_DialogInputTagsHint=フィルターまたは新規タグ
;Source Strings
tstr_SourceDesktopAll=統合デスクトップ
tstr_SourceDesktopID=デスクトップ %ID%
tstr_SourceWinRTNone=[キャプチャー対象がありません]
tstr_SourceWinRTUnknown=[不明なウィンドウ]
tstr_SourceWinRTClosed=[閉じる]:
tstr_SourcePerformanceMonitor=パフォーマンスモニター
tstr_SourceBrowser=ブラウザ
tstr_SourceBrowserNoPage=[ページが読み込めません]
;Notification Icon
tstr_NotificationIconRestoreVR=VRインターフェイスを復元
tstr_NotificationIconOpenOnDesktop=デスクトップ設定を開く
tstr_NotificationIconQuit=閉じる
;Notifications
tstr_NotificationInitialStartupTitleVR=初期設定
tstr_NotificationInitialStartupTitleDesktop=Desktop+ セットアップ
tstr_NotificationInitialStartupMessage=Desktop+ が SteamVR に正常に追加され、SteamVR の実行時に自動的に起動するようになりました。
;Browser
tstr_BrowserErrorPageTitle=ページ読み込みエラー - Desktop+
tstr_BrowserErrorPageHeading=ページを読み込めませんでした
tstr_BrowserErrorPageMessage=%URL% の読み込み中にエラーが発生しました: %ERROR%
================================================
FILE: assets/lang/ko.ini
================================================
[TranslationInfo]
Name=한국어 (Korean)
Author=HisaCat
Locale=ko-KR
[Strings]
;Settings Window
tstr_SettingsWindowTitle=Desktop+ 설정
tstr_SettingsCatInterface=인터페이스
tstr_SettingsCatEnvironment=환경
tstr_SettingsCatProfiles=프로필
tstr_SettingsCatActions=액션
tstr_SettingsCatKeyboard=키보드
tstr_SettingsCatMouse=마우스
tstr_SettingsCatLaserPointer=레이저 포인터
tstr_SettingsCatWindowOverlays=창 오버레이
tstr_SettingsCatBrowser=브라우저
tstr_SettingsCatPerformance=성능
tstr_SettingsCatVersionInfo=버전 정보
tstr_SettingsCatWarnings=경고
tstr_SettingsCatStartup=시작 설정
tstr_SettingsCatTroubleshooting=문제 해결
tstr_SettingsWarningPrefix=경고:
tstr_SettingsWarningCompositorResolution=SteamVR의 내부 해상도가 100% 미만입니다! 이는 오버레이 렌더링 품질에 영향을 줍니다.
tstr_SettingsWarningCompositorQuality=SteamVR 오버레이 렌더링 품질이 높음으로 설정되어 있지 않습니다!
tstr_SettingsWarningProcessElevated=Desktop+가 관리자 권한으로 실행되고 있습니다!
tstr_SettingsWarningElevatedMode=관리자 모드가 활성화되었습니다!
tstr_SettingsWarningBrowserMissing=브라우저 오버레이가 사용 중이지만, Desktop+ 브라우저 구성 요소를 현재 사용할 수 없습니다.
tstr_SettingsWarningBrowserMismatch=설치된 Desktop+ 브라우저 구성 요소가 현재 버전의 Desktop+와 호환되지 않습니다!
tstr_SettingsWarningElevatedProcessFocus=관리자 권한 프로세스가 포커스를 가지고 있습니다!\n현재 Desktop+는 입력 시뮬레이션을 할 수 없습니다.
tstr_SettingsWarningUIAccessLost=Desktop+가 더 이상 UI 접근 권한(UIAccess)으로 실행되고 있지 않습니다!
tstr_SettingsWarningOverlayCreationErrorLimit=오버레이 생성에 실패했습니다! (오버레이 최대 개수 초과)
tstr_SettingsWarningOverlayCreationErrorOther=오버레이 생성에 실패했습니다! (%ERRORNAME%)
tstr_SettingsWarningGraphicsCaptureError=그래픽 캡처 스레드에서 예기치 못한 오류가 발생했습니다! (%ERRORCODE%)
tstr_SettingsWarningAppProfileActive=%APPNAME%의 애플리케이션 프로필이 현재 오버레이 레이아웃을 덮어썼습니다.\n활성화된 동안에는 오버레이 변경 사항이 자동 저장되지 않습니다.
tstr_SettingsWarningConfigMigrated=이전 버전의 Desktop+ 설정 및 프로필이 새로운 형식으로 이전되었습니다.\n원본 파일은 삭제되지 않아, 이전 버전에서도 사용할 수 있습니다.\n처음부터 다시 시작하려면 [기본 설정 복원]을 참고하세요.
tstr_SettingsWarningMenuDontShowAgain=다시 표시하지 않음
tstr_SettingsWarningMenuDismiss=닫기
tstr_SettingsInterfaceLanguage=언어
tstr_SettingsInterfaceLanguageCommunity=%AUTHOR%에 의한 커뮤니티 번역
tstr_SettingsInterfaceLanguageIncompleteWarning=번역이 불완전할 수 있습니다
tstr_SettingsInterfaceAdvancedSettings=고급 설정 표시
tstr_SettingsInterfaceAdvancedSettingsTip=자주 사용되지 않는 고급 설정을 표시합니다
tstr_SettingsInterfaceBlankSpaceDrag=빈 공간 클릭 시 창 드래그
tstr_SettingsInterfacePersistentUI=UI 상태 유지
tstr_SettingsInterfacePersistentUIManage=관리
tstr_SettingsInterfaceDesktopButtons=데스크톱 버튼 표시 방식
tstr_SettingsInterfaceDesktopButtonsNone=없음
tstr_SettingsInterfaceDesktopButtonsIndividual=개별 데스크톱
tstr_SettingsInterfaceDesktopButtonsCycle=순환 버튼
tstr_SettingsInterfaceDesktopButtonsAddCombined=통합 데스크톱 추가 버튼
tstr_SettingsInterfacePersistentUIHelp=Desktop+는 일반 모드(전역)와 SteamVR 대시보드의 Desktop+ 탭 내에서 각각 별도로 창 상태를 기억합니다.
tstr_SettingsInterfacePersistentUIHelp2=아래 컨트롤을 사용해 해당 창의 상태를 직접 변경할 수 있습니다.\n일부 상태는 창을 보이게 하려면 위치를 초기화해야 할 수도 있습니다.
tstr_SettingsInterfacePersistentUIWindowsHeader=창 목록
tstr_SettingsInterfacePersistentUIWindowsSettings=설정
tstr_SettingsInterfacePersistentUIWindowsProperties=오버레이 속성
tstr_SettingsInterfacePersistentUIWindowsKeyboard=Desktop+ 키보드
tstr_SettingsInterfacePersistentUIWindowsStateGlobal=전역
tstr_SettingsInterfacePersistentUIWindowsStateDashboardTab=Desktop+ 탭
tstr_SettingsInterfacePersistentUIWindowsStateVisible=표시됨
tstr_SettingsInterfacePersistentUIWindowsStatePinned=고정됨
tstr_SettingsInterfacePersistentUIWindowsStatePosition=위치
tstr_SettingsInterfacePersistentUIWindowsStatePositionReset=위치 초기화
tstr_SettingsInterfacePersistentUIWindowsStateSize=크기
tstr_SettingsInterfacePersistentUIWindowsStateLaunchRestore=Desktop+ 실행 시 상태 복원
tstr_SettingsEnvironmentBackgroundColor=배경 색상
tstr_SettingsEnvironmentBackgroundColorDispModeNever=항상 숨김
tstr_SettingsEnvironmentBackgroundColorDispModeDPlusTab=Desktop+ 탭에서만 표시
tstr_SettingsEnvironmentBackgroundColorDispModeAlways=항상 표시
tstr_SettingsEnvironmentDimInterface=어두운 인터페이스
tstr_SettingsEnvironmentDimInterfaceTip=Desktop+ 대시보드 탭이 열려 있는 동안 SteamVR 대시보드와 Desktop+ UI를 어둡게 표시합니다.
tstr_SettingsProfilesOverlays=오버레이 프로필
tstr_SettingsProfilesApps=앱 프로필
tstr_SettingsProfilesManage=관리
tstr_SettingsProfilesOverlaysHeader=오버레이 프로필 관리
tstr_SettingsProfilesOverlaysNameDefault=기본값
tstr_SettingsProfilesOverlaysNameNew=[새 프로필]
tstr_SettingsProfilesOverlaysNameNewBase=프로필 %ID%
tstr_SettingsProfilesOverlaysProfileLoad=프로필 불러오기
tstr_SettingsProfilesOverlaysProfileAdd=프로필에서 오버레이 추가
tstr_SettingsProfilesOverlaysProfileSave=현재 오버레이 저장
tstr_SettingsProfilesOverlaysProfileDelete=프로필 삭제
tstr_SettingsProfilesOverlaysProfileDeleteConfirm=정말 삭제하시겠습니까?
tstr_SettingsProfilesOverlaysProfileFailedLoad=프로필 불러오기 실패
tstr_SettingsProfilesOverlaysProfileFailedDelete=프로필 삭제 실패
tstr_SettingsProfilesOverlaysProfileAddSelectHeader=프로필에서 추가할 오버레이 선택
tstr_SettingsProfilesOverlaysProfileAddSelectEmpty=이 프로필에는 오버레이가 없습니다.
tstr_SettingsProfilesOverlaysProfileAddSelectDo=선택한 오버레이 추가
tstr_SettingsProfilesOverlaysProfileAddSelectAll=모두 선택
tstr_SettingsProfilesOverlaysProfileAddSelectNone=선택 해제
tstr_SettingsProfilesOverlaysProfileSaveSelectHeader=현재 오버레이 저장
tstr_SettingsProfilesOverlaysProfileSaveSelectName=프로필 이름
tstr_SettingsProfilesOverlaysProfileSaveSelectNameErrorBlank=이름은 비워둘 수 없습니다
tstr_SettingsProfilesOverlaysProfileSaveSelectNameErrorTaken=이미 사용 중인 이름입니다
tstr_SettingsProfilesOverlaysProfileSaveSelectHeaderList=프로필에 저장할 오버레이 선택
tstr_SettingsProfilesOverlaysProfileSaveSelectDo=선택한 오버레이 저장
tstr_SettingsProfilesOverlaysProfileSaveSelectDoFailed=프로필 저장 실패
tstr_SettingsProfilesAppsHeader=앱 프로필 관리
tstr_SettingsProfilesAppsHeaderNoVRTip=Desktop+가 실행 중이 아닐 때는 기존 앱 프로필만 표시됩니다
tstr_SettingsProfilesAppsListEmpty=사용 가능한 앱이 없습니다
tstr_SettingsProfilesAppsProfileHeaderActive=(현재 활성화됨)
tstr_SettingsProfilesAppsProfileEnabled=앱 실행 시 활성화
tstr_SettingsProfilesAppsProfileOverlayProfile=오버레이 프로필
tstr_SettingsProfilesAppsProfileActionEnter=진입 시 실행할 액션
tstr_SettingsProfilesAppsProfileActionLeave=종료 시 실행할 액션
tstr_SettingsActionsManage=액션
tstr_SettingsActionsManageButton=관리
tstr_SettingsActionsButtonsOrderDefault=액션 버튼 (기본)
tstr_SettingsActionsButtonsOrderOverlayBar=액션 버튼 (오버레이 바)
tstr_SettingsActionsShowBindings=컨트롤러 바인딩 표시
tstr_SettingsActionsActiveShortcuts=활성 컨트롤러 버튼
tstr_SettingsActionsActiveShortcutsTip=오버레이를 가리킬 때의 컨트롤러 바인딩입니다.\nVR 대시보드 컨트롤러 바인딩을 변경하여 어떤 버튼을 사용할지 설정할 수 있습니다.
tstr_SettingsActionsActiveShortuctsHome="홈으로 이동"
tstr_SettingsActionsActiveShortuctsBack="뒤로 가기"
tstr_SettingsActionsGlobalShortcuts=전역 컨트롤러 버튼
tstr_SettingsActionsGlobalShortcutsTip=대시보드가 닫혀 있고 오버레이를 가리키지 않을 때의 컨트롤러 바인딩입니다.\nDesktop+ 컨트롤러 바인딩을 변경하여 어떤 버튼을 사용할지 설정할 수 있습니다.
tstr_SettingsActionsGlobalShortcutsEntry=전역 단축키 #%ID%
tstr_SettingsActionsGlobalShortcutsAdd=단축키 추가
tstr_SettingsActionsGlobalShortcutsRemove=마지막 단축키 제거
tstr_SettingsActionsHotkeys=핫키
tstr_SettingsActionsHotkeysTip=시스템 전역 키보드 단축키입니다.\n단축키를 사용하면 다른 프로그램이 해당 입력을 받지 못하며, 동일한 조합이 이미 다른 곳에 등록되어 있으면 작동하지 않을 수 있습니다.
tstr_SettingsActionsHotkeysAdd=핫키 추가
tstr_SettingsActionsHotkeysRemove=삭제
tstr_SettingsActionsTableHeaderAction=액션
tstr_SettingsActionsTableHeaderShortcut=단축키
tstr_SettingsActionsTableHeaderHotkey=핫키
tstr_SettingsActionsManageHeader=액션 관리
tstr_SettingsActionsManageCopyUID=UID 복사
tstr_SettingsActionsManageNew=새 액션...
tstr_SettingsActionsManageEdit=액션 편집
tstr_SettingsActionsManageDuplicate=액션 복제
tstr_SettingsActionsManageDelete=액션 삭제
tstr_SettingsActionsManageDeleteConfirm=정말 삭제하시겠습니까?
tstr_SettingsActionsManageDuplicatedName=%NAME% (복사본)
tstr_SettingsActionsEditHeader=액션 편집
tstr_SettingsActionsEditName=이름
tstr_SettingsActionsEditNameTranslatedTip=이 액션은 현재 번역 문자열 ID를 이름으로 사용하며, 선택된 앱 언어에 자동으로 맞춰집니다
tstr_SettingsActionsEditTarget=대상
tstr_SettingsActionsEditTargetDefault=기본값
tstr_SettingsActionsEditTargetDefaultTip=액션이 실행된 오버레이를 대상으로 하며, 해당 조건이 없으면 현재 포커스된 오버레이를 대상으로 합니다
tstr_SettingsActionsEditTargetUseTags=태그 사용
tstr_SettingsActionsEditTargetActionTarget=액션 대상
tstr_SettingsActionsEditHeaderAppearance=버튼 외형
tstr_SettingsActionsEditIcon=아이콘
tstr_SettingsActionsEditLabel=라벨
tstr_SettingsActionsEditLabelTranslatedTip=이 액션은 현재 번역 문자열 ID를 라벨로 사용하며, 선택된 앱 언어에 자동으로 맞춰집니다
tstr_SettingsActionsEditHeaderCommands=명령
tstr_SettingsActionsEditNameNew=새 액션
tstr_SettingsActionsEditCommandAdd=명령 추가
tstr_SettingsActionsEditCommandDelete=명령 삭제
tstr_SettingsActionsEditCommandDeleteConfirm=정말 삭제하시겠습니까?
tstr_SettingsActionsEditCommandType=명령 유형
tstr_SettingsActionsEditCommandTypeNone=없음
tstr_SettingsActionsEditCommandTypeKey=키 누르기
tstr_SettingsActionsEditCommandTypeMousePos=마우스 위치 설정
tstr_SettingsActionsEditCommandTypeString=문자열 입력
tstr_SettingsActionsEditCommandTypeLaunchApp=앱 실행
tstr_SettingsActionsEditCommandTypeShowKeyboard=키보드 표시
tstr_SettingsActionsEditCommandTypeCropActiveWindow=활성 창 표시 영역 지정
tstr_SettingsActionsEditCommandTypeShowOverlay=오버레이 표시
tstr_SettingsActionsEditCommandTypeSwitchTask=작업 전환
tstr_SettingsActionsEditCommandTypeLoadOverlayProfile=오버레이 프로필 불러오기
tstr_SettingsActionsEditCommandTypeUnknown=알 수 없음
tstr_SettingsActionsEditCommandVisibilityToggle=토글
tstr_SettingsActionsEditCommandVisibilityShow=항상 표시
tstr_SettingsActionsEditCommandVisibilityHide=항상 숨김
tstr_SettingsActionsEditCommandUndo=해제 시 실행 취소
tstr_SettingsActionsEditCommandKeyCode=키 코드
tstr_SettingsActionsEditCommandKeyToggle=키 토글
tstr_SettingsActionsEditCommandMouseX=X
tstr_SettingsActionsEditCommandMouseY=Y
tstr_SettingsActionsEditCommandMouseUseCurrent=현재 마우스 위치 사용
tstr_SettingsActionsEditCommandString=문자열
tstr_SettingsActionsEditCommandPath=실행 파일 경로
tstr_SettingsActionsEditCommandPathTip=일반 파일이나 URL도 가능합니다
tstr_SettingsActionsEditCommandArgs=앱 인자
tstr_SettingsActionsEditCommandArgsTip=앱에 전달할 인자입니다.\n모를 경우 비워두세요.
tstr_SettingsActionsEditCommandVisibility=표시 여부
tstr_SettingsActionsEditCommandSwitchingMethod=전환 방식
tstr_SettingsActionsEditCommandSwitchingMethodSwitcher=작업 전환기 표시
tstr_SettingsActionsEditCommandSwitchingMethodFocus=창 포커스
tstr_SettingsActionsEditCommandWindow=창
tstr_SettingsActionsEditCommandWindowNone=[창 없음]
tstr_SettingsActionsEditCommandWindowStrictMatchingTip=전환할 창을 찾을 때 제목이 완전히 일치하는 경우만 허용
tstr_SettingsActionsEditCommandCursorWarp=커서를 창 안으로 이동
tstr_SettingsActionsEditCommandProfile=프로필
tstr_SettingsActionsEditCommandProfileClear=기존 오버레이 제거
tstr_SettingsActionsEditCommandDescNone=명령 없음
tstr_SettingsActionsEditCommandDescKey=키 누르기 "%KEYNAME%"
tstr_SettingsActionsEditCommandDescKeyToggle=키 토글 "%KEYNAME%"
tstr_SettingsActionsEditCommandDescMousePos=마우스 위치를 %X%, %Y%로 설정
tstr_SettingsActionsEditCommandDescString=문자열 입력 "%STRING%"
tstr_SettingsActionsEditCommandDescLaunchApp=앱 실행 "%APP%" %ARGSOPT%
tstr_SettingsActionsEditCommandDescLaunchAppArgsOpt="%ARGS%"
tstr_SettingsActionsEditCommandDescKeyboardToggle=키보드 토글
tstr_SettingsActionsEditCommandDescKeyboardShow=키보드 표시
tstr_SettingsActionsEditCommandDescKeyboardHide=키보드 숨김
tstr_SettingsActionsEditCommandDescCropWindow=활성 창 표시 영역 지정
tstr_SettingsActionsEditCommandDescOverlayToggle=오버레이 토글: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayShow=오버레이 표시: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayHide=오버레이 숨김: %TAGS%
tstr_SettingsActionsEditCommandDescOverlayTargetDefault=[액션 대상]
tstr_SettingsActionsEditCommandDescSwitchTask=작업 전환
tstr_SettingsActionsEditCommandDescSwitchTaskWindow=작업을 "%WINDOW%"(으)로 전환
tstr_SettingsActionsEditCommandDescLoadOverlayProfile=오버레이 프로필 "%PROFILE%" 불러오기
tstr_SettingsActionsEditCommandDescLoadOverlayProfileAdd=오버레이 프로필 "%PROFILE%" 추가
tstr_SettingsActionsEditCommandDescUnknown=알 수 없는 명령
tstr_SettingsActionsOrderHeader=액션 순서 변경
tstr_SettingsActionsOrderButtonLabel=%COUNT%개의 액션 선택됨
tstr_SettingsActionsOrderButtonLabelSingular=%COUNT%개의 액션 선택됨
tstr_SettingsActionsOrderNoActions=선택된 액션이 없습니다
tstr_SettingsActionsOrderAdd=액션 추가...
tstr_SettingsActionsOrderRemove=액션 제거
tstr_SettingsActionsAddSelectorHeader=추가할 액션 선택
tstr_SettingsActionsAddSelectorAdd=선택한 액션 추가
tstr_SettingsKeyboardLayout=키보드 레이아웃
tstr_SettingsKeyboardSize=크기
tstr_SettingsKeyboardBehavior=동작
tstr_SettingsKeyboardStickyMod=조합키 고정
tstr_SettingsKeyboardKeyRepeat=키 반복
tstr_SettingsKeyboardAutoShow=자동 표시
tstr_SettingsKeyboardAutoShowDesktopOnly=데스크톱에서만 자동 표시
tstr_SettingsKeyboardAutoShowDesktop=데스크톱과 창 오버레이에서 자동 표시
tstr_SettingsKeyboardAutoShowDesktopTip=실험적인 기능. 모든 앱에서 동작하지 않을 수 있습니다.
tstr_SettingsKeyboardAutoShowBrowser=브라우저 오버레이에서 자동 표시
tstr_SettingsKeyboardLayoutAuthor=%AUTHOR% 제작
tstr_SettingsKeyboardKeyClusters=키 그룹
tstr_SettingsKeyboardKeyClusterBase=일반
tstr_SettingsKeyboardKeyClusterFunction=기능
tstr_SettingsKeyboardKeyClusterNavigation=네비게이션
tstr_SettingsKeyboardKeyClusterNumpad=숫자 키패드
tstr_SettingsKeyboardKeyClusterExtra=기타
tstr_SettingsKeyboardSwitchToEditor=키보드 레이아웃 편집기로 전환
tstr_SettingsMouseShowCursor=커서 표시
tstr_SettingsMouseShowCursorGCUnsupported=이 시스템에서는 그래픽 캡처 오버레이의 커서 비활성화가 지원되지 않습니다
tstr_SettingsMouseShowCursorGCActiveWarning=활성 그래픽 캡처 미러로 인해 데스크톱 복제 오버레이에서 커서가 숨겨지지 않을 수 있습니다
tstr_SettingsMouseScrollSmooth=부드러운 스크롤 사용
tstr_SettingsMouseSimulatePen=펜 입력으로 시뮬레이션
tstr_SettingsMouseSimulatePenUnsupported=이 시스템에서는 펜 입력 시뮬레이션이 지원되지 않습니다
tstr_SettingsMouseAllowLaserPointerOverride=레이저 포인터 재정의 허용
tstr_SettingsMouseAllowLaserPointerOverrideTip=물리 마우스를 빠르게 움직이면 레이저 포인터를 비활성화합니다.\n오버레이를 클릭하면 레이저 포인터가 다시 활성화됩니다.
tstr_SettingsMouseDoubleClickAssist=더블 클릭 보조
tstr_SettingsMouseDoubleClickAssistTip=더블 클릭을 쉽게 하기 위해 마우스 커서를 설정된 시간 동안 고정합니다
tstr_SettingsMouseDoubleClickAssistTipValueOff=끔
tstr_SettingsMouseDoubleClickAssistTipValueAuto=자동
tstr_SettingsMouseSmoothing=부드러운 이동
tstr_SettingsMouseSmoothingLevelNone=없음
tstr_SettingsMouseSmoothingLevelVeryLow=매우 낮음
tstr_SettingsMouseSmoothingLevelLow=낮음
tstr_SettingsMouseSmoothingLevelMedium=중간
tstr_SettingsMouseSmoothingLevelHigh=높음
tstr_SettingsMouseSmoothingLevelVeryHigh=매우 높음
tstr_SettingsLaserPointerTip=이 설정은 SteamVR 대시보드가 닫혀 있을 때 사용되는 Desktop+ 레이저 포인터에 적용됩니다
tstr_SettingsLaserPointerBlockInput=활성 상태에서 게임 입력 차단
tstr_SettingsLaserPointerAutoToggleDistance=자동 활성화 최대 거리
tstr_SettingsLaserPointerAutoToggleDistanceValueOff=끔
tstr_SettingsLaserPointerHMDPointer=시선 기반 HMD 포인터
tstr_SettingsLaserPointerHMDPointerTableHeaderInputAction=입력 동작
tstr_SettingsLaserPointerHMDPointerTableHeaderBinding=키보드 키
tstr_SettingsLaserPointerHMDPointerTableBindingToggle=레이저 포인터 토글
tstr_SettingsLaserPointerHMDPointerTableBindingLeft=마우스 왼쪽 버튼
tstr_SettingsLaserPointerHMDPointerTableBindingRight=마우스 오른쪽 버튼
tstr_SettingsLaserPointerHMDPointerTableBindingMiddle=마우스 휠 클릭
tstr_SettingsLaserPointerHMDPointerTableBindingDrag=오버레이 드래그
tstr_SettingsWindowOverlaysAutoFocus=오버레이를 가리킬 때 해당 창에 포커스
tstr_SettingsWindowOverlaysKeepOnScreen=창을 화면 안에 유지
tstr_SettingsWindowOverlaysKeepOnScreenTip=소스 창의 경계가 화면 작업 영역 밖에 있으면 자동으로 화면 안으로 이동시킵니다
tstr_SettingsWindowOverlaysAutoSizeOverlay=창 크기 변경 시 오버레이 크기 조정
tstr_SettingsWindowOverlaysFocusSceneApp=레이저 포인터가 오버레이를 벗어나면 게임에 포커스
tstr_SettingsWindowOverlaysFocusSceneAppDashboard=대시보드 닫은 후 게임에 포커스
tstr_SettingsWindowOverlaysOnWindowDrag=창 드래그 시 동작
tstr_SettingsWindowOverlaysOnWindowDragDoNothing=아무 것도 하지 않음
tstr_SettingsWindowOverlaysOnWindowDragBlock=드래그 차단
tstr_SettingsWindowOverlaysOnWindowDragOverlay=오버레이 드래그
tstr_SettingsWindowOverlaysOnCaptureLoss=캡처 손실 시 동작
tstr_SettingsWindowOverlaysOnCaptureLossTip=창 캡처가 사라졌을 때의 동작입니다. 보통 대상 창이 닫히면 발생합니다.\n"오버레이 숨김"을 선택하면 캡처가 복원될 때 오버레이가 다시 표시됩니다.
tstr_SettingsWindowOverlaysOnCaptureLossDoNothing=아무 것도 하지 않음
tstr_SettingsWindowOverlaysOnCaptureLossHide=오버레이 숨김
tstr_SettingsWindowOverlaysOnCaptureLossRemove=오버레이 제거
tstr_SettingsBrowserMaxFrameRate=최대 프레임
tstr_SettingsBrowserMaxFrameRateOverrideOff=전역 설정 사용
tstr_SettingsBrowserContentBlocker=콘텐츠 차단기
tstr_SettingsBrowserContentBlockerTip=Adblock Plus 구문을 사용한 차단 목록을 "DesktopPlusBrowser\content_block" 디렉토리에 추가하세요.\n디렉토리에 있는 모든 목록이 로드됩니다.
tstr_SettingsBrowserContentBlockerListCount=(%LISTCOUNT%개의 목록 활성화됨)
tstr_SettingsBrowserContentBlockerListCountSingular=(%LISTCOUNT%개의 목록 활성화됨)
tstr_SettingsPerformanceUpdateLimiter=업데이트 제한
tstr_SettingsPerformanceUpdateLimiterMode=업데이트 제한 모드
tstr_SettingsPerformanceUpdateLimiterModeOff=끔
tstr_SettingsPerformanceUpdateLimiterModeMS=프레임 시간
tstr_SettingsPerformanceUpdateLimiterModeFPS=프레임 레이트
tstr_SettingsPerformanceUpdateLimiterModeOffOverride=전역 설정 사용
tstr_SettingsPerformanceUpdateLimiterModeMSTip="프레임 시간"은 오버레이 업데이트 사이의 최소 간격을 강제합니다
tstr_SettingsPerformanceUpdateLimiterFPSValue=%FPS% fps
tstr_SettingsPerformanceUpdateLimiterOverride=업데이트 제한 재정의
tstr_SettingsPerformanceUpdateLimiterOverrideTip=여러 재정의가 활성화되어 있을 경우, 가장 높은 업데이트 속도를 제공하는 설정이 사용됩니다
tstr_SettingsPerformanceUpdateLimiterModeOverride=업데이트 제한 모드 재정의
tstr_SettingsPerformanceRapidUpdates=레이저 포인터 지연 시간 단축
tstr_SettingsPerformanceRapidUpdatesTip=오버레이를 가리킬 때 CPU 부하를 늘려 레이저 포인터 입력 지연을 줄입니다
tstr_SettingsPerformanceSingleDesktopMirror=단일 데스크톱 미러링
tstr_SettingsPerformanceSingleDesktopMirrorTip=결합된 데스크톱에서 잘라내는 대신, 전환 시 각 데스크톱을 개별적으로 미러링합니다.\n이 기능이 활성화되면 모든 오버레이가 동일한 데스크톱을 표시합니다.
tstr_SettingsPerformanceUseHDR=HDR 미러링
tstr_SettingsPerformanceUseHDRTip=데스크톱과 창을 더 높은 비트 깊이의 텍스처로 미러링하여 HDR 출력을 지원합니다. (실험적인 기능)\n필요하지 않은 경우 성능에 부정적인 영향을 줄 수 있으며 VRAM 사용량이 증가합니다.
tstr_SettingsPerformanceShowFPS=플로팅 UI에 FPS 표시
tstr_SettingsWarningsHidden=숨긴 경고:
tstr_SettingsWarningsReset=숨긴 경고 초기화
tstr_SettingsStartupAutoLaunch=SteamVR 시작 시 자동 실행
tstr_SettingsStartupSteamDisable=Steam 연동 비활성화
tstr_SettingsStartupSteamDisableTip=Steam을 통해 실행된 경우 Steam 없이 Desktop+를 재시작합니다.\n이 기능을 사용하면 앱 내 상태, 사용 시간 통계, 기타 Steam 기능이 비활성화됩니다.
tstr_SettingsTroubleshootingRestart=재시작
tstr_SettingsTroubleshootingRestartSteam=Steam으로 재시작
tstr_SettingsTroubleshootingRestartDesktop=데스크톱 모드로 재시작
tstr_SettingsTroubleshootingElevatedModeEnter=관리자 모드 진입
tstr_SettingsTroubleshootingElevatedModeLeave=관리자 모드 종료
tstr_SettingsTroubleshootingSettingsReset=기본 설정 복원
tstr_SettingsTroubleshootingSettingsResetConfirmDescription=기본 설정을 복원하면 선택된 항목이 기본값으로 초기화됩니다.\n이 작업은 되돌릴 수 없습니다.\n\n다음 항목이 초기화됩니다:
tstr_SettingsTroubleshootingSettingsResetConfirmButton=선택 항목 초기화
tstr_SettingsTroubleshootingSettingsResetConfirmElementOverlays=현재 오버레이 구성
tstr_SettingsTroubleshootingSettingsResetConfirmElementLegacyFiles=사용하지 않는 이전 설정 및 프로필 파일 삭제
tstr_SettingsTroubleshootingSettingsResetShowQuickStart=퀵스타트 가이드 표시
;Keyboard Window
;Keyboard Window
tstr_KeyboardWindowTitle=Desktop+ 키보드
tstr_KeyboardWindowTitleSettings=Desktop+ 키보드 (설정)
tstr_KeyboardWindowTitleOverlay=Desktop+ 키보드 (%OVERLAYNAME%)
tstr_KeyboardWindowTitleOverlayUnknown=[알 수 없는 오버레이]
;Keyboard Shortcuts Window
tstr_KeyboardShortcutsCut=잘라내기
tstr_KeyboardShortcutsCopy=복사
tstr_KeyboardShortcutsPaste=붙여넣기
;Overlay Properties Window
tstr_OvrlPropsCatPosition=위치
tstr_OvrlPropsCatAppearance=외형
tstr_OvrlPropsCatCapture=캡처
tstr_OvrlPropsCatPerformanceMonitor=성능 모니터
tstr_OvrlPropsCatBrowser=브라우저
tstr_OvrlPropsCatAdvanced=고급
tstr_OvrlPropsCatPerformance=성능
tstr_OvrlPropsCatInterface=인터페이스
tstr_OvrlPropsPositionOrigin=기준점
tstr_OvrlPropsPositionOriginRoom=플레이 구역
tstr_OvrlPropsPositionOriginHMDXY=HMD 바닥 위치
tstr_OvrlPropsPositionOriginDashboard=대시보드
tstr_OvrlPropsPositionOriginHMD=HMD
tstr_OvrlPropsPositionOriginSeatedSpace=앉은 위치
tstr_OvrlPropsPositionOriginControllerR=오른쪽 컨트롤러
tstr_OvrlPropsPositionOriginControllerL=왼쪽 컨트롤러
tstr_OvrlPropsPositionOriginTheaterScreen=시어터 스크린
tstr_OvrlPropsPositionOriginTracker1=트래커 #1
tstr_OvrlPropsPositionOriginConfigHMDXYTurning=HMD 회전에 맞춰 회전
tstr_OvrlPropsPositionOriginConfigTheaterScreenEnter=시어터 모드 진입
tstr_OvrlPropsPositionOriginConfigTheaterScreenLeave=시어터 모드 종료
tstr_OvrlPropsPositionOriginTheaterScreenTip=일부 속성은 SteamVR 시어터 스크린에 의해 제어됩니다
tstr_OvrlPropsPositionDispMode=표시 모드
tstr_OvrlPropsPositionDispModeAlways=항상 표시
tstr_OvrlPropsPositionDispModeDashboard=대시보드에서만
tstr_OvrlPropsPositionDispModeScene=게임 중에만
tstr_OvrlPropsPositionDispModeDPlus=Desktop+ 탭에서만
tstr_OvrlPropsPositionPos=위치
tstr_OvrlPropsPositionPosTip=위치는 Desktop+ 실행 중에만 변경하거나 초기화할 수 있습니다
tstr_OvrlPropsPositionChange=변경
tstr_OvrlPropsPositionReset=초기화
tstr_OvrlPropsPositionLock=잠금
tstr_OvrlPropsPositionChangeHeader=오버레이 위치 변경
tstr_OvrlPropsPositionChangeHelp=오버레이를 드래그하여 위치를 변경합니다.\n두 손 제스처 변환은 마우스 오른쪽 버튼을 길게 누릅니다.
tstr_OvrlPropsPositionChangeHelpDesktop="D" 드래그 버튼을 누른 상태에서 마우스로 오버레이를 이동 또는 회전합니다
tstr_OvrlPropsPositionChangeManualAdjustment=수동 조정
tstr_OvrlPropsPositionChangeMove=이동
tstr_OvrlPropsPositionChangeRotate=회전
tstr_OvrlPropsPositionChangeForward=앞으로 이동
tstr_OvrlPropsPositionChangeBackward=뒤로 이동
tstr_OvrlPropsPositionChangeRollCW=Yaw ⟳
tstr_OvrlPropsPositionChangeRollCCW=Yaw ⟲
tstr_OvrlPropsPositionChangeLookAt=HMD 방향으로
tstr_OvrlPropsPositionChangeDragButton=D
tstr_OvrlPropsPositionChangeOffset=추가 오프셋
tstr_OvrlPropsPositionChangeOffsetUpDown=상/하 오프셋
tstr_OvrlPropsPositionChangeOffsetRightLeft=좌/우 오프셋
tstr_OvrlPropsPositionChangeOffsetForwardBackward=앞/뒤 오프셋
tstr_OvrlPropsPositionChangeDragSettings=드래그 설정
tstr_OvrlPropsPositionChangeDragSettingsAutoDocking=가까운 컨트롤러에 도킹
tstr_OvrlPropsPositionChangeDragSettingsForceDistance=고정 거리 강제
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShape=형태
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShapeSphere=구
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShapeCylinder=실린더
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceAutoCurve=자동 곡률
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceAutoTilt=자동 기울기
tstr_OvrlPropsPositionChangeDragSettingsSnapPosition=위치 스냅
tstr_OvrlPropsPositionChangeDragSettingsSnapRotation=회전 스냅
tstr_OvrlPropsPositionChangeDragSettingsSnapRotationPitch=Pitch
tstr_OvrlPropsPositionChangeDragSettingsSnapRotationYaw=Yaw
tstr_OvrlPropsPositionChangeDragSettingsSnapRotationRoll=Roll
tstr_OvrlPropsAppearanceWidth=너비
tstr_OvrlPropsAppearanceCurve=곡률
tstr_OvrlPropsAppearanceOpacity=불투명도
tstr_OvrlPropsAppearanceBrightness=밝기
tstr_OvrlPropsAppearanceCrop=표시 영역 지정
tstr_OvrlPropsAppearanceCropValueMax=최대
tstr_OvrlPropsCrop=표시 영역
tstr_OvrlPropsCropHelp=사각형을 드래그하여 표시 영역을 변경합니다.\n스크롤 입력이나 모서리를 드래그해 크기를 조정할 수 있습니다.
tstr_OvrlPropsCropManualAdjust=수동 조정
tstr_OvrlPropsCropInvalidTip=현재 표시 영역 사각형이 유효하지 않습니다. 오버레이가 표시되지 않을 수 있습니다.
tstr_OvrlPropsCropX=X
tstr_OvrlPropsCropY=Y
tstr_OvrlPropsCropWidth=너비
tstr_OvrlPropsCropHeight=높이
tstr_OvrlPropsCropToWindow=활성 창 표시 영역
tstr_OvrlPropsCaptureMethod=캡처 방식
tstr_OvrlPropsCaptureMethodDup=데스크톱 복제
tstr_OvrlPropsCaptureMethodGC=그래픽 캡처
tstr_OvrlPropsCaptureMethodGCUnsupportedTip=이 시스템에서는 그래픽 캡처를 지원하지 않습니다
tstr_OvrlPropsCaptureMethodGCUnsupportedPartialTip=이 시스템에서는 일부 그래픽 캡처 기능이 지원되지 않습니다
tstr_OvrlPropsCaptureSource=소스
tstr_OvrlPropsCaptureGCSource=그래픽 캡처 소스
tstr_OvrlPropsCaptureSourceUnknownWarning=이 오버레이는 알 수 없는 캡처 소스를 사용하고 있어 올바르게 동작하지 않을 수 있습니다
tstr_OvrlPropsCaptureGCStrictMatching=엄격한 창 일치 사용
tstr_OvrlPropsCaptureGCStrictMatchingTip=이 오버레이의 캡처를 복원할 때 창 제목이 완전히 일치하는 경우에만 허용합니다
tstr_OvrlPropsPerfMonDesktopModeTip=데스크톱 모드에서는 성능 모니터 오버레이가 업데이트되지 않습니다
tstr_OvrlPropsPerfMonGlobalTip=이 설정은 모든 성능 모니터 오버레이에 적용됩니다
tstr_OvrlPropsPerfMonStyle=스타일
tstr_OvrlPropsPerfMonStyleCompact=컴팩트
tstr_OvrlPropsPerfMonStyleLarge=라지
tstr_OvrlPropsPerfMonShowCPU=CPU 상태 표시
tstr_OvrlPropsPerfMonShowGPU=GPU 상태 표시
tstr_OvrlPropsPerfMonShowGraphs=그래프 표시
tstr_OvrlPropsPerfMonShowFrameStats=프레임 통계 표시
tstr_OvrlPropsPerfMonShowTime=시계 표시
tstr_OvrlPropsPerfMonShowBattery=배터리 상태 표시
tstr_OvrlPropsPerfMonShowTrackerBattery=트래커 배터리 잔량 표시
tstr_OvrlPropsPerfMonShowViveWirelessTemp=Vive 무선 온도 표시
tstr_OvrlPropsPerfMonResetValues=누적값 초기화
tstr_OvrlPropsBrowserNotAvailableTip=Desktop+ 브라우저 구성 요소가 설치되지 않았습니다
tstr_OvrlPropsBrowserCloned=복제 출력
tstr_OvrlPropsBrowserClonedTip=이 오버레이는 "%OVERLAYNAME%"의 복제본입니다.\n브라우저 속성 변경은 원본과 이를 복제한 모든 오버레이에 적용됩니다.
tstr_OvrlPropsBrowserClonedConvert=독립형으로 변환
tstr_OvrlPropsBrowserURL=URL
tstr_OvrlPropsBrowserURLHint=주소를 입력하세요
tstr_OvrlPropsBrowserGo=이동
tstr_OvrlPropsBrowserRestore=마지막 입력 복원
tstr_OvrlPropsBrowserWidth=너비
tstr_OvrlPropsBrowserHeight=높이
tstr_OvrlPropsBrowserZoom=줌
tstr_OvrlPropsBrowserAllowTransparency=투명도 허용
tstr_OvrlPropsBrowserAllowTransparencyTip=웹 페이지의 투명도 사용을 허용합니다.\n배경색이 지정되지 않은 사이트는 올바르게 표시되지 않을 수 있습니다.
tstr_OvrlPropsBrowserRecreateContext=브라우저 컨텍스트 재생성
tstr_OvrlPropsBrowserRecreateContextTip=변경 사항을 적용하려면 브라우저 컨텍스트를 재생성해야 합니다.\n이 작업을 실행하면 페이지가 새로고침되고 방문 기록이 초기화됩니다.
tstr_OvrlPropsAdvanced3D=3D
tstr_OvrlPropsAdvancedHSBS=Half Side-by-Side (좌우 절반 폭)
tstr_OvrlPropsAdvancedSBS=Side-by-Side (좌우)
tstr_OvrlPropsAdvancedHOU=Half Over-Under (상하 절반 폭)
tstr_OvrlPropsAdvancedOU=Over-Under (상하)
tstr_OvrlPropsAdvanced3DSwap=좌우 화면 바꾸기
tstr_OvrlPropsAdvancedGazeFade=시선 페이드
tstr_OvrlPropsAdvancedGazeFadeAuto=자동 구성
tstr_OvrlPropsAdvancedGazeFadeDistance=거리
tstr_OvrlPropsAdvancedGazeFadeDistanceValueInf=무한
tstr_OvrlPropsAdvancedGazeFadeSensitivity=민감도
tstr_OvrlPropsAdvancedGazeFadeOpacity=대상 불투명도
tstr_OvrlPropsAdvancedInput=레이저 포인터 입력
tstr_OvrlPropsAdvancedInputInGame=게임 중 활성화
tstr_OvrlPropsAdvancedInputFloatingUI=플로팅 UI 표시
tstr_OvrlPropsAdvancedOverlayTags=오버레이 태그
tstr_OvrlPropsAdvancedOverlayTagsTip=오버레이 태그는 액션에서 오버레이를 대상으로 지정하는 데 사용됩니다
tstr_OvrlPropsPerformanceInvisibleUpdate=숨겨져 있어도 업데이트
tstr_OvrlPropsPerformanceInvisibleUpdateTip=불투명도 설정이나 시선 페이드로 숨겨져 있어도 오버레이를 업데이트합니다.\n서드파티 애플리케이션이 오버레이 내용을 접근하는 데 유용합니다.\n그 외 경우에는 권장하지 않습니다.\n오버레이가 수동 또는 표시 모드 설정으로 숨겨진 경우에는 여전히 업데이트가 중단됩니다.
tstr_OvrlPropsInterfaceOverlayName=오버레이 이름
tstr_OvrlPropsInterfaceOverlayNameAuto=[자동 이름]
tstr_OvrlPropsInterfaceActionOrderCustom=액션 버튼 재정의
tstr_OvrlPropsInterfaceDesktopButtons=데스크톱 버튼 표시
tstr_OvrlPropsInterfaceExtraButtons=추가 버튼 표시
;Overlay Bar
tstr_OverlayBarOvrlHide=숨기기
tstr_OverlayBarOvrlShow=표시
tstr_OverlayBarOvrlClone=복제
tstr_OverlayBarOvrlRemove=제거
tstr_OverlayBarOvrlRemoveConfirm=정말 제거하시겠습니까?
tstr_OverlayBarOvrlProperties=속성...
tstr_OverlayBarOvrlAddWindow=창 추가...
tstr_OverlayBarTooltipOvrlAdd=오버레이 추가
tstr_OverlayBarTooltipSettings=설정
tstr_OverlayBarTooltipResetHold=길게 눌러 창 위치 초기화...
;Floating UI
tstr_FloatingUIHideOverlayTip=오버레이 숨기기
tstr_FloatingUIHideOverlayHoldTip=길게 눌러 오버레이 제거...
tstr_FloatingUIDragModeEnableTip=드래그 모드 활성화
tstr_FloatingUIDragModeDisableTip=드래그 모드 비활성화
tstr_FloatingUIDragModeHoldLockTip=길게 눌러 오버레이 위치 잠금...
tstr_FloatingUIDragModeHoldUnlockTip=길게 눌러 오버레이 위치 잠금 해제...
tstr_FloatingUIWindowAddTip=활성 창 추가
tstr_FloatingUIActionBarShowTip=액션 바 표시
tstr_FloatingUIActionBarHideTip=액션 바 숨기기
tstr_FloatingUIBrowserGoBackTip=이전 페이지로 이동
tstr_FloatingUIBrowserGoForwardTip=다음 페이지로 이동
tstr_FloatingUIBrowserRefreshTip=현재 페이지 새로고침
tstr_FloatingUIBrowserStopTip=페이지 로드 중지
tstr_FloatingUIActionBarDesktopPrev=이전 데스크톱
tstr_FloatingUIActionBarDesktopNext=다음 데스크톱
tstr_FloatingUIActionBarEmpty=활성화된 액션 없음
;Special Actions
tstr_ActionNone=[없음]
tstr_ActionKeyboardShow=키보드 표시
tstr_ActionKeyboardHide=키보드 숨기기
;Default Actions (translation IDs are matched to Action name string)
tstr_DefActionShowKeyboard=키보드 표시
tstr_DefActionActiveWindowCrop=활성 창 크롭
tstr_DefActionActiveWindowCropLabel=활성\n창\n크롭
tstr_DefActionSwitchTask=작업 전환
tstr_DefActionToggleOverlays=오버레이 전환
tstr_DefActionToggleOverlaysLabel=오버레이\n전환
tstr_DefActionMiddleMouse=마우스 휠 클릭
tstr_DefActionMiddleMouseLabel=마우스\n휠\n클릭
tstr_DefActionBackMouse=마우스 뒤로가기 버튼
tstr_DefActionBackMouseLabel=마우스\n뒤로가기\n버튼
tstr_DefActionReadMe=설명서 열기
tstr_DefActionReadMeLabel=설명서\n열기
tstr_DefActionDashboardToggle=SteamVR 대시보드 전환 (디버그 명령)
tstr_DefActionDashboardToggleLabel=대시보드\n전환
;Performance Monitor (text space is very limited here, so keep it short or untranslated)
tstr_PerformanceMonitorCPU=CPU
tstr_PerformanceMonitorGPU=GPU
tstr_PerformanceMonitorRAM=RAM:
tstr_PerformanceMonitorVRAM=VRAM:
tstr_PerformanceMonitorFrameTime=프레임 시간:
tstr_PerformanceMonitorLoad=부하:
tstr_PerformanceMonitorFPS=FPS:
tstr_PerformanceMonitorFPSAverage=평균 FPS:
tstr_PerformanceMonitorReprojectionRatio=리프로젝션 비율:
tstr_PerformanceMonitorDroppedFrames=드롭된 프레임:
tstr_PerformanceMonitorBatteryLeft=왼쪽 컨트롤러:
tstr_PerformanceMonitorBatteryRight=오른쪽 컨트롤러:
tstr_PerformanceMonitorBatteryHMD=헤드셋:
tstr_PerformanceMonitorBatteryTracker=트래커 %ID%:
tstr_PerformanceMonitorBatteryDisconnected=N/A
tstr_PerformanceMonitorViveWirelessTempNotAvailable=N/A
tstr_PerformanceMonitorCompactCPU=CPU
tstr_PerformanceMonitorCompactGPU=GPU
tstr_PerformanceMonitorCompactFPS=FPS
tstr_PerformanceMonitorCompactFPSAverage=AVG
tstr_PerformanceMonitorCompactReprojectionRatio=% RPR
tstr_PerformanceMonitorCompactDroppedFrames=DRP
tstr_PerformanceMonitorCompactBattery=BAT
tstr_PerformanceMonitorCompactBatteryLeft=L
tstr_PerformanceMonitorCompactBatteryRight=R
tstr_PerformanceMonitorCompactBatteryHMD=H
tstr_PerformanceMonitorCompactBatteryTracker=T%ID%
tstr_PerformanceMonitorCompactBatteryDisconnected=N/A
tstr_PerformanceMonitorCompactViveWirelessTempNotAvailable=N/A
tstr_PerformanceMonitorEmpty=활성화된 성능 모니터 항목 없음
;Aux UI
tstr_AuxUIDragHintDocking=놓아서 도킹
tstr_AuxUIDragHintUndocking=놓아서 도킹 해제
tstr_AuxUIDragHintOvrlLocked=이 오버레이를 드래그하려면 오버레이 위치 잠금을 해제하세요
tstr_AuxUIDragHintOvrlTheaterScreenBlocked=오버레이 위치는 SteamVR 시어터 스크린에서 제어합니다
tstr_AuxUIGazeFadeAutoHint=오버레이 중앙을 보고 %SECONDS%초간 기다리세요...
tstr_AuxUIGazeFadeAutoHintSingular=오버레이 중앙을 보고 %SECONDS%초간 기다리세요...
tstr_AuxUIQuickStartWelcomeHeader=Desktop+에 오신 것을 환영합니다!
tstr_AuxUIQuickStartWelcomeBody=이 짧은 가이드는 프로그램의 기본 사항을 소개합니다.\n자세한 내용은 설명서와 사용자 가이드를 참조하세요.\n\n이 단계를 건너뛰려면 [닫기]를 누르세요.
tstr_AuxUIQuickStartOverlaysHeader=오버레이
tstr_AuxUIQuickStartOverlaysBody=Desktop+를 사용하면 데스크톱, 개별 창 등을 미러링하는 오버레이를 만들 수 있습니다.\n생성한 모든 오버레이는 아래 오버레이 바에 표시됩니다.\n\n오버레이 속성은 오버레이 아이콘을 클릭하고 [속성...]을 선택해 변경할 수 있습니다.
tstr_AuxUIQuickStartOverlaysBody2=[+] 버튼을 눌러 목록에서 캡처 소스/오버레이 유형을 선택하면 새 오버레이를 만들 수 있습니다.\n웹 브라우저 오버레이와 같은 일부 유형은 해당 구성 요소가 설치되어 있어야 사용할 수 있습니다.\n\n개별 오버레이 또는 전체 레이아웃을 프로필로 저장할 수 있으며, 현재 구성은 세션 간 자동으로 기억됩니다.
tstr_AuxUIQuickStartOverlayPropertiesHeader=오버레이 속성
tstr_AuxUIQuickStartOverlayPropertiesBody=오버레이에는 다양한 맞춤 설정 옵션이 있습니다.\n기준점과 표시 모드는 오버레이가 표시되는 위치와 시점을 설정합니다.\n표시되어야 할 오버레이가 보이지 않으면 이 설정을 확인하세요.\n위치를 초기화하면 도움이 될 수 있습니다.\n\n또한 오버레이를 컨트롤러 근처로 드래그하면 도킹할 수 있습니다.
tstr_AuxUIQuickStartOverlayPropertiesBody2=일부 속성은 기본적으로 숨겨져 있습니다.\n모든 옵션을 보려면 설정 창에서 [고급 설정 표시]를 켜세요.\n\nUI 창 위치는 해당 버튼을 길게 눌러 초기화할 수 있습니다.\n더블 클릭이나 우클릭은 오버레이 버튼에서도 단축 토글로 동작합니다.
tstr_AuxUIQuickStartSettingsHeader=설정
tstr_AuxUIQuickStartSettingsBody=설정 창에는 Desktop+의 전역 설정이 모두 포함되어 있습니다.\n오버레이 바 오른쪽 끝의 톱니바퀴 버튼을 눌러 열 수 있습니다.\n\n오버레이와 마찬가지로 설정 변경 사항은 자동으로 적용·저장됩니다.
tstr_AuxUIQuickStartProfilesHeader=프로필
tstr_AuxUIQuickStartProfilesBody=Desktop+에는 두 가지 프로필이 있습니다.\n\n오버레이 프로필:\n하나 이상의 오버레이와 해당 속성을 저장합니다.\n수동으로 불러오거나, 액션 및 앱 프로필에 따라 자동 불러올 수 있습니다.\n\n앱 프로필:\nSteamVR 앱 실행 시 자동으로 불러올 오버레이 프로필과 액션을 지정합니다.
tstr_AuxUIQuickStartActionsHeader=액션
tstr_AuxUIQuickStartActionsBody=액션은 컨트롤러 입력이나 앱 프로필에 할당하거나, 오버레이의 액션 바에 추가할 수 있는 일련의 명령입니다.\n데스크톱 입력 시뮬레이션부터 오버레이 레이아웃 변경, 외부 프로그램 실행까지 가능합니다.\n\nDesktop+에는 몇 가지 예시 액션이 포함되어 있습니다.
tstr_AuxUIQuickStartActionsBody2=기본적으로 액션은 실행된 오버레이(버튼 클릭·컨트롤러 입력) 또는 마지막으로 클릭된 포커스 오버레이를 대상으로 합니다.\n대상 오버레이가 중요하지 않은 경우도 있지만, 경우에 따라 특정 오버레이나 여러 개를 지정하면 유용할 수 있습니다.
tstr_AuxUIQuickStartOverlayTagsHeader=오버레이 태그
tstr_AuxUIQuickStartOverlayTagsBody=오버레이 태그에는 자동 태그(녹색)와 사용자 정의 태그가 있습니다.\n자동 태그는 해당 속성에 따라 자동 부여되며, 사용자 태그는 오버레이 속성 창에서 수동 지정할 수 있습니다.\n\n전역 단축키는 컨트롤러 버튼에 직접 바인딩되지 않으므로, SteamVR 입력 바인딩에서 매핑해야 합니다.
tstr_AuxUIQuickStartSettingsEndBody=설정은 다양하니 자유롭게 시도해 보세요.\n\n문제가 생기면 [기본 설정 복원]을 눌러 특정 항목만 초기화할 수도 있습니다.
tstr_AuxUIQuickStartFloatingUIHeader=플로팅 UI
tstr_AuxUIQuickStartFloatingUIBody=플로팅 UI는 오버레이를 가리킬 때 나타납니다.\n대시보드에서는 다른 오버레이를 가리키지 않을 때 항상 표시됩니다.\n\n기본 컨트롤, 액션 버튼 섹션, 기타 오버레이별 컨트롤이 포함되며, 각 오버레이 속성에서 표시 여부를 설정할 수 있습니다.
tstr_AuxUIQuickStartDesktopModeHeader=데스크톱 모드
tstr_AuxUIQuickStartDesktopModeBody=Desktop+는 데스크톱 창에서도 설정할 수 있어, 키보드 입력이 잦거나 컨트롤러 없이 변경할 때 유용합니다.\n기능은 동일하지만 VR 키보드 레이아웃 편집기는 데스크톱 모드에서만 사용할 수 있습니다.\n\n설정 창의 [문제 해결] 섹션이나 시스템 트레이 아이콘에서 전환할 수 있습니다.
tstr_AuxUIQuickStartEndHeader=설명서 & 사용자 가이드
tstr_AuxUIQuickStartEndBody=이 정도면 Desktop+를 시작하기 충분합니다.\n\n문제가 있으면 설명서를 참고하세요. 액션 바 버튼으로 열 수 있습니다.\n자세한 설명과 가이드는 사용자 가이드에 있습니다.\n\n이 가이드를 다시 보려면 [기본 설정 복원] 페이지 하단의 [퀵 스타트 가이드 표시]를 누르세요.
tstr_AuxUIQuickStartButtonNext=다음 페이지
tstr_AuxUIQuickStartButtonPrev=이전 페이지
tstr_AuxUIQuickStartButtonClose=닫기
;Desktop Mode
tstr_DesktopModeCatTools=도구
tstr_DesktopModeCatOverlays=오버레이
tstr_DesktopModeToolSettings=설정
tstr_DesktopModeToolActions=액션
tstr_DesktopModeOverlayListAdd=오버레이 추가
tstr_DesktopModePageAddWindowOverlayTitle=창 오버레이 추가
tstr_DesktopModePageAddWindowOverlayHeader=창 선택
;Keyboard Editor
tstr_KeyboardEditorKeyListTitle=키 목록
tstr_KeyboardEditorKeyListTabContextReplace=내용 교체...
tstr_KeyboardEditorKeyListTabContextClear=서브 레이아웃 지우기
tstr_KeyboardEditorKeyListRow=행 %ID%
tstr_KeyboardEditorKeyListSpacing=[간격]
tstr_KeyboardEditorKeyListKeyAdd=추가
tstr_KeyboardEditorKeyListKeyDuplicate=복제
tstr_KeyboardEditorKeyListKeyRemove=삭제
tstr_KeyboardEditorKeyPropertiesTitle=키 속성
tstr_KeyboardEditorKeyPropertiesNoSelection=선택된 키 없음
tstr_KeyboardEditorKeyPropertiesType=유형
tstr_KeyboardEditorKeyPropertiesTypeBlank=빈 공간
tstr_KeyboardEditorKeyPropertiesTypeVirtualKey=가상 키
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyToggle=가상 키 (토글)
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyIsoEnter=가상 키 (ISO-Enter)
tstr_KeyboardEditorKeyPropertiesTypeString=문자열
tstr_KeyboardEditorKeyPropertiesTypeSublayoutToggle=서브 레이아웃 전환
tstr_KeyboardEditorKeyPropertiesTypeAction=액션
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyIsoEnterTip=인접한 두 행에 "가상 키 (ISO-Enter)"를 배치해 ISO-Enter 모양 키를 만듭니다.\n각 서브 레이아웃당 하나만 허용됩니다.
tstr_KeyboardEditorKeyPropertiesTypeStringTip=기본 문자가 아닌 키에는 문자열 유형을 사용해, 실제 키보드 레이아웃에 맞는 조합을 찾아 호환성을 높입니다.
tstr_KeyboardEditorKeyPropertiesSize=크기
tstr_KeyboardEditorKeyPropertiesLabel=레이블
tstr_KeyboardEditorKeyPropertiesKeyCode=키 코드
tstr_KeyboardEditorKeyPropertiesString=문자열
tstr_KeyboardEditorKeyPropertiesSublayout=서브 레이아웃
tstr_KeyboardEditorKeyPropertiesAction=액션
tstr_KeyboardEditorKeyPropertiesCluster=그룹
tstr_KeyboardEditorKeyPropertiesClusterTip=그룹은 특정 키 로드를 비활성화할 때 사용됩니다.\n주변 빈 공간은 제거되지 않으니, 올바른 전환을 위해 빈 공간 키도 그룹에 배정하세요.
tstr_KeyboardEditorKeyPropertiesBlockModifiers=조합 키 차단
tstr_KeyboardEditorKeyPropertiesBlockModifiersTip=키를 누르는 동안 모든 조합 키를 해제합니다.
tstr_KeyboardEditorKeyPropertiesNoRepeat=반복 안 함
tstr_KeyboardEditorKeyPropertiesNoRepeatTip=키 반복이 켜져 있어도, 누르고 있는 동안 반복 입력을 차단합니다.
tstr_KeyboardEditorMetadataTitle=레이아웃 메타데이터
tstr_KeyboardEditorMetadataName=이름
tstr_KeyboardEditorMetadataAuthor=작성자
tstr_KeyboardEditorMetadataHasAltGr=AltGr 키 사용
tstr_KeyboardEditorMetadataHasAltGrTip=오른쪽 Alt 키를 누르면 AltGr 서브 레이아웃으로 전환
tstr_KeyboardEditorMetadataClusterPreview=그룹 미리보기:
tstr_KeyboardEditorMetadataSave=저장...
tstr_KeyboardEditorMetadataLoad=불러오기...
tstr_KeyboardEditorMetadataSavePopupTitle=키보드 레이아웃 저장
tstr_KeyboardEditorMetadataSavePopupFilename=파일 이름
tstr_KeyboardEditorMetadataSavePopupFilenameBlankTip=파일 이름은 비워둘 수 없습니다
tstr_KeyboardEditorMetadataSavePopupConfirm=레이아웃 저장
tstr_KeyboardEditorMetadataSavePopupConfirmError=레이아웃 저장 실패
tstr_KeyboardEditorMetadataLoadPopupTitle=키보드 레이아웃 불러오기
tstr_KeyboardEditorMetadataLoadPopupConfirm=레이아웃 불러오기
tstr_KeyboardEditorPreviewTitle=키보드 미리보기
tstr_KeyboardEditorSublayoutBase=Base
tstr_KeyboardEditorSublayoutShift=Shift
tstr_KeyboardEditorSublayoutAltGr=AltGr
tstr_KeyboardEditorSublayoutAux=Aux
;General Dialog Strings
tstr_DialogOk=확인
tstr_DialogCancel=취소
tstr_DialogDone=완료
tstr_DialogUndo=실행 취소
tstr_DialogRedo=다시 실행
tstr_DialogColorPickerHeader=색 선택
tstr_DialogColorPickerCurrent=현재
tstr_DialogColorPickerOriginal=원본
tstr_DialogProfilePickerHeader=프로필 선택
tstr_DialogProfilePickerNone=[없음]
tstr_DialogActionPickerHeader=액션 선택
tstr_DialogActionPickerEmpty=사용 가능한 액션 없음
tstr_DialogIconPickerHeader=아이콘 선택
tstr_DialogIconPickerHeaderTip=커스텀 아이콘은 "images\icons\" 디렉토리에 PNG 파일로 추가하세요
tstr_DialogIconPickerNone=[아이콘 없음]
tstr_DialogKeyCodePickerHeader=키 코드 선택
tstr_DialogKeyCodePickerHeaderHotkey=단축키 선택
tstr_DialogKeyCodePickerModifiers=조합 키
tstr_DialogKeyCodePickerKeyCode=키 코드
tstr_DialogKeyCodePickerKeyCodeHint=목록 필터
tstr_DialogKeyCodePickerKeyCodeNone=[없음]
tstr_DialogKeyCodePickerFromInput=입력에서 가져오기...
tstr_DialogKeyCodePickerFromInputPopup=아무 키나 마우스 버튼을 누르세요...
tstr_DialogKeyCodePickerFromInputPopupNoMouse=아무 키나 누르세요...
tstr_DialogWindowPickerHeader=창 선택
tstr_DialogInputTagsHint=필터 또는 새 태그 추가
;Source Strings
tstr_SourceDesktopAll=통합 데스크톱
tstr_SourceDesktopID=데스크톱 %ID%
tstr_SourceWinRTNone=[캡처 대상 없음]
tstr_SourceWinRTUnknown=[알 수 없는 창]
tstr_SourceWinRTClosed=[닫힘]:
tstr_SourcePerformanceMonitor=성능 모니터
tstr_SourceBrowser=브라우저
tstr_SourceBrowserNoPage=[페이지 없음]
;Notification Icon
tstr_NotificationIconRestoreVR=VR 인터페이스 복원
tstr_NotificationIconOpenOnDesktop=데스크톱에서 설정 열기
tstr_NotificationIconQuit=종료
;Notifications
tstr_NotificationInitialStartupTitleVR=초기 설정
tstr_NotificationInitialStartupTitleDesktop=Desktop+ 초기 설정
tstr_NotificationInitialStartupMessage=Desktop+가 SteamVR에 정상적으로 추가되어, 이제 SteamVR 실행 시 자동으로 실행됩니다.
;Browser
tstr_BrowserErrorPageTitle=페이지 로드 오류 - Desktop+
tstr_BrowserErrorPageHeading=페이지를 불러올 수 없습니다
tstr_BrowserErrorPageMessage=%URL% 로드 중 오류 발생: %ERROR%
================================================
FILE: assets/lang/zh_CN.ini
================================================
[TranslationInfo]
Name=简体中文 (Simplified Chinese)
Author=Xuan25_瑄
Locale=zh-CN
;如果该语言在默认字体或其加载顺序上存在问题,则可以在此处定义一个不同的字体,该字体将首先加载
;这应该是带有扩展名的文件名,会从操作系统的字体文件夹中加载,如果找不到,则从 "lang" 文件夹加载
PreferredFont=msyh.ttc
;将文件名设置为 ISO 639-1 语言代码以支持自动检测,如有必要,后面跟上带下划线的 ISO 3166-1 地区代码
;要翻译 SteamVR 输入绑定,请在“input”文件夹中添加新的本地化文件,并在 action_manifest.json 中进行相应调整
[Strings]
;设置窗口
tstr_SettingsWindowTitle=Desktop+ 设置
tstr_SettingsCatInterface=界面
tstr_SettingsCatEnvironment=环境
tstr_SettingsCatProfiles=配置文件
tstr_SettingsCatActions=操作
tstr_SettingsCatKeyboard=键盘
tstr_SettingsCatMouse=鼠标
tstr_SettingsCatLaserPointer=激光指针
tstr_SettingsCatWindowOverlays=窗口叠加
tstr_SettingsCatBrowser=浏览器
tstr_SettingsCatPerformance=性能
tstr_SettingsCatVersionInfo=版本信息
tstr_SettingsCatWarnings=警告
tstr_SettingsCatStartup=启动
tstr_SettingsCatTroubleshooting=故障排除
tstr_SettingsWarningPrefix=警告:
tstr_SettingsWarningCompositorResolution=SteamVR 合成器分辨率低于 100%!这会影响叠加的渲染质量。
tstr_SettingsWarningCompositorQuality=SteamVR 叠加渲染质量未设置为高!
tstr_SettingsWarningProcessElevated=Desktop+ 正在以管理员权限运行!
tstr_SettingsWarningElevatedMode=管理员权限模式已激活!
tstr_SettingsWarningBrowserMissing=正在使用浏览器叠加,但 Desktop+ 浏览器组件当前不可用。
tstr_SettingsWarningBrowserMismatch=已安装的 Desktop+ 浏览器组件与此版本的 Desktop+ 不兼容!
tstr_SettingsWarningElevatedProcessFocus=一个以管理员权限运行的进程当前拥有焦点!\nDesktop+ 无法模拟输入。
tstr_SettingsWarningUIAccessLost=Desktop+ 已不再以 UIAccess 权限运行!
tstr_SettingsWarningOverlayCreationErrorLimit=创建叠加失败!(超过最大叠加数上限)
tstr_SettingsWarningOverlayCreationErrorOther=创建叠加失败!(%ERRORNAME%)
tstr_SettingsWarningGraphicsCaptureError=图形捕获线程出现意外错误!(%ERRORCODE%)
tstr_SettingsWarningAppProfileActive=%APPNAME% 的应用程序配置文件已覆写了当前叠加布局。\n在此激活期间对叠加所做的更改不会自动保存。
tstr_SettingsWarningConfigMigrated=从旧版本 Desktop+ 的配置和配置文件已迁移到新的格式。\n原文件未被删除,仍可与旧版本的应用程序一起使用。\n如需从头开始,可在 [恢复默认设置] 中进行重置。
tstr_SettingsWarningMenuDontShowAgain=不再显示
tstr_SettingsWarningMenuDismiss=忽略
tstr_SettingsInterfaceLanguage=语言
tstr_SettingsInterfaceLanguageCommunity=社区翻译:%AUTHOR%
tstr_SettingsInterfaceLanguageIncompleteWarning=翻译可能不完整
tstr_SettingsInterfaceAdvancedSettings=显示高级设置
tstr_SettingsInterfaceAdvancedSettingsTip=显示高级和不常用的设置
tstr_SettingsInterfaceBlankSpaceDrag=单击空白区域时可拖动窗口
tstr_SettingsInterfacePersistentUI=持久化界面
tstr_SettingsInterfacePersistentUIManage=管理
tstr_SettingsInterfaceDesktopButtons=桌面按钮显示样式
tstr_SettingsInterfaceDesktopButtonsNone=无
tstr_SettingsInterfaceDesktopButtonsIndividual=单独桌面
tstr_SettingsInterfaceDesktopButtonsCycle=循环按钮
tstr_SettingsInterfaceDesktopButtonsAddCombined=增加合并桌面按钮
tstr_SettingsInterfacePersistentUIHelp=Desktop+ 会分别记住界面窗口在普通使用(全局)和在 Desktop+ 的 SteamVR 主面板标签页(Desktop+ Tab)中的状态。
tstr_SettingsInterfacePersistentUIHelp2=以下控件可用于直接管理那些窗口的状态。\n请注意,一些状态可能需要重置位置才能让窗口重回可见范围。
tstr_SettingsInterfacePersistentUIWindowsHeader=窗口
tstr_SettingsInterfacePersistentUIWindowsSettings=设置
tstr_SettingsInterfacePersistentUIWindowsProperties=叠加属性
tstr_SettingsInterfacePersistentUIWindowsKeyboard=Desktop+ 键盘
tstr_SettingsInterfacePersistentUIWindowsStateGlobal=全局
tstr_SettingsInterfacePersistentUIWindowsStateDashboardTab=Desktop+ 标签页
tstr_SettingsInterfacePersistentUIWindowsStateVisible=可见
tstr_SettingsInterfacePersistentUIWindowsStatePinned=固定
tstr_SettingsInterfacePersistentUIWindowsStatePosition=位置
tstr_SettingsInterfacePersistentUIWindowsStatePositionReset=重置
tstr_SettingsInterfacePersistentUIWindowsStateSize=大小
tstr_SettingsInterfacePersistentUIWindowsStateLaunchRestore=在 Desktop+ 启动时恢复状态
tstr_SettingsEnvironmentBackgroundColor=背景颜色
tstr_SettingsEnvironmentBackgroundColorDispModeNever=从不显示
tstr_SettingsEnvironmentBackgroundColorDispModeDPlusTab=仅在 Desktop+ 标签页中显示
tstr_SettingsEnvironmentBackgroundColorDispModeAlways=始终显示
tstr_SettingsEnvironmentDimInterface=界面变暗
tstr_SettingsEnvironmentDimInterfaceTip=在 Desktop+ 主面板标签页打开时,使 SteamVR 主面板和 Desktop+ 界面变暗
tstr_SettingsProfilesOverlays=叠加配置文件
tstr_SettingsProfilesApps=应用程序配置文件
tstr_SettingsProfilesManage=管理
tstr_SettingsProfilesOverlaysHeader=管理叠加配置文件
tstr_SettingsProfilesOverlaysNameDefault=默认
tstr_SettingsProfilesOverlaysNameNew=[新配置文件]
tstr_SettingsProfilesOverlaysNameNewBase=配置文件 %ID%
tstr_SettingsProfilesOverlaysProfileLoad=加载配置文件
tstr_SettingsProfilesOverlaysProfileAdd=从配置文件添加叠加
tstr_SettingsProfilesOverlaysProfileSave=保存当前叠加
tstr_SettingsProfilesOverlaysProfileDelete=删除配置文件
tstr_SettingsProfilesOverlaysProfileDeleteConfirm=确定删除?
tstr_SettingsProfilesOverlaysProfileFailedLoad=加载配置文件失败
tstr_SettingsProfilesOverlaysProfileFailedDelete=删除配置文件失败
tstr_SettingsProfilesOverlaysProfileAddSelectHeader=从配置文件中选择要添加的叠加
tstr_SettingsProfilesOverlaysProfileAddSelectEmpty=此配置文件中不包含任何叠加。
tstr_SettingsProfilesOverlaysProfileAddSelectDo=添加所选叠加
tstr_SettingsProfilesOverlaysProfileAddSelectAll=全选
tstr_SettingsProfilesOverlaysProfileAddSelectNone=全不选
tstr_SettingsProfilesOverlaysProfileSaveSelectHeader=保存当前叠加
tstr_SettingsProfilesOverlaysProfileSaveSelectName=配置文件名称
tstr_SettingsProfilesOverlaysProfileSaveSelectNameErrorBlank=名称不能为空
tstr_SettingsProfilesOverlaysProfileSaveSelectNameErrorTaken=名称已被占用
tstr_SettingsProfilesOverlaysProfileSaveSelectHeaderList=选择要保存到配置文件的叠加
tstr_SettingsProfilesOverlaysProfileSaveSelectDo=保存所选叠加
tstr_SettingsProfilesOverlaysProfileSaveSelectDoFailed=保存配置文件失败
tstr_SettingsProfilesAppsHeader=管理应用程序配置文件
tstr_SettingsProfilesAppsHeaderNoVRTip=Desktop+ 未在运行 VR 时,仅会列出已存在的应用程序配置文件
tstr_SettingsProfilesAppsListEmpty=暂无应用程序
tstr_SettingsProfilesAppsProfileHeaderActive=(当前激活)
tstr_SettingsProfilesAppsProfileEnabled=当应用程序运行时激活
tstr_SettingsProfilesAppsProfileOverlayProfile=叠加配置文件
tstr_SettingsProfilesAppsProfileActionEnter=进入时操作
tstr_SettingsProfilesAppsProfileActionLeave=退出时操作
tstr_SettingsActionsManage=操作
tstr_SettingsActionsManageButton=管理
tstr_SettingsActionsButtonsOrderDefault=操作按钮(默认)
tstr_SettingsActionsButtonsOrderOverlayBar=操作按钮(叠加栏)
tstr_SettingsActionsShowBindings=显示控制器绑定
tstr_SettingsActionsActiveShortcuts=激活的控制器按钮
tstr_SettingsActionsActiveShortcutsTip=指向叠加时的控制器绑定。\n可通过 VR 主面板的控制器绑定界面来更改使用哪些按键。
tstr_SettingsActionsActiveShortuctsHome=“返回首页”
tstr_SettingsActionsActiveShortuctsBack=“后退”
tstr_SettingsActionsGlobalShortcuts=全局控制器按钮
tstr_SettingsActionsGlobalShortcutsTip=在主面板关闭且未指向叠加时的控制器绑定。\n可通过 Desktop+ 的控制器绑定来配置这些按键。
tstr_SettingsActionsGlobalShortcutsEntry=全局快捷键 #%ID%
tstr_SettingsActionsGlobalShortcutsAdd=添加快捷键
tstr_SettingsActionsGlobalShortcutsRemove=移除最后一个快捷键
tstr_SettingsActionsHotkeys=热键
tstr_SettingsActionsHotkeysTip=全系统范围的键盘快捷键。\n热键会拦截其他应用程序对该组合键的接收;如果该组合键已被其他地方注册,可能无法生效。
tstr_SettingsActionsHotkeysAdd=添加热键
tstr_SettingsActionsHotkeysRemove=移除
tstr_SettingsActionsTableHeaderAction=操作
tstr_SettingsActionsTableHeaderShortcut=快捷键
tstr_SettingsActionsTableHeaderHotkey=热键
tstr_SettingsActionsManageHeader=管理操作
tstr_SettingsActionsManageCopyUID=复制操作 UID 到剪贴板
tstr_SettingsActionsManageNew=新建操作...
tstr_SettingsActionsManageEdit=编辑操作
tstr_SettingsActionsManageDuplicate=复制操作
tstr_SettingsActionsManageDelete=删除操作
tstr_SettingsActionsManageDeleteConfirm=确定删除?
tstr_SettingsActionsManageDuplicatedName=%NAME%(副本)
tstr_SettingsActionsEditHeader=编辑操作
tstr_SettingsActionsEditName=名称
tstr_SettingsActionsEditNameTranslatedTip=此操作当前使用翻译字符串 ID 作为名称,会自动匹配所选应用语言
tstr_SettingsActionsEditTarget=目标
tstr_SettingsActionsEditTargetDefault=默认
tstr_SettingsActionsEditTargetDefaultTip=目标为激活此操作的叠加,如果不适用,则目标为当前拥有焦点的叠加
tstr_SettingsActionsEditTargetUseTags=使用标签
tstr_SettingsActionsEditTargetActionTarget=操作目标
tstr_SettingsActionsEditHeaderAppearance=按钮外观
tstr_SettingsActionsEditIcon=图标
tstr_SettingsActionsEditLabel=标签
tstr_SettingsActionsEditLabelTranslatedTip=此操作当前使用翻译字符串 ID 作为标签,会自动匹配所选应用语言
tstr_SettingsActionsEditHeaderCommands=命令
tstr_SettingsActionsEditNameNew=新操作
tstr_SettingsActionsEditCommandAdd=添加命令
tstr_SettingsActionsEditCommandDelete=删除命令
tstr_SettingsActionsEditCommandDeleteConfirm=确定删除?
tstr_SettingsActionsEditCommandType=命令类型
tstr_SettingsActionsEditCommandTypeNone=无
tstr_SettingsActionsEditCommandTypeKey=按键
tstr_SettingsActionsEditCommandTypeMousePos=设置鼠标位置
tstr_SettingsActionsEditCommandTypeString=输入字符串
tstr_SettingsActionsEditCommandTypeLaunchApp=启动应用程序
tstr_SettingsActionsEditCommandTypeShowKeyboard=显示键盘
tstr_SettingsActionsEditCommandTypeCropActiveWindow=裁剪至当前活动窗口
tstr_SettingsActionsEditCommandTypeShowOverlay=显示叠加
tstr_SettingsActionsEditCommandTypeSwitchTask=切换任务
tstr_SettingsActionsEditCommandTypeLoadOverlayProfile=加载叠加配置文件
tstr_SettingsActionsEditCommandTypeUnknown=未知
tstr_SettingsActionsEditCommandVisibilityToggle=切换
tstr_SettingsActionsEditCommandVisibilityShow=始终显示
tstr_SettingsActionsEditCommandVisibilityHide=始终隐藏
tstr_SettingsActionsEditCommandUndo=在松开时撤销
tstr_SettingsActionsEditCommandKeyCode=按键码
tstr_SettingsActionsEditCommandKeyToggle=按键切换
tstr_SettingsActionsEditCommandMouseX=X
tstr_SettingsActionsEditCommandMouseY=Y
tstr_SettingsActionsEditCommandMouseUseCurrent=使用当前鼠标位置
tstr_SettingsActionsEditCommandString=字符串
tstr_SettingsActionsEditCommandPath=可执行文件路径
tstr_SettingsActionsEditCommandPathTip=这也可以是一个普通文件或 URL
tstr_SettingsActionsEditCommandArgs=应用程序参数
tstr_SettingsActionsEditCommandArgsTip=这些会传递给应用程序。\n如果不确定,可以留空。
tstr_SettingsActionsEditCommandVisibility=可见性
tstr_SettingsActionsEditCommandSwitchingMethod=切换方法
tstr_SettingsActionsEditCommandSwitchingMethodSwitcher=显示任务切换器
tstr_SettingsActionsEditCommandSwitchingMethodFocus=聚焦窗口
tstr_SettingsActionsEditCommandWindow=窗口
tstr_SettingsActionsEditCommandWindowNone=[无窗口]
tstr_SettingsActionsEditCommandWindowStrictMatchingTip=在寻找要切换的窗口时,仅允许精确匹配窗口标题
tstr_SettingsActionsEditCommandCursorWarp=将光标移至该窗口
tstr_SettingsActionsEditCommandProfile=配置文件
tstr_SettingsActionsEditCommandProfileClear=移除现有叠加
tstr_SettingsActionsEditCommandDescNone=无命令
tstr_SettingsActionsEditCommandDescKey=按下按键 “%KEYNAME%”
tstr_SettingsActionsEditCommandDescKeyToggle=切换按键 “%KEYNAME%”
tstr_SettingsActionsEditCommandDescMousePos=设置鼠标位置到 %X%, %Y%
tstr_SettingsActionsEditCommandDescString=输入字符串 “%STRING%”
tstr_SettingsActionsEditCommandDescLaunchApp=启动应用程序 “%APP%” %ARGSOPT%
tstr_SettingsActionsEditCommandDescLaunchAppArgsOpt=“%ARGS%”
tstr_SettingsActionsEditCommandDescKeyboardToggle=切换键盘
tstr_SettingsActionsEditCommandDescKeyboardShow=显示键盘
tstr_SettingsActionsEditCommandDescKeyboardHide=隐藏键盘
tstr_SettingsActionsEditCommandDescCropWindow=裁剪至当前活动窗口
tstr_SettingsActionsEditCommandDescOverlayToggle=切换叠加:%TAGS%
tstr_SettingsActionsEditCommandDescOverlayShow=显示叠加:%TAGS%
tstr_SettingsActionsEditCommandDescOverlayHide=隐藏叠加:%TAGS%
tstr_SettingsActionsEditCommandDescOverlayTargetDefault=[操作目标]
tstr_SettingsActionsEditCommandDescSwitchTask=切换任务
tstr_SettingsActionsEditCommandDescSwitchTaskWindow=切换到窗口 “%WINDOW%”
tstr_SettingsActionsEditCommandDescLoadOverlayProfile=加载叠加配置文件 “%PROFILE%”
tstr_SettingsActionsEditCommandDescLoadOverlayProfileAdd=添加叠加配置文件 “%PROFILE%”
tstr_SettingsActionsEditCommandDescUnknown=未知命令
tstr_SettingsActionsOrderHeader=更改操作顺序
tstr_SettingsActionsOrderButtonLabel=已选择 %COUNT% 个操作
tstr_SettingsActionsOrderButtonLabelSingular=已选择 %COUNT% 个操作
tstr_SettingsActionsOrderNoActions=未选择任何操作
tstr_SettingsActionsOrderAdd=添加操作...
tstr_SettingsActionsOrderRemove=移除操作
tstr_SettingsActionsAddSelectorHeader=选择要添加的操作
tstr_SettingsActionsAddSelectorAdd=添加所选操作
tstr_SettingsKeyboardLayout=键盘布局
tstr_SettingsKeyboardSize=大小
tstr_SettingsKeyboardBehavior=行为
tstr_SettingsKeyboardStickyMod=粘滞修饰键
tstr_SettingsKeyboardKeyRepeat=按键重复
tstr_SettingsKeyboardAutoShow=自动显示
tstr_SettingsKeyboardAutoShowDesktopOnly=自动显示
tstr_SettingsKeyboardAutoShowDesktop=针对桌面与窗口叠加
tstr_SettingsKeyboardAutoShowDesktopTip=实验性功能。并非所有应用都支持。
tstr_SettingsKeyboardAutoShowBrowser=针对浏览器叠加
tstr_SettingsKeyboardLayoutAuthor=作者:%AUTHOR%
tstr_SettingsKeyboardKeyClusters=键簇
tstr_SettingsKeyboardKeyClusterBase=基本
tstr_SettingsKeyboardKeyClusterFunction=功能
tstr_SettingsKeyboardKeyClusterNavigation=导航
tstr_SettingsKeyboardKeyClusterNumpad=数字小键盘
tstr_SettingsKeyboardKeyClusterExtra=额外
tstr_SettingsKeyboardSwitchToEditor=切换到键盘布局编辑器
tstr_SettingsMouseShowCursor=显示鼠标指针
tstr_SettingsMouseShowCursorGCUnsupported=此系统不支持在图形捕获(Graphics Capture)叠加中禁用光标
tstr_SettingsMouseShowCursorGCActiveWarning=当前激活的图形捕获镜像可能会阻止桌面复制叠加隐藏光标
tstr_SettingsMouseScrollSmooth=使用平滑滚动
tstr_SettingsMouseSimulatePen=模拟为笔输入
tstr_SettingsMouseSimulatePenUnsupported=此系统不支持笔输入模拟
tstr_SettingsMouseAllowLaserPointerOverride=允许激光指针覆写
tstr_SettingsMouseAllowLaserPointerOverrideTip=当物理鼠标快速移动时,自动禁用激光指针。\n单击叠加可重新启用激光指针。
tstr_SettingsMouseDoubleClickAssist=双击辅助
tstr_SettingsMouseDoubleClickAssistTip=在设定时间内冻结鼠标位置,以便更容易进行双击
tstr_SettingsMouseDoubleClickAssistTipValueOff=关闭
tstr_SettingsMouseDoubleClickAssistTipValueAuto=自动
tstr_SettingsMouseSmoothing=输入平滑
tstr_SettingsMouseSmoothingLevelNone=无
tstr_SettingsMouseSmoothingLevelVeryLow=极低
tstr_SettingsMouseSmoothingLevelLow=低
tstr_SettingsMouseSmoothingLevelMedium=中
tstr_SettingsMouseSmoothingLevelHigh=高
tstr_SettingsMouseSmoothingLevelVeryHigh=极高
tstr_SettingsLaserPointerTip=以下设置适用于在关闭 SteamVR 主面板时 Desktop+ 的激光指针
tstr_SettingsLaserPointerBlockInput=激光指针激活时阻止游戏输入
tstr_SettingsLaserPointerAutoToggleDistance=自动激活的最大距离
tstr_SettingsLaserPointerAutoToggleDistanceValueOff=关闭
tstr_SettingsLaserPointerHMDPointer=基于视线的头显指针
tstr_SettingsLaserPointerHMDPointerTableHeaderInputAction=输入操作
tstr_SettingsLaserPointerHMDPointerTableHeaderBinding=键盘按键
tstr_SettingsLaserPointerHMDPointerTableBindingToggle=切换激光指针
tstr_SettingsLaserPointerHMDPointerTableBindingLeft=鼠标左键
tstr_SettingsLaserPointerHMDPointerTableBindingRight=鼠标右键
tstr_SettingsLaserPointerHMDPointerTableBindingMiddle=鼠标中键
tstr_SettingsLaserPointerHMDPointerTableBindingDrag=拖动叠加
tstr_SettingsWindowOverlaysAutoFocus=指向叠加时自动聚焦窗口
tstr_SettingsWindowOverlaysKeepOnScreen=使窗口保持在屏幕内
tstr_SettingsWindowOverlaysKeepOnScreenTip=若窗口超出屏幕工作区域,则自动移动它至可见范围内
tstr_SettingsWindowOverlaysAutoSizeOverlay=当窗口调整大小时同步调整叠加大小
tstr_SettingsWindowOverlaysFocusSceneApp=从叠加移开激光指针后聚焦游戏
tstr_SettingsWindowOverlaysFocusSceneAppDashboard=关闭主面板后聚焦游戏
tstr_SettingsWindowOverlaysOnWindowDrag=窗口拖动时
tstr_SettingsWindowOverlaysOnWindowDragDoNothing=不执行任何操作
tstr_SettingsWindowOverlaysOnWindowDragBlock=阻止拖动
tstr_SettingsWindowOverlaysOnWindowDragOverlay=拖动叠加
tstr_SettingsWindowOverlaysOnCaptureLoss=捕获丢失时
tstr_SettingsWindowOverlaysOnCaptureLossTip=当窗口捕获丢失(通常是因为目标窗口已关闭)时的行为。\n“隐藏叠加”模式在捕获恢复后会重新显示叠加。
tstr_SettingsWindowOverlaysOnCaptureLossDoNothing=不执行任何操作
tstr_SettingsWindowOverlaysOnCaptureLossHide=隐藏叠加
tstr_SettingsWindowOverlaysOnCaptureLossRemove=移除叠加
tstr_SettingsBrowserMaxFrameRate=最大帧率
tstr_SettingsBrowserMaxFrameRateOverrideOff=使用全局设置
tstr_SettingsBrowserContentBlocker=内容拦截器
tstr_SettingsBrowserContentBlockerTip=在“DesktopPlusBrowser\content_block”目录中添加采用 Adblock Plus 语法的屏蔽列表。\n目录中的所有列表都会被加载。
tstr_SettingsBrowserContentBlockerListCount=(已启用 %LISTCOUNT% 个列表)
tstr_SettingsBrowserContentBlockerListCountSingular=(已启用 %LISTCOUNT% 个列表)
tstr_SettingsPerformanceUpdateLimiter=限制更新
tstr_SettingsPerformanceUpdateLimiterMode=更新限制模式
tstr_SettingsPerformanceUpdateLimiterModeOff=关闭
tstr_SettingsPerformanceUpdateLimiterModeMS=帧间隔
tstr_SettingsPerformanceUpdateLimiterModeFPS=帧率
tstr_SettingsPerformanceUpdateLimiterModeOffOverride=使用全局设置
tstr_SettingsPerformanceUpdateLimiterModeMSTip=“帧间隔”会强制叠加更新之间保持最小时间间隔
tstr_SettingsPerformanceUpdateLimiterFPSValue=%FPS% 帧/秒
tstr_SettingsPerformanceUpdateLimiterOverride=覆写限制更新
tstr_SettingsPerformanceUpdateLimiterOverrideTip=当多个覆写同时启用时,将使用可以提供最高更新率的覆写
tstr_SettingsPerformanceUpdateLimiterModeOverride=覆写更新限制模式
tstr_SettingsPerformanceRapidUpdates=降低激光指针延迟
tstr_SettingsPerformanceRapidUpdatesTip=在指向叠加时,以增加 CPU 负载为代价减少激光指针输入延迟
tstr_SettingsPerformanceSingleDesktopMirror=单独桌面镜像
tstr_SettingsPerformanceSingleDesktopMirrorTip=切换桌面时只镜像单个桌面,而不是从合并桌面进行裁剪。\n启用后,所有叠加都将显示相同的桌面。
tstr_SettingsPerformanceUseHDR=HDR 镜像
tstr_SettingsPerformanceUseHDRTip=使用更高位深纹理镜像桌面或窗口,以支持 HDR 输出(实验性)。\n在不需要时可能会导致性能下降,并增加显存使用量。
tstr_SettingsPerformanceShowFPS=在浮动 UI 上显示帧率
tstr_SettingsWarningsHidden=已隐藏的警告:
tstr_SettingsWarningsReset=重置隐藏的警告
tstr_SettingsStartupAutoLaunch=在 SteamVR 启动时自动启动
tstr_SettingsStartupSteamDisable=禁用 Steam 集成
tstr_SettingsStartupSteamDisableTip=如果 Desktop+ 是由 Steam 启动的,则以不带 Steam 的方式重新启动。\n这样会禁用应用内的永久在线状态、使用时长统计和其他 Steam 功能。
tstr_SettingsTroubleshootingRestart=重启
tstr_SettingsTroubleshootingRestartSteam=随 Steam 重启
tstr_SettingsTroubleshootingRestartDesktop=以桌面模式重启
tstr_SettingsTroubleshootingElevatedModeEnter=进入管理员权限模式
tstr_SettingsTroubleshootingElevatedModeLeave=退出管理员权限模式
tstr_SettingsTroubleshootingSettingsReset=恢复默认设置
tstr_SettingsTroubleshootingSettingsResetConfirmDescription=恢复默认设置将把所选元素重置为默认值。\n此操作无法撤销。\n\n重置以下项目:
tstr_SettingsTroubleshootingSettingsResetConfirmButton=重置所选元素
tstr_SettingsTroubleshootingSettingsResetConfirmElementOverlays=当前叠加设置
tstr_SettingsTroubleshootingSettingsResetConfirmElementLegacyFiles=删除未使用的旧版配置和配置文件
tstr_SettingsTroubleshootingSettingsResetShowQuickStart=显示快速入门指南
;键盘窗口
tstr_KeyboardWindowTitle=Desktop+ 键盘
tstr_KeyboardWindowTitleSettings=Desktop+ 键盘(设置)
tstr_KeyboardWindowTitleOverlay=Desktop+ 键盘(%OVERLAYNAME%)
tstr_KeyboardWindowTitleOverlayUnknown=[未知叠加]
;键盘快捷键窗口
tstr_KeyboardShortcutsCut=剪切
tstr_KeyboardShortcutsCopy=复制
tstr_KeyboardShortcutsPaste=粘贴
;叠加属性窗口
tstr_OvrlPropsCatPosition=位置
tstr_OvrlPropsCatAppearance=外观
tstr_OvrlPropsCatCapture=捕获
tstr_OvrlPropsCatPerformanceMonitor=性能监控
tstr_OvrlPropsCatBrowser=浏览器
tstr_OvrlPropsCatAdvanced=高级
tstr_OvrlPropsCatPerformance=性能
tstr_OvrlPropsCatInterface=界面
tstr_OvrlPropsPositionOrigin=原点
tstr_OvrlPropsPositionOriginRoom=游戏空间
tstr_OvrlPropsPositionOriginHMDXY=头显底部位置
tstr_OvrlPropsPositionOriginDashboard=主面板
tstr_OvrlPropsPositionOriginHMD=头显
tstr_OvrlPropsPositionOriginSeatedSpace=座椅位置
tstr_OvrlPropsPositionOriginControllerR=右手柄
tstr_OvrlPropsPositionOriginControllerL=左手柄
tstr_OvrlPropsPositionOriginTheaterScreen=影院屏幕
tstr_OvrlPropsPositionOriginTracker1=追踪器 #1
tstr_OvrlPropsPositionOriginConfigHMDXYTurning=随头显一起转动
tstr_OvrlPropsPositionOriginConfigTheaterScreenEnter=进入影院模式
tstr_OvrlPropsPositionOriginConfigTheaterScreenLeave=退出影院模式
tstr_OvrlPropsPositionOriginTheaterScreenTip=部分属性由 SteamVR 影院屏幕控制
tstr_OvrlPropsPositionDispMode=显示模式
tstr_OvrlPropsPositionDispModeAlways=始终
tstr_OvrlPropsPositionDispModeDashboard=仅在主面板
tstr_OvrlPropsPositionDispModeScene=仅在游戏中
tstr_OvrlPropsPositionDispModeDPlus=仅在 Desktop+ 标签页中
tstr_OvrlPropsPositionPos=位置
tstr_OvrlPropsPositionPosTip=只有在 Desktop+ 正在运行时才能更改或重置位置
tstr_OvrlPropsPositionChange=更改
tstr_OvrlPropsPositionReset=重置
tstr_OvrlPropsPositionLock=锁定
tstr_OvrlPropsPositionChangeHeader=更改叠加位置
tstr_OvrlPropsPositionChangeHelp=拖动任意叠加以更改其位置。\n按住右键可进行双手手势变换。
tstr_OvrlPropsPositionChangeHelpDesktop=按住拖动按钮(“D”)可使用鼠标移动或旋转叠加。
tstr_OvrlPropsPositionChangeManualAdjustment=手动调整
tstr_OvrlPropsPositionChangeMove=移动
tstr_OvrlPropsPositionChangeRotate=旋转
tstr_OvrlPropsPositionChangeForward=前移
tstr_OvrlPropsPositionChangeBackward=后移
tstr_OvrlPropsPositionChangeRollCW=滚动 ⟳
tstr_OvrlPropsPositionChangeRollCCW=滚动 ⟲
tstr_OvrlPropsPositionChangeLookAt=面向头显
tstr_OvrlPropsPositionChangeDragButton=D
tstr_OvrlPropsPositionChangeOffset=附加偏移
tstr_OvrlPropsPositionChangeOffsetUpDown=上下偏移
tstr_OvrlPropsPositionChangeOffsetRightLeft=左右偏移
tstr_OvrlPropsPositionChangeOffsetForwardBackward=前后偏移
tstr_OvrlPropsPositionChangeDragSettings=拖动设置
tstr_OvrlPropsPositionChangeDragSettingsAutoDocking=接近手柄时自动停靠
tstr_OvrlPropsPositionChangeDragSettingsForceDistance=强制固定距离
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShape=形状
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShapeSphere=球
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceShapeCylinder=圆柱
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceAutoCurve=自动曲面
tstr_OvrlPropsPositionChangeDragSettingsForceDistanceAutoTilt=自动倾斜
tstr_OvrlPropsPositionChangeDragSettingsSnapPosition=位置贴合
tstr_OvrlPropsAppearanceWidth=宽度
tstr_OvrlPropsAppearanceCurve=曲率
tstr_OvrlPropsAppearanceOpacity=透明度
tstr_OvrlPropsAppearanceBrightness=亮度
tstr_OvrlPropsAppearanceCrop=裁剪
tstr_OvrlPropsAppearanceCropValueMax=最大
tstr_OvrlPropsCrop=裁剪区域
tstr_OvrlPropsCropHelp=拖动矩形以调整裁剪范围。\n使用滚轮或拖动边缘来调整裁剪矩形的大小。
tstr_OvrlPropsCropManualAdjust=手动调节
tstr_OvrlPropsCropInvalidTip=当前裁剪矩形无效,可能导致叠加不可见。
tstr_OvrlPropsCropX=X
tstr_OvrlPropsCropY=Y
tstr_OvrlPropsCropWidth=宽度
tstr_OvrlPropsCropHeight=高度
tstr_OvrlPropsCropToWindow=裁剪到当前活动窗口
tstr_OvrlPropsCaptureMethod=捕获方式
tstr_OvrlPropsCaptureMethodDup=桌面复制
tstr_OvrlPropsCaptureMethodGC=图形捕获
tstr_OvrlPropsCaptureMethodGCUnsupportedTip=此系统不支持图形捕获
tstr_OvrlPropsCaptureMethodGCUnsupportedPartialTip=此系统不支持部分图形捕获功能
tstr_OvrlPropsCaptureSource=来源
tstr_OvrlPropsCaptureGCSource=图形捕获来源
tstr_OvrlPropsCaptureSourceUnknownWarning=此叠加使用了未知的捕获来源,可能无法正常工作
tstr_OvrlPropsCaptureGCStrictMatching=使用严格窗口匹配
tstr_OvrlPropsCaptureGCStrictMatchingTip=在恢复此叠加的捕获时,仅允许精确匹配窗口标题
tstr_OvrlPropsPerfMonDesktopModeTip=性能监控叠加在桌面模式下不会更新
tstr_OvrlPropsPerfMonGlobalTip=以下设置应用于所有性能监控叠加
tstr_OvrlPropsPerfMonStyle=样式
tstr_OvrlPropsPerfMonStyleCompact=紧凑
tstr_OvrlPropsPerfMonStyleLarge=大型
tstr_OvrlPropsPerfMonShowCPU=显示 CPU 状态
tstr_OvrlPropsPerfMonShowGPU=显示 GPU 状态
tstr_OvrlPropsPerfMonShowGraphs=显示图表
tstr_OvrlPropsPerfMonShowFrameStats=显示帧信息
tstr_OvrlPropsPerfMonShowTime=显示时间
tstr_OvrlPropsPerfMonShowBattery=显示电池信息
tstr_OvrlPropsPerfMonShowTrackerBattery=显示追踪器电量
tstr_OvrlPropsPerfMonShowViveWirelessTemp=显示 Vive 无线温度
tstr_OvrlPropsPerfMonResetValues=重置累计数值
tstr_OvrlPropsBrowserNotAvailableTip=未安装 Desktop+ 浏览器组件
tstr_OvrlPropsBrowserCloned=克隆输出
tstr_OvrlPropsBrowserClonedTip=此叠加克隆自 "%OVERLAYNAME%"。\n对浏览器属性所做的更改将同时应用到原叠加及所有从其克隆的叠加。
tstr_OvrlPropsBrowserClonedConvert=转换为独立叠加
tstr_OvrlPropsBrowserURL=URL
tstr_OvrlPropsBrowserURLHint=输入地址
tstr_OvrlPropsBrowserGo=前往
tstr_OvrlPropsBrowserRestore=恢复上次输入
tstr_OvrlPropsBrowserWidth=宽度
tstr_OvrlPropsBrowserHeight=高度
tstr_OvrlPropsBrowserZoom=缩放
tstr_OvrlPropsBrowserAllowTransparency=允许透明
tstr_OvrlPropsBrowserAllowTransparencyTip=允许网页使用透明背景。\n如果网站未定义任何背景颜色,页面可能会显示异常。
tstr_OvrlPropsBrowserRecreateContext=重新创建浏览器上下文
tstr_OvrlPropsBrowserRecreateContextTip=需要重新创建浏览器上下文才能应用更改。\n此操作会重新加载页面并清除导航历史记录。
tstr_OvrlPropsAdvanced3D=3D
tstr_OvrlPropsAdvancedHSBS=左右半宽
tstr_OvrlPropsAdvancedSBS=左右并列
tstr_OvrlPropsAdvancedHOU=上下半高
tstr_OvrlPropsAdvancedOU=上下并列
tstr_OvrlPropsAdvanced3DSwap=交换左右眼
tstr_OvrlPropsAdvancedGazeFade=视线淡出
tstr_OvrlPropsAdvancedGazeFadeAuto=自动配置
tstr_OvrlPropsAdvancedGazeFadeDistance=距离
tstr_OvrlPropsAdvancedGazeFadeDistanceValueInf=无限
tstr_OvrlPropsAdvancedGazeFadeSensitivity=灵敏度
tstr_OvrlPropsAdvancedGazeFadeOpacity=目标透明度
tstr_OvrlPropsAdvancedInput=激光指针输入
tstr_OvrlPropsAdvancedInputInGame=游戏中启用
tstr_OvrlPropsAdvancedInputFloatingUI=显示浮动 UI
tstr_OvrlPropsAdvancedOverlayTags=叠加标签
tstr_OvrlPropsAdvancedOverlayTagsTip=叠加标签用于在操作中定位叠加
tstr_OvrlPropsPerformanceInvisibleUpdate=在不可见时仍更新
tstr_OvrlPropsPerformanceInvisibleUpdateTip=即使叠加因透明度设置或视线淡出而不可见时,也继续更新叠加。\n有助于第三方应用程序访问叠加内容。\n除特殊需求外,不建议启用。\n如果叠加是被手动隐藏或因显示模式而被隐藏,更新仍会暂停。
tstr_OvrlPropsInterfaceOverlayName=叠加名称
tstr_OvrlPropsInterfaceOverlayNameAuto=[自动命名]
tstr_OvrlPropsInterfaceActionOrderCustom=覆写操作按钮
tstr_OvrlPropsInterfaceDesktopButtons=显示桌面按钮
tstr_OvrlPropsInterfaceExtraButtons=显示额外按钮
;叠加栏
tstr_OverlayBarOvrlHide=隐藏
tstr_OverlayBarOvrlShow=显示
tstr_OverlayBarOvrlClone=克隆
tstr_OverlayBarOvrlRemove=移除
tstr_OverlayBarOvrlRemoveConfirm=确定删除?
tstr_OverlayBarOvrlProperties=属性...
tstr_OverlayBarOvrlAddWindow=窗口...
tstr_OverlayBarTooltipOvrlAdd=添加叠加
tstr_OverlayBarTooltipSettings=设置
tstr_OverlayBarTooltipResetHold=长按以重置窗口位置...
;浮动界面
tstr_FloatingUIHideOverlayTip=隐藏叠加
tstr_FloatingUIHideOverlayHoldTip=长按以移除叠加...
tstr_FloatingUIDragModeEnableTip=启用拖动模式
tstr_FloatingUIDragModeDisableTip=禁用拖动模式
tstr_FloatingUIDragModeHoldLockTip=长按以锁定叠加位置...
tstr_FloatingUIDragModeHoldUnlockTip=长按以解锁叠加位置...
tstr_FloatingUIWindowAddTip=将当前活动窗口添加为叠加
tstr_FloatingUIActionBarShowTip=显示操作栏
tstr_FloatingUIActionBarHideTip=隐藏操作栏
tstr_FloatingUIBrowserGoBackTip=前往上一页
tstr_FloatingUIBrowserGoForwardTip=前往下一页
tstr_FloatingUIBrowserRefreshTip=刷新当前页面
tstr_FloatingUIBrowserStopTip=停止加载页面
tstr_FloatingUIActionBarDesktopPrev=上一个桌面
tstr_FloatingUIActionBarDesktopNext=下一个桌面
tstr_FloatingUIActionBarEmpty=未启用任何操作
;特殊操作
tstr_ActionNone=[无]
tstr_ActionKeyboardShow=显示键盘
tstr_ActionKeyboardHide=隐藏键盘
;默认操作(翻译 ID 与操作名称匹配)
tstr_DefActionShowKeyboard=显示键盘
tstr_DefActionActiveWindowCrop=裁剪至活动窗口
tstr_DefActionActiveWindowCropLabel=裁剪至\n活动\n窗口
tstr_DefActionSwitchTask=切换任务
tstr_DefActionToggleOverlays=切换叠加
tstr_DefActionToggleOverlaysLabel=切换\n叠加
tstr_DefActionMiddleMouse=鼠标中键
tstr_DefActionMiddleMouseLabel=鼠标\n中键
tstr_DefActionBackMouse=鼠标后退键
tstr_DefActionBackMouseLabel=鼠标\n后退\n键
tstr_DefActionReadMe=打开 ReadMe
tstr_DefActionReadMeLabel=打开\nReadMe
tstr_DefActionDashboardToggle=切换 SteamVR 主面板(调试命令)
tstr_DefActionDashboardToggleLabel=切换\n主面板
;性能监控(界面可用空间很有限,尽量简短或不翻译)
tstr_PerformanceMonitorCPU=CPU
tstr_PerformanceMonitorGPU=GPU
tstr_PerformanceMonitorRAM=内存:
tstr_PerformanceMonitorVRAM=显存:
tstr_PerformanceMonitorFrameTime=帧时间:
tstr_PerformanceMonitorLoad=负载:
tstr_PerformanceMonitorFPS=FPS:
tstr_PerformanceMonitorFPSAverage=平均 FPS:
tstr_PerformanceMonitorReprojectionRatio=重投比例:
tstr_PerformanceMonitorDroppedFrames=丢帧:
tstr_PerformanceMonitorBatteryLeft=左手柄:
tstr_PerformanceMonitorBatteryRight=右手柄:
tstr_PerformanceMonitorBatteryHMD=头显:
tstr_PerformanceMonitorBatteryTracker=追踪器 %ID%:
tstr_PerformanceMonitorBatteryDisconnected=N/A
tstr_PerformanceMonitorViveWirelessTempNotAvailable=N/A
tstr_PerformanceMonitorCompactCPU=CPU
tstr_PerformanceMonitorCompactGPU=GPU
tstr_PerformanceMonitorCompactFPS=FPS
tstr_PerformanceMonitorCompactFPSAverage=AVG
tstr_PerformanceMonitorCompactReprojectionRatio=% RPR
tstr_PerformanceMonitorCompactDroppedFrames=DRP
tstr_PerformanceMonitorCompactBattery=BAT
tstr_PerformanceMonitorCompactBatteryLeft=L
tstr_PerformanceMonitorCompactBatteryRight=R
tstr_PerformanceMonitorCompactBatteryHMD=H
tstr_PerformanceMonitorCompactBatteryTracker=T%ID%
tstr_PerformanceMonitorCompactBatteryDisconnected=N/A
tstr_PerformanceMonitorCompactViveWirelessTempNotAvailable=N/A
tstr_PerformanceMonitorEmpty=未启用任何性能监控项目。
;辅助 UI
tstr_AuxUIDragHintDocking=松开以停靠
tstr_AuxUIDragHintUndocking=松开以取消停靠
tstr_AuxUIDragHintOvrlLocked=解锁叠加位置才能拖动此叠加
tstr_AuxUIDragHintOvrlTheaterScreenBlocked=此叠加的位置由 SteamVR 影院屏幕控制
tstr_AuxUIGazeFadeAutoHint=注视叠加中心并等待 %SECONDS% 秒...
tstr_AuxUIGazeFadeAutoHintSingular=注视叠加中心并等待 %SECONDS% 秒...
tstr_AuxUIQuickStartWelcomeHeader=欢迎使用 Desktop+!
tstr_AuxUIQuickStartWelcomeBody=本简短指南将向您介绍应用程序的基础知识。\n如需更多详细信息,请查看 ReadMe 和用户指南。\n\n如果您想跳过此指南,可直接点击 [关闭]。
tstr_AuxUIQuickStartOverlaysHeader=叠加
tstr_AuxUIQuickStartOverlaysBody=Desktop+ 允许您创建叠加来镜像您的各个桌面、独立窗口等内容。\n所有创建的叠加都会显示在下方的叠加栏中。\n\n单击一个叠加图标并选择 [属性...] 可以修改叠加属性。
tstr_AuxUIQuickStartOverlaysBody2=点击 [+] 并从列表中选择捕获来源或叠加类型可以创建新的叠加。\n某些类型(例如浏览器叠加)仅在安装了相应组件后才可用。\n\n单个叠加或整个布局可以被保存到配置文件。\n当前叠加设置会在不同会话之间自动记住。
tstr_AuxUIQuickStartOverlayPropertiesHeader=叠加属性
tstr_AuxUIQuickStartOverlayPropertiesBody=叠加具有多种自定义选项。\n原点和显示模式决定了叠加显示的位置和时机。\n如果您觉得叠加应该显示却找不到,请检查这些属性。\n若前述方法无效,重置叠加位置也许能解决问题。\n\n您还可以通过将叠加拖拽至运动控制器附近,将其停靠到控制器上。
tstr_AuxUIQuickStartOverlayPropertiesBody2=部分属性默认处于隐藏状态。\n在设置窗口中切换“显示高级设置”即可显示所有选项。\n\n通过长按各自的按钮可以重置 UI 窗口的位置。\n叠加按钮也支持双击和右键单击,作为快捷切换功能。
tstr_AuxUIQuickStartSettingsHeader=设置
tstr_AuxUIQuickStartSettingsBody=设置窗口包含 Desktop+ 的所有全局设置。\n按下叠加栏右端的齿轮按钮即可打开。\n\n与叠加一样,设置的更改会自动应用并保存。
tstr_AuxUIQuickStartProfilesHeader=配置文件
tstr_AuxUIQuickStartProfilesBody=Desktop+ 中有两种配置文件类型。\n\n叠加配置文件:\n存储一个或多个叠加及其属性。\n它们可以被手动加载,也可以根据操作和应用程序配置文件加载。\n\n应用程序配置文件:\n在启动 SteamVR 应用程序时,自动加载指定的叠加配置文件和/或操作。
tstr_AuxUIQuickStartActionsHeader=操作
tstr_AuxUIQuickStartActionsBody=Desktop+ 中的操作是一系列命令,其可以被分配给控制器输入和应用程序配置文件,或添加到叠加的操作栏中。\n这些操作命令包括从模拟桌面输入到调整叠加布局等功能。\n其还可以被用于启动外部应用程序以满足高级用例需求。\n\nDesktop+ 自带一些示例操作供您参考。
tstr_AuxUIQuickStartActionsBody2=默认情况下,操作的目标是被激活的叠加(为叠加显示的操作按钮、叠加上的控制器输入),如果前者不适用,则目标是被聚焦的叠加层。\n\n当前聚焦的叠加是最后一次被点击的叠加。\n在许多情况下,目标叠加并不重要,例如在桌面上模拟输入;但在其他情况下,指定一个不同的目标叠加甚至多个目标叠加可能会有用。
tstr_AuxUIQuickStartOverlayTagsHeader=叠加标签
tstr_AuxUIQuickStartOverlayTagsBody=叠加标签可用于此目的。\n存在自动标签(绿色标签)和用户自定义标签。\n自动标签是基于其所代表的属性自动分配的。\n用户自定义标签可以在叠加属性窗口中手动分配给叠加。\n\n请注意,对于全局快捷键,操作仅与控制器按钮间接绑定。此外,编号的全局快捷键还必须通过 SteamVR 的输入绑定界面分配给控制器输入。
tstr_AuxUIQuickStartSettingsEndBody=还有更多设置可供使用。\n不要害怕尝试各种切换按钮来探索它们。\n\n如果遇到问题,您可以点击窗口底部的 [恢复默认设置] 按钮。\n您还可以选择仅恢复特定的部分。
tstr_AuxUIQuickStartFloatingUIHeader=浮动 UI
tstr_AuxUIQuickStartFloatingUIBody=当您指向某个叠加时,被称为“浮动 UI”的界面会显示出来。\n在主面板中,如果没有指向其他叠加,它将始终显示。\n\n浮动 UI 包含用于修改叠加的基本控件,例如切换拖动模式以允许移动叠加,\n以及一个可自定义的操作按钮的部分。\n\n根据叠加类型,可能还会有其他控件。这些控件的显示可以在各个叠加的属性中进行切换。
tstr_AuxUIQuickStartDesktopModeHeader=桌面模式
tstr_AuxUIQuickStartDesktopModeBody=Desktop+ 也可以在一个桌面窗口上被配置。\n这对于需要频繁使用键盘输入或在未连接运动控制器时进行修改的任务非常有用。\n\n虽然两种模式下几乎所有功能都相同,但用于自定义 Desktop+ VR 键盘布局的键盘布局编辑器仅在桌面模式下可用。\n\n您可以在设置窗口的故障排除部分切换到该模式,或者通过系统托盘/通知区域中的 Desktop+ 图标进入桌面模式。
tstr_AuxUIQuickStartEndHeader=ReadMe 和用户指南
tstr_AuxUIQuickStartEndBody=这应该足以让您开始使用 Desktop+。\n\n如果您遇到任何问题或困惑,请务必查看 ReadMe。\n操作栏中的按钮将为您打开它。\n每个选项的详细说明以及常见使用场景的逐步指南可以在用户指南中找到。\n\n若要再次查看此指南,请点击“恢复默认设置”页面右下角的 [显示快速入门指南] 按钮。
tstr_AuxUIQuickStartButtonNext=下一页
tstr_AuxUIQuickStartButtonPrev=上一页
tstr_AuxUIQuickStartButtonClose=关闭
;桌面模式
tstr_DesktopModeCatTools=工具
tstr_DesktopModeCatOverlays=叠加
tstr_DesktopModeToolSettings=设置
tstr_DesktopModeToolActions=操作
tstr_DesktopModeOverlayListAdd=添加叠加
tstr_DesktopModePageAddWindowOverlayTitle=添加窗口叠加
tstr_DesktopModePageAddWindowOverlayHeader=选择一个窗口
;键盘编辑器
tstr_KeyboardEditorKeyListTitle=按键列表
tstr_KeyboardEditorKeyListTabContextReplace=替换布局内容...
tstr_KeyboardEditorKeyListTabContextClear=清空子布局
tstr_KeyboardEditorKeyListRow=行 %ID%
tstr_KeyboardEditorKeyListSpacing=[空格]
tstr_KeyboardEditorKeyListKeyAdd=添加
tstr_KeyboardEditorKeyListKeyDuplicate=复制
tstr_KeyboardEditorKeyListKeyRemove=移除
tstr_KeyboardEditorKeyPropertiesTitle=按键属性
tstr_KeyboardEditorKeyPropertiesNoSelection=未选择按键
tstr_KeyboardEditorKeyPropertiesType=类型
tstr_KeyboardEditorKeyPropertiesTypeBlank=空白区域
tstr_KeyboardEditorKeyPropertiesTypeVirtualKey=虚拟按键
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyToggle=虚拟按键(切换)
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyIsoEnter=虚拟按键(ISO-Enter)
tstr_KeyboardEditorKeyPropertiesTypeString=字符串
tstr_KeyboardEditorKeyPropertiesTypeSublayoutToggle=切换子布局
tstr_KeyboardEditorKeyPropertiesTypeAction=操作
tstr_KeyboardEditorKeyPropertiesTypeVirtualKeyIsoEnterTip=在相邻的两行里各使用一个“虚拟按键(ISO-Enter)”即可组合成一个 ISO-Enter 形状的键。\n每个子布局中只能有一个这样的组合键。
tstr_KeyboardEditorKeyPropertiesTypeStringTip=对于非基本字符键,请使用字符串类型。\n这样 Desktop+ 能够基于实际键盘布局计算出正确的按键组合,从而提高应用兼容性。
tstr_KeyboardEditorKeyPropertiesSize=大小
tstr_KeyboardEditorKeyPropertiesLabel=标签
tstr_KeyboardEditorKeyPropertiesKeyCode=按键码
tstr_KeyboardEditorKeyPropertiesString=字符串
tstr_KeyboardEditorKeyPropertiesSublayout=子布局
tstr_KeyboardEditorKeyPropertiesAction=操作
tstr_KeyboardEditorKeyPropertiesCluster=键簇
tstr_KeyboardEditorKeyPropertiesClusterTip=键簇分配可用于有选择地禁用按键的加载。\n周围的空白区域不会被移除。请仔细为空白区域类型的按键分配键簇,以确保它们能正确切换。
tstr_KeyboardEditorKeyPropertiesBlockModifiers=阻止修饰键
tstr_KeyboardEditorKeyPropertiesBlockModifiersTip=在此键按下时,释放所有修饰键
tstr_KeyboardEditorKeyPropertiesNoRepeat=禁止重复
tstr_KeyboardEditorKeyPropertiesNoRepeatTip=即使开启了按键重复,也阻止此键被按住时重复输入
tstr_KeyboardEditorMetadataTitle=布局元数据
tstr_KeyboardEditorMetadataName=名称
tstr_KeyboardEditorMetadataAuthor=作者
tstr_KeyboardEditorMetadataHasAltGr=是否有 AltGr
tstr_KeyboardEditorMetadataHasAltGrTip=当按下右 Alt 键时,会切换到 AltGr 子布局
tstr_KeyboardEditorMetadataClusterPreview=预览键簇:
tstr_KeyboardEditorMetadataSave=保存...
tstr_KeyboardEditorMetadataLoad=加载...
tstr_KeyboardEditorMetadataSavePopupTitle=保存键盘布局
tstr_KeyboardEditorMetadataSavePopupFilename=文件名
tstr_KeyboardEditorMetadataSavePopupFilenameBlankTip=文件名不能为空
tstr_KeyboardEditorMetadataSavePopupConfirm=保存布局
tstr_KeyboardEditorMetadataSavePopupConfirmError=保存布局失败
tstr_KeyboardEditorMetadataLoadPopupTitle=加载键盘布局
tstr_KeyboardEditorMetadataLoadPopupConfirm=加载布局
tstr_KeyboardEditorPreviewTitle=键盘预览
tstr_KeyboardEditorSublayoutBase=基本
tstr_KeyboardEditorSublayoutShift=Shift
tstr_KeyboardEditorSublayoutAltGr=AltGr
tstr_KeyboardEditorSublayoutAux=辅助
;通用对话字符串
tstr_DialogOk=确定
tstr_DialogCancel=取消
tstr_DialogDone=完成
tstr_DialogUndo=撤销
tstr_DialogRedo=重做
tstr_DialogColorPickerHeader=选择颜色
tstr_DialogColorPickerCurrent=当前
tstr_DialogColorPickerOriginal=原始
tstr_DialogProfilePickerHeader=选择配置文件
tstr_DialogProfilePickerNone=[无]
tstr_DialogActionPickerHeader=选择操作
tstr_DialogActionPickerEmpty=暂无可用操作
tstr_DialogIconPickerHeader=选择图标
tstr_DialogIconPickerHeaderTip=可以在“images\icons\”目录中添加自定义 PNG 图标
tstr_DialogIconPickerNone=[无图标]
tstr_DialogKeyCodePickerHeader=选择按键码
tstr_DialogKeyCodePickerHeaderHotkey=选择热键
tstr_DialogKeyCodePickerModifiers=修饰键
tstr_DialogKeyCodePickerKeyCode=按键码
tstr_DialogKeyCodePickerKeyCodeHint=搜索列表
tstr_DialogKeyCodePickerKeyCodeNone=[无]
tstr_DialogKeyCodePickerFromInput=从输入获取...
tstr_DialogKeyCodePickerFromInputPopup=请按任意键或鼠标按键...
tstr_DialogKeyCodePickerFromInputPopupNoMouse=请按任意键...
tstr_DialogWindowPickerHeader=选择窗口
tstr_DialogInputTagsHint=搜索或添加新标签
;来源字符串
tstr_SourceDesktopAll=合并桌面
tstr_SourceDesktopID=桌面 %ID%
tstr_SourceWinRTNone=[无捕获目标]
tstr_SourceWinRTUnknown=[未知窗口]
tstr_SourceWinRTClosed=[已关闭]:
tstr_SourcePerformanceMonitor=性能监控
tstr_SourceBrowser=浏览器
tstr_SourceBrowserNoPage=[未加载页面]
;通知图标
tstr_NotificationIconRestoreVR=恢复 VR 界面
tstr_NotificationIconOpenOnDesktop=在桌面打开设置
tstr_NotificationIconQuit=退出
;通知
tstr_NotificationInitialStartupTitleVR=初始设置
tstr_NotificationInitialStartupTitleDesktop=Desktop+ 初始设置
tstr_NotificationInitialStartupMessage=Desktop+ 已成功加入 SteamVR,并将在每次启动 SteamVR 时自动运行。
;浏览器
tstr_BrowserErrorPageTitle=页面加载错误 - Desktop+
tstr_BrowserErrorPageHeading=无法加载此页面
tstr_BrowserErrorPageMessage=尝试加载 %URL% 时发生错误:%ERROR%
================================================
FILE: assets/license.txt
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
================================================
FILE: assets/manifest.vrmanifest
================================================
{
"source": "builtin",
"applications": [
{
"app_key": "steam.overlay.1494460",
"launch_type": "binary",
"binary_path_windows": "DesktopPlus.exe",
"is_dashboard_overlay": true,
"action_manifest_path" : "action_manifest.json",
"image_path" : "images/desktop_plus_capsule.png",
"strings": {
"en_us": {
"name": "Desktop+",
"description": "Desktop+ Overlay"
}
}
},
{
"app_key": "elvissteinjr.DesktopPlusTheaterScreen",
"launch_type": "binary",
"binary_path_windows": "DesktopPlus.exe",
"starts_theater_mode": true,
"is_hidden": true,
"is_internal": true,
"strings": {
"en_us": {
"name": "Desktop+ Theater Screen",
"description": "Desktop+ Theater Screen"
}
}
}]
}
================================================
FILE: assets/misc/!About this folder.txt
================================================
This folder contains scripts enabling Desktop+ to gain higher access rights.
The scripts are provided for convenience. Changes made by these scripts are inherently unsafe due to SteamVR running from an
user-writable location among other things.
! Use your own judgment before running any of them. Don't use them if you don't know what you're doing.
! The author of Desktop+ shall not be liable for any damage caused by the use of these scripts.
The .ps1 PowerShell scripts require to be run as administrator. The .bat files will try do that for you, so it's recommended to
use those instead.
Make sure Desktop+ is not running before executing them.
CreateElevatedTask.ps1:
-
Creates a scheduled task to enable use of Desktop+'s elevated mode.
Elevated mode allows inputs and actions to be executed with administrator privileges, getting around most restrictions of User
Interface Privilege Isolation (UIPI), which it is subject to otherwise.
After running this script, elevated mode can be accessed via [Misc|Troubleshooting|Desktop+: Enter Elevated Mode].
If you move Desktop+'s files, the scheduled task will break but the button remains. Re-run the script to fix this.
Note that actions launching applications will run them with the same privileges as Desktop+, so be careful if you don't intend
them to have administrator rights.
Leave elevated mode again as soon as possible.
EnableUIAccess.ps1:
-
Enables UIAccess rights for Desktop+ to lift interaction restrictions with higher privileged applications and UAC prompts.
This is done by signing the DesktopPlus.exe executable with a self-signed certificate, using a different side-by-side manifest
and changing group policies if needed.
Compared to elevated mode, this enables Desktop+ to simulate input on any applications, including UAC prompts, at any time while
not running with administrator privileges.
This script needs to be run again after Desktop+ has been updated or otherwise modified.
The following group policy is changed by the script:
- "User Account Control: Only elevate UIAccess applications that are installed in secure locations" (set to 0) (only if needed)
In order for Desktop+ to be able to mirror and interact with UAC prompts, the UAC level has to be lowered to not display the
prompts on the secure desktop ("do not dim my desktop").
The elevated scheduled task has to be recreated after enabling UIAccess for the first time or entering elevated mode will fail.
UndoSystemChanges.ps1:
-
Reverts system-wide changes made by above scripts.
The script does not revert changes made to group policies as it may impact other applications.
================================================
FILE: assets/misc/CreateElevatedTask.bat
================================================
@echo off
pushd %~dp0
echo Launching script with administrator privileges...
powershell -command " Start-Process PowerShell -Verb RunAs \""-ExecutionPolicy Bypass -Command `\""cd '%cd%'; & '.\CreateElevatedTask.ps1';`\""\"" "
================================================
FILE: assets/misc/CreateElevatedTask.ps1
================================================
#Requires -RunAsAdministrator
$msg = "Desktop+ Elevated Scheduled Task Creation Script
------------------------------------------------
This creates an elevated scheduled task for Desktop+ to allow entering elevated mode without UAC prompt to simulate inputs with administrator rights.
Desktop+ will only use this task when the button in the UI is used to enter elevated mode.
Please keep the security implications of doing this in mind.
At the very least consider restricting write access to the Desktop+ executable file.
"
Write-Host $msg
#Ask before continuing
$opt_yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Create scheduled task and enable elevated mode for Desktop+"
$opt_no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", 'Cancel'
$options = [System.Management.Automation.Host.ChoiceDescription[]]($opt_yes, $opt_no)
$result = $host.ui.PromptForChoice("", "Continue and create the scheduled task?", $options, 1)
if ($result -eq 1)
{
exit
}
#Apply the changes
#Command and arguments for the scheduled task
$command = $PSScriptRoot + "\..\DesktopPlus.exe"
$arg = "-ElevatedMode"
#Check if UIAccess is currently enabled
$sel = Select-String -Path "..\DesktopPlus.exe.manifest" -Pattern "uiAccess=`"true`""
#String found means UIAccess is enabled
if ($sel -ne $null)
{
#Change the command and arguments to use cmd as a workaround since the elevated scheduled task can't be run directly when UIAccess is enabled
$command = "cmd"
$arg = "/c `"start DesktopPlus.exe -ElevatedMode`""
}
#We use an XML file with schtasks instead of the Powershell cmdlets as we can create the task from an elevated session without additional password input this way.
$xml = '
Desktop+
This scheduled task is run manually by Desktop+ in order to allow it to be run with adminstrator rights without UAC prompt.
Desktop+ will only use it when the button in the settings is used to switch into elevated mode.
InteractiveToken
HighestAvailable
Parallel
false
false
false
false
false
false
false
true
true
false
false
false
PT0S
7
' + $command + '
' + $arg + '
'+ $PSScriptRoot + '\..
'
#Write XML to file
$xml | Set-Content .\DesktopPlusElevatedTask.xml
#Create task
schtasks /Create /TN "DesktopPlus Elevated" /XML "DesktopPlusElevatedTask.xml" /F
#Delete the XML file after we're done
Remove-Item DesktopPlusElevatedTask.xml
Write-Output "`n"
cmd /c pause
================================================
FILE: assets/misc/EnableUIAccess.bat
================================================
@echo off
pushd %~dp0
echo Launching script with administrator privileges...
powershell -command " Start-Process PowerShell -Verb RunAs \""-ExecutionPolicy Bypass -Command `\""cd '%cd%'; & '.\EnableUIAccess.ps1';`\""\"" "
================================================
FILE: assets/misc/EnableUIAccess.ps1
================================================
#Requires -RunAsAdministrator
$msg = "Desktop+ UIAccess Setup Script
------------------------------
This enables UIAccess rights for Desktop+ to lift interaction restrictions with higher privileged applications and UAC prompts.
This is done by signing the DesktopPlus.exe executable with a self-signed certificate, using a different side-by-side manifest and changing group policies if needed.
The private key of the certificate created in this script is deleted afterwards and does not remain on the system.
Nevertheless it's recommended to inspect what this script does and use your own judgement before just running this.
The script needs to be run again each time Desktop+ has been updated or otherwise modified.
"
Write-Host $msg
#Ask before continuing
$opt_yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Apply changes and enable UIAccess for Desktop+"
$opt_no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", 'Cancel'
$options = [System.Management.Automation.Host.ChoiceDescription[]]($opt_yes, $opt_no)
$result = $host.ui.PromptForChoice("", "Continue and apply these changes?", $options, 1)
if ($result -eq 1)
{
exit
}
#Apply the changes
$CertificateSubject = "DesktopPlus UIAccess Script"
#Remove old certificates if they exist
Get-ChildItem Cert:\LocalMachine\Root |
Where-Object { $_.Subject -match $CertificateSubject } |
Remove-Item
Get-ChildItem Cert:\LocalMachine\CA |
Where-Object { $_.Subject -match $CertificateSubject } |
Remove-Item
#Create the new self-signed certificate
$cert = New-SelfSignedCertificate -CertStoreLocation Cert:\LocalMachine\My -Type CodeSigningCert -Subject $CertificateSubject -NotAfter (Get-Date).AddYears(5)
#Export the certificate to import it again as a root certificate, but without the private key
Export-Certificate -Cert $cert -FilePath ".\DesktopPlusCert.cer" | Out-Null
Import-Certificate -FilePath ".\DesktopPlusCert.cer" -CertStoreLocation "Cert:\LocalMachine\Root" | Out-Null
#Sign DesktopPlus.exe
Set-AuthenticodeSignature -FilePath "..\DesktopPlus.exe" -Certificate $cert | Out-Null
#Backup normal side-by-side manifest (doesn't overwrite if already exists)
Move-Item "..\DesktopPlus.exe.manifest" -Destination "EnableUIAccessDesktopPlusBackup.manifest" -ErrorAction SilentlyContinue
#Replace side-by-side manifest with the UIAccess enabled one
Copy-Item "EnableUIAccessDesktopPlus.manifest" -Destination "..\DesktopPlus.exe.manifest"
#Disable "User Account Control: Only elevate UIAccess applications that are installed in secure locations" group policy if we are outside of the Program Files directory
if (!$pwd.path.StartsWith($env:ProgramFiles))
{
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" -Name "EnableSecureUIAPaths" -Value 0
}
#Cleanup
Remove-Item ".\DesktopPlusCert.cer"
#We do not leave the certificate with the private key on the system. Signing with it was a one-time thing.
Set-Location "Cert:"
$cert | Remove-Item -DeleteKey
Write-Host "Done."
Write-Output "`n"
cmd /c pause
================================================
FILE: assets/misc/EnableUIAccessDesktopPlus.manifest
================================================
True/PM
================================================
FILE: assets/misc/UndoSystemChanges.bat
================================================
@echo off
pushd %~dp0
echo Launching script with administrator privileges...
powershell -command " Start-Process PowerShell -Verb RunAs \""-ExecutionPolicy Bypass -Command `\""cd '%cd%'; & '.\UndoSystemChanges.ps1';`\""\"" "
================================================
FILE: assets/misc/UndoSystemChanges.ps1
================================================
#Requires -RunAsAdministrator
$msg = "Desktop+ Cleanup Script
-----------------------
This reverts system-wide changes made by the scripts in the `"misc`" folder of Desktop+.
It removes the following:
- `"DesktopPlus Elevated`" scheduled task
- `"DesktopPlus UIAccess Script`" self-signed certificates
- Desktop+ UIAccess side-by-side manifest
Not reverted are changes which may impact other applications or may not have been different from the start, such as:
- `"User Account Control: Only elevate UIAccess applications that are installed in secure locations`" group policy
"
Write-Host $msg
#Ask before continuing
$opt_yes = New-Object System.Management.Automation.Host.ChoiceDescription "&Yes", "Undo changes made by Desktop+ scripts"
$opt_no = New-Object System.Management.Automation.Host.ChoiceDescription "&No", 'Cancel'
$options = [System.Management.Automation.Host.ChoiceDescription[]]($opt_yes, $opt_no)
$result = $host.ui.PromptForChoice("", "Continue and undo these changes?", $options, 1)
if ($result -eq 1)
{
exit
}
#Delete scheduled task if it exists
schtasks /Query /TN "DesktopPlus Elevated" *>$null
if ($LastExitCode -eq 0)
{
schtasks /Delete /TN "DesktopPlus Elevated" /F
}
#Remove certificates
Get-ChildItem Cert:\LocalMachine\Root |
Where-Object { $_.Subject -match $CertificateSubject } |
Remove-Item
Get-ChildItem Cert:\LocalMachine\CA |
Where-Object { $_.Subject -match $CertificateSubject } |
Remove-Item
#Restore previously backed-up side-by-side manifest if it exists
if (Test-Path -Path "EnableUIAccessDesktopPlusBackup.manifest")
{
Move-Item "EnableUIAccessDesktopPlusBackup.manifest" -Destination "..\DesktopPlus.exe.manifest" -Force
}
Write-Host "Done."
Write-Output "`n"
cmd /c pause
================================================
FILE: assets/profiles/Sample Profile.ini
================================================
[Overlay0]
Name=Dashboard
NameIsCustom=true
Enabled=true
DesktopID=0
CaptureSource=0
Width=165
Curvature=17
Opacity=100
OffsetRight=0
OffsetUp=0
OffsetForward=0
DisplayMode=3
Origin=Dashboard
CroppingEnabled=false
CroppingX=0
CroppingY=0
CroppingWidth=-1
CroppingHeight=-1
3DEnabled=false
3DMode=0
3DSwapped=false
GazeFade=false
GazeFadeDistance=0
GazeFadeRate=100
GazeFadeOpacity=0
UpdateLimitModeOverride=0
UpdateLimitMS=0
UpdateLimitFPS=7
InputEnabled=true
InputDPlusLPEnabled=true
GroupID=0
UpdateInvisible=false
ShowFloatingUI=true
ShowDesktopButtons=true
ShowActionBar=true
ActionBarOrderUseGlobal=true
Transform=[2.12766 0 0 0 0 2.12766 0 0 0 0 2.12766 0 0 0 0 1]
WinRTLastWindowTitle=
WinRTLastWindowClassName=
WinRTLastWindowExeName=
WinRTDesktopID=-2
ActionBarOrderCustom=1 1;2 0;1000 1;1001 0;1002 0;1003 1;3 0;4 0;5 0;
[Overlay1]
Name=Dashboard 🡄
NameIsCustom=true
Enabled=true
DesktopID=0
CaptureSource=0
Width=100
Curvature=0
Opacity=100
OffsetRight=0
OffsetUp=0
OffsetForward=0
DisplayMode=3
Origin=Dashboard
CroppingEnabled=false
CroppingX=0
CroppingY=0
CroppingWidth=-1
CroppingHeight=-1
3DEnabled=false
3DMode=0
3DSwapped=false
GazeFade=false
GazeFadeDistance=0
GazeFadeRate=100
GazeFadeOpacity=0
UpdateLimitModeOverride=0
UpdateLimitMS=0
UpdateLimitFPS=7
InputEnabled=true
InputDPlusLPEnabled=true
GroupID=0
UpdateInvisible=false
ShowFloatingUI=true
ShowDesktopButtons=true
ShowActionBar=false
ActionBarOrderUseGlobal=true
Transform=[1.84261 0 -1.06383 0 0 2.12766 0 0 1.06383 0 1.84261 0 -2.66135 0.425532 0.922328 1]
WinRTLastWindowTitle=
WinRTLastWindowClassName=
WinRTLastWindowExeName=
WinRTDesktopID=-2
ActionBarOrderCustom=1 1;2 0;1000 1;1001 0;1002 0;1003 1;3 0;4 0;5 0;
[Overlay2]
Name=Dashboard 🡆
NameIsCustom=true
Enabled=true
DesktopID=0
CaptureSource=0
Width=100
Curvature=0
Opacity=100
OffsetRight=0
OffsetUp=0
OffsetForward=0
DisplayMode=3
Origin=Dashboard
CroppingEnabled=false
CroppingX=0
CroppingY=0
CroppingWidth=-1
CroppingHeight=-1
3DEnabled=false
3DMode=0
3DSwapped=false
GazeFade=false
GazeFadeDistance=0
GazeFadeRate=100
GazeFadeOpacity=0
UpdateLimitModeOverride=0
UpdateLimitMS=0
UpdateLimitFPS=7
InputEnabled=true
InputDPlusLPEnabled=true
GroupID=0
UpdateInvisible=false
ShowFloatingUI=true
ShowDesktopButtons=true
ShowActionBar=false
ActionBarOrderUseGlobal=true
Transform=[1.84261 0 1.06383 0 0 2.12766 0 0 -1.06383 0 1.84261 0 2.66135 0.425532 0.922327 1]
WinRTLastWindowTitle=
WinRTLastWindowClassName=
WinRTLastWindowExeName=
WinRTDesktopID=-2
ActionBarOrderCustom=1 1;2 0;1000 1;1001 0;1002 0;1003 1;3 0;4 0;5 0;
[Overlay3]
Name=Performance Monitor
NameIsCustom=false
Enabled=true
DesktopID=0
CaptureSource=2
Width=20
Curvature=0
Opacity=75
OffsetRight=0
OffsetUp=0
OffsetForward=0
DisplayMode=0
Origin=5
CroppingEnabled=false
CroppingX=0
CroppingY=0
CroppingWidth=-1
CroppingHeight=-1
3DEnabled=false
3DMode=0
3DSwapped=false
GazeFade=true
GazeFadeDistance=15
GazeFadeRate=150
GazeFadeOpacity=0
UpdateLimitModeOverride=0
UpdateLimitMS=0
UpdateLimitFPS=7
InputEnabled=false
InputDPlusLPEnabled=true
GroupID=0
UpdateInvisible=false
ShowFloatingUI=true
ShowDesktopButtons=false
ShowActionBar=false
ActionBarOrderUseGlobal=true
Transform=[-0.0495577 0.263826 -0.963298 0 -0.423386 -0.879087 -0.218981 0 -0.904595 0.396994 0.155265 0 -0.104406 0.00957629 0.138933 1]
WinRTLastWindowTitle=
WinRTLastWindowClassName=
WinRTLastWindowExeName=
WinRTDesktopID=-2
ActionBarOrderCustom=1 1;2 0;1000 1;1001 0;1002 0;1003 0;3 0;4 0;5 0;
================================================
FILE: assets/readme.txt
================================================
Desktop+, an advanced SteamVR Desktop Overlay, by elvissteinjr
--------------------------------------------------------------
Installation (GitHub Version)
-----------------------------
Extract the complete archive if not done yet. Put the files in a location where they can stay.
Run DesktopPlus.exe. This will also launch SteamVR if it's not already running. A VR-HMD must be connected.
If everything is fine, a message will come up to indicate successful first-time setup.
Desktop+ will continue running in the background as a SteamVR overlay application afterwards.
If the message does not come up on the first launch, check the Troubleshooting section.
Desktop+ will register itself as an overlay application to SteamVR and run automatically on following SteamVR launches.
If you move the files of this application, you'll have to repeat these steps.
Installation (Steam Version)
----------------------------
Simply install the application through Steam and launch it. This will also launch SteamVR if it's not already running.
A VR-HMD must be connected. If everything is fine, a message will come up to indicate successful first-time setup.
Desktop+ will continue running in the background as a SteamVR overlay application afterwards.
If the message does not come up on the first launch, check the "Startup / Shutdown" settings in SteamVR and the Troubleshooting
section.
Updates
-------
The latest version of Desktop+ can be found on https://github.com/elvissteinjr/DesktopPlus/ and on Steam.
Uninstallation (GitHub Version)
-------------------------------
Delete all files that came with the archive.
SteamVR will automatically remove the overlay application entry when the executable isn't present anymore.
Desktop+ does not write to files outside its own directory. However, if you did set up elevated mode, its scheduled task will
remain.
Uninstallation (Steam Version)
------------------------------
Uninstall the application through Steam.
Steam will not delete your configuration and profiles. They can be found in "[Steam Library Path]\SteamApps\common\DesktopPlus"
and be safely deleted if desired. These files are also synced to the Steam Cloud if that feature is enabled.
Additionally, if you did set up elevated mode, its scheduled task will remain.
User Guide
----------
A detailed user guide can be found on https://github.com/elvissteinjr/DesktopPlus/blob/master/docs/user_guide.md
It's recommended to finish reading this document beforehand, however, as the user guide does not cover some topics this readme
covers.
Configuration
-------------
Desktop+ can be fully configured from within VR. If desired, the settings interface can also be used from the desktop, however.
Either run DesktopPlusUI.exe while SteamVR is not running, press [Restart in Desktop Mode] in the Troubleshooting section of the
Settings window, or run DesktopPlusUI.exe with the "--DesktopMode" command line argument to do so.
Some settings are only available while SteamVR is running.
Settings are applied instantly and written to disk when the settings window is dismissed or Desktop+ closes.
The setting slider values can be edited directly by right-clicking the slider.
Overlay Management
------------------
All overlays in Desktop+ are listed in the Overlay Bar that appears in Desktop+'s SteamVR dashboard tab.
They can be rearranged by dragging the icons across the bar. Clicking on an overlay's icon opens a menu to toggle visibility,
duplicate, remove or open the Overlay Properties window for the selected overlay.
The current overlay setup will be remembered automatically between sessions. Overlay profiles can be used to save and restore
multiple of such setups.
Actions
-------
Desktop+ offers so-called actions which can be bound to controller buttons, the Floating UI or just executed from the list.
Actions consist of a series of commands that can control the state of your overlays, simulate input and execute programs.
Custom icons can be added by putting PNG files in the "images/icons" folder. Recommended size is 96x96 pixels.
Global Shortcuts & Input Features
---------------------------------
Actions can be bound to up to 20 different global shortcuts which can be activated by the SteamVR Input bindings.
SteamVR doesn't list overlay applications in the regular application controller configuration list, but the Settings window in
Desktop+ has buttons that lead directly into the input binding screen for Desktop+.
Apart from the global shortcuts, bindings for Desktop+ laser pointer interaction can also be found.
These mirror SteamVR's default laser pointer bindings for known devices, but can be adjusted as desired.
There's also "Enable Global Laser Pointer", which can be used to enable the laser pointer outside of the dashboard.
However, laser pointer auto-activation is enabled by default, making this binding not strictly necessary to use.
Outside of the dashboard/when SteamVR's system laser pointer is not active, Desktop+ uses its own laser pointer implementation.
This is allows for additional features that can be configured in the Laser Pointer section of the Settings window.
Elevated Mode / Enabling UIAccess
---------------------------------
As Desktop+ is subject to User Interface Privilege Isolation (UIPI), it can't simulate input or move the cursor when a higher
privileged application (i.e. running as administrator) is in focus.
Desktop+ offers multiple ways to deal with this, such as elevated mode or enabling UIAccess for the application.
See "misc\!About this folder.txt" for details.
Keyboard
--------
Desktop+ comes with a custom VR keyboard. The used keyboard layout and behavior can be configured in the "Keyboard" section of
the Settings window.
Keys of the Desktop+ keyboard can be right-clicked to toggle their state. The application will not automatically release keys
held down this way while it's running, so keep that in mind.
Desktop+ tries to map the VR keyboard keys to the keyboard layout chosen in the OS to maximize compatibility, before falling back
to string inputs. It's recommended to select a keyboard layout in Desktop+ that matches the one in the OS.
Keyboard layouts can be created or modified in the Keyboard Layout Editor. This editor can only be accessed in desktop mode.
In desktop mode, the editor can be found in the "Keyboard" section of the Settings window, after clicking on the Keyboard Layout
button, [Switch to Keyboard Layout Editor] on the page that follows.
Troubleshooting
---------------
Desktop+ runs as two processes (DesktopPlus.exe & DesktopPlusUI.exe), both of which write log files in the application's install
directory (DesktopPlus.log & DesktopPlusUI.log).
While most errors will be displayed in VR, it is helpful to check the contents of these log files when troubleshooting.
Make sure to include them when seeking help as well.
In general, note that Desktop+ is using APIs which require Windows 8.1 or newer.
Using Graphics Capture overlays requires at least Windows 10 1903 for basic support, Windows 10 2004 or newer for full support,
and Windows 11 to remove the yellow border around captures (only visible on real display).
Additionally, Windows 11 24H2 allows including secondary windows, such as context menus, in the capture.
No first-time setup message / Desktop+ not auto-launching (Steam version):
-
The Steam version detects the need of first time setup by checking if an user configuration existed on launch.
If you previously had Desktop+ installed or the configuration was synced from another machine via Steam Cloud, you can enable
auto-launch manually in the "SteamVR" section of the Settings window or use [Restore Default Settings] in the "Troubleshooting"
section to start fresh.
Black screen with question mark display icon instead of desktop mirror:
-
An error occurred trying to duplicate the desktop. This may happen when displays were disconnected or are unavailable for another
reason. You can try restarting Desktop+ or changing the capture method to Graphics Capture in the Overlay Properties window.
Shaky/Delayed laser pointer:
-
By default, the laser pointed cursor may seem to lag behind a little bit, while other screen updates happen instantly.
This is in order to reduce the CPU load. Enable "[x] Reduce Laser Pointer Latency" in the "Performance" section of the Settings
window to increase the accuracy of the laser pointer.
High GPU load when overlay visible and cursor moving:
-
In order to provide the lowest latency possible, all cursor updates are processed instantly, even if they occur more frequently
than the screen's vertical blanks.
Using the Frame Time limiter in "Update Limiter Mode" in the "Performance" section of either the Settings or Overlay Properties
window with a low limit value can reduce the load from cursor movement while leaving other screen updates unaffected.
Using laser pointer after moving real mouse:
-
By default the laser pointer will be deactivated after the physical mouse was moved. Click on the overlay to activate it again or
disable "[x] Allow Laser Pointer Override" in the "Mouse" section of the Settings window to turn this feature off.
Input not working in certain applications:
-
Input simulated by Desktop+ is subject to User Interface Privilege Isolation (UIPI), see the Elevated Mode readme section for a
workaround.
Overlay is no longer visible:
-
There are several settings controlling overlay visibility and position. Check if they are not set to unexpected values.
Especially of interest are the cropping values. The cropping rectangle is preserved when switching between capture sources, but
that also means it could be invalid for the newly selected mirrored window or desktop.
If that's the case there will be a "(!)" warning next to the Cropping Rectangle section title. Simply reset it then.
Oculus/Meta headsets:
If all overlays, including the SteamVR dashboard disappear, this may be due to running a game that uses the headset's native APIs
and as such bypasses SteamVR entirely.
As Desktop+ relies on SteamVR to function, the only solution to this is to find a way to run the game with SteamVR.
Potential global workaround for this is using Steam Link for Meta Quest or enabling the "Force Use SteamVR" option available in
OpenVR Advanced Settings.
Workarounds for some individual titles also exist, but are beyond the scope of this ReadMe.
No overlays visible on laptop:
-
On laptops with hybrid-GPU solutions, the desktops are typically rendered on the power-saving integrated GPU. Make sure to have
DesktopPlus.exe set to be running on integrated graphics so it can mirror them.
Not all desktops from multiple GPUs available:
-
Desktop+ does not support Desktop Duplication with desktops distributed across multiple GPUs. It only supports copying one GPU's
set of desktops over to the VR-rendering GPU when necessary.
However, the missing desktops are typically still able to be mirrored by using Graphics Capture as the overlay's capture method.
Warnings
--------
Desktop+ may display several warnings in the Settings window. They are mostly informational and can be safely ignored.
Click on warnings to dismiss or not have them show up again.
"Compositor resolution is below 100%! This affects overlay rendering quality.":
-
The resolution of the VR compositor is based on the auto-resolution calculated by SteamVR, regardless of whether this resolution
has been chosen as the VR render resolution or not. There's no official way to change this. The auto-resolution can be increased
by lowering the HMD's refresh rate or getting a faster GPU.
Unofficially, there are tools such as SteamVR-ForceCompositorScale to combat this behavior.
"Overlay render quality is not set to high!":
-
The overlay render quality is a setting in SteamVR. It is recommended to set it to high to improve the visual clarity of the
overlays.
"Desktop+ is running with administrative privileges!":
-
This message serves as a reminder about Desktop+ being elevated.
Using Desktop+'s elevated mode is recommended over running all of Desktop+ with administrative privileges.
"Elevated mode is active!":
-
This message serves as a reminder about Desktop+ being in elevated mode.
Please keep the security implications of that mode in mind and leave it once it's not needed anymore.
"The application profile for [application name] has overridden the current overlay layout. Changes made to overlays are not saved
automatically while it is active.":
-
This message serves as a reminder that an application profile is active.
Automatic saving of overlays is disabled while this is the case. Update the assigned overlay profile if you wish to make
permanent changes that go with the application profile.
"An elevated process has focus! Desktop+ is unable to simulate input right now."
-
User Interface Privilege Isolation (UIPI) prevents Desktop+ from simulating input when an elevated process has focus.
This warning persists until the a window with the same or lesser privilege level has gained focus again.
Clicking on this warning also offers the option to have Desktop+ try to open the task switcher to change the focus to another
window or to enter elevated mode (only if the scheduled task is configured).
"An overlay creation failed!":
-
This message will typically appear with "(Maximum Overlay limit exceeded)" appended. It appears when the total overlay limit
in SteamVR has been exceeded. This limit is not set by Desktop+ and other overlay applications can affect how many overlays can
be created by Desktop+.
While the warning can be safely ignored, the overlays that were attempted to be created will be missing.
If this message appears with a different error status appended, it might be because of a bug in Desktop+ or SteamVR. Please
report it in that case.
"An unexpected error occurred in a Graphics Capture thread!":
-
This message appears when a Graphics Capture thread crashed. One or more overlays using Graphics Capture will not be updated
anymore. While this shouldn't ever happen, this message can be safely ignored if it only appears once. The affected overlays will
need to have their source be set again from either changing it or reloading a profile.
"Desktop+ is no longer running with UIAccess privileges!":
-
This message appears when Desktop+ was previously configured to run with UIAccess privileges but no longer is.
UIAccess is enabled by patching the DesktopPlus.exe executable and as such is not retained across application updates.
Simply redo the process of enabling UIAccess to fix this.
"Browser overlays are being used, but the Desktop+ Browser component is currently not available.":
-
Browser overlays require the optional Desktop+ Browser application component.
It can be installed from https://github.com/elvissteinjr/DesktopPlusBrowser or Steam (as DLC for Desktop+).
"The installed Desktop+ Browser component is incompatible with this version of Desktop+!":
-
The installed version of Desktop+ Browser must be compatible with the running version of Desktop+. While not all updates require
a new version to be installed, it is generally a recommended to use the latest builds of both.
License
-------
Desktop+ is licensed under the GPL 3.0.
For the third-party licenses, see third-party_licenses.txt.
================================================
FILE: assets/third-party_licenses.txt
================================================
Desktop+ includes work of the following third-party projects:
OpenVR, which is covered by the following license:
-
Copyright (c) 2015, Valve Corporation
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
Dear ImGui, which is covered by the following license:
-
The MIT License (MIT)
Copyright (c) 2014-2020 Omar Cornut
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
DXGI Desktop Duplication Sample, which is covered by the following license:
-
The MIT License (MIT)
Copyright (c) Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
--------------------------------------------------------------------------------
Win32CaptureSample, which is covered by the following license:
-
MIT License
Copyright (c) 2019 Robert Mikhayelyan
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
ImPlot, which is covered by the following license:
-
MIT License
Copyright (c) 2020 Evan Pezent
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
--------------------------------------------------------------------------------
Radial Follow Smoothing (specifically RadialFollowCore.cs only), which is covered by the following license:
-
Mozilla Public License, version 2.0
1. Definitions
1.1. “Contributor” means each individual or legal entity that
creates, contributes to the creation of, or owns Covered Software.
1.2. “Contributor Version” means the combination of the
Contributions of others (if any) used by a Contributor and that
particular Contributor’s Contribution.
1.3. “Contribution” means Covered Software of a particular
Contributor.
1.4. “Covered Software” means Source Code Form to which the initial
Contributor has attached the notice in Exhibit A, the Executable
Form of such Source Code Form, and Modifications of such Source Code
Form, in each case including portions thereof.
1.5. “Incompatible With Secondary Licenses” means
that the initial Contributor has attached the notice described in
Exhibit B to the Covered Software; or
that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the terms
of a Secondary License.
1.6. “Executable Form” means any form of the work other than Source
Code Form.
1.7. “Larger Work” means a work that combines Covered Software with
other material, in a separate file or files, that is not Covered
Software.
1.8. “License” means this document.
1.9. “Licensable” means having the right to grant, to the maximum
extent possible, whether at the time of the initial grant or
subsequently, any and all of the rights conveyed by this License.
1.10. “Modifications” means any of the following:
any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered Software;
or
any new file in Source Code Form that contains any Covered Software.
1.11. “Patent Claims” of a Contributor means any patent claim(s),
including without limitation, method, process, and apparatus claims,
in any patent Licensable by such Contributor that would be
infringed, but for the grant of the License, by the making, using,
selling, offering for sale, having made, import, or transfer of
either its Contributions or its Contributor Version.
1.12. “Secondary License” means either the GNU General Public
License, Version 2.0, the GNU Lesser General Public License, Version
2.1, the GNU Affero General Public License, Version 3.0, or any
later versions of those licenses.
1.13. “Source Code Form” means the form of the work preferred for
making modifications.
1.14. “You” (or “Your”) means an individual or a legal entity
exercising rights under this License. For legal entities, “You”
includes any entity that controls, is controlled by, or is under
common control with You. For purposes of this definition, “control”
means (a) the power, direct or indirect, to cause the direction or
management of such entity, whether by contract or otherwise, or (b)
ownership of more than fifty percent (50%) of the outstanding shares
or beneficial ownership of such entity.
2. License Grants and Conditions
2.1. Grants Each Contributor hereby grants You a world-wide,
royalty-free, non-exclusive license:
under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date The licenses granted in Section 2.1 with respect
to any Contribution become effective for each Contribution on the
date the Contributor first distributes such Contribution.
2.3. Limitations on Grant Scope The licenses granted in this Section
2 are the only rights granted under this License. No additional
rights or licenses will be implied from the distribution or
licensing of Covered Software under this License. Notwithstanding
Section 2.1(b) above, no patent license is granted by a Contributor:
for any code that a Contributor has removed from Covered Software;
or
for infringements caused by: (i) Your and any other third party’s
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service
marks, or logos of any Contributor (except as may be necessary to
comply with the notice requirements in Section 3.4).
2.4. Subsequent Licenses No Contributor makes additional grants as a
result of Your choice to distribute the Covered Software under a
subsequent version of this License (see Section 10.2) or under the
terms of a Secondary License (if permitted under the terms of
Section 3.3).
2.5. Representation Each Contributor represents that the Contributor
believes its Contributions are its original creation(s) or it has
sufficient rights to grant the rights to its Contributions conveyed
by this License.
2.6. Fair Use This License is not intended to limit any rights You
have under applicable copyright doctrines of fair use, fair dealing,
or other equivalents.
2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of
the licenses granted in Section 2.1.
3. Responsibilities
3.1. Distribution of Source Form All distribution of Covered
Software in Source Code Form, including any Modifications that You
create or to which You contribute, must be under the terms of this
License. You must inform recipients that the Source Code Form of the
Covered Software is governed by the terms of this License, and how
they can obtain a copy of this License. You may not attempt to alter
or restrict the recipients’ rights in the Source Code Form.
3.2. Distribution of Executable Form If You distribute Covered
Software in Executable Form then:
such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients’ rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work You may create and distribute a
Larger Work under terms of Your choice, provided that You also
comply with the requirements of this License for the Covered
Software. If the Larger Work is a combination of Covered Software
with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient
of the Larger Work may, at their option, further distribute the
Covered Software under the terms of either this License or such
Secondary License(s).
3.4. Notices You may not remove or alter the substance of any
license notices (including copyright notices, patent notices,
disclaimers of warranty, or limitations of liability) contained
within the Source Code Form of the Covered Software, except that You
may alter any license notices to the extent required to remedy known
factual inaccuracies.
3.5. Application of Additional Terms You may choose to offer, and to
charge a fee for, warranty, support, indemnity or liability
obligations to one or more recipients of Covered Software. However,
You may do so only on Your own behalf, and not on behalf of any
Contributor. You must make it absolutely clear that any such
warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for
any liability incurred by such Contributor as a result of warranty,
support, indemnity or liability terms You offer. You may include
additional disclaimers of warranty and limitations of liability
specific to any jurisdiction.
4. Inability to Comply Due to Statute or Regulation
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply
with the terms of this License to the maximum extent possible; and
(b) describe the limitations and the code they affect. Such
description must be placed in a text file included with all
distributions of the Covered Software under this License. Except to
the extent prohibited by statute or regulation, such description
must be sufficiently detailed for a recipient of ordinary skill to
be able to understand it.
5. Termination
5.1. The rights granted under this License will terminate
automatically if You fail to comply with any of its terms. However,
if You become compliant, then the rights granted under this License
from a particular Contributor are reinstated (a) provisionally,
unless and until such Contributor explicitly and finally terminates
Your grants, and (b) on an ongoing basis, if such Contributor fails
to notify You of the non-compliance by some reasonable means prior
to 60 days after You have come back into compliance. Moreover, Your
grants from a particular Contributor are reinstated on an ongoing
basis if such Contributor notifies You of the non-compliance by some
reasonable means, this is the first time You have received notice of
non-compliance with this License from such Contributor, and You
become compliant prior to 30 days after Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a
patent infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor
Version directly or indirectly infringes any patent, then the rights
granted to You by any and all Contributors for the Covered Software
under Section 2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above,
all end user license agreements (excluding distributors and
resellers) which have been validly granted by You or Your
distributors under this License prior to termination shall survive
termination.
6. Disclaimer of Warranty Covered Software is provided under this
License on an “as is” basis, without warranty of any kind, either
expressed, implied, or statutory, including, without limitation,
warranties that the Covered Software is free of defects, merchantable,
fit for a particular purpose or non-infringing. The entire risk as to
the quality and performance of the Covered Software is with You.
Should any Covered Software prove defective in any respect, You (not
any Contributor) assume the cost of any necessary servicing, repair,
or correction. This disclaimer of warranty constitutes an essential
part of this License. No use of any Covered Software is authorized
under this License except under this disclaimer.
7. Limitation of Liability Under no circumstances and under no legal
theory, whether tort (including negligence), contract, or otherwise,
shall any Contributor, or anyone who distributes Covered Software as
permitted above, be liable to You for any direct, indirect, special,
incidental, or consequential damages of any character including,
without limitation, damages for lost profits, loss of goodwill, work
stoppage, computer failure or malfunction, or any and all other
commercial damages or losses, even if such party shall have been
informed of the possibility of such damages. This limitation of
liability shall not apply to liability for death or personal injury
resulting from such party’s negligence to the extent applicable law
prohibits such limitation. Some jurisdictions do not allow the
exclusion or limitation of incidental or consequential damages, so
this exclusion and limitation may not apply to You.
8. Litigation Any litigation relating to this License may be brought
only in the courts of a jurisdiction where the defendant maintains its
principal place of business and such litigation shall be governed by
laws of that jurisdiction, without reference to its conflict-of-law
provisions. Nothing in this Section shall prevent a party’s ability to
bring cross-claims or counter-claims.
9. Miscellaneous This License represents the complete agreement
concerning the subject matter hereof. If any provision of this License
is held to be unenforceable, such provision shall be reformed only to
the extent necessary to make it enforceable. Any law or regulation
which provides that the language of a contract shall be construed
against the drafter shall not be used to construe this License against
a Contributor.
10. Versions of the License
10.1. New Versions Mozilla Foundation is the license steward. Except
as provided in Section 10.3, no one other than the license steward
has the right to modify or publish new versions of this License.
Each version will be given a distinguishing version number.
10.2. Effect of New Versions You may distribute the Covered Software
under the terms of the version of the License under which You
originally received the Covered Software, or under the terms of any
subsequent version published by the license steward.
10.3. Modified Versions If you create software not governed by this
License, and you want to create a new license for such software, you
may create and use a modified version of this License if you rename
the license and remove any references to the name of the license
steward (except to note that such modified license differs from this
License).
10.4. Distributing Source Code Form that is Incompatible With
Secondary Licenses If You choose to distribute Source Code Form that
is Incompatible With Secondary Licenses under the terms of this
version of the License, the notice described in Exhibit B of this
License must be attached.
Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a
LICENSE file in a relevant directory) where a recipient would be
likely to look for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - “Incompatible With Secondary Licenses” Notice
This Source Code Form is “Incompatible With Secondary Licenses”, as
defined by the Mozilla Public License, v. 2.0.
--------------------------------------------------------------------------------
================================================
FILE: docs/user_guide.md
================================================
# Desktop+ User Guide
This document explains the options in Desktop+ and possible usage scenarios in greater detail. It is generally not required to be read in order to get started with Desktop+, but can serve as a reference if one does get stuck.
This guide does not touch upon all topics covered by the [readme file](../assets/readme.txt). It's recommended to read it beforehand.
# Contents
* [Glossary](#glossary)
* [Interface](#interface)
* [Overlay Bar](#overlay-bar)
* [Floating UI](#floating-ui)
* [Main Bar](#main-bar)
* [Action Bar](#action-bar)
* [Settings Window](#settings-window)
* [Main Page](#main-page)
* [Interface](#interface)
* [Environment](#environment)
* [Profiles](#profiles)
* [Actions](#actions)
* [Keyboard](#keyboard)
* [Mouse](#mouse)
* [Laser Pointer](#laser-pointer)
* [Window Overlays](#window-overlays)
* [Browser](#browser)
* [Performance](#performance)
* [Version Info](#version-info)
* [Warnings](#warnings)
* [Startup](#startup)
* [Troubleshooting](#troubleshooting)
* [Persistent UI Page](#persistent-ui-page)
* [Windows](#windows)
* [Manage Overlay Profiles Page](#manage-overlay-profiles-page)
* [Manage Application Profiles Page](#manage-application-profiles-page)
* [Manage Actions Page](#manage-actions-page)
* [Edit Action Page](#manage-actions-page)
* [Button Appearance](#button-appearance)
* [Commands](#commands)
* [Change Action Order Page](#change-action-order-page)
* [Keyboard Layout Page](#keyboard-layout-page)
* [Key Clusters](#key-clusters)
* [Restore Default Settings Page](#restore-default-settings-page)
* [Overlay Properties](#overlay-properties)
* [Main Page](#main-page-1)
* [Position](#position)
* [Appearance](#appearance)
* [Capture](#capture)
* [Performance Monitor](#performance-monitor)
* [Browser](#browser-1)
* [Advanced](#advanced)
* [Performance](#performance-1)
* [Interface](#interface-2)
* [Change Overlay Position Page](#change-overlay-position-page)
* [Manual Adjustment](#manual-adjustment)
* [Additional Offset](#additional-offset)
* [Drag Settings](#drag-settings)
* [Cropping Area Page](#cropping-area-page)
* [Manual Adjustment](#manual-adjustment-1)
* [Desktop Mode](#desktop-mode)
* [Tools](#tools)
* [Overlays](#overlays)
* [Keyboard Controls](#keyboard-controls)
* [Desktop+ Keyboard](#desktop-keyboard)
* [Keyboard Layout Editor](#keyboard-layout-editor)
* [Key List](#key-list)
* [Key Properties](#key-properties)
* [Layout Metadata](#layout-metadata)
* [Keyboard Preview](#keyboard-preview)
* [Performance Considerations](#performance-considerations)
* [Desktop Duplication](#desktop-duplication-1)
* [Graphics Capture](#graphics-capture-1)
* [Desktop Duplication vs. Graphics Capture](#desktop-duplication-vs-graphics-capture)
* [Desktop Duplication](#desktop-duplication-2)
* [Mirroring at Unconstrained Frame-Rates](#mirroring-at-unconstrained-frame-rates)
* [Graphics Capture](#graphics-capture-2)
* [Graphics Capture Feature Support](#graphics-capture-feature-support)
* [VR Interactions](#vr-interactions)
* [Usage Examples](#usage-examples)
* [Attach Overlay to a Motion-Controller, Wristwatch-Style](#attach-overlay-to-a-motion-controller-wristwatch-style)
* [Show Multiple Desktops or Windows in the Dashboard at Once](#show-multiple-desktops-or-windows-in-the-dashboard-at-once)
* [Simulate Keyboard Shortcut from Motion-Controller Input](#simulate-keyboard-shortcut-from-motion-controller-input)
* [Advanced Features](#advanced-features)
* [Hidden Configuration Settings](#hidden-configuration-settings)
* [System-wide Modifications](#system-wide-modifications)
* [Command-Line Arguments](#command-line-arguments)
* [Desktop+ Browser](#desktop-browser)
# Glossary
These are some of the terms used across the application and its documentation.
### Overlay
An overlay in the context of SteamVR refers to a 2D surface displayed either in the SteamVR dashboard or somewhere in the VR space.
In Desktop+, "overlay" usually only refers to overlays that belong to the application, unless it's explicitly spelled out as "SteamVR overlay".
### Dashboard
Dashboard in the context of SteamVR refers to the menu that appears when using the system menu button in SteamVR.
### Floating UI
An interface fading in when hovering an overlay, consisting of a Main Bar with quick access to disabling the overlay, toggling drag-mode, overlay-specific buttons and showing an additional Action Bar with customizable buttons.
### Action
Action in the context of Desktop+ refers to a series of commands that can control the ,state of your overlays simulate input and execute programs.
Actions can be user-created and bound to controller buttons, the Floating UI, or just executed from the action list.
### Desktop Duplication
One of the capture methods available for capturing desktops for overlay display. Its name comes from the DXGI Desktop Duplication API it is using.
### Graphics Capture
One of the capture methods available for capturing desktops and windows for overlay display. Its name comes from the Graphics Capture API it's using.
### Gaze Fade
A method to automatically adjust an overlay's opacity based on the user's gaze. The method in Desktop+ is distance-based, as opposed to other angle-based approaches.
### Browser Overlay
Overlays using the Desktop+ Browser component, a Chromium Embedded Framework-based internal web browser.
This application component is optional and has to be installed separately, either from https://github.com/elvissteinjr/DesktopPlusBrowser or Steam (as DLC for Desktop+).
Options related to browser overlays are hidden if the application component is not installed.
### Primary Dashboard Overlay
The first visible overlay with the dashboard origin. It is used a reference or fallback in some scenarios, but isn't strictly required to exist.
# Interface
## Overlay Bar

The Overlay Bar appears in the Desktop+ dashboard tab and allows managing all active overlays, as well as accessing the settings window.
- **(Overlay Buttons)**:
Click on the button to pop up a menu with options to show/hide, duplicate, remove the overlay, or to change the overlay's properties.
The Overlay Properties window can only be shown for one overlay at once. Trying to show it for another will make the existing window switch to that overlay.
The overlay buttons can also be double-clicked to toggle visibility, right-clicked to quickly access the overlay properties, and long-pressed for a few seconds to reset the Overlay Properties window position.
Pointing at the SteamVR dashboard will result in the Overlay Bar to fade out temporarily. This is to allow using the dashboard's drag handle, as well as to make space for elements of the dashboard that may slide out on interaction.
- **[+]**:
Adds a new overlay. A menu will pop up, listing the available overlay types/capture sources.
After choosing one, keep holding the trigger to drag the newly created overlay to the desired spot and then let it go. If the trigger was released immediately, it requires an additional click to let the overlay go.
- **[⛭]**:
Shows or hides the settings window.
## Floating UI

The Floating UI appears when pointing at an overlay and consists of a Main bar allow quick access to overlay functions, as well as an additional Action Bar with customizable buttons.
The primary dashboard overlay always displays its Floating UI as long as no other overlay is being hovered.
### Main Bar
- **Toggle Action Bar ([...])**:
Toggles visibility of the Action Bar. It appears right above the Main Bar.
- **Enable/Disable Drag-Mode ([Arrows])**:
Toggles the overlay drag-mode. While drag-mode is enabled, all laser pointer input on drag overlays around instead.
During drags, use vertical scroll input to change the distance to the overlay and horizontal scroll input to resize it.
Right-click on overlays to perform a two-handed gesture drag instead, which allows to rotate and scale the overlay in place.
This button can be long-pressed for a few seconds to lock the overlay's position instead.
- **Hide Overlay ([X])**:
Hides the overlay. This is the same as pressing "Hide" in the Overlay Bar.
This button can be long-pressed for a few seconds to remove the overlay instead.
- **Add Active Window as Overlay ([Window with Arrow])** (only visible for desktop overlays):
Press and hold this button to add a new overlay of the currently focused window. This is similar to adding a new overlay in the Overlay Bar.
The new overlay will start as being dragged, allowing you to position it.
This button can be hidden via the ["[x] Show Extra Buttons"](#interface-2) overlay property.
- **Reset Cumulative Values ([Performance Monitor Refresh Icon])** (only visible for Performance Monitor overlays):
Resets most values of the Performance Monitor. This includes everything that represent an average or is counted up over time.
This button can be hidden via the ["[x] Show Extra Buttons"](#interface-2) overlay property.
- **Go to Previous Page ([🡸])** (only visible for browser overlays):
Goes back in the browser's navigation history.
This button can be hidden via the ["[x] Show Extra Buttons"](#interface-2) overlay property.
- **Go to Next Page ([🡺])** (only visible for browser overlays):
Goes forward in the browser's navigation history.
This button can be hidden via the ["[x] Show Extra Buttons"](#interface-2) overlay property.
- **Refresh Current Page/Stop Page Load ([⟳/🚫])** (only visible for browser overlays):
Refreshes/reloads the current page or stops if it's currently loading.
This button can be hidden via the ["[x] Show Extra Buttons"](#interface-2) overlay property.
### Action Bar
The Action Bar contains a set of customizable action buttons which can be set either globally or for individual overlays.
Pressing on action buttons will execute the associated action. Actions pressing keys will hold them as long as the button is held and release them once the button is released.
The *Show Keyboard* action button can be long-pressed for a few seconds to reset the [VR Keyboard's](#desktop-keyboard) position.
## Settings Window
### Main Page
#### Interface

- **Language**:
Change the application language. On first launch, Desktop+ tries to set this to the operating system's language if a matching translation is available.
Some of the languages listed are community translations, which may lag behind after updates and be incomplete.
- **[x] Show Advanced Settings**:
Sets if settings marked as advanced are being shown. Advanced settings are rarely-used settings and more in-depth options for other, always displayed settings.
When unticked, relevant settings are only hidden in the interface, not turned off.
Advanced settings are marked with *(Adv.)* in this document.
- **[x] Drag Windows When Clicking on Blank Space** (Adv.):
Sets if Desktop+ interface windows can be dragged by pressing and holding anywhere on the window as long as there's no widget in the way.
When unticked, interface windows can only be dragged from the title bar.
- **Persistent UI** (Adv.):
Press the button to manage the [Persistent UI settings](#persistent-ui-page).
- **Desktop Buttons Listing Style**:
Changes the way the desktops are listed on the Action Bar or if at all.
- **[x] Add Combined Desktop**:
Adds a button for the combined desktop to the Action Bar.
#### Environment

- **Background Color**:
Allows to partially or fully cover the VR scene in the specified color, either only while looking at the Desktop+ dashboard tab or at all times.
Click on the color button to switch to the color picker page and click on the drop-down list to choose when the background color appears.
On the color picker page, click on the "Original" preview box to restore the previous value. Right click the picker to switch between hue bar and hue wheel style pickers.
- **Dim Interface**:
Dims the SteamVR dashboard and Desktop+ UI while the Desktop+ dashboard tab is open.
#### Profiles

- **Overlay Profiles**:
Press the button to manage overlay profiles. Overlay profiles allow saving and loading multiple overlay setups.
- **Application Profiles**:
Press the button to manage application profiles. Application profiles allow to automatically load an overlay profile or execute actions when a SteamVR application is launched.
#### Actions

- **Actions**:
Press [Manage] to go to the [Manage Actions page](#manage-actions-page).
- **Action Buttons (Default)**:
Press [X Actions Selected] to change the default order of action buttons displayed in the Floating UI.
- **Action Buttons (Overlay Bar)** (Adv.):
Press [X Actions Selected] to change the order of action buttons displayed in the Overlay Bar.
- **Active Controller Buttons**
Controller bindings when pointing at a Desktop+ overlay. The actual buttons used for these are defined in the VR Dashboard SteamVR input bindings.
- **[Show Controller Bindings]** (only visible when SteamVR is running):
Open the SteamVR Controller Bindings screen for the VR Dashboard.
- **(Table)**:
Action triggered when pressing the "Go Home" or "Go Back" button. This is typically one of the face buttons on the controller by default. Click one of the Action column cells to change the action.
- **Global Controller Buttons**
Controller bindings when the dashboard is closed and not pointing at any overlay. The buttons used for these are defined in the Desktop+ SteamVR input bindings. These bindings will trigger globally as long as no laser pointer is active.
- **[Show Controller Bindings]** (only visible when SteamVR is running):
Open the SteamVR Controller Bindings screen for Desktop+.
- **(Table)**:
Action triggered when pressing the respective bound global shortcut input. Click one of the Action column cells to change the action.
- **[Add Shortcut]**:
Add another shortcut. Up to 20 shortcuts can be added.
- **Hotkeys**:
System-wide keyboard shortcuts. Hotkeys block other applications from receiving that input and may not work if the same combination has already been registered elsewhere.
Desktop+ will not register a hotkey if no action is assigned to it. Most hotkeys without modifiers will not work while elevated applications have window focus.
- **(Hotkey Column)**:
Keyboard shortcut that triggers the action. Click on the cell to change the hotkey.
- **(Action Column)**:
Action triggered when pressing the respective hotkey. Click on the cell to change the action.
- **[Remove]**:
Remove the hotkey in the hovered row.
- **[Add Hotkey]**:
Add another hotkey. There's no upper limit on hotkeys.
#### Keyboard

- **Keyboard Layout**:
Press the button to change the keyboard layout of the Desktop+ keyboard. In desktop mode, the Keyboard Layout Editor can be accessed from there as well.
- **Size**:
Size of the Desktop+ keyboard. This slider adjusts the size for both inside and outside of the dashboard. Manage [Persistent UI settings](#persistent-ui-page) or resize the keyboard while dragging to adjust them individually.
- **Behavior**:
- **[x] Sticky Modifier Keys**:
Sets if the modifier keys (ctrl, alt, shift, win) stay pressed until another non-modifier key is pressed.
- **[x] Key Repeat**:
Sets if the key inputs are repeated when held down. Some keys do not repeat even with this enabled in order to mimic real keyboard behavior.
- **Show Automatically**:
Sets if the Desktop+ keyboard appears automatically when focusing a text input field and disappears again when unfocusing (unless it was manually brought up before).
This behavior is experimental for desktop and window overlays as there's no reliable way to detect all input fields.
It works reasonably well for applications using native controls or sending the correct assistive events.
The keyboard is always shown automatically for Desktop+ UI elements.
- **[x] For Desktop & Window Overlays**:
Sets if the keyboard is shown automatically for desktop & window overlays.
- **[x] For Browser Overlays** (only available when Desktop+ Browser is installed):
Sets if the keyboard is shown automatically for browser overlays.
#### Mouse

- **[x] Show Cursor**:
Sets if the cursor is rendered on top of the mirrored overlay. Graphics Capture overlays hide the cursor automatically when the source window is not in focus.
- **[x] Use Smooth Scrolling**:
Sets if smooth scrolling is used instead of discrete mouse wheel-like scrolling. Smooth scrolling also allows for horizontal and minute scrolling in applications that support it.
- **[x] Simulate as Pen Input**:
Sets if laser pointer input simulate a touch pen input device instead of a mouse.
This enables gestures meant for pen inputs, such as click drag scrolling in many applications and holding down left-click to perform a right click.
Some of the pen input behaviors are configurable on the OS-end, and may need to be enabled first.
- **[x] Allow Laser Pointer Override**:
Disables the laser pointer when the physical mouse is moved rapidly. Click the overlay to get the laser pointer back.
This option is primarily useful when sitting in front of the desk using the real mouse and keyboard while wearing the headset to view the desktop.
- **Double-Click Assistant**:
Freezes the mouse cursor for the set duration to ease the input of double-clicks. The "Auto" setting uses the double-click duration configured in Windows.
Moving larger distances from the frozen cursor position cancels the Double-Click Assistant.
- **Input Smoothing**:
Sets the level of smoothing/stabilization applied to mouse inputs.
#### Laser Pointer

These settings apply to Desktop+'s laser pointer only.
- **[x] Block Game Input while Active**:
Sets if the laser pointer being active blocks the game from receiving inputs. Even if this is disabled, inputs bound for the laser pointer still take priority over the game's controller bindings and aren't sent to the game.
- **Auto-Activation Max Distance**:
Maximum allowed distance between overlay and pointing controller to automatically toggle interaction mode while the dashboard is closed. This can be set to "Off" by dragging the slider into the left-most position.
- **[x] Gaze-based HMD-Pointer** (Adv.):
Enables using HMD gaze to point at Desktop+ overlays. The Auto-Activation Max Distance value is used here as well, if no keyboard key is set for the toggle input action.
- **(Table)**:
Input action triggered when pressing the respective keyboard key. Click one of the Keyboard Key column cells to change the keyboard key. The keyboard keys are optional can stay unset for gaze-only input.
#### Window Overlays

- **[x] Focus Game on Dashboard Deactivation** (Adv.):
Tries to change the foreground window to the current VR game after the dashboard was closed. Some games may pause or decide to not process inputs when not being in focus on the desktop.
Note that some applications such as SteamVR Home do not have a desktop window at all.
- **[x] Focus Window when Pointing at Overlay** (Adv.):
Tries to change the foreground window to the mirrored window when pointing at the overlay. This generally recommended to be left enabled in order to prevent unintended inputs on other windows.
Note that always-on-top windows will still be in front of the mirrored window and could possibly take the mouse input instead unless the mirrored window is always-on-top as well.
- **[x] Keep Window on Screen** (Adv.):
Prevents the mirrored window from being outside of the nearest desktop's work area (the space a maximized window takes up). The window is not resized if it does not fit into this space.
- **[x] Adjust Overlay Size when Window Resizes**:
Automatically adjusts the overlay size when the mirrored window's size changes. This directly affects the overlay's width property.
- **[x] Focus Game when Laser Pointer leaves Overlay**:
Tries to change the foreground window to the current VR game when the laser pointer leaves a Graphics Capture window overlay.
- **On Window Drag** (Adv.):
Changes what happens when a window is being dragged. The selected option is only triggered on window drags initiated by Desktop+.
- **On Capture Loss** (Adv.):
Changes what happens when a window capture is being lost. This usually happens after the target window was closed, but will trigger on any kind of capture loss.
The "Hide Overlay" option also automatically shows the overlay again when the capture gets restored.
#### Browser

*This is only available if the Desktop+ Browser application component is installed*
- **Maximum Frame Rate**:
The maximum amount of frames per second browsers overlays will render at. Browser overlays render at variable frame-rates and will not update the overlay if there are no changes.
- **[x] Content Blocker**:
Sets if the content blocker is enabled. The content blocker built into Desktop+ Browser takes block lists in the AdblockPlus syntax, but doesn't implement cosmetic filters.
Desktop+ does not come with any block lists by default. They can be added by placing lists files in "DesktopPlusBrowser\content_block". All lists present in the directory will be loaded and used.
#### Performance

These settings allow tweaking the performance characteristics of Desktop+. However, changing any of them should not be required for good results.
- **Update Limiter**
- **Limiter Mode** (Adv.):
Sets the global limiter mode. May be overridden by overlay-specific update limiter settings.
- **(Limit Slider)**:
Sets the limit in either ms or frames per second.
- **Desktop Duplication** (Adv.)
- **[x] Reduce Laser Pointer Latency**:
When this is active, the cursor position is updated instantly while pointing at the overlay. This causes high CPU load, but makes the laser pointer more responsive.
- **[x] Single Desktop Mirroring**:
Mirrors individual desktops instead of cropping from the combined desktop. All Desktop Duplication overlays will be showing the same desktop when this is active.
There can be a noticeable performance impact when multiple desktops update constantly. This setting can be used to mitigate it.
- **[x] Alternative Cursor Rendering**:
Enables use of alternative methods to retrieve the software cursor image, instead of relying on the cursor returned by the Desktop Duplication API.
This rendering method is slightly slower than the regular one and doesn't support cursor animation. It is intended for users experiencing issues with the hardware cursor capture using Desktop Duplication.
If other means to retrieve the current cursor fail, it may additionally fall back to using the default cursor image or a built-in alternative.
- **[x] HDR Mirroring**
Enables mirroring of desktops and windows using higher bit-depth textures, supporting HDR output.
While the full bit-depth is passed to SteamVR, there are no known HMDs capable of making use of HDR output.
The primary use-case for this setting is to fix SDR content not being captured correctly while HDR is enabled in the OS.
This setting is still considered experimental.
- **[x] Show FPS in Floating UI**
When this is active, the number of captured or rendered frames per second, at which the overlay is updated, is displayed in the Floating UI.
This number will be the same for all Desktop Duplication overlays, since they share a single backing texture.
#### Version Info

Displays version information of the running build of Desktop+.
#### Warnings

- **Warnings Hidden**:
Shows how many warnings have been hidden by clicking on "Don't show this again".
- **[Reset Hidden Warnings]**:
Resets all hidden warnings, causing them to show up again when their condition is fulfilled.
#### Startup

- **[x] Launch Automatically on SteamVR Startup**:
Sets if Desktop+ is launched automatically alongside SteamVR. This setting controls the SteamVR application configuration value directly and as such is saved and restored by SteamVR itself.
- **[x] Disable Steam Integration** (only available if Desktop+ is being run by Steam):
Restarts Desktop+ without Steam when it was launched by it. This disables the permanent in-app status, usage time statistics and other Steam features.
There will still be a brief in-app status and notification from Desktop+ being launched by Steam even if this is active. Steam Cloud functionality may not work as expected.
An alternative to this option is marking Desktop+ as private on the Steam profile.
#### Troubleshooting

These buttons let you quickly restart components of Desktop+ in case it starts behaving unexpected in some way.
Settings are saved before restarting and overlays are typically restored without loss afterwards. However, active sessions in browser overlays will be lost.
- **Desktop+**:
These buttons apply to the main Desktop+ process, responsible for non-UI overlays and most interaction with them.
- **[Restart]**:
Restart Desktop+.
- **[Enter Elevated Mode]**:
Enters elevated mode by launching a secondary Desktop+ process with administrator privileges using the "DesktopPlus Elevated" scheduled task. This button is only visible if [elevated mode](#system-wide-modifications) has been set up.
- **Desktop+ UI**:
These buttons apply to the Desktop+ UI process, responsible for displaying UI elements and modifying settings.
- **[Restart]**:
Restart Desktop+ UI.
- **[Restart in Desktop Mode]**:
Restart Desktop+ UI in desktop mode. The settings window will display on the desktop and all VR interface elements will be unavailable.
- **Restore Default Settings**:
Press the button to open the [Restore Default Settings page](#restore-default-settings-page), which allows further choices before making any changes.
### Persistent UI Page

The *Persistent UI* page allows directly manipulating the state of Desktop+ interface windows.
Settings, Overlay Properties and Desktop+ Keyboard windows can be configured here.
#### Windows
- **[x] Visible**:
Sets if the window is visible. This is the same as manually showing the window in other parts of the application.
- **[x] Pinned**:
Sets if the window is pinned. Pinning a window sets the absolute position for the global state to the current Desktop+ tab position. The Desktop+ keyboard can also be pinned in the Desktop+ tab.
- **Position**:
Press [Reset] to restore the default position for the window. This position is relative to the dashboard for Desktop+ tab state and absolute for the global state.
- **Size**:
Changes the size of the window. Interface windows can also be resized by using stick or touchpad inputs while dragging them.
- **[x] Restore State on Desktop+ Launch**:
Sets if the window state will persist between launches of Desktop+. Note that pinned windows may end up getting lost if left in a far away corner of the VR space, especially when facing away from the user.
However, a position reset can always be done in those cases.
### Manage Overlay Profiles Page

The *Manage Overlay Profiles* page allows loading, saving and deleting overlay profiles.
The buttons below the list apply to the selected item.
If you want to create a new profile, choose "[New Profile]". The default profile can be loaded to quickly reset the overlay configuration.
- **[Load Profile]**:
Load the selected profile. This will replace all existing overlays.
- **[Add Overlay from Profile]**:
Adds the overlays from the profile to the overlay list, without overriding any existing ones.
- **[Save Current Overlays]**:
Saves the selected profile. If you selected "[New Profile]", you it will create a new profile.
Desktop+ will switch to the Save Current Overlays page before saving, letting you choose which overlays to save and enter a name in case of a new profile.
- **[Delete Profile]**:
Deletes the selected profile. The button will change label to prompt for confirmation. Press it another time to really delete the profile.
### Manage Application Profiles Page

The *Manage Application Profiles* page allows configuration of application profiles.
- **(Application List)**:
This is a list of all applications registered in SteamVR + applications a profile exists for.
Most non-Steam VR applications do not register a permanent manifest with SteamVR and as such will only be listed here while they're running, unless an application profile was already created for them.
While Desktop+ isn't running, only applications a profile exist for are listed.
Inactive profiles are listed with a translucent text color. If an application profile is currently active, it is highlighted in the list with a green text color.
#### [Application Name]
The application profile configuration for the application selected in the list above.
- **[x] Activate when Application is Running**:
Toggles whether the application profile is applied or not.
- **Overlay Profile**:
Click the button to pick an overlay profile that is loaded when the application is launched.
The existing overlay configuration is restored when the application is no longer running.
Changes to the overlay configuration made while an overlay profile is loaded by an application profile are not saved automatically.
- **Entry Action**:
Click the button to pick an action that is executed when the application is launched.
- **Exit Action**:
Click the button to pick an action that is executed after the application has stopped running.
- **Delete Profile**:
Deletes the selected profile. The button will change label to prompt for confirmation. Press it another time to really delete the profile.
As the application list contains all registered applications regardless of a profile existing, this will not visibly remove any entry from the list, unless the profile is for an unregistered application.
### Manage Actions Page

The *Manage Actions* page allows creating, editing and deleting actions.
- **(Action List)**:
This is a list of all actions configured. The entries can be dragged up and down to change the order of them.
The buttons below the list apply to the selected item.
- **[Copy UID to Clipboard]** (Adv.):
Copies the action's unique identifier to the clipboard. This ID can be used in conjunction with the [command-line argument](#command-line-arguments) "--DoAction [UID]" to execute actions from external applications.
Action UIDs are 64-bit numbers derived from creation timestamp and a random component with a reasonably low chance of collisions. Default actions use special single digit UIDs.
- **[New Action...]**:
Switches to the [Edit Action page](#edit-action-page) to create a new action.
- **[Edit Action]**:
Switches to the [Edit Action page](#edit-action-page) to edit the selected action.
- **[Duplicate Action]**:
Creates a copy of the selected action.
- **[Delete Action]**:
Deletes the selected action. The button will change label to prompt for confirmation. Press it another time to really delete the action.
Anything referencing the action like controller buttons, hotkeys or application profiles will show their action be set to "[None]" afterwards.
### Edit Action Page

The *Edit Action* page allows creating and editing actions.
- **Name**:
Sets the name of the action. The default actions use special translation string ID names to automatically match the chosen application language.
- **Target**:
Sets the target of the action. Default targeting applies the commands to the overlay the action was activated on, or the focused overlay if the former isn't applicable.
The focused overlay is the overlay that was clicked on with the laser pointer. Clicking on the Floating UI also changes the focused overlay.
Tag targeting uses a list of overlay tags to target one or multiple overlays. This list is inclusive, meaning adding multiple tags for targeting checks each tag individually for matching targets (i.e. does not require multiple tags on a target).
The green tags are implicit auto-tags that match based on overlay properties.
#### Button Appearance
- **(Preview Button)**:
Button showing a preview of the action button with the current settings. It can also be pressed to test the action.
- **Icon**:
Click the button to pick an icon for the action. Icons are optional and rendered behind the label.
Custom icons can be added by placing PNG files in the "images/icons" folder. Recommended size is 96x96 pixels.
- **Label**:
The label of the button with up to 3 lines of text. The lines are always centered and squeezed to fit if they're too long.
The default actions use special translation string ID labels to automatically match the chosen application language.
#### Commands
List of the commands the action runs when executed. Commands are run sequentially but with no delay between them.
Consider using the "Launch Application" command to run external scripts if you need more complex capabilities.
Click the command headers to reveal their properties.
- **[Delete Command]**:
Deletes the command. The button will change label to prompt for confirmation. Press it another time to really delete the command.
- **[Add Command]**:
Adds another command to the command list.
##### None
No-op command. Occupies an entry but does nothing.
##### Press Key
Presses or toggles a key.
- **Key Code**:
Click the button to pick the virtual key code pressed by the command. Mouse buttons also have key codes that can be selected.
- **[x] Toggle Key**:
Toggles the state of the key instead of pressing and holding it until the action is stopped/released.
Note that key toggling is based on the global key state of the OS and held keys will remain in that state if not released otherwise. However, Desktop+ will release all held-down keys on exit.
##### Set Mouse Position
Moves the mouse cursor to the set position.
- **X/Y**:
Screen coordinates the mouse cursor is moved to. Coordinates are relative to the top left corner of the primary desktop.
- **[Use Current Mouse Position]**:
Sets the X and Y values to the current mouse cursor position.
##### Type String
Types the set string.
This is done via text input simulation and does not send key down and up events for individual keys. As such, not every application will read this kind of input.
- **String**:
The string typed by the command.
##### Launch Application
Launches external applications.
- **Executable Path**:
Path of the application launched. As this goes through the Windows shell, file paths and URLs for registered protocols can also be used.
- **Application Arguments**:
Arguments passed to the executable. This can usually be left blank.
##### Show Keyboard
Changes visibility of the [Desktop+ VR keyboard](#desktop-keyboard).
- **Visibility**:
Sets if the commands toggles, always shows, or always hides the keyboard.
##### Crop to Active Window
Crops the targeted overlays to the currently focused desktop window.
This command acts as a toggle, turning overlay cropping off if the crop is already equal to the active window.
##### Show Overlay
Changes visibility of overlays.
- **Visibility**:
Sets if the commands toggles, always shows, or always hides the overlays.
- **Target**:
Sets if the command applies to the action's target overlays or overlays matching a specific set of tags.
- **[x] Undo on Release**:
Reverts the change made by the command when the action is stopped/released.
Note that some ways of executing actions start and stop the action immediately.
##### Switch Task
Shows the Windows task switcher or changes the focus to a specific window.
Showing the task switcher usually works even when an elevated application has focus.
- **Switching Method**:
Set if the command shows the task switcher or focuses a specific window.
- **Window** (Focus Window method only):
Press the button to pick a window to focus.
- **[x] Use Strict Window Matching**:
Only allow exact window title matches when searching the window to focus. By default Desktop+ uses a weak window title matching algorithm to account for document-style changing window titles.
Use this option if you experience false positives with specific applications.
- **[x] Warp Cursor into Window** (Focus Window method only):
Sets the mouse cursor position to the center of the window that is being focused.
##### Load Overlay Profile
Loads an overlay profile.
- **Profile**:
Click on the drop-down list to select the profile the command will load.
- **[x] Remove Existing Overlays**:
Sets if all existing overlays are removed when loading the profile.
This is equivalent to loading a profile instead of adding overlay from a profile in the [Manage Overlay Profiles page](#manage-overlay-profiles-page).
### Change Action Order Page

The *Change Action Order* page allows adding, removing and re-ordering actions to action order lists.
- **(Action List)**:
List of actions in the action order list. The entries can be dragged up and down to change the order of them.
- **[Add Actions...]**:
Click on the button to pick one or more actions to add to the list.
- **[Remove Action]**:
Removes the selected action from the action order list.
### Keyboard Layout Page

The *Keyboard Layout* page allows changing the layout of the Desktop+ VR keyboard.
- **(Keyboard Layout List)**:
List of available keyboard layouts for the Desktop+ VR keyboard.
- **[Switch to Keyboard Layout Editor]** (only available in desktop mode):
Open the keyboard layout editor on the desktop to customize or create custom layouts for the VR keyboard.
#### Key Clusters
- **[x] Function/Navigation/Numpad/Extra**:
Toggles specific sections of the keyboard layout. Some keyboard layouts may not include all clusters.
### Restore Default Settings Page

The *Restore Default Settings* page allows choosing specific elements to reset to their default values.
Tick the desired elements to reset and press [Reset Selected Elements]. Desktop+ will restart after making the changes.
Note that active sessions in browser overlays will be lost when restarting.
It is recommended to use these controls over deleting the configuration files manually. For the Steam version, Steam Cloud would automatically restore the missing files on launch if enabled, undoing the deletion.
## Overlay Properties
### Main Page

- **(Overlay Icon)**:
The overlay icon can be double-clicked as an alternative way of toggling overlay visibility.
#### Position
- **Origin**:
Click on the drop-down list to select the position origin of the overlay. This can be used to attach an overlay to controllers or the HMD.
- **Play Area**:
Attach overlay to the play area. This is absolute positioning relative to the standing play area.
Overlays will not move unless the SteamVR room setup changes.
- **HMD Floor Position**:
Attach overlay to the position of the HMD, without rotation.
Choosing this origin will reveal the option "[x] Turn with HMD", which will spin the origin along without it tilting in any way.
- **Seated Position**:
Attach overlay to the SteamVR seated position. This origin may have an unexpected position if the active VR game isn't using the seated space.
Update the origin position by resetting the seated position in SteamVR. Games may also trigger updates to this position.
- **Dashboard**:
Attach overlay to the SteamVR dashboard. If visibility isn't restricted to dashboard, the origin position updates when the dashboard is brought up.
- **HMD**:
Attach overlay to the HMD. This origin is matches head movement 1:1 and essentially looks static in the headset view.
- **Left Controller**:
Attach overlay to the left motion controller.
- **Right Controller**:
Attach overlay to the right motion controller.
- **Theater Screen**:
Attach overlay to the SteamVR Theater Screen. Choosing to this origin will hide the overlay and reveal the button [Enter Theater Mode] to display the SteamVR Theater Screen. This button does the same as showing the overlay.
There can only be one Theater Screen origin overlay active at once.
If another overlay of that origin is set to be visible, it'll hide every other one automatically. Theater Screen origin overlays will also be hidden if the SteamVR Theater Screen is taken over by an overlay of another application or when using the "Close Overlay" button below the screen.
- **Tracker #1** (only visible if a tracker is connected):
Attach overlay to the first connected SteamVR tracker device. Using other trackers as the origin is not supported at this time.
- **Origin Smoothing** (only visible for HMD Floor Position and HMD origins):
Sets the level of smoothing/stabilization applied to the overlay origin.
- **Display Mode**:
Click on the drop-down list to select a display mode. Display modes restricts under which condition the overlay is visible. For example, "Only In-Game" only shows the overlay while a game/scene application is active and hides it while the dashboard is visible.
- **Position**:
- **[Change]**:
Opens the Position Change page and activates overlay drag-mode. You can use the buttons to do manual position adjustments or simply drag the overlay in the VR space to reposition them.
Using right-click to drag triggers a gesture drag, allowing you to rotate and scale overlays based on the motions of both hand-controllers.
- **[Reset]**:
Reset the position of the overlay. Depending on the origin the new position will be near the zero point of it or next to the dashboard overlay.
- **[x] Lock**:
Lock the position of the overlay. This option only prevents dragging the overlay. Position-locked overlays will still move along with their set origin.
The lock state is temporarily toggled off for the respective overlay when using the Position Change page in the Overlay Properties window, but is restored when leaving the page again.
#### Appearance

- **Width**:
Width of the overlay. The height is automatically determined by the aspect ratio of the mirrored content.
- **Curvature**:
Sets how much the overlay curves inwards.
- **Opacity**:
Sets how translucent the overlay appears. 100% is fully visible.
- **Brightness**:
Sets how bright the overlay appears. 100% is full brightness.
- **[x] Crop**:
Sets if the cropping area is applied.
- **[X, Y | Width x Height]** (Cropping Area button):
Press this button to open the [Cropping Area page](#cropping-area-page) to change how the overlay is cropped.
#### Capture

*This is only shown for desktop and window overlays and if advanced settings are enabled*
- **Capture Method**:
Changes the capture API used for this overlay. Graphics Capture additionally allows capturing windows directly. See [Desktop Duplication vs. Graphics Capture](#desktop-duplication-vs-graphics-capture) for a detailed comparison of both APIs.
- **Source** (Desktop Duplication):
Click on the drop-down list to select a desktop to crop the combined desktop mirror to. This selection is also used when resetting the cropping rectangle.
- **Source** (Graphics Capture):
Click on the button to select the capture source for the overlay. The list on the opening page contains available capturable desktops and windows.
- **[x] Use Strict Window Matching**:
Only allow exact window title matches when restoring the overlay's capture. By default Desktop+ uses a weak window title matching algorithm to account for document-style changing window titles.
Use this option if you experience false positives with specific applications.
#### Performance Monitor

*This is only shown for Performance Monitor overlays*
The Performance Monitor lets you quickly check the current state of your system from within VR in real time.
While it's possible to create as many overlays of it as desired, they will all be showing the same content. Performance Monitor properties are global.
- **Style**:
Switches between a large and compact style of the Performance Monitor.
The compact style does not allow displaying the current time.
- **[x] Show (Item)**:
Allows to customize which performance characteristics and states are displayed on the Performance Monitor.
- **[x] Disable GPU Performance Counters** (Adv.):
Disables display of GPU load % and VRAM usage. This prevents GPU hardware monitoring related stutter with certain NVIDIA drivers.
- **[Reset Cumulative Values]**:
Resets statistics that build up over time, such as average FPS, dropped frames, and reprojection ratio.
These values are also reset automatically when the game/scene application changes or SteamVR goes into standby mode.
Note that Performance Monitor overlays do not update or may be invisible while the Desktop+ UI is open in desktop mode.
#### Browser

*This is only shown for browser overlays*
- **URL**:
Address the browser overlay is displaying. This changes as browsing is done in the overlay. Local addresses can be used as well.
- **[Go]**:
Start navigation. Pressing enter on the text input field works as well.
- **[Restore Last Input]**:
Reset the text input field to the last manually entered URL. This is useful to go back to the initial location after navigating away from it.
- **Width/Height**:
Render resolution of the browser overlay. Changing this is akin to resizing a web browser window.
- **Zoom**:
Zoom level used in the browser overlay.
- **[x] Allow Transparency**:
Allows web pages to use transparency in the page background. This allows for RGBA web rendering if the page is set up for it.
However, this results in the default background color being transparent. This means pages which don't define anything themselves and expect it to be opaque white will not look correct.
Changing this property requires a reset of the browser context. Such a reset will clear the browsing history and reload the page. Click [Recreate Browser Context] to proceed after changing the property.
#### Advanced

- **[x] 3D**:
Sets if the 3D properties are applied.
- **3D Mode**:
Use the drop-down list to change the way the overlay's image is split up between both eyes to create a 3D image.
- **[x] Swap Eyes**:
Swaps which eye gets each split up part of the overlay image.
- **[x] Gaze Fade**
Sets if the Gaze Fade properties are applied.
Gaze Fade gradually fades the overlay to 0% opacity when not looking at it. When the overlay is at 0% opacity, it is considered inactive and will not react to laser pointer input.
- **[Auto-Configure]**:
Configure distance and sensitivity values automatically from the current gaze and overlay settings. After clicking, a pop-up will appear asking to look at the overlay for 3 seconds.
- **Distance** (Adv.):
Distance of the gaze point from the HMD. Put the slider to the leftmost value to set it to "Infinite".
- **Sensitivity** (Adv.):
Rate at which the fading occurs. As Gaze Fade only takes the center point of the overlay in account, adjusting this value depending on the overlay size is recommended.
- **Target Opacity** (Adv.):
Minimum opacity Gaze Fade will set when not looking at the overlay. If this value is higher than the overlay's opacity property, Gaze Fade will be reversed and fade the overlay in when not looking at it.
- **[x] Laser Pointer Input**
Sets if laser pointer input is enabled for this overlay. The laser pointer will pass through the overlay if this is disabled.
- **[x] Enabled In-Game**:
If this is disabled, the laser pointer will pass through the overlay outside of the dashboard.
- **[x] Show Floating UI**:
Sets if the Floating UI is displayed when pointing at this overlay.
- **Overlay Tags**
The tags assigned to this overlay. Overlay tags are used by actions to target specific overlays. See [Overlay Tags](#overlay-tags) for more details.
#### Performance

- **Override Update Limiter Mode** (only visible for desktop and window overlays):
Sets the limiter mode for this overlay only. Overrides the global setting.
Due to the shared textures between overlays, the limit resulting in the most updates among all active overlays of the same capture source is used.
- **Maximum Frame Rate** (only visible for browser overlays):
The maximum amount of frames per second this browser overlay will render at. Overrides the global setting.
- **[x] Update when Invisible** (Adv.):
Updates the overlay even when invisible from Opacity setting or Gaze Fade.
This helps with third-party applications accessing the overlay's contents but is not recommended otherwise. Updates are still suspended if the overlay is disabled or hidden by Display Mode setting.
Allowing overlays to update when not needed can have a significant performance impact.
#### Interface

- **Overlay Name**:
Set a custom name for the overlay. Leave this blank to let Desktop+ choose a name automatically.
- **[x] Override Action Buttons**:
Set if overlay-specific order and visibility of action buttons is used.
- **[x Actions Selected]**:
Press this button to open the [Change Action Order page](#change-action-order-page). Actions can be re-ordered by dragging the entries up and down.
- **[x] Show Desktop Buttons**:
Adds buttons for quickly switching desktops to the Action Bar of the Floating UI.
- **[x] Show Extra Buttons**:
Adds overlay-type-specific buttons to the Main Bar of the Floating UI.
Examples for such buttons are "Add Active Window as Overlay" (desktop overlays only), "Reset Cumulative Values" (Performance Monitor only), and browser overlay controls.
### Change Overlay Position Page

The *Change Overlay Position* page allows making adjustments to the overlay's position and change laser pointer drag behavior.
#### Manual Adjustment
Press the buttons to make manual adjustments to the overlay's position and orientation. The changes are applied relative to the overlay's current position.
- **[D]** (only available in desktop mode):
Click and hold the drag buttons to move or rotate the overlay with the mouse. Scroll wheel input applies forward/backward movement and roll adjustments.
#### Additional Offset
These offsets are applied on top of the overlay's position.
- **Up/Down/Left/Right/Forward/Backward Offset** (Adv.):
Additional offset in the specific direction in meters. The directions are relative to the overlay's orientation.
#### Drag Settings
Change these settings to adjust the laser pointer drag behavior. Drag settings are global and affect every overlay drag, except Desktop+ interface windows.
- **[x] Dock to Controller when Near**:
Sets if auto-docking is enabled. Auto-docking automatically changes the dragged overlay's origin to the left or right hand controller when the drag is released near them.
- **[x] Force Fixed Distance**:
Force a fixed distance between the overlays and the user.
The reference position for this is always based on the headset position, but is only updated when the dashboard is brought up or drag-mode is toggled on while the dashboard is not active. Small changes in position are ignored to allow for a more fixed reference position between adjustments.
Using stick or touchpad to change the distance of an overlay during a drag will adjust the Fixed Distance value while this drag setting is enabled.
- **Shape**:
Sets the shape the fixed distance is computed as. This can be set to sphere or a cylinder.
- **[x] Auto-Curve**:
Automatically change the dragged overlay's curvature property to match the chosen shape.
- **[x] Auto-Tilt**:
Automatically tilt the dragged overlay alongside the chosen shape.
- **[x] Snap Position**:
Snaps the position to a 3D grid of the given size.
Note that the overlay origin itself may still be moving and make it difficult to line up multiple overlays together.
- **[x] Snap Rotation**:
Snaps the rotation to the gvien angle.
Note that the overlay origin itself may still be spinning and make it difficult to line up multiple overlays together.
- **[x] Yaw/Pitch/Roll**
Toggles which axes the snapping is applied to.
### Cropping Area Page

The *Cropping Area* page allows changing the overlay's cropping area.
The cropping area is a rectangle the overlay content is cropped to. If the rectangle is larger than the overlay content, it's clamped to the maximum possible area.
Cropping areas are preserved when switching between capture sources. This can lead to an invalid rectangle being set for the new capture source, making the overlay essentially invisible as a result.
In that case, a "(!)" warning is displayed next to this crop setting on the main page and it's recommended to simply use the *[Reset]* button to fix it.
- **(Draggable Cropping Rectangle**):
Represents the overlay's cropping area in relation to its total content size.
Drag the rectangle to move it within the content bounds. Use scroll input or the edges to adjust the size of the cropping area.
- **[Reset]**:
Reset the cropping area to default values. If a desktop to crop to is set, it will restore the values for that. Otherwise, it resets to maximum possible area.
#### Manual Adjustment
- **X/Y/Width/Height**:
Position and size of the cropping area. Width and height can be set to "Max" by dragging the slider into the right-most position. The cropping area will always assume the maximum possible size in that case.
- **[Crop to Active Window]** (Desktop Duplication):
Change the cropping area to match the foreground window.
## Desktop Mode

Desktop mode displays the same interface for settings and overlay properties as in VR, but condensed into a single window on the desktop with its own main page to allow quick access and overlay management.
To access desktop mode, either run DesktopPlusUI.exe while SteamVR is not running, press [Restart in Desktop Mode] in the Troubleshooting section of the Settings window, or run DesktopPlusUI.exe with the "--DesktopMode" [command-line argument](#command-line-arguments).
Note that some settings are not available when running in desktop mode while SteamVR is not running, and the main application process will not be running either. The UI refers to this as "Desktop+ isn't running", even if the UI process might be seen as part of the application.
- **[🡸]**:
Navigate back to the previous page.
On pages that can have changes reverted by [Cancel], this button is equivalent to pressing it.
### Tools
Quickly access the settings and several key setting pages.
The items listed are the same pages as accessible through the main settings page.
### Overlays
List of overlays, similar to the Overlay Bar.
Overlays can be re-ordered by dragging the entries up and down.
Right-click the entries to access the context menu, which allows to show/hide, clone or remove the overlay.
- **[Add Overlay]**:
Add a new overlay.
Performance Monitor overlays cannot be create while in desktop mode as their appearance relies on Desktop+ UI rendering in VR.
### Keyboard Controls
Desktop mode can be fully operated by keyboard alone and also has some mouse shortcuts. These are the controls:
| **Mouse** | |
| :-------------------------- | :---------------------------------------------------- |
| Left Mouse Button | Interact with Widgets |
| Right Mouse Button | Context Menu on Overlay List/Edit Slider Value |
| Mouse Back Button/Backspace | Return to Previous Page |
| Mouse Wheel | Scroll/Move or Rotate during Overlay Drag |
| **Keyboard** | *(activate keyboard controls for these to work)* |
| :-------------------------- | :---------------------------------------------------- |
| Arrow Keys | Navigate/Activate Keyboard Controls |
| Spacebar | Interact with Widgets |
| Esc | Cancel Widget Selection |
| Enter | Confirm Text Input |
| Menu Key | Context Menu on Overlay List |
| Ctrl | Hold on List Entries while Navigating to Change Order |
## Desktop+ Keyboard

Desktop+ features its own implementation of a customizable VR keyboard. Its behavior, layout, and visible clusters can be changed in the [Keyboard Layout Settings Page](#keyboard).
The keyboard window can be operated with multiple laser pointers simultaneously. Due to API limitations, there are no haptics on the non-primary laser pointers while the dashboard is visible, however.
Its input behavior is mostly straight forward and mimics a normal PC keyboard. Keys held on the VR keyboard are simulated to be held down until they are released on it, with key repeat for most keys if configured.
Modifier keys can be sticky, meaning they will remain simulated as held down until they're pressed again. Every key on the VR keyboard can be made sticky by right-clicking them.
Keys remain pressed when hiding the keyboard, but all still held down keys are released on Desktop+ shutdown.
While shown, the keyboard follows the overlay it was shown for, if any. Or the SteamVR dashboard, while it is visible. Changes to the keyboard position are relative to this, unless the keyboard window is pinned.
Note that input done on the keyboard always gets sent to the focused overlay (the overlay that was last clicked on with the laser pointer). For desktop/window overlays this does not make any difference, however.
### Keyboard Layout Editor

The Keyboard Layout Editor allows to customize or create custom keyboard layouts for the Desktop+ VR Keyboard. It is only available in desktop mode.
#### Key List
List of all keys in the layout. Use the tabs to switch between sub-layouts.
##### Sub-Layouts
- **Base**:
Normal layout displayed by default.
- **Shift**:
Shifted layout displayed when any of the shift keys are held, or caps lock is active.
Usually features capital letters and special characters.
- **AltGr** (only available if "[x] Has AltGR" is ticked):
AltGr layout displayed when AltGr is being held. Not present on US-style layouts.
Usually features third-level letters and special characters.
- **Aux**:
Auxiliary layout. Only displayed if explicitly switched to via a "Toggle Sub-Layout" key.
This can be anything or just remain unused.
##### Sub-Layout Key List
List of all keys in the sub-layout.
Click an entry to select the key or row and displays its properties in the Key Properties window.
Entries can be dragged up or down to re-arrange their order.
##### Bottom Row
- **[Undo]**:
Undo the last change. The changes history includes modifications made in any part of the Keyboard Layout Editor.
Ctrl+Z can also be pressed to trigger this button.
- **[Redo]**:
Redo the reverted last change.
Ctrl+Y can also be pressed to trigger this button.
- **[Add]**:
Add a new key. The key is created next to the current selection.
To add a new row, select an existing row before adding a key.
Adding too many keys in a row will cause them to be cut off. While there is no limit of rows enforced, a scroll bar will appear in the editor preview and VR if the window space is exhausted.
- **[Duplicate]**:
Creates a copy of the currently selected key or row. This copy is created next to the original.
- **[Remove]**:
Removes the selected key or row. There is no confirmation step for this action, but it can always be undone.
#### Key Properties
- **Type**:
Sets the type of the key. Some key types have their own specific properties, listed below.
- **Size**:
Sets the width and height of the key. 100% is the size of a standard key and compatible with common key size definitions.
- **Label** (not available for "Blank Space"-type keys):
The label displayed on the key. The text is horizontally centered per-line, and vertically centered on the key as a whole. 100% height keys typically fit two lines of text (three fit barely, but not recommended).
Per-line centering can be changed to left or right alignment by appending "##L" or "##R" to the end of a line.
- **Cluster**:
Assign clusters to keys to allow selectively disabling them in the settings later. As "Blank Space"-type keys are also part of clusters, the assignments has to be chosen carefully to not break the layout when toggling them.
##### Virtual Key / Virtual Key (Toggle) / Virtual Key (ISO-Enter)
Simulates a key press or toggle of a virtual key code.
The ISO-enter variant allows combining two keys in adjacent rows to build an ISO-enter shaped key.
- **Key Code**:
Click the button to pick the virtual key code pressed by the key. Mouse buttons also have key codes that can be selected.
- **[x] Block Modifiers** (not available for "Virtual Key (Toggle)"):
Releases all modifiers when this key is pressed. This is useful when the key's sub-layout differs from the required modifier state of the simulated input.
- **[x] Never Repeat**:
Disable key repeat for this key, regardless of the global key repeat setting. This is useful for keys that don't repeat when held on a real keyboard.
##### String
If the string is a single character, Desktop+ will try to figure out which keys to press on the real keyboard layout to simulate the input.
If the string is multiple characters, input is done via text input simulation and not sending key down/up events for individual keys. Not every application will read this kind of input.
It is recommended to use this key type for non-basic character keys to increase application compatibility.
- **String**:
The string input simulated when pressing the key.
##### Toggle Sub-Layout
Explicitly switch to a different sub-layout. This is always a toggle. Place an identical key in the target sub-layout to allow switching back.
Shift and AltGr sub-layouts are switched to automatically depending on the related modifier's state, making this only necessary to reach the Aux sub-layout.
- **Sub-Layout**:
Sets the sub-layout to toggle.
##### Action
Execute an action when pressing the key. This works the same as action buttons in the [Floating UI Action Bar](#action-bar).
- **Action**:
Click the button to pick the action that is executed when the key is pressed.
#### Layout Metadata
- **Name**:
Name of the keyboard layout. This is what it is listed as in the [Keyboard Layout Settings Page](#keyboard).
- **Author**:
Name of the keyboard layout's author. This is displayed in the [Keyboard Layout Settings Page](#keyboard).
Official layouts leave this field blank.
- **[x] Has AltGr**:
Sets if the AltGr sub-layout should be present. This is typically left unticked for US-style layouts.
- **Preview Clusters**:
Untick clusters to check how the layout will look with different [key clusters settings](#key-clusters) set.
The state of this does not affect the saved layout.
- **[Save...]**:
Click this button to save the layout. A pop-up will open allowing you to choose the file name or to overwrite an existing layout.
Keyboard layouts are stored in the "keyboards" directory found in the application's install directory.
- **[Load...]**:
Click this button to load a different layout. A pop-up will open allowing you to choose the next layout.
Note that there is no additional confirmation step and unsaved changes will be lost when loading a different layout.
#### Keyboard Preview
A preview of how the keyboard will look in VR.
Left-click individual keys to select them in the key list or right-click a key to select the row it belongs to.
# Performance Considerations
Desktop+ was written to be lean in both computational load and memory footprint. However, functionality does not come without a price, so knowing the performance characteristics of the application can be helpful.
In general, configured, but inactive overlays are without noticeable performance penalty. Mirroring of the desktops or windows is paused completely while no overlays using the source are active.
An overlay is considered inactive when it's not enabled or visible. This means display modes hiding an overlay and Gaze Fade setting its opacity to 0% are ways to set an overlay inactive.
Overlays merely outside the view are *not* considered inactive, but may still have a lower GPU load from being culled from rendering by the VR Compositor.
If possible, avoid using Over-Under 3D, as it involves an extra conversion step and textures for each individual overlay. Textures are not shared for such overlays. Prefer Side-by-Side 3D instead.
On certain multi-GPU setups, such as laptops with a hybrid GPU solution, textures have to be copied between GPUs since the desktops are typically rendered on the integrated GPU while the HMD is connected to the dedicated GPU. This comes with a significant performance penalty. If possible, consider deactivating the integrated GPU altogether.
## Desktop Duplication
All Desktop Duplication overlays share a single texture, meaning additional overlays are fairly cheap. Mirroring is stopped when no Desktop Duplication overlays are active.
Cursor movement is decoupled from the screen's refresh-rate and can cause GPU load spikes, especially when a mouse with a high polling rate is used. If this is undesirable, use the Update Limiter with a low frame time limit to mitigate this.
## Graphics Capture
All Graphics Capture overlays using the same source are sharing a single texture, meaning additional overlays of the same source are fairly cheap.
Graphics Capture is generally a more efficient capture method than Desktop Duplication. However, keep in mind that many individual window overlays can easily end up performing worse than carefully cropped desktop overlays.
## Desktop Duplication vs. Graphics Capture
Desktop+ features two capture methods for overlays. Both have different strengths and weaknesses. Using Graphics Capture for desktop mirroring is usually only recommended on the latest Windows version.
### Desktop Duplication
- Supported on systems with Windows 8.1 and newer
- Mirrors desktops
- Decoupled cursor movement
- Only updates on screen changes
- Supports mirroring applications in exclusive fullscreen mode
- Supports mirroring exclusive fullscreen applications at unconstrained frame-rates
#### Mirroring at Unconstrained Frame-Rates
Desktop Duplication supports mirroring exclusive fullscreen applications at unconstrained frame-rates. Actual support depends on how the application does its rendering, but it works for most.
For this to work, the following conditions have to be met:
- Application has to run in exclusive fullscreen mode
- VSync has to be disabled
- Fullscreen optimizations have to be disabled*
* May not be required for certain games using OpenGL or Vulkan to render.
Fullscreen optimizations can be disabled on a per-application basis in the Compatibility tab of the executable's properties window on Windows 10 version 1803 or newer.
### Graphics Capture
- Supported on systems with Windows 10 version 1903 and newer*
- Mirrors desktops or windows
- Cursor movement at source monitor refresh rate
- Always updates at monitor refresh rate (does not apply to Windows 11 24H2 and newer)
- Supports direct mirroring of occluded windows
- Additional Desktop+ features for window overlays
- Draws yellow rectangle over mirrored sources (not visible in the capture, does not apply to Windows 11 and newer)
- More efficient than Desktop Duplication (but the fixed refresh rate may nullify this with high refresh rate displays)
* Windows 10 version 1903 is required for limited support, version 2004 for full support.
#### Graphics Capture Feature Support
Windows 10 version 1903 is the minimum supported version for Graphics Capture/window overlays.
Version 2004 additionally enables selecting the combined desktop, as well as supporting disabling the cursor.
With Windows 11 the yellow border around captures, which is only visible on real display, is removed.
24H2 additionally enables capturing secondary windows (e.g. pop-up menus), native update limiting, and only updates on content changes.
# VR Interactions
The following interactions done with VR motion controllers trigger specific behavior in Desktop+:
- Holding down left-click on a window's title bar in a desktop overlay and scrolling down: Create window overlay of this window and start dragging it.
- Dragging a window in a window overlay: Trigger configured [window overlay drag behavior](#window-overlays)
- Dragging an overlay near a motion controller: Trigger [overlay auto-docking](#drag-settings) if enabled
# Usage Examples
Provided are a few typical usage scenarios and how to configure Desktop+ for them.
If unsure, load the "Default" overlay profile as a clean starting point before trying them out.
## Attach Overlay to a Motion-Controller, Wristwatch-Style
1. Click *[+]* in the [Overlay Bar](#overlay-bar) to bring up the overlay capture source pop-up menu.
1. Use the opposite controller to press and hold left click/trigger on the desired capture source from the list.
1. While still holding down, move and rotate the controller to move the newly added overlay towards the target controller until a pop-up saying "Release to dock" appears. ([Overlay auto-docking](#drag-settings) may be disabled if this doesn't work)
1. Release left click/trigger to dock the controller.
1. Optionally enable and adjust *Gaze Fade* in the *Overlay Properties* settings page's [Advanced](#advanced) section. See [Performance Considerations](#performance-considerations) for details on why this is recommended.
The size of the new overlay can be adjusted while dragging by moving the stick or sliding the touchpad left and right.
## Show Multiple Desktops or Windows in the Dashboard at Once
1. Click *[+]* in the [Overlay Bar](#overlay-bar) to bring up the overlay capture source pop-up menu.
1. Use the opposite controller to press and hold left click/trigger on the desired capture source from the list.
1. While still holding down, move and rotate the controller to move the newly added overlay to the desired spot.
1. Release left click/trigger.
1. Click on the icon of the newly added overlay in the [Overlay Bar](#overlay-bar) to bring up its pop-up menu.
1. Click on "Properties..." to bring up the [Overlay Properties window](#overlay-properties) for the overlay.
1. At the top of the window, click on the drop-down list next to *Origin* and choose "Dashboard".
1. Optionally click on the drop-down list next to *Display Mode* and choose "Only in Desktop+ Tab" to limit visibility similar to the default Desktop+ dashboard overlay.
1. Repeat these steps for any other overlays you wish to add. You may also choose to duplicate existing overlays from the overlay icon pop-up menu as a starting point instead.
## Simulate Keyboard Shortcut from Motion-Controller Input
1. Click on *[New Action...]* below the *action list* on the [Manage Actions Page](#manage-actions-page).
1. Optionally give the action a fitting name and label.
1. Click on *Add Command*.
1. Click on the drop-down list next to *Command Type* and choose the "Press Key".
1. Click on *[None]* next to *Key Code* and choose the desired keyboard key or mouse button to be simulated.
1. Repeat step 3 to 5 to add more keys. All of them will be simulated as pressed simultaneously.
1. Click on *[Ok]* at the bottom of the to save the new Action.
1. Return to the settings window's main page.
1. Click on *[None]* next to *Global Shortcut #1* in the *Global Controller Buttons* list found in the *Actions* section.
1. Select the newly created action from the bottom of the list.
1. Open the SteamVR controller bindings for Desktop+ by pressing *[Show Controller Bindings]*
1. Configure the input bindings as desired. Desktop+ shortcut bindings are all boolean actions, meaning you'll find them when configuring digital input types such as *"Button"* or *"Toggle Button"*.
1. To bind the action created in this example, select a controller input, use it as *"Button"* and select *"Do Global Shortcut #1"*.
1. Click the check mark to save the changes.
1. The keyboard shortcut should now be pressed whenever the configured input binding is triggered. The dashboard has to be closed, however.
If you wish to configure a keyboard shortcut to simulated when pressing a button while pointing at a Desktop+ overlay, simply select the action for one of the *Active Controller Buttons*.
If you need further help on how to use the SteamVR binding interface, [OpenVR Advanced Settings' SteamVR Input Guide](https://github.com/OpenVR-Advanced-Settings/OpenVR-AdvancedSettings/blob/master/docs/SteamVRInputGuide.md) is a good place to start.
The SteamVR controller bindings can also be accessed by either going into the SteamVR settings' *Controller* page and clicking *Show old Binding UI*, or loading http://localhost:27062/dashboard/controllerbinding.html with a web browser while SteamVR is running.
SteamVR will *not* list Desktop+ in the pop-up that appears by pressing the *Manage Controller Bindings* button, so make sure to not accidentally click that button instead.
SteamVR may not list Desktop+ right after its first launch. Try restarting SteamVR in that case.
# Advanced Features
## Hidden Configuration Settings
There are a handful of advanced configuration settings which aren't exposed in the settings interface.
These config keys can be found in *config_default.ini* alongside comments about their functionality.
## System-wide Modifications
The "misc" directory found in the application's install directory contains a collection of scripts that make system-wide changes to enable additional access privileges for Desktop+.
Please make sure to read "!About this folder.txt" in the same directory before running any of them to understand the consequences of doing so.
## Command-Line Arguments
The following arguments can be passed to Desktop+ to change its behavior. Most of these are accessible in other ways via the UI and don't need to be used manually.
| Executable | Argument | Description |
| :---------------- | :--------------- | :---------- |
| DesktopPlus.exe | --ElevatedMode | Launch in elevated mode. This is what the elevated task uses.
Elevated mode runs as a secondary process taking input from an existing DesktopPlus.exe process and will not work if one isn't already running. |
| DesktopPlus.exe | --DoAction [UID] | Execute the action with the specified UID. An action UID can be obtained by pressing [Copy UID to Clipboard] in the [Manage Actions page](#manage-actions-page).
This runs as a secondary process that passes the command on and then exits and will not work if the DesktopPlus.exe process isn't already running. |
| DesktopPlusUI.exe | --DesktopMode | Force launching in desktop mode. |
| DesktopPlusUI.exe | --KeyboardEditor | Launch as Keyboard Editor. This argument also forces desktop mode. |
| Both | -v [verbosity] | Set the verbosity level used for logging. Available levels are 0 (default), 1 (enables additional verbose logging), INFO, WARNING, ERROR and OFF. |
### Desktop+ Browser
Command-line arguments for Chromium Embedded Framework can be set in config.ini via the *CommandLineArguments* key in the *[Browser]* section.
================================================
FILE: src/DesktopPlus/BackgroundOverlay.cpp
================================================
#include "BackgroundOverlay.h"
#include "ConfigManager.h"
#include "OutputManager.h"
BackgroundOverlay::BackgroundOverlay() : m_OvrlHandle(vr::k_ulOverlayHandleInvalid)
{
//Not calling Update() here since the OutputManager typically needs to load the config and OpenVR first
}
BackgroundOverlay::~BackgroundOverlay()
{
if ((m_OvrlHandle != vr::k_ulOverlayHandleInvalid) && (vr::VROverlay() != nullptr))
{
vr::VROverlay()->DestroyOverlay(m_OvrlHandle);
}
}
void BackgroundOverlay::Update()
{
InterfaceBGColorDisplayMode display_mode = (InterfaceBGColorDisplayMode)ConfigManager::GetValue(configid_int_interface_background_color_display_mode);
if (display_mode == ui_bgcolor_dispmode_never)
{
//Don't keep the overlay around if it's absolutely not needed (which is the case most of the time)
if (m_OvrlHandle != vr::k_ulOverlayHandleInvalid)
{
vr::VROverlay()->DestroyOverlay(m_OvrlHandle);
m_OvrlHandle = vr::k_ulOverlayHandleInvalid;
}
}
else
{
//Create overlay if it doesn't exist yet
if (m_OvrlHandle == vr::k_ulOverlayHandleInvalid)
{
vr::EVROverlayError ovrl_error = vr::VROverlay()->CreateOverlay("elvissteinjr.DesktopPlusBackground", "Desktop+ Background", &m_OvrlHandle);
if (ovrl_error != vr::VROverlayError_None)
return;
//Fill overlay with some pixels
unsigned char bytes[2 * 2 * 4];
std::fill(std::begin(bytes), std::end(bytes), 255); //Full white RGBA
vr::VROverlay()->SetOverlayRaw(m_OvrlHandle, bytes, 2, 2, 4);
//The trick to this overlay is to set it as a panorama attached to the HMD
//Panorama overlays are weird in that they essentially poke a hole into the rendering in the area they'd cover normally
//By doing this we ensure to always cover the field of view. The actual panorama is rendered in front of scene content, but positioned behind all overlays
//IVRCompositor::FadeToColor() can achieve a similar effect, but the colors are washed out. Impossible to achieve full black with it.
vr::VROverlay()->SetOverlayFlag(m_OvrlHandle, vr::VROverlayFlags_Panorama, true);
vr::VROverlay()->SetOverlayWidthInMeters(m_OvrlHandle, 100.0f);
Matrix4 transform;
transform.setTranslation({0.0f, 0.0f, -10.0f});
vr::HmdMatrix34_t transform_openvr = transform.toOpenVR34();
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(m_OvrlHandle, vr::k_unTrackedDeviceIndex_Hmd, &transform_openvr);
}
bool display_overlay = true; //ui_bgcolor_dispmode_always
if (display_mode == ui_bgcolor_dispmode_dplustab)
{
display_overlay = ((OutputManager::Get()) && (OutputManager::Get()->IsDashboardTabActive()));
}
if (display_overlay)
{
//Unpack color value
unsigned int rgba = pun_cast(ConfigManager::GetValue(configid_int_interface_background_color));
float r = (rgba & 0x000000FF) / 255.0f;
float g = ((rgba & 0x0000FF00) >> 8) / 255.0f;
float b = ((rgba & 0x00FF0000) >> 16) / 255.0f;
float a = ((rgba & 0xFF000000) >> 24) / 255.0f;
vr::VROverlay()->SetOverlayColor(m_OvrlHandle, r, g, b);
vr::VROverlay()->SetOverlayAlpha(m_OvrlHandle, a);
if (!vr::VROverlay()->IsOverlayVisible(m_OvrlHandle))
{
vr::VROverlay()->ShowOverlay(m_OvrlHandle);
}
}
else if (vr::VROverlay()->IsOverlayVisible(m_OvrlHandle))
{
vr::VROverlay()->HideOverlay(m_OvrlHandle);
}
}
}
================================================
FILE: src/DesktopPlus/BackgroundOverlay.h
================================================
#pragma once
#include "openvr.h"
class BackgroundOverlay
{
private:
vr::VROverlayHandle_t m_OvrlHandle;
public:
BackgroundOverlay();
~BackgroundOverlay();
void Update();
};
================================================
FILE: src/DesktopPlus/CommonTypes.h
================================================
#ifndef _COMMONTYPES_H_
#define _COMMONTYPES_H_
//If NTDDI_WIN11_GA is available, use it (optional requirement by OutputManager)
//This *could* blow up if one of the few files that include windows.h itself comes first somehow but it's fine for now
#include
#ifdef NTDDI_WIN11_GA
#undef NTDDI_VERSION
#define NTDDI_VERSION NTDDI_WIN11_GA
#else
#pragma message("Using older Windows SDK! Not all Desktop+ features will be available in this build.")
#endif
#define NOMINMAX
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "DPRect.h"
#include "PixelShader.h"
#include "PixelShaderCursor.h"
#include "VertexShader.h"
#define NUMVERTICES 6
#define BPP 4
#define OCCLUSION_STATUS_MSG WM_USER
extern HRESULT SystemTransitionsExpectedErrors[];
extern HRESULT CreateDuplicationExpectedErrors[];
extern HRESULT FrameInfoExpectedErrors[];
extern HRESULT AcquireFrameExpectedError[];
extern HRESULT EnumOutputsExpectedErrors[];
typedef _Return_type_success_(return == DUPL_RETURN_SUCCESS) enum
{
DUPL_RETURN_SUCCESS = 0,
DUPL_RETURN_ERROR_EXPECTED = 1,
DUPL_RETURN_ERROR_UNEXPECTED = 2
}DUPL_RETURN;
//
// Used by OutputManager::Update(), maps to DUPL_RETURN values where applicable
//
typedef _Return_type_success_(return == DUPL_RETURN_UPD_SUCCESS) enum
{
DUPL_RETURN_UPD_SUCCESS = 0,
DUPL_RETURN_UPD_ERROR_EXPECTED = 1,
DUPL_RETURN_UPD_ERROR_UNEXPECTED = 2,
DUPL_RETURN_UPD_QUIT = 3,
DUPL_RETURN_UPD_RETRY = 4,
DUPL_RETURN_UPD_SUCCESS_REFRESHED_OVERLAY = 5
}DUPL_RETURN_UPD;
_Post_satisfies_(return != DUPL_RETURN_SUCCESS)
DUPL_RETURN ProcessFailure(_In_opt_ ID3D11Device* device, _In_ LPCWSTR str, _In_ LPCWSTR title, HRESULT hr, _In_opt_z_ HRESULT* expected_errors = nullptr);
void DisplayMsg(_In_ LPCWSTR str, _In_ LPCWSTR title, HRESULT hr);
//
// Holds info about the pointer/cursor
//
typedef struct _PTR_INFO
{
std::vector ShapeBuffer;
DXGI_OUTDUPL_POINTER_SHAPE_INFO ShapeInfo = {0};
POINT Position = {0, 0};
bool Visible = false;
UINT WhoUpdatedPositionLast = 0;
LARGE_INTEGER LastTimeStamp = {0};
bool CursorShapeChanged = false;
} PTR_INFO;
//
// Structure that holds D3D resources not directly tied to any one thread
//
typedef struct _DX_RESOURCES
{
ID3D11Device* Device;
ID3D11DeviceContext* Context;
ID3D11VertexShader* VertexShader;
ID3D11PixelShader* PixelShader;
ID3D11InputLayout* InputLayout;
ID3D11SamplerState* Sampler;
} DX_RESOURCES;
//
// Structure to pass to a new thread
//
typedef struct _THREAD_DATA
{
// Used to indicate abnormal error condition
HANDLE UnexpectedErrorEvent;
// Used to indicate a transition event occurred e.g. PnpStop, PnpStart, mode change, TDR, desktop switch and the application needs to recreate the duplication interface
HANDLE ExpectedErrorEvent;
HANDLE NewFrameProcessedEvent;
HANDLE PauseDuplicationEvent;
HANDLE ResumeDuplicationEvent;
// Used by WinProc to signal to threads to exit
HANDLE TerminateThreadsEvent;
HANDLE TexSharedHandle;
UINT Output;
INT OffsetX;
INT OffsetY;
PTR_INFO* PtrInfo;
DX_RESOURCES DxRes;
DPRect* DirtyRegionTotal;
bool WMRIgnoreVScreens;
} THREAD_DATA;
//
// FRAME_DATA holds information about an acquired frame
//
typedef struct _FRAME_DATA
{
ID3D11Texture2D* Frame;
DXGI_OUTDUPL_FRAME_INFO FrameInfo;
_Field_size_bytes_((MoveCount * sizeof(DXGI_OUTDUPL_MOVE_RECT)) + (DirtyCount * sizeof(RECT))) BYTE* MetaData;
UINT DirtyCount;
UINT MoveCount;
} FRAME_DATA;
//
// A vertex with a position and texture coordinate
//
typedef struct _VERTEX
{
DirectX::XMFLOAT3 Pos;
DirectX::XMFLOAT2 TexCoord;
} VERTEX;
#endif
================================================
FILE: src/DesktopPlus/DesktopPlus.cpp
================================================
//This code belongs to the Desktop+ OpenVR overlay application, licensed under GPL 3.0
//
//Desktop+ is heavily based on the DXGI Desktop Duplication sample code by Microsoft: https://github.com/microsoft/Windows-classic-samples/tree/master/Samples/DXGIDesktopDuplication
//Much of the code is simply modified or expanded upon it, so structure and many comments are leftovers from that
#include
#include
#include
#include
#include
#include "DesktopPlusWinRT.h"
#include "DPBrowserAPIClient.h"
#include "Util.h"
#include "DisplayManager.h"
#include "DuplicationManager.h"
#include "OutputManager.h"
#include "WindowManager.h"
#include "ThreadManager.h"
#include "InterprocessMessaging.h"
#include "ElevatedMode.h"
#include "Logging.h"
// Below are lists of errors expect from Dxgi API calls when a transition event like mode change, PnpStop, PnpStart
// desktop switch, TDR or session disconnect/reconnect. In all these cases we want the application to clean up the threads that process
// the desktop updates and attempt to recreate them.
// If we get an error that is not on the appropriate list then we exit the application
// These are the errors we expect from general Dxgi API due to a transition
HRESULT SystemTransitionsExpectedErrors[] = {
DXGI_ERROR_DEVICE_REMOVED,
DXGI_ERROR_ACCESS_LOST,
static_cast(WAIT_ABANDONED),
S_OK // Terminate list with zero valued HRESULT
};
// These are the errors we expect from IDXGIOutput1::DuplicateOutput due to a transition
HRESULT CreateDuplicationExpectedErrors[] = {
DXGI_ERROR_DEVICE_REMOVED,
static_cast(E_ACCESSDENIED),
DXGI_ERROR_UNSUPPORTED,
DXGI_ERROR_SESSION_DISCONNECTED,
S_OK // Terminate list with zero valued HRESULT
};
// These are the errors we expect from IDXGIOutputDuplication methods due to a transition
HRESULT FrameInfoExpectedErrors[] = {
DXGI_ERROR_DEVICE_REMOVED,
DXGI_ERROR_ACCESS_LOST,
S_OK // Terminate list with zero valued HRESULT
};
// These are the errors we expect from IDXGIAdapter::EnumOutputs methods due to outputs becoming stale during a transition
HRESULT EnumOutputsExpectedErrors[] = {
DXGI_ERROR_NOT_FOUND,
S_OK // Terminate list with zero valued HRESULT
};
//
// Forward Declarations
//
DWORD WINAPI CaptureThreadEntry(_In_ void* Param);
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
bool SpawnProcessWithDefaultEnv(LPCWSTR application_name, LPWSTR commandline = nullptr);
void ProcessCmdline(bool& use_elevated_mode, bool& cancel_startup);
bool DisplayInitError(vr::EVRInitError vr_init_error, vr::EVROverlayError vr_overlay_error, bool vr_input_success);
//
// Class for progressive waits
//
typedef struct
{
UINT WaitTime;
UINT WaitCount;
}WAIT_BAND;
#define WAIT_BAND_COUNT 5
#define WAIT_BAND_STOP 0
class DYNAMIC_WAIT
{
public :
DYNAMIC_WAIT();
~DYNAMIC_WAIT();
void Wait();
private :
static const WAIT_BAND m_WaitBands[WAIT_BAND_COUNT];
// Period in seconds that a new wait call is considered part of the same wait sequence
static const UINT m_WaitSequenceTimeInSeconds = 2;
UINT m_CurrentWaitBandIdx;
UINT m_WaitCountInCurrentBand;
LARGE_INTEGER m_QPCFrequency;
LARGE_INTEGER m_LastWakeUpTime;
BOOL m_QPCValid;
};
const WAIT_BAND DYNAMIC_WAIT::m_WaitBands[WAIT_BAND_COUNT] = {
{10, 40},
{50, 20},
{250, 20},
{2000, 60},
{5000, WAIT_BAND_STOP} // Never move past this band
};
DYNAMIC_WAIT::DYNAMIC_WAIT() : m_CurrentWaitBandIdx(0), m_WaitCountInCurrentBand(0)
{
m_QPCValid = QueryPerformanceFrequency(&m_QPCFrequency);
m_LastWakeUpTime.QuadPart = 0L;
}
DYNAMIC_WAIT::~DYNAMIC_WAIT()
{
}
void DYNAMIC_WAIT::Wait()
{
LARGE_INTEGER CurrentQPC = {0};
// Is this wait being called with the period that we consider it to be part of the same wait sequence
QueryPerformanceCounter(&CurrentQPC);
if (m_QPCValid && (CurrentQPC.QuadPart <= (m_LastWakeUpTime.QuadPart + (m_QPCFrequency.QuadPart * m_WaitSequenceTimeInSeconds))))
{
// We are still in the same wait sequence, lets check if we should move to the next band
if ((m_WaitBands[m_CurrentWaitBandIdx].WaitCount != WAIT_BAND_STOP) && (m_WaitCountInCurrentBand > m_WaitBands[m_CurrentWaitBandIdx].WaitCount))
{
m_CurrentWaitBandIdx++;
m_WaitCountInCurrentBand = 0;
}
}
else
{
// Either we could not get the current time or we are starting a new wait sequence
m_WaitCountInCurrentBand = 0;
m_CurrentWaitBandIdx = 0;
}
// Sleep for the required period of time
Sleep(m_WaitBands[m_CurrentWaitBandIdx].WaitTime);
// Record the time we woke up so we can detect wait sequences
QueryPerformanceCounter(&m_LastWakeUpTime);
m_WaitCountInCurrentBand++;
}
//
// Program entry point
//
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ INT nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
bool use_elevated_mode = false;
bool cancel_startup = false;
ProcessCmdline(use_elevated_mode, cancel_startup);
if (use_elevated_mode)
{
//Pass all control to eleveated mode and exit when we're done there
return ElevatedModeEnter(hInstance);
}
else if (cancel_startup)
{
//Command line contained a one-off command sent to existing instances, exit
return 0;
}
DPLog_Init("DesktopPlus");
INT SingleOutput = 0;
// Synchronization
HANDLE UnexpectedErrorEvent = nullptr;
HANDLE ExpectedErrorEvent = nullptr;
HANDLE NewFrameProcessedEvent = nullptr;
HANDLE PauseDuplicationEvent = nullptr;
HANDLE ResumeDuplicationEvent = nullptr;
HANDLE TerminateThreadsEvent = nullptr;
// Window
HWND WindowHandle = nullptr;
//Make sure only one instance is running
StopProcessByWindowClass(g_WindowClassNameDashboardApp);
// Event used by the threads to signal an unexpected error and we want to quit the app
UnexpectedErrorEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!UnexpectedErrorEvent)
{
ProcessFailure(nullptr, L"UnexpectedErrorEvent creation failed", L"Desktop+ Error", E_UNEXPECTED);
return 0;
}
// Event for when a thread encounters an expected error
ExpectedErrorEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!ExpectedErrorEvent)
{
ProcessFailure(nullptr, L"ExpectedErrorEvent creation failed", L"Desktop+ Error", E_UNEXPECTED);
return 0;
}
// Event for when a thread succeeded in processing a frame
NewFrameProcessedEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!NewFrameProcessedEvent)
{
ProcessFailure(nullptr, L"NewFrameProcessedEvent creation failed", L"Desktop+ Error", E_UNEXPECTED);
return 0;
}
//Event to tell threads to pause duplication (signaled by default)
PauseDuplicationEvent = ::CreateEvent(nullptr, TRUE, TRUE, nullptr);
if (!PauseDuplicationEvent)
{
ProcessFailure(nullptr, L"PauseDuplicationEvent creation failed", L"Desktop+ Error", E_UNEXPECTED);
return 0;
}
//Event to tell threads to resume duplication
ResumeDuplicationEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!ResumeDuplicationEvent)
{
ProcessFailure(nullptr, L"ResumeDuplicationEvent creation failed", L"Desktop+ Error", E_UNEXPECTED);
return 0;
}
// Event to tell spawned threads to quit
TerminateThreadsEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!TerminateThreadsEvent)
{
ProcessFailure(nullptr, L"TerminateThreadsEvent creation failed", L"Desktop+ Error", E_UNEXPECTED);
return 0;
}
// Register class
WNDCLASSEXW Wc;
Wc.cbSize = sizeof(WNDCLASSEXW);
Wc.style = 0;
Wc.lpfnWndProc = WndProc;
Wc.cbClsExtra = 0;
Wc.cbWndExtra = 0;
Wc.hInstance = hInstance;
Wc.hIcon = nullptr;
Wc.hCursor = nullptr;
Wc.hbrBackground = nullptr;
Wc.lpszMenuName = nullptr;
Wc.lpszClassName = g_WindowClassNameDashboardApp;
Wc.hIconSm = nullptr;
if (!RegisterClassExW(&Wc))
{
ProcessFailure(nullptr, L"Window class registration failed", L"Desktop+ Error", E_UNEXPECTED);
return 0;
}
// Create window
WindowHandle = ::CreateWindowW(g_WindowClassNameDashboardApp, L"Desktop+ Overlay",
0,
0, 0,
1, 1,
HWND_DESKTOP, nullptr, hInstance, nullptr);
if (!WindowHandle)
{
ProcessFailure(nullptr, L"Window creation failed", L"Desktop+ Error", E_FAIL);
return 0;
}
//Init WinRT DLL
DPWinRT_Init();
DPLog_DPWinRT_SupportInfo();
LOG_F(INFO, "Loaded WinRT library");
//Init BrowserClientAPI (this doesn't start the browser process, only checks for presence)
DPBrowserAPIClient::Get().Init();
//Allow IPC messages even when elevated
IPCManager::Get().DisableUIPForRegisteredMessages(WindowHandle);
THREADMANAGER ThreadMgr;
OutputManager OutMgr(PauseDuplicationEvent, ResumeDuplicationEvent);
RECT DeskBounds;
UINT OutputCount;
//Start up UI process unless disabled or already running
if ( (!ConfigManager::GetValue(configid_bool_interface_no_ui)) && (!IPCManager::IsUIAppRunning()) )
{
std::wstring path = WStringConvertFromUTF8(ConfigManager::Get().GetApplicationPath().c_str()) + L"DesktopPlusUI.exe";
SpawnProcessWithDefaultEnv(path.c_str());
LOG_F(INFO, "Launched Desktop+ UI process");
}
//Message loop
MSG msg = {0};
DUPL_RETURN Ret = DUPL_RETURN_SUCCESS;
DUPL_RETURN_UPD RetUpdate = DUPL_RETURN_UPD_SUCCESS;
bool FirstTime = true;
DYNAMIC_WAIT DynamicWait;
LARGE_INTEGER UpdateLimiterStartingTime, UpdateLimiterEndingTime, UpdateLimiterElapsedMicroseconds;
LARGE_INTEGER UpdateLimiterFrequency;
bool IsNewFrame = false;
bool SkipFrame = false;
::QueryPerformanceFrequency(&UpdateLimiterFrequency);
::QueryPerformanceCounter(&UpdateLimiterStartingTime);
while (WM_QUIT != msg.message)
{
if ((!FirstTime) && (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))) //Wait for init before processing messages
{
// Process window messages
if (msg.message >= 0xC000) //Custom message from UI process, handle in output manager (WM_COPYDATA is handled in WndProc())
{
if (OutMgr.HandleIPCMessage(msg))
{
SetEvent(ExpectedErrorEvent);
}
}
else if (msg.message >= WM_DPLUSWINRT) //WinRT library messages
{
OutMgr.HandleWinRTMessage(msg);
}
else if (msg.message == WM_HOTKEY)
{
OutMgr.HandleHotkeyMessage(msg);
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else if (WaitForSingleObjectEx(UnexpectedErrorEvent, 0, FALSE) == WAIT_OBJECT_0)
{
// Unexpected error occurred so exit the application
break;
}
else if ((FirstTime) || (WaitForSingleObjectEx(ExpectedErrorEvent, 0, FALSE) == WAIT_OBJECT_0))
{
if (!FirstTime)
{
LOG_F(INFO, "System transition occured, reinitializing...");
// Terminate other threads
ResetEvent(PauseDuplicationEvent);
SetEvent(ResumeDuplicationEvent);
SetEvent(TerminateThreadsEvent);
ThreadMgr.WaitForThreadTermination();
ResetEvent(TerminateThreadsEvent);
ResetEvent(ExpectedErrorEvent);
ResetEvent(NewFrameProcessedEvent);
ResetEvent(ResumeDuplicationEvent);
// Clean up
ThreadMgr.Clean();
OutMgr.CleanRefs();
// As we have encountered an error due to a system transition we wait before trying again, using this dynamic wait
// the wait periods will get progressively long to avoid wasting too much system resource if this state lasts a long time
DynamicWait.Wait();
}
// Re-initialize
vr::EVRInitError vr_init_error = vr::VRInitError_None;
if (FirstTime) //InitOutput() needs OpenVR to be initialized already
{
auto init_error = OutMgr.InitOverlay();
//Display error message if init error states indicates one
if (DisplayInitError(std::get(init_error), std::get(init_error), std::get(init_error)))
{
//An error message was displayed, abort
Ret = DUPL_RETURN_ERROR_UNEXPECTED;
break;
}
if ( (ConfigManager::GetValue(configid_bool_misc_no_steam)) && (ConfigManager::GetValue(configid_bool_state_misc_process_started_by_steam)) )
{
//Was started by Steam but running through it was turned off, so restart without
std::wstring path = WStringConvertFromUTF8(ConfigManager::Get().GetApplicationPath().c_str()) + L"DesktopPlus.exe";
SpawnProcessWithDefaultEnv(path.c_str());
break;
}
}
Ret = OutMgr.InitOutput(WindowHandle, SingleOutput, &OutputCount, &DeskBounds);
if (Ret == DUPL_RETURN_SUCCESS)
{
HANDLE SharedHandle = OutMgr.GetSharedHandle();
if (SharedHandle)
{
Ret = ThreadMgr.Initialize(SingleOutput, OutputCount, UnexpectedErrorEvent, ExpectedErrorEvent, NewFrameProcessedEvent, PauseDuplicationEvent,
ResumeDuplicationEvent, TerminateThreadsEvent, SharedHandle, &DeskBounds, OutMgr.GetDXGIAdapter(),
(ConfigManager::GetValue(configid_int_interface_wmr_ignore_vscreens) == 1));
}
else
{
DisplayMsg(L"Failed to get handle of shared surface", L"Desktop+ Error", E_FAIL);
Ret = DUPL_RETURN_ERROR_UNEXPECTED;
}
if (FirstTime)
{
// First time through the loop
FirstTime = false;
}
else
{
ForceScreenRefresh();
}
}
else if (Ret == DUPL_RETURN_ERROR_EXPECTED)
{
if (OutputCount == 0) //No outputs right now, oops
{
OutMgr.SetOutputInvalid();
Ret = DUPL_RETURN_SUCCESS; //Entered "valid" state now, prevent auto-retry to needlessly kick in
}
FirstTime = false;
}
}
else //Present frame or handle events as fast as needed
{
if (WaitForSingleObjectEx(NewFrameProcessedEvent, OutMgr.GetMaxRefreshDelay(), FALSE) == WAIT_OBJECT_0) //New frame
{
ResetEvent(NewFrameProcessedEvent);
IsNewFrame = true;
}
else
{
IsNewFrame = (RetUpdate == DUPL_RETURN_UPD_RETRY); //Retry is treated as if it's new frame, otherwise false
}
//Update limiter/skipper
const LARGE_INTEGER& limiter_delay = OutMgr.GetUpdateLimiterDelay();
bool update_limiter_active = (limiter_delay.QuadPart != 0);
if (update_limiter_active)
{
QueryPerformanceCounter(&UpdateLimiterEndingTime);
UpdateLimiterElapsedMicroseconds.QuadPart = UpdateLimiterEndingTime.QuadPart - UpdateLimiterStartingTime.QuadPart;
UpdateLimiterElapsedMicroseconds.QuadPart *= 1000000;
UpdateLimiterElapsedMicroseconds.QuadPart /= UpdateLimiterFrequency.QuadPart;
SkipFrame = (UpdateLimiterElapsedMicroseconds.QuadPart < limiter_delay.QuadPart);
}
else
{
SkipFrame = false;
}
RetUpdate = OutMgr.Update(ThreadMgr.GetPointerInfo(), ThreadMgr.GetDirtyRegionTotal(), IsNewFrame, SkipFrame);
//Map return value to DUPL_RETRUN Ret
switch (RetUpdate)
{
case DUPL_RETURN_UPD_QUIT: Ret = DUPL_RETURN_ERROR_UNEXPECTED; break;
case DUPL_RETURN_UPD_RETRY: Ret = DUPL_RETURN_SUCCESS; break;
case DUPL_RETURN_UPD_SUCCESS_REFRESHED_OVERLAY: Ret = DUPL_RETURN_SUCCESS; break;
default: Ret = (DUPL_RETURN)RetUpdate;
}
if ( (RetUpdate == DUPL_RETURN_UPD_SUCCESS_REFRESHED_OVERLAY) && (update_limiter_active) )
{
QueryPerformanceCounter(&UpdateLimiterStartingTime);
}
OutMgr.UpdatePerformanceStates();
}
// Check if for errors
if (Ret != DUPL_RETURN_SUCCESS)
{
if (Ret == DUPL_RETURN_ERROR_EXPECTED)
{
// Some type of system transition is occurring so retry
SetEvent(ExpectedErrorEvent);
}
else
{
// Unexpected error or exit event, so exit
break;
}
}
}
LOG_F(INFO, "Shutting down...");
//Remove all overlays since they may access things on destruction after we're shut down otherwise
OverlayManager::Get().RemoveAllOverlays();
//Quit Browser processes if they're running (we do this early since the browser doesn't poll for VREvent_Quit itself)
DPBrowserAPIClient::Get().Quit();
// Make sure all other threads have exited
if (SetEvent(TerminateThreadsEvent))
{
//Wake them up first if needed
ResetEvent(PauseDuplicationEvent);
SetEvent(ResumeDuplicationEvent);
ThreadMgr.WaitForThreadTermination();
}
// Clean up
CloseHandle(UnexpectedErrorEvent);
CloseHandle(ExpectedErrorEvent);
CloseHandle(NewFrameProcessedEvent);
CloseHandle(PauseDuplicationEvent);
CloseHandle(ResumeDuplicationEvent);
CloseHandle(TerminateThreadsEvent);
//Tell WindowManager to exit if it's still active
WindowManager::Get().ClearTempTopMostWindow();
WindowManager::Get().SetActive(false);
//Do other shutdown steps, like undimming the dashboard if needed
OutMgr.OnExit();
//Kindly ask elevated mode process to quit if it exists
if (HWND window = ::FindWindow(g_WindowClassNameElevatedMode, nullptr))
{
::PostMessage(window, WM_QUIT, 0, 0);
}
if (msg.message == WM_QUIT)
{
// For a WM_QUIT message we should return the wParam value
return static_cast(msg.wParam);
}
return 0;
}
//
// Window message processor
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COPYDATA:
{
//Forward to output manager
if (OutputManager::Get())
{
bool mirror_reset_required = false;
MSG msg;
// Process all custom window messages posted before this
while (PeekMessage(&msg, nullptr, 0xC000, 0xFFFF, PM_REMOVE))
{
//Skip mirror reset messages here to prevent an endless loop
if ((IPCManager::Get().GetIPCMessageID(msg.message) == ipcmsg_action) && (msg.wParam == ipcact_mirror_reset))
{
continue;
}
if (OutputManager::Get()->HandleIPCMessage(msg))
{
mirror_reset_required = true;
}
}
msg.hwnd = hWnd;
msg.message = message;
msg.wParam = wParam;
msg.lParam = lParam;
if (OutputManager::Get()->HandleIPCMessage(msg))
{
mirror_reset_required = true;
}
if (mirror_reset_required)
{
IPCManager::Get().PostMessageToDashboardApp(ipcmsg_action, ipcact_mirror_reset);
}
}
break;
}
case WM_DISPLAYCHANGE:
{
//Update desktop count and rects
if (OutputManager::Get())
{
LOG_F(INFO, "WM_DISPLAYCHANGE recieved, re-enumerating outputs...");
OutputManager::Get()->EnumerateOutputs();
}
break;
}
case WM_DESTROY:
{
PostQuitMessage(0);
break;
}
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
bool SpawnProcessWithDefaultEnv(LPCWSTR application_name, LPWSTR commandline)
{
LPVOID env = nullptr;
STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};
si.cb = sizeof(si);
//Use a new environment block since we don't want to copy the Steam environment variables if there are any
if (::CreateEnvironmentBlock(&env, ::GetCurrentProcessToken(), FALSE))
{
bool ret = ::CreateProcess(application_name, commandline, nullptr, nullptr, FALSE, CREATE_UNICODE_ENVIRONMENT, env, nullptr, &si, &pi);
//We don't care about these, so close right away
::CloseHandle(pi.hProcess);
::CloseHandle(pi.hThread);
::DestroyEnvironmentBlock(env);
return ret;
}
return false;
}
void ProcessCmdline(bool& use_elevated_mode, bool& cancel_startup)
{
//__argv and __argc are global vars set by system
for (UINT i = 0; i < static_cast(__argc); ++i)
{
if ((strcmp(__argv[i], "-ElevatedMode") == 0) ||
(strcmp(__argv[i], "--ElevatedMode") == 0) ||
(strcmp(__argv[i], "/ElevatedMode") == 0))
{
use_elevated_mode = true;
}
else if ((strcmp(__argv[i], "-DoAction") == 0) ||
(strcmp(__argv[i], "--DoAction") == 0) ||
(strcmp(__argv[i], "/DoAction") == 0))
{
//Take the following argument and parse it as ActionUID to send it to the running dashboard app instance
if (__argc > i + 1)
{
IPCManager::Get().PostMessageToDashboardApp(ipcmsg_action, ipcact_action_do, std::strtoull(__argv[i+1], nullptr, 10));
}
cancel_startup = true;
}
}
}
bool DisplayInitError(vr::EVRInitError vr_init_error, vr::EVROverlayError vr_overlay_error, bool vr_input_success)
{
if (vr_init_error != vr::VRInitError_None)
{
if ( (vr_init_error == vr::VRInitError_Init_HmdNotFound) || (vr_init_error == vr::VRInitError_Init_HmdNotFoundPresenceFailed) )
{
DisplayMsg(L"Failed to init OpenVR: HMD not found.\nRe-launch application with a HMD connected.", L"Desktop+ Error", E_FAIL);
}
else if (vr_init_error == vr::VRInitError_Init_InvalidInterface)
{
DisplayMsg(L"Failed to init OpenVR: Invalid Interface.\nMake sure to have the latest version of SteamVR installed.", L"Desktop+ Error", E_FAIL);
}
else if (vr_init_error != vr::VRInitError_Init_InitCanceledByUser) //Exclude canceled, supposed to be always silent exit
{
std::wstring error_str = L"Failed to init OpenVR: ";
error_str += WStringConvertFromUTF8(vr::VR_GetVRInitErrorAsEnglishDescription(vr_init_error));
error_str += L".";
DisplayMsg(error_str.c_str(), L"Desktop+ Error", E_FAIL);
}
return true;
}
if (vr_overlay_error != vr::VROverlayError_None)
{
std::wstring error_str = L"Failed to init overlay: ";
error_str += WStringConvertFromUTF8(vr::VROverlay()->GetOverlayErrorNameFromEnum(vr_overlay_error));
error_str += L".";
DisplayMsg(error_str.c_str(), L"Desktop+ Error", E_FAIL);
return true;
}
if (!vr_input_success)
{
//VRInput not working is bad, but doesn't stop us from running, so just log it
LOG_F(WARNING, "Failed to load VRInput action manifest. Some input-related functionality will not be available");
}
return false;
}
//
// Entry point for new duplication threads
//
DWORD WINAPI CaptureThreadEntry(_In_ void* Param)
{
// Classes
DISPLAYMANAGER DispMgr;
DUPLICATIONMANAGER DuplMgr;
// D3D objects
ID3D11Texture2D* SharedSurf = nullptr;
IDXGIKeyedMutex* KeyMutex = nullptr;
// Data passed in from thread creation
THREAD_DATA* TData = reinterpret_cast(Param);
// Get desktop
DUPL_RETURN Ret;
HDESK CurrentDesktop = nullptr;
CurrentDesktop = OpenInputDesktop(0, FALSE, GENERIC_ALL);
if (!CurrentDesktop)
{
// We do not have access to the desktop so request a retry
SetEvent(TData->ExpectedErrorEvent);
Ret = DUPL_RETURN_ERROR_EXPECTED;
goto Exit;
}
// Attach desktop to this thread
bool DesktopAttached = SetThreadDesktop(CurrentDesktop) != 0;
CloseDesktop(CurrentDesktop);
CurrentDesktop = nullptr;
if (!DesktopAttached)
{
// We do not have access to the desktop so request a retry
Ret = DUPL_RETURN_ERROR_EXPECTED;
goto Exit;
}
// New display manager
DispMgr.InitD3D(&TData->DxRes);
// Obtain handle to sync shared Surface
HRESULT hr = TData->DxRes.Device->OpenSharedResource(TData->TexSharedHandle, __uuidof(ID3D11Texture2D), reinterpret_cast(&SharedSurf));
if (FAILED (hr))
{
Ret = ProcessFailure(TData->DxRes.Device, L"Opening shared texture failed", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
goto Exit;
}
hr = SharedSurf->QueryInterface(__uuidof(IDXGIKeyedMutex), reinterpret_cast(&KeyMutex));
if (FAILED(hr))
{
Ret = ProcessFailure(nullptr, L"Failed to get keyed mutex interface in spawned thread", L"Desktop+ Error", hr);
goto Exit;
}
D3D11_TEXTURE2D_DESC SharedSurfDesc;
SharedSurf->GetDesc(&SharedSurfDesc);
// Make duplication manager
Ret = DuplMgr.InitDupl(TData->DxRes.Device, TData->Output, TData->WMRIgnoreVScreens, SharedSurfDesc.Format != DXGI_FORMAT_B8G8R8A8_UNORM);
if (Ret != DUPL_RETURN_SUCCESS)
{
goto Exit;
}
// Get output description
DXGI_OUTPUT_DESC DesktopDesc;
RtlZeroMemory(&DesktopDesc, sizeof(DXGI_OUTPUT_DESC));
DuplMgr.GetOutputDesc(&DesktopDesc);
// Main duplication loop
bool WaitToProcessCurrentFrame = false;
FRAME_DATA CurrentData;
while ((WaitForSingleObjectEx(TData->TerminateThreadsEvent, 0, FALSE) == WAIT_TIMEOUT))
{
//Wait if pause event was signaled
if ((WaitForSingleObjectEx(TData->PauseDuplicationEvent, 0, FALSE) == WAIT_OBJECT_0))
{
WaitForSingleObjectEx(TData->ResumeDuplicationEvent, INFINITE, FALSE); //Wait forever. Thread shutdown will also signal resume
}
if (!WaitToProcessCurrentFrame)
{
// Get new frame from desktop duplication
bool TimeOut;
Ret = DuplMgr.GetFrame(&CurrentData, &TimeOut);
if (Ret != DUPL_RETURN_SUCCESS)
{
// An error occurred getting the next frame drop out of loop which
// will check if it was expected or not
break;
}
// Check for timeout
if (TimeOut)
{
// No new frame at the moment
continue;
}
}
// We have a new frame so try and process it
// Try to acquire keyed mutex in order to access shared surface
hr = KeyMutex->AcquireSync(0, 1000);
if (hr == static_cast(WAIT_TIMEOUT))
{
// Can't use shared surface right now, try again later
WaitToProcessCurrentFrame = true;
continue;
}
else if (FAILED(hr))
{
// Generic unknown failure
Ret = ProcessFailure(TData->DxRes.Device, L"Unexpected error acquiring keyed mutex", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
DuplMgr.DoneWithFrame();
break;
}
// We can now process the current frame
WaitToProcessCurrentFrame = false;
// Get mouse info
Ret = DuplMgr.GetMouse(TData->PtrInfo, &(CurrentData.FrameInfo), TData->OffsetX, TData->OffsetY);
if (Ret != DUPL_RETURN_SUCCESS)
{
DuplMgr.DoneWithFrame();
KeyMutex->ReleaseSync(1);
break;
}
// Process new frame
Ret = DispMgr.ProcessFrame(&CurrentData, SharedSurf, TData->OffsetX, TData->OffsetY, &DesktopDesc, *TData->DirtyRegionTotal);
if (Ret != DUPL_RETURN_SUCCESS)
{
DuplMgr.DoneWithFrame();
KeyMutex->ReleaseSync(1);
SetEvent(TData->NewFrameProcessedEvent);
break;
}
// Release acquired keyed mutex
hr = KeyMutex->ReleaseSync(1);
if (FAILED(hr))
{
Ret = ProcessFailure(TData->DxRes.Device, L"Unexpected error releasing the keyed mutex", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
DuplMgr.DoneWithFrame();
break;
}
// Release frame back to desktop duplication
Ret = DuplMgr.DoneWithFrame();
if (Ret != DUPL_RETURN_SUCCESS)
{
break;
}
SetEvent(TData->NewFrameProcessedEvent);
}
Exit:
if (Ret != DUPL_RETURN_SUCCESS)
{
if (Ret == DUPL_RETURN_ERROR_EXPECTED)
{
// The system is in a transition state so request the duplication be restarted
SetEvent(TData->ExpectedErrorEvent);
}
else
{
// Unexpected error so exit the application
SetEvent(TData->UnexpectedErrorEvent);
}
}
if (SharedSurf)
{
SharedSurf->Release();
SharedSurf = nullptr;
}
if (KeyMutex)
{
KeyMutex->Release();
KeyMutex = nullptr;
}
return 0;
}
_Post_satisfies_(return != DUPL_RETURN_SUCCESS)
DUPL_RETURN ProcessFailure(_In_opt_ ID3D11Device* device, _In_ LPCWSTR str, _In_ LPCWSTR title, HRESULT hr, _In_opt_z_ HRESULT* expected_errors)
{
HRESULT translated_hr;
// On an error check if the DX device is lost
if (device)
{
HRESULT device_removed_reason = device->GetDeviceRemovedReason();
switch (device_removed_reason)
{
case DXGI_ERROR_DEVICE_REMOVED:
case DXGI_ERROR_DEVICE_RESET:
case static_cast(E_OUTOFMEMORY):
{
// Our device has been stopped due to an external event on the GPU so map them all to
// device removed and continue processing the condition
translated_hr = DXGI_ERROR_DEVICE_REMOVED;
break;
}
case S_OK:
{
// Device is not removed so use original error
translated_hr = hr;
break;
}
default:
{
// Device is removed but not a error we want to remap
translated_hr = device_removed_reason;
}
}
}
else
{
translated_hr = hr;
}
// Check if this error was expected or not
if (expected_errors)
{
HRESULT* current_result = expected_errors;
while (*current_result != S_OK)
{
if (*(current_result++) == translated_hr)
{
return DUPL_RETURN_ERROR_EXPECTED;
}
}
}
// Error was not expected so display the message box
DisplayMsg(str, title, translated_hr);
return DUPL_RETURN_ERROR_UNEXPECTED;
}
//
// Displays a message
//
void DisplayMsg(_In_ LPCWSTR str, _In_ LPCWSTR title, HRESULT hr)
{
//Generate a proper error message with description from the OS, unless it's E_FAIL which is unspecified anyways and we use it for our own errors passed to this function
std::wstringstream ss;
if (hr != E_FAIL)
{
std::error_code ec(hr, std::system_category());
ss << str << L":\n" << WStringConvertFromLocalEncoding(ec.message().c_str()) << L" (0x" << std::hex << std::setfill(L'0') << std::setw(8) << hr << L")";
}
else
{
ss << str;
}
std::string str_u8 = StringConvertFromUTF16(ss.str().c_str());
//Try having the UI app display it if it's an error
if (!SUCCEEDED(hr))
{
HWND window = (OutputManager::Get() != nullptr) ? OutputManager::Get()->GetWindowHandle() : nullptr;
IPCManager::Get().SendStringToUIApp(configid_str_state_dashboard_error_string, str_u8.c_str(), window);
}
//Get rid of newlines before logging (system error strings comes with CRLF while \n is treated as LF-only before writing to file...)
StringReplaceAll(str_u8, "\r\n", " ");
StringReplaceAll(str_u8, "\n", " ");
VLOG_F((SUCCEEDED(hr)) ? loguru::Verbosity_INFO : loguru::Verbosity_ERROR, str_u8.c_str());
//While we always try to send error messages to the UI app to have it display in VR, show the message on the desktop if UI is not running or VR isn't loaded
if ((!IPCManager::IsUIAppRunning()) || (vr::VROverlay() == nullptr))
{
::MessageBoxW(nullptr, str, title, MB_OK);
}
}
================================================
FILE: src/DesktopPlus/DesktopPlus.manifest
================================================
================================================
FILE: src/DesktopPlus/DesktopPlus.ruleset
================================================
================================================
FILE: src/DesktopPlus/DesktopPlus.vcxproj
================================================
Debug
x64
Release
x64
{05050918-71E9-AF87-0B3C-6F34D471A55A}
DesktopPlus
DesktopPlus
Application
true
Unicode
v142
Application
false
Unicode
v142
true
true
DesktopPlus.ruleset
false
false
DesktopPlus.ruleset
false
LOGURU_FILENAME_WIDTH=30;LOGURU_VERBOSE_SCOPE_ENDINGS=0;DPLUS_SHA=$(DPLUS_SHA);WIN32;_DEBUG;_WINDOWS;_WIN32_WINNT=_WIN32_WINNT_WIN8;%(PreprocessorDefinitions)
MultiThreadedDebug
EditAndContinue
Disabled
$(OutDir);..\Shared;..\DesktopPlus;..\DesktopPlusWinRT
true
%(DisableSpecificWarnings)
true
Windows
d3d11.lib;dxgi.lib;openvr_api.lib;winmm.lib;dwmapi.lib;userenv.lib;DesktopPlusWinRT.lib;%(AdditionalDependencies)
$(SolutionDir)Shared;$(OutputPath)
PerMonitorHighDPIAware
"DesktopPlus_$([System.DateTime]::Now.ToFileTime()), version=1"
LOGURU_FILENAME_WIDTH=30;LOGURU_VERBOSE_SCOPE_ENDINGS=0;DPLUS_SHA=$(DPLUS_SHA);WIN32;NDEBUG;_WINDOWS;_WIN32_WINNT=_WIN32_WINNT_WIN8;%(PreprocessorDefinitions)
MultiThreaded
None
Full
$(OutDir);..\Shared;..\DesktopPlus;..\DesktopPlusWinRT
true
%(DisableSpecificWarnings)
AnySuitable
true
Speed
true
true
true
false
Windows
true
true
d3d11.lib;dxgi.lib;openvr_api.lib;winmm.lib;dwmapi.lib;userenv.lib;DesktopPlusWinRT.lib;%(AdditionalDependencies)
$(SolutionDir)Shared;$(OutputPath)
PerMonitorHighDPIAware
"DesktopPlus_$([System.DateTime]::Now.ToFileTime()), version=1"
$(OutDir);%(AdditionalIncludeDirectories)
$(OutDir);%(AdditionalIncludeDirectories)
PS
Pixel
4.0_level_9_1
PS
4.0_level_9_1
Pixel
$(OutDir)%(Filename).h
$(OutDir)%(Filename).h
PSCURSOR
Pixel
$(OutDir)%(Filename).h
PSCURSOR
$(OutDir)%(Filename).h
Pixel
VS
4.0_level_9_1
VS
4.0_level_9_1
Vertex
$(OutDir)%(Filename).h
$(OutDir)%(Filename).h
Vertex
PreserveNewest
true
%(RecursiveDir)\%(Filename)%(Extension)
================================================
FILE: src/DesktopPlus/DesktopPlus.vcxproj.filters
================================================
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
{0572e516-12c3-49ec-a3ae-3696d6eb2490}
================================================
FILE: src/DesktopPlus/DesktopPlus.vcxproj.user
================================================
$(OutputPath)
WindowsLocalDebugger
$(OutputPath)
WindowsLocalDebugger
================================================
FILE: src/DesktopPlus/DisplayManager.cpp
================================================
#include "DisplayManager.h"
using namespace DirectX;
#include "DPRect.h"
//
// Constructor NULLs out vars
//
DISPLAYMANAGER::DISPLAYMANAGER() : m_Device(nullptr),
m_DeviceContext(nullptr),
m_MoveSurf(nullptr),
m_VertexShader(nullptr),
m_PixelShader(nullptr),
m_InputLayout(nullptr),
m_RTV(nullptr),
m_SamplerLinear(nullptr),
m_DirtyVertexBufferAlloc(nullptr),
m_DirtyVertexBufferAllocSize(0)
{
}
//
// Destructor calls CleanRefs to destroy everything
//
DISPLAYMANAGER::~DISPLAYMANAGER()
{
CleanRefs();
if (m_DirtyVertexBufferAlloc)
{
delete [] m_DirtyVertexBufferAlloc;
m_DirtyVertexBufferAlloc = nullptr;
}
}
//
// Initialize D3D variables
//
void DISPLAYMANAGER::InitD3D(DX_RESOURCES* Data)
{
m_Device = Data->Device;
m_DeviceContext = Data->Context;
m_VertexShader = Data->VertexShader;
m_PixelShader = Data->PixelShader;
m_InputLayout = Data->InputLayout;
m_SamplerLinear = Data->Sampler;
m_Device->AddRef();
m_DeviceContext->AddRef();
m_VertexShader->AddRef();
m_PixelShader->AddRef();
m_InputLayout->AddRef();
m_SamplerLinear->AddRef();
}
//
// Process a given frame and its metadata
//
DUPL_RETURN DISPLAYMANAGER::ProcessFrame(_In_ FRAME_DATA* Data, _Inout_ ID3D11Texture2D* SharedSurf, INT OffsetX, INT OffsetY, _In_ DXGI_OUTPUT_DESC* DeskDesc, _Inout_ DPRect& DirtyRectTotal)
{
DUPL_RETURN Ret = DUPL_RETURN_SUCCESS;
// Process dirties and moves
if (Data->FrameInfo.TotalMetadataBufferSize)
{
D3D11_TEXTURE2D_DESC Desc;
Data->Frame->GetDesc(&Desc);
if (Data->MoveCount)
{
Ret = CopyMove(SharedSurf, reinterpret_cast(Data->MetaData), Data->MoveCount, OffsetX, OffsetY, DeskDesc, Desc.Width, Desc.Height, DirtyRectTotal);
if (Ret != DUPL_RETURN_SUCCESS)
{
return Ret;
}
}
if (Data->DirtyCount)
{
Ret = CopyDirty(Data->Frame, SharedSurf, reinterpret_cast(Data->MetaData + (Data->MoveCount * sizeof(DXGI_OUTDUPL_MOVE_RECT))), Data->DirtyCount, OffsetX, OffsetY, DeskDesc,
DirtyRectTotal);
}
}
return Ret;
}
//
// Returns D3D device being used
//
ID3D11Device* DISPLAYMANAGER::GetDevice()
{
return m_Device;
}
//
// Set appropriate source and destination rects for move rects
//
void DISPLAYMANAGER::SetMoveRect(_Out_ RECT* SrcRect, _Out_ RECT* DestRect, _In_ DXGI_OUTPUT_DESC* DeskDesc, _In_ DXGI_OUTDUPL_MOVE_RECT* MoveRect, INT TexWidth, INT TexHeight)
{
switch (DeskDesc->Rotation)
{
case DXGI_MODE_ROTATION_UNSPECIFIED:
case DXGI_MODE_ROTATION_IDENTITY:
{
SrcRect->left = MoveRect->SourcePoint.x;
SrcRect->top = MoveRect->SourcePoint.y;
SrcRect->right = MoveRect->SourcePoint.x + MoveRect->DestinationRect.right - MoveRect->DestinationRect.left;
SrcRect->bottom = MoveRect->SourcePoint.y + MoveRect->DestinationRect.bottom - MoveRect->DestinationRect.top;
*DestRect = MoveRect->DestinationRect;
break;
}
case DXGI_MODE_ROTATION_ROTATE90:
{
SrcRect->left = TexHeight - (MoveRect->SourcePoint.y + MoveRect->DestinationRect.bottom - MoveRect->DestinationRect.top);
SrcRect->top = MoveRect->SourcePoint.x;
SrcRect->right = TexHeight - MoveRect->SourcePoint.y;
SrcRect->bottom = MoveRect->SourcePoint.x + MoveRect->DestinationRect.right - MoveRect->DestinationRect.left;
DestRect->left = TexHeight - MoveRect->DestinationRect.bottom;
DestRect->top = MoveRect->DestinationRect.left;
DestRect->right = TexHeight - MoveRect->DestinationRect.top;
DestRect->bottom = MoveRect->DestinationRect.right;
break;
}
case DXGI_MODE_ROTATION_ROTATE180:
{
SrcRect->left = TexWidth - (MoveRect->SourcePoint.x + MoveRect->DestinationRect.right - MoveRect->DestinationRect.left);
SrcRect->top = TexHeight - (MoveRect->SourcePoint.y + MoveRect->DestinationRect.bottom - MoveRect->DestinationRect.top);
SrcRect->right = TexWidth - MoveRect->SourcePoint.x;
SrcRect->bottom = TexHeight - MoveRect->SourcePoint.y;
DestRect->left = TexWidth - MoveRect->DestinationRect.right;
DestRect->top = TexHeight - MoveRect->DestinationRect.bottom;
DestRect->right = TexWidth - MoveRect->DestinationRect.left;
DestRect->bottom = TexHeight - MoveRect->DestinationRect.top;
break;
}
case DXGI_MODE_ROTATION_ROTATE270:
{
SrcRect->left = MoveRect->SourcePoint.x;
SrcRect->top = TexWidth - (MoveRect->SourcePoint.x + MoveRect->DestinationRect.right - MoveRect->DestinationRect.left);
SrcRect->right = MoveRect->SourcePoint.y + MoveRect->DestinationRect.bottom - MoveRect->DestinationRect.top;
SrcRect->bottom = TexWidth - MoveRect->SourcePoint.x;
DestRect->left = MoveRect->DestinationRect.top;
DestRect->top = TexWidth - MoveRect->DestinationRect.right;
DestRect->right = MoveRect->DestinationRect.bottom;
DestRect->bottom = TexWidth - MoveRect->DestinationRect.left;
break;
}
default:
{
RtlZeroMemory(DestRect, sizeof(RECT));
RtlZeroMemory(SrcRect, sizeof(RECT));
break;
}
}
}
//
// Copy move rectangles
//
DUPL_RETURN DISPLAYMANAGER::CopyMove(_Inout_ ID3D11Texture2D* SharedSurf, _In_reads_(MoveCount) DXGI_OUTDUPL_MOVE_RECT* MoveBuffer, UINT MoveCount, INT OffsetX, INT OffsetY, _In_ DXGI_OUTPUT_DESC* DeskDesc,
INT TexWidth, INT TexHeight, _Inout_ DPRect& DirtyRectTotal)
{
D3D11_TEXTURE2D_DESC FullDesc;
SharedSurf->GetDesc(&FullDesc);
// Make new intermediate surface to copy into for moving
if (!m_MoveSurf)
{
D3D11_TEXTURE2D_DESC MoveDesc;
MoveDesc = FullDesc;
MoveDesc.Width = DeskDesc->DesktopCoordinates.right - DeskDesc->DesktopCoordinates.left;
MoveDesc.Height = DeskDesc->DesktopCoordinates.bottom - DeskDesc->DesktopCoordinates.top;
MoveDesc.BindFlags = D3D11_BIND_RENDER_TARGET;
MoveDesc.MiscFlags = 0;
HRESULT hr = m_Device->CreateTexture2D(&MoveDesc, nullptr, &m_MoveSurf);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create staging texture for move rects", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
}
for (UINT i = 0; i < MoveCount; ++i)
{
RECT SrcRect;
RECT DestRect;
SetMoveRect(&SrcRect, &DestRect, DeskDesc, &(MoveBuffer[i]), TexWidth, TexHeight);
// Copy rect out of shared surface
D3D11_BOX Box;
Box.left = SrcRect.left + DeskDesc->DesktopCoordinates.left - OffsetX;
Box.top = SrcRect.top + DeskDesc->DesktopCoordinates.top - OffsetY;
Box.front = 0;
Box.right = SrcRect.right + DeskDesc->DesktopCoordinates.left - OffsetX;
Box.bottom = SrcRect.bottom + DeskDesc->DesktopCoordinates.top - OffsetY;
Box.back = 1;
m_DeviceContext->CopySubresourceRegion(m_MoveSurf, 0, SrcRect.left, SrcRect.top, 0, SharedSurf, 0, &Box);
// Copy back to shared surface
Box.left = SrcRect.left;
Box.top = SrcRect.top;
Box.front = 0;
Box.right = SrcRect.right;
Box.bottom = SrcRect.bottom;
Box.back = 1;
//Adjust by desktop and destination offsets
DestRect.left += DeskDesc->DesktopCoordinates.left - OffsetX;
DestRect.top += DeskDesc->DesktopCoordinates.top - OffsetY;
m_DeviceContext->CopySubresourceRegion(SharedSurf, 0, DestRect.left, DestRect.top, 0, m_MoveSurf, 0, &Box);
//Add rect to total dirty region rect
DPRect drect(DestRect.left, DestRect.top, (int)Box.right, (int)Box.bottom);
(DirtyRectTotal.GetTL().x == -1) ? DirtyRectTotal = drect : DirtyRectTotal.Add(drect);
}
return DUPL_RETURN_SUCCESS;
}
//
// Sets up vertices for dirty rects for rotated desktops
//
#pragma warning(push)
#pragma warning(disable:__WARNING_USING_UNINIT_VAR) // false positives in SetDirtyVert due to tool bug
void DISPLAYMANAGER::SetDirtyVert(_Out_writes_(NUMVERTICES) VERTEX* Vertices, _In_ RECT* Dirty, INT OffsetX, INT OffsetY, _In_ DXGI_OUTPUT_DESC* DeskDesc, _In_ D3D11_TEXTURE2D_DESC* FullDesc,
_In_ D3D11_TEXTURE2D_DESC* ThisDesc, _Inout_ DPRect& DirtyRectTotal)
{
FLOAT CenterX = FullDesc->Width / 2.0f;
FLOAT CenterY = FullDesc->Height / 2.0f;
INT Width = DeskDesc->DesktopCoordinates.right - DeskDesc->DesktopCoordinates.left;
INT Height = DeskDesc->DesktopCoordinates.bottom - DeskDesc->DesktopCoordinates.top;
// Rotation compensated destination rect
RECT DestDirty = *Dirty;
// Set appropriate coordinates compensated for rotation
switch (DeskDesc->Rotation)
{
case DXGI_MODE_ROTATION_ROTATE90:
{
DestDirty.left = Width - Dirty->bottom;
DestDirty.top = Dirty->left;
DestDirty.right = Width - Dirty->top;
DestDirty.bottom = Dirty->right;
Vertices[0].TexCoord = XMFLOAT2(Dirty->right / static_cast(ThisDesc->Width), Dirty->bottom / static_cast(ThisDesc->Height));
Vertices[1].TexCoord = XMFLOAT2(Dirty->left / static_cast(ThisDesc->Width), Dirty->bottom / static_cast(ThisDesc->Height));
Vertices[2].TexCoord = XMFLOAT2(Dirty->right / static_cast(ThisDesc->Width), Dirty->top / static_cast(ThisDesc->Height));
Vertices[5].TexCoord = XMFLOAT2(Dirty->left / static_cast(ThisDesc->Width), Dirty->top / static_cast(ThisDesc->Height));
break;
}
case DXGI_MODE_ROTATION_ROTATE180:
{
DestDirty.left = Width - Dirty->right;
DestDirty.top = Height - Dirty->bottom;
DestDirty.right = Width - Dirty->left;
DestDirty.bottom = Height - Dirty->top;
Vertices[0].TexCoord = XMFLOAT2(Dirty->right / static_cast(ThisDesc->Width), Dirty->top / static_cast(ThisDesc->Height));
Vertices[1].TexCoord = XMFLOAT2(Dirty->right / static_cast(ThisDesc->Width), Dirty->bottom / static_cast(ThisDesc->Height));
Vertices[2].TexCoord = XMFLOAT2(Dirty->left / static_cast(ThisDesc->Width), Dirty->top / static_cast(ThisDesc->Height));
Vertices[5].TexCoord = XMFLOAT2(Dirty->left / static_cast(ThisDesc->Width), Dirty->bottom / static_cast(ThisDesc->Height));
break;
}
case DXGI_MODE_ROTATION_ROTATE270:
{
DestDirty.left = Dirty->top;
DestDirty.top = Height - Dirty->right;
DestDirty.right = Dirty->bottom;
DestDirty.bottom = Height - Dirty->left;
Vertices[0].TexCoord = XMFLOAT2(Dirty->left / static_cast(ThisDesc->Width), Dirty->top / static_cast(ThisDesc->Height));
Vertices[1].TexCoord = XMFLOAT2(Dirty->right / static_cast(ThisDesc->Width), Dirty->top / static_cast(ThisDesc->Height));
Vertices[2].TexCoord = XMFLOAT2(Dirty->left / static_cast(ThisDesc->Width), Dirty->bottom / static_cast(ThisDesc->Height));
Vertices[5].TexCoord = XMFLOAT2(Dirty->right / static_cast(ThisDesc->Width), Dirty->bottom / static_cast(ThisDesc->Height));
break;
}
default:
assert(false); // drop through
case DXGI_MODE_ROTATION_UNSPECIFIED:
case DXGI_MODE_ROTATION_IDENTITY:
{
Vertices[0].TexCoord = XMFLOAT2(Dirty->left / static_cast(ThisDesc->Width), Dirty->bottom / static_cast(ThisDesc->Height));
Vertices[1].TexCoord = XMFLOAT2(Dirty->left / static_cast(ThisDesc->Width), Dirty->top / static_cast(ThisDesc->Height));
Vertices[2].TexCoord = XMFLOAT2(Dirty->right / static_cast(ThisDesc->Width), Dirty->bottom / static_cast(ThisDesc->Height));
Vertices[5].TexCoord = XMFLOAT2(Dirty->right / static_cast(ThisDesc->Width), Dirty->top / static_cast(ThisDesc->Height));
break;
}
}
// Set positions
Vertices[0].Pos = XMFLOAT3((DestDirty.left + DeskDesc->DesktopCoordinates.left - OffsetX - CenterX) / CenterX,
-1 * (DestDirty.bottom + DeskDesc->DesktopCoordinates.top - OffsetY - CenterY) / CenterY,
0.0f);
Vertices[1].Pos = XMFLOAT3((DestDirty.left + DeskDesc->DesktopCoordinates.left - OffsetX - CenterX) / CenterX,
-1 * (DestDirty.top + DeskDesc->DesktopCoordinates.top - OffsetY - CenterY) / CenterY,
0.0f);
Vertices[2].Pos = XMFLOAT3((DestDirty.right + DeskDesc->DesktopCoordinates.left - OffsetX - CenterX) / CenterX,
-1 * (DestDirty.bottom + DeskDesc->DesktopCoordinates.top - OffsetY - CenterY) / CenterY,
0.0f);
Vertices[3].Pos = Vertices[2].Pos;
Vertices[4].Pos = Vertices[1].Pos;
Vertices[5].Pos = XMFLOAT3((DestDirty.right + DeskDesc->DesktopCoordinates.left - OffsetX - CenterX) / CenterX,
-1 * (DestDirty.top + DeskDesc->DesktopCoordinates.top - OffsetY - CenterY) / CenterY,
0.0f);
Vertices[3].TexCoord = Vertices[2].TexCoord;
Vertices[4].TexCoord = Vertices[1].TexCoord;
//Add rect to total dirty region rect
DPRect drect(DestDirty.left, DestDirty.top, DestDirty.right, DestDirty.bottom);
drect.Translate({DeskDesc->DesktopCoordinates.left - OffsetX, DeskDesc->DesktopCoordinates.top - OffsetY});
(DirtyRectTotal.GetTL().x == -1) ? DirtyRectTotal = drect : DirtyRectTotal.Add(drect);
}
#pragma warning(pop) // re-enable __WARNING_USING_UNINIT_VAR
//
// Copies dirty rectangles
//
DUPL_RETURN DISPLAYMANAGER::CopyDirty(_In_ ID3D11Texture2D* SrcSurface, _Inout_ ID3D11Texture2D* SharedSurf, _In_reads_(DirtyCount) RECT* DirtyBuffer, UINT DirtyCount, INT OffsetX, INT OffsetY,
_In_ DXGI_OUTPUT_DESC* DeskDesc, _Inout_ DPRect& DirtyRectTotal)
{
HRESULT hr;
D3D11_TEXTURE2D_DESC FullDesc;
SharedSurf->GetDesc(&FullDesc);
D3D11_TEXTURE2D_DESC ThisDesc;
SrcSurface->GetDesc(&ThisDesc);
if (!m_RTV)
{
hr = m_Device->CreateRenderTargetView(SharedSurf, nullptr, &m_RTV);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create render target view for dirty rects", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
}
D3D11_SHADER_RESOURCE_VIEW_DESC ShaderDesc;
ShaderDesc.Format = ThisDesc.Format;
ShaderDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
ShaderDesc.Texture2D.MostDetailedMip = ThisDesc.MipLevels - 1;
ShaderDesc.Texture2D.MipLevels = ThisDesc.MipLevels;
// Create new shader resource view
ID3D11ShaderResourceView* ShaderResource = nullptr;
hr = m_Device->CreateShaderResourceView(SrcSurface, &ShaderDesc, &ShaderResource);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create shader resource view for dirty rects", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
FLOAT BlendFactor[4] = {0.f, 0.f, 0.f, 0.f};
m_DeviceContext->OMSetBlendState(nullptr, BlendFactor, 0xFFFFFFFF);
m_DeviceContext->OMSetRenderTargets(1, &m_RTV, nullptr);
m_DeviceContext->VSSetShader(m_VertexShader, nullptr, 0);
m_DeviceContext->PSSetShader(m_PixelShader, nullptr, 0);
m_DeviceContext->PSSetShaderResources(0, 1, &ShaderResource);
m_DeviceContext->PSSetSamplers(0, 1, &m_SamplerLinear);
m_DeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// Create space for vertices for the dirty rects if the current space isn't large enough
UINT BytesNeeded = sizeof(VERTEX) * NUMVERTICES * DirtyCount;
if (BytesNeeded > m_DirtyVertexBufferAllocSize)
{
if (m_DirtyVertexBufferAlloc)
{
delete [] m_DirtyVertexBufferAlloc;
}
m_DirtyVertexBufferAlloc = new (std::nothrow) BYTE[BytesNeeded];
if (!m_DirtyVertexBufferAlloc)
{
m_DirtyVertexBufferAllocSize = 0;
return ProcessFailure(nullptr, L"Failed to allocate memory for dirty vertex buffer", L"Desktop+ Error", E_OUTOFMEMORY);
}
m_DirtyVertexBufferAllocSize = BytesNeeded;
}
// Fill them in
VERTEX* DirtyVertex = reinterpret_cast(m_DirtyVertexBufferAlloc);
for (UINT i = 0; i < DirtyCount; ++i, DirtyVertex += NUMVERTICES)
{
SetDirtyVert(DirtyVertex, &(DirtyBuffer[i]), OffsetX, OffsetY, DeskDesc, &FullDesc, &ThisDesc, DirtyRectTotal);
}
// Create vertex buffer
D3D11_BUFFER_DESC BufferDesc;
RtlZeroMemory(&BufferDesc, sizeof(BufferDesc));
BufferDesc.Usage = D3D11_USAGE_DEFAULT;
BufferDesc.ByteWidth = BytesNeeded;
BufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
BufferDesc.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA InitData;
RtlZeroMemory(&InitData, sizeof(InitData));
InitData.pSysMem = m_DirtyVertexBufferAlloc;
ID3D11Buffer* VertBuf = nullptr;
hr = m_Device->CreateBuffer(&BufferDesc, &InitData, &VertBuf);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create vertex buffer in dirty rect processing", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
UINT Stride = sizeof(VERTEX);
UINT Offset = 0;
m_DeviceContext->IASetVertexBuffers(0, 1, &VertBuf, &Stride, &Offset);
D3D11_VIEWPORT VP;
VP.Width = static_cast(FullDesc.Width);
VP.Height = static_cast(FullDesc.Height);
VP.MinDepth = 0.0f;
VP.MaxDepth = 1.0f;
VP.TopLeftX = 0.0f;
VP.TopLeftY = 0.0f;
m_DeviceContext->RSSetViewports(1, &VP);
m_DeviceContext->Draw(NUMVERTICES * DirtyCount, 0);
VertBuf->Release();
VertBuf = nullptr;
ShaderResource->Release();
ShaderResource = nullptr;
return DUPL_RETURN_SUCCESS;
}
//
// Clean all references
//
void DISPLAYMANAGER::CleanRefs()
{
if (m_DeviceContext)
{
m_DeviceContext->Release();
m_DeviceContext = nullptr;
}
if (m_Device)
{
m_Device->Release();
m_Device = nullptr;
}
if (m_MoveSurf)
{
m_MoveSurf->Release();
m_MoveSurf = nullptr;
}
if (m_VertexShader)
{
m_VertexShader->Release();
m_VertexShader = nullptr;
}
if (m_PixelShader)
{
m_PixelShader->Release();
m_PixelShader = nullptr;
}
if (m_InputLayout)
{
m_InputLayout->Release();
m_InputLayout = nullptr;
}
if (m_SamplerLinear)
{
m_SamplerLinear->Release();
m_SamplerLinear = nullptr;
}
if (m_RTV)
{
m_RTV->Release();
m_RTV = nullptr;
}
}
================================================
FILE: src/DesktopPlus/DisplayManager.h
================================================
#ifndef _DISPLAYMANAGER_H_
#define _DISPLAYMANAGER_H_
#include "CommonTypes.h"
//
// Handles the task of processing frames
//
class DISPLAYMANAGER
{
public:
DISPLAYMANAGER();
~DISPLAYMANAGER();
void InitD3D(DX_RESOURCES* Data);
ID3D11Device* GetDevice();
DUPL_RETURN ProcessFrame(_In_ FRAME_DATA* Data, _Inout_ ID3D11Texture2D* SharedSurf, INT OffsetX, INT OffsetY, _In_ DXGI_OUTPUT_DESC* DeskDesc, _Inout_ DPRect& DirtyRectTotal);
void CleanRefs();
private:
// methods
DUPL_RETURN CopyDirty(_In_ ID3D11Texture2D* SrcSurface, _Inout_ ID3D11Texture2D* SharedSurf, _In_reads_(DirtyCount) RECT* DirtyBuffer, UINT DirtyCount, INT OffsetX, INT OffsetY,
_In_ DXGI_OUTPUT_DESC* DeskDesc, _Inout_ DPRect& DirtyRectTotal);
DUPL_RETURN CopyMove(_Inout_ ID3D11Texture2D* SharedSurf, _In_reads_(MoveCount) DXGI_OUTDUPL_MOVE_RECT* MoveBuffer, UINT MoveCount, INT OffsetX, INT OffsetY, _In_ DXGI_OUTPUT_DESC* DeskDesc,
INT TexWidth, INT TexHeight, _Inout_ DPRect& DirtyRectTotal);
void SetDirtyVert(_Out_writes_(NUMVERTICES) VERTEX* Vertices, _In_ RECT* Dirty, INT OffsetX, INT OffsetY, _In_ DXGI_OUTPUT_DESC* DeskDesc, _In_ D3D11_TEXTURE2D_DESC* FullDesc,
_In_ D3D11_TEXTURE2D_DESC* ThisDesc, _Inout_ DPRect& DirtyRectTotal);
void SetMoveRect(_Out_ RECT* SrcRect, _Out_ RECT* DestRect, _In_ DXGI_OUTPUT_DESC* DeskDesc, _In_ DXGI_OUTDUPL_MOVE_RECT* MoveRect, INT TexWidth, INT TexHeight);
// variables
ID3D11Device* m_Device;
ID3D11DeviceContext* m_DeviceContext;
ID3D11Texture2D* m_MoveSurf;
ID3D11VertexShader* m_VertexShader;
ID3D11PixelShader* m_PixelShader;
ID3D11InputLayout* m_InputLayout;
ID3D11RenderTargetView* m_RTV;
ID3D11SamplerState* m_SamplerLinear;
BYTE* m_DirtyVertexBufferAlloc;
UINT m_DirtyVertexBufferAllocSize;
};
#endif
================================================
FILE: src/DesktopPlus/DuplicationManager.cpp
================================================
#include "DuplicationManager.h"
#include
#include
//Keep building with 10.0.17763.0 / 1809 SDK optional
#ifdef NTDDI_WIN10_RS5
#include
#else
#define DPLUS_DUP_NO_HDR
#endif
//
// Constructor sets up references / variables
//
DUPLICATIONMANAGER::DUPLICATIONMANAGER() : m_DeskDupl(nullptr),
m_AcquiredDesktopImage(nullptr),
m_MetaDataBuffer(nullptr),
m_MetaDataSize(0),
m_OutputNumber(0),
m_Device(nullptr)
{
RtlZeroMemory(&m_OutputDesc, sizeof(m_OutputDesc));
}
//
// Destructor simply calls CleanRefs to destroy everything
//
DUPLICATIONMANAGER::~DUPLICATIONMANAGER()
{
if (m_DeskDupl)
{
m_DeskDupl->Release();
m_DeskDupl = nullptr;
}
if (m_AcquiredDesktopImage)
{
m_AcquiredDesktopImage->Release();
m_AcquiredDesktopImage = nullptr;
}
if (m_MetaDataBuffer)
{
delete [] m_MetaDataBuffer;
m_MetaDataBuffer = nullptr;
}
if (m_Device)
{
m_Device->Release();
m_Device = nullptr;
}
}
//
// Initialize duplication interfaces
//
DUPL_RETURN DUPLICATIONMANAGER::InitDupl(_In_ ID3D11Device* Device, UINT Output, bool WMRIgnoreVScreens, bool UseHDR)
{
m_OutputNumber = Output;
#ifdef DPLUS_DUP_NO_HDR
UseHDR = false;
#endif
// Take a reference on the device
m_Device = Device;
m_Device->AddRef();
//Enumerate adapters the same way the main thread does so IDs match in multi-GPU situations
//Desktop Duplication of desktops spanned across multiple GPUs is not supported right now, however
//DuplicateOutput() will fail if the adapter for the output doesn't match Device
Microsoft::WRL::ComPtr factory_ptr;
Microsoft::WRL::ComPtr adapter_ptr_output;
int output_id_adapter = Output; //Output ID on the adapter actually used. Only different from initial Output if there's desktops across multiple GPUs
HRESULT hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&factory_ptr);
if (!FAILED(hr))
{
Microsoft::WRL::ComPtr adapter_ptr;
UINT i = 0;
int output_count = 0;
while (factory_ptr->EnumAdapters(i, &adapter_ptr) != DXGI_ERROR_NOT_FOUND)
{
//Check if this a WMR virtual display adapter and skip it when the option is enabled
if (WMRIgnoreVScreens)
{
DXGI_ADAPTER_DESC adapter_desc;
adapter_ptr->GetDesc(&adapter_desc);
if (wcscmp(adapter_desc.Description, L"Virtual Display Adapter") == 0)
{
++i;
continue;
}
}
//Count the available outputs
Microsoft::WRL::ComPtr output_ptr;
UINT output_index = 0;
while (adapter_ptr->EnumOutputs(output_index, &output_ptr) != DXGI_ERROR_NOT_FOUND)
{
//Check if this happens to be the output we're looking for
if ( (adapter_ptr_output == nullptr) && (Output == output_count) )
{
adapter_ptr_output = adapter_ptr;
output_id_adapter = output_index;
}
++output_count;
++output_index;
}
++i;
}
}
if (adapter_ptr_output == nullptr)
{
return ProcessFailure(m_Device, L"Failed to get the output's DXGI adapter", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
//Get output
Microsoft::WRL::ComPtr DxgiOutput;
hr = adapter_ptr_output->EnumOutputs(output_id_adapter, &DxgiOutput);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to get specified DXGI output", L"Desktop+ Error", hr, EnumOutputsExpectedErrors);
}
DxgiOutput->GetDesc(&m_OutputDesc);
//Create desktop duplication
if (!UseHDR)
{
Microsoft::WRL::ComPtr DxgiOutput1;
hr = DxgiOutput.As(&DxgiOutput1);
if (FAILED(hr))
{
return ProcessFailure(nullptr, L"Failed to get output as DxgiOutput1", L"Desktop+ Error", hr);
}
hr = DxgiOutput1->DuplicateOutput(m_Device, &m_DeskDupl);
if (FAILED(hr))
{
if (hr == DXGI_ERROR_NOT_CURRENTLY_AVAILABLE)
{
ProcessFailure(m_Device, L"There is already the maximum number of applications using the Desktop Duplication API running, please close one of those applications and then try again", L"Desktop+ Error", hr);
return DUPL_RETURN_ERROR_UNEXPECTED;
}
return ProcessFailure(m_Device, L"Failed to get duplicate output", L"Desktop+ Error", hr, CreateDuplicationExpectedErrors);
}
}
else
{
#ifndef DPLUS_DUP_NO_HDR
Microsoft::WRL::ComPtr DxgiOutput5;
hr = DxgiOutput.As(&DxgiOutput5);
if (FAILED(hr))
{
return ProcessFailure(nullptr, L"Failed to get output as DxgiOutput5", L"Desktop+ Error", hr);
}
const DXGI_FORMAT supported_formats[] = {DXGI_FORMAT_R16G16B16A16_FLOAT};
hr = DxgiOutput5->DuplicateOutput1(m_Device, 0, 1, supported_formats, &m_DeskDupl);
if (FAILED(hr))
{
if (hr == DXGI_ERROR_NOT_CURRENTLY_AVAILABLE)
{
ProcessFailure(m_Device, L"There is already the maximum number of applications using the Desktop Duplication API running, please close one of those applications and then try again", L"Desktop+ Error", hr);
return DUPL_RETURN_ERROR_UNEXPECTED;
}
return ProcessFailure(m_Device, L"Failed to get duplicate output", L"Desktop+ Error", hr, CreateDuplicationExpectedErrors);
}
#endif
}
return DUPL_RETURN_SUCCESS;
}
//
// Retrieves mouse info and write it into PtrInfo
//
DUPL_RETURN DUPLICATIONMANAGER::GetMouse(_Inout_ PTR_INFO* PtrInfo, _In_ DXGI_OUTDUPL_FRAME_INFO* FrameInfo, INT OffsetX, INT OffsetY)
{
// A non-zero mouse update timestamp indicates that there is a mouse position update and optionally a shape change
if (FrameInfo->LastMouseUpdateTime.QuadPart == 0)
{
return DUPL_RETURN_SUCCESS;
}
bool UpdatePosition = true;
// Make sure we don't update pointer position wrongly
// If pointer is invisible, make sure we did not get an update from another output that the last time that said pointer
// was visible, if so, don't set it to invisible or update.
if (!FrameInfo->PointerPosition.Visible && (PtrInfo->WhoUpdatedPositionLast != m_OutputNumber))
{
UpdatePosition = false;
}
// If two outputs both say they have a visible, only update if new update has newer timestamp
if (FrameInfo->PointerPosition.Visible && PtrInfo->Visible && (PtrInfo->WhoUpdatedPositionLast != m_OutputNumber) && (PtrInfo->LastTimeStamp.QuadPart > FrameInfo->LastMouseUpdateTime.QuadPart))
{
UpdatePosition = false;
}
// Update position
if (UpdatePosition)
{
PtrInfo->Position.x = FrameInfo->PointerPosition.Position.x + m_OutputDesc.DesktopCoordinates.left - OffsetX;
PtrInfo->Position.y = FrameInfo->PointerPosition.Position.y + m_OutputDesc.DesktopCoordinates.top - OffsetY;
PtrInfo->WhoUpdatedPositionLast = m_OutputNumber;
PtrInfo->LastTimeStamp = FrameInfo->LastMouseUpdateTime;
PtrInfo->Visible = FrameInfo->PointerPosition.Visible != 0;
//If pointer is not visible, set the hotspot to 0,0
if (!PtrInfo->Visible)
{
PtrInfo->ShapeInfo.HotSpot.x = 0;
PtrInfo->ShapeInfo.HotSpot.y = 0;
}
}
// No new shape
if (FrameInfo->PointerShapeBufferSize == 0)
{
PtrInfo->CursorShapeChanged = false;
return DUPL_RETURN_SUCCESS;
}
PtrInfo->CursorShapeChanged = true;
// Get shape
PtrInfo->ShapeBuffer.resize(FrameInfo->PointerShapeBufferSize, 0);
UINT BufferSizeRequired;
HRESULT hr = m_DeskDupl->GetFramePointerShape(FrameInfo->PointerShapeBufferSize, PtrInfo->ShapeBuffer.data(), &BufferSizeRequired, &(PtrInfo->ShapeInfo));
if (FAILED(hr))
{
PtrInfo->ShapeBuffer.clear();
return ProcessFailure(m_Device, L"Failed to get frame pointer shape", L"Desktop+ Error", hr, FrameInfoExpectedErrors);
}
return DUPL_RETURN_SUCCESS;
}
//
// Get next frame and write it into Data
//
_Success_(*Timeout == false && return == DUPL_RETURN_SUCCESS)
DUPL_RETURN DUPLICATIONMANAGER::GetFrame(_Out_ FRAME_DATA* Data, _Out_ bool* Timeout)
{
IDXGIResource* DesktopResource = nullptr;
DXGI_OUTDUPL_FRAME_INFO FrameInfo;
// Get new frame
HRESULT hr = m_DeskDupl->AcquireNextFrame(100, &FrameInfo, &DesktopResource);
if (hr == DXGI_ERROR_WAIT_TIMEOUT)
{
*Timeout = true;
return DUPL_RETURN_SUCCESS;
}
*Timeout = false;
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to acquire next frame", L"Desktop+ Error", hr, FrameInfoExpectedErrors);
}
// If still holding old frame, destroy it
if (m_AcquiredDesktopImage)
{
m_AcquiredDesktopImage->Release();
m_AcquiredDesktopImage = nullptr;
}
// QI for IDXGIResource
hr = DesktopResource->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast(&m_AcquiredDesktopImage));
DesktopResource->Release();
DesktopResource = nullptr;
if (FAILED(hr))
{
return ProcessFailure(nullptr, L"Failed to QI for ID3D11Texture2D from acquired IDXGIResource", L"Desktop+ Error", hr);
}
// Get metadata
if (FrameInfo.TotalMetadataBufferSize)
{
// Old buffer too small
if (FrameInfo.TotalMetadataBufferSize > m_MetaDataSize)
{
if (m_MetaDataBuffer)
{
delete [] m_MetaDataBuffer;
m_MetaDataBuffer = nullptr;
}
m_MetaDataBuffer = new (std::nothrow) BYTE[FrameInfo.TotalMetadataBufferSize];
if (!m_MetaDataBuffer)
{
m_MetaDataSize = 0;
Data->MoveCount = 0;
Data->DirtyCount = 0;
return ProcessFailure(nullptr, L"Failed to allocate memory for metadata", L"Desktop+ Error", E_OUTOFMEMORY);
}
m_MetaDataSize = FrameInfo.TotalMetadataBufferSize;
}
UINT BufSize = FrameInfo.TotalMetadataBufferSize;
// Get move rectangles
hr = m_DeskDupl->GetFrameMoveRects(BufSize, reinterpret_cast(m_MetaDataBuffer), &BufSize);
if (FAILED(hr))
{
Data->MoveCount = 0;
Data->DirtyCount = 0;
return ProcessFailure(nullptr, L"Failed to get frame move rects", L"Desktop+ Error", hr, FrameInfoExpectedErrors);
}
Data->MoveCount = BufSize / sizeof(DXGI_OUTDUPL_MOVE_RECT);
BYTE* DirtyRects = m_MetaDataBuffer + BufSize;
BufSize = FrameInfo.TotalMetadataBufferSize - BufSize;
// Get dirty rectangles
hr = m_DeskDupl->GetFrameDirtyRects(BufSize, reinterpret_cast(DirtyRects), &BufSize);
if (FAILED(hr))
{
Data->MoveCount = 0;
Data->DirtyCount = 0;
return ProcessFailure(nullptr, L"Failed to get frame dirty rects", L"Desktop+ Error", hr, FrameInfoExpectedErrors);
}
Data->DirtyCount = BufSize / sizeof(RECT);
Data->MetaData = m_MetaDataBuffer;
}
Data->Frame = m_AcquiredDesktopImage;
Data->FrameInfo = FrameInfo;
return DUPL_RETURN_SUCCESS;
}
//
// Release frame
//
DUPL_RETURN DUPLICATIONMANAGER::DoneWithFrame()
{
HRESULT hr = m_DeskDupl->ReleaseFrame();
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to release frame", L"Desktop+ Error", hr, FrameInfoExpectedErrors);
}
if (m_AcquiredDesktopImage)
{
m_AcquiredDesktopImage->Release();
m_AcquiredDesktopImage = nullptr;
}
return DUPL_RETURN_SUCCESS;
}
//
// Gets output desc into DescPtr
//
void DUPLICATIONMANAGER::GetOutputDesc(_Out_ DXGI_OUTPUT_DESC* DescPtr)
{
*DescPtr = m_OutputDesc;
}
================================================
FILE: src/DesktopPlus/DuplicationManager.h
================================================
#ifndef _DUPLICATIONMANAGER_H_
#define _DUPLICATIONMANAGER_H_
#include "CommonTypes.h"
//
// Handles the task of duplicating an output.
//
class DUPLICATIONMANAGER
{
public:
DUPLICATIONMANAGER();
~DUPLICATIONMANAGER();
_Success_(*Timeout == false && return == DUPL_RETURN_SUCCESS) DUPL_RETURN GetFrame(_Out_ FRAME_DATA* Data, _Out_ bool* Timeout);
DUPL_RETURN DoneWithFrame();
DUPL_RETURN InitDupl(_In_ ID3D11Device* Device, UINT Output, bool WMRIgnoreVScreens, bool UseHDR);
DUPL_RETURN GetMouse(_Inout_ PTR_INFO* PtrInfo, _In_ DXGI_OUTDUPL_FRAME_INFO* FrameInfo, INT OffsetX, INT OffsetY);
void GetOutputDesc(_Out_ DXGI_OUTPUT_DESC* DescPtr);
private:
// vars
IDXGIOutputDuplication* m_DeskDupl;
ID3D11Texture2D* m_AcquiredDesktopImage;
_Field_size_bytes_(m_MetaDataSize) BYTE* m_MetaDataBuffer;
UINT m_MetaDataSize;
UINT m_OutputNumber;
DXGI_OUTPUT_DESC m_OutputDesc;
ID3D11Device* m_Device;
};
#endif
================================================
FILE: src/DesktopPlus/ElevatedMode.cpp
================================================
#include "ElevatedMode.h"
#include
#include "InterprocessMessaging.h"
#include "InputSimulator.h"
#include "WindowManager.h"
#include "Util.h"
#include "Logging.h"
static bool g_ElevatedMode_ComInitDone = false;
LRESULT CALLBACK WndProcElevated(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
bool HandleIPCMessage(MSG msg);
int ElevatedModeEnter(HINSTANCE hinstance)
{
DPLog_Init("DesktopPlusElevatedMode");
//Don't run if the dashboard app is not running or process isn't even elevated
if (!IPCManager::Get().IsDashboardAppRunning())
{
LOG_F(INFO, "Started in elevated mode, but dashboard process is not running. Exiting...");
return E_NOT_VALID_STATE;
}
else if (!IsProcessElevated())
{
LOG_F(INFO, "Started in elevated mode, but process was not launched elevated. Exiting...");
return E_NOT_VALID_STATE;
}
LOG_F(INFO, "Desktop+ running in elevated mode");
//Register class
WNDCLASSEXW wc;
wc.cbSize = sizeof(WNDCLASSEXW);
wc.style = 0;
wc.lpfnWndProc = WndProcElevated;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hinstance;
wc.hIcon = nullptr;
wc.hCursor = nullptr;
wc.hbrBackground = nullptr;
wc.lpszMenuName = nullptr;
wc.lpszClassName = g_WindowClassNameElevatedMode;
wc.hIconSm = nullptr;
if (!::RegisterClassExW(&wc))
{
return E_FAIL;
}
//Create window
HWND window_handle = ::CreateWindowW(g_WindowClassNameElevatedMode, L"Desktop+ Elevated",
0,
0, 0,
1, 1,
HWND_MESSAGE, nullptr, hinstance, nullptr);
if (!window_handle)
{
return E_FAIL;
}
//Allow IPC messages even when elevated
IPCManager::Get().DisableUIPForRegisteredMessages(window_handle);
//Send config update to dashboard and UI process to set elevated mode active
IPCManager::Get().PostConfigMessageToDashboardApp(configid_bool_state_misc_elevated_mode_active, true);
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_misc_elevated_mode_active, true);
LOG_F(INFO, "Finished startup");
//Wait for callbacks, update or quit message
MSG msg;
while (::GetMessage(&msg, 0, 0, 0))
{
//Custom IPC messages
if (msg.message >= 0xC000)
{
HandleIPCMessage(msg);
}
}
LOG_F(INFO, "Shutting down...");
//Send config update to dashboard and UI process to disable it again
IPCManager::Get().PostConfigMessageToDashboardApp(configid_bool_state_misc_elevated_mode_active, false);
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_misc_elevated_mode_active, false);
//Uninitialize COM if it was used
if (!g_ElevatedMode_ComInitDone)
{
::CoUninitialize();
g_ElevatedMode_ComInitDone = false;
}
WindowManager::Get().ClearTempTopMostWindow();
return 0;
}
LRESULT CALLBACK WndProcElevated(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COPYDATA:
{
MSG msg;
// Process all custom window messages posted before this
while (::PeekMessage(&msg, nullptr, 0xC000, 0xFFFF, PM_REMOVE))
{
HandleIPCMessage(msg);
}
msg.hwnd = hWnd;
msg.message = message;
msg.wParam = wParam;
msg.lParam = lParam;
HandleIPCMessage(msg);
break;
}
case WM_DESTROY:
{
::PostQuitMessage(0);
break;
}
default:
return ::DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
bool HandleIPCMessage(MSG msg)
{
static InputSimulator input_sim;
static std::string action_exe_path;
static std::string action_exe_arg;
//Input strings come as WM_COPYDATA
if (msg.message == WM_COPYDATA)
{
//At least check if the dashboard app is running and the wParam is its window
HWND dashboard_app = ::FindWindow(g_WindowClassNameDashboardApp, nullptr);
if ((dashboard_app == nullptr) || ((HWND)msg.wParam != dashboard_app))
{
return false;
}
COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)msg.lParam;
//Arbitrary size limit to prevent some malicous applications from sending bad data
if (pcds->cbData <= 4096)
{
std::string copystr((char*)pcds->lpData, pcds->cbData); //We rely on the data length. The data is sent without the NUL byte
switch (pcds->dwData)
{
case ipcestrid_keyboard_text:
case ipcestrid_keyboard_text_force_unicode:
{
//Pass string to InputSimulator
input_sim.KeyboardText(copystr.c_str(), (pcds->dwData == ipcestrid_keyboard_text_force_unicode) );
break;
}
case ipcestrid_launch_application_path:
{
action_exe_path = copystr;
action_exe_arg = ""; //Also reset arg str since it's often not needed
break;
}
case ipcestrid_launch_application_arg:
{
action_exe_arg = copystr;
break;
}
}
}
}
else
{
IPCMsgID msgid = IPCManager::Get().GetIPCMessageID(msg.message);
if (msgid == ipcmsg_elevated_action)
{
switch (msg.wParam)
{
case ipceact_refresh:
{
input_sim.RefreshScreenOffsets();
break;
}
case ipceact_mouse_move:
{
input_sim.MouseMove(GET_X_LPARAM(msg.lParam), GET_Y_LPARAM(msg.lParam));
break;
};
case ipceact_mouse_hwheel:
{
input_sim.MouseWheelHorizontal(pun_cast(msg.lParam));
break;
};
case ipceact_mouse_vwheel:
{
input_sim.MouseWheelVertical(pun_cast(msg.lParam));
break;
};
case ipceact_pen_move:
{
input_sim.PenMove(GET_X_LPARAM(msg.lParam), GET_Y_LPARAM(msg.lParam));
break;
};
case ipceact_pen_button_down:
{
(msg.lParam == 0) ? input_sim.PenSetPrimaryDown(true) : input_sim.PenSetSecondaryDown(true);
break;
};
case ipceact_pen_button_up:
{
(msg.lParam == 0) ? input_sim.PenSetPrimaryDown(false) : input_sim.PenSetSecondaryDown(false);
break;
};
case ipceact_pen_leave:
{
input_sim.PenLeave();
break;
};
case ipceact_key_down:
{
//Copy 3 keycodes from lparam
unsigned char keycodes[3];
memcpy(keycodes, &msg.lParam, 3);
input_sim.KeyboardSetDown(keycodes);
break;
};
case ipceact_key_up:
{
//Copy 3 keycodes from lparam
unsigned char keycodes[3];
memcpy(keycodes, &msg.lParam, 3);
input_sim.KeyboardSetUp(keycodes);
break;
};
case ipceact_key_toggle:
{
//Copy 3 keycodes from lparam
unsigned char keycodes[3];
memcpy(keycodes, &msg.lParam, 3);
input_sim.KeyboardToggleState(keycodes);
break;
};
case ipceact_key_press_and_release:
{
input_sim.KeyboardPressAndRelease(msg.lParam);
break;
};
case ipceact_key_togglekey_set:
{
input_sim.KeyboardSetToggleKey(LOWORD(msg.lParam), HIWORD(msg.lParam));
break;
}
case ipceact_keystate_w32_set:
{
input_sim.KeyboardSetFromWin32KeyState(LOWORD(msg.lParam), HIWORD(msg.lParam));
break;
}
case ipceact_keystate_set:
{
input_sim.KeyboardSetKeyState((IPCKeyboardKeystateFlags)LOWORD(msg.lParam), HIWORD(msg.lParam));
break;
}
case ipceact_keyboard_text_finish:
{
input_sim.KeyboardTextFinish();
break;
}
case ipceact_launch_application:
{
//Init COM if necessary
if (!g_ElevatedMode_ComInitDone)
{
if (::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) != RPC_E_CHANGED_MODE)
{
g_ElevatedMode_ComInitDone = true;
}
}
//Convert path and arg to utf16
std::wstring path_wstr = WStringConvertFromUTF8(action_exe_path.c_str());
std::wstring arg_wstr = WStringConvertFromUTF8(action_exe_arg.c_str());
if (!path_wstr.empty())
{
::ShellExecute(nullptr, nullptr, path_wstr.c_str(), arg_wstr.c_str(), nullptr, SW_SHOWNORMAL);
}
break;
}
case ipceact_window_topmost_set:
{
HWND window = (HWND)msg.lParam;
(window != nullptr) ? WindowManager::Get().SetTempTopMostWindow(window) : WindowManager::Get().ClearTempTopMostWindow();
break;
}
}
}
}
return true;
}
================================================
FILE: src/DesktopPlus/ElevatedMode.h
================================================
#pragma once
#define NOMINMAX
#include
//This runs the process in elevated input command mode, in which it only takes select window messages and passes them to InputSimulator.
//
//This isn't secure at all, I'm well aware.
//There are certainly other more complex ways of implementation to make this seem more secure than the current one, but as far as I'm concerned we're fighting a battle that cannot be won.
//As long as we're open source, not enforcing signed binaries and access rights to the application directory, there will always be an attack vector.
//There's no way to actually trust the unelevated process requesting the inputs. Even if we isolated DesktopPlus.exe, we still couldn't trust SteamVR to not be compromised.
//Both of these live in an environment where they are expected to be updated automatically by an unelevated process as well.
//
//The attack vector is at least smaller than when running Steam and everything related elevated as well.
//Targeted attacks would be fairly easy if such code found the way of an user's machine, though.
int ElevatedModeEnter(HINSTANCE hinstance);
================================================
FILE: src/DesktopPlus/InputSimulator.cpp
================================================
#include "InputSimulator.h"
#include "InterprocessMessaging.h"
#include "OutputManager.h"
#include "Util.h"
enum KeyboardWin32KeystateFlags
{
kbd_w32keystate_flag_shift_down = 1 << 0,
kbd_w32keystate_flag_ctrl_down = 1 << 1,
kbd_w32keystate_flag_alt_down = 1 << 2
};
fn_CreateSyntheticPointerDevice InputSimulator::s_p_CreateSyntheticPointerDevice = nullptr;
fn_InjectSyntheticPointerInput InputSimulator::s_p_InjectSyntheticPointerInput = nullptr;
fn_DestroySyntheticPointerDevice InputSimulator::s_p_DestroySyntheticPointerDevice = nullptr;
InputSimulator::InputSimulator()
{
RefreshScreenOffsets();
//Try to init pen functions if they're not loaded yet
if (!IsPenSimulationSupported())
{
LoadPenFunctions();
}
//Init pen state (rest can stay at default 0)
m_PenState.type = PT_PEN;
m_PenState.penInfo.pointerInfo.pointerType = PT_PEN;
m_PenState.penInfo.pressure = 1024; //Full pressure
m_PenState.penInfo.penMask = PEN_MASK_PRESSURE; //Marked as optional, but without, applications expecting it may read 0 pressure still
}
InputSimulator::~InputSimulator()
{
if (m_PenDevice != nullptr)
{
s_p_DestroySyntheticPointerDevice(m_PenDevice);
}
}
void InputSimulator::SetEventForMouseKeyCode(INPUT& input_event, unsigned char keycode, bool down)
{
input_event.type = INPUT_MOUSE;
if (down)
{
switch (keycode)
{
case VK_LBUTTON: input_event.mi.dwFlags = MOUSEEVENTF_LEFTDOWN; break;
case VK_RBUTTON: input_event.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN; break;
case VK_MBUTTON: input_event.mi.dwFlags = MOUSEEVENTF_MIDDLEDOWN; break;
case VK_XBUTTON1: input_event.mi.dwFlags = MOUSEEVENTF_XDOWN;
input_event.mi.mouseData = XBUTTON1; break;
case VK_XBUTTON2: input_event.mi.dwFlags = MOUSEEVENTF_XDOWN;
input_event.mi.mouseData = XBUTTON2; break;
default: break;
}
}
else
{
switch (keycode)
{
case VK_LBUTTON: input_event.mi.dwFlags = MOUSEEVENTF_LEFTUP; break;
case VK_RBUTTON: input_event.mi.dwFlags = MOUSEEVENTF_RIGHTUP; break;
case VK_MBUTTON: input_event.mi.dwFlags = MOUSEEVENTF_MIDDLEUP; break;
case VK_XBUTTON1: input_event.mi.dwFlags = MOUSEEVENTF_XUP;
input_event.mi.mouseData = XBUTTON1; break;
case VK_XBUTTON2: input_event.mi.dwFlags = MOUSEEVENTF_XUP;
input_event.mi.mouseData = XBUTTON2; break;
default: break;
}
}
}
bool InputSimulator::SetEventForKeyCode(INPUT& input_event, unsigned char keycode, bool down, bool skip_check)
{
//Check if the mouse buttons are swapped as this also affects SendInput
if ( ((keycode == VK_LBUTTON) || (keycode == VK_RBUTTON)) && (::GetSystemMetrics(SM_SWAPBUTTON) != 0) )
{
keycode = (keycode == VK_LBUTTON) ? VK_RBUTTON : VK_LBUTTON;
}
bool key_down = (::GetAsyncKeyState(keycode) < 0);
if ( (keycode == 0) || ((key_down == down) && (!skip_check)) )
return false;
if ((keycode <= 6) && (keycode != VK_CANCEL)) //Mouse buttons need to be handled differently
{
SetEventForMouseKeyCode(input_event, keycode, down);
}
else
{
input_event.type = INPUT_KEYBOARD;
//Use scancodes if possible to increase compatibility (e.g. DirectInput games need scancodes)
UINT scancode = ::MapVirtualKey(keycode, MAPVK_VK_TO_VSC_EX);
//Pause/PrintScreen have a scancode too long for SendInput. May be possible to get to work with multiple input calls, but let's not bother for now
if ( (scancode != 0) || (keycode == VK_PAUSE) || (keycode == VK_SNAPSHOT) )
{
BYTE highbyte = HIBYTE(scancode);
bool is_extended = ((highbyte == 0xe0) || (highbyte == 0xe1));
//Not extended but needs to be for proper input simulation
if (!is_extended)
{
switch (keycode)
{
case VK_INSERT:
case VK_DELETE:
case VK_PRIOR:
case VK_NEXT:
case VK_END:
case VK_HOME:
case VK_LEFT:
case VK_UP:
case VK_RIGHT:
case VK_DOWN:
{
is_extended = true;
}
default: break;
}
}
input_event.ki.dwFlags = (is_extended) ? KEYEVENTF_SCANCODE | KEYEVENTF_EXTENDEDKEY : KEYEVENTF_SCANCODE;
input_event.ki.wScan = scancode;
}
else //No scancode, use keycode
{
input_event.ki.dwFlags = 0;
input_event.ki.wVk = keycode;
}
if (!down)
{
input_event.ki.dwFlags |= KEYEVENTF_KEYUP;
}
}
return true;
}
void InputSimulator::LoadPenFunctions()
{
HMODULE h_user32 = ::LoadLibraryW(L"user32.dll");
if (h_user32 != nullptr)
{
s_p_CreateSyntheticPointerDevice = (fn_CreateSyntheticPointerDevice) GetProcAddress(h_user32, "CreateSyntheticPointerDevice");
s_p_InjectSyntheticPointerInput = (fn_InjectSyntheticPointerInput) GetProcAddress(h_user32, "InjectSyntheticPointerInput");
s_p_DestroySyntheticPointerDevice = (fn_DestroySyntheticPointerDevice)GetProcAddress(h_user32, "DestroySyntheticPointerDevice");
}
}
void InputSimulator::CreatePenDeviceIfNeeded()
{
if (m_PenDevice == nullptr)
{
m_PenDevice = s_p_CreateSyntheticPointerDevice(PT_PEN, 1, POINTER_FEEDBACK_INDIRECT);
}
}
void InputSimulator::RefreshScreenOffsets()
{
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_refresh);
}
m_SpaceMaxX = GetSystemMetrics(SM_CXVIRTUALSCREEN);
m_SpaceMaxY = GetSystemMetrics(SM_CYVIRTUALSCREEN);
m_SpaceMultiplierX = 65536.0f / m_SpaceMaxX;
m_SpaceMultiplierY = 65536.0f / m_SpaceMaxY;
m_SpaceOffsetX = GetSystemMetrics(SM_XVIRTUALSCREEN) * -1;
m_SpaceOffsetY = GetSystemMetrics(SM_YVIRTUALSCREEN) * -1;
}
void InputSimulator::MouseMove(int x, int y)
{
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_mouse_move, MAKELPARAM(x, y));
return;
}
INPUT input_event = { 0 };
input_event.type = INPUT_MOUSE;
input_event.mi.dx = LONG((x + m_SpaceOffsetX) * m_SpaceMultiplierX);
input_event.mi.dy = LONG((y + m_SpaceOffsetY) * m_SpaceMultiplierY);
input_event.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_VIRTUALDESK | MOUSEEVENTF_ABSOLUTE;
::SendInput(1, &input_event, sizeof(INPUT));
}
void InputSimulator::MouseSetLeftDown(bool down)
{
(down) ? KeyboardSetDown(VK_LBUTTON) : KeyboardSetUp(VK_LBUTTON);
}
void InputSimulator::MouseSetRightDown(bool down)
{
(down) ? KeyboardSetDown(VK_RBUTTON) : KeyboardSetUp(VK_RBUTTON);
}
void InputSimulator::MouseSetMiddleDown(bool down)
{
(down) ? KeyboardSetDown(VK_MBUTTON) : KeyboardSetUp(VK_MBUTTON);
}
void InputSimulator::MouseWheelHorizontal(float delta)
{
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_mouse_hwheel, pun_cast(delta));
return;
}
INPUT input_event = {0};
input_event.type = INPUT_MOUSE;
input_event.mi.dwFlags = MOUSEEVENTF_HWHEEL;
input_event.mi.mouseData = DWORD(WHEEL_DELTA * delta);
::SendInput(1, &input_event, sizeof(INPUT));
}
void InputSimulator::MouseWheelVertical(float delta)
{
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_mouse_vwheel, pun_cast(delta));
return;
}
INPUT input_event = {0};
input_event.type = INPUT_MOUSE;
input_event.mi.dwFlags = MOUSEEVENTF_WHEEL;
input_event.mi.mouseData = DWORD(WHEEL_DELTA * delta);
::SendInput(1, &input_event, sizeof(INPUT));
}
void InputSimulator::PenMove(int x, int y)
{
if (!IsPenSimulationSupported())
return;
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_pen_move, MAKELPARAM(x, y));
return;
}
CreatePenDeviceIfNeeded();
m_PenState.penInfo.pointerInfo.pointerFlags |= POINTER_FLAG_INRANGE | POINTER_FLAG_UPDATE;
//Pen input position doesn't appear to be clamped by OS like mouse input and can do weird things if it goes out of range
m_PenState.penInfo.pointerInfo.ptPixelLocation.x = clamp(x + m_SpaceOffsetX, 0, m_SpaceMaxX);
m_PenState.penInfo.pointerInfo.ptPixelLocation.y = clamp(y + m_SpaceOffsetY, 0, m_SpaceMaxY);
s_p_InjectSyntheticPointerInput(m_PenDevice, &m_PenState, 1);
}
void InputSimulator::PenSetPrimaryDown(bool down)
{
if (!IsPenSimulationSupported())
return;
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, (down) ? ipceact_pen_button_down : ipceact_pen_button_up, 0);
return;
}
CreatePenDeviceIfNeeded();
if (down)
{
m_PenState.penInfo.pointerInfo.pointerFlags |= POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN | POINTER_FLAG_FIRSTBUTTON;
m_PenState.penInfo.pointerInfo.ButtonChangeType = POINTER_CHANGE_FIRSTBUTTON_DOWN;
}
else
{
m_PenState.penInfo.pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN | POINTER_FLAG_FIRSTBUTTON);
m_PenState.penInfo.pointerInfo.pointerFlags |= POINTER_FLAG_UP;
m_PenState.penInfo.pointerInfo.ButtonChangeType = POINTER_CHANGE_FIRSTBUTTON_UP;
}
m_PenState.penInfo.pointerInfo.pointerFlags &= ~POINTER_FLAG_UPDATE;
s_p_InjectSyntheticPointerInput(m_PenDevice, &m_PenState, 1);
m_PenState.penInfo.pointerInfo.pointerFlags &= ~(POINTER_FLAG_DOWN | POINTER_FLAG_UP);
m_PenState.penInfo.pointerInfo.ButtonChangeType = POINTER_CHANGE_NONE;
}
void InputSimulator::PenSetSecondaryDown(bool down)
{
if (!IsPenSimulationSupported())
return;
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, (down) ? ipceact_pen_button_down : ipceact_pen_button_up, 1);
return;
}
CreatePenDeviceIfNeeded();
if (down)
{
m_PenState.penInfo.pointerInfo.pointerFlags |= POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN | POINTER_FLAG_SECONDBUTTON;
m_PenState.penInfo.pointerInfo.ButtonChangeType = POINTER_CHANGE_SECONDBUTTON_DOWN;
m_PenState.penInfo.penFlags = PEN_FLAG_BARREL;
}
else
{
m_PenState.penInfo.pointerInfo.pointerFlags &= ~(POINTER_FLAG_INCONTACT | POINTER_FLAG_DOWN | POINTER_FLAG_SECONDBUTTON);
m_PenState.penInfo.pointerInfo.pointerFlags |= POINTER_FLAG_UP;
m_PenState.penInfo.pointerInfo.ButtonChangeType = POINTER_CHANGE_SECONDBUTTON_UP;
m_PenState.penInfo.penFlags = 0;
}
m_PenState.penInfo.pointerInfo.pointerFlags &= ~POINTER_FLAG_UPDATE;
s_p_InjectSyntheticPointerInput(m_PenDevice, &m_PenState, 1);
m_PenState.penInfo.pointerInfo.pointerFlags &= ~(POINTER_FLAG_DOWN | POINTER_FLAG_UP);
m_PenState.penInfo.pointerInfo.ButtonChangeType = POINTER_CHANGE_NONE;
}
void InputSimulator::PenLeave()
{
if (!IsPenSimulationSupported())
return;
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_pen_leave);
return;
}
CreatePenDeviceIfNeeded();
if (m_PenState.penInfo.pointerInfo.pointerFlags & POINTER_FLAG_INRANGE)
{
m_PenState.penInfo.pointerInfo.pointerFlags = POINTER_FLAG_UPDATE;
m_PenState.penInfo.pointerInfo.ButtonChangeType = POINTER_CHANGE_NONE;
m_PenState.penInfo.penFlags = 0;
s_p_InjectSyntheticPointerInput(m_PenDevice, &m_PenState, 1);
}
}
void InputSimulator::KeyboardSetDown(unsigned char keycode)
{
if (keycode == 0)
return;
if (m_ForwardToElevatedModeProcess)
{
LPARAM elevated_keycodes = 0;
memcpy(&elevated_keycodes, &keycode, 1);
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_key_down, elevated_keycodes);
return;
}
INPUT input_event = {0};
if (SetEventForKeyCode(input_event, keycode, true))
{
::SendInput(1, &input_event, sizeof(INPUT));
}
}
void InputSimulator::KeyboardSetDown(unsigned char keycode, bool down)
{
(down) ? KeyboardSetDown(keycode) : KeyboardSetUp(keycode);
}
void InputSimulator::KeyboardSetUp(unsigned char keycode)
{
if (keycode == 0)
return;
if (m_ForwardToElevatedModeProcess)
{
LPARAM elevated_keycodes = 0;
memcpy(&elevated_keycodes, &keycode, 1);
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_key_up, elevated_keycodes);
return;
}
INPUT input_event = {0};
if (SetEventForKeyCode(input_event, keycode, false))
{
::SendInput(1, &input_event, sizeof(INPUT));
}
}
//Why so awfully specific, seems wasteful? Spamming the key events separately can confuse applications sometimes and we want to make sure the keys are really pressed at once
void InputSimulator::KeyboardSetDown(unsigned char keycodes[3])
{
if (m_ForwardToElevatedModeProcess)
{
LPARAM elevated_keycodes = 0;
memcpy(&elevated_keycodes, keycodes, 3);
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_key_down, elevated_keycodes);
return;
}
INPUT input_event[3] = { 0 };
int used_event_count = 0;
for (int i = 0; i < 3; ++i)
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], keycodes[i], true);
}
if (used_event_count != 0)
{
::SendInput(used_event_count, input_event, sizeof(INPUT));
}
}
void InputSimulator::KeyboardSetUp(unsigned char keycodes[3])
{
if (m_ForwardToElevatedModeProcess)
{
LPARAM elevated_keycodes = 0;
memcpy(&elevated_keycodes, keycodes, 3);
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_key_up, elevated_keycodes);
return;
}
INPUT input_event[3] = { 0 };
int used_event_count = 0;
for (int i = 0; i < 3; ++i)
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], keycodes[i], false);
}
if (used_event_count != 0)
{
::SendInput(used_event_count, input_event, sizeof(INPUT));
}
}
void InputSimulator::KeyboardToggleState(unsigned char keycode)
{
if (keycode == 0)
return;
//GetAsyncKeyState is subject to UIPI, so always forward it
if (m_ForwardToElevatedModeProcess)
{
LPARAM elevated_keycodes = 0;
memcpy(&elevated_keycodes, &keycode, 1);
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_key_toggle, elevated_keycodes);
return;
}
if (IsKeyDown(keycode)) //If already pressed, release key
{
KeyboardSetUp(keycode);
}
else
{
KeyboardSetDown(keycode);
}
}
void InputSimulator::KeyboardToggleState(unsigned char keycodes[3])
{
if (m_ForwardToElevatedModeProcess)
{
LPARAM elevated_keycodes = 0;
memcpy(&elevated_keycodes, keycodes, 3);
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_key_toggle, elevated_keycodes);
return;
}
INPUT input_event[3] = {0};
int used_event_count = 0;
for (int i = 0; i < 3; ++i)
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], keycodes[i], !IsKeyDown(keycodes[i]));
}
if (used_event_count != 0)
{
::SendInput(used_event_count, input_event, sizeof(INPUT));
}
}
void InputSimulator::KeyboardPressAndRelease(unsigned char keycode)
{
if (keycode == 0)
return;
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_key_press_and_release, keycode);
return;
}
INPUT input_event[2] = {0};
int used_event_count = 0;
used_event_count += SetEventForKeyCode(input_event[used_event_count], keycode, true);
used_event_count += SetEventForKeyCode(input_event[used_event_count], keycode, false);
::SendInput(used_event_count, input_event, sizeof(INPUT));
}
void InputSimulator::KeyboardSetToggleKey(unsigned char keycode, bool toggled)
{
if (keycode == 0)
return;
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_key_togglekey_set, MAKELPARAM(keycode, toggled));
return;
}
bool is_toggled = ((::GetKeyState(keycode) & 0x0001) != 0);
if (toggled == is_toggled)
return;
INPUT input_event[3] = {0};
int used_event_count = 0;
used_event_count += SetEventForKeyCode(input_event[used_event_count], keycode, false); //Release if it happens to be down
used_event_count += SetEventForKeyCode(input_event[used_event_count], keycode, true, true); //Press...
used_event_count += SetEventForKeyCode(input_event[used_event_count], keycode, false, true); //...and release, even if the state wouldn't change (last arg)
::SendInput(used_event_count, input_event, sizeof(INPUT));
}
void InputSimulator::KeyboardSetFromWin32KeyState(unsigned short keystate, bool down)
{
unsigned char keycode = LOBYTE(keystate);
bool key_down = IsKeyDown(keycode);
if (key_down == down)
return; //Nothing to be done
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_keystate_w32_set, MAKELPARAM(keystate, down));
return;
}
unsigned char flags = HIBYTE(keystate);
bool flag_shift = (flags & kbd_w32keystate_flag_shift_down);
bool flag_ctrl = (flags & kbd_w32keystate_flag_ctrl_down);
bool flag_alt = (flags & kbd_w32keystate_flag_alt_down);
bool caps_toggled = ((::GetKeyState(VK_CAPITAL) & 0x0001) != 0);
INPUT input_event[16] = { 0 };
int used_event_count = 0;
//Add events for modifier keys if needed
if (!flag_shift)
{
//Try releasing both if any is down
if (IsKeyDown(VK_SHIFT))
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_LSHIFT, false);
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_RSHIFT, false);
}
}
else if (!IsKeyDown(VK_SHIFT)) //Only set the left key if none are down
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_LSHIFT, true);
}
if (!flag_ctrl)
{
if (IsKeyDown(VK_CONTROL))
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_LCONTROL, false);
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_RCONTROL, false);
}
}
else if (!IsKeyDown(VK_CONTROL))
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_LCONTROL, true);
}
if (!flag_alt)
{
if (IsKeyDown(VK_MENU))
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_LMENU, false);
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_RMENU, false);
}
}
else if (!IsKeyDown(VK_MENU))
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_LMENU, true);
}
//Add events to handle caps lock state
if (caps_toggled)
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_CAPITAL, false); //Release if it happens to be down
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_CAPITAL, true, true); //Press...
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_CAPITAL, false, true); //...and release, even if the state wouldn't change (last arg)
}
//Add event for actual keycode
used_event_count += SetEventForKeyCode(input_event[used_event_count], keycode, down);
if (used_event_count != 0)
{
::SendInput(used_event_count, input_event, sizeof(INPUT));
}
}
void InputSimulator::KeyboardSetKeyState(IPCKeyboardKeystateFlags flags, unsigned char keycode)
{
if (m_ForwardToElevatedModeProcess)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_keystate_set, MAKELPARAM(flags, keycode));
return;
}
//Numpad input simulation is a bit weird
//It seems like numpad (especially shift+numpad) is something that is taken care of in a lower layer and as such confuses Windows if we just send our straight inputs
//Basically it either doesn't produce an input with numlock off or shift can get stuck in a way that requires both shift keys to be released
//So we just end up handling this manually below
if (((keycode >= VK_NUMPAD0) && (keycode <= VK_NUMPAD9)) || (keycode == VK_DECIMAL))
{
const bool is_numlock_on = ((::GetKeyState(VK_NUMLOCK) & 0x0001) != 0);
const bool is_shift_down = ((flags & kbd_keystate_flag_lshift_down) || (flags & kbd_keystate_flag_rshift_down));
const bool is_double_shift_down = ((flags & kbd_keystate_flag_lshift_down) && (flags & kbd_keystate_flag_rshift_down));
if ((!is_numlock_on) || (is_shift_down))
{
//Swap numpad keycodes with regular ones
//One might think these should be sent as unextended scancodes down the line, but doing so makes them be treated as numpad inputs again with all the issues we're trying to avoid here
switch (keycode)
{
case VK_NUMPAD0: keycode = VK_INSERT; break;
case VK_NUMPAD1: keycode = VK_END; break;
case VK_NUMPAD2: keycode = VK_DOWN; break;
case VK_NUMPAD3: keycode = VK_NEXT; break;
case VK_NUMPAD4: keycode = VK_LEFT; break;
case VK_NUMPAD5: keycode = VK_CLEAR; break;
case VK_NUMPAD6: keycode = VK_RIGHT; break;
case VK_NUMPAD7: keycode = VK_HOME; break;
case VK_NUMPAD8: keycode = VK_UP; break;
case VK_NUMPAD9: keycode = VK_PRIOR; break;
case VK_DECIMAL: keycode = VK_DELETE; break;
}
//Shift needs to be up to get normal cursor movement (unless both shifts are down)
if ((is_numlock_on) && (is_shift_down) && (!is_double_shift_down))
{
flags = IPCKeyboardKeystateFlags(flags & ~(kbd_keystate_flag_lshift_down | kbd_keystate_flag_rshift_down));
}
}
}
INPUT input_event[10] = {0};
int used_event_count = 0;
//Add events for modifier keys if needed
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_LSHIFT, (flags & kbd_keystate_flag_lshift_down));
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_RSHIFT, (flags & kbd_keystate_flag_rshift_down));
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_LCONTROL, (flags & kbd_keystate_flag_lctrl_down));
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_RCONTROL, (flags & kbd_keystate_flag_rctrl_down));
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_LMENU, (flags & kbd_keystate_flag_lalt_down));
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_RMENU, (flags & kbd_keystate_flag_ralt_down));
//Add events to handle caps lock state
bool caps_toggled = ((::GetKeyState(VK_CAPITAL) & 0x0001) != 0);
if (caps_toggled != bool(flags & kbd_keystate_flag_capslock_toggled))
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_CAPITAL, false); //Release if it happens to be down
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_CAPITAL, true, true); //Press...
used_event_count += SetEventForKeyCode(input_event[used_event_count], VK_CAPITAL, false, true); //...and release, even if the state wouldn't change (last arg)
}
//Add event for actual keycode, but skip keys handled by the keystate flags
switch (keycode)
{
case VK_SHIFT:
case VK_LSHIFT:
case VK_RSHIFT:
case VK_CONTROL:
case VK_LCONTROL:
case VK_RCONTROL:
case VK_MENU:
case VK_LMENU:
case VK_RMENU:
case VK_CAPITAL:
{
break;
}
default:
{
used_event_count += SetEventForKeyCode(input_event[used_event_count], keycode, (flags & kbd_keystate_flag_key_down));
}
}
if (used_event_count != 0)
{
::SendInput(used_event_count, input_event, sizeof(INPUT));
}
}
void InputSimulator::KeyboardText(const char* str_utf8, bool always_use_unicode_event)
{
if (m_ForwardToElevatedModeProcess)
{
OutputManager* outmgr = OutputManager::Get();
if (outmgr != nullptr)
{
IPCElevatedStringID str_id = (always_use_unicode_event) ? ipcestrid_keyboard_text_force_unicode : ipcestrid_keyboard_text;
IPCManager::Get().SendStringToElevatedModeProcess(str_id, str_utf8, outmgr->GetWindowHandle());
m_ElevatedModeHasTextQueued = true;
}
return;
}
//Convert to UTF16
std::wstring wstr = WStringConvertFromUTF8(str_utf8);
INPUT input_event = { 0 };
input_event.type = INPUT_KEYBOARD;
if (!always_use_unicode_event)
{
//This function could just use KEYEVENTF_UNICODE on all printable characters to get working text input (we still do that for type string actions though)
//However, in order to trigger shortcuts and non-text events in applications, at least 0-9 & A-Z are simulated as proper key events
//For consistent handling, capslock and shift are reset at the start, but that shouldn't be an issue in practice
if ((::GetKeyState(VK_CAPITAL) & 0x0001) != 0) //Turn off capslock if it's on
{
input_event.ki.dwFlags = 0;
input_event.ki.wVk = VK_CAPITAL;
m_KeyboardTextQueue.push_back(input_event);
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
m_KeyboardTextQueue.push_back(input_event);
}
if (::GetAsyncKeyState(VK_SHIFT) < 0) //Release shift if it's down
{
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
input_event.ki.wVk = VK_SHIFT;
m_KeyboardTextQueue.push_back(input_event);
}
for (wchar_t current_char: wstr)
{
if (current_char == '\b') //Backspace, needs special handling
{
input_event.ki.dwFlags = 0;
input_event.ki.wVk = VK_BACK;
m_KeyboardTextQueue.push_back(input_event);
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
m_KeyboardTextQueue.push_back(input_event);
}
else if (current_char == '\n') //Enter, needs special handling
{
input_event.ki.dwFlags = 0;
input_event.ki.wVk = VK_RETURN;
m_KeyboardTextQueue.push_back(input_event);
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
m_KeyboardTextQueue.push_back(input_event);
}
else if ( ((current_char >= '0') && (current_char <= '9')) || (current_char == ' ') ) //0 - 9 and space, simulate keydown/up
{
input_event.ki.dwFlags = 0;
input_event.ki.wVk = current_char;
m_KeyboardTextQueue.push_back(input_event);
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
m_KeyboardTextQueue.push_back(input_event);
}
else if ((current_char >= 'a') && (current_char <= 'z')) //a - z, simulate keydown/up
{
input_event.ki.dwFlags = 0;
input_event.ki.wVk = current_char - ('a' - 'A');
m_KeyboardTextQueue.push_back(input_event);
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
m_KeyboardTextQueue.push_back(input_event);
}
else if ((current_char >= 'A') && (current_char <= 'Z')) //A - Z, simulate keydown/up
{
input_event.ki.dwFlags = 0;
input_event.ki.wVk = VK_SHIFT;
m_KeyboardTextQueue.push_back(input_event);
input_event.ki.dwFlags = 0;
input_event.ki.wVk = current_char;
m_KeyboardTextQueue.push_back(input_event);
input_event.ki.dwFlags = KEYEVENTF_KEYUP;
m_KeyboardTextQueue.push_back(input_event);
input_event.ki.wVk = VK_SHIFT;
m_KeyboardTextQueue.push_back(input_event);
}
else
{
input_event.ki.dwFlags = KEYEVENTF_UNICODE;
input_event.ki.wVk = 0;
input_event.ki.wScan = current_char;
m_KeyboardTextQueue.push_back(input_event);
input_event.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;
m_KeyboardTextQueue.push_back(input_event);
}
}
}
else
{
for (wchar_t current_char: wstr)
{
input_event.ki.dwFlags = KEYEVENTF_UNICODE;
input_event.ki.wScan = current_char;
m_KeyboardTextQueue.push_back(input_event);
input_event.ki.dwFlags = KEYEVENTF_UNICODE | KEYEVENTF_KEYUP;
m_KeyboardTextQueue.push_back(input_event);
}
}
}
void InputSimulator::KeyboardTextFinish()
{
if (m_ForwardToElevatedModeProcess)
{
//Only send if we know there is queued text in that process
if (m_ElevatedModeHasTextQueued)
{
IPCManager::Get().PostMessageToElevatedModeProcess(ipcmsg_elevated_action, ipceact_keyboard_text_finish);
m_ElevatedModeHasTextQueued = false;
}
return;
}
if (!m_KeyboardTextQueue.empty())
{
::SendInput((UINT)m_KeyboardTextQueue.size(), m_KeyboardTextQueue.data(), sizeof(INPUT));
m_KeyboardTextQueue.clear();
}
}
void InputSimulator::SetElevatedModeForwardingActive(bool do_forward)
{
m_ForwardToElevatedModeProcess = do_forward;
}
bool InputSimulator::IsKeyDown(unsigned char keycode)
{
//Check if the mouse buttons are swapped
if ( ((keycode == VK_LBUTTON) || (keycode == VK_RBUTTON)) && (::GetSystemMetrics(SM_SWAPBUTTON) != 0) )
{
keycode = (keycode == VK_LBUTTON) ? VK_RBUTTON : VK_LBUTTON;
}
return (::GetAsyncKeyState(keycode) < 0);
}
bool InputSimulator::IsPenSimulationSupported()
{
return (s_p_CreateSyntheticPointerDevice != nullptr);
}
================================================
FILE: src/DesktopPlus/InputSimulator.h
================================================
#ifndef _INPUTSIMULATOR_H_
#define _INPUTSIMULATOR_H_
#define NOMINMAX
#include
#include
//Dashboard_Back exists, but not doesn't map to "Go Back" ...okay!
#define Button_Dashboard_GoHome vr::k_EButton_IndexController_A
#define Button_Dashboard_GoBack vr::k_EButton_IndexController_B
enum IPCKeyboardKeystateFlags : unsigned char;
//SyntheticPointer functions are loaded manually to not require OS support to run the application (Windows 10 1809+ should have them though)
typedef HANDLE HSYNTHETICPOINTERDEVICE_DPLUS;
#ifndef NTDDI_WIN10_RS5
typedef enum { POINTER_FEEDBACK_DEFAULT = 1, POINTER_FEEDBACK_INDIRECT = 2, POINTER_FEEDBACK_NONE = 3 } POINTER_FEEDBACK_MODE;
#endif
typedef HSYNTHETICPOINTERDEVICE_DPLUS (WINAPI* fn_CreateSyntheticPointerDevice) (_In_ POINTER_INPUT_TYPE pointerType, _In_ ULONG maxCount, _In_ POINTER_FEEDBACK_MODE mode);
typedef BOOL (WINAPI* fn_InjectSyntheticPointerInput) (_In_ HSYNTHETICPOINTERDEVICE_DPLUS device, _In_reads_(count) CONST POINTER_TYPE_INFO* pointerInfo, _In_ UINT32 count);
typedef void (WINAPI* fn_DestroySyntheticPointerDevice)(_In_ HSYNTHETICPOINTERDEVICE_DPLUS device);
class InputSimulator
{
private:
int m_SpaceMaxX = 0;
int m_SpaceMaxY = 0;
float m_SpaceMultiplierX = 1.0f;
float m_SpaceMultiplierY = 1.0f;
int m_SpaceOffsetX = 0;
int m_SpaceOffsetY = 0;
static fn_CreateSyntheticPointerDevice s_p_CreateSyntheticPointerDevice;
static fn_InjectSyntheticPointerInput s_p_InjectSyntheticPointerInput;
static fn_DestroySyntheticPointerDevice s_p_DestroySyntheticPointerDevice;
HSYNTHETICPOINTERDEVICE_DPLUS m_PenDevice = nullptr;
POINTER_TYPE_INFO m_PenState = {0};
std::vector m_KeyboardTextQueue;
bool m_ForwardToElevatedModeProcess = false;
bool m_ElevatedModeHasTextQueued = false;
void CreatePenDeviceIfNeeded();
static void LoadPenFunctions();
static void SetEventForMouseKeyCode(INPUT& input_event, unsigned char keycode, bool down);
//Set the event if it would change key state. Returns if anything was written to input_event
static bool SetEventForKeyCode(INPUT& input_event, unsigned char keycode, bool down, bool skip_check = false);
public:
InputSimulator();
~InputSimulator();
void RefreshScreenOffsets();
void MouseMove(int x, int y);
void MouseSetLeftDown(bool down);
void MouseSetRightDown(bool down);
void MouseSetMiddleDown(bool down);
void MouseWheelHorizontal(float delta);
void MouseWheelVertical(float delta);
void PenMove(int x, int y);
void PenSetPrimaryDown(bool down);
void PenSetSecondaryDown(bool down);
void PenLeave();
void KeyboardSetDown(unsigned char keycode);
void KeyboardSetDown(unsigned char keycode, bool down);
void KeyboardSetUp(unsigned char keycode);
void KeyboardSetDown(unsigned char keycodes[3]);
void KeyboardSetUp(unsigned char keycodes[3]);
void KeyboardToggleState(unsigned char keycode);
void KeyboardToggleState(unsigned char keycodes[3]);
void KeyboardPressAndRelease(unsigned char keycode);
void KeyboardSetToggleKey(unsigned char keycode, bool toggled);
void KeyboardSetFromWin32KeyState(unsigned short keystate, bool down); //Keystate as returned by VkKeyScan()
void KeyboardSetKeyState(IPCKeyboardKeystateFlags flags, unsigned char keycode);
void KeyboardText(const char* str_utf8, bool always_use_unicode_event = false);
void KeyboardTextFinish();
void SetElevatedModeForwardingActive(bool do_forward);
static bool IsPenSimulationSupported();
static bool IsKeyDown(unsigned char keycode);
};
#endif
================================================
FILE: src/DesktopPlus/LaserPointer.cpp
================================================
#include "LaserPointer.h"
#include "ConfigManager.h"
#include "OverlayManager.h"
#include "OutputManager.h"
#include "Util.h"
#include "OpenVRExt.h"
#define LASER_POINTER_OVERLAY_WIDTH 0.0025f
#define LASER_POINTER_DEFAULT_LENGTH 5.0f
LaserPointer::LaserPointer() : m_ActivationOrigin(dplp_activation_origin_none),
m_HadPrimaryPointerDevice(false),
m_DeviceMaxActiveID(0),
m_LastPrimaryDeviceSwitchTick(0),
m_LastScrollTick(0),
m_DeviceHapticPending(vr::k_unTrackedDeviceIndexInvalid),
m_IsForceTargetOverlayActive(false),
m_ForceTargetOverlayHandle(vr::k_ulOverlayHandleInvalid)
{
//Not calling Update() here since the OutputManager typically needs to load the config and OpenVR first
}
LaserPointer::~LaserPointer()
{
if (vr::VROverlay() != nullptr)
{
for (auto& lp_device : m_Devices)
{
if (lp_device.OvrlHandle != vr::k_ulOverlayHandleInvalid)
{
vr::VROverlay()->DestroyOverlay(lp_device.OvrlHandle);
}
}
}
}
void LaserPointer::CreateDeviceOverlay(vr::TrackedDeviceIndex_t device_index)
{
if (device_index >= vr::k_unMaxTrackedDeviceCount)
return;
LaserPointerDevice& lp_device = m_Devices[device_index];
std::string key = "elvissteinjr.DesktopPlusPointer" + std::to_string(device_index);
vr::EVROverlayError ovrl_error = vr::VROverlay()->CreateOverlay(key.c_str(), "Desktop+ Laser Pointer", &lp_device.OvrlHandle);
if (ovrl_error != vr::VROverlayError_None)
return;
//Set overlay as 2x2 half transparent blue-ish image
uint32_t pixels[2 * 2];
std::fill(std::begin(pixels), std::end(pixels), 0xFFBFA75F); //Fairly close to SteamVR's pointer color (ABGR / little-endian order)
vr::VROverlay()->SetOverlayRaw(lp_device.OvrlHandle, pixels, 2, 2, 4);
vr::VROverlay()->SetOverlayWidthInMeters(lp_device.OvrlHandle, LASER_POINTER_OVERLAY_WIDTH);
vr::VROverlay()->SetOverlaySortOrder(lp_device.OvrlHandle, 2);
}
void LaserPointer::UpdateDeviceOverlay(vr::TrackedDeviceIndex_t device_index)
{
if (device_index >= vr::k_unMaxTrackedDeviceCount)
return;
LaserPointerDevice& lp_device = m_Devices[device_index];
//Don't show any overlay when using HMD as origin
if (lp_device.UseHMDAsOrigin)
return;
//Create overlay if it doesn't exist yet
if (lp_device.OvrlHandle == vr::k_ulOverlayHandleInvalid)
{
CreateDeviceOverlay(device_index);
}
//Adjust visibility
bool do_show_later = false;
bool is_active = IsActive();
if ( (!lp_device.IsVisible) && (is_active) )
{
lp_device.IsVisible = true;
do_show_later = true; //Postpone actually showing the overlay until after we set position and visuals
}
else if ( (lp_device.IsVisible) && (!is_active) )
{
vr::VROverlay()->HideOverlay(lp_device.OvrlHandle);
lp_device.IsVisible = false;
}
//Skip rest if not visible
if (!lp_device.IsVisible)
return;
//Position laser
Matrix4 transform_tip, transform_offset;
//Perform rotation locally and offset laser forward so it starts at the tip
transform_offset.rotateX(-90.0f);
transform_offset.translate_relative(0.0f, lp_device.LaserLength / 2.0f, 0.0f);
//Use tip if there is one
transform_tip = vr::IVRSystemEx::GetControllerTipMatrix( vr::VRSystem()->GetControllerRoleForTrackedDeviceIndex(device_index) );
transform_tip = transform_tip * transform_offset;
//A smart person could probably figure out how to also have the overlay spin towards the HMD so it doesn't appear flat
vr::HmdMatrix34_t transform_openvr = transform_tip.toOpenVR34();
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(lp_device.OvrlHandle, (lp_device.UseHMDAsOrigin) ? vr::k_unTrackedDeviceIndex_Hmd : device_index, &transform_openvr);
//Adjust pointer alpha/brightness
bool is_primary_device = (device_index == (vr::TrackedDeviceIndex_t)ConfigManager::GetValue(configid_int_state_dplus_laser_pointer_device));
if (is_primary_device)
{
vr::VROverlay()->SetOverlayAlpha(lp_device.OvrlHandle, 1.0f);
if (lp_device.OvrlHandleTargetLast != vr::k_ulOverlayHandleInvalid)
{
vr::VROverlay()->SetOverlayColor(lp_device.OvrlHandle, 1.0f, 1.0f, 1.0f);
}
else
{
vr::VROverlay()->SetOverlayColor(lp_device.OvrlHandle, 0.25f, 0.25f, 0.25f);
}
}
else if (lp_device.IsActiveForMultiLaserInput)
{
vr::VROverlay()->SetOverlayAlpha(lp_device.OvrlHandle, 0.75f);
vr::VROverlay()->SetOverlayColor(lp_device.OvrlHandle, 0.50f, 0.50f, 0.50f);
}
else
{
vr::VROverlay()->SetOverlayAlpha(lp_device.OvrlHandle, 0.125f);
vr::VROverlay()->SetOverlayColor(lp_device.OvrlHandle, 0.25f, 0.25f, 0.25f);
}
if (do_show_later)
{
vr::VROverlay()->ShowOverlay(lp_device.OvrlHandle);
}
}
void LaserPointer::UpdateIntersection(vr::TrackedDeviceIndex_t device_index)
{
if (device_index >= vr::k_unMaxTrackedDeviceCount)
return;
LaserPointerDevice& lp_device = m_Devices[device_index];
bool is_primary_device = (device_index == (vr::TrackedDeviceIndex_t)ConfigManager::GetValue(configid_int_state_dplus_laser_pointer_device));
bool was_active_for_multilaser_input = lp_device.IsActiveForMultiLaserInput;
bool skip_intersection_test = vr::IVROverlayEx::IsSystemLaserPointerActive();
bool skip_input = skip_intersection_test;
//Set up intersection test
bool hit_multilaser = false;
vr::VROverlayIntersectionParams_t params = {0};
vr::VROverlayIntersectionResults_t results = {0};
if (!vr::IVROverlayEx::GetOverlayIntersectionParamsForDevice(params, (lp_device.UseHMDAsOrigin) ? vr::k_unTrackedDeviceIndex_Hmd : device_index, vr::TrackingUniverseStanding))
{
skip_intersection_test = true; //Skip if pose isn't valid
}
//Find the nearest intersecting overlay
vr::VROverlayHandle_t nearest_target_overlay = vr::k_ulOverlayHandleInvalid;
OverlayTextureSource nearest_texture_source = ovrl_texsource_none;
vr::VROverlayIntersectionResults_t nearest_results = {0};
nearest_results.fDistance = FLT_MAX;
//If requested via ForceTargetOverlay(), force a different target overlay
if (m_IsForceTargetOverlayActive)
{
skip_intersection_test = true;
nearest_target_overlay = m_ForceTargetOverlayHandle;
//Check if forced target overlay is UI overlay
const bool is_forced_ui = ( (std::find(m_OverlayHandlesUI.begin(), m_OverlayHandlesUI.end(), nearest_target_overlay) != m_OverlayHandlesUI.end()) ||
(std::find(m_OverlayHandlesMultiLaser.begin(), m_OverlayHandlesMultiLaser.end(), nearest_target_overlay) != m_OverlayHandlesMultiLaser.end()) );
if (is_forced_ui)
{
nearest_texture_source = ovrl_texsource_ui;
}
else
{
unsigned int overlay_id = OverlayManager::Get().FindOverlayID(m_ForceTargetOverlayHandle);
nearest_texture_source = OverlayManager::Get().GetOverlay(overlay_id).GetTextureSource();
}
}
//If input is held down, do not switch overlays and act as if overlay space is extending past the surface when not hitting it
if (!skip_intersection_test)
{
if (lp_device.InputDownCount != 0)
{
//Check if input is still enabled
vr::VROverlayInputMethod input_method = vr::VROverlayInputMethod_None;
vr::VROverlay()->GetOverlayInputMethod(lp_device.OvrlHandleTargetLast, &input_method);
if (input_method == vr::VROverlayInputMethod_Mouse)
{
if (vr::VROverlay()->ComputeOverlayIntersection(lp_device.OvrlHandleTargetLast, ¶ms, &results))
{
if ( (vr::IVROverlayEx::IsOverlayIntersectionHitFrontFacing(params, results)) && (IntersectionMaskHitTest(lp_device.OvrlHandleTargetLastTextureSource, results.vUVs)) )
{
nearest_target_overlay = lp_device.OvrlHandleTargetLast;
}
}
//These results are still valid even when not actually hitting
nearest_results = results;
}
}
else
{
//Desktop+ overlays
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const Overlay& overlay = OverlayManager::Get().GetOverlay(i);
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if ( (overlay.IsVisible()) && (data.ConfigBool[configid_bool_overlay_input_dplus_lp_enabled]) )
{
//Check if input is enabled right now (could differ from config setting)
vr::VROverlayInputMethod input_method = vr::VROverlayInputMethod_None;
vr::VROverlay()->GetOverlayInputMethod(overlay.GetHandle(), &input_method);
if ( (input_method == vr::VROverlayInputMethod_Mouse) &&
(vr::VROverlay()->ComputeOverlayIntersection(overlay.GetHandle(), ¶ms, &results)) && (results.fDistance < nearest_results.fDistance) )
{
//If Desktop Duplication/Performance Montior, this also performs an extra hit-test as described below
if ( (vr::IVROverlayEx::IsOverlayIntersectionHitFrontFacing(params, results)) && (IntersectionMaskHitTest(overlay.GetTextureSource(), results.vUVs)) )
{
nearest_target_overlay = overlay.GetHandle();
nearest_texture_source = overlay.GetTextureSource();
nearest_results = results;
}
}
}
}
//Desktop+ UI overlays
//
//ComputeOverlayIntersection() does not take intersection masks into account. This has been reported under issue #1601 in the OpenVR repo.
//As masks can change any frame, sending them over all the time seems tedious and inefficient... we're doing this for now though to work around that.
for (vr::VROverlayHandle_t overlay_handle : m_OverlayHandlesUI)
{
if (vr::VROverlay()->IsOverlayVisible(overlay_handle))
{
//Check if input is enabled
vr::VROverlayInputMethod input_method = vr::VROverlayInputMethod_None;
vr::VROverlay()->GetOverlayInputMethod(overlay_handle, &input_method);
if ( (input_method == vr::VROverlayInputMethod_Mouse) && (vr::VROverlay()->ComputeOverlayIntersection(overlay_handle, ¶ms, &results)) &&
(results.fDistance < nearest_results.fDistance) )
{
if ( (vr::IVROverlayEx::IsOverlayIntersectionHitFrontFacing(params, results)) && (IntersectionMaskHitTest(ovrl_texsource_ui, results.vUVs)) )
{
nearest_target_overlay = overlay_handle;
nearest_texture_source = ovrl_texsource_ui;
nearest_results = results;
}
}
}
}
//MultiLaser overlays (just keyboard right now)
for (vr::VROverlayHandle_t overlay_handle : m_OverlayHandlesMultiLaser)
{
if (vr::VROverlay()->IsOverlayVisible(overlay_handle))
{
//Check if input is enabled
vr::VROverlayInputMethod input_method = vr::VROverlayInputMethod_None;
vr::VROverlay()->GetOverlayInputMethod(overlay_handle, &input_method);
if ( (input_method == vr::VROverlayInputMethod_Mouse) && (vr::VROverlay()->ComputeOverlayIntersection(overlay_handle, ¶ms, &results)) &&
(results.fDistance < nearest_results.fDistance) )
{
if ( (vr::IVROverlayEx::IsOverlayIntersectionHitFrontFacing(params, results)) && (IntersectionMaskHitTest(ovrl_texsource_ui, results.vUVs)) )
{
hit_multilaser = true;
nearest_target_overlay = overlay_handle;
nearest_texture_source = ovrl_texsource_ui;
nearest_results = results;
}
}
}
}
//Non-MultiLaser hits are discared when not using the primary device
if ( (!hit_multilaser) && (!is_primary_device) )
{
nearest_target_overlay = vr::k_ulOverlayHandleInvalid;
nearest_texture_source = ovrl_texsource_none;
}
lp_device.IsActiveForMultiLaserInput = hit_multilaser;
}
}
//If we hit a different overlay (or lack thereof)...
if ( ( (lp_device.InputDownCount == 0) && (nearest_target_overlay != lp_device.OvrlHandleTargetLast) ) || (m_IsForceTargetOverlayActive) )
{
//...send focus leave event to last entered overlay
if (lp_device.OvrlHandleTargetLast != vr::k_ulOverlayHandleInvalid)
{
vr::VREvent_t vr_event = {0};
vr_event.trackedDeviceIndex = device_index;
vr_event.eventType = vr::VREvent_FocusLeave;
vr::VROverlayView()->PostOverlayEvent(lp_device.OvrlHandleTargetLast, &vr_event);
//Clear overlay cursor if we were probably the last one to set it
if ( (!was_active_for_multilaser_input) || (is_primary_device) )
{
vr::VROverlay()->ClearOverlayCursorPositionOverride(lp_device.OvrlHandleTargetLast);
}
}
//...and enter to the new one, if any
if (nearest_target_overlay != vr::k_ulOverlayHandleInvalid)
{
vr::VREvent_t vr_event = {0};
vr_event.trackedDeviceIndex = device_index;
vr_event.eventType = vr::VREvent_FocusEnter;
vr::VROverlayView()->PostOverlayEvent(nearest_target_overlay, &vr_event);
}
}
//Send mouse move event if we hit an overlay
if ( (nearest_target_overlay != vr::k_ulOverlayHandleInvalid) && (IsActive()) ) //Check for IsActive() so we don't send mouse move events for 1 frame after deactivation
{
vr::HmdVector2_t mouse_scale;
vr::VROverlay()->GetOverlayMouseScale(nearest_target_overlay, &mouse_scale);
vr::VREvent_t vr_event = {0};
vr_event.trackedDeviceIndex = device_index;
vr_event.eventType = vr::VREvent_MouseMove;
vr_event.data.mouse.x = nearest_results.vUVs.v[0] * mouse_scale.v[0];
vr_event.data.mouse.y = nearest_results.vUVs.v[1] * mouse_scale.v[1];
vr::VROverlayView()->PostOverlayEvent(nearest_target_overlay, &vr_event);
//Set blob
if (is_primary_device)
{
bool hide_intersection = false;
vr::VROverlay()->GetOverlayFlag(nearest_target_overlay, vr::VROverlayFlags_HideLaserIntersection, &hide_intersection);
if (!hide_intersection)
{
vr::VRTextureBounds_t tex_bounds;
vr::VROverlay()->GetOverlayTextureBounds(nearest_target_overlay, &tex_bounds);
int mapped_x = (mouse_scale.v[0] * tex_bounds.uMin);
int mapped_y = (mouse_scale.v[1] * tex_bounds.vMin);
int mapped_width = (mouse_scale.v[0] * tex_bounds.uMax) - mapped_x;
int mapped_height = (mouse_scale.v[1] * tex_bounds.vMax) - mapped_y;
//Flip to normal 2D space
float pointer_y = -round(vr_event.data.mouse.y) + mouse_scale.v[1];
float new_u = (vr_event.data.mouse.x - mapped_x) / (mapped_width);
float new_v = (pointer_y - mapped_y) / (mapped_height);
//Flip it back
new_v = -new_v + 1.0f;
vr::HmdVector2_t pos = {new_u * mouse_scale.v[0], new_v * mouse_scale.v[1]};
vr::VROverlay()->SetOverlayCursorPositionOverride(nearest_target_overlay, &pos);
}
else
{
vr::VROverlay()->ClearOverlayCursorPositionOverride(nearest_target_overlay);
}
}
//Set pointer length to overlay distance
lp_device.LaserLength = nearest_results.fDistance;
}
else if ( (!skip_input) && (lp_device.InputDownCount != 0) ) //Nothing hit, but input is down, extend
{
vr::HmdVector2_t mouse_scale;
vr::VROverlay()->GetOverlayMouseScale(lp_device.OvrlHandleTargetLast, &mouse_scale);
vr::VREvent_t vr_event = {0};
vr_event.trackedDeviceIndex = device_index;
vr_event.eventType = vr::VREvent_MouseMove;
vr_event.data.mouse.x = nearest_results.vUVs.v[0] * mouse_scale.v[0];
vr_event.data.mouse.y = (-nearest_results.vUVs.v[1] + 1.0f) * mouse_scale.v[1]; //For some reason the V position is upside-down when the intersection did not happen
vr::VROverlayView()->PostOverlayEvent(lp_device.OvrlHandleTargetLast, &vr_event);
//Clear cursor override when we're off the overlay since the override won't go past it like the real cursor would
if (is_primary_device)
{
vr::VROverlay()->ClearOverlayCursorPositionOverride(lp_device.OvrlHandleTargetLast);
}
//Act as if the overlay was hit for the rest of this function
nearest_target_overlay = lp_device.OvrlHandleTargetLast;
nearest_texture_source = lp_device.OvrlHandleTargetLastTextureSource;
}
else
{
//Set pointer length to default
lp_device.LaserLength = LASER_POINTER_DEFAULT_LENGTH;
}
//Input events
if (nearest_target_overlay != vr::k_ulOverlayHandleInvalid)
{
VRInput& vr_input = OutputManager::Get()->GetVRInput();
//Clicking
auto click_state_array = vr_input.GetLaserPointerClickState(lp_device.InputValueHandle);
uint32_t mouse_button_id = vr::VRMouseButton_Left;
lp_device.InputDownCount = 0;
for (const auto& input_data : click_state_array)
{
if (input_data.bActive)
{
if (input_data.bChanged)
{
vr::VREvent_t vr_event = {0};
vr_event.trackedDeviceIndex = device_index;
vr_event.eventType = (input_data.bState) ? vr::VREvent_MouseButtonDown : vr::VREvent_MouseButtonUp;
vr_event.data.mouse.button = mouse_button_id;
vr::VROverlayView()->PostOverlayEvent(nearest_target_overlay, &vr_event);
//As VRInput::GetActionOrigins() isn't reliable for some reason, we may not have the input value handle yet... we grab it from here instead as a workaround
if (lp_device.InputValueHandle == vr::k_ulInvalidInputValueHandle)
{
vr::InputOriginInfo_t origin_info = {0};
if (vr::VRInput()->GetOriginTrackedDeviceInfo(input_data.activeOrigin, &origin_info, sizeof(vr::InputOriginInfo_t)) == vr::VRInputError_None)
{
if ( (origin_info.trackedDeviceIndex < vr::k_unMaxTrackedDeviceCount) )
{
m_Devices[origin_info.trackedDeviceIndex].InputValueHandle = origin_info.devicePath;
}
}
}
}
if (input_data.bState)
{
lp_device.InputDownCount++;
}
}
mouse_button_id *= 2; //with click_state_array containing 5 elements, this goes over the OpenVR defined buttons to include our custom VRMouseButton_DP_Aux01/02
}
//Scrolling
//Get scroll mode from overlay and set it for VRInput
bool send_scroll_discrete = false, send_scroll_smooth = false;
vr::VROverlay()->GetOverlayFlag(nearest_target_overlay, vr::VROverlayFlags_SendVRDiscreteScrollEvents, &send_scroll_discrete);
vr::VROverlay()->GetOverlayFlag(nearest_target_overlay, vr::VROverlayFlags_SendVRSmoothScrollEvents, &send_scroll_smooth);
VRInputScrollMode scroll_mode = (send_scroll_smooth) ? vrinput_scroll_smooth : (send_scroll_discrete) ? vrinput_scroll_discrete : vrinput_scroll_none;
vr_input.SetLaserPointerScrollMode(scroll_mode);
switch (scroll_mode)
{
case vrinput_scroll_discrete:
{
vr::InputAnalogActionData_t input_data = vr_input.GetLaserPointerScrollDiscreteState();
//If active and changed or not 0 on any axis
if ( (input_data.bActive) && ( (input_data.x != 0.0f) || (input_data.y != 0.0f) || (input_data.deltaX != 0.0f) || (input_data.deltaY != 0.0f) ) )
{
vr::VREvent_t vr_event = {0};
vr_event.trackedDeviceIndex = device_index;
vr_event.eventType = vr::VREvent_ScrollDiscrete;
vr_event.data.scroll.xdelta = input_data.x;
vr_event.data.scroll.ydelta = input_data.y;
vr::VROverlayView()->PostOverlayEvent(nearest_target_overlay, &vr_event);
m_LastScrollTick = ::GetTickCount64();
}
break;
}
case vrinput_scroll_smooth:
{
vr::InputAnalogActionData_t input_data = vr_input.GetLaserPointerScrollSmoothState();
//If active and changed or not 0 on any axis
if ( (input_data.bActive) && ( (input_data.x != 0.0f) || (input_data.y != 0.0f) || (input_data.deltaX != 0.0f) || (input_data.deltaY != 0.0f) ) )
{
vr::VREvent_t vr_event = {0};
vr_event.trackedDeviceIndex = device_index;
vr_event.eventType = vr::VREvent_ScrollSmooth;
vr_event.data.scroll.xdelta = input_data.x;
vr_event.data.scroll.ydelta = input_data.y;
vr::VROverlayView()->PostOverlayEvent(nearest_target_overlay, &vr_event);
m_LastScrollTick = ::GetTickCount64();
}
break;
}
default: break;
}
//Direct Drag
{
vr::InputDigitalActionData_t input_data = vr_input.GetLaserPointerDragState(lp_device.InputValueHandle);
if (input_data.bActive)
{
if (input_data.bChanged)
{
SendDirectDragCommand(nearest_target_overlay, input_data.bState);
lp_device.IsDragDown = input_data.bState;
}
if (input_data.bState)
{
lp_device.InputDownCount++;
}
}
}
}
else
{
lp_device.InputDownCount = 0;
}
if (nearest_target_overlay != lp_device.OvrlHandleTargetLast)
{
lp_device.OvrlHandleTargetLast = nearest_target_overlay;
lp_device.OvrlHandleTargetLastTextureSource = nearest_texture_source;
if (is_primary_device)
{
ConfigManager::SetValue(configid_handle_state_dplus_laser_pointer_target_overlay, lp_device.OvrlHandleTargetLast);
IPCManager::Get().PostConfigMessageToUIApp(configid_handle_state_dplus_laser_pointer_target_overlay, pun_cast(lp_device.OvrlHandleTargetLast));
}
}
m_IsForceTargetOverlayActive = false;
//Update overlay length
vr::VRTextureBounds_t tex_bounds = {0};
tex_bounds.uMax = 1.0f;
tex_bounds.vMax = lp_device.LaserLength / LASER_POINTER_OVERLAY_WIDTH;
vr::VROverlay()->SetOverlayTextureBounds(lp_device.OvrlHandle, &tex_bounds);
}
void LaserPointer::SendDirectDragCommand(vr::VROverlayHandle_t overlay_handle_target, bool do_start_drag)
{
unsigned int overlay_id = OverlayManager::Get().FindOverlayID(overlay_handle_target);
if (overlay_id == k_ulOverlayID_None)
{
//Check if target overlay is UI overlay
const bool is_ui = ( (std::find(m_OverlayHandlesUI.begin(), m_OverlayHandlesUI.end(), overlay_handle_target) != m_OverlayHandlesUI.end()) ||
(std::find(m_OverlayHandlesMultiLaser.begin(), m_OverlayHandlesMultiLaser.end(), overlay_handle_target) != m_OverlayHandlesMultiLaser.end()) );
//Tell UI to start a drag on it if it is
if (is_ui)
{
IPCManager::Get().PostMessageToUIApp(ipcmsg_action, ipcact_lpointer_ui_drag, (do_start_drag) ? 1 : 0);
}
}
else
{
(do_start_drag) ? OutputManager::Get()->OverlayDirectDragStart(overlay_id) : OutputManager::Get()->OverlayDirectDragFinish(overlay_id);
}
}
void LaserPointer::Update()
{
//Refresh/Init handles in any case if they're empty
if (m_OverlayHandlesUI.empty())
{
RefreshCachedOverlayHandles();
}
vr::TrackedDeviceIndex_t primary_pointer_device = (vr::TrackedDeviceIndex_t)ConfigManager::GetValue(configid_int_state_dplus_laser_pointer_device);
if ( (primary_pointer_device == vr::k_unTrackedDeviceIndexInvalid) && (!m_HadPrimaryPointerDevice) )
return;
VRInput& vr_input = OutputManager::Get()->GetVRInput();
//Trigger pending vibrations if we know the action set is now active
if ( (m_HadPrimaryPointerDevice) && (m_DeviceHapticPending != vr::k_unTrackedDeviceIndexInvalid) )
{
TriggerLaserPointerHaptics(m_DeviceHapticPending);
m_DeviceHapticPending = vr::k_unTrackedDeviceIndexInvalid;
}
//Primary device switching
if (!vr::IVROverlayEx::IsSystemLaserPointerActive())
{
vr::InputDigitalActionData_t left_click_state = vr_input.GetLaserPointerLeftClickState();
if ( (left_click_state.bChanged) && (left_click_state.bState) )
{
vr::InputOriginInfo_t origin_info = vr_input.GetOriginTrackedDeviceInfoEx(left_click_state.activeOrigin);
if ( (origin_info.trackedDeviceIndex < vr::k_unMaxTrackedDeviceCount) && (origin_info.trackedDeviceIndex != primary_pointer_device) &&
(!m_Devices[origin_info.trackedDeviceIndex].IsActiveForMultiLaserInput))
{
//Set device path here since we have it ready anyways
m_Devices[origin_info.trackedDeviceIndex].InputValueHandle = origin_info.devicePath;
//Store current tick to allow a grace period when switching devices while auto toggling is active and not actually hovering anything with the new pointer
m_LastPrimaryDeviceSwitchTick = ::GetTickCount64();
SetActiveDevice(origin_info.trackedDeviceIndex, m_ActivationOrigin);
}
}
}
//Don't do anything if a dashboard pointer is active or we have no primary pointer ourselves
bool should_pointer_be_active = IsActive();
//Also clear active device when we shouldn't be active
if (!should_pointer_be_active)
{
ClearActiveDevice();
}
if ( (m_HadPrimaryPointerDevice) || (should_pointer_be_active) )
{
for (vr::TrackedDeviceIndex_t i = 0; i <= m_DeviceMaxActiveID; ++i)
{
if ( (m_Devices[i].OvrlHandle != vr::k_ulOverlayHandleInvalid) || (m_Devices[i].UseHMDAsOrigin) )
{
UpdateIntersection(i);
}
}
//Store if we were active the previous frame so we can sent overlay leave events in this iteration and clean up state before not doing this when not needed afterwards
m_HadPrimaryPointerDevice = should_pointer_be_active;
}
for (vr::TrackedDeviceIndex_t i = 0; i <= m_DeviceMaxActiveID; ++i)
{
if (m_Devices[i].OvrlHandle != vr::k_ulOverlayHandleInvalid)
{
UpdateDeviceOverlay(i);
}
}
vr_input.SetLaserPointerActive(should_pointer_be_active);
}
void LaserPointer::SetActiveDevice(vr::TrackedDeviceIndex_t device_index, LaserPointerActivationOrigin activation_origin)
{
if (device_index >= vr::k_unMaxTrackedDeviceCount)
return;
vr::TrackedDeviceIndex_t previous_active_device = (vr::TrackedDeviceIndex_t)ConfigManager::GetValue(configid_int_state_dplus_laser_pointer_device);
if (previous_active_device == device_index)
return;
//Clear last overlay cursor override
if (previous_active_device < vr::k_unMaxTrackedDeviceCount)
{
LaserPointerDevice& lp_device_prev = m_Devices[previous_active_device];
if (lp_device_prev.OvrlHandleTargetLast != vr::k_ulOverlayHandleInvalid)
{
vr::VROverlay()->ClearOverlayCursorPositionOverride(lp_device_prev.OvrlHandleTargetLast);
}
}
LaserPointerDevice& lp_device = m_Devices[device_index];
//Set UseHMDAsOrigin, which forces using the HMD as an origin and not actually display any laser
lp_device.UseHMDAsOrigin = ((device_index == vr::k_unTrackedDeviceIndex_Hmd) || (vr::VRSystem()->GetBoolTrackedDeviceProperty(device_index, vr::Prop_NeverTracked_Bool)));
//Create overlay if it doesn't exist yet (and HMD isn't used as origin)
if ( (!lp_device.UseHMDAsOrigin) && (lp_device.OvrlHandle == vr::k_ulOverlayHandleInvalid) )
{
CreateDeviceOverlay(device_index);
}
//Try finding the input value handle for this device if it's not set yet
if (lp_device.InputValueHandle == vr::k_ulInvalidInputValueHandle)
{
std::vector devices_info = OutputManager::Get()->GetVRInput().GetLaserPointerDevicesInfo();
const auto it = std::find_if(devices_info.begin(), devices_info.end(), [&](const auto& input_origin_info){ return (input_origin_info.trackedDeviceIndex == device_index); });
if (it != devices_info.end())
{
lp_device.InputValueHandle = it->devicePath;
}
}
//Adjust max active ID if it's smaller
if (m_DeviceMaxActiveID < device_index)
{
m_DeviceMaxActiveID = device_index;
}
//Vibrate if this activates the Desktop+ pointer (not just switching)
if (previous_active_device == vr::k_unTrackedDeviceIndexInvalid)
{
m_DeviceHapticPending = device_index; //Postpone the actual call until later since the relevant action set is not active yet
}
m_ActivationOrigin = activation_origin;
ConfigManager::SetValue(configid_int_state_dplus_laser_pointer_device, device_index);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_dplus_laser_pointer_device, device_index);
ConfigManager::SetValue(configid_handle_state_dplus_laser_pointer_target_overlay, lp_device.OvrlHandleTargetLast);
IPCManager::Get().PostConfigMessageToUIApp(configid_handle_state_dplus_laser_pointer_target_overlay, pun_cast(lp_device.OvrlHandleTargetLast));
}
void LaserPointer::ClearActiveDevice()
{
//Clear last overlay cursor override
vr::TrackedDeviceIndex_t previous_active_device = (vr::TrackedDeviceIndex_t)ConfigManager::GetValue(configid_int_state_dplus_laser_pointer_device);
if (previous_active_device < vr::k_unMaxTrackedDeviceCount)
{
LaserPointerDevice& lp_device_prev = m_Devices[previous_active_device];
//Finish active direct drag
if (lp_device_prev.IsDragDown)
{
SendDirectDragCommand(lp_device_prev.OvrlHandleTargetLast, false);
lp_device_prev.IsDragDown = false;
}
//Send focus leave event to last entered overlay if there is any
if (lp_device_prev.OvrlHandleTargetLast != vr::k_ulOverlayHandleInvalid)
{
vr::VREvent_t vr_event = {0};
vr_event.trackedDeviceIndex = previous_active_device;
vr_event.eventType = vr::VREvent_FocusLeave;
vr::VROverlayView()->PostOverlayEvent(lp_device_prev.OvrlHandleTargetLast, &vr_event);
//Also clear cursor override
vr::VROverlay()->ClearOverlayCursorPositionOverride(lp_device_prev.OvrlHandleTargetLast);
}
}
m_ActivationOrigin = dplp_activation_origin_none;
ConfigManager::SetValue(configid_int_state_dplus_laser_pointer_device, vr::k_unTrackedDeviceIndexInvalid);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_dplus_laser_pointer_device, vr::k_unTrackedDeviceIndexInvalid);
ConfigManager::SetValue(configid_handle_state_dplus_laser_pointer_target_overlay, vr::k_ulOverlayHandleInvalid);
IPCManager::Get().PostConfigMessageToUIApp(configid_handle_state_dplus_laser_pointer_target_overlay, vr::k_ulOverlayHandleInvalid);
}
void LaserPointer::RemoveDevice(vr::TrackedDeviceIndex_t device_index)
{
if (device_index >= vr::k_unMaxTrackedDeviceCount)
return;
LaserPointerDevice& lp_device = m_Devices[device_index];
if (lp_device.OvrlHandle != vr::k_ulOverlayHandleInvalid)
{
vr::VROverlay()->DestroyOverlay(lp_device.OvrlHandle);
}
//Send focus leave event to last entered overlay if there is any
if (lp_device.OvrlHandleTargetLast != vr::k_ulOverlayHandleInvalid)
{
vr::VREvent_t vr_event = {0};
vr_event.trackedDeviceIndex = device_index;
vr_event.eventType = vr::VREvent_FocusLeave;
vr::VROverlayView()->PostOverlayEvent(lp_device.OvrlHandleTargetLast, &vr_event);
//Also remove pointer override in case there is any
bool is_primary_device = (device_index == (vr::TrackedDeviceIndex_t)ConfigManager::GetValue(configid_int_state_dplus_laser_pointer_device));
if ( (!lp_device.IsActiveForMultiLaserInput) || (is_primary_device) )
{
vr::VROverlay()->ClearOverlayCursorPositionOverride(lp_device.OvrlHandleTargetLast);
}
}
lp_device = LaserPointerDevice();
}
void LaserPointer::RefreshCachedOverlayHandles()
{
m_OverlayHandlesUI.clear();
m_OverlayHandlesMultiLaser.clear();
vr::VROverlayHandle_t overlay_handle;
//Find UI overlays (except keyboard)
const char* ui_overlay_keys[] =
{
"elvissteinjr.DesktopPlusUI",
"elvissteinjr.DesktopPlusUIFloating",
"elvissteinjr.DesktopPlusUISettings",
"elvissteinjr.DesktopPlusUIOverlayProperties",
"elvissteinjr.DesktopPlusUIAux"
};
for (const char* key : ui_overlay_keys)
{
vr::VROverlay()->FindOverlay(key, &overlay_handle);
if (overlay_handle != vr::k_ulOverlayHandleInvalid)
{
m_OverlayHandlesUI.push_back(overlay_handle);
}
}
//Find MultiLaser overlays (which is just keyboard right now)
vr::VROverlay()->FindOverlay("elvissteinjr.DesktopPlusUIKeyboard", &overlay_handle);
if (overlay_handle != vr::k_ulOverlayHandleInvalid)
{
m_OverlayHandlesMultiLaser.push_back(overlay_handle);
}
//Cache UI mouse scale, since it's not going to change
if (!m_OverlayHandlesUI.empty())
{
vr::HmdVector2_t mouse_scale;
vr::VROverlay()->GetOverlayMouseScale(m_OverlayHandlesUI[0], &mouse_scale);
m_UIMouseScale.set((int)mouse_scale.v[0], (int)mouse_scale.v[1]);
}
}
void LaserPointer::TriggerLaserPointerHaptics(vr::TrackedDeviceIndex_t device_index) const
{
OutputManager::Get()->GetVRInput().TriggerLaserPointerHaptics((device_index < vr::k_unMaxTrackedDeviceCount) ? m_Devices[device_index].InputValueHandle : vr::k_ulInvalidInputValueHandle);
}
void LaserPointer::ForceTargetOverlay(vr::VROverlayHandle_t overlay_handle)
{
vr::TrackedDeviceIndex_t primary_pointer_device = (vr::TrackedDeviceIndex_t)ConfigManager::GetValue(configid_int_state_dplus_laser_pointer_device);
if (primary_pointer_device >= vr::k_unMaxTrackedDeviceCount)
return;
LaserPointerDevice& lp_device = m_Devices[primary_pointer_device];
//Don't do anything if there is no target overlay to switch from or if it would be a no-op
if ( (lp_device.OvrlHandleTargetLast == vr::k_ulOverlayHandleInvalid) || (lp_device.OvrlHandleTargetLast == overlay_handle) )
return;
//Run UpdateIntersection() with last target removed so overlay leave events are sent
m_IsForceTargetOverlayActive = true;
m_ForceTargetOverlayHandle = vr::k_ulOverlayHandleInvalid;
UpdateIntersection(primary_pointer_device);
//Set force variables again so they apply next time UpdateIntersection() us called
m_IsForceTargetOverlayActive = true;
m_ForceTargetOverlayHandle = overlay_handle;
}
LaserPointerActivationOrigin LaserPointer::GetActivationOrigin() const
{
return m_ActivationOrigin;
}
bool LaserPointer::IsActive() const
{
vr::TrackedDeviceIndex_t primary_pointer_device = (vr::TrackedDeviceIndex_t)ConfigManager::GetValue(configid_int_state_dplus_laser_pointer_device);
return ( (!vr::IVROverlayEx::IsSystemLaserPointerActive()) && (primary_pointer_device != vr::k_unTrackedDeviceIndexInvalid) );
}
vr::TrackedDeviceIndex_t LaserPointer::IsAnyOverlayHovered(float max_distance) const
{
//If active, just check if the primary pointer has a last target overlay
if (IsActive())
{
vr::TrackedDeviceIndex_t primary_pointer_device = (vr::TrackedDeviceIndex_t)ConfigManager::GetValue(configid_int_state_dplus_laser_pointer_device);
if (primary_pointer_device >= vr::k_unMaxTrackedDeviceCount)
return vr::k_unTrackedDeviceIndexInvalid;
const LaserPointerDevice& lp_device = m_Devices[primary_pointer_device];
//Allow a 1500ms grace period after device switching even when not actually hovering anything & ignore distance if input is down
if ( (m_LastPrimaryDeviceSwitchTick + 1500 > ::GetTickCount64() ) || ( (lp_device.OvrlHandleTargetLast != vr::k_ulOverlayHandleInvalid) && (lp_device.LaserLength <= max_distance) ) ||
(lp_device.InputDownCount != 0) )
{
return primary_pointer_device;
}
}
else //...otherwise check all possible overlays
{
//Check left and right hand controller and HMD if setting is enabled
const bool lp_hmd_enabled_and_toggle_unbound = ((ConfigManager::GetValue(configid_bool_input_laser_pointer_hmd_device)) &&
(ConfigManager::GetValue(configid_int_input_laser_pointer_hmd_device_keycode_toggle) == 0));
const vr::TrackedDeviceIndex_t devices[] =
{
(lp_hmd_enabled_and_toggle_unbound) ? vr::k_unTrackedDeviceIndex_Hmd : vr::k_unTrackedDeviceIndexInvalid,
vr::VRSystem()->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_LeftHand),
vr::VRSystem()->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_RightHand),
};
for (int i = 0; i < sizeof(devices)/sizeof(*devices); ++i)
{
const vr::TrackedDeviceIndex_t device_index = devices[i];
OverlayOrigin origin_avoid = (OverlayOrigin)(ovrl_origin_hmd+i);
//Set up intersection test
vr::VROverlayIntersectionParams_t params = {0};
vr::VROverlayIntersectionResults_t results = {0};
if (!vr::IVROverlayEx::GetOverlayIntersectionParamsForDevice(params, device_index, vr::TrackingUniverseStanding))
{
continue; //Skip if pose isn't valid
}
//Desktop+ overlays
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const Overlay& overlay = OverlayManager::Get().GetOverlay(i);
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if ( (data.ConfigInt[configid_int_overlay_origin] != origin_avoid) && (overlay.IsVisible()) && (data.ConfigBool[configid_bool_overlay_input_dplus_lp_enabled]) )
{
//Check if input is enabled right now (could differ from config setting)
vr::VROverlayInputMethod input_method = vr::VROverlayInputMethod_None;
vr::VROverlay()->GetOverlayInputMethod(overlay.GetHandle(), &input_method);
if ( (input_method == vr::VROverlayInputMethod_Mouse) &&
(vr::VROverlay()->ComputeOverlayIntersection(overlay.GetHandle(), ¶ms, &results)) && (results.fDistance <= max_distance) )
{
if ( (vr::IVROverlayEx::IsOverlayIntersectionHitFrontFacing(params, results)) && (IntersectionMaskHitTest(overlay.GetTextureSource(), results.vUVs)) )
{
return device_index;
}
}
}
}
//Desktop+ UI overlays
//
//See UpdateIntersection() for current issues
for (vr::VROverlayHandle_t overlay_handle : m_OverlayHandlesUI)
{
if (vr::VROverlay()->IsOverlayVisible(overlay_handle))
{
if ( (vr::VROverlay()->ComputeOverlayIntersection(overlay_handle, ¶ms, &results)) && (results.fDistance <= max_distance) )
{
if ( (vr::IVROverlayEx::IsOverlayIntersectionHitFrontFacing(params, results)) && (IntersectionMaskHitTest(ovrl_texsource_ui, results.vUVs)) )
{
return device_index;
}
}
}
}
//MultiLaser overlays (just keyboard right now)
for (vr::VROverlayHandle_t overlay_handle : m_OverlayHandlesMultiLaser)
{
if (vr::VROverlay()->IsOverlayVisible(overlay_handle))
{
if ( (vr::VROverlay()->ComputeOverlayIntersection(overlay_handle, ¶ms, &results)) && (results.fDistance <= max_distance) )
{
if ( (vr::IVROverlayEx::IsOverlayIntersectionHitFrontFacing(params, results)) && (IntersectionMaskHitTest(ovrl_texsource_ui, results.vUVs)) )
{
return device_index;
}
}
}
}
}
}
return vr::k_unTrackedDeviceIndexInvalid;
}
bool LaserPointer::IsScrolling() const
{
//Treat a 500ms window after the last scroll event as active scrolling (discrete scroll events aren't a constant stream when active)
return (m_LastScrollTick + 500 > ::GetTickCount64());
}
bool LaserPointer::IntersectionMaskHitTest(OverlayTextureSource texsource, vr::HmdVector2_t& uv) const
{
if (texsource == ovrl_texsource_desktop_duplication)
{
Vector2Int point(int(uv.v[0] * OutputManager::Get()->GetDesktopWidth()), int((-uv.v[1] + 1.0f) * OutputManager::Get()->GetDesktopHeight()));
for (const auto& rect : OutputManager::Get()->GetDesktopRects())
{
if (rect.Contains(point))
{
return true;
}
}
return false;
}
else if (texsource == ovrl_texsource_ui)
{
Vector2Int point(int(uv.v[0] * m_UIMouseScale.x), int((-uv.v[1] + 1.0f) * m_UIMouseScale.y));
for (const auto& rect : m_UIIntersectionMaskRects)
{
if (rect.Contains(point))
{
return true;
}
}
return false;
}
//Other texture sources don't have masks, so they always pass
return true;
}
void LaserPointer::UIIntersectionMaskAddRect(DPRect& rect)
{
m_UIIntersectionMaskRectsPending.push_back(rect);
}
void LaserPointer::UIIntersectionMaskFinish()
{
m_UIIntersectionMaskRects = m_UIIntersectionMaskRectsPending;
m_UIIntersectionMaskRectsPending.clear();
}
================================================
FILE: src/DesktopPlus/LaserPointer.h
================================================
#pragma once
#include "DPRect.h"
#include "Overlays.h"
#include "openvr.h"
#include
struct LaserPointerDevice
{
vr::VROverlayHandle_t OvrlHandle = vr::k_ulOverlayHandleInvalid;
vr::VRInputValueHandle_t InputValueHandle = vr::k_ulInvalidInputValueHandle;
bool UseHMDAsOrigin = false;
bool IsVisible = false;
float LaserLength = 0.0f;
vr::VROverlayHandle_t OvrlHandleTargetLast = vr::k_ulOverlayHandleInvalid;
OverlayTextureSource OvrlHandleTargetLastTextureSource = ovrl_texsource_none;
bool IsActiveForMultiLaserInput = false;
int InputDownCount = 0;
bool IsDragDown = false;
};
//Optional origin that can be passed to SetActiveDevice() in order to keep track what activated the laser pointer (and thus should be responsible for deactivating)
enum LaserPointerActivationOrigin
{
dplp_activation_origin_none,
dplp_activation_origin_auto_toggle,
dplp_activation_origin_input_binding
};
//Custom laser pointer implementation for use outside of the dashboard
//Operates on Desktop+ and Desktop+ UI overlays, sending overlay events matching SteamVR laser input behavior as closely as possible
//Actually sends middle mouse events when bound
//Supports use of any tracked device that has the corrects inputs bound, though IsAnyOverlayHovered() only checks handed controllers for auto interaction toggle
//Paralell/Multi-Laser input is supported for certain overlays that can handle it (currently only the VR keyboard)
class LaserPointer
{
private:
LaserPointerDevice m_Devices[vr::k_unMaxTrackedDeviceCount];
std::vector m_OverlayHandlesUI;
std::vector m_OverlayHandlesMultiLaser;
LaserPointerActivationOrigin m_ActivationOrigin;
bool m_HadPrimaryPointerDevice;
vr::TrackedDeviceIndex_t m_DeviceMaxActiveID;
ULONGLONG m_LastPrimaryDeviceSwitchTick;
ULONGLONG m_LastScrollTick;
vr::TrackedDeviceIndex_t m_DeviceHapticPending;
//State set by ForceTargetOverlay()
bool m_IsForceTargetOverlayActive;
vr::VROverlayHandle_t m_ForceTargetOverlayHandle;
Vector2Int m_UIMouseScale;
std::vector m_UIIntersectionMaskRects;
std::vector m_UIIntersectionMaskRectsPending;
void CreateDeviceOverlay(vr::TrackedDeviceIndex_t device_index);
void UpdateDeviceOverlay(vr::TrackedDeviceIndex_t device_index);
void UpdateIntersection(vr::TrackedDeviceIndex_t device_index);
void SendDirectDragCommand(vr::VROverlayHandle_t overlay_handle_target, bool do_start_drag);
public:
LaserPointer();
~LaserPointer();
void Update();
void SetActiveDevice(vr::TrackedDeviceIndex_t device_index, LaserPointerActivationOrigin activation_origin = dplp_activation_origin_none);
void ClearActiveDevice();
void RemoveDevice(vr::TrackedDeviceIndex_t device_index); //Clears device entry, called on device disconnect
void RefreshCachedOverlayHandles();
void TriggerLaserPointerHaptics(vr::TrackedDeviceIndex_t device_index) const;
void ForceTargetOverlay(vr::VROverlayHandle_t overlay_handle); //Forces a different overlay to be current pointer target (only if there's currently one)
//ComputeOverlayIntersection() does not take intersection masks into account. It's a bit cumbersome, but we track the DDP/UI ones ourselves to get around that.
bool IntersectionMaskHitTest(OverlayTextureSource texsource, vr::HmdVector2_t& uv) const;
void UIIntersectionMaskAddRect(DPRect& rect);
void UIIntersectionMaskFinish();
LaserPointerActivationOrigin GetActivationOrigin() const;
bool IsActive() const;
vr::TrackedDeviceIndex_t IsAnyOverlayHovered(float max_distance) const; //Returns hovering device_index (or invalid if none). Only checks for LaserPointer supported overlays
bool IsScrolling() const;
};
================================================
FILE: src/DesktopPlus/OutputManager.cpp
================================================
#include "OutputManager.h"
//Keep building with 10.0.17763.0 / 1809 SDK optional
#ifdef NTDDI_WIN10_RS5
#include
#else
#define DPLUS_DUP_NO_HDR
#endif
#include
#include
#include
#include
using namespace DirectX;
#include
#include
#include
#include "WindowManager.h"
#include "Util.h"
#include "OpenVRExt.h"
#include "Logging.h"
#include "DesktopPlusWinRT.h"
#include "DPBrowserAPIClient.h"
static OutputManager* g_OutputManager; //May not always exist, but there also should never be two, so this is fine
OutputManager* OutputManager::Get()
{
return g_OutputManager;
}
//
//Quick note about OutputManager (and Desktop+ in general) handles multi-overlay access:
//Most functions use the "current" overlay as set by the OverlayManager or by having ConfigManager forward config values from the *_overlay_* configids
//When needed, the current overlay is temporarily changed to the one to act on.
//To have the UI act in such a scenario, the configid_int_state_overlay_current_id_override is typically used, as there may be visible changes to the user for one frame otherwise
//To change the current overlay while nested in a temporary override, post a configid_int_interface_overlay_current_id message to both applications instead of just the counterpart
//
//This may all seem a bit messy, but helped retrofit the single overlay code a lot. Feel like cleaning this up with a way better scheme? Go ahead.
//
OutputManager::OutputManager(HANDLE PauseDuplicationEvent, HANDLE ResumeDuplicationEvent) :
m_Device(nullptr),
m_DeviceContext(nullptr),
m_Sampler(nullptr),
m_BlendState(nullptr),
m_RasterizerState(nullptr),
m_VertexShader(nullptr),
m_PixelShader(nullptr),
m_PixelShaderCursor(nullptr),
m_InputLayout(nullptr),
m_SharedSurf(nullptr),
m_VertexBuffer(nullptr),
m_ShaderResource(nullptr),
m_KeyMutex(nullptr),
m_WindowHandle(nullptr),
m_PauseDuplicationEvent(PauseDuplicationEvent),
m_ResumeDuplicationEvent(ResumeDuplicationEvent),
m_DesktopX(0),
m_DesktopY(0),
m_DesktopWidth(-1),
m_DesktopHeight(-1),
m_MaxActiveRefreshDelay(16),
m_OutputPendingSkippedFrame(false),
m_OutputPendingFullRefresh(false),
m_OutputHDRAvailable(false),
m_OutputInvalid(false),
m_OutputPendingDirtyRect{-1, -1, -1, -1},
m_OutputAlphaCheckFailed(false),
m_OutputAlphaChecksPending(0),
m_OvrlHandleIcon(vr::k_ulOverlayHandleInvalid),
m_OvrlHandleDashboardDummy(vr::k_ulOverlayHandleInvalid),
m_OvrlHandleDesktopTexture(vr::k_ulOverlayHandleInvalid),
m_OvrlTex(nullptr),
m_OvrlRTV(nullptr),
m_OvrlActiveCount(0),
m_OvrlDesktopDuplActiveCount(0),
m_OvrlDashboardActive(false),
m_OvrlInputActive(false),
m_OvrlDirectDragActive(false),
m_OvrlTempDragStartTick(0),
m_PendingDashboardDummyHeight(0.0f),
m_LastApplyTransformTick(0),
m_LastFrameTransformUpdateTick(0),
m_MouseLastClickTick(0),
m_MouseIgnoreMoveEvent(false),
m_MouseCursorNeedsUpdate(false),
m_MouseLaserPointerUsedLastUpdate(false),
m_MouseLastLaserPointerMoveBlocked(false),
m_MouseLastLaserPointerX(-1),
m_MouseLastLaserPointerY(-1),
m_MouseIgnoreMoveEventMissCount(0),
m_MouseLeftDownOverlayID(k_ulOverlayID_None),
m_MouseLaserPointerScrollDeltaStart{0},
m_MouseLaserPointerScrollDeltaFrequency{0},
m_IsFirstLaunch(false),
m_ComInitDone(false),
m_DashboardActivatedOnce(false),
m_MultiGPUTargetDevice(nullptr),
m_MultiGPUTargetDeviceContext(nullptr),
m_MultiGPUTexStaging(nullptr),
m_MultiGPUTexTarget(nullptr),
m_PerformanceFrameCount(0),
m_PerformanceFrameCountStartTick(0),
m_PerformanceUpdateLimiterDelay{0},
m_IsAnyHotkeyActive(false),
m_RegisteredHotkeyCount(0)
{
m_MouseLastInfo.ShapeInfo.Type = DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR;
::QueryPerformanceFrequency(&m_MouseLaserPointerScrollDeltaFrequency);
//Initialize ConfigManager and set first launch state based on existence of config file (used to detect first launch in Steam version)
m_IsFirstLaunch = !ConfigManager::Get().LoadConfigFromFile();
g_OutputManager = this;
}
//
// Destructor which calls CleanRefs to release all references and memory.
//
OutputManager::~OutputManager()
{
CleanRefs();
g_OutputManager = nullptr;
}
//
// Releases all references
//
void OutputManager::CleanRefs()
{
//Release the shared desktop overlay texture since we're destroying the D3D11 device it's attached to (don't need this after shutting down OpenVR though)
if (vr::VROverlay() != nullptr)
{
vr::VROverlayEx()->ReleaseSharedOverlayTexture(m_OvrlHandleDesktopTexture);
}
if (m_VertexShader)
{
m_VertexShader->Release();
m_VertexShader = nullptr;
}
if (m_PixelShader)
{
m_PixelShader->Release();
m_PixelShader = nullptr;
}
if (m_PixelShaderCursor)
{
m_PixelShaderCursor->Release();
m_PixelShaderCursor = nullptr;
}
if (m_InputLayout)
{
m_InputLayout->Release();
m_InputLayout = nullptr;
}
if (m_Sampler)
{
m_Sampler->Release();
m_Sampler = nullptr;
}
if (m_BlendState)
{
m_BlendState->Release();
m_BlendState = nullptr;
}
if (m_RasterizerState)
{
m_RasterizerState->Release();
m_RasterizerState = nullptr;
}
if (m_DeviceContext)
{
m_DeviceContext->Release();
m_DeviceContext = nullptr;
}
if (m_Device)
{
m_Device->Release();
m_Device = nullptr;
}
if (m_SharedSurf)
{
m_SharedSurf->Release();
m_SharedSurf = nullptr;
}
if (m_VertexBuffer)
{
m_VertexBuffer->Release();
m_VertexBuffer = nullptr;
}
if (m_ShaderResource)
{
m_ShaderResource->Release();
m_ShaderResource = nullptr;
}
if (m_OvrlTex)
{
m_OvrlTex->Release();
m_OvrlTex = nullptr;
}
if (m_OvrlRTV)
{
m_OvrlRTV->Release();
m_OvrlRTV = nullptr;
}
m_MouseTex.Reset();
m_MouseShaderRes.Reset();
//Reset mouse state variables too
m_MouseLastClickTick = 0;
m_MouseIgnoreMoveEvent = false;
m_MouseLastInfo = PTR_INFO();
m_MouseLastInfo.ShapeInfo.Type = DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR;
m_MouseLastLaserPointerX = -1;
m_MouseLastLaserPointerY = -1;
if (m_KeyMutex)
{
m_KeyMutex->Release();
m_KeyMutex = nullptr;
}
if (m_ComInitDone)
{
::CoUninitialize();
}
if (m_MultiGPUTargetDevice)
{
m_MultiGPUTargetDevice->Release();
m_MultiGPUTargetDevice = nullptr;
}
if (m_MultiGPUTargetDeviceContext)
{
m_MultiGPUTargetDeviceContext->Release();
m_MultiGPUTargetDeviceContext = nullptr;
}
if (m_MultiGPUTexStaging)
{
m_MultiGPUTexStaging->Release();
m_MultiGPUTexStaging = nullptr;
}
if (m_MultiGPUTexTarget)
{
m_MultiGPUTexTarget->Release();
m_MultiGPUTexTarget = nullptr;
}
}
//
// Initialize all state
//
DUPL_RETURN OutputManager::InitOutput(HWND Window, _Out_ INT& SingleOutput, _Out_ UINT* OutCount, _Out_ RECT* DeskBounds)
{
LOG_F(INFO, "Initializing Desktop Duplication...");
HRESULT hr = S_OK;
m_OutputInvalid = false;
if (ConfigManager::GetValue(configid_bool_performance_single_desktop_mirroring))
{
SingleOutput = clamp(ConfigManager::GetValue(configid_int_overlay_desktop_id), -1, ::GetSystemMetrics(SM_CMONITORS) - 1);
}
else
{
SingleOutput = -1;
}
// Store window handle
m_WindowHandle = Window;
//Get preferred adapter if there is any, this detects which GPU the target desktop is on
Microsoft::WRL::ComPtr adapter_ptr_preferred;
Microsoft::WRL::ComPtr adapter_ptr_vr;
std::vector desktop_rects_prev = m_DesktopRects;
int desktop_x_prev = m_DesktopX;
int desktop_y_prev = m_DesktopY;
EnumerateOutputs(SingleOutput, &adapter_ptr_preferred, &adapter_ptr_vr);
//If there's no preferred adapter it should default to the one the HMD is connected to
if (adapter_ptr_preferred == nullptr)
{
//If both are nullptr it'll still try to find a working adapter to init, though it'll probably not work at the end in that scenario
adapter_ptr_preferred = adapter_ptr_vr;
}
//If they're the same, we don't need to do any multi-gpu handling
if (adapter_ptr_vr == adapter_ptr_preferred)
{
adapter_ptr_vr = nullptr;
}
// Driver types supported
D3D_DRIVER_TYPE DriverTypes[] =
{
D3D_DRIVER_TYPE_HARDWARE,
D3D_DRIVER_TYPE_WARP, //WARP shouldn't work, but this was like this in the duplication sample, so eh
D3D_DRIVER_TYPE_REFERENCE,
};
UINT NumDriverTypes = ARRAYSIZE(DriverTypes);
// Feature levels supported
D3D_FEATURE_LEVEL FeatureLevels[] =
{
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_1
};
UINT NumFeatureLevels = ARRAYSIZE(FeatureLevels);
D3D_FEATURE_LEVEL FeatureLevel;
// Create device
if (adapter_ptr_preferred != nullptr) //Try preferred adapter first if we have one
{
hr = D3D11CreateDevice(adapter_ptr_preferred.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, FeatureLevels, NumFeatureLevels,
D3D11_SDK_VERSION, &m_Device, &FeatureLevel, &m_DeviceContext);
if (FAILED(hr))
{
adapter_ptr_preferred = nullptr;
}
}
if (adapter_ptr_preferred == nullptr)
{
LOG_F(WARNING, "Output enumeration did not return a preferred GPU. Trying first available device...");
for (UINT DriverTypeIndex = 0; DriverTypeIndex < NumDriverTypes; ++DriverTypeIndex)
{
hr = D3D11CreateDevice(nullptr, DriverTypes[DriverTypeIndex], nullptr, 0, FeatureLevels, NumFeatureLevels,
D3D11_SDK_VERSION, &m_Device, &FeatureLevel, &m_DeviceContext);
if (SUCCEEDED(hr))
{
//Device creation succeeded. Take adapter from the device so it can be used further down
adapter_ptr_preferred.Attach(GetDXGIAdapter());
break;
}
}
}
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Device creation failed", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
//Create multi-gpu target device if needed
if (adapter_ptr_vr != nullptr)
{
hr = D3D11CreateDevice(adapter_ptr_vr.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, FeatureLevels, NumFeatureLevels,
D3D11_SDK_VERSION, &m_MultiGPUTargetDevice, &FeatureLevel, &m_MultiGPUTargetDeviceContext);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Secondary device creation failed", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
adapter_ptr_vr = nullptr;
LOG_F(INFO, "Using cross-GPU copy");
}
//Check Desktop Duplication HDR support
m_OutputHDRAvailable = false;
#ifndef DPLUS_DUP_NO_HDR
if (adapter_ptr_preferred != nullptr)
{
Microsoft::WRL::ComPtr DxgiOutput;
hr = adapter_ptr_preferred->EnumOutputs(0, &DxgiOutput);
if (SUCCEEDED(hr))
{
Microsoft::WRL::ComPtr DxgiOutput5;
hr = DxgiOutput.As(&DxgiOutput5);
m_OutputHDRAvailable = SUCCEEDED(hr);
}
}
if (m_OutputHDRAvailable)
{
LOG_F(INFO, "Desktop Duplication HDR mirroring is supported");
}
else
{
LOG_F(INFO, "Desktop Duplication HDR mirroring is not supported");
}
#else
LOG_F(INFO, "Desktop+ was not built with Desktop Duplication HDR support");
#endif
// Create shared texture
DUPL_RETURN Return = CreateTextures(SingleOutput, OutCount, DeskBounds);
if (Return != DUPL_RETURN_SUCCESS)
{
return Return;
}
// Make new render target view
Return = MakeRTV();
if (Return != DUPL_RETURN_SUCCESS)
{
return Return;
}
// Set view port
D3D11_VIEWPORT VP;
VP.Width = static_cast(m_DesktopWidth);
VP.Height = static_cast(m_DesktopHeight);
VP.MinDepth = 0.0f;
VP.MaxDepth = 1.0f;
VP.TopLeftX = 0;
VP.TopLeftY = 0;
m_DeviceContext->RSSetViewports(1, &VP);
// Create the sample state
D3D11_SAMPLER_DESC SampDesc;
RtlZeroMemory(&SampDesc, sizeof(SampDesc));
SampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
SampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
SampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
SampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
SampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
SampDesc.MinLOD = 0;
SampDesc.MaxLOD = D3D11_FLOAT32_MAX;
hr = m_Device->CreateSamplerState(&SampDesc, &m_Sampler);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create sampler state", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
// Create the blend state
D3D11_BLEND_DESC BlendStateDesc;
BlendStateDesc.AlphaToCoverageEnable = FALSE;
BlendStateDesc.IndependentBlendEnable = FALSE;
BlendStateDesc.RenderTarget[0].BlendEnable = TRUE;
BlendStateDesc.RenderTarget[0].SrcBlend = D3D11_BLEND_SRC_ALPHA;
BlendStateDesc.RenderTarget[0].DestBlend = D3D11_BLEND_INV_SRC_ALPHA;
BlendStateDesc.RenderTarget[0].BlendOp = D3D11_BLEND_OP_ADD;
BlendStateDesc.RenderTarget[0].SrcBlendAlpha = D3D11_BLEND_ONE;
BlendStateDesc.RenderTarget[0].DestBlendAlpha = D3D11_BLEND_ZERO;
BlendStateDesc.RenderTarget[0].BlendOpAlpha = D3D11_BLEND_OP_ADD;
BlendStateDesc.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_RED | D3D11_COLOR_WRITE_ENABLE_GREEN | D3D11_COLOR_WRITE_ENABLE_BLUE;
hr = m_Device->CreateBlendState(&BlendStateDesc, &m_BlendState);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create blend state", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
//Create the rasterizer state
D3D11_RASTERIZER_DESC RasterizerDesc;
RtlZeroMemory(&RasterizerDesc, sizeof(RasterizerDesc));
RasterizerDesc.FillMode = D3D11_FILL_SOLID;
RasterizerDesc.CullMode = D3D11_CULL_BACK;
RasterizerDesc.ScissorEnable = true;
hr = m_Device->CreateRasterizerState(&RasterizerDesc, &m_RasterizerState);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create rasterizer state", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
m_DeviceContext->RSSetState(m_RasterizerState);
//Create vertex buffer for drawing whole texture
VERTEX Vertices[NUMVERTICES] =
{
{ XMFLOAT3(-1.0f, -1.0f, 0.0f), XMFLOAT2(0.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, 0.0f), XMFLOAT2(0.0f, 0.0f) },
{ XMFLOAT3( 1.0f, -1.0f, 0.0f), XMFLOAT2(1.0f, 1.0f) },
{ XMFLOAT3( 1.0f, -1.0f, 0.0f), XMFLOAT2(1.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, 0.0f), XMFLOAT2(0.0f, 0.0f) },
{ XMFLOAT3( 1.0f, 1.0f, 0.0f), XMFLOAT2(1.0f, 0.0f) },
};
D3D11_BUFFER_DESC BufferDesc;
RtlZeroMemory(&BufferDesc, sizeof(BufferDesc));
BufferDesc.Usage = D3D11_USAGE_DEFAULT;
BufferDesc.ByteWidth = sizeof(VERTEX) * NUMVERTICES;
BufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
BufferDesc.CPUAccessFlags = 0;
D3D11_SUBRESOURCE_DATA InitData;
RtlZeroMemory(&InitData, sizeof(InitData));
InitData.pSysMem = Vertices;
// Create vertex buffer
hr = m_Device->CreateBuffer(&BufferDesc, &InitData, &m_VertexBuffer);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create vertex buffer", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
//Set scissor rect to full
const D3D11_RECT rect_scissor_full = { 0, 0, m_DesktopWidth, m_DesktopHeight };
m_DeviceContext->RSSetScissorRects(1, &rect_scissor_full);
// Initialize shaders
Return = InitShaders();
if (Return != DUPL_RETURN_SUCCESS)
{
return Return;
}
//In case this was called due to a resolution change, check if the crop was just exactly the set desktop in each overlay and adapt then
if (!ConfigManager::GetValue(configid_bool_performance_single_desktop_mirroring))
{
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication)
{
int desktop_id = data.ConfigInt[configid_int_overlay_desktop_id];
if ((desktop_id >= 0) && (desktop_id < desktop_rects_prev.size()) && (desktop_id < m_DesktopRects.size()))
{
int crop_x = data.ConfigInt[configid_int_overlay_crop_x];
int crop_y = data.ConfigInt[configid_int_overlay_crop_y];
int crop_width = data.ConfigInt[configid_int_overlay_crop_width];
int crop_height = data.ConfigInt[configid_int_overlay_crop_height];
DPRect crop_rect(crop_x, crop_y, crop_x + crop_width, crop_y + crop_height);
DPRect desktop_rect = desktop_rects_prev[desktop_id];
desktop_rect.Translate({-desktop_x_prev, -desktop_y_prev});
if (crop_rect == desktop_rect)
{
OverlayManager::Get().SetCurrentOverlayID(i);
CropToDisplay(desktop_id, true);
}
}
}
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
ResetOverlays();
//On some systems, the Desktop Duplication output is translucent for some reason
//We check the texture's pixels for the first few frame updates to make sure we can use the straight copy path, which should be the case on most machines
//If it fails we use a pixel shader to fix the alpha channel during frame updates
m_OutputAlphaCheckFailed = false;
m_OutputAlphaChecksPending = 10;
return Return;
}
std::tuple OutputManager::InitOverlay()
{
vr::EVRInitError init_error = vr::VRInitError_None;
vr::VROverlayError ovrl_error = vr::VROverlayError_None;
vr::VR_Init(&init_error, vr::VRApplication_Overlay);
if (init_error != vr::VRInitError_None)
return {init_error, ovrl_error, false};
if (!vr::VROverlay())
return {vr::VRInitError_Init_InvalidInterface, ovrl_error, false};
DPLog_SteamVR_SystemInfo();
m_OvrlHandleDashboardDummy = vr::k_ulOverlayHandleInvalid;
m_OvrlHandleIcon = vr::k_ulOverlayHandleInvalid;
m_OvrlHandleDesktopTexture = vr::k_ulOverlayHandleInvalid;
//We already got rid of another instance of this app if there was any, but this loop takes care of it too if the detection failed or something uses our overlay key
for (int tries = 0; tries < 10; ++tries)
{
//Create dashboard dummy overlay. It's only used to get a button, transform origin and position the top bar in the dashboard
ovrl_error = vr::VROverlay()->CreateDashboardOverlay("elvissteinjr.DesktopPlusDashboard", "Desktop+", &m_OvrlHandleDashboardDummy, &m_OvrlHandleIcon);
if (ovrl_error == vr::VROverlayError_KeyInUse) //If the key is already in use, kill the owning process (hopefully another instance of this app)
{
ovrl_error = vr::VROverlay()->FindOverlay("elvissteinjr.DesktopPlusDashboard", &m_OvrlHandleDashboardDummy);
if ((ovrl_error == vr::VROverlayError_None) && (m_OvrlHandleDashboardDummy != vr::k_ulOverlayHandleInvalid))
{
LOG_F(INFO, "Overlay key already in use, killing owning process...");
uint32_t pid = vr::VROverlay()->GetOverlayRenderingPid(m_OvrlHandleDashboardDummy);
HANDLE phandle = ::OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, TRUE, pid);
if (phandle != nullptr)
{
::TerminateProcess(phandle, 0);
::CloseHandle(phandle);
}
else
{
ovrl_error = vr::VROverlayError_KeyInUse;
}
}
else
{
ovrl_error = vr::VROverlayError_KeyInUse;
}
}
else
{
break;
}
//Try again in a bit to check if it's just a race with some external cleanup
::Sleep(200);
}
if (m_OvrlHandleDashboardDummy != vr::k_ulOverlayHandleInvalid)
{
//Create desktop texture overlay. This overlay holds the desktop texture shared between Desktop Duplication Overlays
ovrl_error = vr::VROverlay()->CreateOverlay("elvissteinjr.DesktopPlusDesktopTexture", "Desktop+", &m_OvrlHandleDesktopTexture);
//Load config again to properly initialize overlays that were loaded before OpenVR was available
const bool loaded_overlay_profile = ConfigManager::Get().GetAppProfileManager().ActivateProfileForCurrentSceneApp(); //Check if overlays from app profile need to be loaded first
if (!loaded_overlay_profile)
{
ConfigManager::Get().LoadConfigFromFile();
}
if (m_OvrlHandleDashboardDummy != vr::k_ulOverlayHandleInvalid)
{
unsigned char bytes[2 * 2 * 4] = {0}; //2x2 transparent RGBA
//Set dashboard dummy content instead of leaving it totally blank, which is undefined
vr::VROverlay()->SetOverlayRaw(m_OvrlHandleDashboardDummy, bytes, 2, 2, 4);
vr::VROverlay()->SetOverlayInputMethod(m_OvrlHandleDashboardDummy, vr::VROverlayInputMethod_None);
vr::VROverlay()->SetOverlayWidthInMeters(m_OvrlHandleDashboardDummy, 1.5f);
vr::VROverlay()->SetOverlayFlag(m_OvrlHandleDashboardDummy, vr::VROverlayFlags_MinimalControlBar, true);
//ResetOverlays() is called later
//Use different icon if GamepadUI (SteamVR 2 dashboard) exists
vr::VROverlayHandle_t handle_gamepad_ui = vr::k_ulOverlayHandleInvalid;
vr::VROverlay()->FindOverlay("valve.steam.gamepadui.bar", &handle_gamepad_ui);
const char* icon_file = (handle_gamepad_ui != vr::k_ulOverlayHandleInvalid) ? "images/icon_dashboard_gamepadui.png" : "images/icon_dashboard.png";
vr::VROverlay()->SetOverlayFromFile(m_OvrlHandleIcon, (ConfigManager::Get().GetApplicationPath() + icon_file).c_str());
}
}
m_MaxActiveRefreshDelay = 1000.0f / GetHMDFrameRate();
//Check if this process was launched by Steam by checking if the "SteamClientLaunch" environment variable exists
bool is_steam_app = (::GetEnvironmentVariable(L"SteamClientLaunch", nullptr, 0) != 0);
ConfigManager::SetValue(configid_bool_state_misc_process_started_by_steam, is_steam_app);
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_misc_process_started_by_steam, is_steam_app);
//Add application manifest and set app key to Steam one if needed (setting the app key will make it load Steam input bindings even when not launched by it)
if (!is_steam_app)
{
LOG_F(INFO, "Process was not launched by Steam, setting SteamVR application identity");
vr::VRApplications()->IdentifyApplication(::GetCurrentProcessId(), g_AppKeyDashboardApp);
m_IsFirstLaunch = (!vr::VRApplications()->IsApplicationInstalled(g_AppKeyDashboardApp));
}
//Even if not first launch, make sure the Theater Screen dummy app is installed
//The Theater Screen needs a separate app entry as on Steam we don't control the contents of the app manifest for our appid, yet need to have "starts_theater_mode" in it somewhere
if ((m_IsFirstLaunch) || (!vr::VRApplications()->IsApplicationInstalled(g_AppKeyTheaterScreen)))
{
vr::VRApplications()->AddApplicationManifest((ConfigManager::Get().GetApplicationPath() + "manifest.vrmanifest").c_str());
}
//Set application auto-launch to true if it's the first launch
if (m_IsFirstLaunch)
{
LOG_F(INFO, "First launch detected. Setting application to auto-launch with SteamVR");
vr::EVRApplicationError app_error = vr::VRApplications()->SetApplicationAutoLaunch(g_AppKeyDashboardApp, true);
if (app_error == vr::VRApplicationError_None)
{
//Have the UI app display the initial setup notification
IPCManager::Get().PostMessageToUIApp(ipcmsg_action, ipcact_notification_show);
//Show the dashboard overlay as well to make it easier to find when first using the app
vr::VROverlay()->ShowDashboard("elvissteinjr.DesktopPlusDashboard");
}
}
const bool vrinput_init_success = m_VRInput.Init();
//Check if it's a WMR system and set up for that if needed
ConfigManager::Get().InitConfigForWMR();
DPWinRT_SetDesktopEnumerationFlags( (ConfigManager::GetValue(configid_int_interface_wmr_ignore_vscreens) == 1) );
LOG_IF_F(INFO, (ConfigManager::GetValue(configid_int_interface_wmr_ignore_vscreens) == 1), "WMR headset detected, ignoring additional virtual displays");
//Set pen simulation support state so the UI can act on it
ConfigManager::SetValue(configid_bool_state_pen_simulation_supported, m_InputSim.IsPenSimulationSupported());
LOG_F(INFO, "Pen input simulation is %s", (m_InputSim.IsPenSimulationSupported()) ? "supported" : "not supported");
//Init background overlay if needed
m_BackgroundOverlay.Update();
//Hotkeys can trigger actions requiring OpenVR, so only register after OpenVR init
RegisterHotkeys();
//Try to get dashboard in proper state if needed
FixInvalidDashboardLaunchState();
//Return error state to allow for accurate display if needed
return {vr::VRInitError_None, ovrl_error, vrinput_init_success};
}
//
// Update Overlay and handle events
//
DUPL_RETURN_UPD OutputManager::Update(_In_ PTR_INFO* PointerInfo, _In_ DPRect& DirtyRectTotal, bool NewFrame, bool SkipFrame)
{
if (HandleOpenVREvents()) //If quit event received, quit.
{
return DUPL_RETURN_UPD_QUIT;
}
UINT64 sync_key = 1; //Key used by duplication threads to lock for this function (duplication threads lock with 1, Update() with 0 and unlock vice versa)
//If we previously skipped a frame, we want to actually process a new one at the next valid opportunity
if ( (m_OutputPendingSkippedFrame) && (!SkipFrame) )
{
//If there isn't new frame yet, we have to unlock the keyed mutex with the one we locked it with ourselves before
//However, if the laser pointer was used since the last update, we simply use the key for new frame data to wait for the new mouse position or frame
//Not waiting for it reduces latency usually, but laser pointer mouse movements are weirdly not picked up without doing this or enabling the rapid laser pointer update setting
if ( (!NewFrame) && (!m_MouseLaserPointerUsedLastUpdate) )
{
sync_key = 0;
}
NewFrame = true; //Treat this as a new frame now
m_MouseLaserPointerUsedLastUpdate = false;
}
//If frame skipped and no new frame, do nothing (if there's a new frame, we have to at least re-lock the keyed mutex so the duplication threads can access it again)
if ( (SkipFrame) && (!NewFrame) )
{
m_OutputPendingSkippedFrame = true; //Process the frame next time we can
return DUPL_RETURN_UPD_SUCCESS;
}
//When invalid output is set, key mutex can be null, so just do nothing
if (m_KeyMutex == nullptr)
{
return DUPL_RETURN_UPD_SUCCESS;
}
// Try and acquire sync on common display buffer (needed to safely access the PointerInfo)
HRESULT hr = m_KeyMutex->AcquireSync(sync_key, GetMaxRefreshDelay());
if (hr == static_cast(WAIT_TIMEOUT))
{
// Another thread has the keyed mutex so try again later
return DUPL_RETURN_UPD_RETRY;
}
else if (FAILED(hr))
{
return (DUPL_RETURN_UPD)ProcessFailure(m_Device, L"Failed to acquire keyed mutex", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
DUPL_RETURN_UPD ret = DUPL_RETURN_UPD_SUCCESS;
//If alternative cursor rendering is enabled, try to get software cursor data and use that as PointerInfo instead
if (ConfigManager::GetValue(configid_bool_performance_alternative_cursor_rendering))
{
m_MouseAlternativeCursor.SynthesizeDDPCursorInfo();
PointerInfo = &m_MouseAlternativeCursor.GetDDPCursorInfo();
}
//Got mutex, so we can access pointer info and shared surface
DPRect mouse_rect = {PointerInfo->Position.x, PointerInfo->Position.y, int(PointerInfo->Position.x + PointerInfo->ShapeInfo.Width),
int(PointerInfo->Position.y + PointerInfo->ShapeInfo.Height)};
//If mouse state got updated, expand dirty rect to include old and new cursor regions
if ( (ConfigManager::GetValue(configid_bool_input_mouse_render_cursor)) && (m_MouseLastInfo.LastTimeStamp.QuadPart < PointerInfo->LastTimeStamp.QuadPart) )
{
//Only invalidate if position or shape changed, otherwise it would be a visually identical result
if ( (m_MouseLastInfo.Position.x != PointerInfo->Position.x) || (m_MouseLastInfo.Position.y != PointerInfo->Position.y) ||
(PointerInfo->CursorShapeChanged) || (m_MouseCursorNeedsUpdate) || (m_MouseLastInfo.Visible != PointerInfo->Visible) )
{
if ( (PointerInfo->Visible) )
{
(DirtyRectTotal.GetTL().x == -1) ? DirtyRectTotal = mouse_rect : DirtyRectTotal.Add(mouse_rect);
}
if (m_MouseLastInfo.Visible)
{
DPRect mouse_rect_last(m_MouseLastInfo.Position.x, m_MouseLastInfo.Position.y, int(m_MouseLastInfo.Position.x + m_MouseLastInfo.ShapeInfo.Width),
int(m_MouseLastInfo.Position.y + m_MouseLastInfo.ShapeInfo.Height));
(DirtyRectTotal.GetTL().x == -1) ? DirtyRectTotal = mouse_rect_last : DirtyRectTotal.Add(mouse_rect_last);
}
}
}
//If frame is skipped, skip all GPU work
if (SkipFrame)
{
//Collect dirty rects for the next time we render
(m_OutputPendingDirtyRect.GetTL().x == -1) ? m_OutputPendingDirtyRect = DirtyRectTotal : m_OutputPendingDirtyRect.Add(DirtyRectTotal);
//Remember if the cursor changed so it's updated the next time we actually render it
if (PointerInfo->CursorShapeChanged)
{
m_MouseCursorNeedsUpdate = true;
}
m_OutputPendingSkippedFrame = true;
hr = m_KeyMutex->ReleaseSync(0);
return DUPL_RETURN_UPD_SUCCESS;
}
else if (m_OutputPendingDirtyRect.GetTL().x != -1) //Add previously collected dirty rects if there are any
{
(DirtyRectTotal.GetTL().x == -1) ? DirtyRectTotal = m_OutputPendingDirtyRect : DirtyRectTotal.Add(m_OutputPendingDirtyRect);
}
bool has_updated_overlay = false;
//Check all overlays for overlap and collect clipping region from matches
DPRect clipping_region(-1, -1, -1, -1);
if (!m_OutputPendingFullRefresh)
{
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const Overlay& overlay = OverlayManager::Get().GetOverlay(i);
if ( (overlay.IsVisible()) && ( (overlay.GetTextureSource() == ovrl_texsource_desktop_duplication) || (overlay.GetTextureSource() == ovrl_texsource_desktop_duplication_3dou_converted) ) )
{
const DPRect& cropping_region = overlay.GetValidatedCropRect();
if (DirtyRectTotal.Overlaps(cropping_region))
{
if (clipping_region.GetTL().x != -1)
{
clipping_region.Add(cropping_region);
}
else
{
clipping_region = cropping_region;
}
}
}
}
DirtyRectTotal.ClipWithFull(clipping_region);
}
else //Set dirty & clipping rect to total surface for full refresh
{
DirtyRectTotal = {0, 0, m_DesktopWidth, m_DesktopHeight};
clipping_region = DirtyRectTotal;
m_OutputPendingFullRefresh = false;
}
m_OutputLastClippingRect = clipping_region;
if (clipping_region.GetTL().x != -1) //Overlapped with at least one overlay
{
//Set scissor rect for overlay drawing function
const D3D11_RECT rect_scissor = { DirtyRectTotal.GetTL().x, DirtyRectTotal.GetTL().y, DirtyRectTotal.GetBR().x, DirtyRectTotal.GetBR().y };
m_DeviceContext->RSSetScissorRects(1, &rect_scissor);
//Draw shared surface to overlay texture to avoid trouble with transparency on some systems
bool is_full_texture = DirtyRectTotal.Contains({0, 0, m_DesktopWidth, m_DesktopHeight});
DrawFrameToOverlayTex(is_full_texture);
//Only handle cursor if it's in cropping region
if (mouse_rect.Overlaps(DirtyRectTotal))
{
DrawMouseToOverlayTex(PointerInfo);
}
else if (PointerInfo->CursorShapeChanged) //But remember if the cursor changed for next time
{
m_MouseCursorNeedsUpdate = true;
}
//Set Overlay texture
ret = RefreshOpenVROverlayTexture(DirtyRectTotal);
//Reset scissor rect
const D3D11_RECT rect_scissor_full = { 0, 0, m_DesktopWidth, m_DesktopHeight };
m_DeviceContext->RSSetScissorRects(1, &rect_scissor_full);
has_updated_overlay = (ret == DUPL_RETURN_UPD_SUCCESS_REFRESHED_OVERLAY);
}
else if (PointerInfo->CursorShapeChanged) //But remember if the cursor changed for next time
{
m_MouseCursorNeedsUpdate = true;
}
//Set cached mouse values (we don't want to copy the shape buffer, so no straight assignment)
m_MouseLastInfo.ShapeInfo = PointerInfo->ShapeInfo;
m_MouseLastInfo.Position = PointerInfo->Position;
m_MouseLastInfo.Visible = PointerInfo->Visible;
m_MouseLastInfo.WhoUpdatedPositionLast = PointerInfo->WhoUpdatedPositionLast;
m_MouseLastInfo.LastTimeStamp = PointerInfo->LastTimeStamp;
m_MouseLastInfo.CursorShapeChanged = PointerInfo->CursorShapeChanged;
//Reset dirty rect
DirtyRectTotal = DPRect(-1, -1, -1, -1);
// Release keyed mutex
hr = m_KeyMutex->ReleaseSync(0);
if (FAILED(hr))
{
return (DUPL_RETURN_UPD)ProcessFailure(m_Device, L"Failed to Release keyed mutex", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
//Count frames
if (has_updated_overlay)
{
m_PerformanceFrameCount++;
}
m_OutputPendingSkippedFrame = false;
m_OutputPendingDirtyRect = {-1, -1, -1, -1};
return ret;
}
void OutputManager::BusyUpdate()
{
//Improve responsiveness of temp drag during overlay creation when applying capture source can take a bit longer (i.e. browser overlays)
if ( (m_OverlayDragger.IsDragActive()) && (ConfigManager::GetValue(configid_bool_state_overlay_dragmode_temp)) )
{
m_OverlayDragger.DragUpdate();
}
}
bool OutputManager::HandleIPCMessage(const MSG& msg)
{
//Handle messages sent by browser process in the APIClient
if (msg.message == DPBrowserAPIClient::Get().GetRegisteredMessageID())
{
DPBrowserAPIClient::Get().HandleIPCMessage(msg);
return false;
}
//Apply overlay id override if needed
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
int overlay_override_id = ConfigManager::GetValue(configid_int_state_overlay_current_id_override);
if (overlay_override_id != -1)
{
OverlayManager::Get().SetCurrentOverlayID(overlay_override_id);
}
//Config strings come as WM_COPYDATA
if (msg.message == WM_COPYDATA)
{
COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)msg.lParam;
//Arbitrary size limit to prevent some malicous applications from sending bad data, especially when this is running elevated
if ( (pcds->dwData < configid_str_MAX) && (pcds->cbData <= 4096) )
{
std::string copystr((char*)pcds->lpData, pcds->cbData); //We rely on the data length. The data is sent without the NUL byte
ConfigID_String str_id = (ConfigID_String)pcds->dwData;
ConfigManager::SetValue(str_id, copystr);
switch (str_id)
{
case configid_str_state_keyboard_string:
{
m_InputSim.KeyboardText(copystr.c_str());
break;
}
case configid_str_state_app_profile_data:
{
const std::string& app_key = ConfigManager::GetValue(configid_str_state_app_profile_key);
if (!app_key.empty())
{
AppProfile new_profile;
new_profile.Deserialize(copystr);
const bool loaded_overlay_profile = ConfigManager::Get().GetAppProfileManager().StoreProfile(app_key, new_profile);
if (loaded_overlay_profile)
ResetOverlays();
}
break;
}
case configid_str_state_action_data:
{
Action new_action;
new_action.Deserialize(copystr);
ConfigManager::Get().GetActionManager().StoreAction(new_action);
break;
}
default: break;
}
}
//Restore overlay id override
if (overlay_override_id != -1)
{
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
return false;
}
bool reset_mirroring = false;
IPCMsgID msgid = IPCManager::Get().GetIPCMessageID(msg.message);
switch (msgid)
{
case ipcmsg_action:
{
switch (msg.wParam)
{
case ipcact_mirror_reset:
{
reset_mirroring = true;
break;
}
case ipcact_overlay_position_reset:
{
DetachedTransformReset();
break;
}
case ipcact_overlay_position_adjust:
{
DetachedTransformAdjust(msg.lParam);
break;
}
case ipcact_action_delete:
{
ConfigManager::Get().GetActionManager().RemoveAction(msg.lParam);
break;
}
case ipcact_action_do:
{
ConfigManager::Get().GetActionManager().DoAction(msg.lParam, (overlay_override_id != -1) ? (unsigned int)overlay_override_id : k_ulOverlayID_None);
break;
}
case ipcact_action_start:
{
ConfigManager::Get().GetActionManager().StartAction(msg.lParam, (overlay_override_id != -1) ? (unsigned int)overlay_override_id : k_ulOverlayID_None);
break;
}
case ipcact_action_stop:
{
ConfigManager::Get().GetActionManager().StopAction(msg.lParam, (overlay_override_id != -1) ? (unsigned int)overlay_override_id : k_ulOverlayID_None);
break;
}
case ipcact_keyboard_vkey:
case ipcact_keyboard_wchar:
{
HandleKeyboardMessage((IPCActionID)msg.wParam, msg.lParam);
break;
}
case ipcact_overlay_profile_load:
{
reset_mirroring = HandleOverlayProfileLoadMessage(msg.lParam);
break;
}
case ipcact_crop_to_active_window:
{
CropToActiveWindow();
break;
}
case ipcact_switch_task:
{
ShowWindowSwitcher();
break;
}
case ipcact_overlay_duplicate:
{
DuplicateOverlay((unsigned int)msg.lParam);
break;
}
case ipcact_overlay_new:
{
int desktop_id = msg.lParam;
OverlayCaptureSource capsource;
switch (desktop_id)
{
case -2: capsource = ovrl_capsource_winrt_capture; break;
case -3: capsource = ovrl_capsource_ui; break;
case -4: capsource = ovrl_capsource_browser; break;
default: capsource = ovrl_capsource_desktop_duplication;
}
AddOverlay(capsource, desktop_id, (HWND)ConfigManager::GetValue(configid_handle_state_arg_hwnd));
break;
}
case ipcact_overlay_new_drag:
{
int desktop_id = GET_X_LPARAM(msg.lParam);
float pointer_distance = GET_Y_LPARAM(msg.lParam) / 100.0f;
OverlayCaptureSource capsource;
switch (desktop_id)
{
case -2: capsource = ovrl_capsource_winrt_capture; break;
case -3: capsource = ovrl_capsource_ui; break;
case -4: capsource = ovrl_capsource_browser; break;
default: capsource = ovrl_capsource_desktop_duplication;
}
AddOverlayDrag(pointer_distance, capsource, desktop_id, (HWND)ConfigManager::GetValue(configid_handle_state_arg_hwnd));
break;
}
case ipcact_overlay_remove:
{
OverlayManager::Get().RemoveOverlay((unsigned int)msg.lParam);
//RemoveOverlay() may have changed active ID, keep in sync
ConfigManager::SetValue(configid_int_interface_overlay_current_id, OverlayManager::Get().GetCurrentOverlayID());
break;
}
case ipcact_overlay_transform_sync:
{
DetachedTransformSyncAll();
break;
}
case ipcact_overlay_swap:
{
OverlayManager::Get().SwapOverlays(OverlayManager::Get().GetCurrentOverlayID(), (unsigned int)msg.lParam);
break;
}
case ipcact_overlay_gaze_fade_auto:
{
DetachedOverlayGazeFadeAutoConfigure();
break;
}
case ipcact_overlay_make_standalone:
{
OverlayManager::Get().ConvertDuplicatedOverlayToStandalone((unsigned int)msg.lParam);
break;
}
case ipcact_browser_navigate_to_url:
{
unsigned int overlay_id = (unsigned int)msg.lParam;
DPBrowserAPIClient::Get().DPBrowser_SetURL(OverlayManager::Get().GetOverlay(overlay_id).GetHandle(),
OverlayManager::Get().GetConfigData(overlay_id).ConfigStr[configid_str_overlay_browser_url]);
break;
}
case ipcact_browser_recreate_context:
{
unsigned int overlay_id = (unsigned int)msg.lParam;
DPBrowserAPIClient::Get().DPBrowser_RecreateBrowser(OverlayManager::Get().GetOverlay(overlay_id).GetHandle(),
OverlayManager::Get().GetConfigData(overlay_id).ConfigBool[configid_bool_overlay_browser_allow_transparency]);
break;
}
case ipcact_winmanager_drag_start:
{
unsigned int overlay_id = (unsigned int)msg.lParam;
const bool use_pen = ConfigManager::GetValue(configid_bool_input_mouse_simulate_pen_input);
if (m_OverlayDragger.GetDragDeviceID() == -1)
{
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(overlay_id);
//Check if it's still being hovered since it could be off before the message is processed
if (ConfigManager::Get().IsLaserPointerTargetOverlay(OverlayManager::Get().GetCurrentOverlay().GetHandle(), true))
{
//Reset input and WindowManager state manually since the overlay mouse up even will be consumed to finish the drag later
(use_pen) ? m_InputSim.PenSetPrimaryDown(false) : m_InputSim.MouseSetLeftDown(false);
WindowManager::Get().SetTargetWindow(nullptr);
if (ConfigManager::GetValue(configid_int_overlay_origin) != ovrl_origin_theater_screen)
{
if (!ConfigManager::GetValue(configid_bool_overlay_transform_locked))
{
m_OverlayDragger.DragStart(overlay_id);
}
else
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_device, ConfigManager::Get().GetPrimaryLaserPointerDevice());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 1);
}
}
else
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_device, ConfigManager::Get().GetPrimaryLaserPointerDevice());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 2);
}
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
else if (overlay_id == k_ulOverlayID_None) //Means it came from a blocked drag, reset input and WindowManager state
{
(use_pen) ? m_InputSim.PenSetPrimaryDown(false) : m_InputSim.MouseSetLeftDown(false);
WindowManager::Get().SetTargetWindow(nullptr);
}
break;
}
case ipcact_winmanager_winlist_add:
case ipcact_winmanager_winlist_update:
{
const WindowInfo* window_info = nullptr;
bool has_title_changed = true;
if (msg.wParam == ipcact_winmanager_winlist_add)
window_info = &WindowManager::Get().WindowListAdd((HWND)msg.lParam);
else
window_info = WindowManager::Get().WindowListUpdateTitle((HWND)msg.lParam, &has_title_changed);
//Find inactive overlays using the window and start capture for them
if ( (window_info != nullptr) && (has_title_changed) ) //Only do this when the title changed
{
for (auto& i : OverlayManager::Get().FindInactiveOverlaysForWindow(*window_info))
{
OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] = msg.lParam;
if (ConfigManager::GetValue(configid_int_windows_winrt_capture_lost_behavior) == window_caplost_hide_overlay)
data.ConfigBool[configid_bool_overlay_enabled] = true;
OnSetOverlayWinRTCaptureWindow(i);
//Send to UI
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, (int)i);
IPCManager::Get().PostConfigMessageToUIApp(configid_handle_overlay_state_winrt_hwnd, msg.lParam);
if (ConfigManager::GetValue(configid_int_windows_winrt_capture_lost_behavior) == window_caplost_hide_overlay)
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_overlay_enabled, true);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
}
}
break;
}
case ipcact_winmanager_winlist_remove:
{
std::wstring last_title_w = WindowManager::Get().WindowListRemove((HWND)msg.lParam);
std::string last_title = StringConvertFromUTF16(last_title_w.c_str());
//Some windows clear their title entirely before ceasing to exist, skip those
if (last_title.empty())
break;
//Set last known title for overlays that captured this window
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] == msg.lParam)
{
data.ConfigStr[configid_str_overlay_winrt_last_window_title] = last_title;
}
}
break;
}
case ipcact_winmanager_text_input_focus:
{
WindowManager::Get().UpdateTextInputFocusedState(msg.lParam);
break;
}
case ipcact_sync_config_state:
{
//Overlay state
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const Overlay& overlay = OverlayManager::Get().GetOverlay(i);
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, (int)i);
IPCManager::Get().PostConfigMessageToUIApp(configid_handle_overlay_state_overlay_handle, data.ConfigHandle[configid_handle_overlay_state_overlay_handle]);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_state_content_width, data.ConfigInt[configid_int_overlay_state_content_width]);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_state_content_height, data.ConfigInt[configid_int_overlay_state_content_height]);
//Send over current HWND if there's an active capture
if ( (overlay.GetTextureSource() == ovrl_texsource_winrt_capture) && (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0))
{
IPCManager::Get().PostConfigMessageToUIApp(configid_handle_overlay_state_winrt_hwnd, data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd]);
}
else if (overlay.GetTextureSource() == ovrl_texsource_browser) //Send browser nav state if it's an active browser overlay
{
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_overlay_state_browser_nav_can_go_back, data.ConfigBool[configid_bool_overlay_state_browser_nav_can_go_back]);
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_overlay_state_browser_nav_can_go_forward, data.ConfigBool[configid_bool_overlay_state_browser_nav_can_go_forward]);
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_overlay_state_browser_nav_is_loading, data.ConfigBool[configid_bool_overlay_state_browser_nav_is_loading]);
}
IPCManager::Get().PostConfigMessageToUIApp(configid_float_overlay_state_brightness_extra_multiplier, data.ConfigFloat[configid_float_overlay_state_brightness_extra_multiplier]);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
}
//Global config state
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_interface_desktop_count, ConfigManager::GetValue(configid_int_state_interface_desktop_count));
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_overlay_dragmode, ConfigManager::GetValue(configid_bool_state_overlay_dragmode));
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_overlay_selectmode, ConfigManager::GetValue(configid_bool_state_overlay_selectmode));
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_overlay_dragselectmode_show_hidden, ConfigManager::GetValue(configid_bool_state_overlay_dragselectmode_show_hidden));
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_overlay_dragmode_temp, ConfigManager::GetValue(configid_bool_state_overlay_dragmode_temp));
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_pen_simulation_supported, ConfigManager::GetValue(configid_bool_state_pen_simulation_supported));
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_window_focused_process_elevated, ConfigManager::GetValue(configid_bool_state_window_focused_process_elevated));
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_misc_process_elevated, ConfigManager::GetValue(configid_bool_state_misc_process_elevated));
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_misc_process_started_by_steam, ConfigManager::GetValue(configid_bool_state_misc_process_started_by_steam));
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_dplus_laser_pointer_device, ConfigManager::GetValue(configid_int_state_dplus_laser_pointer_device));
IPCManager::Get().PostConfigMessageToUIApp(configid_handle_state_dplus_laser_pointer_target_overlay, ConfigManager::GetValue(configid_handle_state_dplus_laser_pointer_target_overlay));
IPCManager::Get().PostConfigMessageToUIApp(configid_handle_state_theater_orig_overlay_handle, ConfigManager::GetValue(configid_handle_state_theater_orig_overlay_handle));
//Sync usually means new UI process, so get new handles
m_LaserPointer.RefreshCachedOverlayHandles();
break;
}
case ipcact_focus_window:
{
WindowManager::Get().RaiseAndFocusWindow((HWND)msg.lParam, &m_InputSim);
break;
}
case ipcact_keyboard_ovrl_focus_enter:
{
//If a WinRT window capture is the focused overlay, check for window auto-focus so it's possible to type things
if (ConfigManager::GetValue(configid_bool_windows_winrt_auto_focus))
{
const bool drag_or_select_mode_enabled = ( (ConfigManager::GetValue(configid_bool_state_overlay_dragmode)) || (ConfigManager::GetValue(configid_bool_state_overlay_selectmode)) );
if (!drag_or_select_mode_enabled)
{
int focused_overlay_id = ConfigManager::Get().GetValue(configid_int_state_overlay_focused_id);
if (focused_overlay_id != -1)
{
const Overlay& overlay = OverlayManager::Get().GetOverlay((unsigned int)focused_overlay_id);
const OverlayConfigData& data = OverlayManager::Get().GetConfigData((unsigned int)focused_overlay_id);
if ((overlay.GetTextureSource() == ovrl_texsource_winrt_capture) && (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0))
{
WindowManager::Get().RaiseAndFocusWindow((HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd], &m_InputSim);
}
}
}
}
break;
}
case ipcact_keyboard_ovrl_focus_leave:
{
//If leaving the keyboard while a WinRT window capture is the focused overlay and the option is enabled, focus the active scene app
if (ConfigManager::GetValue(configid_bool_windows_winrt_auto_focus_scene_app))
{
int focused_overlay_id = ConfigManager::Get().GetValue(configid_int_state_overlay_focused_id);
if (focused_overlay_id != -1)
{
const Overlay& overlay = OverlayManager::Get().GetOverlay((unsigned int)focused_overlay_id);
const OverlayConfigData& data = OverlayManager::Get().GetConfigData((unsigned int)focused_overlay_id);
if ((overlay.GetTextureSource() == ovrl_texsource_winrt_capture) && (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0))
{
WindowManager::Get().FocusActiveVRSceneApp(&m_InputSim);
}
}
}
break;
}
case ipcact_lpointer_trigger_haptics:
{
m_LaserPointer.TriggerLaserPointerHaptics((vr::TrackedDeviceIndex_t)msg.lParam);
break;
}
case ipcact_lpointer_ui_mask_rect:
{
if (msg.lParam == -1)
{
m_LaserPointer.UIIntersectionMaskFinish();
}
else
{
DPRect rect;
rect.Unpack16(msg.lParam);
m_LaserPointer.UIIntersectionMaskAddRect(rect);
}
break;
}
case ipcact_app_profile_remove:
{
const bool loaded_overlay_profile = ConfigManager::Get().GetAppProfileManager().RemoveProfile(ConfigManager::GetValue(configid_str_state_app_profile_key));
if (loaded_overlay_profile)
ResetOverlays();
break;
}
case ipcact_global_shortcut_set:
{
ActionManager::ActionList& global_shortcut_list = ConfigManager::Get().GetGlobalShortcuts();
int shortcut_id = msg.lParam;
if ((shortcut_id >= 0) && (shortcut_id < (int)global_shortcut_list.size()))
{
global_shortcut_list[shortcut_id] = ConfigManager::GetValue(configid_handle_state_action_uid);
}
break;
}
case ipcact_hotkey_set:
{
ConfigHotkeyList& hotkey_list = ConfigManager::Get().GetHotkeys();
const std::string& hotkey_data = ConfigManager::GetValue(configid_str_state_hotkey_data);
int hotkey_id = msg.lParam;
if ((hotkey_id >= 0) && (hotkey_id < (int)hotkey_list.size()))
{
if (!hotkey_data.empty())
{
hotkey_list[hotkey_id].Deserialize(hotkey_data);
}
else
{
hotkey_list.erase(hotkey_list.begin() + msg.lParam);
}
}
else if (!hotkey_data.empty())
{
ConfigHotkey hotkey_new;
hotkey_new.Deserialize(hotkey_data);
hotkey_list.push_back(hotkey_new);
}
RegisterHotkeys();
break;
}
}
break;
}
case ipcmsg_set_config:
{
if (msg.wParam < configid_bool_MAX)
{
ConfigID_Bool bool_id = (ConfigID_Bool)msg.wParam;
bool previous_value = ConfigManager::GetValue(bool_id);
ConfigManager::SetValue(bool_id, msg.lParam);
switch (bool_id)
{
case configid_bool_overlay_3D_enabled:
{
ApplySettingTransform();
ApplySetting3DMode();
break;
}
case configid_bool_overlay_3D_swapped:
{
ApplySetting3DMode();
break;
}
case configid_bool_overlay_origin_hmd_floor_use_turning:
{
const OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
OverlayOriginConfig origin_config = OverlayManager::Get().GetOriginConfigFromData(data);
OverlayOriginConfig origin_config_prev = origin_config;
origin_config_prev.HMDFloorUseTurning = previous_value;
OverlayOrigin origin = (OverlayOrigin)data.ConfigInt[configid_int_overlay_origin];
DetachedTransformConvertOrigin(OverlayManager::Get().GetCurrentOverlayID(), origin, origin, origin_config_prev, origin_config);
ApplySettingTransform();
break;
}
case configid_bool_overlay_enabled:
case configid_bool_overlay_show_backside:
case configid_bool_overlay_gazefade_enabled:
case configid_bool_overlay_update_invisible:
{
ApplySettingTransform();
break;
}
case configid_bool_overlay_crop_enabled:
{
ApplySettingCrop();
ApplySettingTransform();
ApplySettingMouseScale();
break;
}
case configid_bool_overlay_winrt_window_matching_strict:
{
OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
//Check if new matching setting finds an existing window
if ((OverlayManager::Get().GetCurrentOverlay().GetTextureSource() == ovrl_texsource_winrt_capture) && (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0))
break;
HWND window = WindowInfo::FindClosestWindowForTitle(data.ConfigStr[configid_str_overlay_winrt_last_window_title], data.ConfigStr[configid_str_overlay_winrt_last_window_class_name],
data.ConfigStr[configid_str_overlay_winrt_last_window_exe_name], WindowManager::Get().WindowListGet(),
data.ConfigBool[configid_bool_overlay_winrt_window_matching_strict]);
if (window != nullptr)
{
data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] = (uint64_t)window;
if (ConfigManager::GetValue(configid_int_windows_winrt_capture_lost_behavior) == window_caplost_hide_overlay)
{
data.ConfigBool[configid_bool_overlay_enabled] = true;
}
OnSetOverlayWinRTCaptureWindow(OverlayManager::Get().GetCurrentOverlayID());
//Send to UI
IPCManager::Get().PostConfigMessageToUIApp(configid_handle_overlay_state_winrt_hwnd, (LPARAM)window);
if (ConfigManager::GetValue(configid_int_windows_winrt_capture_lost_behavior) == window_caplost_hide_overlay)
{
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_overlay_enabled, true);
}
}
break;
}
case configid_bool_overlay_input_enabled:
case configid_bool_input_mouse_render_intersection_blob:
{
ApplySettingMouseInput();
break;
}
case configid_bool_input_mouse_allow_pointer_override:
{
//Reset Pointer override
if (m_MouseIgnoreMoveEvent)
{
m_MouseIgnoreMoveEvent = false;
ResetMouseLastLaserPointerPos();
ApplySettingMouseInput();
}
break;
}
case configid_bool_interface_dim_ui:
{
DimDashboard( ((m_OvrlDashboardActive) && (msg.lParam)) );
break;
}
case configid_bool_performance_single_desktop_mirroring:
{
if (msg.lParam) //Unify the desktop IDs when turning the setting on
{
CropToDisplay(OverlayManager::Get().GetConfigData(0).ConfigInt[configid_int_overlay_desktop_id], true);
}
reset_mirroring = true;
break;
}
case configid_bool_performance_hdr_mirroring:
{
reset_mirroring = true;
DPWinRT_SetHDREnabled(msg.lParam);
break;
}
case configid_bool_performance_alternative_cursor_rendering:
{
m_MouseCursorNeedsUpdate = true;
break;
}
case configid_bool_input_mouse_render_cursor:
{
m_OutputPendingFullRefresh = true;
if (DPWinRT_IsCaptureCursorEnabledPropertySupported())
DPWinRT_SetCaptureCursorEnabled(msg.lParam);
break;
}
case configid_bool_input_mouse_scroll_smooth:
{
ApplySettingMouseInput();
break;
}
case configid_bool_input_laser_pointer_block_input:
{
//Set SteamVR setting to allow global overlay input as we need it for this to work. Messing with user settings is not ideal, but we're not the first to do so
if ( (msg.lParam) && (!vr::VRSettings()->GetBool(vr::k_pch_SteamVR_Section, vr::k_pch_SteamVR_AllowGlobalActionSetPriority)) )
{
vr::VRSettings()->SetBool(vr::k_pch_SteamVR_Section, vr::k_pch_SteamVR_AllowGlobalActionSetPriority, true);
}
break;
}
case configid_bool_input_laser_pointer_hmd_device:
{
//Laser pointer keyboard input hotkeys should be disabled if this setting is off
RegisterHotkeys();
break;
}
case configid_bool_windows_winrt_keep_on_screen:
{
WindowManager::Get().UpdateConfigState();
break;
}
case configid_bool_browser_content_blocker:
{
DPBrowserAPIClient::Get().DPBrowser_ContentBlockSetEnabled(msg.lParam);
break;
}
case configid_bool_state_overlay_dragmode:
{
//Update temporary standing position if dragmode has been activated and dashboard tab isn't active
if ((msg.lParam) && (!m_OvrlDashboardActive))
{
m_OverlayDragger.UpdateTempStandingPosition();
}
ApplySettingInputMode();
break;
}
case configid_bool_state_overlay_selectmode:
{
ApplySettingInputMode();
break;
}
case configid_bool_state_misc_elevated_mode_active:
{
m_InputSim.SetElevatedModeForwardingActive(msg.lParam);
break;
}
default: break;
}
}
else if (msg.wParam < configid_bool_MAX + configid_int_MAX)
{
ConfigID_Int int_id = (ConfigID_Int)(msg.wParam - configid_bool_MAX);
int previous_value = ConfigManager::GetValue(int_id);
ConfigManager::SetValue(int_id, msg.lParam);
switch (int_id)
{
case configid_int_interface_overlay_current_id:
{
OverlayManager::Get().SetCurrentOverlayID(msg.lParam);
current_overlay_old = (unsigned int)msg.lParam;
break;
}
case configid_int_interface_background_color:
case configid_int_interface_background_color_display_mode:
{
m_BackgroundOverlay.Update();
break;
}
case configid_int_overlay_desktop_id:
{
if (ConfigManager::GetValue(configid_bool_overlay_crop_enabled))
{
CropToDisplay(msg.lParam);
}
else //Don't touch cropping setting values if it's disabled and just update the validated crop rect instead
{
OverlayManager::Get().GetCurrentOverlay().UpdateValidatedCropRect();
ApplySettingCrop();
ApplySettingTransform();
ApplySettingMouseScale();
ApplySettingExtraBrightness();
}
reset_mirroring = (ConfigManager::GetValue(configid_bool_performance_single_desktop_mirroring) && (msg.lParam != previous_value));
break;
}
case configid_int_overlay_capture_source:
{
ResetOverlayActiveCount();
ResetCurrentOverlay();
break;
}
case configid_int_overlay_winrt_desktop_id:
{
if (previous_value != msg.lParam)
{
OverlayManager::Get().GetCurrentOverlay().SetTextureSource(ovrl_texsource_none);
ResetCurrentOverlay();
}
break;
}
case configid_int_overlay_user_width:
case configid_int_overlay_user_height:
{
if (OverlayManager::Get().GetCurrentOverlay().GetTextureSource() == ovrl_texsource_browser)
{
const int user_width = ConfigManager::GetValue(configid_int_overlay_user_width);
const int user_height = ConfigManager::GetValue(configid_int_overlay_user_height);
DPBrowserAPIClient::Get().DPBrowser_SetResolution(OverlayManager::Get().GetCurrentOverlay().GetHandle(), user_width, user_height);
//Also set as content width
ConfigManager::SetValue(configid_int_overlay_state_content_width, user_width);
ConfigManager::SetValue(configid_int_overlay_state_content_height, user_height);
//Update crop as it depends on user size
if ((ConfigManager::GetValue(configid_bool_overlay_crop_enabled)) || (ConfigManager::GetValue(configid_bool_overlay_3D_enabled)))
{
ApplySettingCrop();
}
//Send to UI
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, (int)OverlayManager::Get().GetCurrentOverlayID());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_state_content_width, user_width);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_state_content_height, user_height);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
//Also do it for everything using this as duplication source
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
for (unsigned int overlay_id : OverlayManager::Get().FindDuplicatedOverlaysForOverlay(OverlayManager::Get().GetCurrentOverlayID()))
{
OverlayManager::Get().SetCurrentOverlayID(overlay_id);
//Set config values for duplicated overlays as well so code only reading from it doesn't have to care about them being duplicated
ConfigManager::SetValue(configid_int_overlay_user_width, user_width);
ConfigManager::SetValue(configid_int_overlay_user_height, user_height);
ConfigManager::SetValue(configid_int_overlay_state_content_width, user_width);
ConfigManager::SetValue(configid_int_overlay_state_content_height, user_height);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, (int)overlay_id);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_state_content_width, user_width);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_state_content_height, user_height);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
if (ConfigManager::GetValue(configid_bool_overlay_crop_enabled))
{
ApplySettingCrop();
}
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
ApplySettingMouseInput();
}
break;
}
case configid_int_overlay_crop_x:
case configid_int_overlay_crop_y:
case configid_int_overlay_crop_width:
case configid_int_overlay_crop_height:
{
ApplySettingCrop();
ApplySettingTransform();
ApplySettingMouseScale();
break;
}
case configid_int_overlay_3D_mode:
{
ApplySettingTransform();
ApplySetting3DMode();
break;
}
case configid_int_overlay_display_mode:
{
ApplySettingTransform();
break;
}
case configid_int_overlay_origin:
{
DetachedTransformConvertOrigin(OverlayManager::Get().GetCurrentOverlayID(), (OverlayOrigin)previous_value, (OverlayOrigin)msg.lParam);
ApplySettingTransform();
break;
}
case configid_int_overlay_origin_smoothing_level:
{
//Reset smoothers if there was previously no smoothing enabled
if (previous_value == 0)
{
Overlay& overlay = OverlayManager::Get().GetCurrentOverlay();
overlay.GetSmootherPos().ResetLastPos();
overlay.GetSmootherRot().ResetLastPos();
}
ApplySettingTransform();
break;
}
case configid_int_overlay_browser_max_fps_override:
{
if (OverlayManager::Get().GetCurrentOverlay().GetTextureSource() == ovrl_texsource_browser)
{
DPBrowserAPIClient::Get().DPBrowser_SetFPS(OverlayManager::Get().GetCurrentOverlay().GetHandle(), msg.lParam);
}
break;
}
case configid_int_interface_wmr_ignore_vscreens:
{
DPWinRT_SetDesktopEnumerationFlags((msg.lParam == 1));
//May affect desktop enumeration, reset mirroring
reset_mirroring = true;
break;
}
case configid_int_input_mouse_dbl_click_assist_duration_ms:
{
ApplySettingMouseInput();
break;
}
case configid_int_input_laser_pointer_hmd_device_keycode_toggle:
case configid_int_input_laser_pointer_hmd_device_keycode_left:
case configid_int_input_laser_pointer_hmd_device_keycode_right:
case configid_int_input_laser_pointer_hmd_device_keycode_middle:
{
RegisterHotkeys();
break;
}
case configid_int_windows_winrt_dragging_mode:
{
WindowManager::Get().UpdateConfigState();
break;
}
case configid_int_performance_update_limit_mode:
case configid_int_performance_update_limit_fps:
case configid_int_overlay_update_limit_override_mode:
case configid_int_overlay_update_limit_override_fps:
{
ApplySettingUpdateLimiter();
break;
}
case configid_int_browser_max_fps:
{
DPBrowserAPIClient::Get().DPBrowser_GlobalSetFPS(msg.lParam);
break;
}
default: break;
}
}
else if (msg.wParam < configid_bool_MAX + configid_int_MAX + configid_float_MAX)
{
ConfigID_Float float_id = (ConfigID_Float)(msg.wParam - configid_bool_MAX - configid_int_MAX);
float value = pun_cast(msg.lParam);
float previous_value = ConfigManager::GetValue(float_id);
ConfigManager::SetValue(float_id, value);
switch (float_id)
{
case configid_float_overlay_width:
case configid_float_overlay_curvature:
case configid_float_overlay_opacity:
case configid_float_overlay_brightness:
case configid_float_overlay_offset_right:
case configid_float_overlay_offset_up:
case configid_float_overlay_offset_forward:
{
ApplySettingTransform();
break;
}
case configid_float_overlay_browser_zoom:
{
if (OverlayManager::Get().GetCurrentOverlay().GetTextureSource() == ovrl_texsource_browser)
{
DPBrowserAPIClient::Get().DPBrowser_SetZoomLevel(OverlayManager::Get().GetCurrentOverlay().GetHandle(), value);
}
break;
}
case configid_float_performance_update_limit_ms:
case configid_float_overlay_update_limit_override_ms:
{
ApplySettingUpdateLimiter();
break;
}
default: break;
}
}
else if (msg.wParam < configid_bool_MAX + configid_int_MAX + configid_float_MAX + configid_handle_MAX)
{
ConfigID_Handle handle_id = (ConfigID_Handle)(msg.wParam - configid_bool_MAX - configid_int_MAX - configid_float_MAX);
uint64_t value = pun_cast(msg.lParam);
uint64_t previous_value = ConfigManager::GetValue(handle_id);
ConfigManager::SetValue(handle_id, value);
switch (handle_id)
{
case configid_handle_overlay_state_winrt_hwnd:
{
if (value != previous_value)
{
OnSetOverlayWinRTCaptureWindow(OverlayManager::Get().GetCurrentOverlayID());
}
break;
}
}
}
break;
}
}
//Restore overlay id override
if (overlay_override_id != -1)
{
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
return reset_mirroring;
}
void OutputManager::HandleWinRTMessage(const MSG& msg)
{
switch (msg.message)
{
case WM_DPLUSWINRT_SIZE:
{
const unsigned int overlay_id = OverlayManager::Get().FindOverlayID(msg.wParam);
if (overlay_id == k_ulOverlayID_None)
{
break;
}
const int content_width = GET_X_LPARAM(msg.lParam);
const int content_height = GET_Y_LPARAM(msg.lParam);
const Overlay& overlay = OverlayManager::Get().GetOverlay(overlay_id);
OverlayConfigData& data = OverlayManager::Get().GetConfigData(overlay_id);
//Skip if no real change
if ((data.ConfigInt[configid_int_overlay_state_content_width] == content_width) && (data.ConfigInt[configid_int_overlay_state_content_height] == content_height))
{
break;
}
//Adaptive Size
bool adaptive_size_apply = ( (ConfigManager::GetValue(configid_bool_windows_winrt_auto_size_overlay)) && (overlay.GetTextureSource() == ovrl_texsource_winrt_capture) &&
(data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0) && (data.ConfigInt[configid_int_overlay_state_content_width] != -1) &&
(content_width != -1) );
if (adaptive_size_apply)
{
data.ConfigFloat[configid_float_overlay_width] *= (float)content_width / data.ConfigInt[configid_int_overlay_state_content_width];
}
data.ConfigInt[configid_int_overlay_state_content_width] = content_width;
data.ConfigInt[configid_int_overlay_state_content_height] = content_height;
//Send update to UI
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, (int)overlay_id);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_state_content_width, content_width);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_state_content_height, content_height);
if (adaptive_size_apply)
{
IPCManager::Get().PostConfigMessageToUIApp(configid_float_overlay_width, data.ConfigFloat[configid_float_overlay_width]);
}
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
//Apply change to overlay
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(overlay_id);
ApplySettingCrop();
ApplySettingTransform();
ApplySettingMouseScale();
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
break;
}
case WM_DPLUSWINRT_CAPTURE_LOST:
{
const unsigned int overlay_id = OverlayManager::Get().FindOverlayID(msg.wParam);
if (overlay_id == k_ulOverlayID_None)
{
break;
}
Overlay& overlay = OverlayManager::Get().GetOverlay(overlay_id);
OverlayConfigData& data = OverlayManager::Get().GetConfigData(overlay_id);
//Hide affected overlay if setting enabled
if ( (data.ConfigBool[configid_bool_overlay_enabled]) && (ConfigManager::GetValue(configid_int_windows_winrt_capture_lost_behavior) == window_caplost_hide_overlay) )
{
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(overlay_id);
data.ConfigBool[configid_bool_overlay_enabled] = false;
ApplySettingTransform();
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
//Send to UI
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, (int)overlay_id);
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_overlay_enabled, false);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
}
else if (ConfigManager::GetValue(configid_int_windows_winrt_capture_lost_behavior) == window_caplost_remove_overlay) //Or remove it
{
//Queue up removal instead of doing it right away in case there are multiple overlays with the same target lost at once (which breaks otherwise)
m_RemoveOverlayQueue.push_back(overlay_id);
break;
}
//Only change texture source if the overlay is still a winrt capture (this can be false when a picker gets canceled late)
if ( (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_winrt_capture) || (overlay.GetTextureSource() == ovrl_texsource_winrt_capture) )
{
overlay.SetTextureSource(ovrl_texsource_none);
}
break;
}
case WM_DPLUSWINRT_THREAD_ERROR:
{
//We get capture lost messages for each affected overlay, so just forward the error to the UI so a warning can be displayed for now
IPCManager::Get().PostMessageToUIApp(ipcmsg_action, ipcact_winrt_thread_error, msg.lParam);
LOG_F(ERROR, "Unexpected error occurred in a Graphics Capture thread! (%#x)", (unsigned int)msg.lParam);
break;
}
case WM_DPLUSWINRT_FPS:
{
const unsigned int overlay_id = OverlayManager::Get().FindOverlayID(msg.wParam);
if (overlay_id == k_ulOverlayID_None)
{
break;
}
OverlayConfigData& data = OverlayManager::Get().GetConfigData(overlay_id);
data.ConfigInt[configid_int_overlay_state_fps] = msg.lParam;
//Send update to UI
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, (int)overlay_id);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_state_fps, msg.lParam);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
break;
}
}
}
void OutputManager::HandleHotkeyMessage(const MSG& msg)
{
ConfigHotkeyList& hotkey_list = ConfigManager::Get().GetHotkeys();
int hotkey_id = msg.wParam;
if ((hotkey_id >= 0) && (hotkey_id < (int)hotkey_list.size()))
{
ConfigHotkey& hotkey = hotkey_list[hotkey_id];
//StateIsDown blocks HandleHotkeys() and the hotkey messages from triggering hotkey actions twice. It's reset in HandleHotkeys() when the key is no longer pressed
if (!hotkey.StateIsDown)
{
ConfigManager::Get().GetActionManager().DoAction(hotkey.ActionUID);
hotkey.StateIsDown = true;
}
}
}
void OutputManager::OnExit()
{
//Release all held keyboard keys. Might be going overboard but better than keeping keys down unexpectedly
for (int i = 0; i < 256; ++i)
{
m_InputSim.KeyboardSetKeyState((IPCKeyboardKeystateFlags)0, i);
}
//Undo dimmed dashboard
DimDashboard(false);
//Shutdown VR for good
vr::VR_Shutdown();
}
HWND OutputManager::GetWindowHandle()
{
return m_WindowHandle;
}
//
// Returns shared handle
//
HANDLE OutputManager::GetSharedHandle()
{
HANDLE Hnd = nullptr;
// QI IDXGIResource interface to synchronized shared surface.
IDXGIResource* DXGIResource = nullptr;
HRESULT hr = m_SharedSurf->QueryInterface(__uuidof(IDXGIResource), reinterpret_cast(&DXGIResource));
if (SUCCEEDED(hr))
{
// Obtain handle to IDXGIResource object.
DXGIResource->GetSharedHandle(&Hnd);
DXGIResource->Release();
DXGIResource = nullptr;
}
return Hnd;
}
IDXGIAdapter* OutputManager::GetDXGIAdapter()
{
HRESULT hr;
// Get DXGI factory
IDXGIDevice* DxgiDevice = nullptr;
hr = m_Device->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast(&DxgiDevice));
if (FAILED(hr))
{
return nullptr;
}
IDXGIAdapter* DxgiAdapter = nullptr;
hr = DxgiDevice->GetParent(__uuidof(IDXGIAdapter), reinterpret_cast(&DxgiAdapter));
DxgiDevice->Release();
DxgiDevice = nullptr;
if (FAILED(hr))
{
return nullptr;
}
return DxgiAdapter;
}
void OutputManager::ResetOverlays()
{
//Check if process is elevated and send that info to the UI too (DPBrowser needs this info so do this first)
bool elevated = IsProcessElevated();
ConfigManager::SetValue(configid_bool_state_misc_process_elevated, elevated);
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_misc_process_elevated, elevated);
//Reset all overlays
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayManager::Get().SetCurrentOverlayID(i);
ApplySettingCrop();
ApplySettingTransform();
ApplySettingCaptureSource();
ApplySetting3DMode();
}
//Second pass for browser overlays using a duplication ID that is higher than the overlay's
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayManager::Get().SetCurrentOverlayID(i);
const OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
if ( (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_browser) && (data.ConfigInt[configid_int_overlay_duplication_id] != -1) )
{
ApplySettingCrop();
ApplySettingCaptureSource();
ApplySetting3DMode();
}
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
//These apply to all overlays within the function itself
ApplySettingInputMode();
ApplySettingUpdateLimiter();
ResetOverlayActiveCount();
//Post overlays reset message to UI app
IPCManager::Get().PostMessageToUIApp(ipcmsg_action, ipcact_overlays_reset);
//Make sure that the entire overlay texture gets at least one full update for regions that will never be dirty (i.e. blank space not occupied by any desktop)
m_OutputPendingFullRefresh = true;
}
void OutputManager::ResetCurrentOverlay()
{
if ((OverlayManager::Get().GetCurrentOverlayID() == k_ulOverlayID_None) || (OverlayManager::Get().GetCurrentOverlay().GetID() == k_ulOverlayID_None))
return;
ApplySettingCrop();
ApplySettingTransform();
ApplySettingCaptureSource();
ApplySettingInputMode();
ApplySetting3DMode();
ApplySettingUpdateLimiter();
//Make sure that the entire overlay texture gets at least one full update for regions that will never be dirty (i.e. blank space not occupied by any desktop)
if (ConfigManager::GetValue(configid_int_overlay_capture_source) == ovrl_capsource_desktop_duplication)
{
m_OutputPendingFullRefresh = true;
}
}
ID3D11Texture2D* OutputManager::GetOverlayTexture() const
{
return m_OvrlTex;
}
ID3D11Texture2D* OutputManager::GetMultiGPUTargetTexture() const
{
return m_MultiGPUTexTarget;
}
vr::VROverlayHandle_t OutputManager::GetDesktopTextureOverlay() const
{
return m_OvrlHandleDesktopTexture;
}
bool OutputManager::GetOverlayActive() const
{
return (m_OvrlActiveCount != 0);
}
bool OutputManager::GetOverlayInputActive() const
{
return m_OvrlInputActive;
}
DWORD OutputManager::GetMaxRefreshDelay() const
{
if ( (m_OvrlActiveCount != 0) || (m_OvrlDashboardActive) || (m_LaserPointer.IsActive()) )
{
//Actually causes extreme load while not really being necessary (looks nice tho)
if ( (m_OvrlInputActive) && (ConfigManager::GetValue(configid_bool_performance_rapid_laser_pointer_updates)) )
{
return 0;
}
else if (m_LaserPointer.IsScrolling())
{
//While scrolling with the Desktop+ Laser Pointer, we actually need to update frequently to keep scroll speeds and generated haptic feedback at the usual pace
return 3;
}
else if (m_OvrlInputActive)
{
//While input is active, especially with the HMD pointer, we need to update more frequently to allow for smooth cursor movements
return m_MaxActiveRefreshDelay / 2;
}
else
{
return m_MaxActiveRefreshDelay;
}
}
else if ( (m_VRInput.IsAnyGlobalActionBound()) || (IsAnyOverlayUsingGazeFade()) || (m_IsAnyHotkeyActive) )
{
return m_MaxActiveRefreshDelay * 2;
}
else
{
return 300;
}
}
float OutputManager::GetHMDFrameRate() const
{
return vr::VRSystem()->GetFloatTrackedDeviceProperty(vr::k_unTrackedDeviceIndex_Hmd, vr::Prop_DisplayFrequency_Float);
}
int OutputManager::GetDesktopWidth() const
{
return m_DesktopWidth;
}
int OutputManager::GetDesktopHeight() const
{
return m_DesktopHeight;
}
const std::vector& OutputManager::GetDesktopRects() const
{
return m_DesktopRects;
}
float OutputManager::GetDesktopHDRWhiteLevelAdjustment(int desktop_id, bool is_for_graphics_capture, bool wmr_ignore_vscreens) const
{
#ifdef DPLUS_DUP_NO_HDR
return 1.0f;
#else
if (desktop_id == -1)
desktop_id = 0;
if ((!m_OutputHDRAvailable) && (!is_for_graphics_capture))
return 1.0f;
DXGI_OUTPUT_DESC output_desc = {};
Microsoft::WRL::ComPtr factory_ptr;
//This needs to go through DXGI as QueryDisplayConfig()'s order can be different
HRESULT hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&factory_ptr);
if (!FAILED(hr))
{
Microsoft::WRL::ComPtr adapter_ptr;
UINT i = 0;
int output_count = 0;
while (factory_ptr->EnumAdapters(i, &adapter_ptr) != DXGI_ERROR_NOT_FOUND)
{
//Check if this a WMR virtual display adapter and skip it when the option is enabled
if (wmr_ignore_vscreens)
{
DXGI_ADAPTER_DESC adapter_desc;
adapter_ptr->GetDesc(&adapter_desc);
if (wcscmp(adapter_desc.Description, L"Virtual Display Adapter") == 0)
{
++i;
continue;
}
}
//Enum the available outputs
Microsoft::WRL::ComPtr output_ptr;
UINT output_index = 0;
while (adapter_ptr->EnumOutputs(output_index, &output_ptr) != DXGI_ERROR_NOT_FOUND)
{
//Check if this happens to be the output we're looking for
if (desktop_id == output_count)
{
//Get output desc
output_ptr->GetDesc(&output_desc);
}
++output_index;
++output_count;
}
++i;
}
}
//Find display config with the same device path
std::vector paths;
std::vector modes;
const UINT32 flags = QDC_ONLY_ACTIVE_PATHS | QDC_VIRTUAL_MODE_AWARE;
LONG result = ERROR_SUCCESS;
//Loop until buffer allocation for paths match the requirements
do
{
UINT32 path_count, mode_count;
result = ::GetDisplayConfigBufferSizes(flags, &path_count, &mode_count);
if (result != ERROR_SUCCESS)
{
LOG_F(ERROR, "GetDisplayConfigBufferSizes() failed with %ld", result);
return 1.0f;
}
paths.resize(path_count);
modes.resize(mode_count);
result = ::QueryDisplayConfig(flags, &path_count, paths.data(), &mode_count, modes.data(), nullptr);
paths.resize(path_count);
modes.resize(mode_count);
}
while (result == ERROR_INSUFFICIENT_BUFFER);
if (result != ERROR_SUCCESS)
{
LOG_F(ERROR, "QueryDisplayConfig() failed with %ld", result);
return 1.0f;
}
//Check each active path
for (auto& path : paths)
{
DISPLAYCONFIG_SOURCE_DEVICE_NAME source_name = {};
source_name.header.adapterId = path.sourceInfo.adapterId;
source_name.header.id = path.sourceInfo.id;
source_name.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
source_name.header.size = sizeof(source_name);
result = ::DisplayConfigGetDeviceInfo(&source_name.header);
if (result == ERROR_SUCCESS)
{
if (wcscmp(source_name.viewGdiDeviceName, output_desc.DeviceName) == 0)
{
//Found the right display config path, time to grab the data
bool is_hdr_enabled = false;
bool is_8bit = true;
ULONG sdr_white_level = 1000;
#if (NTDDI_VERSION >= 0x0A00000F/*NTDDI_WIN11_GA*/)
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2 adv_color_info_2 = {};
adv_color_info_2.header.adapterId = path.targetInfo.adapterId;
adv_color_info_2.header.id = path.targetInfo.id;
adv_color_info_2.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO_2;
adv_color_info_2.header.size = sizeof(adv_color_info_2);
result = ::DisplayConfigGetDeviceInfo(&adv_color_info_2.header);
if (result == ERROR_SUCCESS)
{
is_8bit = (adv_color_info_2.bitsPerColorChannel == 8);
//DISPLAYCONFIG_ADVANCED_COLOR_MODE_WCG is still higher bit-depth but seems like it needs to be handled differently
is_hdr_enabled = (adv_color_info_2.activeColorMode == DISPLAYCONFIG_ADVANCED_COLOR_MODE_HDR);
}
if (is_hdr_enabled)
{
DISPLAYCONFIG_SDR_WHITE_LEVEL config_sdr_white_level = {};
config_sdr_white_level.header.adapterId = path.targetInfo.adapterId;
config_sdr_white_level.header.id = path.targetInfo.id;
config_sdr_white_level.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SDR_WHITE_LEVEL;
config_sdr_white_level.header.size = sizeof(sdr_white_level);
result = ::DisplayConfigGetDeviceInfo(&config_sdr_white_level.header);
if (result == ERROR_SUCCESS)
{
sdr_white_level = config_sdr_white_level.SDRWhiteLevel;
}
}
#endif
DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO adv_color_info = {};
adv_color_info.header.adapterId = path.targetInfo.adapterId;
adv_color_info.header.id = path.targetInfo.id;
adv_color_info.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_ADVANCED_COLOR_INFO;
adv_color_info.header.size = sizeof(adv_color_info);
result = ::DisplayConfigGetDeviceInfo(&adv_color_info.header);
if (result == ERROR_SUCCESS)
{
is_8bit = (adv_color_info.bitsPerColorChannel == 8);
}
//The following is based on potentially incomplete observations and doesn't appear to be documented anywhere otherwise
//Checking different OS versions without access to a HDR display in a VM makes things a little bit messy... so this likely needs to be fixed up later
if (is_8bit)
{
//This the easiest to check and has been observed across several Windows 10 and 11 versions, why it's like this I don't know
return (is_for_graphics_capture) ? 0.5f : 1.0f;
}
else if (is_hdr_enabled)
{
//Observed on Windows 11 24H2, but not on Windows 10 (DISPLAYCONFIG_GET_ADVANCED_COLOR_INFO_2 doesn't exist there so it won't hit this path)
return 1000.0f / sdr_white_level;
}
else
{
//Observed on Windows 10 20H2, but potentially always applies to DISPLAYCONFIG_ADVANCED_COLOR_MODE_WCG in general and DISPLAYCONFIG_ADVANCED_COLOR_MODE_HDR doesn't exist there?
//However, also observed displays set to HDR get non-linear pixel data written by Desktop Duplication on there (Graphics Capture and Desktop Duplication non-HDR display pixels are correct)
//Might have some unknown factor causing it, even if fixable with extra steps in theory... so it is what it is for now.
return (is_for_graphics_capture) ? 0.5f : 1.0f;
}
}
}
}
LOG_F(WARNING, "Could not find display config for desktop %d, defaulting to 100%% brightness adjustment", desktop_id);
return 1.0f;
#endif //DPLUS_DUP_NO_HDR
}
void OutputManager::ShowOverlay(unsigned int id)
{
Overlay& overlay = OverlayManager::Get().GetOverlay(id);
if (overlay.IsVisible()) //Already visible? Abort.
{
return;
}
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(id);
vr::VROverlayHandle_t ovrl_handle = overlay.GetHandle();
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(id);
if (m_OvrlActiveCount == 0) //First overlay to become active
{
::timeBeginPeriod(1); //This is somewhat frowned upon, but we want to hit the polling rate, it's only when active and we're in a high performance situation anyways
//Set last pointer values to current to not trip the movement detection up
ResetMouseLastLaserPointerPos();
m_MouseIgnoreMoveEvent = false;
WindowManager::Get().SetOverlayActive(true);
}
m_OvrlActiveCount++;
if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication)
{
if (m_OvrlDesktopDuplActiveCount == 0) //First Desktop Duplication overlay to become active
{
//Signal duplication threads to resume in case they're paused
::ResetEvent(m_PauseDuplicationEvent);
::SetEvent(m_ResumeDuplicationEvent);
ForceScreenRefresh();
}
m_OvrlDesktopDuplActiveCount++;
}
else if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_winrt_capture)
{
//Unpause capture
DPWinRT_PauseCapture(ovrl_handle, false);
}
else if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_browser)
{
//Don't call this before texture source is set (browser process may not be running), ApplySettingCaptureSource() will call it in that case
if (overlay.GetTextureSource() == ovrl_texsource_browser)
{
//Unpause browser
DPBrowserAPIClient::Get().DPBrowser_PauseBrowser(overlay.GetHandle(), false);
}
}
overlay.SetVisible(true);
ApplySettingTransform();
//Overlay could affect update limiter, so apply setting
if (data.ConfigInt[configid_int_overlay_update_limit_override_mode] != update_limit_mode_off)
{
ApplySettingUpdateLimiter();
}
//If the last clipping rect doesn't fully contain the overlay's crop rect, the desktop texture overlay is probably outdated there, so force a full refresh
if ( (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication) && (!m_OutputLastClippingRect.Contains(overlay.GetValidatedCropRect())) )
{
RefreshOpenVROverlayTexture(DPRect(-1, -1, -1, -1), true);
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
void OutputManager::ShowTheaterOverlay(unsigned int id)
{
if (OverlayManager::Get().GetTheaterOverlayID() == id)
return;
//Don't set theater overlay before texture source is initialized
if (OverlayManager::Get().GetOverlay(id).GetTextureSource() == ovrl_texsource_invalid)
return;
OverlayManager::Get().SetTheaterOverlayID(id);
//Check every other existing overlay for theater origin and disable them (only one theater overlay can be active)
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
Overlay& overlay = OverlayManager::Get().GetOverlay(i);
OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if ((data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_theater_screen) && (i != id))
{
SetOverlayEnabled(i, false);
}
}
//Reset overlay so the theater overlay has every property applied
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(id);
ResetCurrentOverlay();
OverlayManager::Get().GetCurrentOverlay().AssignDesktopDuplicationTexture(); //Desktop Duplication texture isn't reset and only assigned on change, so do it manually
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
void OutputManager::HideOverlay(unsigned int id)
{
Overlay& overlay = OverlayManager::Get().GetOverlay(id);
if (!overlay.IsVisible()) //Already hidden? Abort.
{
return;
}
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(id);
vr::VROverlayHandle_t ovrl_handle = overlay.GetHandle();
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(id);
overlay.SetVisible(false);
//Overlay could've affected update limiter, so apply setting
if (data.ConfigInt[configid_int_overlay_update_limit_override_mode] != update_limit_mode_off)
{
ApplySettingUpdateLimiter();
}
m_OvrlActiveCount--;
if (m_OvrlActiveCount == 0) //Last overlay to become inactive
{
::timeEndPeriod(1);
WindowManager::Get().SetOverlayActive(false);
}
if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication)
{
m_OvrlDesktopDuplActiveCount--;
if (m_OvrlDesktopDuplActiveCount == 0) //Last Desktop Duplication overlay to become inactive
{
//Signal duplication threads to pause since we don't need them to do needless work
::ResetEvent(m_ResumeDuplicationEvent);
::SetEvent(m_PauseDuplicationEvent);
}
}
else if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_winrt_capture)
{
//Pause capture
DPWinRT_PauseCapture(ovrl_handle, true);
}
else if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_browser)
{
//Don't call this before texture source is set (browser process may not be running), ApplySettingCaptureSource() will call it in that case
if (overlay.GetTextureSource() == ovrl_texsource_browser)
{
//Pause browser
DPBrowserAPIClient::Get().DPBrowser_PauseBrowser(overlay.GetHandle(), true);
}
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
void OutputManager::ResetOverlayActiveCount()
{
bool desktop_duplication_was_paused = (m_OvrlDesktopDuplActiveCount == 0);
m_OvrlActiveCount = 0;
m_OvrlDesktopDuplActiveCount = 0;
//Check every existing overlay for visibility and count them as active
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
Overlay& overlay = OverlayManager::Get().GetOverlay(i);
OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if (overlay.IsVisible())
{
m_OvrlActiveCount++;
if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication)
{
m_OvrlDesktopDuplActiveCount++;
}
}
}
//Fixup desktop duplication state
if ( (desktop_duplication_was_paused) && (m_OvrlDesktopDuplActiveCount > 0) )
{
//Signal duplication threads to resume
::ResetEvent(m_PauseDuplicationEvent);
::SetEvent(m_ResumeDuplicationEvent);
ForceScreenRefresh();
}
else if ( (!desktop_duplication_was_paused) && (m_OvrlDesktopDuplActiveCount == 0) )
{
//Signal duplication threads to pause
::ResetEvent(m_ResumeDuplicationEvent);
::SetEvent(m_PauseDuplicationEvent);
}
//Fixup WindowManager state
WindowManager::Get().SetOverlayActive( (m_OvrlActiveCount > 0) );
}
bool OutputManager::HasDashboardBeenActivatedOnce() const
{
return m_DashboardActivatedOnce;
}
bool OutputManager::IsDashboardTabActive() const
{
return m_OvrlDashboardActive;
}
float OutputManager::GetDashboardScale() const
{
vr::HmdMatrix34_t matrix = {0};
vr::VROverlay()->GetTransformForOverlayCoordinates(m_OvrlHandleDashboardDummy, vr::TrackingUniverseStanding, {0.5f, 0.5f}, &matrix);
Vector3 row_1(matrix.m[0][0], matrix.m[1][0], matrix.m[2][0]);
return row_1.length(); //Scaling is always uniform so we just check the x-axis
}
float OutputManager::GetOverlayHeight(unsigned int overlay_id) const
{
const Overlay& overlay = OverlayManager::Get().GetOverlay(overlay_id);
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(overlay_id);
const DPRect& crop_rect = overlay.GetValidatedCropRect();
int crop_width = crop_rect.GetWidth(), crop_height = crop_rect.GetHeight();
bool is_3d_enabled = data.ConfigBool[configid_bool_overlay_3D_enabled];
int mode_3d = data.ConfigInt[configid_int_overlay_3D_mode];
if ( (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication) && (m_OutputInvalid) ) //No cropping on invalid output image
{
crop_width = k_lOverlayOutputErrorTextureWidth;
crop_height = k_lOverlayOutputErrorTextureHeight;
is_3d_enabled = false;
}
else if ( (overlay.GetTextureSource() == ovrl_texsource_none) || (crop_width <= 0) || (crop_height <= 0) )
{
//Get dimensions from mouse scale if possible
vr::HmdVector2_t mouse_scale;
if (vr::VROverlay()->GetOverlayMouseScale(overlay.GetHandle(), &mouse_scale) == vr::VROverlayError_None)
{
crop_width = mouse_scale.v[0];
crop_height = mouse_scale.v[1];
}
else
{
crop_width = k_lOverlayOutputErrorTextureWidth;
crop_height = k_lOverlayOutputErrorTextureHeight;
}
is_3d_enabled = false;
}
else if ( (is_3d_enabled) && (mode_3d == ovrl_3Dmode_ou) ) //Converted Over-Under changes texture dimensions, so adapt
{
crop_width *= 2;
crop_height /= 2;
}
//Overlay is twice as tall when SBS3D/OU3D is active
if ( (is_3d_enabled) && ( (mode_3d == ovrl_3Dmode_sbs) || (mode_3d == ovrl_3Dmode_ou) ) )
crop_height *= 2;
return data.ConfigFloat[configid_float_overlay_width] * ((float)crop_height / crop_width);
}
Matrix4 OutputManager::GetFallbackOverlayTransform() const
{
Matrix4 transform;
//Get HMD pose
vr::TrackedDevicePose_t poses[vr::k_unTrackedDeviceIndex_Hmd + 1];
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, vr::IVRSystemEx::GetTimeNowToPhotons(), poses, vr::k_unTrackedDeviceIndex_Hmd + 1);
if (poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid)
{
//Set to HMD position and offset 2m away
Matrix4 mat_hmd(poses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking);
transform = mat_hmd;
transform.translate_relative(0.0f, 0.0f, -2.0f);
//Rotate towards HMD position
vr::IVRSystemEx::TransformLookAt(transform, mat_hmd.getTranslation());
}
return transform;
}
void OutputManager::SetOutputErrorTexture(vr::VROverlayHandle_t overlay_handle)
{
vr::EVROverlayError vr_error = vr::VROverlayEx()->SetOverlayFromFileEx(overlay_handle, (ConfigManager::Get().GetApplicationPath() + "images/output_error.png").c_str());
vr::VRTextureBounds_t tex_bounds = {0.0f};
tex_bounds.uMax = 1.0f;
tex_bounds.vMax = 1.0f;
vr::VROverlay()->SetOverlayTextureBounds(overlay_handle, &tex_bounds);
//Make sure to remove 3D on the overlay too
vr::VROverlay()->SetOverlayFlag(overlay_handle, vr::VROverlayFlags_SideBySide_Parallel, false);
vr::VROverlay()->SetOverlayFlag(overlay_handle, vr::VROverlayFlags_SideBySide_Crossed, false);
vr::VROverlay()->SetOverlayTexelAspect(overlay_handle, 1.0f);
//Mouse scale needs to be updated as well
ApplySettingMouseInput();
}
void OutputManager::SetOutputInvalid()
{
LOG_F(WARNING, "No outputs available!");
m_OutputInvalid = true;
SetOutputErrorTexture(m_OvrlHandleDesktopTexture);
m_DesktopWidth = k_lOverlayOutputErrorTextureWidth;
m_DesktopHeight = k_lOverlayOutputErrorTextureHeight;
ResetOverlays();
}
bool OutputManager::IsOutputInvalid() const
{
return m_OutputInvalid;
}
void OutputManager::SetOverlayEnabled(unsigned int overlay_id, bool is_enabled)
{
OverlayConfigData& data = OverlayManager::Get().GetConfigData(overlay_id);
if (data.ConfigBool[configid_bool_overlay_enabled] == is_enabled)
return;
data.ConfigBool[configid_bool_overlay_enabled] = is_enabled;
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(overlay_id);
ApplySettingTransform();
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
//Sync change
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, overlay_id);
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_overlay_enabled, data.ConfigBool[configid_bool_overlay_enabled]);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
}
void OutputManager::CropToActiveWindowToggle(unsigned int overlay_id)
{
//If the action is used with one of the controller buttons, the events will fire another time if the new cropping values happen to have the laser pointer leave and
//re-enter the overlay for a split second while the button is still down during the dimension change.
//This would immediately undo the action, which we want to prevent, so a 100 ms pause between toggles is enforced
//-Currently not actually enforcing this with the new action commands to see where this goes-
static ULONGLONG last_toggle_tick = 0;
if ((overlay_id == k_ulOverlayID_None) /*|| (::GetTickCount64() <= last_toggle_tick + 100)*/)
return;
last_toggle_tick = ::GetTickCount64();
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(overlay_id);
bool& crop_enabled = ConfigManager::GetRef(configid_bool_overlay_crop_enabled);
int& crop_x = ConfigManager::GetRef(configid_int_overlay_crop_x);
int& crop_y = ConfigManager::GetRef(configid_int_overlay_crop_y);
int& crop_width = ConfigManager::GetRef(configid_int_overlay_crop_width);
int& crop_height = ConfigManager::GetRef(configid_int_overlay_crop_height);
//Check if crop is just exactly the current desktop
bool crop_equals_current_desktop = false;
const int desktop_id = ConfigManager::GetValue(configid_int_overlay_desktop_id);
if ( (desktop_id >= 0) && (desktop_id < m_DesktopRects.size()) )
{
DPRect crop_rect(crop_x, crop_y, crop_x + crop_width, crop_y + crop_height);
crop_equals_current_desktop = (crop_rect == m_DesktopRects[desktop_id]);
}
//Check if crop already matches the active window
bool crop_equals_new_window_crop = false;
int crop_x_new = crop_x;
int crop_y_new = crop_y;
int crop_width_new = crop_width;
int crop_height_new = crop_height;
if (CropToActiveWindow(crop_x_new, crop_y_new, crop_width_new, crop_height_new))
{
crop_equals_new_window_crop = ((crop_x == crop_x_new) && (crop_y == crop_y_new) && (crop_width == crop_width_new) && (crop_height == crop_height_new));
}
//If uncropped or different, crop to active window
if ( (!crop_enabled) || (!crop_equals_new_window_crop) )
{
CropToActiveWindow();
}
else //Otherwise, disable cropping (leaving the existing cropping values around)
{
crop_enabled = false;
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_overlay_crop_enabled, crop_enabled);
ApplySettingCrop();
ApplySettingTransform();
ApplySettingMouseScale();
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
void OutputManager::ShowWindowSwitcher()
{
InitComIfNeeded();
Microsoft::WRL::ComPtr shell_dispatch;
HRESULT sc = ::CoCreateInstance(CLSID_Shell, nullptr, CLSCTX_SERVER, IID_PPV_ARGS(&shell_dispatch));
if (SUCCEEDED(sc))
{
shell_dispatch->WindowSwitcher();
}
}
void OutputManager::SwitchToWindow(HWND window, bool warp_cursor)
{
WindowManager::Get().RaiseAndFocusWindow(window, &m_InputSim);
if (warp_cursor)
{
//Figure out center point of the window and put the cursor there
RECT window_rect = {0};
//Just using GetWindowRect() can include shadows and such, which we don't want
if (::DwmGetWindowAttribute(window, DWMWA_EXTENDED_FRAME_BOUNDS, &window_rect, sizeof(window_rect)) == S_OK)
{
DPRect window_dprect(window_rect.left, window_rect.top, window_rect.right, window_rect.bottom);
Vector2Int pt_center = window_dprect.GetCenter();
m_InputSim.MouseMove(pt_center.x, pt_center.y);
}
}
}
void OutputManager::OverlayDirectDragStart(unsigned int overlay_id)
{
if (overlay_id == k_ulOverlayID_None)
return;
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(overlay_id);
if (m_OverlayDragger.GetDragDeviceID() == -1)
{
if (data.ConfigInt[configid_int_overlay_origin] != ovrl_origin_theater_screen)
{
if (!data.ConfigBool[configid_bool_overlay_transform_locked])
{
m_OverlayDragger.DragStart(overlay_id);
if (m_OverlayDragger.IsDragActive())
{
m_OvrlDirectDragActive = true;
ApplySettingInputMode();
if (m_LaserPointer.IsActive())
{
m_LaserPointer.ForceTargetOverlay(OverlayManager::Get().GetOverlay(overlay_id).GetHandle());
}
}
}
else
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_device, ConfigManager::Get().GetPrimaryLaserPointerDevice());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 1);
}
}
else
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_device, ConfigManager::Get().GetPrimaryLaserPointerDevice());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 2);
}
}
}
void OutputManager::OverlayDirectDragFinish(unsigned int overlay_id)
{
if (overlay_id == k_ulOverlayID_None)
return;
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(overlay_id);
if ((m_OverlayDragger.GetDragOverlayID() == overlay_id) && (m_OverlayDragger.IsDragActive()))
{
OnDragFinish();
m_OverlayDragger.DragFinish();
ApplySettingTransform();
}
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 0);
}
VRInput& OutputManager::GetVRInput()
{
return m_VRInput;
}
InputSimulator& OutputManager::GetInputSimulator()
{
return m_InputSim;
}
void OutputManager::UpdatePerformanceStates()
{
//Frame counter, the frames themselves are counted in Update()
if (::GetTickCount64() >= m_PerformanceFrameCountStartTick + 1000)
{
//A second has passed, reset the value
ConfigManager::SetValue(configid_int_state_performance_duplication_fps, m_PerformanceFrameCount);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_performance_duplication_fps, m_PerformanceFrameCount);
m_PerformanceFrameCountStartTick = ::GetTickCount64();
m_PerformanceFrameCount = 0;
}
}
const LARGE_INTEGER& OutputManager::GetUpdateLimiterDelay()
{
return m_PerformanceUpdateLimiterDelay;
}
int OutputManager::EnumerateOutputs(int target_desktop_id, Microsoft::WRL::ComPtr* out_adapter_preferred, Microsoft::WRL::ComPtr* out_adapter_vr)
{
Microsoft::WRL::ComPtr factory_ptr;
Microsoft::WRL::ComPtr adapter_ptr_preferred;
Microsoft::WRL::ComPtr adapter_ptr_vr;
int output_id_adapter = target_desktop_id; //Output ID on the adapter actually used. Only different from initial SingleOutput if there's desktops across multiple GPUs
m_DesktopRects.clear();
m_DesktopRectTotal = DPRect(); //Figure out right dimensions for full size desktop rect (this is also done in CreateTextures() but for Desktop Duplication only)
m_DesktopHDRWhiteLevelAdjustments.clear();
const bool is_hdr_in_use = ((m_OutputHDRAvailable) && (ConfigManager::GetValue(configid_bool_performance_hdr_mirroring)));
const UINT target_adapter_deviceid = (ConfigManager::GetValue(configid_int_misc_force_gpu_deviceid) == -1) ? 0 : (UINT)ConfigManager::GetValue(configid_int_misc_force_gpu_deviceid);
const UINT target_adapter_deviceid_vr = (ConfigManager::GetValue(configid_int_misc_force_gpu_vr_deviceid) == -1) ? 0 : (UINT)ConfigManager::GetValue(configid_int_misc_force_gpu_vr_deviceid);
//Also look for the device the HMD is connected to
int32_t vr_gpu_id;
vr::VRSystem()->GetDXGIOutputInfo(&vr_gpu_id);
HRESULT hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&factory_ptr);
if (!FAILED(hr))
{
LOG_SCOPE_F(INFO, "Detected Outputs");
Microsoft::WRL::ComPtr adapter_ptr;
UINT i = 0;
int output_count = 0;
bool wmr_ignore_vscreens = (ConfigManager::GetValue(configid_int_interface_wmr_ignore_vscreens) == 1);
while (factory_ptr->EnumAdapters(i, &adapter_ptr) != DXGI_ERROR_NOT_FOUND)
{
DXGI_ADAPTER_DESC adapter_desc;
adapter_ptr->GetDesc(&adapter_desc);
//Check if this is the adapter SteamVR wants
if ( ((target_adapter_deviceid_vr == 0) && (i == vr_gpu_id)) || ((adapter_ptr_vr == nullptr) && (adapter_desc.DeviceId == target_adapter_deviceid_vr)) )
{
adapter_ptr_vr = adapter_ptr;
}
//Check if this a WMR virtual display adapter and skip it when the option is enabled
//This still only works correctly when they have the last desktops in the system, but that should pretty much be always the case
if (wmr_ignore_vscreens)
{
if (wcscmp(adapter_desc.Description, L"Virtual Display Adapter") == 0)
{
LOG_F(INFO, "Skipping \"Virtual Display Adapter\"");
++i;
continue;
}
}
//Count the available outputs
Microsoft::WRL::ComPtr output_ptr;
//Check if there are gonna be any outputs before logging the GPU to avoid confusion (there may be multiple adapters per GPU with no outputs attached)
if (adapter_ptr->EnumOutputs(0, &output_ptr) == DXGI_ERROR_NOT_FOUND)
{
//Still log them with higher verbosity level just in case
LOG_F(1, "GPU %u: %s (Device ID %u) (No Outputs)", i + 1, StringConvertFromUTF16(adapter_desc.Description).c_str(), adapter_desc.DeviceId);
++i;
continue;
}
LOG_SCOPE_F(INFO, "GPU %u: %s (Device ID %u)", i + 1, StringConvertFromUTF16(adapter_desc.Description).c_str(), adapter_desc.DeviceId);
UINT output_index = 0;
while (adapter_ptr->EnumOutputs(output_index, &output_ptr) != DXGI_ERROR_NOT_FOUND)
{
//Check if this happens to be the output we're looking for (or for combined desktop, set the first adapter with available output unless forced otherwise)
if ( (adapter_ptr_preferred == nullptr) &&
( (target_desktop_id == output_count) || ((target_desktop_id == -1) && ((target_adapter_deviceid == 0) || (adapter_desc.DeviceId == target_adapter_deviceid))) ) )
{
adapter_ptr_preferred = adapter_ptr;
if (target_desktop_id != -1)
{
output_id_adapter = output_index;
}
}
//Cache rect of the output
DXGI_OUTPUT_DESC output_desc;
output_ptr->GetDesc(&output_desc);
m_DesktopRects.emplace_back(output_desc.DesktopCoordinates.left, output_desc.DesktopCoordinates.top,
output_desc.DesktopCoordinates.right, output_desc.DesktopCoordinates.bottom);
(m_DesktopRectTotal.GetWidth() == 0) ? m_DesktopRectTotal = m_DesktopRects.back() : m_DesktopRectTotal.Add(m_DesktopRects.back());
//Cache HDR white level adjustment
const float white_level_adjust = GetDesktopHDRWhiteLevelAdjustment(output_count, false, wmr_ignore_vscreens);
m_DesktopHDRWhiteLevelAdjustments.push_back(white_level_adjust);
//Log display info, with white level adjustment if HDR is actually on
LOG_IF_F(INFO, !is_hdr_in_use, "Desktop %u: %4d,%4d | %4dx%4d (%s)", output_count + 1,
output_desc.DesktopCoordinates.left, output_desc.DesktopCoordinates.top,
output_desc.DesktopCoordinates.right - output_desc.DesktopCoordinates.left, output_desc.DesktopCoordinates.bottom - output_desc.DesktopCoordinates.top,
StringConvertFromUTF16(output_desc.DeviceName).c_str());
LOG_IF_F(INFO, is_hdr_in_use, "Desktop %u: %4d,%4d | %4dx%4d (%s) | %.2fx SDR Brightness", output_count + 1,
output_desc.DesktopCoordinates.left, output_desc.DesktopCoordinates.top,
output_desc.DesktopCoordinates.right - output_desc.DesktopCoordinates.left, output_desc.DesktopCoordinates.bottom - output_desc.DesktopCoordinates.top,
StringConvertFromUTF16(output_desc.DeviceName).c_str(), 1.0f / white_level_adjust);
++output_count;
++output_index;
}
++i;
}
//Store output/desktop count and send it over to UI
ConfigManager::SetValue(configid_int_state_interface_desktop_count, output_count);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_interface_desktop_count, output_count);
}
LOG_IF_F(WARNING, (adapter_ptr_preferred == nullptr) && (target_adapter_deviceid == 0) && (target_desktop_id == -1), "No GPU with desktop outputs was found!");
LOG_IF_F(WARNING, (adapter_ptr_preferred == nullptr) && (target_adapter_deviceid != 0) && (target_desktop_id == -1), "GPU forced by config (DeviceID %u) was not found or has no outputs!", target_adapter_deviceid);
LOG_IF_F(WARNING, (adapter_ptr_preferred == nullptr) && (target_desktop_id != -1), "GPU for target Desktop %i was not found!", target_desktop_id);
LOG_IF_F(WARNING, (adapter_ptr_vr == nullptr) && (target_adapter_deviceid_vr == 0), "GPU requested by SteamVR (%u) was not found!", vr_gpu_id + 1);
LOG_IF_F(WARNING, (adapter_ptr_vr == nullptr) && (target_adapter_deviceid_vr != 0), "VR GPU forced by config (DeviceID %u) was not found!", target_adapter_deviceid_vr);
if (out_adapter_preferred != nullptr)
{
*out_adapter_preferred = adapter_ptr_preferred;
}
if (out_adapter_vr != nullptr)
{
*out_adapter_vr = adapter_ptr_vr;
}
m_InputSim.RefreshScreenOffsets();
ResetMouseLastLaserPointerPos();
return output_id_adapter;
}
void OutputManager::CropToDisplay(int display_id, int& crop_x, int& crop_y, int& crop_width, int& crop_height)
{
if ( (!ConfigManager::GetValue(configid_bool_performance_single_desktop_mirroring)) && (display_id >= 0) && (display_id < m_DesktopRects.size()) )
{
//Individual desktop on full desktop texture
const DPRect& rect = m_DesktopRects[display_id];
crop_x = rect.GetTL().x;
crop_y = rect.GetTL().y;
crop_width = rect.GetWidth();
crop_height = rect.GetHeight();
//Offset by desktop coordinates
crop_x -= m_DesktopX;
crop_y -= m_DesktopY;
}
else //Full desktop
{
crop_x = 0;
crop_y = 0;
crop_width = -1;
crop_height = -1;
}
}
bool OutputManager::CropToActiveWindow(int& crop_x, int& crop_y, int& crop_width, int& crop_height)
{
HWND window_handle = ::GetForegroundWindow();
if (window_handle != nullptr)
{
RECT window_rect = {0};
//Just using GetWindowRect() can include shadows and such, which we don't want
if (::DwmGetWindowAttribute(window_handle, DWMWA_EXTENDED_FRAME_BOUNDS, &window_rect, sizeof(window_rect)) == S_OK)
{
DPRect crop_rect(window_rect.left, window_rect.top, window_rect.right, window_rect.bottom);
crop_rect.Translate({-m_DesktopX, -m_DesktopY}); //Translate crop rect by desktop offset to get desktop-local coordinates
crop_rect.ClipWithFull({0, 0, m_DesktopWidth, m_DesktopHeight}); //Clip to available desktop space
if ((crop_rect.GetWidth() > 0) && (crop_rect.GetHeight() > 0))
{
//Set new crop values
crop_x = crop_rect.GetTL().x;
crop_y = crop_rect.GetTL().y;
crop_width = crop_rect.GetWidth();
crop_height = crop_rect.GetHeight();
return true;
}
}
}
return false;
}
void OutputManager::InitComIfNeeded()
{
if (!m_ComInitDone)
{
if (::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) != RPC_E_CHANGED_MODE)
{
m_ComInitDone = true;
}
}
}
void OutputManager::ConvertOUtoSBS(Overlay& overlay, OUtoSBSConverter& converter)
{
//Convert()'s arguments are almost all stuff from OutputManager, so we take this roundabout way of calling it
const DPRect& crop_rect = overlay.GetValidatedCropRect();
HRESULT hr = converter.Convert(m_Device, m_DeviceContext, m_MultiGPUTargetDevice, m_MultiGPUTargetDeviceContext, m_OvrlTex,
m_DesktopWidth, m_DesktopHeight, crop_rect.GetTL().x, crop_rect.GetTL().y, crop_rect.GetWidth(), crop_rect.GetHeight());
if (hr == S_OK)
{
vr::Texture_t vrtex;
vrtex.eType = vr::TextureType_DirectX;
vrtex.eColorSpace = vr::ColorSpace_Gamma;
vrtex.handle = converter.GetTexture(); //OUtoSBSConverter takes care of multi-gpu support automatically, so no further processing needed
vr::VROverlay()->SetOverlayTexture(overlay.GetHandle(), &vrtex);
}
else
{
ProcessFailure(m_Device, L"Failed to convert OU texture to SBS", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
}
//
// Process both masked and monochrome pointers
//
DUPL_RETURN OutputManager::ProcessMonoMask(bool is_mono, PTR_INFO& ptr_info, int& ptr_width, int& ptr_height, int& ptr_left, int& ptr_top,
Microsoft::WRL::ComPtr& out_tex, DXGI_FORMAT& out_tex_format, D3D11_BOX& box)
{
out_tex_format = DXGI_FORMAT_UNKNOWN;
//ShapeBuffer can sometimes be empty when the secure desktop is active, skip
if (ptr_info.ShapeBuffer.empty())
return DUPL_RETURN_SUCCESS;
//Desktop dimensions
D3D11_TEXTURE2D_DESC FullDesc;
m_SharedSurf->GetDesc(&FullDesc);
int desktop_width = FullDesc.Width;
int desktop_height = FullDesc.Height;
//Pointer position
int ptr_info_pos_left = ptr_info.Position.x;
int ptr_info_pos_top = ptr_info.Position.y;
//Figure out if any adjustment is needed for out of bound positions
if (ptr_info_pos_left < 0)
{
ptr_width = ptr_info_pos_left + (int)ptr_info.ShapeInfo.Width;
}
else if ((ptr_info_pos_left + (int)ptr_info.ShapeInfo.Width) > desktop_width)
{
ptr_width = desktop_height - ptr_info_pos_left;
}
else
{
ptr_width = (int)ptr_info.ShapeInfo.Width;
}
if (is_mono)
{
ptr_info.ShapeInfo.Height = ptr_info.ShapeInfo.Height / 2;
}
if (ptr_info_pos_top < 0)
{
ptr_height = ptr_info_pos_top + (int)ptr_info.ShapeInfo.Height;
}
else if ((ptr_info_pos_top + (int)ptr_info.ShapeInfo.Height) > desktop_height)
{
ptr_height = desktop_height - ptr_info_pos_top;
}
else
{
ptr_height = (int)ptr_info.ShapeInfo.Height;
}
if (is_mono)
{
ptr_info.ShapeInfo.Height = ptr_info.ShapeInfo.Height * 2;
}
ptr_left = (ptr_info_pos_left < 0) ? 0 : ptr_info_pos_left;
ptr_top = (ptr_info_pos_top < 0) ? 0 : ptr_info_pos_top;
// Staging buffer/texture
D3D11_TEXTURE2D_DESC copy_buffer_desc = {};
copy_buffer_desc.Width = ptr_width;
copy_buffer_desc.Height = ptr_height;
copy_buffer_desc.MipLevels = 1;
copy_buffer_desc.ArraySize = 1;
copy_buffer_desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
copy_buffer_desc.SampleDesc.Count = 1;
copy_buffer_desc.SampleDesc.Quality = 0;
copy_buffer_desc.Usage = D3D11_USAGE_STAGING;
copy_buffer_desc.BindFlags = 0;
copy_buffer_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
copy_buffer_desc.MiscFlags = 0;
Microsoft::WRL::ComPtr copy_buffer;
HRESULT hr = m_Device->CreateTexture2D(©_buffer_desc, nullptr, ©_buffer);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed creating staging texture for pointer", L"Desktop+ Error", S_OK, SystemTransitionsExpectedErrors); //Shouldn't be critical
}
//Copy needed part of desktop image
box.left = ptr_left;
box.top = ptr_top;
box.right = ptr_left + ptr_width;
box.bottom = ptr_top + ptr_height;
m_DeviceContext->CopySubresourceRegion(copy_buffer.Get(), 0, 0, 0, 0, m_SharedSurf, 0, &box);
//QI for IDXGISurface
Microsoft::WRL::ComPtr copy_surface;
hr = copy_buffer.As(©_surface);
if (FAILED(hr))
{
return ProcessFailure(nullptr, L"Failed to QI staging texture into IDXGISurface for pointer", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
// Map pixels
DXGI_MAPPED_RECT mapped_surface;
hr = copy_surface->Map(&mapped_surface, DXGI_MAP_READ);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to map surface for pointer", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
//New mouseshape buffer
auto init_buffer = std::unique_ptr{new (std::nothrow)BYTE[ptr_width * ptr_height * 4]};
if (init_buffer == nullptr)
{
return ProcessFailure(nullptr, L"Failed to allocate memory for new mouse shape buffer.", L"Desktop+ Error", E_OUTOFMEMORY);
}
uint32_t* init_buffer_u32 = (uint32_t*)init_buffer.get();
uint32_t* desktop_buffer_u32 = (uint32_t*)mapped_surface.pBits;
int desktop_buffer_pitch = mapped_surface.Pitch / sizeof(uint32_t);
// What to skip (pixel offset)
unsigned int ptr_skip_x = (ptr_info_pos_left < 0) ? (-1 * ptr_info_pos_left) : (0);
unsigned int ptr_skip_y = (ptr_info_pos_top < 0) ? (-1 * ptr_info_pos_top) : (0);
int ptr_height_half = ptr_info.ShapeInfo.Height / 2;
if (is_mono)
{
//Iterate through pointer shape pixels
for (size_t ptr_pixel_row = 0; ptr_pixel_row < ptr_height; ++ptr_pixel_row)
{
//Set mask
uint8_t mask_base = 0x80;
mask_base = mask_base >> (ptr_skip_x % 8);
for (int ptr_pixel_col = 0; ptr_pixel_col < ptr_width; ++ptr_pixel_col)
{
//Get masks using appropriate offsets
size_t mask_offset_base = ((ptr_pixel_col + ptr_skip_x) / 8);
BYTE mask_and = ptr_info.ShapeBuffer[mask_offset_base + ((ptr_pixel_row + ptr_skip_y) * ptr_info.ShapeInfo.Pitch) ] & mask_base;
BYTE mask_xor = ptr_info.ShapeBuffer[mask_offset_base + ((ptr_pixel_row + ptr_skip_y + ptr_height_half) * ptr_info.ShapeInfo.Pitch)] & mask_base;
uint32_t mask_and_u32 = (mask_and) ? 0xFFFFFFFF : 0xFF000000;
uint32_t mask_xor_u32 = (mask_xor) ? 0x00FFFFFF : 0x00000000;
//Set new pixel
init_buffer_u32[(ptr_pixel_row * ptr_width) + ptr_pixel_col] = (desktop_buffer_u32[(ptr_pixel_row * desktop_buffer_pitch) + ptr_pixel_col] & mask_and_u32) ^ mask_xor_u32;
//Adjust mask
mask_base = (mask_base == 0x01) ? 0x80 : mask_base >> 1;
}
}
}
else
{
uint32_t* shape_buffer_u32 = (uint32_t*)ptr_info.ShapeBuffer.data();
//Iterate through pointer shape pixels
for (size_t ptr_pixel_row = 0; ptr_pixel_row < ptr_height; ++ptr_pixel_row)
{
for (size_t ptr_pixel_col = 0; ptr_pixel_col < ptr_width; ++ptr_pixel_col)
{
// Set up mask
uint32_t MaskVal = 0xFF000000 & shape_buffer_u32[(ptr_pixel_col + ptr_skip_x) + ((ptr_pixel_row + ptr_skip_y) * uint32_t(ptr_info.ShapeInfo.Pitch / sizeof(uint32_t)))];
if (MaskVal)
{
//mask_value was 0xFF
init_buffer_u32[(ptr_pixel_row * ptr_width) + ptr_pixel_col] = (desktop_buffer_u32[(ptr_pixel_row * desktop_buffer_pitch) + ptr_pixel_col] ^ shape_buffer_u32[(ptr_pixel_col + ptr_skip_x) +
((ptr_pixel_row + ptr_skip_y) * uint32_t(ptr_info.ShapeInfo.Pitch / sizeof(uint32_t)))]) | 0xFF000000;
}
else
{
//mask_value was 0x00
init_buffer_u32[(ptr_pixel_row * ptr_width) + ptr_pixel_col] = shape_buffer_u32[(ptr_pixel_col + ptr_skip_x) +
((ptr_pixel_row + ptr_skip_y) * uint32_t(ptr_info.ShapeInfo.Pitch / sizeof(uint32_t)))] | 0xFF000000;
}
}
}
}
//Unmap surface
hr = copy_surface->Unmap();
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to unmap surface for pointer", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
//Create texture
D3D11_TEXTURE2D_DESC tex_desc = {};
tex_desc.Width = ptr_width;
tex_desc.Height = ptr_height;
tex_desc.MipLevels = 1;
tex_desc.ArraySize = 1;
tex_desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
tex_desc.SampleDesc.Count = 1;
tex_desc.SampleDesc.Quality = 0;
tex_desc.Usage = D3D11_USAGE_DEFAULT;
tex_desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
tex_desc.CPUAccessFlags = 0;
tex_desc.MiscFlags = 0;
//Set shader resource properties
D3D11_SHADER_RESOURCE_VIEW_DESC srv_desc = {};
srv_desc.Format = tex_desc.Format;
srv_desc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srv_desc.Texture2D.MostDetailedMip = tex_desc.MipLevels - 1;
srv_desc.Texture2D.MipLevels = tex_desc.MipLevels;
//Set up init data
D3D11_SUBRESOURCE_DATA init_data = {};
init_data.pSysMem = init_buffer.get();
init_data.SysMemPitch = ptr_width * 4;
//Create mouse pointer texture
hr = m_Device->CreateTexture2D(&tex_desc, &init_data, &out_tex);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create mouse pointer texture", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
out_tex_format = tex_desc.Format;
return DUPL_RETURN_SUCCESS;
}
DUPL_RETURN OutputManager::ProcessMonoMaskFloat16(bool is_mono, PTR_INFO& ptr_info, int& ptr_width, int& ptr_height, int& ptr_left, int& ptr_top,
Microsoft::WRL::ComPtr& out_tex, DXGI_FORMAT& out_tex_format, D3D11_BOX& box)
{
out_tex_format = DXGI_FORMAT_UNKNOWN;
//ShapeBuffer can sometimes be empty when the secure desktop is active, skip
if (ptr_info.ShapeBuffer.empty())
return DUPL_RETURN_SUCCESS;
//Desktop dimensions
D3D11_TEXTURE2D_DESC FullDesc;
m_SharedSurf->GetDesc(&FullDesc);
int DesktopWidth = FullDesc.Width;
int DesktopHeight = FullDesc.Height;
//Pointer position
int ptr_info_pos_left = ptr_info.Position.x;
int ptr_info_pos_top = ptr_info.Position.y;
//Figure out if any adjustment is needed for out of bound positions
if (ptr_info_pos_left < 0)
{
ptr_width = ptr_info_pos_left + (int)ptr_info.ShapeInfo.Width;
}
else if ((ptr_info_pos_left + (int)ptr_info.ShapeInfo.Width) > DesktopWidth)
{
ptr_width = DesktopWidth - ptr_info_pos_left;
}
else
{
ptr_width = (int)ptr_info.ShapeInfo.Width;
}
if (is_mono)
{
ptr_info.ShapeInfo.Height = ptr_info.ShapeInfo.Height / 2;
}
if (ptr_info_pos_top < 0)
{
ptr_height = ptr_info_pos_top + (int)ptr_info.ShapeInfo.Height;
}
else if ((ptr_info_pos_top + (int)ptr_info.ShapeInfo.Height) > DesktopHeight)
{
ptr_height = DesktopHeight - ptr_info_pos_top;
}
else
{
ptr_height = (int)ptr_info.ShapeInfo.Height;
}
if (is_mono)
{
ptr_info.ShapeInfo.Height = ptr_info.ShapeInfo.Height * 2;
}
ptr_left = (ptr_info_pos_left < 0) ? 0 : ptr_info_pos_left;
ptr_top = (ptr_info_pos_top < 0) ? 0 : ptr_info_pos_top;
//Staging buffer/texture
D3D11_TEXTURE2D_DESC copy_buffer_desc = {};
copy_buffer_desc.Width = ptr_width;
copy_buffer_desc.Height = ptr_height;
copy_buffer_desc.MipLevels = 1;
copy_buffer_desc.ArraySize = 1;
copy_buffer_desc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
copy_buffer_desc.SampleDesc.Count = 1;
copy_buffer_desc.SampleDesc.Quality = 0;
copy_buffer_desc.Usage = D3D11_USAGE_STAGING;
copy_buffer_desc.BindFlags = 0;
copy_buffer_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
copy_buffer_desc.MiscFlags = 0;
Microsoft::WRL::ComPtr copy_buffer;
HRESULT hr = m_Device->CreateTexture2D(©_buffer_desc, nullptr, ©_buffer);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed creating staging texture for pointer", L"Desktop+ Error", S_OK, SystemTransitionsExpectedErrors); //Shouldn't be critical
}
//Copy needed part of desktop image
box.left = ptr_left;
box.top = ptr_top;
box.right = ptr_left + ptr_width;
box.bottom = ptr_top + ptr_height;
m_DeviceContext->CopySubresourceRegion(copy_buffer.Get(), 0, 0, 0, 0, m_SharedSurf, 0, &box);
//QI for IDXGISurface
Microsoft::WRL::ComPtr copy_surface;
hr = copy_buffer.As(©_surface);
if (FAILED(hr))
{
return ProcessFailure(nullptr, L"Failed to QI staging texture into IDXGISurface for pointer", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
//Map pixels
DXGI_MAPPED_RECT mapped_surface;
hr = copy_surface->Map(&mapped_surface, DXGI_MAP_READ);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to map surface for pointer", L"Error", hr, SystemTransitionsExpectedErrors);
}
//New mouseshape buffer
auto init_buffer = std::unique_ptr{new (std::nothrow)BYTE[ptr_width * ptr_height * 4 * sizeof(PackedVector::HALF)]};
if (init_buffer == nullptr)
{
return ProcessFailure(nullptr, L"Failed to allocate memory for new mouse shape buffer.", L"Desktop+ Error", E_OUTOFMEMORY);
}
PackedVector::HALF* init_buffer_f16 = (PackedVector::HALF*)init_buffer.get();
PackedVector::HALF* desktop_buffer_f16 = (PackedVector::HALF*)mapped_surface.pBits;
int desktop_buffer_pitch = mapped_surface.Pitch / sizeof(PackedVector::HALF);
//What to skip (pixel offset)
unsigned int ptr_skip_x = (ptr_info_pos_left < 0) ? (-1 * ptr_info_pos_left) : (0);
unsigned int ptr_skip_y = (ptr_info_pos_top < 0) ? (-1 * ptr_info_pos_top) : (0);
int ptr_height_half = ptr_info.ShapeInfo.Height / 2;
//While the float value of SDR white may not be 1.0 depending on OS and system settings, the cursor texture is always 8-bit per channel
//This might not be 100% accurate, but masked color cursors are also very rare, so we mostly care about XOR negative color effects working
const float sdr_white_level_adjustment = (m_DesktopHDRWhiteLevelAdjustments.empty()) ? 1.0f :
m_DesktopHDRWhiteLevelAdjustments[clamp((size_t)ptr_info.WhoUpdatedPositionLast, (size_t)0, m_DesktopHDRWhiteLevelAdjustments.size()-1)];
if (is_mono)
{
//Iterate through pointer shape pixels
for (size_t ptr_pixel_row = 0; ptr_pixel_row < ptr_height; ++ptr_pixel_row)
{
//Set mask
uint8_t mask_base = 0x80;
mask_base = mask_base >> (ptr_skip_x % 8);
for (size_t ptr_pixel_col = 0; ptr_pixel_col < ptr_width; ++ptr_pixel_col)
{
const int offset_in = (ptr_pixel_row * desktop_buffer_pitch) + (ptr_pixel_col * 4);
const int offset_out = ((ptr_pixel_row * ptr_width) + (ptr_pixel_col)) * 4;
//Get masks using appropriate offsets
size_t mask_offset_base = ((ptr_pixel_col + ptr_skip_x) / 8);
BYTE mask_and = ptr_info.ShapeBuffer[mask_offset_base + ((ptr_pixel_row + ptr_skip_y) * ptr_info.ShapeInfo.Pitch) ] & mask_base;
BYTE mask_xor = ptr_info.ShapeBuffer[mask_offset_base + ((ptr_pixel_row + ptr_skip_y + ptr_height_half) * ptr_info.ShapeInfo.Pitch)] & mask_base;
//Set new pixel
float f32_value_r = (mask_and) ? PackedVector::XMConvertHalfToFloat(desktop_buffer_f16[offset_in]) : 0.0f;
float f32_value_g = (mask_and) ? PackedVector::XMConvertHalfToFloat(desktop_buffer_f16[offset_in + 1]) : 0.0f;
float f32_value_b = (mask_and) ? PackedVector::XMConvertHalfToFloat(desktop_buffer_f16[offset_in + 2]) : 0.0f;
float f32_value_a = (mask_and) ? PackedVector::XMConvertHalfToFloat(desktop_buffer_f16[offset_in + 3]) : 0.0f;
if (mask_xor)
{
//Approximation for XOR negative color effect in non-linear space
const float xor_neg = 0.77f / sdr_white_level_adjustment;
init_buffer_f16[offset_out] = PackedVector::XMConvertFloatToHalf( std::max(xor_neg - f32_value_r, 0.0f) );
init_buffer_f16[offset_out + 1] = PackedVector::XMConvertFloatToHalf( std::max(xor_neg - f32_value_g, 0.0f) );
init_buffer_f16[offset_out + 2] = PackedVector::XMConvertFloatToHalf( std::max(xor_neg - f32_value_b, 0.0f) );
}
else
{
init_buffer_f16[offset_out] = PackedVector::XMConvertFloatToHalf(f32_value_r);
init_buffer_f16[offset_out + 1] = PackedVector::XMConvertFloatToHalf(f32_value_g);
init_buffer_f16[offset_out + 2] = PackedVector::XMConvertFloatToHalf(f32_value_b);
}
init_buffer_f16[offset_out + 3] = desktop_buffer_f16[offset_in + 3];
//Adjust mask
mask_base = (mask_base == 0x01) ? 0x80 : mask_base >> 1;
}
}
}
else
{
uint32_t* shape_buffer_u32 = (uint32_t*)ptr_info.ShapeBuffer.data();
//Iterate through pointer shape pixels
for (size_t ptr_pixel_row = 0; ptr_pixel_row < ptr_height; ++ptr_pixel_row)
{
for (size_t ptr_pixel_col = 0; ptr_pixel_col < ptr_width; ++ptr_pixel_col)
{
// Set up mask
const int offset_in = (ptr_pixel_row * desktop_buffer_pitch) + (ptr_pixel_col * 4);
const int offset_out = ((ptr_pixel_row * ptr_width) + (ptr_pixel_col)) * 4;
uint32_t mask_value = 0xFF000000 & shape_buffer_u32[(ptr_pixel_col + ptr_skip_x) + ((ptr_pixel_row + ptr_skip_y) * uint32_t(ptr_info.ShapeInfo.Pitch / sizeof(uint32_t)))];
if (mask_value)
{
//mask_value was 0xFF
//Cast float values to regular RGB ones and XOR them as intended (though this is still in linear color space)
float f32_value_r = PackedVector::XMConvertHalfToFloat(desktop_buffer_f16[offset_in]);
float f32_value_g = PackedVector::XMConvertHalfToFloat(desktop_buffer_f16[offset_in + 1]);
float f32_value_b = PackedVector::XMConvertHalfToFloat(desktop_buffer_f16[offset_in + 2]);
float f32_value_a = PackedVector::XMConvertHalfToFloat(desktop_buffer_f16[offset_in + 3]);
uint32_t u32_value_r = f32_value_r * 255.0f * sdr_white_level_adjustment;
uint32_t u32_value_g = f32_value_g * 255.0f * sdr_white_level_adjustment;
uint32_t u32_value_b = f32_value_b * 255.0f * sdr_white_level_adjustment;
uint32_t ptr_rgba = shape_buffer_u32[(ptr_pixel_col + ptr_skip_x) + ((ptr_pixel_row + ptr_skip_y) * uint32_t(ptr_info.ShapeInfo.Pitch / sizeof(uint32_t)))];
uint32_t u32_buff_r = (ptr_rgba >> 16) & 0xFF;
uint32_t u32_buff_g = (ptr_rgba >> 8) & 0xFF;
uint32_t u32_buff_b = ptr_rgba & 0xFF;
u32_value_r ^= u32_buff_r;
u32_value_g ^= u32_buff_g;
u32_value_b ^= u32_buff_b;
//Cast them back again
f32_value_r = u32_value_r / 255.0f / sdr_white_level_adjustment;
f32_value_g = u32_value_g / 255.0f / sdr_white_level_adjustment;
f32_value_b = u32_value_b / 255.0f / sdr_white_level_adjustment;
init_buffer_f16[offset_out] = PackedVector::XMConvertFloatToHalf(f32_value_r);
init_buffer_f16[offset_out + 1] = PackedVector::XMConvertFloatToHalf(f32_value_g);
init_buffer_f16[offset_out + 2] = PackedVector::XMConvertFloatToHalf(f32_value_b);
init_buffer_f16[offset_out + 3] = desktop_buffer_f16[offset_in + 3];
}
else
{
//mask_value was 0x00
uint32_t ptr_rgb = shape_buffer_u32[(ptr_pixel_col + ptr_skip_x) + ((ptr_pixel_row + ptr_skip_y) * uint32_t(ptr_info.ShapeInfo.Pitch / sizeof(UINT)))];
float f32_value_r = ((ptr_rgb >> 16) & 0xFF) / 255.0f / sdr_white_level_adjustment;
float f32_value_g = ((ptr_rgb >> 8) & 0xFF) / 255.0f / sdr_white_level_adjustment;
float f32_value_b = (ptr_rgb & 0xFF) / 255.0f / sdr_white_level_adjustment;
init_buffer_f16[offset_out] = PackedVector::XMConvertFloatToHalf( f32_value_r );
init_buffer_f16[offset_out + 1] = PackedVector::XMConvertFloatToHalf( f32_value_g );
init_buffer_f16[offset_out + 2] = PackedVector::XMConvertFloatToHalf( f32_value_b );
init_buffer_f16[offset_out + 3] = desktop_buffer_f16[offset_in + 3];
}
}
}
}
//Unmap surface
hr = copy_surface->Unmap();
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to unmap surface for pointer", L"Error", hr, SystemTransitionsExpectedErrors);
}
//Create texture
D3D11_TEXTURE2D_DESC tex_desc = {};
tex_desc.Width = ptr_width;
tex_desc.Height = ptr_height;
tex_desc.MipLevels = 1;
tex_desc.ArraySize = 1;
tex_desc.Format = DXGI_FORMAT_R16G16B16A16_FLOAT;
tex_desc.SampleDesc.Count = 1;
tex_desc.SampleDesc.Quality = 0;
tex_desc.Usage = D3D11_USAGE_DEFAULT;
tex_desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
tex_desc.CPUAccessFlags = 0;
tex_desc.MiscFlags = 0;
//Set shader resource properties
D3D11_SHADER_RESOURCE_VIEW_DESC srv_desc = {};
srv_desc.Format = tex_desc.Format;
srv_desc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
srv_desc.Texture2D.MostDetailedMip = tex_desc.MipLevels - 1;
srv_desc.Texture2D.MipLevels = tex_desc.MipLevels;
//Set up init data
D3D11_SUBRESOURCE_DATA init_data = {};
init_data.pSysMem = init_buffer.get();
init_data.SysMemPitch = ptr_width * 4 * (int)sizeof(PackedVector::HALF);
//Create mouse pointer texture
hr = m_Device->CreateTexture2D(&tex_desc, &init_data, &out_tex);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create mouse pointer texture", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
out_tex_format = tex_desc.Format;
return DUPL_RETURN_SUCCESS;
}
//
// Reset render target view
//
DUPL_RETURN OutputManager::MakeRTV()
{
D3D11_TEXTURE2D_DESC desc_ovrl_tex;
m_OvrlTex->GetDesc(&desc_ovrl_tex);
//Create render target view for overlay texture
D3D11_RENDER_TARGET_VIEW_DESC ovrl_tex_rtv_desc = {};
ovrl_tex_rtv_desc.Format = desc_ovrl_tex.Format;
ovrl_tex_rtv_desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
ovrl_tex_rtv_desc.Texture2D.MipSlice = 0;
m_Device->CreateRenderTargetView(m_OvrlTex, &ovrl_tex_rtv_desc, &m_OvrlRTV);
return DUPL_RETURN_SUCCESS;
}
//
// Initialize shaders for drawing
//
DUPL_RETURN OutputManager::InitShaders()
{
HRESULT hr;
UINT Size = ARRAYSIZE(g_VS);
hr = m_Device->CreateVertexShader(g_VS, Size, nullptr, &m_VertexShader);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create vertex shader", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
D3D11_INPUT_ELEMENT_DESC Layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }
};
UINT NumElements = ARRAYSIZE(Layout);
hr = m_Device->CreateInputLayout(Layout, NumElements, g_VS, Size, &m_InputLayout);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create input layout", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
m_DeviceContext->IASetInputLayout(m_InputLayout);
Size = ARRAYSIZE(g_PS);
hr = m_Device->CreatePixelShader(g_PS, Size, nullptr, &m_PixelShader);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create pixel shader", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
Size = ARRAYSIZE(g_PSCURSOR);
hr = m_Device->CreatePixelShader(g_PSCURSOR, Size, nullptr, &m_PixelShaderCursor);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create cursor pixel shader", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
return DUPL_RETURN_SUCCESS;
}
//
// Recreate textures
//
DUPL_RETURN OutputManager::CreateTextures(INT SingleOutput, _Out_ UINT* OutCount, _Out_ RECT* DeskBounds)
{
HRESULT hr;
*OutCount = 0;
const int desktop_count = m_DesktopRects.size();
//Output doesn't exist. This will result in a soft-error invalid output state (system may be in an transition state, in which case we'll automatically recover)
if (SingleOutput >= desktop_count)
{
m_DesktopX = 0;
m_DesktopY = 0;
m_DesktopWidth = -1;
m_DesktopHeight = -1;
return DUPL_RETURN_ERROR_EXPECTED;
}
//Figure out right dimensions for full size desktop texture
DPRect output_rect_total;
if (SingleOutput < 0)
{
//Combined desktop, also count desktops on the used adapter
Microsoft::WRL::ComPtr adapter_ptr;
adapter_ptr.Attach(GetDXGIAdapter());
UINT output_index_adapter = 0;
hr = S_OK;
while (SUCCEEDED(hr))
{
//Break early if used desktop count is lower than actual output count
if (output_index_adapter >= desktop_count)
{
++output_index_adapter;
break;
}
Microsoft::WRL::ComPtr output_ptr;
hr = adapter_ptr->EnumOutputs(output_index_adapter, &output_ptr);
if ((output_ptr != nullptr) && (hr != DXGI_ERROR_NOT_FOUND))
{
DXGI_OUTPUT_DESC output_desc;
output_ptr->GetDesc(&output_desc);
DPRect output_rect(output_desc.DesktopCoordinates.left, output_desc.DesktopCoordinates.top,
output_desc.DesktopCoordinates.right, output_desc.DesktopCoordinates.bottom);
(output_rect_total.GetWidth() == 0) ? output_rect_total = output_rect : output_rect_total.Add(output_rect);
}
++output_index_adapter;
}
*OutCount = output_index_adapter - 1;
}
else
{
//Single desktop, grab cached desktop rect
if (SingleOutput < desktop_count)
{
output_rect_total = m_DesktopRects[SingleOutput];
*OutCount = 1;
}
}
//Store size and position
m_DesktopX = output_rect_total.GetTL().x;
m_DesktopY = output_rect_total.GetTL().y;
m_DesktopWidth = output_rect_total.GetWidth();
m_DesktopHeight = output_rect_total.GetHeight();
DeskBounds->left = output_rect_total.GetTL().x;
DeskBounds->top = output_rect_total.GetTL().y;
DeskBounds->right = output_rect_total.GetBR().x;
DeskBounds->bottom = output_rect_total.GetBR().y;
//Set it as mouse scale on the desktop texture overlay for the UI to read the resolution from there
vr::HmdVector2_t mouse_scale = {0};
mouse_scale.v[0] = m_DesktopWidth;
mouse_scale.v[1] = m_DesktopHeight;
vr::VROverlay()->SetOverlayMouseScale(m_OvrlHandleDesktopTexture, &mouse_scale);
//Create shared texture for all duplication threads to draw into
D3D11_TEXTURE2D_DESC TexD;
RtlZeroMemory(&TexD, sizeof(D3D11_TEXTURE2D_DESC));
TexD.Width = m_DesktopWidth;
TexD.Height = m_DesktopHeight;
TexD.MipLevels = 1;
TexD.ArraySize = 1;
TexD.Format = ((m_OutputHDRAvailable) && (ConfigManager::GetValue(configid_bool_performance_hdr_mirroring))) ? DXGI_FORMAT_R16G16B16A16_FLOAT : DXGI_FORMAT_B8G8R8A8_UNORM;
TexD.SampleDesc.Count = 1;
TexD.Usage = D3D11_USAGE_DEFAULT;
TexD.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
TexD.CPUAccessFlags = 0;
TexD.MiscFlags = D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX;
hr = m_Device->CreateTexture2D(&TexD, nullptr, &m_SharedSurf);
if (!FAILED(hr))
{
TexD.MiscFlags = 0;
hr = m_Device->CreateTexture2D(&TexD, nullptr, &m_OvrlTex);
}
if (FAILED(hr))
{
if (output_rect_total.GetWidth() != 0)
{
// If we are duplicating the complete desktop we try to create a single texture to hold the
// complete desktop image and blit updates from the per output DDA interface. The GPU can
// always support a texture size of the maximum resolution of any single output but there is no
// guarantee that it can support a texture size of the desktop.
return ProcessFailure(m_Device, L"Failed to create shared texture. Combined desktop texture size may be larger than the maximum supported supported size of the GPU", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
else
{
return ProcessFailure(m_Device, L"Failed to create shared texture", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
}
// Get keyed mutex
hr = m_SharedSurf->QueryInterface(__uuidof(IDXGIKeyedMutex), reinterpret_cast(&m_KeyMutex));
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to query for keyed mutex", L"Desktop+ Error", hr);
}
//Create shader resource for shared texture
D3D11_TEXTURE2D_DESC FrameDesc;
m_SharedSurf->GetDesc(&FrameDesc);
D3D11_SHADER_RESOURCE_VIEW_DESC ShaderDesc;
ShaderDesc.Format = FrameDesc.Format;
ShaderDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
ShaderDesc.Texture2D.MostDetailedMip = FrameDesc.MipLevels - 1;
ShaderDesc.Texture2D.MipLevels = FrameDesc.MipLevels;
// Create new shader resource view
hr = m_Device->CreateShaderResourceView(m_SharedSurf, &ShaderDesc, &m_ShaderResource);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create shader resource", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
//Create textures for multi GPU handling if needed
if (m_MultiGPUTargetDevice != nullptr)
{
//Staging texture
TexD.Usage = D3D11_USAGE_STAGING;
TexD.BindFlags = 0;
TexD.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
TexD.MiscFlags = 0;
hr = m_Device->CreateTexture2D(&TexD, nullptr, &m_MultiGPUTexStaging);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create staging texture", L"Desktop+ Error", hr);
}
//Copy-target texture
TexD.Usage = D3D11_USAGE_DYNAMIC;
TexD.BindFlags = D3D11_BIND_SHADER_RESOURCE;
TexD.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
TexD.MiscFlags = 0;
hr = m_MultiGPUTargetDevice->CreateTexture2D(&TexD, nullptr, &m_MultiGPUTexTarget);
if (FAILED(hr))
{
return ProcessFailure(m_MultiGPUTargetDevice, L"Failed to create copy-target texture", L"Desktop+ Error", hr);
}
}
return DUPL_RETURN_SUCCESS;
}
void OutputManager::DrawFrameToOverlayTex(bool clear_rtv)
{
//Do a straight copy if there are no issues with that or do the alpha check if it's still pending
if ((!m_OutputAlphaCheckFailed) || (m_OutputAlphaChecksPending > 0))
{
m_DeviceContext->CopyResource(m_OvrlTex, m_SharedSurf);
if (m_OutputAlphaChecksPending > 0)
{
//Check for translucent pixels (not fast)
m_OutputAlphaCheckFailed = DesktopTextureAlphaCheck();
m_OutputAlphaChecksPending--;
LOG_IF_F(WARNING, (m_OutputAlphaCheckFailed) && (m_OutputAlphaChecksPending == 0), "Failed Desktop Duplication alpha check, using extra render pass");
}
}
//Draw the frame to the texture with the alpha channel fixing pixel shader if we have to
if (m_OutputAlphaCheckFailed)
{
// Set resources
UINT Stride = sizeof(VERTEX);
UINT Offset = 0;
const FLOAT blendFactor[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
m_DeviceContext->OMSetBlendState(nullptr, blendFactor, 0xffffffff);
m_DeviceContext->OMSetRenderTargets(1, &m_OvrlRTV, nullptr);
m_DeviceContext->VSSetShader(m_VertexShader, nullptr, 0);
m_DeviceContext->PSSetShader(m_PixelShader, nullptr, 0);
m_DeviceContext->PSSetShaderResources(0, 1, &m_ShaderResource);
m_DeviceContext->PSSetSamplers(0, 1, &m_Sampler);
m_DeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
m_DeviceContext->IASetVertexBuffers(0, 1, &m_VertexBuffer, &Stride, &Offset);
// Draw textured quad onto render target
if (clear_rtv)
{
const float bgColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f };
m_DeviceContext->ClearRenderTargetView(m_OvrlRTV, bgColor);
}
m_DeviceContext->Draw(NUMVERTICES, 0);
}
}
//
// Draw mouse provided in buffer to overlay texture
//
DUPL_RETURN OutputManager::DrawMouseToOverlayTex(_In_ PTR_INFO* PtrInfo)
{
//Just return if we don't need to render it
if ((!ConfigManager::GetValue(configid_bool_input_mouse_render_cursor)) || (!PtrInfo->Visible))
{
return DUPL_RETURN_SUCCESS;
}
ID3D11Buffer* VertexBuffer = nullptr;
// Vars to be used
D3D11_SUBRESOURCE_DATA InitData = {};
D3D11_TEXTURE2D_DESC Desc = {};
D3D11_SHADER_RESOURCE_VIEW_DESC SDesc = {};
// Position will be changed based on mouse position
VERTEX Vertices[NUMVERTICES] =
{
{ XMFLOAT3(-1.0f, -1.0f, 0), XMFLOAT2(0.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, 0), XMFLOAT2(0.0f, 0.0f) },
{ XMFLOAT3(1.0f, -1.0f, 0), XMFLOAT2(1.0f, 1.0f) },
{ XMFLOAT3(1.0f, -1.0f, 0), XMFLOAT2(1.0f, 1.0f) },
{ XMFLOAT3(-1.0f, 1.0f, 0), XMFLOAT2(0.0f, 0.0f) },
{ XMFLOAT3(1.0f, 1.0f, 0), XMFLOAT2(1.0f, 0.0f) },
};
// Center of desktop dimensions
FLOAT CenterX = (m_DesktopWidth / 2.0f);
FLOAT CenterY = (m_DesktopHeight / 2.0f);
// Clipping adjusted coordinates / dimensions
INT PtrWidth = 0;
INT PtrHeight = 0;
INT PtrLeft = 0;
INT PtrTop = 0;
// Buffer used if necessary (in case of monochrome or masked pointer)
Microsoft::WRL::ComPtr CursorTexNew;
DXGI_FORMAT CursorTexNewFormat = DXGI_FORMAT_UNKNOWN;
// Used for copying pixels if necessary
D3D11_BOX Box = {};
Box.front = 0;
Box.back = 1;
//Process shape (or just get position when not new cursor)
switch (PtrInfo->ShapeInfo.Type)
{
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR:
{
PtrLeft = PtrInfo->Position.x;
PtrTop = PtrInfo->Position.y;
PtrWidth = static_cast(PtrInfo->ShapeInfo.Width);
PtrHeight = static_cast(PtrInfo->ShapeInfo.Height);
break;
}
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME:
case DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MASKED_COLOR:
{
PtrInfo->CursorShapeChanged = true; //Texture content is screen dependent
const bool is_mono_cursor = (PtrInfo->ShapeInfo.Type == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME);
//Process for HDR is needed
if ((m_OutputHDRAvailable) && (ConfigManager::GetValue(configid_bool_performance_hdr_mirroring)))
{
ProcessMonoMaskFloat16(is_mono_cursor, *PtrInfo, PtrWidth, PtrHeight, PtrLeft, PtrTop, CursorTexNew, CursorTexNewFormat, Box);
}
else
{
ProcessMonoMask(is_mono_cursor, *PtrInfo, PtrWidth, PtrHeight, PtrLeft, PtrTop, CursorTexNew, CursorTexNewFormat, Box);
}
break;
}
default: break;
}
if (m_MouseCursorNeedsUpdate)
{
PtrInfo->CursorShapeChanged = true;
}
// VERTEX creation
Vertices[0].Pos.x = (PtrLeft - CenterX) / CenterX;
Vertices[0].Pos.y = -1 * ((PtrTop + PtrHeight) - CenterY) / CenterY;
Vertices[1].Pos.x = (PtrLeft - CenterX) / CenterX;
Vertices[1].Pos.y = -1 * (PtrTop - CenterY) / CenterY;
Vertices[2].Pos.x = ((PtrLeft + PtrWidth) - CenterX) / CenterX;
Vertices[2].Pos.y = -1 * ((PtrTop + PtrHeight) - CenterY) / CenterY;
Vertices[3].Pos.x = Vertices[2].Pos.x;
Vertices[3].Pos.y = Vertices[2].Pos.y;
Vertices[4].Pos.x = Vertices[1].Pos.x;
Vertices[4].Pos.y = Vertices[1].Pos.y;
Vertices[5].Pos.x = ((PtrLeft + PtrWidth) - CenterX) / CenterX;
Vertices[5].Pos.y = -1 * (PtrTop - CenterY) / CenterY;
//Vertex buffer description
D3D11_BUFFER_DESC BDesc;
ZeroMemory(&BDesc, sizeof(D3D11_BUFFER_DESC));
BDesc.Usage = D3D11_USAGE_DEFAULT;
BDesc.ByteWidth = sizeof(VERTEX) * NUMVERTICES;
BDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
BDesc.CPUAccessFlags = 0;
ZeroMemory(&InitData, sizeof(D3D11_SUBRESOURCE_DATA));
InitData.pSysMem = Vertices;
// Create vertex buffer
HRESULT hr = m_Device->CreateBuffer(&BDesc, &InitData, &VertexBuffer);
if (FAILED(hr))
{
m_MouseShaderRes.Reset();
m_MouseTex.Reset();
return ProcessFailure(m_Device, L"Failed to create mouse pointer vertex buffer in OutputManager", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
//It can occasionally happen that no cursor shape update is detected after resetting duplication, so the m_MouseTex check is more of a workaround, but unproblematic
if ( (PtrInfo->CursorShapeChanged) || (m_MouseTex == nullptr) )
{
//Only create a texture here for regular color cursors (mask/mono were already created)
if (PtrInfo->ShapeInfo.Type == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR)
{
Desc.Width = PtrWidth;
Desc.Height = PtrHeight;
Desc.MipLevels = 1;
Desc.ArraySize = 1;
Desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
Desc.SampleDesc.Count = 1;
Desc.SampleDesc.Quality = 0;
Desc.Usage = D3D11_USAGE_DEFAULT;
Desc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
Desc.CPUAccessFlags = 0;
Desc.MiscFlags = 0;
//Set up init data
InitData.pSysMem = PtrInfo->ShapeBuffer.data();
InitData.SysMemPitch = PtrInfo->ShapeInfo.Pitch;
InitData.SysMemSlicePitch = 0;
// Create mouseshape as texture
hr = m_Device->CreateTexture2D(&Desc, &InitData, &m_MouseTex);
if (FAILED(hr))
{
return ProcessFailure(m_Device, L"Failed to create mouse pointer texture", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
}
else
{
m_MouseTex = CursorTexNew;
}
if (m_MouseTex != nullptr)
{
//Set shader resource properties
SDesc.Format = (PtrInfo->ShapeInfo.Type == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR) ? DXGI_FORMAT_B8G8R8A8_UNORM : CursorTexNewFormat;
SDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
SDesc.Texture2D.MostDetailedMip = 0;
SDesc.Texture2D.MipLevels = 1;
// Create shader resource from texture
hr = m_Device->CreateShaderResourceView(m_MouseTex.Get(), &SDesc, &m_MouseShaderRes);
if (FAILED(hr))
{
m_MouseTex.Reset();
return ProcessFailure(m_Device, L"Failed to create shader resource from mouse pointer texture", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
}
}
// Set resources
FLOAT BlendFactor[4] = { 0.f, 0.f, 0.f, 0.f };
UINT Stride = sizeof(VERTEX);
UINT Offset = 0;
m_DeviceContext->IASetVertexBuffers(0, 1, &VertexBuffer, &Stride, &Offset);
m_DeviceContext->OMSetBlendState(m_BlendState, BlendFactor, 0xFFFFFFFF);
m_DeviceContext->OMSetRenderTargets(1, &m_OvrlRTV, nullptr);
m_DeviceContext->VSSetShader(m_VertexShader, nullptr, 0);
m_DeviceContext->PSSetShader(m_PixelShaderCursor, nullptr, 0);
m_DeviceContext->PSSetShaderResources(0, 1, m_MouseShaderRes.GetAddressOf());
m_DeviceContext->PSSetSamplers(0, 1, &m_Sampler);
// Draw
m_DeviceContext->Draw(NUMVERTICES, 0);
// Clean
if (VertexBuffer)
{
VertexBuffer->Release();
VertexBuffer = nullptr;
}
m_MouseCursorNeedsUpdate = false;
return DUPL_RETURN_SUCCESS;
}
DUPL_RETURN_UPD OutputManager::RefreshOpenVROverlayTexture(DPRect& DirtyRectTotal, bool force_full_copy)
{
if ((m_OvrlHandleDesktopTexture != vr::k_ulOverlayHandleInvalid) && (m_OvrlTex))
{
vr::Texture_t vrtex;
vrtex.eType = vr::TextureType_DirectX;
vrtex.eColorSpace = ((m_OutputHDRAvailable) && (ConfigManager::GetValue(configid_bool_performance_hdr_mirroring))) ? vr::ColorSpace_Linear : vr::ColorSpace_Gamma;
vrtex.handle = m_OvrlTex;
//The intermediate texture can be assumed to be not complete when a full copy is forced, so redraw that
if (force_full_copy)
{
//Try to acquire sync for shared surface needed by DrawFrameToOverlayTex()
HRESULT hr = m_KeyMutex->AcquireSync(0, m_MaxActiveRefreshDelay);
if (hr == static_cast(WAIT_TIMEOUT))
{
//Another thread has the keyed mutex so there will be a new frame ready after this.
//Bail out and just set the pending dirty region to full so everything gets drawn over on the next update
m_OutputPendingDirtyRect = {0, 0, m_DesktopWidth, m_DesktopHeight};
return DUPL_RETURN_UPD_RETRY;
}
else if (FAILED(hr))
{
return (DUPL_RETURN_UPD)ProcessFailure(m_Device, L"Failed to acquire keyed mutex", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
DrawFrameToOverlayTex(true);
//Release keyed mutex
hr = m_KeyMutex->ReleaseSync(0);
if (FAILED(hr))
{
return (DUPL_RETURN_UPD)ProcessFailure(m_Device, L"Failed to Release keyed mutex", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
//We don't draw the cursor here as this can lead to tons of issues for little gain. We might not even know what the cursor looks like if it was cropped out previously, etc.
//We do mark where the cursor has last been seen as pending dirty region, however, so it gets updated at the next best moment even if it didn't move
if (m_MouseLastInfo.Visible)
{
m_OutputPendingDirtyRect = { m_MouseLastInfo.Position.x, m_MouseLastInfo.Position.y, int(m_MouseLastInfo.Position.x + m_MouseLastInfo.ShapeInfo.Width),
int(m_MouseLastInfo.Position.y + m_MouseLastInfo.ShapeInfo.Height) };
}
}
//Copy texture over to GPU connected to VR HMD if needed
if (m_MultiGPUTargetDevice != nullptr)
{
//This isn't very fast but the only way to my knowledge. Happy to receive improvements on this though
m_DeviceContext->CopyResource(m_MultiGPUTexStaging, m_OvrlTex);
D3D11_MAPPED_SUBRESOURCE mapped_resource_staging;
RtlZeroMemory(&mapped_resource_staging, sizeof(D3D11_MAPPED_SUBRESOURCE));
HRESULT hr = m_DeviceContext->Map(m_MultiGPUTexStaging, 0, D3D11_MAP_READ, 0, &mapped_resource_staging);
if (FAILED(hr))
{
return (DUPL_RETURN_UPD)ProcessFailure(m_Device, L"Failed to map staging texture", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
D3D11_MAPPED_SUBRESOURCE mapped_resource_target;
RtlZeroMemory(&mapped_resource_target, sizeof(D3D11_MAPPED_SUBRESOURCE));
hr = m_MultiGPUTargetDeviceContext->Map(m_MultiGPUTexTarget, 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped_resource_target);
if (FAILED(hr))
{
return (DUPL_RETURN_UPD)ProcessFailure(m_MultiGPUTargetDevice, L"Failed to map copy-target texture", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
memcpy(mapped_resource_target.pData, mapped_resource_staging.pData, m_DesktopHeight * mapped_resource_staging.RowPitch);
m_DeviceContext->Unmap(m_MultiGPUTexStaging, 0);
m_MultiGPUTargetDeviceContext->Unmap(m_MultiGPUTexTarget, 0);
vrtex.handle = m_MultiGPUTexTarget;
}
//Do a simple full copy (done below) if the rect covers the whole texture (this isn't slower than a full rect copy and works with size changes)
force_full_copy = ( (force_full_copy) || (DirtyRectTotal.Contains({0, 0, m_DesktopWidth, m_DesktopHeight})) );
if (!force_full_copy) //Otherwise do a partial copy
{
//Get overlay texture from OpenVR and copy dirty rect directly into it
ID3D11Texture2D* reference_texture = (ID3D11Texture2D*)vrtex.handle;
ID3D11ShaderResourceView* ovrl_shader_rsv;
ovrl_shader_rsv = vr::VROverlayEx()->GetOverlayTextureEx(m_OvrlHandleDesktopTexture, reference_texture);
if (ovrl_shader_rsv != nullptr)
{
ID3D11DeviceContext* device_context = (m_MultiGPUTargetDevice != nullptr) ? m_MultiGPUTargetDeviceContext : m_DeviceContext;
Microsoft::WRL::ComPtr ovrl_tex;
ovrl_shader_rsv->GetResource(&ovrl_tex);
D3D11_BOX box = {0};
box.left = DirtyRectTotal.GetTL().x;
box.top = DirtyRectTotal.GetTL().y;
box.front = 0;
box.right = DirtyRectTotal.GetBR().x;
box.bottom = DirtyRectTotal.GetBR().y;
box.back = 1;
device_context->CopySubresourceRegion(ovrl_tex.Get(), 0, box.left, box.top, 0, reference_texture, 0, &box);
//RSV is kept around by IVROverlayEx and not released here
}
else //Usually shouldn't fail, but fall back to full copy then
{
force_full_copy = true;
}
}
if (force_full_copy) //This is down here so a failed partial copy is picked up as well
{
bool refresh_shared_texture = false;
vr::VROverlayEx()->SetOverlayTextureEx(m_OvrlHandleDesktopTexture, &vrtex, {m_DesktopWidth, m_DesktopHeight}, &refresh_shared_texture);
//Apply potential texture change to all overlays and notify them of duplication update
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
Overlay& overlay = OverlayManager::Get().GetOverlay(i);
if (refresh_shared_texture)
{
overlay.AssignDesktopDuplicationTexture();
}
overlay.OnDesktopDuplicationUpdate();
}
}
else
{
//Notifiy all overlays of duplication update
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayManager::Get().GetOverlay(i).OnDesktopDuplicationUpdate();
}
}
}
return DUPL_RETURN_UPD_SUCCESS_REFRESHED_OVERLAY;
}
bool OutputManager::DesktopTextureAlphaCheck()
{
if (m_DesktopRects.empty())
return false;
//Sanity check texture dimensions
D3D11_TEXTURE2D_DESC desc_ovrl_tex;
m_OvrlTex->GetDesc(&desc_ovrl_tex);
if ( ((UINT)m_DesktopWidth != desc_ovrl_tex.Width) || ((UINT)m_DesktopHeight != desc_ovrl_tex.Height) )
return false;
//Read one pixel for each desktop
const int pixel_count = m_DesktopRects.size();
//Create a staging texture
D3D11_TEXTURE2D_DESC desc = {0};
desc.Width = pixel_count;
desc.Height = 1;
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = desc_ovrl_tex.Format;
desc.SampleDesc.Count = 1;
desc.SampleDesc.Quality = 0;
desc.Usage = D3D11_USAGE_STAGING;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
desc.BindFlags = 0;
desc.MiscFlags = 0;
Microsoft::WRL::ComPtr tex_staging;
HRESULT hr = m_Device->CreateTexture2D(&desc, nullptr, &tex_staging);
if (FAILED(hr))
{
return false;
}
//Copy a single pixel to staging texture for each desktop
D3D11_BOX box = {0};
box.front = 0;
box.back = 1;
UINT dst_x = 0;
for (const DPRect& rect : m_DesktopRects)
{
box.left = clamp(rect.GetTL().x - m_DesktopX, 0, m_DesktopWidth - 1);
box.right = clamp(box.left + 1, 1u, (UINT)m_DesktopWidth);
box.top = clamp(rect.GetTL().y - m_DesktopY, 0, m_DesktopHeight - 1);
box.bottom = clamp(box.top + 1, 1u, (UINT)m_DesktopHeight);
m_DeviceContext->CopySubresourceRegion(tex_staging.Get(), 0, dst_x, 0, 0, m_OvrlTex, 0, &box);
dst_x++;
}
//Map texture and get the pixels we just copied
D3D11_MAPPED_SUBRESOURCE mapped_resource;
hr = m_DeviceContext->Map(tex_staging.Get(), 0, D3D11_MAP_READ, 0, &mapped_resource);
if (FAILED(hr))
{
return false;
}
//Check alpha value for anything between 0% and 100% transparency, which should not happen but apparently does
bool ret = false;
if (desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM)
{
for (int i = 0; i < pixel_count * 4; i += 4)
{
unsigned char a = ((unsigned char*)mapped_resource.pData)[i + 3];
if ((a > 0) && (a < 255))
{
ret = true;
break;
}
}
}
else if (desc.Format == DXGI_FORMAT_R16G16B16A16_FLOAT)
{
for (int i = 0; i < pixel_count * 4; i += 4)
{
PackedVector::HALF a_half = ((PackedVector::HALF*)mapped_resource.pData)[i + 3];
float a = PackedVector::XMConvertHalfToFloat(a_half);
if ((a > 0.0f) && (a < 1.0f))
{
ret = true;
break;
}
}
}
//Cleanup
m_DeviceContext->Unmap(tex_staging.Get(), 0);
return ret;
}
bool OutputManager::HandleOpenVREvents()
{
vr::VREvent_t vr_event;
//Handle Dashboard dummy ones first
while (vr::VROverlay()->PollNextOverlayEvent(m_OvrlHandleDashboardDummy, &vr_event, sizeof(vr_event)))
{
switch (vr_event.eventType)
{
case vr::VREvent_OverlayShown:
{
if (!m_OvrlDashboardActive)
{
m_OvrlDashboardActive = true;
m_DashboardActivatedOnce = true; //Bringing up the Desktop+ also fixes what we work around with by keeping track of this
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if (data.ConfigInt[configid_int_overlay_display_mode] != ovrl_dispmode_scene)
{
ShowOverlay(i);
}
}
m_BackgroundOverlay.Update();
m_OverlayDragger.UpdateTempStandingPosition();
if (ConfigManager::GetValue(configid_bool_interface_dim_ui))
{
DimDashboard(true);
}
}
break;
}
case vr::VREvent_OverlayHidden:
{
if (m_OvrlDashboardActive)
{
m_OvrlDashboardActive = false;
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if (data.ConfigInt[configid_int_overlay_display_mode] == ovrl_dispmode_dplustab)
{
HideOverlay(i);
}
}
m_BackgroundOverlay.Update();
if (ConfigManager::GetValue(configid_bool_interface_dim_ui))
{
DimDashboard(false);
}
}
break;
}
case vr::VREvent_DashboardActivated:
{
//The dashboard transform we're using basically cannot be trusted to be correct unless the dashboard has been manually brought up once.
//On launch, SteamVR activates the dashboard automatically. Sometimes with and sometimes without this event firing.
//In that case, the primary dashboard device is the HMD (or sometimes just invalid). That means we can be sure other dashboard device's activations are user-initiated.
//We simply don't show dashboard-origin overlays until the dashboard has been properly activated once.
//For HMD-only usage, switching to the Desktop+ dashboard tab also works
if ( (!m_DashboardActivatedOnce) && (vr::VROverlay()->GetPrimaryDashboardDevice() != vr::k_unTrackedDeviceIndex_Hmd) &&
(vr::VROverlay()->GetPrimaryDashboardDevice() != vr::k_unTrackedDeviceIndexInvalid) )
{
m_DashboardActivatedOnce = true;
}
//Get current HMD y-position, used for getting the overlay position
UpdateDashboardHMD_Y();
//Hacky workaround, need to wait for the dashboard to finish appearing when not in Desktop+ tab
if (!m_OvrlDashboardActive)
{
::Sleep(300);
}
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if ((m_DashboardActivatedOnce) && (data.ConfigInt[configid_int_overlay_display_mode] == ovrl_dispmode_dashboard))
{
ShowOverlay(i);
}
else if (data.ConfigInt[configid_int_overlay_display_mode] == ovrl_dispmode_scene)
{
HideOverlay(i);
}
else if (data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_dashboard) //Dashboard origin with Always display mode, update pos
{
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(i);
ApplySettingTransform();
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
}
//Finish a direct drag if one's going as the Desktop+ laser pointer will not be told inputs are released when the dashboard is up
if (m_OvrlDirectDragActive)
{
OverlayDirectDragFinish(m_OverlayDragger.GetDragOverlayID());
}
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 0);
break;
}
case vr::VREvent_DashboardDeactivated:
{
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if (data.ConfigInt[configid_int_overlay_display_mode] == ovrl_dispmode_scene)
{
ShowOverlay(i);
}
else if (data.ConfigInt[configid_int_overlay_display_mode] == ovrl_dispmode_dashboard)
{
HideOverlay(i);
}
}
if (ConfigManager::GetValue(configid_bool_windows_auto_focus_scene_app_dashboard))
{
WindowManager::Get().FocusActiveVRSceneApp(&m_InputSim);
}
//In unfortunate situations we can have a target window set and close the dashboard without getting a mouse up event ever,
//so we reset the target and mouse on dashboard close
if (WindowManager::Get().GetTargetWindow() != nullptr)
{
m_InputSim.MouseSetLeftDown(false);
WindowManager::Get().SetTargetWindow(nullptr);
}
//Finish a temp drag if one's going
DetachedTempDragFinish();
//If there is a drag going with a dashboard origin overlay (or related display mode), cancel or finish it to avoid the overlay ending up in a random spot
if ( (m_OverlayDragger.IsDragActive()) || (m_OverlayDragger.IsDragGestureActive()) )
{
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(m_OverlayDragger.GetDragOverlayID());
if ( (data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_dashboard) || (data.ConfigInt[configid_int_overlay_display_mode] == ovrl_dispmode_dashboard) ||
(data.ConfigInt[configid_int_overlay_display_mode] == ovrl_dispmode_dplustab) )
{
if (m_OverlayDragger.IsDragActive())
{
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(m_OverlayDragger.GetDragOverlayID());
OnDragFinish();
m_OverlayDragger.DragCancel(); //Overlay can still disappear, so we just cancel the drag instead
ApplySettingTransform();
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
else if (m_OverlayDragger.IsDragGestureActive())
{
m_OverlayDragger.DragGestureFinish();
}
}
}
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 0);
break;
}
case vr::VREvent_SeatedZeroPoseReset:
case vr::VREvent_ChaperoneUniverseHasChanged:
{
DetachedTransformUpdateSeatedPosition();
break;
}
case vr::VREvent_SceneApplicationChanged:
{
DetachedTransformUpdateSeatedPosition();
const bool loaded_overlay_profile = ConfigManager::Get().GetAppProfileManager().ActivateProfileForProcess(vr_event.data.process.pid);
if (loaded_overlay_profile)
ResetOverlays();
break;
}
case vr::VREvent_Input_ActionManifestReloaded:
case vr::VREvent_Input_BindingsUpdated:
case vr::VREvent_Input_BindingLoadSuccessful:
{
m_VRInput.RefreshAnyGlobalActionBound();
break;
}
case vr::VREvent_TrackedDeviceActivated:
{
m_VRInput.RefreshAnyGlobalActionBound();
//Apply transforms of all device origin overlays in case a previously unavailable device was used by one of them
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if ((data.ConfigInt[configid_int_overlay_origin] >= ovrl_origin_left_hand) && (data.ConfigInt[configid_int_overlay_origin] <= ovrl_origin_aux))
{
OverlayManager::Get().SetCurrentOverlayID(i);
ApplySettingTransform();
}
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
break;
}
case vr::VREvent_TrackedDeviceDeactivated:
{
m_VRInput.RefreshAnyGlobalActionBound();
m_LaserPointer.RemoveDevice(vr_event.trackedDeviceIndex);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 0);
break;
}
case vr::VREvent_Quit:
{
return true;
}
}
}
//Now handle events for the actual overlays
int overlay_focus_count = (m_OvrlInputActive) ? 1 : 0; //Keep track of multiple overlay focus enter/leave happening within the same frame to set m_OvrlInputActive correctly afterwards
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayManager::Get().SetCurrentOverlayID(i);
Overlay& overlay = OverlayManager::Get().GetCurrentOverlay();
vr::VROverlayHandle_t ovrl_handle = overlay.GetHandle();
const OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
while (vr::VROverlay()->PollNextOverlayEvent(ovrl_handle, &vr_event, sizeof(vr_event)))
{
switch (vr_event.eventType)
{
case vr::VREvent_MouseMove:
case vr::VREvent_MouseButtonDown:
case vr::VREvent_MouseButtonUp:
case vr::VREvent_ScrollDiscrete:
case vr::VREvent_ScrollSmooth:
{
OnOpenVRMouseEvent(vr_event, current_overlay_old);
break;
}
case vr::VREvent_ButtonPress:
{
if (vr_event.data.controller.button == Button_Dashboard_GoHome)
{
ConfigManager::Get().GetActionManager().StartAction(ConfigManager::GetValue(configid_handle_input_go_home_action_uid), i);
}
else if (vr_event.data.controller.button == Button_Dashboard_GoBack)
{
ConfigManager::Get().GetActionManager().StartAction(ConfigManager::GetValue(configid_handle_input_go_back_action_uid), i);
}
break;
}
case vr::VREvent_ButtonUnpress:
{
if (vr_event.data.controller.button == Button_Dashboard_GoHome)
{
ConfigManager::Get().GetActionManager().StopAction(ConfigManager::GetValue(configid_handle_input_go_home_action_uid), i);
}
else if (vr_event.data.controller.button == Button_Dashboard_GoBack)
{
ConfigManager::Get().GetActionManager().StopAction(ConfigManager::GetValue(configid_handle_input_go_back_action_uid), i);
}
break;
}
case vr::VREvent_FocusEnter:
{
overlay_focus_count++;
const bool drag_or_select_mode_enabled = ( (ConfigManager::GetValue(configid_bool_state_overlay_dragmode)) || (ConfigManager::GetValue(configid_bool_state_overlay_selectmode)) );
if (!drag_or_select_mode_enabled)
{
if (ConfigManager::Get().GetPrimaryLaserPointerDevice() == vr::k_unTrackedDeviceIndex_Hmd)
{
ResetMouseLastLaserPointerPos();
}
//If it's a WinRT window capture, check for window management stuff
if ( (overlay.GetTextureSource() == ovrl_texsource_winrt_capture) && (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0) )
{
if ( (!m_MouseIgnoreMoveEvent) && (ConfigManager::GetValue(configid_bool_windows_winrt_auto_focus)) )
{
WindowManager::Get().RaiseAndFocusWindow((HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd], &m_InputSim);
}
if (ConfigManager::GetValue(configid_bool_windows_winrt_keep_on_screen))
{
WindowManager::MoveWindowIntoWorkArea((HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd]);
}
}
}
break;
}
case vr::VREvent_FocusLeave:
{
overlay_focus_count--;
const bool drag_or_select_mode_enabled = ( (ConfigManager::GetValue(configid_bool_state_overlay_dragmode)) || (ConfigManager::GetValue(configid_bool_state_overlay_selectmode)) );
if (!drag_or_select_mode_enabled)
{
//If leaving a WinRT window capture and the option is enabled, focus the active scene app
if ( (!m_MouseIgnoreMoveEvent) && (ConfigManager::GetValue(configid_bool_windows_winrt_auto_focus_scene_app)) &&
(overlay.GetTextureSource() == ovrl_texsource_winrt_capture) && (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0) )
{
WindowManager::Get().FocusActiveVRSceneApp(&m_InputSim);
}
//A resize while drag can make the pointer lose focus, which is pretty janky. Remove target and do mouse up at least.
if (WindowManager::Get().GetTargetWindow() != nullptr)
{
const bool use_pen = ConfigManager::GetValue(configid_bool_input_mouse_simulate_pen_input);
(use_pen) ? m_InputSim.PenSetPrimaryDown(false) : m_InputSim.MouseSetLeftDown(false);
WindowManager::Get().SetTargetWindow(nullptr);
}
WindowManager::Get().ClearTempTopMostWindow();
}
//Finish drag if there's somehow still one going (and not temp drag mode, where this is expected)
if ( (m_OverlayDragger.IsDragActive()) && (!ConfigManager::GetValue(configid_bool_state_overlay_dragmode_temp)) )
{
OnDragFinish();
m_OverlayDragger.DragFinish();
ApplySettingTransform();
}
//For browser overlays, forward leave event to browser process
if (overlay.GetTextureSource() == ovrl_texsource_browser)
{
DPBrowserAPIClient::Get().DPBrowser_MouseLeave(overlay.GetHandle());
break;
}
m_InputSim.PenLeave();
break;
}
case vr::VREvent_OverlayClosed:
case vr::VREvent_OverlayHidden:
{
//Theater overlay was hidden by something (close button, other overlay taking over, etc.), disable it
//There may be cases where the overlay doesn't send the hidden event for this (varies between SteamVR builds), for that we also have a hack further below,
//though it's mostly harmless if this doesn't work (phantom dashboard tab)
if (OverlayManager::Get().GetTheaterOverlayID() == i)
{
SetOverlayEnabled(i, false);
}
break;
}
case vr::VREvent_ChaperoneUniverseHasChanged:
{
//We also get this when tracking is lost, which ends up updating the dashboard position
if (m_OvrlActiveCount != 0)
{
ApplySettingTransform();
}
break;
}
default:
{
//Output unhandled events when looking for something useful
/*std::wstringstream ss;
ss << L"Event: " << (int)vr_event.eventType << L"\n";
OutputDebugString(ss.str().c_str());*/
break;
}
}
}
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
m_OvrlInputActive = (overlay_focus_count > 0);
//Handle stuff coming from SteamVR Input
m_VRInput.Update();
//Handle Enable Global Laser Pointer binding
vr::InputDigitalActionData_t enable_laser_pointer_state = m_VRInput.GetEnableGlobalLaserPointerState();
if (enable_laser_pointer_state.bChanged)
{
if (enable_laser_pointer_state.bState)
{
if (!m_LaserPointer.IsActive()) //Don't switch devices if the pointer is already active
{
//Get tracked device index from origin
vr::InputOriginInfo_t origin_info = m_VRInput.GetOriginTrackedDeviceInfoEx(enable_laser_pointer_state.activeOrigin);
if (origin_info.trackedDeviceIndex != vr::k_unTrackedDeviceIndexInvalid)
{
m_LaserPointer.SetActiveDevice(origin_info.trackedDeviceIndex, dplp_activation_origin_input_binding);
//Disable active laser pointer override so the device's pointer can be used right away
m_MouseIgnoreMoveEvent = false;
ResetMouseLastLaserPointerPos();
}
}
}
else if (m_LaserPointer.GetActivationOrigin() == dplp_activation_origin_input_binding)
{
m_LaserPointer.ClearActiveDevice();
}
}
m_VRInput.HandleGlobalActionShortcuts(*this);
//Finish up pending keyboard input collected into the queue
m_InputSim.KeyboardTextFinish();
HandleHotkeys();
HandleKeyboardAutoVisibility();
//Update position if necessary
bool dashboard_origin_was_updated = false;
if (HasDashboardMoved()) //The dashboard can move from events we can't detect, like putting the HMD back on, so we check manually as a workaround
{
UpdateDashboardHMD_Y();
dashboard_origin_was_updated = true;
}
bool transform_frame_update_was_done = false; //Frame transform updates need to track the last time they were done, but only once for all overlays
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayManager::Get().SetCurrentOverlayID(i);
Overlay& overlay = OverlayManager::Get().GetCurrentOverlay();
const OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
if (data.ConfigBool[configid_bool_overlay_enabled])
{
if (overlay.IsVisible())
{
if (m_OverlayDragger.GetDragOverlayID() == overlay.GetID())
{
if (m_OverlayDragger.IsDragActive())
{
m_OverlayDragger.DragUpdate();
}
else if (m_OverlayDragger.IsDragGestureActive())
{
m_OverlayDragger.DragGestureUpdate();
}
}
else if ((data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_hmd_floor) || (data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_hmd))
{
if (DetachedTransformFrameUpdate())
{
transform_frame_update_was_done = true;
}
}
else if ( (dashboard_origin_was_updated) && (m_OverlayDragger.GetDragDeviceID() == -1) && (!m_OverlayDragger.IsDragGestureActive()) &&
(data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_dashboard) )
{
ApplySettingTransform();
}
}
DetachedOverlayGazeFade();
}
}
if (transform_frame_update_was_done)
{
m_LastFrameTransformUpdateTick = ::GetTickCount64();
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
DetachedInteractionAutoToggleAll();
DetachedOverlayAutoDockingAll();
m_LaserPointer.Update();
//Hack:
//We don't get an event when the Theater Screen close button is being used, which puts it in the dashboard (we don't want this),
//but IsActiveDashboardOverlay() is true if it's visible on the screen so it doesn't help either to detected this.
//So we instead check if its middle transform gets too close to the dashboard (transform is based on Theater Screen if it's on there, otherwise the dashboard tab).
//The case of this happening in normal use is fairly low, so it'll have to do for now
if (OverlayManager::Get().GetTheaterOverlayID() != k_ulOverlayID_None)
{
if (vr::VROverlay()->IsDashboardVisible())
{
vr::VROverlayHandle_t system_dashboard;
vr::VROverlay()->FindOverlay("system.systemui", &system_dashboard);
const Matrix4 mat_dashboard = m_OverlayDragger.GetBaseOffsetMatrix(ovrl_origin_dashboard);
const Matrix4 mat_theater = OverlayManager::Get().GetOverlayMiddleTransform(OverlayManager::Get().GetTheaterOverlayID());
Vector3 pos_dashboard = mat_dashboard.getTranslation();
Vector3 pos_theater = mat_theater.getTranslation();
pos_dashboard.y = pos_theater.y;
float distance = pos_dashboard.distance(pos_theater);
//Distance should be between 0.009f and 0.019f (SteamVR 2 dashboard) in practice so this hopefully has a big enough tolerance
if (distance < 0.05f)
{
vr::VROverlay()->ShowDashboard("elvissteinjr.DesktopPlusDashboard"); //Defaults to Steam tab so try to get it to display ours instead
SetOverlayEnabled(OverlayManager::Get().GetTheaterOverlayID(), false);
}
}
}
//Handle delayed dashboard dummy updates
if ( (m_PendingDashboardDummyHeight != 0.0f) && (m_LastApplyTransformTick + 100 < ::GetTickCount64()) )
{
UpdatePendingDashboardDummyHeight();
}
FinishQueuedOverlayRemovals();
return false;
}
void OutputManager::OnOpenVRMouseEvent(const vr::VREvent_t& vr_event, unsigned int& current_overlay_old)
{
const Overlay& overlay_current = OverlayManager::Get().GetCurrentOverlay();
const OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
const bool use_pen = ConfigManager::GetValue(configid_bool_input_mouse_simulate_pen_input);
switch (vr_event.eventType)
{
case vr::VREvent_MouseMove:
{
if ( (!data.ConfigBool[configid_bool_overlay_input_enabled]) || ( (m_MouseIgnoreMoveEvent) && (overlay_current.GetTextureSource() != ovrl_texsource_browser) ) ||
(ConfigManager::GetValue(configid_bool_state_overlay_dragmode)) || (ConfigManager::GetValue(configid_bool_state_overlay_selectmode)) ||
(m_OverlayDragger.IsDragActive()) || (overlay_current.GetTextureSource() == ovrl_texsource_none) || (overlay_current.GetTextureSource() == ovrl_texsource_ui) )
{
break;
}
Vector2 event_mouse_pos(vr_event.data.mouse.x, vr_event.data.mouse.y);
//Smooth input
if (ConfigManager::GetValue(configid_int_input_mouse_input_smoothing_level) != 0)
{
m_MouseLaserPointerSmoother.ApplyPresetSettings(ConfigManager::GetValue(configid_int_input_mouse_input_smoothing_level));
event_mouse_pos = m_MouseLaserPointerSmoother.Filter(event_mouse_pos);
}
//Offset depending on capture source
int content_height = data.ConfigInt[configid_int_overlay_state_content_height];
int offset_x = 0;
int offset_y = 0;
if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_winrt_capture)
{
int desktop_id = data.ConfigInt[configid_int_overlay_winrt_desktop_id];
if (desktop_id != -2) //Desktop capture through WinRT
{
if ( (desktop_id >= 0) && (desktop_id < m_DesktopRects.size()) )
{
offset_x = m_DesktopRects[desktop_id].GetTL().x;
offset_y = m_DesktopRects[desktop_id].GetTL().y;
}
else if (desktop_id == -1) //Combined desktop
{
content_height = m_DesktopRectTotal.GetHeight();
offset_x = m_DesktopRectTotal.GetTL().x;
offset_y = m_DesktopRectTotal.GetTL().y;
}
}
else //Window capture
{
HWND window_handle = (HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd];
//Get position of the window
RECT window_rect = {0};
if (::DwmGetWindowAttribute(window_handle, DWMWA_EXTENDED_FRAME_BOUNDS, &window_rect, sizeof(window_rect)) == S_OK)
{
offset_x = window_rect.left;
offset_y = window_rect.top;
}
}
}
else if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication)
{
content_height = m_DesktopHeight;
offset_x = m_DesktopX;
offset_y = m_DesktopY;
}
//SteamVR ignores 3D properties when adjusting coordinates for custom UV values, so we need to add offsets manually
if (data.ConfigBool[configid_bool_overlay_3D_enabled])
{
Overlay3DMode mode_3D = (Overlay3DMode)data.ConfigInt[configid_int_overlay_3D_mode];
if (mode_3D >= ovrl_3Dmode_hou)
{
offset_x += overlay_current.GetValidatedCropRect().GetTL().x;
}
else
{
offset_x += overlay_current.GetValidatedCropRect().GetTL().x / 2;
}
}
//GL space (0,0 is bottom left), so we need to flip that around (not correct for browser overlays, but also not relevant for how the values are used with them right now)
int pointer_x = round(event_mouse_pos.x) + offset_x;
int pointer_y = (-round(event_mouse_pos.y) + content_height) + offset_y;
//If double click assist is current active, check if there was an obviously deliberate movement and cancel it then
if ( (ConfigManager::GetValue(configid_int_state_mouse_dbl_click_assist_duration_ms) != 0) &&
(::GetTickCount64() < m_MouseLastClickTick + ConfigManager::GetValue(configid_int_state_mouse_dbl_click_assist_duration_ms)) )
{
if ((abs(pointer_x - m_MouseLastLaserPointerX) > 64) || (abs(pointer_y - m_MouseLastLaserPointerY) > 64))
{
m_MouseLastClickTick = 0;
}
else //But if not, still block the movement
{
m_MouseLastLaserPointerMoveBlocked = true;
break;
}
}
//If browser overlay, pass event along and skip the rest
if (overlay_current.GetTextureSource() == ovrl_texsource_browser)
{
DPBrowserAPIClient::Get().DPBrowser_MouseMove(overlay_current.GetHandle(), pointer_x, pointer_y);
m_MouseLastLaserPointerX = pointer_x;
m_MouseLastLaserPointerY = pointer_y;
break;
}
//Check if this mouse move would start a drag of a maximized window's title bar
if ( (ConfigManager::GetValue(configid_int_windows_winrt_dragging_mode) != window_dragging_none) &&
(overlay_current.GetTextureSource() == ovrl_texsource_winrt_capture) && (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0) )
{
if (WindowManager::Get().WouldDragMaximizedTitleBar((HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd],
m_MouseLastLaserPointerX, m_MouseLastLaserPointerY, pointer_x, pointer_y))
{
//Reset input and WindowManager state manually to block the drag but still move the cursor on the next mouse move event
(use_pen) ? m_InputSim.PenSetPrimaryDown(false) : m_InputSim.MouseSetLeftDown(false);
WindowManager::Get().SetTargetWindow(nullptr);
//Start overlay drag if setting enabled
if (ConfigManager::GetValue(configid_int_windows_winrt_dragging_mode) == window_dragging_overlay)
{
if (data.ConfigInt[configid_int_overlay_origin] != ovrl_origin_theater_screen)
{
if (!data.ConfigBool[configid_bool_overlay_transform_locked])
{
m_OverlayDragger.DragStart(OverlayManager::Get().GetCurrentOverlayID());
}
else
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_device, ConfigManager::Get().GetPrimaryLaserPointerDevice());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 1);
}
}
else
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_device, ConfigManager::Get().GetPrimaryLaserPointerDevice());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 2);
}
}
break; //We're not moving the cursor this time, get out
}
}
//Check coordinates if laser pointer override is enabled, unless left mouse is held down by the laser pointer
if ( (m_MouseLeftDownOverlayID == k_ulOverlayID_None) && (ConfigManager::GetValue(configid_bool_input_mouse_allow_pointer_override)) )
{
POINT pt;
::GetCursorPos(&pt);
//Only check for override if the last laser pointer position was inside a desktop (outside coordinates are possible via extended laser drag and combined desktop edge cases)
bool do_check_for_override = false;
const Vector2Int laser_pos(m_MouseLastLaserPointerX, m_MouseLastLaserPointerY);
for (const DPRect& rect : m_DesktopRects)
{
if (rect.Contains(laser_pos))
{
do_check_for_override = true;
break;
}
}
//Simulated pen input does not move the mouse cursor when an application supports pen input directly, which makes the normal check result in false positives
//In this case we only check for override when the mouse cursor position changed compared to last time
//While it might seem to make sense to always do this, it actually makes it harder to trigger the override during mouse simulation
if (ConfigManager::GetValue(configid_bool_input_mouse_simulate_pen_input))
{
static POINT pt_last = {0};
if ((pt.x == pt_last.x) && (pt.y == pt_last.y))
{
do_check_for_override = false;
}
pt_last = pt;
}
//Check if the cursor is near the corner of one of the desktops and opt out of the pointer override to avoid sticky mouse corners triggering it
//It can still happen sometimes, but this catches most cases
if (do_check_for_override)
{
//Only do for combined desktops though
if ( ((data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication) && (data.ConfigInt[configid_int_overlay_desktop_id] == -1)) ||
((data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_winrt_capture) && (data.ConfigInt[configid_int_overlay_winrt_desktop_id] == -1)) )
{
const Vector2Int cursor_pos(pt.x, pt.y);
for (const DPRect& rect : m_DesktopRects)
{
if ((fabs(cursor_pos.distance(rect.GetTL())) < 64.0f) || (fabs(cursor_pos.distance(rect.GetTR())) < 64.0f) ||
(fabs(cursor_pos.distance(rect.GetBL())) < 64.0f) || (fabs(cursor_pos.distance(rect.GetBR())) < 64.0f))
{
do_check_for_override = false;
break;
}
}
}
}
//If mouse coordinates are not what the last laser pointer was (with tolerance), meaning some other source moved it
if ((do_check_for_override) && ((abs(pt.x - m_MouseLastLaserPointerX) > 32) || (abs(pt.y - m_MouseLastLaserPointerY) > 32)))
{
m_MouseIgnoreMoveEventMissCount++; //GetCursorPos() may lag behind or other jumps may occasionally happen. We count up a few misses first before acting on them
int max_miss_count = 10; //Arbitrary number, but appears to work reliably
//Reduce max miss count to 1 for simulated pen input
if (ConfigManager::GetValue(configid_bool_input_mouse_simulate_pen_input))
{
max_miss_count = 1;
}
else
{
//When updates are limited, try adapting for the lower update rate
if (m_PerformanceUpdateLimiterDelay.QuadPart != 0)
{
max_miss_count = std::max(1, max_miss_count - int((m_PerformanceUpdateLimiterDelay.QuadPart / 1000) / 20));
}
}
if (m_MouseIgnoreMoveEventMissCount > max_miss_count)
{
m_MouseIgnoreMoveEvent = true;
//Set flag for all overlays
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const Overlay& overlay = OverlayManager::Get().GetOverlay(i);
if ( (overlay.GetTextureSource() != ovrl_texsource_none) && (overlay.GetTextureSource() != ovrl_texsource_ui) && (overlay.GetTextureSource() != ovrl_texsource_browser) )
{
vr::VROverlay()->SetOverlayFlag(overlay.GetHandle(), vr::VROverlayFlags_HideLaserIntersection, true);
}
}
}
break;
}
else
{
m_MouseIgnoreMoveEventMissCount = 0;
}
}
//To improve compatibility with dragging certain windows around, simulate a small movement first before fully unlocking the cursor from double-click assist
if (m_MouseLastLaserPointerMoveBlocked)
{
//Move a single pixel in the direction of the new pointer position
pointer_x = m_MouseLastLaserPointerX + sgn(pointer_x - m_MouseLastLaserPointerX);
pointer_y = m_MouseLastLaserPointerY + sgn(pointer_y - m_MouseLastLaserPointerY);
(use_pen) ? m_InputSim.PenMove(pointer_x, pointer_y) : m_InputSim.MouseMove(pointer_x, pointer_y);
m_MouseLastLaserPointerMoveBlocked = false;
//Real movement continues on the next mouse move event
}
else
{
//Finally do the actual cursor movement if we're still here
(use_pen) ? m_InputSim.PenMove(pointer_x, pointer_y) : m_InputSim.MouseMove(pointer_x, pointer_y);
m_MouseLastLaserPointerX = pointer_x;
m_MouseLastLaserPointerY = pointer_y;
}
//This is only relevant when limiting updates. See Update() for details.
m_MouseLaserPointerUsedLastUpdate = true;
break;
}
case vr::VREvent_MouseButtonDown:
{
ActionManager& action_manager = ConfigManager::Get().GetActionManager();
if (ConfigManager::GetValue(configid_bool_state_overlay_selectmode))
{
if (vr_event.data.mouse.button == vr::VRMouseButton_Left)
{
//Select this as current overlay
IPCManager::Get().PostConfigMessageToUIApp(configid_int_interface_overlay_current_id, overlay_current.GetID());
current_overlay_old = overlay_current.GetID(); //Set a new reset value since we're in the middle of a temporary current overlay loop
}
break;
}
else if (ConfigManager::GetValue(configid_bool_state_overlay_dragmode))
{
if (vr_event.data.mouse.button == vr::VRMouseButton_Left)
{
if (m_OverlayDragger.GetDragDeviceID() == -1)
{
if (data.ConfigInt[configid_int_overlay_origin] != ovrl_origin_theater_screen)
{
if (!data.ConfigBool[configid_bool_overlay_transform_locked])
{
m_OverlayDragger.DragStart(OverlayManager::Get().GetCurrentOverlayID());
}
else
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_device, ConfigManager::Get().GetPrimaryLaserPointerDevice());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 1);
}
}
else
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_device, ConfigManager::Get().GetPrimaryLaserPointerDevice());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 2);
}
}
}
else if (vr_event.data.mouse.button == vr::VRMouseButton_Right)
{
if (!m_OverlayDragger.IsDragGestureActive())
{
if (data.ConfigInt[configid_int_overlay_origin] != ovrl_origin_theater_screen)
{
if (!data.ConfigBool[configid_bool_overlay_transform_locked])
{
m_OverlayDragger.DragGestureStart(OverlayManager::Get().GetCurrentOverlayID());
}
else
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_device, ConfigManager::Get().GetPrimaryLaserPointerDevice());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 1);
}
}
else
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_device, ConfigManager::Get().GetPrimaryLaserPointerDevice());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 2);
}
}
}
break;
}
//Set focused ID when clicking on an overlay
ConfigManager::Get().SetValue(configid_int_state_overlay_focused_id, (int)overlay_current.GetID());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_focused_id, (int)overlay_current.GetID());
if (overlay_current.GetTextureSource() == ovrl_texsource_browser)
{
if (vr_event.data.mouse.button <= vr::VRMouseButton_Middle)
{
m_MouseLastClickTick = ::GetTickCount64();
DPBrowserAPIClient::Get().DPBrowser_MouseDown(overlay_current.GetHandle(), (vr::EVRMouseButton)vr_event.data.mouse.button);
}
else
{
switch (vr_event.data.mouse.button)
{
case VRMouseButton_DP_Aux01: action_manager.StartAction(ConfigManager::GetValue(configid_handle_input_go_back_action_uid), overlay_current.GetID()); break;
case VRMouseButton_DP_Aux02: action_manager.StartAction(ConfigManager::GetValue(configid_handle_input_go_home_action_uid), overlay_current.GetID()); break;
}
}
break;
}
else if ((overlay_current.GetTextureSource() == ovrl_texsource_none) || (overlay_current.GetTextureSource() == ovrl_texsource_ui))
{
break;
}
if (m_MouseIgnoreMoveEvent) //This can only be true if AllowPointerOverride enabled
{
m_MouseIgnoreMoveEvent = false;
ResetMouseLastLaserPointerPos();
ApplySettingMouseInput();
//If it's a WinRT window capture, also check for window management stuff that would've happened on overlay focus enter otherwise
if ( (overlay_current.GetTextureSource() == ovrl_texsource_winrt_capture) && (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0) )
{
if (ConfigManager::GetValue(configid_bool_windows_winrt_auto_focus))
{
WindowManager::Get().RaiseAndFocusWindow((HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd], &m_InputSim);
}
}
break; //Click to restore shouldn't generate a mouse click
}
//If a WindowManager drag event could occur, set the current window for it
if ( (vr_event.data.mouse.button == vr::VRMouseButton_Left) && (overlay_current.GetTextureSource() == ovrl_texsource_winrt_capture) &&
(data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0) )
{
WindowManager::Get().SetTargetWindow((HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd], overlay_current.GetID());
}
if (vr_event.data.mouse.button <= vr::VRMouseButton_Middle)
{
m_MouseLastClickTick = ::GetTickCount64();
if (vr_event.data.mouse.button == vr::VRMouseButton_Left)
{
m_MouseLeftDownOverlayID = overlay_current.GetID();
if (ConfigManager::GetValue(configid_bool_input_keyboard_auto_show_desktop))
{
WindowManager::Get().OnTextInputLeftMouseClick();
}
}
}
switch (vr_event.data.mouse.button)
{
case vr::VRMouseButton_Left: (use_pen) ? m_InputSim.PenSetPrimaryDown(true) : m_InputSim.MouseSetLeftDown(true); break;
case vr::VRMouseButton_Right: (use_pen) ? m_InputSim.PenSetSecondaryDown(true) : m_InputSim.MouseSetRightDown(true); break;
case vr::VRMouseButton_Middle: m_InputSim.MouseSetMiddleDown(true); break;
case VRMouseButton_DP_Aux01: action_manager.StartAction(ConfigManager::GetValue(configid_handle_input_go_back_action_uid), overlay_current.GetID()); break;
case VRMouseButton_DP_Aux02: action_manager.StartAction(ConfigManager::GetValue(configid_handle_input_go_home_action_uid), overlay_current.GetID()); break;
}
break;
}
case vr::VREvent_MouseButtonUp:
{
ActionManager& action_manager = ConfigManager::Get().GetActionManager();
if (ConfigManager::GetValue(configid_bool_state_overlay_selectmode))
{
break;
}
else if (ConfigManager::GetValue(configid_bool_state_overlay_dragmode_temp))
{
//Temp drag doesn't have proper overlay focus so can be called on any overlay if it's in front. Finish drag in any case.
if (vr_event.data.mouse.button == vr::VRMouseButton_Left)
{
//Don't actually react to this unless it's been a few milliseconds
//That way releasing the trigger after the UI selection instead of holding it doesn't just drop the overlay on the spot
if (::GetTickCount64() >= m_OvrlTempDragStartTick + 250)
{
OnDragFinish();
DetachedTempDragFinish();
break;
}
}
}
else if ( (m_OverlayDragger.GetDragOverlayID() == overlay_current.GetID()) && ( (m_OverlayDragger.IsDragActive()) || (m_OverlayDragger.IsDragGestureActive()) ) )
{
if ((vr_event.data.mouse.button == vr::VRMouseButton_Left) && (m_OverlayDragger.IsDragActive()))
{
OnDragFinish();
m_OverlayDragger.DragFinish();
ApplySettingTransform();
}
else if ((vr_event.data.mouse.button == vr::VRMouseButton_Right) && (m_OverlayDragger.IsDragGestureActive()))
{
m_OverlayDragger.DragGestureFinish();
ApplySettingTransform();
}
break;
}
else if (overlay_current.GetTextureSource() == ovrl_texsource_browser)
{
if (vr_event.data.mouse.button <= vr::VRMouseButton_Middle)
{
DPBrowserAPIClient::Get().DPBrowser_MouseUp(overlay_current.GetHandle(), (vr::EVRMouseButton)vr_event.data.mouse.button);
}
else
{
switch (vr_event.data.mouse.button)
{
case VRMouseButton_DP_Aux01: action_manager.StopAction(ConfigManager::GetValue(configid_handle_input_go_back_action_uid), overlay_current.GetID()); break;
case VRMouseButton_DP_Aux02: action_manager.StopAction(ConfigManager::GetValue(configid_handle_input_go_home_action_uid), overlay_current.GetID()); break;
}
}
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 0);
break;
}
else if ((overlay_current.GetTextureSource() == ovrl_texsource_none) || (overlay_current.GetTextureSource() == ovrl_texsource_ui))
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 0);
break;
}
switch (vr_event.data.mouse.button)
{
case vr::VRMouseButton_Left: (use_pen) ? m_InputSim.PenSetPrimaryDown(false) : m_InputSim.MouseSetLeftDown(false); break;
case vr::VRMouseButton_Right: (use_pen) ? m_InputSim.PenSetSecondaryDown(false) : m_InputSim.MouseSetRightDown(false); break;
case vr::VRMouseButton_Middle: m_InputSim.MouseSetMiddleDown(false); break;
case VRMouseButton_DP_Aux01: action_manager.StopAction(ConfigManager::GetValue(configid_handle_input_go_back_action_uid), overlay_current.GetID()); break;
case VRMouseButton_DP_Aux02: action_manager.StopAction(ConfigManager::GetValue(configid_handle_input_go_home_action_uid), overlay_current.GetID()); break;
}
//If there was a possible WindowManager drag event prepared for, reset the target window
if ( (vr_event.data.mouse.button == vr::VRMouseButton_Left) && (overlay_current.GetTextureSource() == ovrl_texsource_winrt_capture) &&
(data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0) )
{
WindowManager::Get().SetTargetWindow(nullptr);
}
if (vr_event.data.mouse.button == vr::VRMouseButton_Left)
{
m_MouseLeftDownOverlayID = k_ulOverlayID_None;
}
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_drag_hint_type, 0);
break;
}
case vr::VREvent_ScrollDiscrete:
case vr::VREvent_ScrollSmooth:
{
//Discrete scroll events are sent at a fixed frame interval for the system laser pointer and at a fixed interval relative to SteamVR Input action set update calls for Desktop+ pointer
//This can result in vastly different scroll rates at different frame or update rates
//We counteract this by scaling the sent scroll values by the delta between the events to achieve a constant scroll rate
float scroll_step_multiplier = 1.0f;
if (vr_event.eventType == vr::VREvent_ScrollDiscrete)
{
LARGE_INTEGER scroll_delta = {0};
const float scroll_step_ms = 58.31f; //7 frame tick of a 120 Hz HMD... hardly universal, but going with that for now.
::QueryPerformanceCounter(&scroll_delta);
scroll_delta.QuadPart -= m_MouseLaserPointerScrollDeltaStart.QuadPart;
scroll_delta.QuadPart *= 1000000;
scroll_delta.QuadPart /= m_MouseLaserPointerScrollDeltaFrequency.QuadPart;
scroll_step_multiplier = scroll_delta.QuadPart / (1000.0f * scroll_step_ms);
//We typically don't need more than 2x, so treat everything higher as interrupted scrolling and use 1x for them
if (scroll_step_multiplier > 2.0f)
{
scroll_step_multiplier = 1.0f;
}
::QueryPerformanceCounter(&m_MouseLaserPointerScrollDeltaStart);
}
const float xdelta = vr_event.data.scroll.xdelta * scroll_step_multiplier; //Discrete scrolling will never have X as non-0, but just in case this ever changes
const float ydelta = vr_event.data.scroll.ydelta * scroll_step_multiplier;
//Check deadzone
const float xdelta_abs = fabs(xdelta);
const float ydelta_abs = fabs(ydelta);
const bool do_scroll_h = (xdelta_abs > 0.025f);
const bool do_scroll_v = (ydelta_abs > 0.025f);
//Drag-mode scroll
if (m_OverlayDragger.IsDragActive())
{
//Block drag scroll actions when a temp drag just started to avoid mis-inputs
if (::GetTickCount64() <= m_OvrlTempDragStartTick + 250)
break;
//Additional deadzone
if ((xdelta_abs > 0.05f) || (ydelta_abs > 0.05f))
{
//Add distance as long as y-delta input is bigger
if (xdelta_abs < ydelta_abs)
{
m_OverlayDragger.DragAddDistance(ydelta);
}
else
{
m_OverlayDragger.DragAddWidth(xdelta * -0.25f);
}
}
break;
}
//Overlay scrolls
if ( (ConfigManager::GetValue(configid_bool_state_overlay_dragmode)) || (ConfigManager::GetValue(configid_bool_state_overlay_selectmode)) ||
(overlay_current.GetTextureSource() == ovrl_texsource_none) || (overlay_current.GetTextureSource() == ovrl_texsource_ui) )
{
break;
}
else if (overlay_current.GetTextureSource() == ovrl_texsource_browser)
{
if ((do_scroll_h) || (do_scroll_v))
{
DPBrowserAPIClient::Get().DPBrowser_Scroll(overlay_current.GetHandle(), (do_scroll_h) ? xdelta : 0.0f, (do_scroll_v) ? ydelta : 0.0f);
}
break;
}
//Overlay drag-scroll window-title-pull creation... thingy
if (do_scroll_v)
{
bool is_desktop_overlay = ( (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication) ||
((data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_winrt_capture) && (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] == 0)) );
const float delta_trigger_min = (ConfigManager::GetValue(configid_bool_input_mouse_scroll_smooth)) ? -0.16f : 0.9f;
if ( (is_desktop_overlay) && (m_MouseLeftDownOverlayID == overlay_current.GetID()) && (ydelta <= delta_trigger_min) )
{
HWND current_window = ::GetForegroundWindow();
if ( (WindowManager::Get().IsHoveringCapturableTitleBar(current_window, m_MouseLastLaserPointerX, m_MouseLastLaserPointerY)) )
{
vr::TrackedDeviceIndex_t device_index = ConfigManager::Get().GetPrimaryLaserPointerDevice();
//If no dashboard device, try finding one
if (device_index == vr::k_unTrackedDeviceIndexInvalid)
{
device_index = vr::IVROverlayEx::FindPointerDeviceForOverlay(overlay_current.GetHandle());
}
float source_distance = 1.0f;
float target_width = 0.3f;
vr::VROverlayIntersectionResults_t results;
if (vr::IVROverlayEx::ComputeOverlayIntersectionForDevice(overlay_current.GetHandle(), device_index, vr::TrackingUniverseStanding, &results))
{
source_distance = results.fDistance;
//Get window width in meters relative to the source overlay width
RECT window_rect = {0};
if (::DwmGetWindowAttribute(current_window, DWMWA_EXTENDED_FRAME_BOUNDS, &window_rect, sizeof(window_rect)) == S_OK)
{
target_width = (float(window_rect.right - window_rect.left) / overlay_current.GetValidatedCropRect().GetWidth()) * data.ConfigFloat[configid_float_overlay_width];
target_width = std::max(target_width, 0.2f); //Don't let it become too tiny
}
}
//Set device as hint, just in case
ConfigManager::SetValue(configid_int_state_laser_pointer_device_hint, (int)device_index);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_laser_pointer_device_hint, (int)device_index);
//Send to UI
IPCManager::Get().PostConfigMessageToUIApp(configid_handle_state_arg_hwnd, (LPARAM)current_window);
IPCManager::Get().PostMessageToUIApp(ipcmsg_action, ipcact_overlay_new_drag, MAKELPARAM(-2, 0 /*UI doesn't need distance*/));
//Reset input and WindowManager state manually since the overlay mouse up even will be consumed to finish the drag later
m_InputSim.MouseSetLeftDown(false);
WindowManager::Get().SetTargetWindow(nullptr);
//Start drag
AddOverlayDrag(source_distance, ovrl_capsource_winrt_capture, -2, current_window, target_width);
break;
}
}
}
//Normal scrolling
if (do_scroll_v)
{
m_InputSim.MouseWheelVertical(ydelta);
}
if (do_scroll_h)
{
m_InputSim.MouseWheelHorizontal(-xdelta);
}
break;
}
}
}
void OutputManager::HandleKeyboardMessage(IPCActionID ipc_action_id, LPARAM lparam)
{
switch (ipc_action_id)
{
case ipcact_keyboard_vkey:
{
m_InputSim.KeyboardSetKeyState((IPCKeyboardKeystateFlags)LOWORD(lparam), HIWORD(lparam));
break;
}
case ipcact_keyboard_wchar:
{
wchar_t wchar = LOWORD(lparam);
bool down = (HIWORD(lparam) == 1);
//Check if it can be pressed on the current windows keyboard layout
SHORT w32_keystate = ::VkKeyScanW(wchar);
unsigned char keycode = LOBYTE(w32_keystate);
unsigned char flags = HIBYTE(w32_keystate);
bool is_valid = ((keycode != 255) || (flags != 255));
if (is_valid)
{
m_InputSim.KeyboardSetFromWin32KeyState(w32_keystate, down);
}
else if (down) //Otherwise use it as a text input
{
wchar_t wstr[2] = {0};
wstr[0] = wchar;
m_InputSim.KeyboardText(StringConvertFromUTF16(wstr).c_str(), true);
}
break;
}
default: return;
}
}
bool OutputManager::HandleOverlayProfileLoadMessage(LPARAM lparam)
{
IPCActionOverlayProfileLoadArg profile_load_arg = (IPCActionOverlayProfileLoadArg)LOWORD(lparam);
int profile_overlay_id = GET_Y_LPARAM(lparam);
int desktop_id_prev = ConfigManager::GetValue(configid_int_overlay_desktop_id);
const std::string& profile_name = ConfigManager::GetValue(configid_str_state_profile_name_load);
if (profile_overlay_id == -2)
{
ConfigManager::Get().LoadOverlayProfileDefault(true);
}
else if (profile_load_arg == ipcactv_ovrl_profile_multi)
{
ConfigManager::Get().LoadMultiOverlayProfileFromFile(profile_name + ".ini", true);
}
else if (profile_load_arg == ipcactv_ovrl_profile_multi_add)
{
if (profile_overlay_id == -1) //Load queued up overlays
{
//Usually, elements should have been sent in order, but lets make sure they really are
std::sort(m_ProfileAddOverlayIDQueue.begin(), m_ProfileAddOverlayIDQueue.end());
std::vector ovrl_inclusion_list;
ovrl_inclusion_list.reserve(m_ProfileAddOverlayIDQueue.back());
//Build overlay inclusion list
for (int ovrl_id : m_ProfileAddOverlayIDQueue)
{
if ((int)ovrl_inclusion_list.size() > ovrl_id) //Skip if already in list (double entry)
{
continue;
}
while ((int)ovrl_inclusion_list.size() < ovrl_id) //Exclude overlays not queued
{
ovrl_inclusion_list.push_back(0);
}
ovrl_inclusion_list.push_back(1); //Include ovrl_id
}
ConfigManager::Get().LoadMultiOverlayProfileFromFile(profile_name + ".ini", false, &ovrl_inclusion_list);
m_ProfileAddOverlayIDQueue.clear();
}
else if ( (profile_overlay_id >= 0) && (profile_overlay_id < 1000) ) //Queue up overlays
{
m_ProfileAddOverlayIDQueue.push_back(profile_overlay_id);
return false;
}
}
//Reset mirroing entirely if desktop was changed (only in single desktop mode)
if ( (ConfigManager::GetValue(configid_bool_performance_single_desktop_mirroring)) && (ConfigManager::GetValue(configid_int_overlay_desktop_id) != desktop_id_prev) )
return true; //Reset mirroring
ResetOverlays(); //This does everything relevant
return false;
}
void OutputManager::ResetMouseLastLaserPointerPos()
{
//Set last pointer values to current to not trip the movement detection up
POINT pt;
::GetCursorPos(&pt);
m_MouseLastLaserPointerX = pt.x;
m_MouseLastLaserPointerY = pt.y;
//Also reset this state which may be left unclean when window drags get triggered
m_MouseLeftDownOverlayID = k_ulOverlayID_None;
}
void OutputManager::CropToActiveWindow()
{
bool& crop_enabled = ConfigManager::GetRef(configid_bool_overlay_crop_enabled);
int& crop_x = ConfigManager::GetRef(configid_int_overlay_crop_x);
int& crop_y = ConfigManager::GetRef(configid_int_overlay_crop_y);
int& crop_width = ConfigManager::GetRef(configid_int_overlay_crop_width);
int& crop_height = ConfigManager::GetRef(configid_int_overlay_crop_height);
if (CropToActiveWindow(crop_x, crop_y, crop_width, crop_height))
{
crop_enabled = true;
//Send them over to UI
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_overlay_crop_enabled, crop_enabled);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_crop_x, crop_x);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_crop_y, crop_y);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_crop_width, crop_width);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_crop_height, crop_height);
ApplySettingCrop();
ApplySettingTransform();
ApplySettingMouseScale();
}
}
void OutputManager::CropToDisplay(int display_id, bool do_not_apply_setting)
{
int& crop_x = ConfigManager::GetRef(configid_int_overlay_crop_x);
int& crop_y = ConfigManager::GetRef(configid_int_overlay_crop_y);
int& crop_width = ConfigManager::GetRef(configid_int_overlay_crop_width);
int& crop_height = ConfigManager::GetRef(configid_int_overlay_crop_height);
CropToDisplay(display_id, crop_x, crop_y, crop_width, crop_height);
//Send change to UI as well (also set override since this may be called during one)
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, OverlayManager::Get().GetCurrentOverlayID());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_crop_x, crop_x);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_crop_y, crop_y);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_crop_width, crop_width);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_crop_height, crop_height);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
//In single desktop mode, set desktop ID for all overlays
if (ConfigManager::GetValue(configid_bool_performance_single_desktop_mirroring))
{
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayManager::Get().GetConfigData(i).ConfigInt[configid_int_overlay_desktop_id] = display_id;
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, (int)i);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_desktop_id, display_id);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
}
}
//Applying the setting when a duplication resets happens right after has the chance of screwing up the transform (too many transform updates?), so give the option to not do it
if (!do_not_apply_setting)
{
ApplySettingCrop();
ApplySettingTransform();
ApplySettingMouseScale();
ApplySettingExtraBrightness();
}
}
void OutputManager::DuplicateOverlay(unsigned int base_id, bool is_ui_overlay)
{
//Add overlay based on data of base_id overlay and reset it
unsigned int new_id = k_ulOverlayID_None;
if (!is_ui_overlay)
{
new_id = OverlayManager::Get().DuplicateOverlay(OverlayManager::Get().GetConfigData(base_id), base_id);
}
else
{
new_id = OverlayManager::Get().AddUIOverlay();
}
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(new_id);
if (!is_ui_overlay)
{
const Overlay& overlay_base = OverlayManager::Get().GetOverlay(base_id);
Overlay& overlay_current = OverlayManager::Get().GetCurrentOverlay();
//If base overlay is an active WinRT Capture, duplicate capture before resetting the overlay
if (overlay_base.GetTextureSource() == ovrl_texsource_winrt_capture)
{
if (DPWinRT_StartCaptureFromOverlay(overlay_current.GetHandle(), overlay_base.GetHandle()))
{
overlay_current.SetTextureSource(ovrl_texsource_winrt_capture);
}
}
//Automatically reset the matrix to a saner default by putting it next to the base overlay in most cases
DetachedTransformReset(overlay_base.GetID());
}
else
{
DetachedTransformReset();
}
ResetCurrentOverlay();
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
unsigned int OutputManager::AddOverlay(OverlayCaptureSource capture_source, int desktop_id, HWND window_handle)
{
unsigned int new_id = OverlayManager::Get().AddOverlay(capture_source, desktop_id, window_handle);
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(new_id);
if (capture_source == ovrl_capsource_desktop_duplication)
{
CropToDisplay(desktop_id, true);
}
//Adjust width to a more suited default (UI does the same so no need to send over)
ConfigManager::SetValue(configid_float_overlay_width, 1.0f);
//Reset transform to default next to the primary dashboard overlay in most cases
DetachedTransformReset();
ResetCurrentOverlay();
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
return new_id;
}
unsigned int OutputManager::AddOverlayDrag(float source_distance, OverlayCaptureSource capture_source, int desktop_id, HWND window_handle, float overlay_width)
{
unsigned int new_id = OverlayManager::Get().AddOverlay(capture_source, desktop_id, window_handle);
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(new_id);
if (capture_source == ovrl_capsource_desktop_duplication)
{
CropToDisplay(desktop_id, true);
}
//Adjust width and send it over to UI app
ConfigManager::SetValue(configid_float_overlay_width, overlay_width);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, (int)new_id);
IPCManager::Get().PostConfigMessageToUIApp(configid_float_overlay_width, overlay_width);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
//Start drag and apply overlay config
DetachedTempDragStart(new_id, std::max(source_distance - 0.25f, 0.01f));
ResetCurrentOverlay();
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
return new_id;
}
void OutputManager::ApplySettingCaptureSource()
{
Overlay& overlay = OverlayManager::Get().GetCurrentOverlay();
switch (ConfigManager::GetValue(configid_int_overlay_capture_source))
{
case ovrl_capsource_desktop_duplication:
{
if (!m_OutputInvalid)
{
OverlayTextureSource tex_source = overlay.GetTextureSource();
if ((tex_source != ovrl_texsource_desktop_duplication) || (tex_source != ovrl_texsource_desktop_duplication_3dou_converted))
{
ApplySetting3DMode(); //Sets texture source for us when capture source is desktop duplication
}
}
else
{
overlay.SetTextureSource(ovrl_texsource_none);
}
break;
}
case ovrl_capsource_winrt_capture:
{
if (overlay.GetTextureSource() != ovrl_texsource_winrt_capture)
{
if (DPWinRT_IsCaptureFromHandleSupported())
{
OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
if (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0)
{
//Set configid_str_overlay_winrt_last_* strings from window info so returning windows can be restored later
const WindowInfo* window_info = WindowManager::Get().WindowListFindWindow((HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd]);
if (window_info != nullptr)
{
data.ConfigStr[configid_str_overlay_winrt_last_window_title] = StringConvertFromUTF16(window_info->GetTitle().c_str());
data.ConfigStr[configid_str_overlay_winrt_last_window_class_name] = StringConvertFromUTF16(window_info->GetWindowClassName().c_str());
data.ConfigStr[configid_str_overlay_winrt_last_window_exe_name] = window_info->GetExeName();
}
if (DPWinRT_StartCaptureFromHWND(overlay.GetHandle(), (HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd]))
{
overlay.SetTextureSource(ovrl_texsource_winrt_capture);
ApplySetting3DMode(); //Syncs 3D state if needed
ApplySettingUpdateLimiter();
//Pause if not visible
if (!overlay.IsVisible())
{
DPWinRT_PauseCapture(overlay.GetHandle(), true);
}
if (ConfigManager::GetValue(configid_bool_windows_winrt_auto_focus))
{
WindowManager::Get().RaiseAndFocusWindow((HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd], &m_InputSim);
}
}
break;
}
else if (data.ConfigInt[configid_int_overlay_winrt_desktop_id] != -2)
{
if (DPWinRT_StartCaptureFromDesktop(overlay.GetHandle(), data.ConfigInt[configid_int_overlay_winrt_desktop_id]))
{
overlay.SetTextureSource(ovrl_texsource_winrt_capture);
ApplySetting3DMode();
ApplySettingUpdateLimiter();
//Pause if not visible
if (!overlay.IsVisible())
{
DPWinRT_PauseCapture(overlay.GetHandle(), true);
}
}
break;
}
}
//Couldn't set up capture, set source to none
overlay.SetTextureSource(ovrl_texsource_none);
}
break;
}
case ovrl_capsource_ui:
{
//Set texture source to UI if possible, which sets the rendering PID to the UI process
overlay.SetTextureSource(IPCManager::IsUIAppRunning() ? ovrl_texsource_ui : ovrl_texsource_none);
break;
}
case ovrl_capsource_browser:
{
if (overlay.GetTextureSource() != ovrl_texsource_browser)
{
bool has_started_browser = false;
if (DPBrowserAPIClient::Get().IsBrowserAvailable())
{
OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
//Load placeholder texture and apply input mode now since browser startup can take a few seconds
vr::VROverlay()->SetOverlayFromFile(overlay.GetHandle(), (ConfigManager::Get().GetApplicationPath() + "images/browser_load.png").c_str());
ApplySettingInputMode();
if (data.ConfigInt[configid_int_overlay_duplication_id] == -1)
{
DPBrowserAPIClient::Get().DPBrowser_StartBrowser(overlay.GetHandle(), data.ConfigStr[configid_str_overlay_browser_url],
data.ConfigBool[configid_bool_overlay_browser_allow_transparency]);
DPBrowserAPIClient::Get().DPBrowser_SetResolution(overlay.GetHandle(), data.ConfigInt[configid_int_overlay_user_width], data.ConfigInt[configid_int_overlay_user_height]);
DPBrowserAPIClient::Get().DPBrowser_SetFPS(overlay.GetHandle(), data.ConfigInt[configid_int_overlay_browser_max_fps_override]);
DPBrowserAPIClient::Get().DPBrowser_SetZoomLevel(overlay.GetHandle(), data.ConfigFloat[configid_float_overlay_browser_zoom]);
has_started_browser = true;
}
else
{
const Overlay& overlay_src = OverlayManager::Get().GetOverlay((unsigned int)data.ConfigInt[configid_int_overlay_duplication_id]);
//Source overlay may be invalid on launch or not have texture source set up if duplication ID is higher than this overlay's ID
if ( (overlay_src.GetHandle() != vr::k_ulOverlayHandleInvalid) && (overlay_src.GetTextureSource() == ovrl_texsource_browser) )
{
DPBrowserAPIClient::Get().DPBrowser_DuplicateBrowserOutput(overlay_src.GetHandle(), overlay.GetHandle());
DPBrowserAPIClient::Get().DPBrowser_SetFPS(overlay.GetHandle(), data.ConfigInt[configid_int_overlay_browser_max_fps_override]);
has_started_browser = true;
}
}
data.ConfigInt[configid_int_overlay_state_content_width] = data.ConfigInt[configid_int_overlay_user_width];
data.ConfigInt[configid_int_overlay_state_content_height] = data.ConfigInt[configid_int_overlay_user_height];
ApplySetting3DMode();
ApplySettingUpdateLimiter();
//Set pause state in case overlay is hidden or differs from duplication source
DPBrowserAPIClient::Get().DPBrowser_PauseBrowser(overlay.GetHandle(), !overlay.IsVisible());
}
//Set texture source to browser if possible, which sets the rendering PID to the browser server process
overlay.SetTextureSource((has_started_browser) ? ovrl_texsource_browser : ovrl_texsource_none);
}
break;
}
default:
{
//Unknown capture source, perhaps from the future. Set to texture source none.
overlay.SetTextureSource(ovrl_texsource_none);
}
}
ApplySettingExtraBrightness();
}
void OutputManager::ApplySetting3DMode()
{
const Overlay& overlay_current = OverlayManager::Get().GetCurrentOverlay();
const OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
vr::VROverlayHandle_t ovrl_handle = overlay_current.GetHandle();
bool is_enabled = ConfigManager::GetValue(configid_bool_overlay_3D_enabled);
int mode = ConfigManager::GetValue(configid_int_overlay_3D_mode);
//Override mode to none if texsource is none or the desktop duplication output is invalid
if ( (overlay_current.GetTextureSource() == ovrl_texsource_none) || ( (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication) && (m_OutputInvalid) ) )
{
is_enabled = false;
}
if (is_enabled)
{
if (data.ConfigBool[configid_bool_overlay_3D_swapped])
{
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_SideBySide_Parallel, false);
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_SideBySide_Crossed, true);
}
else
{
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_SideBySide_Parallel, true);
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_SideBySide_Crossed, false);
}
switch (mode)
{
case ovrl_3Dmode_hsbs:
{
vr::VROverlay()->SetOverlayTexelAspect(ovrl_handle, 2.0f);
break;
}
case ovrl_3Dmode_sbs:
case ovrl_3Dmode_ou: //Over-Under is converted to SBS
{
vr::VROverlay()->SetOverlayTexelAspect(ovrl_handle, 1.0f);
break;
}
case ovrl_3Dmode_hou: //Half-Over-Under is converted to SBS with half height
{
vr::VROverlay()->SetOverlayTexelAspect(ovrl_handle, 0.5f);
break;
}
default: break;
}
}
else
{
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_SideBySide_Parallel, false);
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_SideBySide_Crossed, false);
vr::VROverlay()->SetOverlayTexelAspect(ovrl_handle, 1.0f);
}
if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication)
{
if ( (is_enabled) && ((mode == ovrl_3Dmode_ou) || (mode == ovrl_3Dmode_hou)) )
{
OverlayManager::Get().GetCurrentOverlay().SetTextureSource(ovrl_texsource_desktop_duplication_3dou_converted);
}
else
{
OverlayManager::Get().GetCurrentOverlay().SetTextureSource(ovrl_texsource_desktop_duplication);
}
RefreshOpenVROverlayTexture(DPRect(-1, -1, -1, -1), true);
}
//WinRT OU3D state is set in ApplySettingCrop since it needs cropping values
ApplySettingCrop();
ApplySettingMouseScale();
}
void OutputManager::ApplySettingTransform()
{
Overlay& overlay = OverlayManager::Get().GetCurrentOverlay();
const OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
vr::VROverlayHandle_t ovrl_handle = overlay.GetHandle();
//Fixup overlay visibility if needed
//This has to be done first since there seem to be issues with moving invisible overlays
bool should_be_visible = overlay.ShouldBeVisible();
if ( (!should_be_visible) && (m_OvrlDashboardActive) && (m_OvrlDashboardActive) && (ConfigManager::GetValue(configid_bool_overlay_enabled)) &&
(ConfigManager::GetValue(configid_bool_state_overlay_dragselectmode_show_hidden)) )
{
should_be_visible = true;
overlay.SetOpacity(0.25f);
}
else if ( (!ConfigManager::GetValue(configid_bool_overlay_gazefade_enabled)) && (overlay.GetOpacity() != ConfigManager::GetValue(configid_float_overlay_opacity)) )
{
overlay.SetOpacity(ConfigManager::GetValue(configid_float_overlay_opacity));
should_be_visible = overlay.ShouldBeVisible(); //Re-evaluate this in case the overlay was left hidden after deactivating gaze fade
}
if ( (should_be_visible) && (!overlay.IsVisible()) )
{
ShowOverlay(overlay.GetID());
return; //ShowOverlay() calls this function so we back out here
}
else if ( (!should_be_visible) && (overlay.IsVisible()) )
{
HideOverlay(overlay.GetID());
}
unsigned int primary_dashboard_overlay_id = OverlayManager::Get().GetPrimaryDashboardOverlay().GetID();
bool is_primary_dashboard_overlay = (primary_dashboard_overlay_id == overlay.GetID());
float width = ConfigManager::GetValue(configid_float_overlay_width);
float height = 0.0f;
float dummy_height = 0.0f;
OverlayOrigin overlay_origin = (OverlayOrigin)ConfigManager::GetValue(configid_int_overlay_origin);
if (is_primary_dashboard_overlay)
{
height = GetOverlayHeight(overlay.GetID());
//Dashboard uses differently scaled transform depending on the current setting. We counteract that scaling to ensure the config value actually matches world scale
dummy_height = height / GetDashboardScale();
}
else
{
//Clear theater mode if this overlay is used as source and being disabled or no longer set to theater origin
if ((OverlayManager::Get().GetTheaterOverlayID() == overlay.GetID()) && ((!ConfigManager::GetValue(configid_bool_overlay_enabled)) || (overlay_origin != ovrl_origin_theater_screen)) )
{
OverlayManager::Get().ClearTheaterOverlay();
ResetCurrentOverlay(); //Reset overlay so all changes made to the theater one get reflected on the normal one
overlay.AssignDesktopDuplicationTexture(); //Re-assign Desktop Duplication Texture in case it has changed during theater mode
return;
}
else if ((ConfigManager::GetValue(configid_bool_overlay_enabled) && (overlay_origin == ovrl_origin_theater_screen))) //Enable theater mode if needed
{
ShowTheaterOverlay(overlay.GetID());
}
}
//Dashboard dummy still needs correct width/height set for the top dashboard bar above it to be visible
if ( (is_primary_dashboard_overlay) || (primary_dashboard_overlay_id == k_ulOverlayID_None) ) //When no dashboard overlay exists we set this on every overlay, not ideal.
{
float old_dummy_height = 0.0f;
vr::VROverlay()->GetOverlayWidthInMeters(m_OvrlHandleDashboardDummy, &old_dummy_height);
dummy_height = std::max(dummy_height + 0.30f, 1.5f); //Enforce minimum height to fit default height offset (which makes space for Floating UI)
//Sanity check. Things like inf can make the entire interface disappear
if (dummy_height > 20.0f)
{
dummy_height = 1.525f;
}
if (dummy_height != old_dummy_height)
{
//Delay setting the dummy height to after we made sure ApplySettingTransform() is not called right again
//Avoiding flicker properly unfortunately needs a sleep, so we can't call this right away or else we're just gonna stutter around
m_PendingDashboardDummyHeight = dummy_height;
}
}
//Update transform
vr::HmdMatrix34_t matrix = {0};
vr::TrackingUniverseOrigin universe_origin = vr::TrackingUniverseStanding;
switch (overlay_origin)
{
case ovrl_origin_room:
{
matrix = ConfigManager::Get().GetOverlayDetachedTransform().toOpenVR34();
//Offset transform by additional offset values
vr::IVRSystemEx::TransformOpenVR34TranslateRelative(matrix, ConfigManager::GetValue(configid_float_overlay_offset_right),
ConfigManager::GetValue(configid_float_overlay_offset_up),
ConfigManager::GetValue(configid_float_overlay_offset_forward));
vr::VROverlay()->SetOverlayTransformAbsolute(ovrl_handle, universe_origin, &matrix);
break;
}
case ovrl_origin_hmd_floor:
{
DetachedTransformFrameUpdate();
break;
}
case ovrl_origin_seated_universe:
{
Matrix4 matrix_base = m_OverlayDragger.GetBaseOffsetMatrix() * ConfigManager::Get().GetOverlayDetachedTransform();
//Offset transform by additional offset values
matrix_base.translate_relative(ConfigManager::GetValue(configid_float_overlay_offset_right),
ConfigManager::GetValue(configid_float_overlay_offset_up),
ConfigManager::GetValue(configid_float_overlay_offset_forward));
matrix = matrix_base.toOpenVR34();
vr::VROverlay()->SetOverlayTransformAbsolute(ovrl_handle, vr::TrackingUniverseStanding, &matrix);
break;
}
case ovrl_origin_dashboard:
{
Matrix4 matrix_base = m_OverlayDragger.GetBaseOffsetMatrix() * ConfigManager::Get().GetOverlayDetachedTransform();
//SteamVR 2.15.1 made changes that affected legacy dashboard positioning in... odd ways
//This more of a band-aid solution for the rarely used legacy dashboard, short of just not supporting it anymore
bool has_applied_bandaid_fix = false;
const bool is_v2_15 = (strstr(vr::VRSystem()->GetRuntimeVersion(), "2.15") != nullptr);
if (is_v2_15)
{
vr::VROverlayHandle_t handle_gamepad_ui = vr::k_ulOverlayHandleInvalid;
vr::VROverlay()->FindOverlay("valve.steam.gamepadui.bar", &handle_gamepad_ui);
if (handle_gamepad_ui == vr::k_ulOverlayHandleInvalid)
{
if (is_primary_dashboard_overlay)
{
vr::HmdMatrix34_t matrix_dplus_tab;
vr::TrackingUniverseOrigin origin = vr::TrackingUniverseStanding;
vr::VROverlay()->GetTransformForOverlayCoordinates(m_OvrlHandleDashboardDummy, origin, {0.5f, 0.5f}, &matrix_dplus_tab);
matrix_base = matrix_dplus_tab;
Vector3 translation = matrix_base.getTranslation();
matrix_base.setTranslation({0.0f, 0.0f, 0.0f});
matrix_base.scale(1.0f / GetDashboardScale());
matrix_base.setTranslation(translation);
has_applied_bandaid_fix = true;
//Yes, this does not apply the overlay's transform at all
}
}
}
//Offset transform by additional offset values
matrix_base.translate_relative(ConfigManager::GetValue(configid_float_overlay_offset_right),
ConfigManager::GetValue(configid_float_overlay_offset_up),
ConfigManager::GetValue(configid_float_overlay_offset_forward));
if (!has_applied_bandaid_fix)
{
//Apply origin offset, which basically adjusts transform to be aligned bottom-center instead of centered on both axes
if (height == 0.0f) //Get overlay height if it's not set yet
{
height = GetOverlayHeight(overlay.GetID());
}
matrix_base.translate_relative(0.0f, height / 2.0f, 0.0f);
}
matrix = matrix_base.toOpenVR34();
vr::VROverlay()->SetOverlayTransformAbsolute(ovrl_handle, universe_origin, &matrix);
break;
}
case ovrl_origin_hmd:
{
//Unsmoothed uses device-relative transform, but smoothed ones are absolute updated each frame, like HMD Floor origin
if (ConfigManager::GetValue(configid_int_overlay_origin_smoothing_level) == 0)
{
matrix = ConfigManager::Get().GetOverlayDetachedTransform().toOpenVR34();
//Offset transform by additional offset values
vr::IVRSystemEx::TransformOpenVR34TranslateRelative(matrix, ConfigManager::GetValue(configid_float_overlay_offset_right),
ConfigManager::GetValue(configid_float_overlay_offset_up),
ConfigManager::GetValue(configid_float_overlay_offset_forward));
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(ovrl_handle, vr::k_unTrackedDeviceIndex_Hmd, &matrix);
}
else
{
DetachedTransformFrameUpdate();
}
break;
}
case ovrl_origin_right_hand:
{
vr::TrackedDeviceIndex_t device_index = vr::VRSystem()->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_RightHand);
if (device_index != vr::k_unTrackedDeviceIndexInvalid)
{
matrix = ConfigManager::Get().GetOverlayDetachedTransform().toOpenVR34();
//Offset transform by additional offset values
vr::IVRSystemEx::TransformOpenVR34TranslateRelative(matrix, ConfigManager::GetValue(configid_float_overlay_offset_right),
ConfigManager::GetValue(configid_float_overlay_offset_up),
ConfigManager::GetValue(configid_float_overlay_offset_forward));
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(ovrl_handle, device_index, &matrix);
}
else //No controller connected, uh put it to 0?
{
vr::VROverlay()->SetOverlayTransformAbsolute(ovrl_handle, universe_origin, &matrix);
}
break;
}
case ovrl_origin_left_hand:
{
vr::TrackedDeviceIndex_t device_index = vr::VRSystem()->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_LeftHand);
if (device_index != vr::k_unTrackedDeviceIndexInvalid)
{
matrix = ConfigManager::Get().GetOverlayDetachedTransform().toOpenVR34();
//Offset transform by additional offset values
vr::IVRSystemEx::TransformOpenVR34TranslateRelative(matrix, ConfigManager::GetValue(configid_float_overlay_offset_right),
ConfigManager::GetValue(configid_float_overlay_offset_up),
ConfigManager::GetValue(configid_float_overlay_offset_forward));
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(ovrl_handle, device_index, &matrix);
}
else //No controller connected, uh put it to 0?
{
vr::VROverlay()->SetOverlayTransformAbsolute(ovrl_handle, universe_origin, &matrix);
}
break;
}
case ovrl_origin_aux:
{
vr::TrackedDeviceIndex_t index_tracker = vr::IVRSystemEx::GetFirstVRTracker();
if (index_tracker != vr::k_unTrackedDeviceIndexInvalid)
{
matrix = ConfigManager::Get().GetOverlayDetachedTransform().toOpenVR34();
//Offset transform by additional offset values
vr::IVRSystemEx::TransformOpenVR34TranslateRelative(matrix, ConfigManager::GetValue(configid_float_overlay_offset_right),
ConfigManager::GetValue(configid_float_overlay_offset_up),
ConfigManager::GetValue(configid_float_overlay_offset_forward));
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(ovrl_handle, index_tracker, &matrix);
}
else //Not connected, uh put it to 0?
{
vr::VROverlay()->SetOverlayTransformAbsolute(ovrl_handle, universe_origin, &matrix);
}
break;
}
}
//Update Width
vr::VROverlay()->SetOverlayWidthInMeters(ovrl_handle, width);
//Update Curvature
vr::VROverlay()->SetOverlayCurvature(ovrl_handle, ConfigManager::GetValue(configid_float_overlay_curvature));
//Update Brightness
//We use the logarithmic counterpart since the changes in higher steps are barely visible while the lower range can really use those additional steps
float brightness = lin2log(ConfigManager::GetValue(configid_float_overlay_brightness)) * ConfigManager::GetValue(configid_float_overlay_state_brightness_extra_multiplier);
vr::VROverlay()->SetOverlayColor(ovrl_handle, brightness, brightness, brightness);
//Set backside visibility
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_NoBackside, !data.ConfigBool[configid_bool_overlay_show_backside]);
//Set last tick for dashboard dummy delayed update
m_LastApplyTransformTick = ::GetTickCount64();
}
void OutputManager::ApplySettingCrop()
{
Overlay& overlay = OverlayManager::Get().GetCurrentOverlay();
OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
vr::VROverlayHandle_t ovrl_handle = overlay.GetHandle();
//UI overlays don't do any cropping and handle the texture bounds themselves
if (overlay.GetTextureSource() == ovrl_texsource_ui)
return;
//Set up overlay cropping
vr::VRTextureBounds_t tex_bounds;
vr::VRTextureBounds_t tex_bounds_prev;
const int content_width = data.ConfigInt[configid_int_overlay_state_content_width];
const int content_height = data.ConfigInt[configid_int_overlay_state_content_height];
if ( (overlay.GetTextureSource() == ovrl_texsource_none) || ((content_width == -1) && (content_height == -1)) )
{
tex_bounds.uMin = 0.0f;
tex_bounds.vMin = 0.0f;
tex_bounds.uMax = 1.0f;
tex_bounds.vMax = 1.0f;
vr::VROverlay()->SetOverlayTextureBounds(ovrl_handle, &tex_bounds);
return;
}
overlay.UpdateValidatedCropRect();
const DPRect& crop_rect = overlay.GetValidatedCropRect();
const bool is_3d_enabled = ConfigManager::GetValue(configid_bool_overlay_3D_enabled);
const int mode_3d = ConfigManager::GetValue(configid_int_overlay_3D_mode);
const bool is_ou3d = ( (is_3d_enabled) && ((mode_3d == ovrl_3Dmode_ou) || (mode_3d == ovrl_3Dmode_hou)) );
//Use full texture if everything checks out or 3D mode is Over-Under (converted to a 1:1 fitting texture)
if ( (is_ou3d) || ( (crop_rect.GetTL().x == 0) && (crop_rect.GetTL().y == 0) && (crop_rect.GetWidth() == content_width) && (crop_rect.GetHeight() == content_height) ) )
{
tex_bounds.uMin = 0.0f;
tex_bounds.vMin = 0.0f;
tex_bounds.uMax = 1.0f;
tex_bounds.vMax = 1.0f;
}
else
{
//Otherwise offset the calculated texel coordinates a bit. This is to reduce having colors from outside the cropping area bleeding in from the texture filtering
//This means the border pixels of the overlay are smaller, but that's something we need to accept it seems
//This doesn't 100% solve texel bleed, especially not on high overlay rendering quality where it can require pretty big offsets depending on overlay size/distance
float offset_x = (crop_rect.GetWidth() <= 2) ? 0.0f : 1.5f, offset_y = (crop_rect.GetHeight() <= 2) ? 0.0f : 1.5f; //Yes, we do handle the case of <3 pixel crops
tex_bounds.uMin = (crop_rect.GetTL().x + offset_x) / content_width;
tex_bounds.vMin = (crop_rect.GetTL().y + offset_y) / content_height;
tex_bounds.uMax = (crop_rect.GetBR().x - offset_x) / content_width;
tex_bounds.vMax = (crop_rect.GetBR().y - offset_y) / content_height;
}
//If capture source is WinRT, set 3D mode with cropping values
if (ConfigManager::GetValue(configid_int_overlay_capture_source) == ovrl_capsource_winrt_capture)
{
DPWinRT_SetOverlayOverUnder3D(ovrl_handle, is_ou3d, crop_rect.GetTL().x, crop_rect.GetTL().y, crop_rect.GetWidth(), crop_rect.GetHeight());
}
else if (ConfigManager::GetValue(configid_int_overlay_capture_source) == ovrl_capsource_browser) //Same with browser
{
DPBrowserAPIClient::Get().DPBrowser_SetOverUnder3D(ovrl_handle, is_ou3d, crop_rect.GetTL().x, crop_rect.GetTL().y, crop_rect.GetWidth(), crop_rect.GetHeight());
}
else
{
//For Desktop Duplication, compare old to new bounds to see if a full refresh is required
//However, for Over-Under 3D this won't work and we just always do the full refresh
bool needs_full_refresh = is_ou3d;
if (!needs_full_refresh)
{
vr::VROverlay()->GetOverlayTextureBounds(ovrl_handle, &tex_bounds_prev);
needs_full_refresh = ((tex_bounds.uMin < tex_bounds_prev.uMin) || (tex_bounds.vMin < tex_bounds_prev.vMin) ||
(tex_bounds.uMax > tex_bounds_prev.uMax) || (tex_bounds.vMax > tex_bounds_prev.vMax));
}
if (needs_full_refresh)
{
RefreshOpenVROverlayTexture(DPRect(-1, -1, -1, -1), true);
}
}
vr::VROverlay()->SetOverlayTextureBounds(ovrl_handle, &tex_bounds);
}
void OutputManager::ApplySettingInputMode()
{
//Apply/Restore mouse settings first
ApplySettingMouseInput();
const bool drag_or_select_mode_enabled = ( (ConfigManager::GetValue(configid_bool_state_overlay_dragmode)) || (ConfigManager::GetValue(configid_bool_state_overlay_selectmode)) );
//Always applies to all overlays
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayManager::Get().SetCurrentOverlayID(i);
const Overlay& overlay_current = OverlayManager::Get().GetCurrentOverlay();
vr::VROverlayHandle_t ovrl_handle = overlay_current.GetHandle();
if ((ConfigManager::GetValue(configid_bool_overlay_input_enabled)) || (drag_or_select_mode_enabled) )
{
//Don't activate drag mode for HMD origin when the pointer is also the HMD (or it's the dashboard overlay)
if ( ((ConfigManager::Get().GetPrimaryLaserPointerDevice() == vr::k_unTrackedDeviceIndex_Hmd) && (ConfigManager::GetValue(configid_int_overlay_origin) == ovrl_origin_hmd)) )
{
vr::VROverlay()->SetOverlayInputMethod(ovrl_handle, vr::VROverlayInputMethod_None);
}
else
{
vr::VROverlay()->SetOverlayInputMethod(ovrl_handle, vr::VROverlayInputMethod_Mouse);
}
}
else
{
vr::VROverlay()->SetOverlayInputMethod(ovrl_handle, vr::VROverlayInputMethod_None);
}
//Sync matrix if it's been turned off
if ( (!drag_or_select_mode_enabled) && (!ConfigManager::GetValue(configid_bool_state_overlay_dragmode)) )
{
DetachedTransformSync(i);
}
ApplySettingTransform();
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
void OutputManager::ApplySettingMouseInput()
{
//Set double-click assist duration from user config value
if (ConfigManager::GetValue(configid_int_input_mouse_dbl_click_assist_duration_ms) == -1)
{
ConfigManager::SetValue(configid_int_state_mouse_dbl_click_assist_duration_ms, ::GetDoubleClickTime());
}
else
{
ConfigManager::SetValue(configid_int_state_mouse_dbl_click_assist_duration_ms, ConfigManager::GetValue(configid_int_input_mouse_dbl_click_assist_duration_ms));
}
const bool drag_mode_enabled = ((ConfigManager::GetValue(configid_bool_state_overlay_dragmode)) || (m_OvrlDirectDragActive));
const bool select_mode_enabled = ConfigManager::GetValue(configid_bool_state_overlay_selectmode);
const bool drag_or_select_mode_enabled = ((drag_mode_enabled) || (select_mode_enabled));
//Always applies to all overlays
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayManager::Get().SetCurrentOverlayID(i);
Overlay& overlay = OverlayManager::Get().GetCurrentOverlay();
vr::VROverlayHandle_t ovrl_handle = overlay.GetHandle();
//Set input method (possibly overridden by ApplyInputMethod() right afterwards)
if ((ConfigManager::GetValue(configid_bool_overlay_input_enabled) || (drag_or_select_mode_enabled)))
{
//Temp drag needs every input-enabled overlay to have smooth scroll
if ( (ConfigManager::GetValue(configid_bool_input_mouse_scroll_smooth)) || (ConfigManager::GetValue(configid_bool_state_overlay_dragmode_temp)) || (drag_mode_enabled) )
{
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_SendVRDiscreteScrollEvents, false);
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_SendVRSmoothScrollEvents, true);
}
else
{
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_SendVRSmoothScrollEvents, false);
}
vr::VROverlay()->SetOverlayInputMethod(ovrl_handle, vr::VROverlayInputMethod_Mouse);
}
else
{
vr::VROverlay()->SetOverlayInputMethod(ovrl_handle, vr::VROverlayInputMethod_None);
}
//Set intersection blob state
bool hide_intersection = false;
if (OverlayManager::Get().GetTheaterOverlayID() == i) //Always hide if theater overlay
{
hide_intersection = true;
}
else if ( (overlay.GetTextureSource() != ovrl_texsource_none) && (overlay.GetTextureSource() != ovrl_texsource_ui) && (overlay.GetTextureSource() != ovrl_texsource_browser) &&
(!drag_mode_enabled) && (!select_mode_enabled) )
{
hide_intersection = !ConfigManager::GetValue(configid_bool_input_mouse_render_intersection_blob);
}
vr::VROverlay()->SetOverlayFlag(ovrl_handle, vr::VROverlayFlags_HideLaserIntersection, hide_intersection);
ApplySettingMouseScale();
//Set intersection mask for desktop duplication overlays
if (overlay.GetTextureSource() == ovrl_texsource_desktop_duplication)
{
std::vector primitives;
primitives.reserve(m_DesktopRects.size());
for (const DPRect& rect : m_DesktopRects)
{
vr::VROverlayIntersectionMaskPrimitive_t primitive;
primitive.m_nPrimitiveType = vr::OverlayIntersectionPrimitiveType_Rectangle;
primitive.m_Primitive.m_Rectangle.m_flTopLeftX = rect.GetTL().x - m_DesktopX;
primitive.m_Primitive.m_Rectangle.m_flTopLeftY = rect.GetTL().y - m_DesktopY;
primitive.m_Primitive.m_Rectangle.m_flWidth = rect.GetWidth();
primitive.m_Primitive.m_Rectangle.m_flHeight = rect.GetHeight();
primitives.push_back(primitive);
}
vr::EVROverlayError err = vr::VROverlay()->SetOverlayIntersectionMask(ovrl_handle, primitives.data(), (uint32_t)primitives.size());
}
else if (overlay.GetTextureSource() != ovrl_texsource_ui) //Or reset intersection mask if not UI overlay
{
vr::VROverlay()->SetOverlayIntersectionMask(ovrl_handle, nullptr, 0);
}
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
void OutputManager::ApplySettingMouseScale()
{
Overlay& overlay = OverlayManager::Get().GetCurrentOverlay();
OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
vr::VROverlayHandle_t ovrl_handle = overlay.GetHandle();
//UI overlays handle the mouse scale themselves
if (overlay.GetTextureSource() == ovrl_texsource_ui)
return;
//Set mouse scale based on capture source
vr::HmdVector2_t mouse_scale = {0};
if (overlay.GetTextureSource() == ovrl_texsource_none)
{
//The mouse scale defines the surface aspect ratio for the intersection test... yeah. If it's off there will be hits over empty space, so try to match it even here
//uint32_t ovrl_tex_width = 1, ovrl_tex_height = 1;
//Content size might not be what the current texture size is in case of ovrl_texsource_none
/*if (vr::VROverlay()->GetOverlayTextureSize(ovrl_handle, &ovrl_tex_width, &ovrl_tex_height) == vr::VROverlayError_None) //GetOverlayTextureSize() currently leaks, so don't use it
{
mouse_scale.v[0] = ovrl_tex_width;
mouse_scale.v[1] = ovrl_tex_height;
}
else*/ //ovrl_texsource_none pretty much means overlay output error texture, so fall back to that if we can't get the real size
{
mouse_scale.v[0] = k_lOverlayOutputErrorTextureWidth;
mouse_scale.v[1] = k_lOverlayOutputErrorTextureHeight;
}
}
else
{
switch (data.ConfigInt[configid_int_overlay_capture_source])
{
case ovrl_capsource_desktop_duplication:
{
mouse_scale.v[0] = m_DesktopWidth;
mouse_scale.v[1] = m_DesktopHeight;
break;
}
case ovrl_capsource_winrt_capture:
case ovrl_capsource_browser:
{
//Use duplication IDs' data if any is set
int duplication_id = ConfigManager::GetValue(configid_int_overlay_duplication_id);
const OverlayConfigData& overlay_data = (duplication_id != -1) ? OverlayManager::Get().GetConfigData((unsigned int)duplication_id) : data;
mouse_scale.v[0] = overlay_data.ConfigInt[configid_int_overlay_state_content_width];
mouse_scale.v[1] = overlay_data.ConfigInt[configid_int_overlay_state_content_height];
break;
}
}
//Adjust for 3D so surface aspect ratio for laser pointing matches what is seen.
//This blocks half or more pixels, but it's not clear what the real target coordinates are with content being different in each eye
if (data.ConfigBool[configid_bool_overlay_3D_enabled])
{
switch (data.ConfigInt[configid_int_overlay_3D_mode])
{
case ovrl_3Dmode_hsbs:
case ovrl_3Dmode_sbs:
{
mouse_scale.v[0] /= 2.0f;
break;
}
case ovrl_3Dmode_hou:
case ovrl_3Dmode_ou:
{
//OU converted to SBS will have texture size based on crop rect
const DPRect& crop_rect = overlay.GetValidatedCropRect();
mouse_scale.v[0] = crop_rect.GetWidth();
mouse_scale.v[1] = crop_rect.GetHeight() / 2.0f;
}
}
}
}
vr::VROverlay()->SetOverlayMouseScale(ovrl_handle, &mouse_scale);
}
void OutputManager::ApplySettingUpdateLimiter()
{
//Here's the deal with the fps-based limiter: It just barely works
//A simple fps cut-off doesn't work since mouse updates add up to them
//Using the right frame time value seems to work in most cases
//A full-range, user-chosen fps value doesn't really work though, as the frame time values required don't seem to predictably change ("1000/fps" is close, but the needed adjustment varies)
//The frame time method also doesn't work reliably above 50 fps. It limits, but the resulting fps isn't constant.
//This is why the fps limiter is somewhat restricted in what settings it offers. It does cover the most common cases, however.
//The frame time limiter is still there to offer more fine-tuning after all
//Map tested frame time values to the fps enum IDs
//FPS: 1 2 5 10 15 20 25 30 40 50
const float fps_enum_values_ms[] = { 985.0f, 485.0f, 195.0f, 96.50f, 63.77f, 47.76f, 33.77f, 31.73f, 23.72f, 15.81f };
float limit_ms = 0.0f;
//Set limiter value from global setting
if (ConfigManager::GetValue(configid_int_performance_update_limit_mode) == update_limit_mode_ms)
{
limit_ms = ConfigManager::GetValue(configid_float_performance_update_limit_ms);
}
else if (ConfigManager::GetValue(configid_int_performance_update_limit_mode) == update_limit_mode_fps)
{
int enum_id = ConfigManager::GetValue(configid_int_performance_update_limit_fps);
if (enum_id <= update_limit_fps_50)
{
limit_ms = fps_enum_values_ms[enum_id];
}
}
LARGE_INTEGER limit_delay_global = {0};
limit_delay_global.QuadPart = 1000.0f * limit_ms;
//See if there are any overrides from visible overlays
//This is the straight forward and least error-prone way, not quite the most efficient one
//Calls to this are minimized and there typically aren't many overlays so it's not really that bad (and we do iterate over all of them in many other places too)
bool is_first_override = true;
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const Overlay& overlay = OverlayManager::Get().GetOverlay(i);
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if ( (overlay.IsVisible()) && (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_desktop_duplication) &&
(data.ConfigInt[configid_int_overlay_update_limit_override_mode] != update_limit_mode_off) )
{
float override_ms = 0.0f;
if (data.ConfigInt[configid_int_overlay_update_limit_override_mode] == update_limit_mode_ms)
{
override_ms = data.ConfigFloat[configid_float_overlay_update_limit_override_ms];
}
else
{
int enum_id = data.ConfigInt[configid_int_overlay_update_limit_override_fps];
if (enum_id <= update_limit_fps_50)
{
override_ms = fps_enum_values_ms[enum_id];
}
}
//Use override if it results in more updates (except first override, which always has priority over global setting)
if ( (is_first_override) || (override_ms < limit_ms) )
{
limit_ms = override_ms;
is_first_override = false;
}
}
else if (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_winrt_capture) //Set limit values for WinRT overlays as well
{
LARGE_INTEGER limit_delay = limit_delay_global;
if (data.ConfigInt[configid_int_overlay_update_limit_override_mode] == update_limit_mode_ms)
{
limit_delay.QuadPart = 1000.0f * data.ConfigFloat[configid_float_overlay_update_limit_override_ms];
}
else if (data.ConfigInt[configid_int_overlay_update_limit_override_mode] == update_limit_mode_fps)
{
int enum_id = data.ConfigInt[configid_int_overlay_update_limit_override_fps];
if (enum_id <= update_limit_fps_50)
{
limit_delay.QuadPart = 1000.0f * fps_enum_values_ms[enum_id];
}
}
//Calling this regardless of change might be overkill, but doesn't seem too bad for now
DPWinRT_SetOverlayUpdateLimitDelay(overlay.GetHandle(), limit_delay.QuadPart);
}
}
m_PerformanceUpdateLimiterDelay.QuadPart = 1000.0f * limit_ms;
}
void OutputManager::ApplySettingExtraBrightness()
{
Overlay& overlay = OverlayManager::Get().GetCurrentOverlay();
OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
float extra_brightness_mulitplier = 1.0f;
if (ConfigManager::GetValue(configid_bool_performance_hdr_mirroring))
{
const bool ignore_wmr_vscreens = (ConfigManager::GetValue(configid_int_interface_wmr_ignore_vscreens) == 1);
switch (overlay.GetTextureSource())
{
case ovrl_texsource_desktop_duplication:
case ovrl_texsource_desktop_duplication_3dou_converted:
{
extra_brightness_mulitplier = GetDesktopHDRWhiteLevelAdjustment(data.ConfigInt[configid_int_overlay_desktop_id], false, ignore_wmr_vscreens);
break;
}
case ovrl_texsource_winrt_capture:
{
int desktop_id = data.ConfigInt[configid_int_overlay_winrt_desktop_id];
//Window capture, need to find the desktop ID it's on
if (desktop_id == -2)
{
HMONITOR monitor_handle = ::MonitorFromWindow((HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd], MONITOR_DEFAULTTONEAREST);
desktop_id = GetDisplayIDFromHMonitor(monitor_handle, ignore_wmr_vscreens);
}
extra_brightness_mulitplier = GetDesktopHDRWhiteLevelAdjustment(desktop_id, true, ignore_wmr_vscreens);
}
default: break;
}
}
if (data.ConfigFloat[configid_float_overlay_state_brightness_extra_multiplier] != extra_brightness_mulitplier)
{
data.ConfigFloat[configid_float_overlay_state_brightness_extra_multiplier] = extra_brightness_mulitplier;
//Send change over to UI
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, (int)overlay.GetID());
IPCManager::Get().PostConfigMessageToUIApp(configid_float_overlay_state_brightness_extra_multiplier, extra_brightness_mulitplier);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
}
}
void OutputManager::DetachedTransformSync(unsigned int overlay_id)
{
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_transform_sync_target_id, (int)overlay_id);
const float* values = OverlayManager::Get().GetConfigData(overlay_id).ConfigTransform.get();
for (size_t i = 0; i < 16; ++i)
{
IPCManager::Get().PostConfigMessageToUIApp(configid_float_state_overlay_transform_sync_value, values[i]);
}
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_transform_sync_target_id, -1);
}
void OutputManager::DetachedTransformSyncAll()
{
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
DetachedTransformSync(i);
}
}
void OutputManager::DetachedTransformReset(unsigned int overlay_id_ref)
{
vr::TrackingUniverseOrigin universe_origin = vr::TrackingUniverseStanding;
Matrix4& transform = ConfigManager::Get().GetOverlayDetachedTransform();
transform.identity(); //Reset to identity
OverlayOrigin overlay_origin = (OverlayOrigin)ConfigManager::GetValue(configid_int_overlay_origin);
const Overlay& overlay_ref = OverlayManager::Get().GetOverlay(overlay_id_ref);
vr::VROverlayHandle_t ovrl_handle_ref = overlay_ref.GetHandle(); //Results in vr::k_ulOverlayHandleInvalid for k_ulOverlayID_None
const bool dont_use_reference = (overlay_origin >= ovrl_origin_hmd) || ( (overlay_origin == ovrl_origin_hmd_floor) && (ConfigManager::GetValue(configid_bool_overlay_origin_hmd_floor_use_turning)) );
if ( (ovrl_handle_ref == vr::k_ulOverlayHandleInvalid) && (!dont_use_reference) )
{
const Overlay& overlay_dashboard = OverlayManager::Get().GetPrimaryDashboardOverlay();
if (overlay_dashboard.GetID() != OverlayManager::Get().GetCurrentOverlayID())
{
overlay_id_ref = overlay_dashboard.GetID();
ovrl_handle_ref = overlay_dashboard.GetHandle();
}
}
//Position next to reference if available
if (ovrl_handle_ref != vr::k_ulOverlayHandleInvalid)
{
OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
const OverlayConfigData& data_ref = OverlayManager::Get().GetConfigData(overlay_id_ref);
vr::HmdMatrix34_t overlay_transform;
vr::HmdVector2_t mouse_scale;
bool ref_overlay_changed = false;
float ref_overlay_alpha_orig = 0.0f;
//GetTransformForOverlayCoordinates() won't work if the reference overlay is not visible, so make it "visible" by showing it with 0% alpha
if (!vr::VROverlay()->IsOverlayVisible(ovrl_handle_ref))
{
vr::VROverlay()->GetOverlayAlpha(ovrl_handle_ref, &ref_overlay_alpha_orig);
vr::VROverlay()->SetOverlayAlpha(ovrl_handle_ref, 0.0f);
vr::VROverlay()->ShowOverlay(ovrl_handle_ref);
//Showing overlays and getting coordinates from them has a race condition if it's the first time the overlay is shown
//Doesn't seem like it can be truly detected when it's ready, so as cheap as it is, this Sleep() seems to get around the issue
::Sleep(50);
ref_overlay_changed = true;
}
//Get mouse scale for overlay coordinate offset
vr::VROverlay()->GetOverlayMouseScale(ovrl_handle_ref, &mouse_scale);
//Get x-offset multiplier, taking width differences into account
float ref_overlay_width;
vr::VROverlay()->GetOverlayWidthInMeters(ovrl_handle_ref, &ref_overlay_width);
float dashboard_scale = GetDashboardScale();
float x_offset_mul = ( (data.ConfigFloat[configid_float_overlay_width] / ref_overlay_width) / 2.0f) + 1.0f;
//Put it next to the reference overlay so it can actually be seen
vr::HmdVector2_t coordinate_offset = {mouse_scale.v[0] * x_offset_mul, mouse_scale.v[1] / 2.0f};
vr::VROverlay()->GetTransformForOverlayCoordinates(ovrl_handle_ref, universe_origin, coordinate_offset, &overlay_transform);
transform = overlay_transform;
//Counteract additonal offset that might've been present on the transform
transform.translate_relative(-data_ref.ConfigFloat[configid_float_overlay_offset_right],
-data_ref.ConfigFloat[configid_float_overlay_offset_up],
-data_ref.ConfigFloat[configid_float_overlay_offset_forward]);
//Counteract origin offset for dashboard origin overlays
if (overlay_origin == ovrl_origin_dashboard)
{
float height = GetOverlayHeight(overlay_id_ref);
transform.translate_relative(0.0f, height / -2.0f, 0.0f);
}
//Restore reference overlay state if it was changed
if (ref_overlay_changed)
{
vr::VROverlay()->HideOverlay(ovrl_handle_ref);
vr::VROverlay()->SetOverlayAlpha(ovrl_handle_ref, ref_overlay_alpha_orig);
}
//If the reference overlay appears to be below ground we assume it has an invalid origin (i.e. dashboard tab never opened for dashboard overlay) and use the fallback transform
if (transform.getTranslation().y < 0.0f)
{
transform = GetFallbackOverlayTransform();
}
//Adapt to base offset for non-room origins
if (overlay_origin != ovrl_origin_room)
{
Matrix4 transform_base = m_OverlayDragger.GetBaseOffsetMatrix();
transform_base.invert();
transform = transform_base * transform;
}
}
else //Otherwise add some default offset to the previously reset to identity value
{
switch (overlay_origin)
{
case ovrl_origin_room:
case ovrl_origin_hmd_floor:
{
//Put it in front of the user's face instead when turning is enabled
if ( (overlay_origin == ovrl_origin_hmd_floor) && (ConfigManager::GetValue(configid_bool_overlay_origin_hmd_floor_use_turning)) )
{
transform.translate_relative(0.0f, 1.0f, -2.5f);
break;
}
vr::HmdMatrix34_t overlay_transform = {0};
vr::VROverlay()->GetTransformForOverlayCoordinates(m_OvrlHandleDashboardDummy, vr::TrackingUniverseStanding, {0.5f, 0.5f}, &overlay_transform);
transform = overlay_transform;
//Use fallback transform if dashboard dummy is still set to identity (never opened Desktop+ dashboard tab)
if (transform == Matrix4())
{
transform = GetFallbackOverlayTransform();
}
//Adapt to base offset for non-room origin
if (overlay_origin != ovrl_origin_room)
{
Matrix4 transform_base = m_OverlayDragger.GetBaseOffsetMatrix();
transform_base.invert();
transform = transform_base * transform;
}
//Remove dashboard scale from transform
Vector3 translation = transform.getTranslation();
transform.setTranslation({0.0f, 0.0f, 0.0f});
transform.scale(1.0f / GetDashboardScale());
transform.setTranslation(translation);
break;
}
case ovrl_origin_seated_universe:
{
transform.translate_relative(0.0f, 0.0f, -1.0f);
break;
}
case ovrl_origin_dashboard:
{
//Counteract dashboard scale applied by origin
transform.scale(1.0f / GetDashboardScale());
break;
}
case ovrl_origin_hmd:
{
transform.translate_relative(0.0f, 0.0f, -2.5f);
break;
}
case ovrl_origin_right_hand:
{
//Set it to a controller component so it doesn't appear right where the laser pointer comes out
//There's some doubt about this code, but it seems to work in the end (is there really no better way?)
char buffer[vr::k_unMaxPropertyStringSize];
vr::VRSystem()->GetStringTrackedDeviceProperty(vr::VRSystem()->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_RightHand),
vr::Prop_RenderModelName_String, buffer, vr::k_unMaxPropertyStringSize);
vr::VRInputValueHandle_t input_value = vr::k_ulInvalidInputValueHandle;
vr::VRInput()->GetInputSourceHandle("/user/hand/right", &input_value);
vr::RenderModel_ControllerMode_State_t controller_state = {0};
vr::RenderModel_ComponentState_t component_state = {0};
if (vr::VRRenderModels()->GetComponentStateForDevicePath(buffer, vr::k_pch_Controller_Component_HandGrip, input_value, &controller_state, &component_state))
{
transform = component_state.mTrackingToComponentLocal;
transform.rotateX(-90.0f);
transform.translate_relative(0.0f, -0.1f, 0.0f); //This seems like a good default, at least for Index controllers
}
break;
}
case ovrl_origin_left_hand:
{
char buffer[vr::k_unMaxPropertyStringSize];
vr::VRSystem()->GetStringTrackedDeviceProperty(vr::VRSystem()->GetTrackedDeviceIndexForControllerRole(vr::TrackedControllerRole_LeftHand),
vr::Prop_RenderModelName_String, buffer, vr::k_unMaxPropertyStringSize);
vr::VRInputValueHandle_t input_value = vr::k_ulInvalidInputValueHandle;
vr::VRInput()->GetInputSourceHandle("/user/hand/left", &input_value);
vr::RenderModel_ControllerMode_State_t controller_state = {0};
vr::RenderModel_ComponentState_t component_state = {0};
if (vr::VRRenderModels()->GetComponentStateForDevicePath(buffer, vr::k_pch_Controller_Component_HandGrip, input_value, &controller_state, &component_state))
{
transform = component_state.mTrackingToComponentLocal;
transform.rotateX(-90.0f);
transform.translate_relative(0.0f, -0.1f, 0.0f);
}
break;
}
case ovrl_origin_aux:
{
transform.translate_relative(0.0f, 0.0f, -0.05f);
break;
}
default: break;
}
}
//Sync reset with UI app
DetachedTransformSync(OverlayManager::Get().GetCurrentOverlayID());
ApplySettingTransform();
}
void OutputManager::DetachedTransformAdjust(unsigned int packed_value)
{
Matrix4& transform = ConfigManager::Get().GetOverlayDetachedTransform();
//Unpack
IPCActionOverlayPosAdjustTarget target = (IPCActionOverlayPosAdjustTarget)(packed_value & 0xF);
bool increase = (packed_value >> 4);
//"To HMD" / LookAt button, seperate code path entirely
if (target == ipcactv_ovrl_pos_adjust_lookat)
{
//Get HMD pose
vr::TrackedDevicePose_t poses[vr::k_unTrackedDeviceIndex_Hmd + 1];
vr::TrackingUniverseOrigin universe_origin = vr::TrackingUniverseStanding;
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(universe_origin, vr::IVRSystemEx::GetTimeNowToPhotons(), poses, vr::k_unTrackedDeviceIndex_Hmd + 1);
if (poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid)
{
Matrix4 mat_base_offset = m_OverlayDragger.GetBaseOffsetMatrix();
//Preserve scaling from transform, which can be present in matrices originating from the dashboard
Vector3 row_1(transform[0], transform[1], transform[2]);
float scale_x = row_1.length(); //Scaling is always uniform so we just check the x-axis
//Dashboard origin itself also contains scale, so take the base scale in account as well
Vector3 row_1_base(mat_base_offset[0], mat_base_offset[1], mat_base_offset[2]);
float scale_x_base = row_1_base.length();
scale_x *= scale_x_base;
//Rotate towards HMD position
Matrix4 mat_hmd(poses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking);
Matrix4 mat_lookat = mat_base_offset * transform; //Apply base offset for LookAt
//Apply dashboard origin offset if needed
float overlay_height = 0.0f;
if (ConfigManager::GetValue(configid_int_overlay_origin) == ovrl_origin_dashboard)
{
overlay_height = GetOverlayHeight(OverlayManager::Get().GetCurrentOverlayID());
mat_lookat.translate_relative(0.0f, overlay_height / 2.0f, 0.0f);
}
vr::IVRSystemEx::TransformLookAt(mat_lookat, mat_hmd.getTranslation());
//Remove dashboard origin offset again
if (ConfigManager::GetValue(configid_int_overlay_origin) == ovrl_origin_dashboard)
{
mat_lookat.translate_relative(0.0f, overlay_height / -2.0f, 0.0f);
}
//Remove base offset again
mat_base_offset.invert();
mat_lookat = mat_base_offset * mat_lookat;
//Restore scale factor
mat_lookat.setTranslation({0.0f, 0.0f, 0.0f});
mat_lookat.scale(scale_x);
mat_lookat.setTranslation(transform.getTranslation());
transform = mat_lookat;
ApplySettingTransform();
}
return;
}
Matrix4 mat_back;
if (target >= ipcactv_ovrl_pos_adjust_rotx)
{
//Perform rotation locally
mat_back = transform;
transform.identity();
}
float distance = 0.05f;
float angle = 1.0f;
//Use snap size as distance if drag snapping is enabled
if (ConfigManager::GetValue(configid_bool_input_drag_snap_position))
{
distance = ConfigManager::GetValue(configid_float_input_drag_snap_position_size);
}
if (!increase)
{
distance *= -1.0f;
angle *= -1.0f;
}
switch (target)
{
case ipcactv_ovrl_pos_adjust_updown: transform.translate_relative(0.0f, distance, 0.0f); break;
case ipcactv_ovrl_pos_adjust_rightleft: transform.translate_relative(distance, 0.0f, 0.0f); break;
case ipcactv_ovrl_pos_adjust_forwback: transform.translate_relative(0.0f, 0.0f, distance); break;
case ipcactv_ovrl_pos_adjust_rotx: transform.rotateX(angle); break;
case ipcactv_ovrl_pos_adjust_roty: transform.rotateY(angle); break;
case ipcactv_ovrl_pos_adjust_rotz: transform.rotateZ(angle); break;
}
if (target >= ipcactv_ovrl_pos_adjust_rotx)
{
transform = mat_back * transform;
}
ApplySettingTransform();
}
void OutputManager::DetachedTransformConvertOrigin(unsigned int overlay_id, OverlayOrigin origin_from, OverlayOrigin origin_to)
{
if (origin_from == origin_to)
return;
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(overlay_id);
const OverlayOriginConfig origin_config = OverlayManager::Get().GetOriginConfigFromData(data);
DetachedTransformConvertOrigin(overlay_id, origin_from, origin_to, origin_config, origin_config);
}
void OutputManager::DetachedTransformConvertOrigin(unsigned int overlay_id, OverlayOrigin origin_from, OverlayOrigin origin_to,
const OverlayOriginConfig& origin_config_from, const OverlayOriginConfig& origin_config_to)
{
Overlay& overlay = OverlayManager::Get().GetOverlay(overlay_id);
OverlayConfigData& data = OverlayManager::Get().GetConfigData(overlay_id);
Matrix4& transform = data.ConfigTransform;
//With origin smoothing, just take the current in-between overlay transform even if it's technically less correct
bool has_direct_transform = false;
if ( (origin_from == ovrl_origin_hmd_floor) && ((origin_from == ovrl_origin_hmd) && (data.ConfigInt[configid_int_overlay_origin_smoothing_level] != 0)) )
{
//Only trust that transform if the overlay is visible, however
if (vr::VROverlay()->IsOverlayVisible(overlay.GetHandle()))
{
vr::HmdMatrix34_t transform_ovr;
vr::TrackingUniverseOrigin origin;
vr::VROverlay()->GetOverlayTransformAbsolute(overlay.GetHandle(), &origin, &transform_ovr);
transform = transform_ovr;
//Undo additional offset present in transform
transform.translate_relative(-ConfigManager::GetValue(configid_float_overlay_offset_right),
-ConfigManager::GetValue(configid_float_overlay_offset_up),
-ConfigManager::GetValue(configid_float_overlay_offset_forward));
has_direct_transform = true;
}
}
//Usual case, not HMD Floor or origin smoothing applied
if (!has_direct_transform)
{
OverlayOriginConfig origin_config = OverlayManager::Get().GetOriginConfigFromData(data);
Matrix4 mat_origin_from = m_OverlayDragger.GetBaseOffsetMatrix(origin_from, origin_config_from);
//Apply origin-from matrix to get absolute transform
transform = mat_origin_from * transform;
if ( (origin_from == ovrl_origin_dashboard) || (origin_to == ovrl_origin_dashboard) )
{
//Apply or counteract origin offset used in ApplySettingTransform for dashboard origin
float height = GetOverlayHeight(overlay_id);
transform.translate_relative(0.0f, (origin_from == ovrl_origin_dashboard) ? height / 2.0f : height / -2.0f, 0.0f);
}
}
//Apply inverse of origin-to matrix to get transform relative to new origin
Matrix4 mat_origin_to_inverse = m_OverlayDragger.GetBaseOffsetMatrix(origin_to, origin_config_to);
mat_origin_to_inverse.invert();
transform = mat_origin_to_inverse * transform;
//Sync transform so the UI doesn't have the wrong idea if no drag occurs after this
DetachedTransformSync(overlay_id);
//Reset smoothers to avoid potential hitching from previous uses
overlay.GetSmootherPos().ResetLastPos();
overlay.GetSmootherRot().ResetLastPos();
}
bool OutputManager::DetachedTransformFrameUpdate()
{
const OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
//Skip if no frame level adjustments are needed
if ( (data.ConfigInt[configid_int_overlay_origin] != ovrl_origin_hmd_floor) &&
((data.ConfigInt[configid_int_overlay_origin] != ovrl_origin_hmd) || (data.ConfigInt[configid_int_overlay_origin_smoothing_level] == 0)) )
{
return false;
}
//We need to update the overlay pos at a somewhat constant rate for smoothing to be actually smooth
//The way RadialFollowCore works doesn't take time into account however
//Ideally this would be offloaded into a separate thread doing this at a truly fixed rate, but for now we just skip if we're going too fast (active overlays already force minimum update rate)
if (::GetTickCount64() < m_LastFrameTransformUpdateTick + m_MaxActiveRefreshDelay - 3)
{
return false;
}
Overlay& overlay = OverlayManager::Get().GetCurrentOverlay();
Matrix4 matrix = m_OverlayDragger.GetBaseOffsetMatrix();
matrix *= ConfigManager::Get().GetOverlayDetachedTransform();
//Offset transform by additional offset values
matrix.translate_relative(data.ConfigFloat[configid_float_overlay_offset_right],
data.ConfigFloat[configid_float_overlay_offset_up],
data.ConfigFloat[configid_float_overlay_offset_forward]);
//Use overlay's smoothers to filter the new matrix' position and rotation
if (data.ConfigInt[configid_int_overlay_origin_smoothing_level] != 0)
{
DetachedTransformFrameUpdateApplySmoothingParameters(overlay, data.ConfigInt[configid_int_overlay_origin_smoothing_level]);
matrix.setTranslation( overlay.GetSmootherPos().Filter(matrix.getTranslation()) );
matrix.setRotation( overlay.GetSmootherRot().FilterWrapped(matrix.getRotation(), 0.0f, 360.0f) );
}
vr::HmdMatrix34_t matrix_ovr = matrix.toOpenVR34();
vr::VROverlay()->SetOverlayTransformAbsolute(OverlayManager::Get().GetCurrentOverlay().GetHandle(), vr::TrackingUniverseStanding, &matrix_ovr);
return true;
}
void OutputManager::DetachedTransformFrameUpdateApplySmoothingParameters(Overlay& overlay, int preset_id)
{
preset_id = clamp(preset_id, 0, 5);
RadialFollowCore& smoother_pos = overlay.GetSmootherPos();
RadialFollowCore& smoother_rot = overlay.GetSmootherRot();
switch (preset_id)
{
case 0: //Not really used, calling Filter() is skipped entirely instead
{
smoother_pos.SetOuterRadius(0.0);
smoother_pos.SetInnerRadius(0.0);
smoother_pos.SetSmoothingCoefficient(0.0);
smoother_pos.SetSoftKneeScale(0.0);
smoother_pos.SetSmoothingLeakCoefficient(0.0);
smoother_rot.SetOuterRadius(0.0);
smoother_rot.SetInnerRadius(0.0);
smoother_rot.SetSmoothingCoefficient(0.0);
smoother_rot.SetSoftKneeScale(0.0);
smoother_rot.SetSmoothingLeakCoefficient(0.0);
break;
}
case 1:
{
smoother_pos.SetOuterRadius(0.0);
smoother_pos.SetInnerRadius(0.0);
smoother_pos.SetSmoothingCoefficient(0.85);
smoother_pos.SetSoftKneeScale(1.0);
smoother_pos.SetSmoothingLeakCoefficient(1.0);
smoother_rot.SetOuterRadius(0.0);
smoother_rot.SetInnerRadius(0.0);
smoother_rot.SetSmoothingCoefficient(0.8);
smoother_rot.SetSoftKneeScale(1.0);
smoother_rot.SetSmoothingLeakCoefficient(1.0);
break;
}
case 2:
{
smoother_pos.SetOuterRadius(0.0);
smoother_pos.SetInnerRadius(0.0);
smoother_pos.SetSmoothingCoefficient(0.90);
smoother_pos.SetSoftKneeScale(1.0);
smoother_pos.SetSmoothingLeakCoefficient(0.95);
smoother_rot.SetOuterRadius(0.5);
smoother_rot.SetInnerRadius(0.0);
smoother_rot.SetSmoothingCoefficient(0.85);
smoother_rot.SetSoftKneeScale(1.0);
smoother_rot.SetSmoothingLeakCoefficient(1.0);
break;
}
case 3:
{
smoother_pos.SetOuterRadius(0.02);
smoother_pos.SetInnerRadius(0.01);
smoother_pos.SetSmoothingCoefficient(0.95);
smoother_pos.SetSoftKneeScale(1.0);
smoother_pos.SetSmoothingLeakCoefficient(0.90);
smoother_rot.SetOuterRadius(1.0);
smoother_rot.SetInnerRadius(0.5);
smoother_rot.SetSmoothingCoefficient(0.75);
smoother_rot.SetSoftKneeScale(1.0);
smoother_rot.SetSmoothingLeakCoefficient(1.00);
break;
}
case 4:
{
smoother_pos.SetOuterRadius(0.10);
smoother_pos.SetInnerRadius(0.10);
smoother_pos.SetSmoothingCoefficient(0.93);
smoother_pos.SetSoftKneeScale(1.0);
smoother_pos.SetSmoothingLeakCoefficient(0.97);
smoother_rot.SetOuterRadius(7.5);
smoother_rot.SetInnerRadius(7.5);
smoother_rot.SetSmoothingCoefficient(0.75);
smoother_rot.SetSoftKneeScale(0.8);
smoother_rot.SetSmoothingLeakCoefficient(1.0);
break;
}
case 5:
{
smoother_pos.SetOuterRadius(0.25);
smoother_pos.SetInnerRadius(0.20);
smoother_pos.SetSmoothingCoefficient(0.95);
smoother_pos.SetSoftKneeScale(1.0);
smoother_pos.SetSmoothingLeakCoefficient(0.97);
smoother_rot.SetOuterRadius(30.0);
smoother_rot.SetInnerRadius(10.0);
smoother_rot.SetSmoothingCoefficient(0.96);
smoother_rot.SetSoftKneeScale(0.80);
smoother_rot.SetSmoothingLeakCoefficient(0.85);
break;
}
}
}
void OutputManager::DetachedTransformUpdateSeatedPosition()
{
Matrix4 mat_seated_zero = vr::VRSystem()->GetSeatedZeroPoseToStandingAbsoluteTrackingPose();
//Sounds stupid, but we can be too fast to react to position updates and get the old seated zero pose as a result... so let's wait once if that happens
if (mat_seated_zero == m_SeatedTransformLast)
{
::Sleep(100);
mat_seated_zero = vr::VRSystem()->GetSeatedZeroPoseToStandingAbsoluteTrackingPose();
}
//Update transforms of relevant overlays
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
OverlayManager::Get().SetCurrentOverlayID(i);
if (ConfigManager::GetValue(configid_int_overlay_origin) == ovrl_origin_seated_universe)
{
ApplySettingTransform();
}
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
m_SeatedTransformLast = mat_seated_zero;
}
void OutputManager::DetachedInteractionAutoToggleAll()
{
//Don't change flags while any drag is currently active
if ( (m_OverlayDragger.IsDragActive()) || (m_OverlayDragger.IsDragGestureActive()) )
return;
float max_distance = ConfigManager::GetValue(configid_float_input_detached_interaction_max_distance);
if ( (max_distance != 0.0f) && (!vr::VROverlay()->IsDashboardVisible()) )
{
bool do_set_interactive = false;
//Add some additional distance for disabling interaction again
if (m_LaserPointer.IsActive())
{
//...but abort if pointer wasn't activated by this function (not our job to deactivate it)
if (m_LaserPointer.GetActivationOrigin() != dplp_activation_origin_auto_toggle)
return;
max_distance += 0.01f;
}
vr::TrackedDeviceIndex_t device_index_hovering = m_LaserPointer.IsAnyOverlayHovered(max_distance);
//Set pointer device if interaction not active yet and a device is hovering
if ( (!m_LaserPointer.IsActive()) && (device_index_hovering != vr::k_unTrackedDeviceIndexInvalid) )
{
m_LaserPointer.SetActiveDevice(device_index_hovering, dplp_activation_origin_auto_toggle);
}
else if ( (m_LaserPointer.IsActive()) && (device_index_hovering == vr::k_unTrackedDeviceIndexInvalid) ) //Clear pointer device if interaction active and no device is hovering
{
m_LaserPointer.ClearActiveDevice();
}
}
}
void OutputManager::DetachedOverlayGazeFade()
{
if (ConfigManager::GetValue(configid_bool_overlay_gazefade_enabled))
{
Overlay& current_overlay = OverlayManager::Get().GetCurrentOverlay();
//When drag/select mode are active or HMD pose not available, default to most visible alpha setting
const float max_alpha = ConfigManager::GetValue(configid_float_overlay_opacity);
const float min_alpha = ConfigManager::GetValue(configid_float_overlay_gazefade_opacity);
float alpha = std::max(min_alpha, max_alpha);
if ((!ConfigManager::GetValue(configid_bool_state_overlay_dragmode)) && (!ConfigManager::GetValue(configid_bool_state_overlay_selectmode)))
{
vr::TrackingUniverseOrigin universe_origin = vr::TrackingUniverseStanding;
vr::TrackedDevicePose_t poses[vr::k_unTrackedDeviceIndex_Hmd + 1];
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(universe_origin, vr::IVRSystemEx::GetTimeNowToPhotons(), poses, vr::k_unTrackedDeviceIndex_Hmd + 1);
if (poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid)
{
//Distance the gaze point is offset from HMD (useful range 0.25 - 1.0)
float gaze_distance = ConfigManager::GetValue(configid_float_overlay_gazefade_distance);
//Rate the fading gets applied when looking off the gaze point (useful range 4.0 - 30, depends on overlay size)
float fade_rate = ConfigManager::GetValue(configid_float_overlay_gazefade_rate) * 10.0f;
Matrix4 mat_pose = poses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking;
Matrix4 mat_overlay = m_OverlayDragger.GetBaseOffsetMatrix();
mat_overlay *= ConfigManager::Get().GetOverlayDetachedTransform();
//Infinite/Auto distance mode
if (gaze_distance == 0.0f)
{
gaze_distance = mat_overlay.getTranslation().distance(mat_pose.getTranslation()); //Match gaze distance to distance between HMD and overlay
}
else
{
gaze_distance += 0.20f; //Useful range starts at ~0.20 - 0.25 (lower is in HMD or culled away), so offset the settings value
}
mat_pose.translate_relative(0.0f, 0.0f, -gaze_distance);
Vector3 pos_gaze = mat_pose.getTranslation();
float distance = mat_overlay.getTranslation().distance(pos_gaze);
gaze_distance = std::min(gaze_distance, 1.0f); //To get useful fading past 1m distance we'll have to limit the value to 1m here for the math below
alpha = clamp((distance * -fade_rate) + ((gaze_distance - 0.1f) * 10.0f), 0.0f, 1.0f); //There's nothing smart behind this, just trial and error
//Use max alpha when the overlay or the Floating UI targeting the overlay is being pointed at
if ((ConfigManager::Get().IsLaserPointerTargetOverlay(current_overlay.GetHandle())) ||
((unsigned int)ConfigManager::GetValue(configid_int_state_interface_floating_ui_hovered_id) == current_overlay.GetID()))
{
alpha = std::max(min_alpha, max_alpha); //Take whatever's more visible as the user probably wants to be able to see the overlay
}
else //Adapt alpha result from a 0.0 - 1.0 range to gazefade_opacity - overlay_opacity and invert if necessary
{
const float range_length = max_alpha - min_alpha;
if (range_length >= 0.0f)
{
alpha = (alpha * range_length) + min_alpha;
}
else //Gaze Fade target opacity higher than overlay opcacity, invert behavior
{
alpha = ((alpha - 1.0f) * range_length) + max_alpha;
}
}
}
}
//Limit alpha change per frame to smooth out things when abrupt changes happen (i.e. overlay capture took a bit to re-enable or laser pointer forces full alpha)
const float prev_alpha = current_overlay.GetOpacity();
const float diff = alpha - prev_alpha;
current_overlay.SetOpacity(prev_alpha + clamp(diff, -0.1f, 0.1f));
}
}
void OutputManager::DetachedOverlayGazeFadeAutoConfigure()
{
vr::TrackedDevicePose_t poses[vr::k_unTrackedDeviceIndex_Hmd + 1];
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, vr::IVRSystemEx::GetTimeNowToPhotons(), poses, vr::k_unTrackedDeviceIndex_Hmd + 1);
if (poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid)
{
OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
Matrix4 mat_pose = poses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking;
Matrix4 mat_overlay = m_OverlayDragger.GetBaseOffsetMatrix();
mat_overlay *= ConfigManager::Get().GetOverlayDetachedTransform();
//Match gaze distance to distance between HMD and overlay
float gaze_distance = mat_overlay.getTranslation().distance(mat_pose.getTranslation());
gaze_distance -= 0.20f;
//Set fade rate to roughly decrease when the overlay is bigger and further away
float fade_rate = 2.5f / data.ConfigFloat[configid_float_overlay_width] * gaze_distance;
//Don't let the math go overboard
gaze_distance = std::max(gaze_distance, 0.01f);
fade_rate = clamp(fade_rate, 0.3f, 1.75f);
data.ConfigFloat[configid_float_overlay_gazefade_distance] = gaze_distance;
data.ConfigFloat[configid_float_overlay_gazefade_rate] = fade_rate;
IPCManager::Get().PostConfigMessageToUIApp(configid_float_overlay_gazefade_distance, gaze_distance);
IPCManager::Get().PostConfigMessageToUIApp(configid_float_overlay_gazefade_rate, fade_rate);
}
}
void OutputManager::DetachedOverlayAutoDockingAll()
{
if ( (!m_OverlayDragger.IsDragActive()) || (!ConfigManager::Get().GetValue(configid_bool_input_drag_auto_docking)) )
{
//Set config value to 0 if the drag ended while it wasn't yet
if (ConfigManager::GetValue(configid_int_state_auto_docking_state) != 0)
{
ConfigManager::SetValue(configid_int_state_auto_docking_state, 0);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_auto_docking_state, 0);
}
return;
}
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(m_OverlayDragger.GetDragOverlayID());
int config_value = 0;
vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount];
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, vr::IVRSystemEx::GetTimeNowToPhotons(), poses, vr::k_unMaxTrackedDeviceCount);
//Check left and right hand controller
vr::ETrackedControllerRole controller_role = vr::TrackedControllerRole_LeftHand;
for (int controller_role = vr::TrackedControllerRole_LeftHand; controller_role <= vr::TrackedControllerRole_RightHand; ++controller_role)
{
vr::TrackedDeviceIndex_t device_index = vr::VRSystem()->GetTrackedDeviceIndexForControllerRole((vr::ETrackedControllerRole)controller_role);
if ( ((int)device_index != m_OverlayDragger.GetDragDeviceID()) && (device_index < vr::k_unMaxTrackedDeviceCount) && (poses[device_index].bPoseIsValid) )
{
//Get distance between dragged overlay and controller
Matrix4 controller_pose = poses[device_index].mDeviceToAbsoluteTracking;
float distance = m_OverlayDragger.GetDragOverlayMatrix().getTranslation().distance( controller_pose.getTranslation() );
bool is_docked = false;
//Check if overlay is already docked
if ( ((controller_role == vr::TrackedControllerRole_LeftHand) && (data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_left_hand)) ||
((controller_role == vr::TrackedControllerRole_RightHand) && (data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_right_hand)) )
{
is_docked = true;
}
//Set config value if distance is low enough
if ( (!is_docked) && (distance < 0.21f) )
{
config_value = controller_role;
}
else if ( (is_docked) && (distance > 0.23f) ) //...or high enough if already docked
{
config_value = controller_role + 2;
}
}
}
//Adjust config value if it changed
if (config_value != ConfigManager::GetValue(configid_int_state_auto_docking_state))
{
ConfigManager::SetValue(configid_int_state_auto_docking_state, config_value);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_auto_docking_state, config_value);
}
}
void OutputManager::DetachedTempDragStart(unsigned int overlay_id, float offset_forward)
{
m_OverlayDragger.DragStart(overlay_id);
//Drag may fail when dashboard device goes missing
//Should not happen as the relevant functions try their best to get an alternative before calling this, but avoid being stuck in temp drag mode
if (m_OverlayDragger.IsDragActive())
{
m_OverlayDragger.AbsoluteModeSet(true, offset_forward);
m_OvrlTempDragStartTick = ::GetTickCount64();
if (m_LaserPointer.IsActive())
{
m_LaserPointer.ForceTargetOverlay(OverlayManager::Get().GetOverlay(overlay_id).GetHandle());
}
ConfigManager::SetValue(configid_bool_state_overlay_dragmode_temp, true);
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_overlay_dragmode_temp, true);
}
}
void OutputManager::DetachedTempDragFinish()
{
if ( (m_OverlayDragger.IsDragActive()) && (ConfigManager::GetValue(configid_bool_state_overlay_dragmode_temp)) )
{
OnDragFinish();
//Adjust current overlay for ApplySettingTransform()
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(m_OverlayDragger.GetDragOverlayID());
m_OverlayDragger.DragFinish();
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_state_overlay_dragmode_temp, false);
ConfigManager::SetValue(configid_bool_state_overlay_dragmode_temp, false);
ApplySettingTransform();
DetachedTransformSync(OverlayManager::Get().GetCurrentOverlayID());
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
ApplySettingMouseInput();
}
}
void OutputManager::OnDragFinish()
{
if (!m_OverlayDragger.IsDragActive())
return;
if (m_OvrlDirectDragActive)
{
m_OvrlDirectDragActive = false;
ApplySettingMouseInput();
}
ResetMouseLastLaserPointerPos();
//Handle auto-docking
int auto_dock_value = ConfigManager::GetValue(configid_int_state_auto_docking_state);
if (auto_dock_value != 0)
{
//Unpack settings value
vr::ETrackedControllerRole role = (auto_dock_value % 2 == 0) ? vr::TrackedControllerRole_RightHand : vr::TrackedControllerRole_LeftHand;
bool is_docking = (auto_dock_value <= 2);
OverlayConfigData& data = OverlayManager::Get().GetConfigData(m_OverlayDragger.GetDragOverlayID());
if (is_docking)
{
data.ConfigInt[configid_int_overlay_origin] = (role == vr::TrackedControllerRole_RightHand) ? ovrl_origin_right_hand : ovrl_origin_left_hand;
}
else
{
data.ConfigInt[configid_int_overlay_origin] = ovrl_origin_room;
}
//Send change over to UI
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, (int)m_OverlayDragger.GetDragOverlayID());
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_origin, data.ConfigInt[configid_int_overlay_origin]);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
}
Overlay& overlay = OverlayManager::Get().GetOverlay(m_OverlayDragger.GetDragOverlayID());
overlay.GetSmootherPos().ResetLastPos();
overlay.GetSmootherRot().ResetLastPos();
}
void OutputManager::OnSetOverlayWinRTCaptureWindow(unsigned int overlay_id)
{
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(overlay_id);
Overlay& overlay = OverlayManager::Get().GetCurrentOverlay();
OverlayConfigData& data = OverlayManager::Get().GetCurrentConfigData();
overlay.SetTextureSource(ovrl_texsource_none);
ResetCurrentOverlay();
//Reset config_str_overlay_winrt_last_* strings when HWND was explicitly set to null
if (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] == 0)
{
data.ConfigStr[configid_str_overlay_winrt_last_window_title] = "";
data.ConfigStr[configid_str_overlay_winrt_last_window_class_name] = "";
data.ConfigStr[configid_str_overlay_winrt_last_window_exe_name] = "";
}
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
void OutputManager::FinishQueuedOverlayRemovals()
{
if (m_RemoveOverlayQueue.empty())
return;
//Sort in descending order since removals from end will end up with less re-ordering (or even none if the top removed overlay is also the last one)
std::sort(m_RemoveOverlayQueue.rbegin(), m_RemoveOverlayQueue.rend());
for (unsigned int overlay_id : m_RemoveOverlayQueue)
{
OverlayManager::Get().RemoveOverlay(overlay_id);
IPCManager::Get().PostMessageToUIApp(ipcmsg_action, ipcact_overlay_remove, overlay_id);
}
m_RemoveOverlayQueue.clear();
//RemoveOverlay() may have changed active ID, keep in sync
ConfigManager::SetValue(configid_int_interface_overlay_current_id, OverlayManager::Get().GetCurrentOverlayID());
}
void OutputManager::FixInvalidDashboardLaunchState()
{
//Workaround for glitchy behavior in SteamVR
//When launching SteamVR while wearing the HMD, the dashboard appears automatically. In this state, IsDashboardVisible() returns false and the HMD button is unable to close the dashboard.
//There are other things that aren't quite right and Desktop+ relies on the info to be correct in many cases.
//This somewhat works around the issue by opening the dashboard for our dashboard overlay.
//While a little bit intrusive and not 100% reliable when Desktop+ is auto-launched alongside, it's better than nothing.
vr::VROverlayHandle_t system_dashboard;
vr::VROverlay()->FindOverlay("system.systemui", &system_dashboard);
if ( (vr::VROverlay()->IsOverlayVisible(system_dashboard)) && (!vr::VROverlay()->IsDashboardVisible()) )
{
vr::VROverlay()->ShowDashboard("elvissteinjr.DesktopPlusDashboard");
}
}
void OutputManager::UpdateDashboardHMD_Y()
{
m_OverlayDragger.UpdateDashboardHMD_Y();
}
bool OutputManager::HasDashboardMoved()
{
//Don't trust anything if it's not visible
if (!vr::VROverlay()->IsDashboardVisible())
return false;
vr::HmdMatrix34_t hmd_matrix = {0};
vr::TrackingUniverseOrigin universe_origin = vr::TrackingUniverseStanding;
if (vr::VROverlay()->IsOverlayVisible(m_OvrlHandleDashboardDummy))
{
vr::VROverlay()->GetTransformForOverlayCoordinates(m_OvrlHandleDashboardDummy, universe_origin, {0.0f, 0.0f}, &hmd_matrix);
}
else
{
vr::VROverlayHandle_t handle_gamepad_ui = vr::k_ulOverlayHandleInvalid;
vr::VROverlay()->FindOverlay("valve.steam.gamepadui.bar", &handle_gamepad_ui);
//Use GamepadUI as a reference if available since it's more stable due to the systemui overlay changing size depending on the active dashboard overlay's flags
vr::VROverlayHandle_t handle_dashboard = handle_gamepad_ui;
if (handle_dashboard == vr::k_ulOverlayHandleInvalid)
{
vr::VROverlay()->FindOverlay("system.systemui", &handle_dashboard);
}
vr::HmdVector2_t overlay_dashboard_size;
vr::VROverlay()->GetOverlayMouseScale(handle_dashboard, &overlay_dashboard_size); //Coordinate size should be mouse scale
vr::VROverlay()->GetTransformForOverlayCoordinates(handle_dashboard, universe_origin, { overlay_dashboard_size.v[0]/2.0f, 0.0f }, &hmd_matrix);
}
Matrix4 matrix_new = hmd_matrix;
if (m_DashboardTransformLast != matrix_new)
{
m_DashboardTransformLast = hmd_matrix;
return true;
}
return false;
}
void OutputManager::DimDashboard(bool do_dim)
{
if (vr::VROverlay() == nullptr)
return;
//This *could* terribly conflict with other apps messing with these settings, but I'm unaware of any that are right now, so let's just say we're the first
vr::VROverlayHandle_t system_dashboard;
vr::VROverlay()->FindOverlay("system.systemui", &system_dashboard);
if (system_dashboard != vr::k_ulOverlayHandleInvalid)
{
if (do_dim)
{
vr::VROverlay()->SetOverlayColor(system_dashboard, 0.05f, 0.05f, 0.05f);
}
else
{
vr::VROverlay()->SetOverlayColor(system_dashboard, 1.0f, 1.0f, 1.0f);
}
}
vr::VROverlayHandle_t gamepadui;
vr::VROverlay()->FindOverlay("valve.steam.gamepadui.bar", &gamepadui);
if (gamepadui != vr::k_ulOverlayHandleInvalid)
{
if (do_dim)
{
vr::VROverlay()->SetOverlayColor(gamepadui, 0.05f, 0.05f, 0.05f);
}
else
{
vr::VROverlay()->SetOverlayColor(gamepadui, 1.0f, 1.0f, 1.0f);
}
}
}
void OutputManager::UpdatePendingDashboardDummyHeight()
{
float old_dummy_height = 0.0f;
vr::VROverlay()->GetOverlayWidthInMeters(m_OvrlHandleDashboardDummy, &old_dummy_height);
//Check for dummy height changes but enforce a minimum difference as the underlying size seems to flicker under certain conditions in current SteamVR versions for some reason
if (fabs(m_PendingDashboardDummyHeight - old_dummy_height) > 0.01f)
{
vr::VROverlay()->SetOverlayWidthInMeters(m_OvrlHandleDashboardDummy, m_PendingDashboardDummyHeight);
//Perform tactical sleep to avoid flickering caused by dummy adjustments not being done in time when dashboard overlay positioning depends on it
if (vr::VROverlay()->IsOverlayVisible(m_OvrlHandleDashboardDummy))
{
::Sleep(100);
}
m_PendingDashboardDummyHeight = 0.0f;
}
}
bool OutputManager::IsAnyOverlayUsingGazeFade() const
{
//This is the straight forward, simple version. The smart one, efficiently keeping track properly, could come some other time*
//*we know it probably won't happen any time soon
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
if ( (data.ConfigBool[configid_bool_overlay_enabled]) && (data.ConfigBool[configid_bool_overlay_gazefade_enabled]) )
{
return true;
}
}
return false;
}
void OutputManager::RegisterHotkeys()
{
//Just unregister all we have when updating any
for (int i = 0; i < m_RegisteredHotkeyCount; ++i)
{
::UnregisterHotKey(nullptr, i);
}
m_IsAnyHotkeyActive = false;
//...and register them again if there's an action assigned
int id = 0;
for (ConfigHotkey& hotkey : ConfigManager::Get().GetHotkeys())
{
if (hotkey.ActionUID != k_ActionUID_Invalid)
{
::RegisterHotKey(nullptr, id, hotkey.Modifiers | MOD_NOREPEAT, hotkey.KeyCode);
m_IsAnyHotkeyActive = true;
}
++id;
}
m_RegisteredHotkeyCount = id;
//Laser Pointer HMD Device uses hotkeys just as means to block key input to other applications
//Actual inputs are checked via VRInput::UpdateKeyboardDeviceState(), so they do not need any handling in HandleHotkeys() or HandleHotkeyMessage()
const int hmd_device_hotkey_count = configid_int_input_laser_pointer_hmd_device_keycode_drag - configid_int_input_laser_pointer_hmd_device_keycode_toggle;
for (int i = 0; i < hmd_device_hotkey_count + 1; ++i)
{
ConfigID_Int config_id = (ConfigID_Int)(configid_int_input_laser_pointer_hmd_device_keycode_toggle + i);
::UnregisterHotKey(nullptr, 0xBFFF - i); //0xBFFF is max allowed id, we count down from that to avoid conflicts
}
if (ConfigManager::GetValue(configid_bool_input_laser_pointer_hmd_device)) //No need to block these keys when the feature is disabled
{
for (int i = 0; i < hmd_device_hotkey_count + 1; ++i)
{
ConfigID_Int config_id = (ConfigID_Int)(configid_int_input_laser_pointer_hmd_device_keycode_toggle + i);
::RegisterHotKey(nullptr, 0xBFFF - i, MOD_NOREPEAT, ConfigManager::GetValue(config_id));
}
}
}
void OutputManager::HandleHotkeys()
{
//This function handles hotkeys manually via GetAsyncKeyState() for some very special games that think consuming all keyboard input is a nice thing to do
//Win32 hotkeys are still used simultaneously. Their input blocking might be considered an advantage and the hotkey configurability is designed around them already
//Win32 hotkeys also still work while an elevated application has focus, GetAsyncKeyState() doesn't
if (!m_IsAnyHotkeyActive)
return;
for (ConfigHotkey& hotkey : ConfigManager::Get().GetHotkeys())
{
if (hotkey.ActionUID != k_ActionUID_Invalid)
{
if ( (::GetAsyncKeyState(hotkey.KeyCode) < 0) &&
( ((hotkey.Modifiers & MOD_SHIFT) == 0) || (::GetAsyncKeyState(VK_SHIFT) < 0) ) &&
( ((hotkey.Modifiers & MOD_CONTROL) == 0) || (::GetAsyncKeyState(VK_CONTROL) < 0) ) &&
( ((hotkey.Modifiers & MOD_ALT) == 0) || (::GetAsyncKeyState(VK_MENU) < 0) ) &&
( ((hotkey.Modifiers & MOD_WIN) == 0) || ((::GetAsyncKeyState(VK_LWIN) < 0) || (::GetAsyncKeyState(VK_RWIN) < 0)) ) )
{
if (!hotkey.StateIsDown)
{
hotkey.StateIsDown = true;
ConfigManager::Get().GetActionManager().DoAction(hotkey.ActionUID);
}
}
else if (hotkey.StateIsDown)
{
hotkey.StateIsDown = false;
}
}
}
}
void OutputManager::HandleKeyboardAutoVisibility()
{
//This only handles auto-visibility for desktop/window overlays
//Check if state changed
static bool is_text_input_focused_prev = false;
bool is_text_input_focused = ( (!ConfigManager::GetValue(configid_bool_input_keyboard_auto_show_desktop)) || (m_MouseIgnoreMoveEvent) ) ? false : WindowManager::Get().IsTextInputFocused();
//Overlay input has to be active during a state change for it to count as focused
if ((is_text_input_focused != is_text_input_focused_prev) && (!m_OvrlInputActive))
{
is_text_input_focused = false;
}
if (is_text_input_focused != is_text_input_focused_prev)
{
//Send update to UI process
IPCManager::Get().PostMessageToUIApp(ipcmsg_action, ipcact_keyboard_show_auto, (is_text_input_focused) ? ConfigManager::GetValue(configid_int_state_overlay_focused_id) : -1);
is_text_input_focused_prev = is_text_input_focused;
}
}
================================================
FILE: src/DesktopPlus/OutputManager.h
================================================
#ifndef _OUTPUTMANAGER_H_
#define _OUTPUTMANAGER_H_
#include
#include "CommonTypes.h"
#include "warning.h"
#include "openvr.h"
#include "Matrices.h"
#include "RadialFollowSmoothing.h"
#include "OverlayManager.h"
#include "ConfigManager.h"
#include "SoftwareCursorGrabber.h"
#include "InputSimulator.h"
#include "VRInput.h"
#include "BackgroundOverlay.h"
#include "OUtoSBSConverter.h"
#include "InterprocessMessaging.h"
#include "OverlayDragger.h"
#include "LaserPointer.h"
class Overlay;
//
// This class evolved into handling almost everything
// Updates the output texture, sends it to OpenVR, handles OpenVR events, IPC messages...
// Most of the tasks are related, but splitting stuff up might be an idea in the future
//
class OutputManager
{
public:
static OutputManager* Get();
OutputManager(HANDLE PauseDuplicationEvent, HANDLE ResumeDuplicationEvent);
~OutputManager();
void CleanRefs();
DUPL_RETURN InitOutput(HWND Window, _Out_ INT& SingleOutput, _Out_ UINT* OutCount, _Out_ RECT* DeskBounds);
std::tuple InitOverlay(); //Returns error state
DUPL_RETURN_UPD Update(_In_ PTR_INFO* PointerInfo, _In_ DPRect& DirtyRegionTotal, bool NewFrame, bool SkipFrame);
void BusyUpdate(); //Updates minimal state (i.e. OverlayDragger) during busy waits (i.e. waiting for browser startup) to appear more responsive
bool HandleIPCMessage(const MSG& msg); //Returns true if message caused a duplication reset (i.e. desktop switch)
void HandleWinRTMessage(const MSG& msg); //Messages sent by the Desktop+ WinRT library
void HandleHotkeyMessage(const MSG& msg);
void OnExit();
HWND GetWindowHandle();
HANDLE GetSharedHandle();
IDXGIAdapter* GetDXGIAdapter(); //Don't forget to call Release() on the returned pointer when done with it
void ResetOverlays();
void ResetCurrentOverlay();
ID3D11Texture2D* GetOverlayTexture() const; //This returns m_OvrlTex, the backing texture used by the desktop texture overlay (and all overlays stealing its texture)
ID3D11Texture2D* GetMultiGPUTargetTexture() const;
vr::VROverlayHandle_t GetDesktopTextureOverlay() const;
bool GetOverlayActive() const;
bool GetOverlayInputActive() const;
DWORD GetMaxRefreshDelay() const;
float GetHMDFrameRate() const;
int GetDesktopWidth() const;
int GetDesktopHeight() const;
const std::vector& GetDesktopRects() const;
float GetDesktopHDRWhiteLevelAdjustment(int desktop_id, bool is_for_graphics_capture, bool wmr_ignore_vscreens) const;
void ShowOverlay(unsigned int id);
void ShowTheaterOverlay(unsigned int id);
void HideOverlay(unsigned int id);
void ResetOverlayActiveCount(); //Called by OverlayManager after removing all overlays, makes sure the active counts are correct
bool HasDashboardBeenActivatedOnce() const;
bool IsDashboardTabActive() const;
float GetDashboardScale() const;
float GetOverlayHeight(unsigned int overlay_id) const;
Matrix4 GetFallbackOverlayTransform() const; //Returns a matrix resulting in the overlay facing the HMD at 2m distance, used when other referenced aren't available
void SetOutputErrorTexture(vr::VROverlayHandle_t overlay_handle);
void SetOutputInvalid(); //Handles state when there's no valid output
bool IsOutputInvalid() const;
void SetOverlayEnabled(unsigned int overlay_id, bool is_enabled);
void CropToActiveWindowToggle(unsigned int overlay_id);
void ShowWindowSwitcher();
void SwitchToWindow(HWND window, bool warp_cursor);
void OverlayDirectDragStart(unsigned int overlay_id);
void OverlayDirectDragFinish(unsigned int overlay_id);
VRInput& GetVRInput();
InputSimulator& GetInputSimulator();
void UpdatePerformanceStates();
const LARGE_INTEGER& GetUpdateLimiterDelay();
//This updates the cached desktop rects and count and optionally chooses the adapters/desktop for desktop duplication (previously part of InitOutput())
int EnumerateOutputs(int target_desktop_id = -1, Microsoft::WRL::ComPtr* out_adapter_preferred = nullptr, Microsoft::WRL::ComPtr* out_adapter_vr = nullptr);
void CropToDisplay(int display_id, int& crop_x, int& crop_y, int& crop_width, int& crop_height);
bool CropToActiveWindow(int& crop_x, int& crop_y, int& crop_width, int& crop_height); //Returns true if values have changed
void InitComIfNeeded();
void ConvertOUtoSBS(Overlay& overlay, OUtoSBSConverter& converter);
private:
DUPL_RETURN ProcessMonoMask(bool is_mono, PTR_INFO& ptr_info, int& ptr_width, int& ptr_height, int& ptr_left, int& ptr_top,
Microsoft::WRL::ComPtr& out_tex, DXGI_FORMAT& out_tex_format, D3D11_BOX& box);
DUPL_RETURN ProcessMonoMaskFloat16(bool is_mono, PTR_INFO& ptr_info, int& ptr_width, int& ptr_height, int& ptr_left, int& ptr_top,
Microsoft::WRL::ComPtr& out_tex, DXGI_FORMAT& out_tex_format, D3D11_BOX& box);
DUPL_RETURN MakeRTV();
DUPL_RETURN InitShaders();
DUPL_RETURN CreateTextures(INT SingleOutput, _Out_ UINT* OutCount, _Out_ RECT* DeskBounds);
void DrawFrameToOverlayTex(bool clear_rtv = true);
DUPL_RETURN DrawMouseToOverlayTex(_In_ PTR_INFO* PtrInfo);
DUPL_RETURN_UPD RefreshOpenVROverlayTexture(DPRect& DirtyRectTotal, bool force_full_copy = false); //Refreshes the overlay texture of the VR runtime with content of the m_OvrlTex backing texture
bool DesktopTextureAlphaCheck();
bool HandleOpenVREvents(); //Returns true if quit event happened
void OnOpenVRMouseEvent(const vr::VREvent_t& vr_event, unsigned int& current_overlay_old);
void HandleKeyboardMessage(IPCActionID ipc_action_id, LPARAM lparam);
bool HandleOverlayProfileLoadMessage(LPARAM lparam);
void ResetMouseLastLaserPointerPos();
void CropToActiveWindow();
void CropToDisplay(int display_id, bool do_not_apply_setting = false);
void DuplicateOverlay(unsigned int base_id, bool is_ui_overlay = false);
unsigned int AddOverlay(OverlayCaptureSource capture_source, int desktop_id = -2, HWND window_handle = nullptr);
unsigned int AddOverlayDrag(float source_distance, OverlayCaptureSource capture_source, int desktop_id = -2, HWND window_handle = nullptr, float overlay_width = 0.5f);
void ApplySettingCaptureSource();
void ApplySetting3DMode();
void ApplySettingTransform();
void ApplySettingCrop();
void ApplySettingInputMode();
void ApplySettingMouseInput();
void ApplySettingMouseScale();
void ApplySettingUpdateLimiter();
void ApplySettingExtraBrightness();
void DetachedTransformSync(unsigned int overlay_id);
void DetachedTransformSyncAll();
void DetachedTransformReset(unsigned int overlay_id_ref = k_ulOverlayID_None);
void DetachedTransformAdjust(unsigned int packed_value);
void DetachedTransformConvertOrigin(unsigned int overlay_id, OverlayOrigin origin_from, OverlayOrigin origin_to);
void DetachedTransformConvertOrigin(unsigned int overlay_id, OverlayOrigin origin_from, OverlayOrigin origin_to,
const OverlayOriginConfig& origin_config_from, const OverlayOriginConfig& origin_config_to);
bool DetachedTransformFrameUpdate(); //Returns if update was applied (not skipped)
void DetachedTransformFrameUpdateApplySmoothingParameters(Overlay& overlay, int preset_id);
void DetachedTransformUpdateSeatedPosition();
void DetachedInteractionAutoToggleAll();
void DetachedOverlayGazeFade();
void DetachedOverlayGazeFadeAutoConfigure();
void DetachedOverlayAutoDockingAll();
void DetachedTempDragStart(unsigned int overlay_id, float offset_forward = 0.5f);
void DetachedTempDragFinish(); //Calls OnDragFinish()
void OnDragFinish(); //Called before calling m_OverlayDragger.DragFinish() to handle OutputManager post drag state adjustments (like auto docking)
void OnSetOverlayWinRTCaptureWindow(unsigned int overlay_id); //Called when configid_intptr_overlay_state_winrt_hwnd changed
void FinishQueuedOverlayRemovals(); //Overlay removals are currently only queued up when requested during HandleWinRTMessage()
void FixInvalidDashboardLaunchState();
void UpdateDashboardHMD_Y();
bool HasDashboardMoved();
void DimDashboard(bool do_dim);
void UpdatePendingDashboardDummyHeight();
bool IsAnyOverlayUsingGazeFade() const;
void RegisterHotkeys();
void HandleHotkeys();
void HandleKeyboardAutoVisibility();
InputSimulator m_InputSim;
VRInput m_VRInput;
IPCManager m_IPCMan;
BackgroundOverlay m_BackgroundOverlay;
OverlayDragger m_OverlayDragger;
LaserPointer m_LaserPointer;
ID3D11Device* m_Device;
ID3D11DeviceContext* m_DeviceContext;
ID3D11SamplerState* m_Sampler;
ID3D11BlendState* m_BlendState;
ID3D11RasterizerState* m_RasterizerState;
ID3D11VertexShader* m_VertexShader;
ID3D11PixelShader* m_PixelShader;
ID3D11PixelShader* m_PixelShaderCursor;
ID3D11InputLayout* m_InputLayout;
ID3D11Texture2D* m_SharedSurf;
ID3D11Buffer* m_VertexBuffer;
ID3D11ShaderResourceView* m_ShaderResource;
IDXGIKeyedMutex* m_KeyMutex;
HWND m_WindowHandle;
//These handles are not created or closed by this class, they're valid for the entire runtime though
HANDLE m_PauseDuplicationEvent;
HANDLE m_ResumeDuplicationEvent;
int m_DesktopX; //These are the desktop coordinates/size valid for Desktop Duplication and may be different from the m_DesktopRectTotal
int m_DesktopY;
int m_DesktopWidth;
int m_DesktopHeight;
std::vector m_DesktopRects; //Cached position and size of available desktops
DPRect m_DesktopRectTotal; //Total rect of all available desktops (may not be the same as above Desktop Duplication rect if that's not using the combined desktop)
std::vector m_DesktopHDRWhiteLevelAdjustments; //Cached GetDesktopHDRWhiteLevelAdjustment() results used during cursor updates
DWORD m_MaxActiveRefreshDelay;
bool m_OutputHDRAvailable; //False if OS doesn't support the required interface, regardless of hardware connected
bool m_OutputInvalid;
bool m_OutputPendingSkippedFrame;
bool m_OutputPendingFullRefresh;
DPRect m_OutputPendingDirtyRect;
DPRect m_OutputLastClippingRect;
int m_OutputAlphaChecksPending;
bool m_OutputAlphaCheckFailed; //Output appears to be translucent and needs its alpha channel stripped during texture copy
vr::VROverlayHandle_t m_OvrlHandleDashboardDummy;
vr::VROverlayHandle_t m_OvrlHandleIcon;
vr::VROverlayHandle_t m_OvrlHandleDesktopTexture;
ID3D11Texture2D* m_OvrlTex;
ID3D11RenderTargetView* m_OvrlRTV;
int m_OvrlActiveCount;
int m_OvrlDesktopDuplActiveCount;
bool m_OvrlDashboardActive;
bool m_OvrlInputActive;
bool m_OvrlDirectDragActive;
ULONGLONG m_OvrlTempDragStartTick;
float m_PendingDashboardDummyHeight;
ULONGLONG m_LastApplyTransformTick;
ULONGLONG m_LastFrameTransformUpdateTick;
Microsoft::WRL::ComPtr m_MouseTex;
Microsoft::WRL::ComPtr m_MouseShaderRes;
SoftwareCursorGrabber m_MouseAlternativeCursor;
ULONGLONG m_MouseLastClickTick;
bool m_MouseIgnoreMoveEvent;
bool m_MouseCursorNeedsUpdate;
PTR_INFO m_MouseLastInfo;
Vector2Int m_MouseLastCursorSize;
bool m_MouseLaserPointerUsedLastUpdate;
bool m_MouseLastLaserPointerMoveBlocked;
int m_MouseLastLaserPointerX;
int m_MouseLastLaserPointerY;
int m_MouseIgnoreMoveEventMissCount;
unsigned int m_MouseLeftDownOverlayID;
RadialFollowCore m_MouseLaserPointerSmoother;
LARGE_INTEGER m_MouseLaserPointerScrollDeltaStart;
LARGE_INTEGER m_MouseLaserPointerScrollDeltaFrequency;
bool m_IsFirstLaunch;
bool m_ComInitDone;
bool m_DashboardActivatedOnce;
Matrix4 m_DashboardTransformLast; //This is only used to check if the dashboard has moved from events we can't detect otherwise
Matrix4 m_SeatedTransformLast;
//These are only used when duplicating outputs from a different GPU
ID3D11Device* m_MultiGPUTargetDevice; //Target D3D11 device, meaning the one the HMD is connected to
ID3D11DeviceContext* m_MultiGPUTargetDeviceContext;
ID3D11Texture2D* m_MultiGPUTexStaging; //Staging texture, owned by m_Device
ID3D11Texture2D* m_MultiGPUTexTarget; //Target texture to copy to, owned by m_MultiGPUTargetDevice
int m_PerformanceFrameCount;
ULONGLONG m_PerformanceFrameCountStartTick;
LARGE_INTEGER m_PerformanceUpdateLimiterDelay;
std::vector m_ProfileAddOverlayIDQueue;
std::vector m_RemoveOverlayQueue;
bool m_IsAnyHotkeyActive;
int m_RegisteredHotkeyCount;
};
#endif
================================================
FILE: src/DesktopPlus/Overlays.cpp
================================================
#include "Overlays.h"
#include
#include "CommonTypes.h"
#include "OpenVRExt.h"
#include "OverlayManager.h"
#include "OutputManager.h"
#include "DesktopPlusWinRT.h"
#include "DPBrowserAPIClient.h"
Overlay::Overlay(unsigned int id) : m_ID(id),
m_OvrlHandle(vr::k_ulOverlayHandleInvalid),
m_Visible(false),
m_Opacity(1.0f),
m_TextureSource(ovrl_texsource_invalid)
{
//Don't call InitOverlay when OpenVR isn't loaded yet. This happens during startup when loading the config and will be fixed up by OutputManager::InitOverlay() afterwards
if (vr::VROverlay() != nullptr)
{
InitOverlay();
}
m_SmootherPos.SetDetectInterruptions(false);
m_SmootherRot.SetDetectInterruptions(false);
}
Overlay::Overlay(Overlay&& b)
{
m_OvrlHandle = vr::k_ulOverlayHandleInvalid; //This needs a valid value first
*this = std::move(b);
}
Overlay& Overlay::operator=(Overlay&& b)
{
if (this != &b)
{
if (m_OvrlHandle != vr::k_ulOverlayHandleInvalid)
{
if (m_TextureSource == ovrl_texsource_winrt_capture)
{
DPWinRT_StopCapture(m_OvrlHandle);
}
else if (m_TextureSource == ovrl_texsource_browser)
{
DPBrowserAPIClient::Get().DPBrowser_StopBrowser(m_OvrlHandle);
}
vr::VROverlayEx()->DestroyOverlayEx(m_OvrlHandle);
}
m_ID = b.m_ID;
m_OvrlHandle = b.m_OvrlHandle;
m_Visible = b.m_Visible;
m_Opacity = b.m_Opacity;
m_ValidatedCropRect = b.m_ValidatedCropRect;
m_TextureSource = b.m_TextureSource;
//m_OUtoSBSConverter should just be left alone, it only holds cached state anyways
b.m_OvrlHandle = vr::k_ulOverlayHandleInvalid;
}
return *this;
}
Overlay::~Overlay()
{
if (m_OvrlHandle != vr::k_ulOverlayHandleInvalid)
{
if (m_TextureSource == ovrl_texsource_winrt_capture)
{
DPWinRT_StopCapture(m_OvrlHandle);
}
else if (m_TextureSource == ovrl_texsource_browser)
{
DPBrowserAPIClient::Get().DPBrowser_StopBrowser(m_OvrlHandle);
}
vr::VROverlayEx()->DestroyOverlayEx(m_OvrlHandle);
}
}
void Overlay::InitOverlay()
{
unsigned int id_offset = 0;
std::string key;
vr::VROverlayHandle_t overlay_handle_find;
//Generate overlay key from ID and check if it's not used yet, add to it if it's not
//Overlay keys & handles are fixed and don't change when overlays are re-ordered or deleted
do
{
key = "elvissteinjr.DesktopPlus" + std::to_string(m_ID + id_offset);
overlay_handle_find = vr::k_ulOverlayHandleInvalid;
id_offset++;
vr::VROverlay()->FindOverlay(key.c_str(), &overlay_handle_find);
}
while (overlay_handle_find != vr::k_ulOverlayHandleInvalid);
vr::VROverlayError ovrl_error = vr::VROverlayError_None;
ovrl_error = vr::VROverlay()->CreateOverlay(key.c_str(), "Desktop+", &m_OvrlHandle);
if (ovrl_error == vr::VROverlayError_None)
{
vr::VROverlay()->SetOverlayAlpha(m_OvrlHandle, m_Opacity);
}
else //Creation failed, send error to UI so the user at least knows (typically this only happens when the overlay limit is exceeded)
{
IPCManager::Get().PostMessageToUIApp(ipcmsg_action, ipcact_overlay_creation_error, ovrl_error);
}
}
void Overlay::AssignDesktopDuplicationTexture()
{
if (m_OvrlHandle != vr::k_ulOverlayHandleInvalid)
{
OverlayConfigData& data = OverlayManager::Get().GetConfigData(m_ID);
if (data.ConfigInt[configid_int_overlay_capture_source] != ovrl_capsource_desktop_duplication)
return;
OutputManager* outmgr = OutputManager::Get();
if (outmgr == nullptr)
return;
//Set content size to desktop duplication values
int dwidth = outmgr->GetDesktopWidth();
int dheight = outmgr->GetDesktopHeight();
//Avoid sending it over to UI if we can help it
if ( (data.ConfigInt[configid_int_overlay_state_content_width] != dwidth) || (data.ConfigInt[configid_int_overlay_state_content_height] != dheight) )
{
data.ConfigInt[configid_int_overlay_state_content_width] = dwidth;
data.ConfigInt[configid_int_overlay_state_content_height] = dheight;
UpdateValidatedCropRect();
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, m_ID);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_state_content_width, dwidth);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_overlay_state_content_height, dheight);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
}
//Exclude indirect desktop duplication sources, like converted Over-Under 3D
if (m_TextureSource != ovrl_texsource_desktop_duplication)
return;
//Use desktop texture overlay as source for a shared overlay texture
ID3D11Resource* device_texture_ref = (OutputManager::Get()->GetMultiGPUTargetTexture() != nullptr) ? OutputManager::Get()->GetMultiGPUTargetTexture() : OutputManager::Get()->GetOverlayTexture();
vr::VROverlayEx()->SetSharedOverlayTexture(outmgr->GetDesktopTextureOverlay(), m_OvrlHandle, device_texture_ref);
}
}
unsigned int Overlay::GetID() const
{
return m_ID;
}
void Overlay::SetID(unsigned int id)
{
m_ID = id;
}
vr::VROverlayHandle_t Overlay::GetHandle() const
{
return m_OvrlHandle;
}
void Overlay::SetHandle(vr::VROverlayHandle_t handle)
{
m_OvrlHandle = handle;
}
void Overlay::SetOpacity(float opacity)
{
if (opacity == m_Opacity)
return;
OutputManager* outmgr = OutputManager::Get();
if (outmgr == nullptr)
return;
vr::VROverlay()->SetOverlayAlpha(m_OvrlHandle, opacity);
if (m_Opacity == 0.0f) //If it was previously 0%, show if needed
{
m_Opacity = opacity; //ShouldBeVisible() depends on this being correct, so set it here
if ( (!m_Visible) && (ShouldBeVisible()) )
{
outmgr->ShowOverlay(m_ID);
}
}
else if (opacity == 0.0f) //If it's 0% now, hide if it shouldn't be visible (Update when Invisble setting can make ShouldBeVisible() return true still)
{
m_Opacity = opacity;
if (!ShouldBeVisible())
{
outmgr->HideOverlay(m_ID);
}
}
m_Opacity = opacity;
}
float Overlay::GetOpacity() const
{
return m_Opacity;
}
void Overlay::SetVisible(bool visible)
{
m_Visible = visible;
(visible) ? vr::VROverlay()->ShowOverlay(m_OvrlHandle) : vr::VROverlay()->HideOverlay(m_OvrlHandle);
m_SmootherPos.ResetLastPos();
m_SmootherRot.ResetLastPos();
}
bool Overlay::IsVisible() const
{
return m_Visible;
}
bool Overlay::ShouldBeVisible() const
{
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(m_ID);
if ( (m_Opacity == 0.0f) && (!data.ConfigBool[configid_bool_overlay_update_invisible]) )
return false;
bool should_be_visible = false;
if (!data.ConfigBool[configid_bool_overlay_enabled])
return false;
//Enabled theater mode is always visible
if (data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_theater_screen)
return true;
OutputManager* outmgr = OutputManager::Get();
if (outmgr == nullptr)
return false;
switch (data.ConfigInt[configid_int_overlay_display_mode])
{
case ovrl_dispmode_always:
{
should_be_visible = true;
break;
}
case ovrl_dispmode_dashboard:
{
//Our method for getting the dashboard transform only works after it has been manually been brought up once OR the Desktop+ tab has been shown
//In practice this means we won't be showing dashboard display mode overlays on the initial SteamVR dashboard that is active when booting up
should_be_visible = ( (outmgr->HasDashboardBeenActivatedOnce()) && (vr::VROverlay()->IsDashboardVisible()) );
break;
}
case ovrl_dispmode_scene:
{
should_be_visible = !vr::VROverlay()->IsDashboardVisible();
break;
}
case ovrl_dispmode_dplustab:
{
should_be_visible = (outmgr->IsDashboardTabActive());
break;
}
}
//Also apply above when origin is dashboard
if ( (should_be_visible) && (data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_dashboard) )
{
should_be_visible = outmgr->HasDashboardBeenActivatedOnce();
}
return should_be_visible;
}
void Overlay::UpdateValidatedCropRect()
{
OutputManager* outmgr = OutputManager::Get();
if (outmgr == nullptr)
return;
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(m_ID);
int x, y, width, height;
if (data.ConfigBool[configid_bool_overlay_crop_enabled])
{
x = std::min( std::max(0, data.ConfigInt[configid_int_overlay_crop_x]), data.ConfigInt[configid_int_overlay_state_content_width]);
y = std::min( std::max(0, data.ConfigInt[configid_int_overlay_crop_y]), data.ConfigInt[configid_int_overlay_state_content_height]);
width = data.ConfigInt[configid_int_overlay_crop_width];
height = data.ConfigInt[configid_int_overlay_crop_height];
}
else //Fall back to default crop when cropping is disabled
{
//Current desktop cropping values for desktop duplication
if ((m_TextureSource == ovrl_texsource_desktop_duplication) || (m_TextureSource == ovrl_texsource_desktop_duplication_3dou_converted))
{
outmgr->CropToDisplay(data.ConfigInt[configid_int_overlay_desktop_id], x, y, width, height);
}
else //Content size for everything else
{
x = 0;
y = 0;
width = data.ConfigInt[configid_int_overlay_state_content_width];
height = data.ConfigInt[configid_int_overlay_state_content_height];
}
}
int width_max = std::max(data.ConfigInt[configid_int_overlay_state_content_width] - x, 1);
int height_max = std::max(data.ConfigInt[configid_int_overlay_state_content_height] - y, 1);
if (width == -1)
width = width_max;
else
width = std::min(width, width_max);
if (height == -1)
height = height_max;
else
height = std::min(height, height_max);
m_ValidatedCropRect = DPRect(x, y, x + width, y + height);
}
const DPRect& Overlay::GetValidatedCropRect() const
{
return m_ValidatedCropRect;
}
void Overlay::SetTextureSource(OverlayTextureSource tex_source)
{
//Skip if nothing changed (except texsource_ui/browser which are always re-applied)
if ( (m_TextureSource == tex_source) && (tex_source != ovrl_texsource_ui) && (tex_source != ovrl_texsource_browser) )
return;
//Cleanup old sources if needed
switch (m_TextureSource)
{
case ovrl_texsource_desktop_duplication_3dou_converted: m_OUtoSBSConverter.CleanRefs(); break;
case ovrl_texsource_winrt_capture: DPWinRT_StopCapture(m_OvrlHandle); break;
case ovrl_texsource_ui:
{
if (tex_source != ovrl_texsource_ui)
{
vr::VROverlay()->SetOverlayRenderingPid(m_OvrlHandle, ::GetCurrentProcessId());
vr::VROverlay()->SetOverlayIntersectionMask(m_OvrlHandle, nullptr, 0);
}
break;
}
case ovrl_texsource_browser:
{
if (tex_source != ovrl_texsource_browser)
{
DPBrowserAPIClient::Get().DPBrowser_StopBrowser(m_OvrlHandle);
vr::VROverlay()->SetOverlayRenderingPid(m_OvrlHandle, ::GetCurrentProcessId());
}
break;
}
default: break;
}
//If this overlay is the theater overlay, mark for refresh down below if the source changed
bool theater_refresh_needed = ( (m_TextureSource != tex_source) && (OverlayManager::Get().GetTheaterOverlayID() == m_ID) );
m_TextureSource = tex_source;
switch (m_TextureSource)
{
case ovrl_texsource_none:
{
if (OutputManager* outmgr = OutputManager::Get())
{
outmgr->SetOutputErrorTexture(m_OvrlHandle);
}
break;
}
case ovrl_texsource_desktop_duplication: /*fallthrough*/
case ovrl_texsource_desktop_duplication_3dou_converted: AssignDesktopDuplicationTexture(); break;
case ovrl_texsource_ui: vr::VROverlay()->SetOverlayRenderingPid(m_OvrlHandle, IPCManager::GetUIAppProcessID()); break;
case ovrl_texsource_browser: vr::VROverlay()->SetOverlayRenderingPid(m_OvrlHandle, DPBrowserAPIClient::Get().GetServerAppProcessID()); break;
default: break;
}
//Set output error config state
{
const bool has_no_output = (m_TextureSource == ovrl_texsource_none);
OverlayConfigData& data = OverlayManager::Get().GetConfigData(m_ID);
if (data.ConfigBool[configid_bool_overlay_state_no_output] != has_no_output)
{
data.ConfigBool[configid_bool_overlay_state_no_output] = has_no_output;
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, m_ID);
IPCManager::Get().PostConfigMessageToUIApp(configid_bool_overlay_state_no_output, has_no_output);
IPCManager::Get().PostConfigMessageToUIApp(configid_int_state_overlay_current_id_override, -1);
}
}
if (theater_refresh_needed)
{
if (OutputManager* outmgr = OutputManager::Get())
{
unsigned int current_overlay_old = OverlayManager::Get().GetCurrentOverlayID();
OverlayManager::Get().SetCurrentOverlayID(m_ID);
outmgr->ResetCurrentOverlay();
OverlayManager::Get().SetCurrentOverlayID(current_overlay_old);
}
}
}
OverlayTextureSource Overlay::GetTextureSource() const
{
return m_TextureSource;
}
void Overlay::OnDesktopDuplicationUpdate()
{
if ( (m_Visible) && (m_TextureSource == ovrl_texsource_desktop_duplication_3dou_converted) )
{
OutputManager::Get()->ConvertOUtoSBS(*this, m_OUtoSBSConverter);
}
}
RadialFollowCore& Overlay::GetSmootherPos()
{
return m_SmootherPos;
}
RadialFollowCore& Overlay::GetSmootherRot()
{
return m_SmootherRot;
}
================================================
FILE: src/DesktopPlus/Overlays.h
================================================
#pragma once
#include "openvr.h"
#include "DPRect.h"
#include "OUtoSBSConverter.h"
#include "RadialFollowSmoothing.h"
//About the Overlay class:
//OutputManager's m_OvrlHandleDesktopTexture holds the actual texture handle for every other desktop duplication overlay created by SteamVR
//This is *not* documented functionality in SteamVR, but it is the one with the best results.
//Additional overlays are also almost free except for the compositor rendering them.
//The alternative approach for this would be the documented way of using one texture handle for every overlay created by the overlay application, but this
//prevents SteamVR from using the advanced overlay texture filter, so we'd get blurry overlays. Not good.
//Given that variant exists, doing it this way is probably somewhat safe. It wouldn't be super hard to fix this up if it broke eventually, though.
//Using a separate texture for every overlay would be slower and take up more memory, so there's honestly no upside of that.
enum OverlayTextureSource
{
ovrl_texsource_invalid = -1, //Initial state, shouldn't be set to unless immediately calling SetTextureSource() to explicitly re-apply certain sources
ovrl_texsource_none, //Used with capture sources other than desktop duplication while capture is not active
ovrl_texsource_desktop_duplication,
ovrl_texsource_desktop_duplication_3dou_converted,
ovrl_texsource_winrt_capture,
ovrl_texsource_ui,
ovrl_texsource_browser
};
class Overlay
{
private:
unsigned int m_ID;
vr::VROverlayHandle_t m_OvrlHandle;
bool m_Visible; //IVROverlay::IsOverlayVisible() is unreliable if the state changed during the same frame so we keep track ourselves
float m_Opacity; //This is the opacity the overlay is currently set at, which may differ from what the config value is
DPRect m_ValidatedCropRect; //Validated cropping rectangle used in OutputManager::Update() to check against dirty update regions
OverlayTextureSource m_TextureSource;
OUtoSBSConverter m_OUtoSBSConverter;
RadialFollowCore m_SmootherPos;
RadialFollowCore m_SmootherRot;
public:
Overlay(unsigned int id);
Overlay(Overlay&& b);
Overlay& operator=(Overlay&& b);
~Overlay();
void InitOverlay();
void AssignDesktopDuplicationTexture();
unsigned int GetID() const;
void SetID(unsigned int id);
vr::VROverlayHandle_t GetHandle() const;
void SetHandle(vr::VROverlayHandle_t handle); //Sets the handle of the overlay without calling DestroyOverlay() on the previous one, used by OverlayManager
void SetOpacity(float opacity);
float GetOpacity() const;
void SetVisible(bool visible); //Call OutputManager::Show/HideOverlay() instead of this to properly manage duplication state based on active overlays
bool IsVisible() const;
bool ShouldBeVisible() const;
void UpdateValidatedCropRect();
const DPRect& GetValidatedCropRect() const;
void SetTextureSource(OverlayTextureSource tex_source);
OverlayTextureSource GetTextureSource() const;
void OnDesktopDuplicationUpdate(); //Called by OutputManager::RefreshOpenVROverlayTexture() for every overlay, but only if the texture has actually changed
RadialFollowCore& GetSmootherPos();
RadialFollowCore& GetSmootherRot();
};
================================================
FILE: src/DesktopPlus/PixelShader.hlsl
================================================
Texture2D tx : register( t0 );
SamplerState samLinear : register( s0 );
struct PS_INPUT
{
float4 Pos : SV_POSITION;
float2 Tex : TEXCOORD;
};
//--------------------------------------------------------------------------------------
// Pixel Shader
//--------------------------------------------------------------------------------------
float4 PS(PS_INPUT input) : SV_Target
{
float4 color;
color = tx.Sample(samLinear, input.Tex);
color.a = (color.a > 0.0) ? 1.0 : 0.0; //Enforce 1-bit alpha as on some systems the duplication output isn't opaque for some reason
return color;
}
================================================
FILE: src/DesktopPlus/PixelShaderCursor.hlsl
================================================
Texture2D tx : register( t0 );
SamplerState samLinear : register( s0 );
struct PS_INPUT
{
float4 Pos : SV_POSITION;
float2 Tex : TEXCOORD;
};
//--------------------------------------------------------------------------------------
// Pixel Shader
//--------------------------------------------------------------------------------------
float4 PSCURSOR(PS_INPUT input) : SV_Target
{
return tx.Sample(samLinear, input.Tex);
}
================================================
FILE: src/DesktopPlus/RadialFollowSmoothing.cpp
================================================
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//This source file and accompanying header is based on
//AbstractQbit's Radial Follow Smoothing OpenTabletDriver Plugin (https://github.com/AbstractQbit/AbstractOTDPlugins) (RadialFollowCore.cs only)
#include "RadialFollowSmoothing.h"
#include
double RadialFollowCore::GetOuterRadius()
{
return m_RadiusOuter;
}
void RadialFollowCore::SetOuterRadius(double value)
{
m_RadiusOuter = clamp(m_RadiusOuter, 0.0, 1000000.0);
}
double RadialFollowCore::GetInnerRadius()
{
return m_RadiusInner;
}
void RadialFollowCore::SetInnerRadius(double value)
{
m_RadiusInner = clamp(value, 0.0, 1000000.0);
}
double RadialFollowCore::GetSmoothingCoefficient()
{
return m_SmoothingCoef;
}
void RadialFollowCore::SetSmoothingCoefficient(double value)
{
m_SmoothingCoef = clamp(value, 0.0001, 1.0);
}
double RadialFollowCore::GetSoftKneeScale()
{
return m_SoftKneeScale;
}
void RadialFollowCore::SetSoftKneeScale(double value)
{
m_SoftKneeScale = clamp(value, 0.0, 100.0);
UpdateDerivedParams();
}
double RadialFollowCore::GetSmoothingLeakCoefficient()
{
return m_SmoothingLeakCoef;
}
void RadialFollowCore::SetSmoothingLeakCoefficient(double value)
{
m_SmoothingLeakCoef = clamp(value, 0.0, 1.0);
}
bool RadialFollowCore::GetDetectInterruptions()
{
return m_DetectInterruptions;
}
void RadialFollowCore::SetDetectInterruptions(bool value)
{
m_DetectInterruptions = value;
}
void RadialFollowCore::ApplyPresetSettings(int preset_id)
{
preset_id = clamp(preset_id, 0, 5);
//These are just presets used by Desktop+, in hopes that they make sense for laser pointing and are easier to use than adjusting values directly
switch (preset_id)
{
case 0: //Not really used, skip calling Filter() entirely instead
{
SetOuterRadius(0.0);
SetInnerRadius(0.0);
SetSmoothingCoefficient(0.0);
SetSoftKneeScale(0.0);
SetSmoothingLeakCoefficient(0.0);
break;
}
case 1:
{
SetOuterRadius(5.0);
SetInnerRadius(0.5);
SetSmoothingCoefficient(0.95);
SetSoftKneeScale(1.0);
SetSmoothingLeakCoefficient(0.0);
break;
}
case 2:
{
SetOuterRadius(5.0);
SetInnerRadius(3.0);
SetSmoothingCoefficient(0.95);
SetSoftKneeScale(1.0);
SetSmoothingLeakCoefficient(0.0);
break;
}
case 3:
{
SetOuterRadius(7.5);
SetInnerRadius(4.5);
SetSmoothingCoefficient(1.0);
SetSoftKneeScale(5.0);
SetSmoothingLeakCoefficient(0.25);
break;
}
case 4:
{
SetOuterRadius(12.5);
SetInnerRadius(7.5);
SetSmoothingCoefficient(1.0);
SetSoftKneeScale(10.0);
SetSmoothingLeakCoefficient(0.5);
break;
}
case 5:
{
SetOuterRadius(32.0);
SetInnerRadius(12.0);
SetSmoothingCoefficient(1.0);
SetSoftKneeScale(50.0);
SetSmoothingLeakCoefficient(0.75);
break;
}
}
}
float RadialFollowCore::SampleRadialCurve(float dist)
{
return (float)DeltaFn(dist, m_XOffset, m_ScaleComp);
}
Vector2 RadialFollowCore::Filter(const Vector2& target)
{
Vector2 direction = target - m_LastPos;
float distToMove = SampleRadialCurve(direction.length());
direction.normalize();
m_LastPos = m_LastPos + (direction * distToMove);
//Catch NaNs and interrupted input
if ( !((std::isfinite(m_LastPos.x)) && (std::isfinite(m_LastPos.y))) || ((m_DetectInterruptions) && (::GetTickCount64() > m_LastTick + 50)) )
m_LastPos = target;
m_LastTick = ::GetTickCount64();
return m_LastPos;
}
Vector3 RadialFollowCore::Filter(const Vector3& target)
{
Vector3 direction = target - m_LastPos3;
float distToMove = SampleRadialCurve(direction.length());
direction.normalize();
m_LastPos3 = m_LastPos3 + (direction * distToMove);
//Catch NaNs and interrupted input
if ( !((std::isfinite(m_LastPos3.x)) && (std::isfinite(m_LastPos3.y)) && (std::isfinite(m_LastPos3.z))) || ((m_DetectInterruptions) && (::GetTickCount64() > m_LastTick + 50)) )
m_LastPos3 = target;
m_LastTick = ::GetTickCount64();
return m_LastPos3;
}
Vector3 RadialFollowCore::FilterWrapped(const Vector3& target, float value_min, float value_max)
{
auto unwrap_to_nearest = [&](const float value_wrapped, const float value_prev)
{
//Unwrap value in a way that ensures close to the previous one
const float range_width = value_max - value_min;
const float value_in_range = value_wrapped - std::floor((value_wrapped - value_min) / range_width) * range_width;
//Shift by multiples of range_width so the value is +/- half range_width to the previous value
const float delta = value_prev - value_in_range;
return value_in_range + std::round(delta / range_width) * range_width;
};
Vector3 target_unwrapped = target;
target_unwrapped.x = unwrap_to_nearest(target.x, m_LastPos3.x);
target_unwrapped.y = unwrap_to_nearest(target.y, m_LastPos3.y);
target_unwrapped.z = unwrap_to_nearest(target.z, m_LastPos3.z);
Vector3 direction = target_unwrapped - m_LastPos3;
float distToMove = SampleRadialCurve(direction.length());
direction.normalize();
m_LastPos3 = m_LastPos3 + (direction * distToMove);
//Catch NaNs and interrupted input
if ( !((std::isfinite(m_LastPos3.x)) && (std::isfinite(m_LastPos3.y)) && (std::isfinite(m_LastPos3.z))) || ((m_DetectInterruptions) && (::GetTickCount64() > m_LastTick + 50)) )
m_LastPos3 = target;
m_LastTick = ::GetTickCount64();
return m_LastPos3;
}
void RadialFollowCore::ResetLastPos()
{
//Filter() functions will already fall back to the target pos if any results aren't finite so this does the trick
m_LastPos.x = INFINITY;
m_LastPos3.x = INFINITY;
}
void RadialFollowCore::UpdateDerivedParams()
{
if (m_SoftKneeScale > 0.0001f)
{
m_XOffset = GetXOffset();
m_ScaleComp = GetScaleComp();
}
else //Calculating them with functions would use / by 0
{
m_XOffset = -1.0;
m_ScaleComp = 1.0;
}
}
double RadialFollowCore::KneeFunc(double x)
{
if (x < -3.0)
return x;
else if (x < 3.0)
return log(tanh(exp(x)));
else
return 0.0;
}
double RadialFollowCore::KneeScaled(double x)
{
if (m_SoftKneeScale > 0.0001)
return m_SoftKneeScale * KneeFunc(x / m_SoftKneeScale) + 1.0;
else
return (x > 0.0) ? 1.0 : 1.0 + x;
}
double RadialFollowCore::InverseTanh(double x)
{
return log((1.0 + x) / (1.0 - x)) / 2.0;
}
double RadialFollowCore::InverseKneeScaled(double x)
{
return m_SoftKneeScale * log(InverseTanh(exp((x - 1.0) / m_SoftKneeScale)));
}
double RadialFollowCore::DeriveKneeScaled(double x)
{
const double x_e = exp(x / m_SoftKneeScale);
const double x_e_tanh = tanh(x_e);
return (x_e - x_e * (x_e_tanh * x_e_tanh)) / x_e_tanh;
}
double RadialFollowCore::GetXOffset()
{
return InverseKneeScaled(0.0);
}
double RadialFollowCore::GetScaleComp()
{
return DeriveKneeScaled(GetXOffset());
}
double RadialFollowCore::GetRadiusOuterAdjusted()
{
return m_GridScale * std::max(m_RadiusOuter, m_RadiusInner + 0.0001);
}
double RadialFollowCore::GetRadiusInnerAdjusted()
{
return m_GridScale * m_RadiusInner;
}
double RadialFollowCore::LeakedFn(double x, double offset, double scaleComp)
{
return KneeScaled(x + offset) * (1 - m_SmoothingLeakCoef) + x * m_SmoothingLeakCoef * scaleComp;
}
double RadialFollowCore::SmoothedFn(double x, double offset, double scaleComp)
{
return LeakedFn(x * m_SmoothingCoef / scaleComp, offset, scaleComp);
}
double RadialFollowCore::ScaleToOuter(double x, double offset, double scaleComp)
{
return (GetRadiusOuterAdjusted() - GetRadiusInnerAdjusted()) * SmoothedFn(x / (GetRadiusOuterAdjusted() - GetRadiusInnerAdjusted()), offset, scaleComp);
}
double RadialFollowCore::DeltaFn(double x, double offset, double scaleComp)
{
return (x > GetRadiusInnerAdjusted()) ? x - ScaleToOuter(x - GetRadiusInnerAdjusted(), offset, scaleComp) - GetRadiusInnerAdjusted() : 0.0;
}
================================================
FILE: src/DesktopPlus/RadialFollowSmoothing.h
================================================
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//This header and accompanying source file is based on
//AbstractQbit's Radial Follow Smoothing OpenTabletDriver Plugin (https://github.com/AbstractQbit/AbstractOTDPlugins) (RadialFollowCore.cs only)
#pragma once
#include "Util.h"
class RadialFollowCore
{
public:
double GetOuterRadius();
void SetOuterRadius(double value);
double GetInnerRadius();
void SetInnerRadius(double value);
double GetSmoothingCoefficient();
void SetSmoothingCoefficient(double value);
double GetSoftKneeScale();
void SetSoftKneeScale(double value);
double GetSmoothingLeakCoefficient();
void SetSmoothingLeakCoefficient(double value);
bool GetDetectInterruptions();
void SetDetectInterruptions(bool value);
void ApplyPresetSettings(int preset_id);
float SampleRadialCurve(float dist);
Vector2 Filter(const Vector2& target);
Vector3 Filter(const Vector3& target);
Vector3 FilterWrapped(const Vector3& target, float value_min, float value_max); //Treats changes like max to min as small steps, but doesn't wrap the return value
void ResetLastPos();
private:
double m_RadiusOuter = 5.0;
double m_RadiusInner = 0.0;
double m_SmoothingCoef = 0.95;
double m_SoftKneeScale = 1.0;
double m_SmoothingLeakCoef = 0.0;
double m_GridScale = 1.0;
Vector2 m_LastPos;
Vector3 m_LastPos3;
ULONGLONG m_LastTick = 0;
bool m_DetectInterruptions = true;
double m_XOffset = -1.0;
double m_ScaleComp = 1.0;
void UpdateDerivedParams();
//Math functions
double KneeFunc(double x);
double KneeScaled(double x);
double InverseTanh(double x);
double InverseKneeScaled(double x);
double DeriveKneeScaled(double x);
double GetXOffset();
double GetScaleComp();
double GetRadiusOuterAdjusted();
double GetRadiusInnerAdjusted();
double LeakedFn(double x, double offset, double scaleComp);
double SmoothedFn(double x, double offset, double scaleComp);
double ScaleToOuter(double x, double offset, double scaleComp);
double DeltaFn(double x, double offset, double scaleComp);
};
================================================
FILE: src/DesktopPlus/SoftwareCursorGrabber.cpp
================================================
#include "SoftwareCursorGrabber.h"
#include "Logging.h"
bool SoftwareCursorGrabber::CopyMonoMask(const ICONINFO& icon_info)
{
bool ret = false;
HDC hdc = ::GetDC(nullptr);
if (!hdc)
return ret;
//Get bitmap info from cursor bitmap
BITMAPINFO bmp_info = {0};
bmp_info.bmiHeader.biSize = sizeof(bmp_info.bmiHeader);
if (::GetDIBits(hdc, icon_info.hbmMask, 0, 0, nullptr, &bmp_info, DIB_RGB_COLORS) != 0)
{
int cursor_width = bmp_info.bmiHeader.biWidth;
int cursor_height = abs(bmp_info.bmiHeader.biHeight);
bmp_info.bmiHeader.biSize = sizeof(bmp_info.bmiHeader);
bmp_info.bmiHeader.biBitCount = 1;
bmp_info.bmiHeader.biCompression = BI_RGB;
bmp_info.bmiHeader.biHeight = -cursor_height; //Always use top-down order (negative height)
//Add monochrome palette (and deal with the dynamic struct size)
RGBQUAD palette[2] = {{0, 0, 0, 0}, {255, 255, 255, 255}};
auto bmp_info_extended_data = std::unique_ptr{new BYTE[sizeof(bmp_info.bmiHeader) + sizeof(palette)]};
BITMAPINFO* bmp_info_extended_ptr = (BITMAPINFO*)bmp_info_extended_data.get();
memcpy(&bmp_info_extended_ptr->bmiHeader, &bmp_info.bmiHeader, sizeof(bmp_info.bmiHeader));
memcpy(bmp_info_extended_ptr->bmiColors, palette, sizeof(palette));
//Read the actual bitmap buffer into a temporary pixel data array (4-byte aligned)
const int padded_stride = ((cursor_width + 31) / 32) * 4;
auto temp_buffer = std::unique_ptr{new BYTE[padded_stride * cursor_height]};
if (::GetDIBits(hdc, icon_info.hbmMask, 0, cursor_height, temp_buffer.get(), bmp_info_extended_ptr, DIB_RGB_COLORS) != 0)
{
//Remove padding returned by GetDIBits so we end up with something that matches what Desktop Duplication returns
size_t used_bytes_per_scanline = ((size_t)cursor_width + 7) / 8;
m_DDPPointerInfo.ShapeBuffer.assign(used_bytes_per_scanline * cursor_height, 0);
for (int y = 0; y < cursor_height; ++y)
{
BYTE* src_line = temp_buffer.get() + y * padded_stride;
BYTE* dst_line = m_DDPPointerInfo.ShapeBuffer.data() + y * used_bytes_per_scanline;
memcpy(dst_line, src_line, used_bytes_per_scanline);
}
m_DDPPointerInfo.ShapeInfo.Width = cursor_width;
m_DDPPointerInfo.ShapeInfo.Height = cursor_height;
m_DDPPointerInfo.ShapeInfo.Pitch = 4;
ret = true;
}
}
::ReleaseDC(nullptr, hdc);
return ret;
}
bool SoftwareCursorGrabber::CopyColor(const ICONINFO& icon_info)
{
bool ret = false;
HDC hdc = ::GetDC(nullptr);
if (!hdc)
return ret;
BITMAPINFO bmp_info = {0};
bmp_info.bmiHeader.biSize = sizeof(bmp_info.bmiHeader);
if (::GetDIBits(hdc, icon_info.hbmColor, 0, 0, nullptr, &bmp_info, DIB_RGB_COLORS) != 0)
{
int cursor_width = bmp_info.bmiHeader.biWidth;
int cursor_height = abs(bmp_info.bmiHeader.biHeight);
bmp_info.bmiHeader.biSize = sizeof(bmp_info.bmiHeader);
bmp_info.bmiHeader.biBitCount = 32;
bmp_info.bmiHeader.biCompression = BI_RGB;
bmp_info.bmiHeader.biHeight = -cursor_height; //Always use top-down order (negative height)
m_DDPPointerInfo.ShapeBuffer.resize((size_t)cursor_width * cursor_height * 4);
if (::GetDIBits(hdc, icon_info.hbmColor, 0, cursor_height, m_DDPPointerInfo.ShapeBuffer.data(), &bmp_info, DIB_RGB_COLORS) != 0)
{
//Even if we don't override biBitCount to 32, it's still returned as that for 24-bit and lower bit-depth cursors (probably just the screen DC format)
//It seems the only way to check if the cursor needs its mask applied is to see if the alpha channel is fully blank
//32-bit cursors still come with masks, but applying them means to override the alpha channel with a 1-bit one (and doing so is also wasteful)
const bool needs_mask = IsShapeBufferBlank(m_DDPPointerInfo.ShapeBuffer.data(), m_DDPPointerInfo.ShapeBuffer.size(), DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR);
if (needs_mask)
{
auto pixel_data_mask = std::unique_ptr{new BYTE[m_DDPPointerInfo.ShapeBuffer.size()]};
//Read the mask bitmap buffer
if (::GetDIBits(hdc, icon_info.hbmMask, 0, cursor_height, (LPVOID)pixel_data_mask.get(), &bmp_info, DIB_RGB_COLORS) != 0)
{
//Apply mask to color pixel data
BYTE* psrc = m_DDPPointerInfo.ShapeBuffer.data() + 3; //BGRA alpha pixel
BYTE* pmsk = pixel_data_mask.get(); //BGRA blue pixel (alpha channel is blank for the mask)
const BYTE* const psrc_end = m_DDPPointerInfo.ShapeBuffer.data() + m_DDPPointerInfo.ShapeBuffer.size();
for (; psrc < psrc_end; psrc += 4, pmsk += 4)
{
*psrc = ~(*pmsk);
}
}
}
m_DDPPointerInfo.ShapeInfo.Width = cursor_width;
m_DDPPointerInfo.ShapeInfo.Height = cursor_height;
m_DDPPointerInfo.ShapeInfo.Pitch = cursor_width * 4;
ret = true;
}
}
::ReleaseDC(nullptr, hdc);
return ret;
}
bool SoftwareCursorGrabber::IsShapeBufferBlank(BYTE* psrc, size_t size, UINT type)
{
if (type == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR)
{
const BYTE* const psrc_end = psrc + size;
psrc = psrc + 3; //first BGRA alpha pixel
for (; psrc < psrc_end; psrc += 4)
{
if (*psrc != 0)
{
return false;
}
}
}
else if (type == DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME)
{
const BYTE* const psrc_end = psrc + size;
psrc = psrc + (size / 2); //first mask pixel
for (; psrc < psrc_end; ++psrc)
{
if (*psrc != 0)
{
return false;
}
}
}
return true;
}
bool SoftwareCursorGrabber::SynthesizeDDPCursorInfo()
{
LOG_IF_F(INFO, !m_LoggedOnceUsed, "Using Alternative Cursor Rendering");
m_LoggedOnceUsed = true;
CURSORINFO cursor_info = {0};
cursor_info.cbSize = sizeof(CURSORINFO);
bool ret = false;
if (::GetCursorInfo(&cursor_info))
{
const bool shape_changed = (cursor_info.hCursor != m_CursorInfoLast.hCursor);
if (shape_changed)
{
if (cursor_info.hCursor != nullptr)
{
ICONINFO icon_info = {};
if (::GetIconInfo(cursor_info.hCursor, &icon_info))
{
ret = (icon_info.hbmColor != nullptr) ? CopyColor(icon_info) : CopyMonoMask(icon_info);
m_DDPPointerInfo.ShapeInfo.Type = (icon_info.hbmColor != nullptr) ? DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR : DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME;
::DeleteObject(icon_info.hbmColor);
::DeleteObject(icon_info.hbmMask);
}
//Fallback: If getting the icon info failed or "succeeded" with blank data, try copying the default arrow pointer instead
if ((!ret) || (IsShapeBufferBlank(m_DDPPointerInfo.ShapeBuffer.data(), m_DDPPointerInfo.ShapeBuffer.size(), m_DDPPointerInfo.ShapeInfo.Type)))
{
LOG_IF_F(WARNING, !m_LoggedOnceFallbackDefault, "Alternative Cursor Rendering: Getting current cursor failed. Falling back to default cursor.");
m_LoggedOnceFallbackDefault = true;
ret = false;
if (::GetIconInfo(::LoadCursor(nullptr, IDC_ARROW), &icon_info))
{
ret = (icon_info.hbmColor != nullptr) ? CopyColor(icon_info) : CopyMonoMask(icon_info);
m_DDPPointerInfo.ShapeInfo.Type = (icon_info.hbmColor != nullptr) ? DXGI_OUTDUPL_POINTER_SHAPE_TYPE_COLOR : DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME;
//Check if somehow still blank
if (ret)
{
ret = !IsShapeBufferBlank(m_DDPPointerInfo.ShapeBuffer.data(), m_DDPPointerInfo.ShapeBuffer.size(), m_DDPPointerInfo.ShapeInfo.Type);
}
::DeleteObject(icon_info.hbmColor);
::DeleteObject(icon_info.hbmMask);
}
}
//Set other data if cursor copy was successful
if (ret)
{
m_DDPPointerInfo.CursorShapeChanged = true;
m_DDPPointerInfo.ShapeInfo.HotSpot.x = icon_info.xHotspot;
m_DDPPointerInfo.ShapeInfo.HotSpot.y = icon_info.yHotspot;
}
else
{
LOG_IF_F(WARNING, !m_LoggedOnceFallbackBlob, "Alternative Cursor Rendering: Getting default cursor failed. Falling back to simple blob.");
m_LoggedOnceFallbackBlob = true;
//Everything else failed, put a blob there
m_DDPPointerInfo.ShapeInfo.Width = 12;
m_DDPPointerInfo.ShapeInfo.Height = 19 * 2; //Double height for mask
m_DDPPointerInfo.ShapeBuffer.assign((size_t)m_DDPPointerInfo.ShapeInfo.Width * m_DDPPointerInfo.ShapeInfo.Height, 255);
m_DDPPointerInfo.CursorShapeChanged = true;
m_DDPPointerInfo.ShapeInfo.Type = DXGI_OUTDUPL_POINTER_SHAPE_TYPE_MONOCHROME;
m_DDPPointerInfo.ShapeInfo.Pitch = 4;
m_DDPPointerInfo.ShapeInfo.HotSpot.x = 0;
m_DDPPointerInfo.ShapeInfo.HotSpot.y = 0;
ret = true;
}
}
else
{
//Create blank 1x1 pixel data for null cursor handle
m_DDPPointerInfo.ShapeBuffer.assign(4, 0);
m_DDPPointerInfo.ShapeInfo.Width = 1;
m_DDPPointerInfo.ShapeInfo.Height = 1;
ret = true;
}
}
else
{
ret = true;
}
if (ret)
{
m_DDPPointerInfo.Visible = (cursor_info.flags == CURSOR_SHOWING);
m_DDPPointerInfo.Position = cursor_info.ptScreenPos;
m_DDPPointerInfo.Position.x -= m_DDPPointerInfo.ShapeInfo.HotSpot.x;
m_DDPPointerInfo.Position.y -= m_DDPPointerInfo.ShapeInfo.HotSpot.y;
QueryPerformanceCounter(&m_DDPPointerInfo.LastTimeStamp);
}
m_CursorInfoLast = cursor_info;
}
return ret;
}
PTR_INFO& SoftwareCursorGrabber::GetDDPCursorInfo()
{
return m_DDPPointerInfo;
}
================================================
FILE: src/DesktopPlus/SoftwareCursorGrabber.h
================================================
#pragma once
#include
#include "CommonTypes.h"
//Alternative method to grab the cursor image data
//Stores the data in the same way the DuplicationManager class does for Desktop Duplication cursor output to act as a drop-in alternative
class SoftwareCursorGrabber
{
private:
CURSORINFO m_CursorInfoLast = {};
PTR_INFO m_DDPPointerInfo = {};
std::unordered_map m_CursorUseMaskCache;
//Log things only once per session as it would be quite spammy otherwise
bool m_LoggedOnceUsed = false;
bool m_LoggedOnceFallbackDefault = false;
bool m_LoggedOnceFallbackBlob = false;
bool CopyMonoMask(const ICONINFO& icon_info);
bool CopyColor(const ICONINFO& icon_info);
static bool IsShapeBufferBlank(BYTE* psrc, size_t size, UINT type);
public:
bool SynthesizeDDPCursorInfo();
PTR_INFO& GetDDPCursorInfo();
};
================================================
FILE: src/DesktopPlus/ThreadManager.cpp
================================================
#include "ThreadManager.h"
DWORD WINAPI CaptureThreadEntry(_In_ void* Param);
THREADMANAGER::THREADMANAGER() : m_ThreadCount(0),
m_ThreadHandles(nullptr),
m_ThreadData(nullptr)
{
}
THREADMANAGER::~THREADMANAGER()
{
Clean();
}
//
// Clean up resources
//
void THREADMANAGER::Clean()
{
m_PtrInfo = PTR_INFO();
if (m_ThreadHandles)
{
for (UINT i = 0; i < m_ThreadCount; ++i)
{
if (m_ThreadHandles[i])
{
CloseHandle(m_ThreadHandles[i]);
}
}
delete [] m_ThreadHandles;
m_ThreadHandles = nullptr;
}
if (m_ThreadData)
{
for (UINT i = 0; i < m_ThreadCount; ++i)
{
CleanDx(&m_ThreadData[i].DxRes);
}
delete [] m_ThreadData;
m_ThreadData = nullptr;
}
m_ThreadCount = 0;
}
//
// Clean up DX_RESOURCES
//
void THREADMANAGER::CleanDx(_Inout_ DX_RESOURCES* Data)
{
if (Data->Device)
{
Data->Device->Release();
Data->Device = nullptr;
}
if (Data->Context)
{
Data->Context->Release();
Data->Context = nullptr;
}
if (Data->VertexShader)
{
Data->VertexShader->Release();
Data->VertexShader = nullptr;
}
if (Data->PixelShader)
{
Data->PixelShader->Release();
Data->PixelShader = nullptr;
}
if (Data->InputLayout)
{
Data->InputLayout->Release();
Data->InputLayout = nullptr;
}
if (Data->Sampler)
{
Data->Sampler->Release();
Data->Sampler = nullptr;
}
}
//
// Start up threads for DDA
//
DUPL_RETURN THREADMANAGER::Initialize(INT SingleOutput, UINT OutputCount, HANDLE UnexpectedErrorEvent, HANDLE ExpectedErrorEvent, HANDLE NewFrameProcessedEvent,
HANDLE PauseDuplicationEvent, HANDLE ResumeDuplicationEvent, HANDLE TerminateThreadsEvent,
HANDLE SharedHandle, _In_ RECT* DesktopDim, IDXGIAdapter* DXGIAdapter, bool WMRIgnoreVScreens)
{
m_ThreadCount = OutputCount;
m_ThreadHandles = new (std::nothrow) HANDLE[m_ThreadCount];
m_ThreadData = new (std::nothrow) THREAD_DATA[m_ThreadCount];
if (!m_ThreadHandles || !m_ThreadData)
{
return ProcessFailure(nullptr, L"Failed to allocate array for threads", L"Desktop+ Error", E_OUTOFMEMORY);
}
// Create appropriate # of threads for duplication
DUPL_RETURN Ret = DUPL_RETURN_SUCCESS;
for (UINT i = 0; i < m_ThreadCount; ++i)
{
m_ThreadData[i].UnexpectedErrorEvent = UnexpectedErrorEvent;
m_ThreadData[i].ExpectedErrorEvent = ExpectedErrorEvent;
m_ThreadData[i].NewFrameProcessedEvent = NewFrameProcessedEvent;
m_ThreadData[i].PauseDuplicationEvent = PauseDuplicationEvent;
m_ThreadData[i].ResumeDuplicationEvent = ResumeDuplicationEvent;
m_ThreadData[i].TerminateThreadsEvent = TerminateThreadsEvent;
m_ThreadData[i].Output = (SingleOutput < 0) ? i : SingleOutput;
m_ThreadData[i].TexSharedHandle = SharedHandle;
m_ThreadData[i].OffsetX = DesktopDim->left;
m_ThreadData[i].OffsetY = DesktopDim->top;
m_ThreadData[i].PtrInfo = &m_PtrInfo;
m_ThreadData[i].DirtyRegionTotal = &m_DirtyRegionTotal;
m_ThreadData[i].WMRIgnoreVScreens = WMRIgnoreVScreens;
RtlZeroMemory(&m_ThreadData[i].DxRes, sizeof(DX_RESOURCES));
Ret = InitializeDx(&m_ThreadData[i].DxRes, DXGIAdapter);
if (Ret != DUPL_RETURN_SUCCESS)
{
if (DXGIAdapter != nullptr)
DXGIAdapter->Release();
return Ret;
}
DWORD ThreadId;
m_ThreadHandles[i] = CreateThread(nullptr, 0, CaptureThreadEntry, &m_ThreadData[i], 0, &ThreadId);
if (m_ThreadHandles[i] == nullptr)
{
if (DXGIAdapter != nullptr)
DXGIAdapter->Release();
return ProcessFailure(nullptr, L"Failed to create thread", L"Desktop+ Error", E_FAIL);
}
}
if (DXGIAdapter != nullptr)
DXGIAdapter->Release();
return Ret;
}
//
// Get DX_RESOURCES
//
DUPL_RETURN THREADMANAGER::InitializeDx(_Out_ DX_RESOURCES* Data, IDXGIAdapter* DXGIAdapter)
{
HRESULT hr = S_OK;
// Driver types supported
D3D_DRIVER_TYPE DriverTypes[] =
{
D3D_DRIVER_TYPE_HARDWARE,
D3D_DRIVER_TYPE_WARP,
D3D_DRIVER_TYPE_REFERENCE,
};
UINT NumDriverTypes = ARRAYSIZE(DriverTypes);
// Feature levels supported
D3D_FEATURE_LEVEL FeatureLevels[] =
{
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_9_1
};
UINT NumFeatureLevels = ARRAYSIZE(FeatureLevels);
D3D_FEATURE_LEVEL FeatureLevel;
// Create device
hr = D3D11CreateDevice(DXGIAdapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, FeatureLevels, NumFeatureLevels,
D3D11_SDK_VERSION, &Data->Device, &FeatureLevel, &Data->Context);
if (FAILED(hr))
{
return ProcessFailure(nullptr, L"Failed to create device for thread", L"Desktop+ Error", hr);
}
// VERTEX shader
UINT Size = ARRAYSIZE(g_VS);
hr = Data->Device->CreateVertexShader(g_VS, Size, nullptr, &Data->VertexShader);
if (FAILED(hr))
{
return ProcessFailure(Data->Device, L"Failed to create vertex shader for thread", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
// Input layout
D3D11_INPUT_ELEMENT_DESC Layout[] =
{
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0},
{"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0}
};
UINT NumElements = ARRAYSIZE(Layout);
hr = Data->Device->CreateInputLayout(Layout, NumElements, g_VS, Size, &Data->InputLayout);
if (FAILED(hr))
{
return ProcessFailure(Data->Device, L"Failed to create input layout for thread", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
Data->Context->IASetInputLayout(Data->InputLayout);
// Pixel shader
Size = ARRAYSIZE(g_PS);
hr = Data->Device->CreatePixelShader(g_PS, Size, nullptr, &Data->PixelShader);
if (FAILED(hr))
{
return ProcessFailure(Data->Device, L"Failed to create pixel shader for thread", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
// Set up sampler
D3D11_SAMPLER_DESC SampDesc;
RtlZeroMemory(&SampDesc, sizeof(SampDesc));
SampDesc.Filter = D3D11_FILTER_MIN_MAG_MIP_POINT;
SampDesc.AddressU = D3D11_TEXTURE_ADDRESS_CLAMP;
SampDesc.AddressV = D3D11_TEXTURE_ADDRESS_CLAMP;
SampDesc.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
SampDesc.ComparisonFunc = D3D11_COMPARISON_NEVER;
SampDesc.MinLOD = 0;
SampDesc.MaxLOD = D3D11_FLOAT32_MAX;
hr = Data->Device->CreateSamplerState(&SampDesc, &Data->Sampler);
if (FAILED(hr))
{
return ProcessFailure(Data->Device, L"Failed to create sampler state for thread", L"Desktop+ Error", hr, SystemTransitionsExpectedErrors);
}
return DUPL_RETURN_SUCCESS;
}
//
// Getter for the PTR_INFO structure
//
PTR_INFO* THREADMANAGER::GetPointerInfo()
{
return &m_PtrInfo;
}
DPRect& THREADMANAGER::GetDirtyRegionTotal()
{
return m_DirtyRegionTotal;
}
//
// Waits infinitely for all spawned threads to terminate
//
void THREADMANAGER::WaitForThreadTermination()
{
if (m_ThreadCount != 0)
{
WaitForMultipleObjectsEx(m_ThreadCount, m_ThreadHandles, TRUE, INFINITE, FALSE);
}
}
================================================
FILE: src/DesktopPlus/ThreadManager.h
================================================
#ifndef _THREADMANAGER_H_
#define _THREADMANAGER_H_
#include "CommonTypes.h"
class THREADMANAGER
{
public:
THREADMANAGER();
~THREADMANAGER();
void Clean();
DUPL_RETURN Initialize(INT SingleOutput, UINT OutputCount, HANDLE UnexpectedErrorEvent, HANDLE ExpectedErrorEvent, HANDLE NewFrameProcessedEvent,
HANDLE PauseDuplicationEvent, HANDLE ResumeDuplicationEvent, HANDLE TerminateThreadsEvent,
HANDLE SharedHandle, _In_ RECT* DesktopDim, IDXGIAdapter* DXGIAdapter, bool WMRIgnoreVScreens);
PTR_INFO* GetPointerInfo(); //Should only be called when shared surface mutex has be aquired
DPRect& GetDirtyRegionTotal(); //Should only be called when shared surface mutex has be aquired
void WaitForThreadTermination();
private:
DUPL_RETURN InitializeDx(_Out_ DX_RESOURCES* Data, IDXGIAdapter* DXGIAdapter); //Doesn't Release() the DXGIAdapter
void CleanDx(_Inout_ DX_RESOURCES* Data);
PTR_INFO m_PtrInfo;
DPRect m_DirtyRegionTotal;
UINT m_ThreadCount;
_Field_size_(m_ThreadCount) HANDLE* m_ThreadHandles;
_Field_size_(m_ThreadCount) THREAD_DATA* m_ThreadData;
};
#endif
================================================
FILE: src/DesktopPlus/VRInput.cpp
================================================
#include "VRInput.h"
#include "VRInput.h"
#define NOMINMAX
#include
#include
#include
#include "ConfigManager.h"
#include "OutputManager.h"
#include "OpenVRExt.h"
VRInput::VRInput() : m_HandleActionsetShortcuts(vr::k_ulInvalidActionSetHandle),
m_HandleActionsetLaserPointer(vr::k_ulInvalidActionSetHandle),
m_HandleActionsetScrollDiscrete(vr::k_ulInvalidActionSetHandle),
m_HandleActionsetScrollSmooth(vr::k_ulInvalidActionSetHandle),
m_HandleActionEnableGlobalLaserPointer(vr::k_ulInvalidActionHandle),
m_HandleActionLaserPointerLeftClick(vr::k_ulInvalidActionHandle),
m_HandleActionLaserPointerRightClick(vr::k_ulInvalidActionHandle),
m_HandleActionLaserPointerMiddleClick(vr::k_ulInvalidActionHandle),
m_HandleActionLaserPointerAux01Click(vr::k_ulInvalidActionHandle),
m_HandleActionLaserPointerAux02Click(vr::k_ulInvalidActionHandle),
m_HandleActionLaserPointerScrollDiscrete(vr::k_ulInvalidActionHandle),
m_HandleActionLaserPointerScrollSmooth(vr::k_ulInvalidActionHandle),
m_HandleActionLaserPointerHaptic(vr::k_ulInvalidActionHandle),
m_IsAnyGlobalActionBound(false),
m_IsAnyGlobalActionBoundStateValid(false),
m_IsLaserPointerInputActive(false),
m_LaserPointerScrollMode(vrinput_scroll_none),
m_KeyboardDeviceInputValueHandle(vr::k_ulInvalidInputValueHandle),
m_GamepadDeviceInputValueHandle(vr::k_ulInvalidInputValueHandle),
m_KeyboardDeviceToggleState{0},
m_KeyboardDeviceIsToggleKeyDown(false),
m_KeyboardDeviceClickState{0},
m_KeyboardDeviceDragState{0}
{
}
void VRInput::UpdateKeyboardDeviceState()
{
auto update_input_data = [](vr::InputDigitalActionData_t& input_data, int keycode)
{
if (keycode != 0)
{
if ((ConfigManager::GetValue(configid_bool_input_laser_pointer_hmd_device)) && (!vr::IVROverlayEx::IsSystemLaserPointerActive()))
{
input_data.bActive = true;
input_data.bChanged = false;
if (::GetAsyncKeyState(keycode) < 0)
{
if (!input_data.bState)
{
input_data.bChanged = true;
input_data.bState = true;
}
}
else if (input_data.bState)
{
input_data.bChanged = true;
input_data.bState = false;
}
}
else
{
//Drop inputs if settings disabled or system laser pointer is active
input_data.bActive = input_data.bState; //true for one frame before we set it inactive
input_data.bChanged = input_data.bState;
input_data.bState = false;
}
}
else
{
input_data.bActive = false;
input_data.bChanged = false;
input_data.bState = false;
}
};
auto update_input_data_toggle = [](vr::InputDigitalActionData_t& input_data, int keycode, bool& is_key_down)
{
if (keycode != 0)
{
if ((ConfigManager::GetValue(configid_bool_input_laser_pointer_hmd_device)) && (!vr::IVROverlayEx::IsSystemLaserPointerActive()))
{
input_data.bActive = true;
input_data.bChanged = false;
if (::GetAsyncKeyState(keycode) < 0)
{
if (!is_key_down)
{
input_data.bChanged = true;
input_data.bState = !input_data.bState;
}
is_key_down = true;
}
else
{
is_key_down = false;
}
}
else
{
//Drop inputs if settings disabled or system laser pointer is active (Desktop+ pointer won't be doing anything either way, but the toggle key should reset at least)
input_data.bActive = input_data.bState; //true for one frame before we set it inactive
input_data.bChanged = input_data.bState;
input_data.bState = false;
}
}
else
{
input_data.bActive = false;
input_data.bChanged = false;
input_data.bState = false;
}
};
//Toggle action state is always set up as a toggle binding
update_input_data_toggle(m_KeyboardDeviceToggleState, ConfigManager::GetValue(configid_int_input_laser_pointer_hmd_device_keycode_toggle), m_KeyboardDeviceIsToggleKeyDown);
update_input_data(m_KeyboardDeviceClickState[0], ConfigManager::GetValue(configid_int_input_laser_pointer_hmd_device_keycode_left));
update_input_data(m_KeyboardDeviceClickState[1], ConfigManager::GetValue(configid_int_input_laser_pointer_hmd_device_keycode_right));
update_input_data(m_KeyboardDeviceClickState[2], ConfigManager::GetValue(configid_int_input_laser_pointer_hmd_device_keycode_middle));
//Aux01/02 are not configurable but fields exist for parity with the regular action data array (they can still be pressed via actions if really needed)
update_input_data(m_KeyboardDeviceDragState, ConfigManager::GetValue(configid_int_input_laser_pointer_hmd_device_keycode_drag));
}
vr::InputDigitalActionData_t VRInput::CombineDigitalActionData(vr::InputDigitalActionData_t data_a, vr::InputDigitalActionData_t data_b)
{
//Trying to make sense of having multiple action data sources at once, with some bias towards data_a
vr::InputDigitalActionData_t data_out = {0};
data_out.bActive = data_a.bActive || data_b.bActive;
data_out.bChanged = data_a.bChanged || data_b.bChanged;
data_out.bState = data_a.bState || data_b.bState;
data_out.activeOrigin = (data_a.bState == data_out.bState) ? data_a.activeOrigin : ((data_b.bState == data_out.bState) ? data_b.activeOrigin : data_a.activeOrigin);
return data_out;
}
bool VRInput::Init()
{
//Load manifest, this will fail with VRInputError_MismatchedActionManifest when a Steam configured manifest is already associated with the app key, but we can just ignore that
vr::EVRInputError input_error = vr::VRInput()->SetActionManifestPath( (ConfigManager::Get().GetApplicationPath() + "action_manifest.json").c_str() );
if ( (input_error == vr::VRInputError_None) || (input_error == vr::VRInputError_MismatchedActionManifest) )
{
input_error = vr::VRInput()->GetActionSetHandle("/actions/shortcuts", &m_HandleActionsetShortcuts);
if (input_error != vr::VRInputError_None)
return false;
//Load actions (we assume that the files are not messed with and skip some error checking)
vr::VRInput()->GetActionHandle("/actions/shortcuts/in/EnableGlobalLaserPointer", &m_HandleActionEnableGlobalLaserPointer);
//Load as many global shortcut input actions as we can find. Up to configid_int_input_global_shortcuts_max_count at least.
//This allows for extended amounts via end-user modification, though the Steam manifest takes priority if present
m_HandleActionDoGlobalShortcuts.clear();
const int shortcut_max = ConfigManager::GetValue(configid_int_input_global_shortcuts_max_count);
for (int i = 0; i < shortcut_max; ++i)
{
vr::VRActionHandle_t handle_global_shortcut = vr::k_ulInvalidActionHandle;
std::stringstream ss;
ss << "/actions/shortcuts/in/GlobalShortcut" << std::setfill('0') << std::setw(2) << i + 1;
vr::VRInput()->GetActionHandle(ss.str().c_str(), &handle_global_shortcut);
//We do check if we got a handle here, but as of writing this, GetActionHandle() will always return a valid handle, regardless of presence in the manifest.
if (handle_global_shortcut != vr::k_ulInvalidActionHandle)
{
m_HandleActionDoGlobalShortcuts.push_back(handle_global_shortcut);
}
else
{
break;
}
}
vr::VRInput()->GetActionSetHandle("/actions/laserpointer", &m_HandleActionsetLaserPointer);
vr::VRInput()->GetActionHandle("/actions/laserpointer/in/LeftClick", &m_HandleActionLaserPointerLeftClick);
vr::VRInput()->GetActionHandle("/actions/laserpointer/in/RightClick" , &m_HandleActionLaserPointerRightClick);
vr::VRInput()->GetActionHandle("/actions/laserpointer/in/MiddleClick", &m_HandleActionLaserPointerMiddleClick);
vr::VRInput()->GetActionHandle("/actions/laserpointer/in/Aux01Click", &m_HandleActionLaserPointerAux01Click);
vr::VRInput()->GetActionHandle("/actions/laserpointer/in/Aux02Click", &m_HandleActionLaserPointerAux02Click);
vr::VRInput()->GetActionHandle("/actions/laserpointer/in/Drag", &m_HandleActionLaserPointerDrag);
vr::VRInput()->GetActionHandle("/actions/laserpointer/out/Haptic", &m_HandleActionLaserPointerHaptic);
vr::VRInput()->GetActionSetHandle("/actions/scroll_discrete", &m_HandleActionsetScrollDiscrete);
vr::VRInput()->GetActionHandle("/actions/scroll_discrete/in/ScrollDiscrete", &m_HandleActionLaserPointerScrollDiscrete);
vr::VRInput()->GetActionSetHandle("/actions/scroll_smooth", &m_HandleActionsetScrollSmooth);
vr::VRInput()->GetActionHandle("/actions/scroll_smooth/in/ScrollSmooth", &m_HandleActionLaserPointerScrollSmooth);
//This mimics OpenXR device path pattern but isn't actually formally defined (and this is OpenVR anyhow)
vr::VRInput()->GetInputSourceHandle("/user/keyboard", &m_KeyboardDeviceInputValueHandle);
//Frequently used gamepad device path
vr::VRInput()->GetInputSourceHandle("/user/gamepad", &m_GamepadDeviceInputValueHandle);
m_KeyboardDeviceToggleState.activeOrigin = m_KeyboardDeviceInputValueHandle;
for (auto& input_data : m_KeyboardDeviceClickState)
{
input_data.activeOrigin = m_KeyboardDeviceInputValueHandle;
}
m_KeyboardDeviceDragState.activeOrigin = m_KeyboardDeviceInputValueHandle;
return true;
}
return false;
}
void VRInput::Update()
{
if (m_HandleActionsetShortcuts == vr::k_ulInvalidActionSetHandle)
return;
vr::VRActiveActionSet_t actionset_desc[3] = { 0 };
int actionset_active_count = 1;
actionset_desc[0].ulActionSet = m_HandleActionsetShortcuts;
actionset_desc[0].nPriority = 100; //Arbitrary number, but probably higher than the scene application's... if that even matters
if (m_IsLaserPointerInputActive)
{
actionset_active_count = 2;
actionset_desc[1].ulActionSet = m_HandleActionsetLaserPointer;
//+2 when blocking since OVRAS uses vr::k_nActionSetOverlayGlobalPriorityMin + 1 as priority for global input
//When not blocking laser pointer inputs should have priority over global shortcuts
actionset_desc[1].nPriority = ConfigManager::GetValue(configid_bool_input_laser_pointer_block_input) ? vr::k_nActionSetOverlayGlobalPriorityMin + 2 : 101;
if (m_LaserPointerScrollMode != vrinput_scroll_none)
{
actionset_active_count = 3;
actionset_desc[2].ulActionSet = (m_LaserPointerScrollMode == vrinput_scroll_discrete) ? m_HandleActionsetScrollDiscrete : m_HandleActionsetScrollSmooth;
actionset_desc[2].nPriority = actionset_desc[1].nPriority;
}
}
vr::VRInput()->UpdateActionState(actionset_desc, sizeof(vr::VRActiveActionSet_t), actionset_active_count);
UpdateKeyboardDeviceState();
//SteamVR Input is incredibly weird with the initial action state. The first couple attempts at getting any action state will fail. Probably some async loading stuff
//However, SteamVR also does not send any events once the initial state goes valid (it does for binding state changes after this)
//As we don't want to needlessly refresh the any-action-bound state on every update, we poll it until it succeeds once and then rely on events afterwards
if (!m_IsAnyGlobalActionBoundStateValid)
{
RefreshAnyGlobalActionBound();
}
}
void VRInput::RefreshAnyGlobalActionBound()
{
auto is_action_bound = [&](vr::VRActionHandle_t action_handle)
{
vr::VRInputValueHandle_t action_origin = vr::k_ulInvalidInputValueHandle;
vr::EVRInputError error = vr::VRInput()->GetActionOrigins(m_HandleActionsetShortcuts, action_handle, &action_origin, 1);
if (action_origin != vr::k_ulInvalidInputValueHandle)
{
m_IsAnyGlobalActionBoundStateValid = true;
vr::InputOriginInfo_t action_origin_info = {0};
vr::EVRInputError error = vr::VRInput()->GetOriginTrackedDeviceInfo(action_origin, &action_origin_info, sizeof(vr::InputOriginInfo_t));
if ( (error == vr::VRInputError_None) && (vr::VRSystem()->IsTrackedDeviceConnected(action_origin_info.trackedDeviceIndex)) )
{
return true;
}
}
return false;
};
//Doesn't trigger on app start since its actions are not valid yet
m_IsAnyGlobalActionBound = false;
if (is_action_bound(m_HandleActionEnableGlobalLaserPointer))
{
m_IsAnyGlobalActionBound = true;
return;
}
for (auto handle : m_HandleActionDoGlobalShortcuts)
{
if (is_action_bound(handle))
{
m_IsAnyGlobalActionBound = true;
return;
}
}
}
void VRInput::HandleGlobalActionShortcuts(OutputManager& outmgr)
{
const ActionManager::ActionList& shortcut_actions = ConfigManager::Get().GetGlobalShortcuts();
vr::InputDigitalActionData_t data;
size_t shortcut_id = 0;
for (const auto shortcut_handle : m_HandleActionDoGlobalShortcuts)
{
vr::EVRInputError input_error = vr::VRInput()->GetDigitalActionData(shortcut_handle, &data, sizeof(data), vr::k_ulInvalidInputValueHandle);
if ((shortcut_id < shortcut_actions.size()) && (input_error == vr::VRInputError_None) && (data.bChanged))
{
if (data.bState)
{
ConfigManager::Get().GetActionManager().StartAction(shortcut_actions[shortcut_id]);
}
else
{
ConfigManager::Get().GetActionManager().StopAction(shortcut_actions[shortcut_id]);
}
}
++shortcut_id;
}
}
void VRInput::TriggerLaserPointerHaptics(vr::VRInputValueHandle_t restrict_to_device) const
{
if (restrict_to_device == m_KeyboardDeviceInputValueHandle)
return;
//There have been problems with rumble getting stuck indefinitely when calling TriggerHapticVibrationAction() on a gamepad device (even though haptics aren't even bound)
//Unsure if this an isolated issue, we're just avoid calling this function on gamepads altogether... this could be a one-liner otherwise
if (restrict_to_device == vr::k_ulInvalidInputValueHandle)
{
//All devices, but we're going to exclude the gamepad one and trigger manually for the rest
//Asking for haptic action origin doesn't seem to work, but GetLaserPointerDevicesInfo() gets every device with left click bound, which is good enough
//This function is usually not called with this value either way
std::vector lp_devices_info = GetLaserPointerDevicesInfo();
for (vr::InputOriginInfo_t device_info : lp_devices_info)
{
if (device_info.devicePath != m_GamepadDeviceInputValueHandle)
{
vr::VRInput()->TriggerHapticVibrationAction(m_HandleActionLaserPointerHaptic, 0.0f, 0.0f, 1.0f, 0.16f, device_info.devicePath);
}
}
}
else
{
//Don't trigger the vibration if this was called for the gamepad
vr::InputOriginInfo_t device_info = GetOriginTrackedDeviceInfoEx(restrict_to_device);
if (device_info.devicePath != m_GamepadDeviceInputValueHandle)
{
vr::VRInput()->TriggerHapticVibrationAction(m_HandleActionLaserPointerHaptic, 0.0f, 0.0f, 1.0f, 0.16f, restrict_to_device);
}
}
}
vr::InputOriginInfo_t VRInput::GetOriginTrackedDeviceInfoEx(vr::VRInputValueHandle_t origin) const
{
vr::InputOriginInfo_t origin_info = {0};
origin_info.trackedDeviceIndex = vr::k_unTrackedDeviceIndexInvalid;
if (origin == m_KeyboardDeviceInputValueHandle)
{
origin_info.trackedDeviceIndex = vr::k_unTrackedDeviceIndex_Hmd;
origin_info.devicePath = origin;
}
else
{
vr::VRInput()->GetOriginTrackedDeviceInfo(origin, &origin_info, sizeof(vr::InputOriginInfo_t));
}
return origin_info;
}
vr::InputDigitalActionData_t VRInput::GetEnableGlobalLaserPointerState() const
{
vr::InputDigitalActionData_t data;
vr::VRInput()->GetDigitalActionData(m_HandleActionEnableGlobalLaserPointer, &data, sizeof(data), vr::k_ulInvalidInputValueHandle);
if (ConfigManager::GetValue(configid_bool_input_laser_pointer_hmd_device))
{
data = CombineDigitalActionData(data, m_KeyboardDeviceToggleState);
}
return data;
}
std::vector VRInput::GetLaserPointerDevicesInfo() const
{
std::vector devices_info;
vr::VRInputValueHandle_t input_value_handles[vr::k_unMaxTrackedDeviceCount];
vr::EVRInputError err = vr::VRInput()->GetActionOrigins(m_HandleActionsetLaserPointer, m_HandleActionLaserPointerLeftClick, input_value_handles, vr::k_unMaxTrackedDeviceCount);
vr::InputOriginInfo_t origin_info = {0};
for (auto input_value_handle : input_value_handles)
{
if ( (input_value_handle != vr::k_ulInvalidInputValueHandle) && (vr::VRInput()->GetOriginTrackedDeviceInfo(input_value_handle, &origin_info, sizeof(vr::InputOriginInfo_t)) == vr::VRInputError_None) )
{
devices_info.push_back(origin_info);
}
}
//If GetActionOrigins() did not return anything useful, try at least getting origins for left and right hand controllers
if (devices_info.empty())
{
for (int controller_role = vr::TrackedControllerRole_LeftHand; controller_role <= vr::TrackedControllerRole_RightHand; ++controller_role)
{
origin_info = {0};
origin_info.trackedDeviceIndex = vr::VRSystem()->GetTrackedDeviceIndexForControllerRole((vr::ETrackedControllerRole)controller_role);
if (origin_info.trackedDeviceIndex != vr::k_unTrackedDeviceIndexInvalid)
{
vr::VRInputValueHandle_t input_value = vr::k_ulInvalidInputValueHandle;
vr::VRInput()->GetInputSourceHandle((controller_role == vr::TrackedControllerRole_LeftHand) ? "/user/hand/left" : "/user/hand/right", &origin_info.devicePath);
devices_info.push_back(origin_info);
}
}
}
if (ConfigManager::GetValue(configid_bool_input_laser_pointer_hmd_device))
{
vr::InputOriginInfo_t origin_info = {0};
origin_info.trackedDeviceIndex = vr::k_unTrackedDeviceIndex_Hmd; //Simulated Keyboard device is used for HMD interaction only so we use that
origin_info.devicePath = m_KeyboardDeviceInputValueHandle;
devices_info.push_back(origin_info);
}
return devices_info;
}
vr::InputDigitalActionData_t VRInput::GetLaserPointerLeftClickState(vr::VRInputValueHandle_t restrict_to_device) const
{
vr::InputDigitalActionData_t data = {0};
vr::VRInput()->GetDigitalActionData(m_HandleActionLaserPointerLeftClick, &data, sizeof(data), restrict_to_device);
if (ConfigManager::GetValue(configid_bool_input_laser_pointer_hmd_device))
{
if ((restrict_to_device == vr::k_ulInvalidInputValueHandle) || (restrict_to_device == m_KeyboardDeviceInputValueHandle))
{
data = CombineDigitalActionData(data, m_KeyboardDeviceClickState[0]);
}
}
return data;
}
std::array VRInput::GetLaserPointerClickState(vr::VRInputValueHandle_t restrict_to_device) const
{
std::array data = {0};
vr::VRInput()->GetDigitalActionData(m_HandleActionLaserPointerLeftClick, &data[0], sizeof(vr::InputDigitalActionData_t), restrict_to_device);
vr::VRInput()->GetDigitalActionData(m_HandleActionLaserPointerRightClick, &data[1], sizeof(vr::InputDigitalActionData_t), restrict_to_device);
vr::VRInput()->GetDigitalActionData(m_HandleActionLaserPointerMiddleClick, &data[2], sizeof(vr::InputDigitalActionData_t), restrict_to_device);
vr::VRInput()->GetDigitalActionData(m_HandleActionLaserPointerAux01Click, &data[3], sizeof(vr::InputDigitalActionData_t), restrict_to_device);
vr::VRInput()->GetDigitalActionData(m_HandleActionLaserPointerAux02Click, &data[4], sizeof(vr::InputDigitalActionData_t), restrict_to_device);
if (ConfigManager::GetValue(configid_bool_input_laser_pointer_hmd_device))
{
if ((restrict_to_device == vr::k_ulInvalidInputValueHandle) || (restrict_to_device == m_KeyboardDeviceInputValueHandle))
{
for (int i = 0; i < data.size(); ++i)
{
data[i] = CombineDigitalActionData(data[i], m_KeyboardDeviceClickState[i]);
}
}
}
return data;
}
vr::InputAnalogActionData_t VRInput::GetLaserPointerScrollDiscreteState() const
{
vr::InputAnalogActionData_t data = {0};
vr::VRInput()->GetAnalogActionData(m_HandleActionLaserPointerScrollDiscrete, &data, sizeof(vr::InputAnalogActionData_t), vr::k_ulInvalidInputValueHandle);
return data;
}
vr::InputAnalogActionData_t VRInput::GetLaserPointerScrollSmoothState() const
{
vr::InputAnalogActionData_t data = {0};
vr::VRInput()->GetAnalogActionData(m_HandleActionLaserPointerScrollSmooth, &data, sizeof(vr::InputAnalogActionData_t), vr::k_ulInvalidInputValueHandle);
return data;
}
vr::InputDigitalActionData_t VRInput::GetLaserPointerDragState(vr::VRInputValueHandle_t restrict_to_device) const
{
vr::InputDigitalActionData_t data = {0};
vr::VRInput()->GetDigitalActionData(m_HandleActionLaserPointerDrag, &data, sizeof(data), restrict_to_device);
if (ConfigManager::GetValue(configid_bool_input_laser_pointer_hmd_device))
{
if ((restrict_to_device == vr::k_ulInvalidInputValueHandle) || (restrict_to_device == m_KeyboardDeviceInputValueHandle))
{
data = CombineDigitalActionData(data, m_KeyboardDeviceDragState);
}
}
return data;
}
void VRInput::SetLaserPointerActive(bool is_active)
{
m_IsLaserPointerInputActive = is_active;
}
void VRInput::SetLaserPointerScrollMode(VRInputScrollMode scroll_mode)
{
m_LaserPointerScrollMode = scroll_mode;
}
VRInputScrollMode VRInput::GetLaserPointerScrollMode() const
{
return m_LaserPointerScrollMode;
}
bool VRInput::IsAnyGlobalActionBound() const
{
return m_IsAnyGlobalActionBound;
}
vr::VRInputValueHandle_t VRInput::GetKeyboardDeviceInputValueHandle() const
{
return m_KeyboardDeviceInputValueHandle;
}
================================================
FILE: src/DesktopPlus/VRInput.h
================================================
#ifndef _VRINPUT_H_
#define _VRINPUT_H_
#include "openvr.h"
#include
#include
class OutputManager;
//Additional VRMouseButton values to get full state auxiliary click events
//Normal implementations shouldn't have issues with these, but they're only sent to Desktop+ overlays anyways
#define VRMouseButton_DP_Aux01 0x0008
#define VRMouseButton_DP_Aux02 0x0010
enum VRInputScrollMode
{
vrinput_scroll_none,
vrinput_scroll_discrete,
vrinput_scroll_smooth,
};
//Can't be used with open dashboard, but handles global shortcuts and Desktop+ laser pointer input instead.
class VRInput
{
private:
vr::VRActionSetHandle_t m_HandleActionsetShortcuts;
vr::VRActionSetHandle_t m_HandleActionsetLaserPointer;
vr::VRActionSetHandle_t m_HandleActionsetScrollDiscrete;
vr::VRActionSetHandle_t m_HandleActionsetScrollSmooth;
vr::VRActionHandle_t m_HandleActionEnableGlobalLaserPointer;
std::vector m_HandleActionDoGlobalShortcuts;
vr::VRActionHandle_t m_HandleActionLaserPointerLeftClick;
vr::VRActionHandle_t m_HandleActionLaserPointerRightClick;
vr::VRActionHandle_t m_HandleActionLaserPointerMiddleClick;
vr::VRActionHandle_t m_HandleActionLaserPointerAux01Click;
vr::VRActionHandle_t m_HandleActionLaserPointerAux02Click;
vr::VRActionHandle_t m_HandleActionLaserPointerDrag;
vr::VRActionHandle_t m_HandleActionLaserPointerScrollDiscrete;
vr::VRActionHandle_t m_HandleActionLaserPointerScrollSmooth;
vr::VRActionHandle_t m_HandleActionLaserPointerHaptic;
bool m_IsAnyGlobalActionBound; //"Bound" meaning assigned and the device is actually active
bool m_IsAnyGlobalActionBoundStateValid;
bool m_IsLaserPointerInputActive;
VRInputScrollMode m_LaserPointerScrollMode;
vr::VRInputValueHandle_t m_KeyboardDeviceInputValueHandle;
vr::VRInputValueHandle_t m_GamepadDeviceInputValueHandle;
vr::InputDigitalActionData_t m_KeyboardDeviceToggleState;
bool m_KeyboardDeviceIsToggleKeyDown;
std::array m_KeyboardDeviceClickState;
vr::InputDigitalActionData_t m_KeyboardDeviceDragState;
void UpdateKeyboardDeviceState();
static vr::InputDigitalActionData_t CombineDigitalActionData(vr::InputDigitalActionData_t data_a, vr::InputDigitalActionData_t data_b);
public:
VRInput();
bool Init();
void Update();
void RefreshAnyGlobalActionBound();
void HandleGlobalActionShortcuts(OutputManager& outmgr);
void TriggerLaserPointerHaptics(vr::VRInputValueHandle_t restrict_to_device = vr::k_ulInvalidInputValueHandle) const;
vr::InputOriginInfo_t GetOriginTrackedDeviceInfoEx(vr::VRInputValueHandle_t origin) const; //Wraps GetOriginTrackedDeviceInfo() with keyboard device support
vr::InputDigitalActionData_t GetEnableGlobalLaserPointerState() const;
std::vector GetLaserPointerDevicesInfo() const;
vr::InputDigitalActionData_t GetLaserPointerLeftClickState(vr::VRInputValueHandle_t restrict_to_device = vr::k_ulInvalidInputValueHandle) const;
std::array GetLaserPointerClickState(vr::VRInputValueHandle_t restrict_to_device = vr::k_ulInvalidInputValueHandle) const;
vr::InputAnalogActionData_t GetLaserPointerScrollDiscreteState() const;
vr::InputAnalogActionData_t GetLaserPointerScrollSmoothState() const;
vr::InputDigitalActionData_t GetLaserPointerDragState(vr::VRInputValueHandle_t restrict_to_device = vr::k_ulInvalidInputValueHandle) const;
void SetLaserPointerActive(bool is_active);
void SetLaserPointerScrollMode(VRInputScrollMode scroll_mode);
VRInputScrollMode GetLaserPointerScrollMode() const;
bool IsAnyGlobalActionBound() const;
vr::VRInputValueHandle_t GetKeyboardDeviceInputValueHandle() const;
};
#endif
================================================
FILE: src/DesktopPlus/VertexShader.hlsl
================================================
struct VS_INPUT
{
float4 Pos : POSITION;
float2 Tex : TEXCOORD;
};
struct VS_OUTPUT
{
float4 Pos : SV_POSITION;
float2 Tex : TEXCOORD;
};
//--------------------------------------------------------------------------------------
// Vertex Shader
//--------------------------------------------------------------------------------------
VS_OUTPUT VS(VS_INPUT input)
{
return input;
}
================================================
FILE: src/DesktopPlus/resource.h
================================================
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by DesktopPlus.rc
//
#define IDI_ICON1 101
#define IDI_DPLUS 101
// Next default values for new objects
//
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 102
#define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001
#define _APS_NEXT_SYMED_VALUE 101
#endif
#endif
================================================
FILE: src/DesktopPlus.sln
================================================
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29519.87
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DesktopPlus", "DesktopPlus\DesktopPlus.vcxproj", "{05050918-71E9-AF87-0B3C-6F34D471A55A}"
ProjectSection(ProjectDependencies) = postProject
{045FFB0E-D0D4-404D-8C33-13C7074B3236} = {045FFB0E-D0D4-404D-8C33-13C7074B3236}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DesktopPlusUI", "DesktopPlusUI\DesktopPlusUI.vcxproj", "{14405CEC-DF3D-435E-8F11-B79BAC56D7D8}"
ProjectSection(ProjectDependencies) = postProject
{045FFB0E-D0D4-404D-8C33-13C7074B3236} = {045FFB0E-D0D4-404D-8C33-13C7074B3236}
EndProjectSection
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DesktopPlusWinRT", "DesktopPlusWinRT\DesktopPlusWinRT.vcxproj", "{045FFB0E-D0D4-404D-8C33-13C7074B3236}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{05050918-71E9-AF87-0B3C-6F34D471A55A}.Debug|x64.ActiveCfg = Debug|x64
{05050918-71E9-AF87-0B3C-6F34D471A55A}.Debug|x64.Build.0 = Debug|x64
{05050918-71E9-AF87-0B3C-6F34D471A55A}.Release|x64.ActiveCfg = Release|x64
{05050918-71E9-AF87-0B3C-6F34D471A55A}.Release|x64.Build.0 = Release|x64
{14405CEC-DF3D-435E-8F11-B79BAC56D7D8}.Debug|x64.ActiveCfg = Debug|x64
{14405CEC-DF3D-435E-8F11-B79BAC56D7D8}.Debug|x64.Build.0 = Debug|x64
{14405CEC-DF3D-435E-8F11-B79BAC56D7D8}.Release|x64.ActiveCfg = Release|x64
{14405CEC-DF3D-435E-8F11-B79BAC56D7D8}.Release|x64.Build.0 = Release|x64
{045FFB0E-D0D4-404D-8C33-13C7074B3236}.Debug|x64.ActiveCfg = Debug|x64
{045FFB0E-D0D4-404D-8C33-13C7074B3236}.Debug|x64.Build.0 = Debug|x64
{045FFB0E-D0D4-404D-8C33-13C7074B3236}.Release|x64.ActiveCfg = Release|x64
{045FFB0E-D0D4-404D-8C33-13C7074B3236}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6AC6EA93-8452-42F0-A7BF-B88BDB800DB6}
EndGlobalSection
EndGlobal
================================================
FILE: src/DesktopPlusUI/AuxUI.cpp
================================================
#include "AuxUI.h"
#include "UIManager.h"
#include "InterprocessMessaging.h"
#include "WindowManager.h"
#include "OpenVRExt.h"
AuxUIWindow::AuxUIWindow(AuxUIID ui_id) : m_AuxUIID(ui_id), m_Visible(false), m_Alpha(0.0f), m_IsTransitionFading(false), m_AutoSizeFrames(-1)
{
//Nothing
}
bool AuxUIWindow::WindowUpdateBase()
{
AuxUI& aux_ui = UIManager::Get()->GetAuxUI();
if (m_Visible)
{
if (aux_ui.GetActiveUI() != m_AuxUIID)
{
Hide();
return true;
}
//Wait for previous fade out to be done before continuing
if (aux_ui.IsUIFadingOut())
return false;
if ( (m_Alpha == 0.0f) && (m_AutoSizeFrames == -1) )
{
m_AutoSizeFrames = 2;
ApplyPendingValues();
}
float alpha_prev = m_Alpha;
//Alpha fade animation
if (m_AutoSizeFrames <= 0)
{
m_AutoSizeFrames = -1;
m_Alpha += ImGui::GetIO().DeltaTime * 7.5f;
if (m_Alpha > 1.0f)
m_Alpha = 1.0f;
}
//Set overlay alpha when not in desktop mode
if ( (!UIManager::Get()->IsInDesktopMode()) && (alpha_prev != m_Alpha) )
{
vr::VROverlay()->SetOverlayAlpha(UIManager::Get()->GetOverlayHandleAuxUI(), m_Alpha);
}
}
else if (m_Alpha != 0.0f)
{
float alpha_prev = m_Alpha;
//Alpha fade animation
m_Alpha -= ImGui::GetIO().DeltaTime * 7.5f;
if (m_Alpha < 0.0f)
m_Alpha = 0.0f;
//Set overlay alpha when not in desktop mode
if ( (!UIManager::Get()->IsInDesktopMode()) && (alpha_prev != m_Alpha) )
{
vr::VROverlay()->SetOverlayAlpha(UIManager::Get()->GetOverlayHandleAuxUI(), m_Alpha);
}
if (m_Alpha == 0.0f)
{
if (m_IsTransitionFading)
{
m_IsTransitionFading = false;
Show();
}
else
{
aux_ui.SetFadeOutFinished();
aux_ui.ClearActiveUI(m_AuxUIID);
}
if (!UIManager::Get()->IsInDesktopMode())
vr::VROverlay()->HideOverlay(UIManager::Get()->GetOverlayHandleAuxUI());
}
}
else
{
return false;
}
return true;
}
void AuxUIWindow::DrawFullDimmedRectBehindWindow()
{
//Based on ImGui::RenderDimmedBackgrounds()
ImDrawList* draw_list = ImGui::GetWindowDrawList();
if (draw_list->CmdBuffer.Size == 0)
draw_list->AddDrawCmd();
draw_list->PushClipRect({0.0f, 0.0f}, ImGui::GetIO().DisplaySize, false); //ImGui FIXME: Need to stricty ensure ImDrawCmd are not merged (ElemCount==6 checks below will verify that)
draw_list->AddRectFilled({0.0f, 0.0f}, ImGui::GetIO().DisplaySize, ImGui::ColorConvertFloat4ToU32({0.0f, 0.0f, 0.0f, m_Alpha * 0.5f}));
ImDrawCmd cmd = draw_list->CmdBuffer.back();
//IM_ASSERT(cmd.ElemCount == 6); //This seems to block in the way we use this function, but also appears fine without for now?
draw_list->CmdBuffer.pop_back();
draw_list->CmdBuffer.push_front(cmd);
draw_list->AddDrawCmd(); //ImGui: We need to create a command as CmdBuffer.back().IdxOffset won't be correct if we append to same command.
draw_list->PopClipRect();
}
void AuxUIWindow::SetUpTextureBounds()
{
//Set overlay texture bounds based on m_Pos and m_Size (with padding), clamped to available texture space
vr::VRTextureBounds_t bounds = {};
const DPRect& rect_total = UITextureSpaces::Get().GetRect(ui_texspace_total);
float tex_width = (float)rect_total.GetWidth();
float tex_height = (float)rect_total.GetHeight();
const DPRect& rect_aux_ui = UITextureSpaces::Get().GetRect(ui_texspace_aux_ui);
ImVec2 pos_ovrl = {m_Pos.x - rect_aux_ui.GetTL().x, m_Pos.y - rect_aux_ui.GetTL().y};
bounds.uMin = clamp(int(m_Pos.x - 2), rect_aux_ui.GetTL().x, rect_aux_ui.GetBR().x) / tex_width;
bounds.vMin = clamp(int(m_Pos.y - 2), rect_aux_ui.GetTL().y, rect_aux_ui.GetBR().y) / tex_height;
bounds.uMax = clamp(int(m_Pos.x + m_Size.x + 2), rect_aux_ui.GetTL().x, rect_aux_ui.GetBR().x) / tex_width;
bounds.vMax = clamp(int(m_Pos.y + m_Size.y + 2), rect_aux_ui.GetTL().y, rect_aux_ui.GetBR().y) / tex_height;
vr::VROverlay()->SetOverlayTextureBounds(UIManager::Get()->GetOverlayHandleAuxUI(), &bounds);
}
void AuxUIWindow::StartTransitionFade()
{
if (m_Alpha != 0.0f)
{
m_Visible = false;
m_IsTransitionFading = true;
}
else //Just skip transition if the window is already invisible
{
m_Visible = true;
}
}
void AuxUIWindow::ApplyPendingValues()
{
//Does nothing by default
}
bool AuxUIWindow::Show()
{
if (m_IsTransitionFading)
return true;
m_Visible = UIManager::Get()->GetAuxUI().SetActiveUI(m_AuxUIID);
UIManager::Get()->GetIdleState().AddActiveTime();
return m_Visible;
}
void AuxUIWindow::Hide()
{
m_Visible = false;
m_IsTransitionFading = false;
//Clear right away if this window never had the chance to fade in
if (m_Alpha == 0.0f)
{
AuxUI& aux_ui = UIManager::Get()->GetAuxUI();
aux_ui.ClearActiveUI(m_AuxUIID);
aux_ui.SetFadeOutFinished();
}
UIManager::Get()->GetIdleState().AddActiveTime();
}
bool AuxUIWindow::IsVisible()
{
return m_Visible;
}
//--WindowDragHint
WindowDragHint::WindowDragHint() : AuxUIWindow(auxui_drag_hint), m_TargetDevice(vr::k_unTrackedDeviceIndexInvalid), m_HintType(hint_docking), m_HintTypePending(hint_docking)
{
//Leave 2 pixel padding around so interpolation doesn't cut off the pixel border
const DPRect& rect = UITextureSpaces::Get().GetRect(ui_texspace_aux_ui);
m_Pos = {float(rect.GetTL().x + 2), float(rect.GetTL().y + 2)};
m_Size = {-1.0f, -1.0f};
}
void WindowDragHint::SetUpOverlay()
{
vr::VROverlayHandle_t overlay_handle = UIManager::Get()->GetOverlayHandleAuxUI();
const float overlay_width = OVERLAY_WIDTH_METERS_AUXUI_DRAG_HINT * (m_Size.x / 200.0f); //Scale width based on window width for consistent sizing
vr::VROverlay()->SetOverlayWidthInMeters(overlay_handle, overlay_width);
vr::VROverlay()->SetOverlaySortOrder(overlay_handle, 100);
vr::VROverlay()->SetOverlayAlpha(overlay_handle, 0.0f);
vr::VROverlay()->SetOverlayInputMethod(overlay_handle, vr::VROverlayInputMethod_None);
SetUpTextureBounds();
vr::VROverlay()->ShowOverlay(overlay_handle);
}
void WindowDragHint::UpdateOverlayPos()
{
//Check for never tracked device and show for HMD instead then
const bool is_device_never_tracked = vr::VRSystem()->GetBoolTrackedDeviceProperty(m_TargetDevice, vr::Prop_NeverTracked_Bool);
vr::TrackedDeviceIndex_t device_index = (is_device_never_tracked) ? vr::k_unTrackedDeviceIndex_Hmd : m_TargetDevice;
Matrix4 mat;
if (device_index == vr::k_unTrackedDeviceIndex_Hmd) //Show in front of HMD
{
mat.scale(2.0f); //Scale on the transform itself for simplicity
mat.translate_relative(0.0f, 0.0f, -0.5f); //Means this is -1m though
}
else //Show on controller
{
//Initial offset (somewhat centered inside controller model, for Index at least)
mat.translate_relative(0.0f, 0.0f, 0.10f);
Vector3 pos = mat.getTranslation();
//Get poses
vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount];
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, vr::IVRSystemEx::GetTimeNowToPhotons(), poses, vr::k_unMaxTrackedDeviceCount);
if ( (poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid) && (device_index < vr::k_unMaxTrackedDeviceCount) && (poses[device_index].bPoseIsValid) )
{
//Rotate towards HMD position
Matrix4 mat_hmd(poses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking);
Matrix4 mat_controller(poses[device_index].mDeviceToAbsoluteTracking);
mat = mat_controller;
vr::IVRSystemEx::TransformLookAt(mat, mat_hmd.getTranslation());
//Apply rotation difference between controller used as origin and lookat angle
mat.setTranslation({0.0f, 0.0f, 0.0f});
mat_controller.setTranslation({0.0f, 0.0f, 0.0f});
mat_controller.invert();
mat = mat_controller * mat;
//Restore position
mat.setTranslation(pos);
}
//Additional offset (move away from controller to avoid clipping, again value fits mostly for Index)
mat.translate_relative(0.0f, 0.0f, 0.10f);
//At least on the controller we need to keep active time to keep rotation updated
UIManager::Get()->GetIdleState().AddActiveTime();
}
//Set transform
vr::HmdMatrix34_t mat_ovr = mat.toOpenVR34();
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(UIManager::Get()->GetOverlayHandleAuxUI(), device_index, &mat_ovr);
}
void WindowDragHint::ApplyPendingValues()
{
m_HintType = m_HintTypePending;
}
void WindowDragHint::Update()
{
bool render_window = WindowUpdateBase() || m_Size.x == -1.0f;
if (!render_window)
return;
const bool desktop_mode = UIManager::Get()->IsInDesktopMode();
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize;
if (!m_Visible)
flags |= ImGuiWindowFlags_NoInputs;
//Center on screen in desktop mode
if (desktop_mode)
{
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, m_Alpha);
ImGui::SetNextWindowPos({ImGui::GetIO().DisplaySize.x / 2.0f, ImGui::GetIO().DisplaySize.y / 2.0f}, ImGuiCond_Always, {0.5f, 0.5f});
}
else
{
ImGui::SetNextWindowPos(m_Pos, ImGuiCond_Always);
}
ImGui::Begin("WindowDragHint", nullptr, flags);
switch (m_HintType)
{
case WindowDragHint::hint_docking: ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIDragHintDocking)); break;
case WindowDragHint::hint_undocking: ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIDragHintUndocking)); break;
case WindowDragHint::hint_ovrl_locked: ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIDragHintOvrlLocked)); break;
case WindowDragHint::hint_ovrl_theater_screen_blocked: ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIDragHintOvrlTheaterScreenBlocked)); break;
}
if (desktop_mode)
DrawFullDimmedRectBehindWindow();
m_Size = ImGui::GetWindowSize();
ImGui::End();
if (desktop_mode)
ImGui::PopStyleVar();
if ((!desktop_mode) && (!m_IsTransitionFading))
{
UpdateOverlayPos();
}
if (m_AutoSizeFrames > 0)
{
m_AutoSizeFrames--;
if ((m_AutoSizeFrames == 0) && (!desktop_mode))
{
SetUpOverlay();
}
}
}
void WindowDragHint::SetHintType(vr::TrackedDeviceIndex_t device_index, WindowDragHint::HintType hint_type)
{
if ((m_TargetDevice == device_index) && (m_HintType == hint_type))
return;
m_TargetDevice = device_index;
m_HintTypePending = hint_type; //Actual value set after transition
if (m_Visible)
StartTransitionFade();
}
//--WindowGazeFadeAutoHint
WindowGazeFadeAutoHint::WindowGazeFadeAutoHint() : AuxUIWindow(auxui_gazefade_auto_hint), m_TargetOverlay(k_ulOverlayID_None), m_Countdown(3), m_TickTime(0.0)
{
//Leave 2 pixel padding around so interpolation doesn't cut off the pixel border
const DPRect& rect = UITextureSpaces::Get().GetRect(ui_texspace_aux_ui);
m_Pos = {float(rect.GetTL().x + 2), float(rect.GetTL().y + 2)};
m_Size = {-1.0f, -1.0f};
}
void WindowGazeFadeAutoHint::SetUpOverlay()
{
vr::VROverlayHandle_t overlay_handle = UIManager::Get()->GetOverlayHandleAuxUI();
vr::VROverlay()->SetOverlayWidthInMeters(overlay_handle, OVERLAY_WIDTH_METERS_AUXUI_GAZEFADE_AUTO_HINT);
vr::VROverlay()->SetOverlaySortOrder(overlay_handle, 100);
vr::VROverlay()->SetOverlayAlpha(overlay_handle, 0.0f);
vr::VROverlay()->SetOverlayInputMethod(overlay_handle, vr::VROverlayInputMethod_None);
SetUpTextureBounds();
vr::VROverlay()->ShowOverlay(overlay_handle);
}
void WindowGazeFadeAutoHint::UpdateOverlayPos()
{
//Initial offset
Matrix4 mat;
mat.translate_relative(0.0f, 0.0f, -1.00f);
//Set transform
vr::HmdMatrix34_t mat_ovr = mat.toOpenVR34();
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(UIManager::Get()->GetOverlayHandleAuxUI(), vr::k_unTrackedDeviceIndex_Hmd, &mat_ovr);
}
bool WindowGazeFadeAutoHint::Show()
{
m_Countdown = 3;
m_TickTime = 0.0; //Triggers label update right away on Update()
UIManager::Get()->GetIdleState().AddActiveTime(5000);
//Deactivate GazeFade during the countdown so the overlay is visible to the user
IPCManager::Get().PostConfigMessageToDashboardApp(configid_int_state_overlay_current_id_override, (int)m_TargetOverlay);
IPCManager::Get().PostConfigMessageToDashboardApp(configid_bool_overlay_gazefade_enabled, false);
IPCManager::Get().PostConfigMessageToDashboardApp(configid_int_state_overlay_current_id_override, -1);
return AuxUIWindow::Show();
}
void WindowGazeFadeAutoHint::Hide()
{
//Trigger GazeFade auto-configure
IPCManager::Get().PostConfigMessageToDashboardApp(configid_int_state_overlay_current_id_override, (int)m_TargetOverlay);
IPCManager::Get().PostMessageToDashboardApp(ipcmsg_action, ipcact_overlay_gaze_fade_auto);
IPCManager::Get().PostConfigMessageToDashboardApp(configid_bool_overlay_gazefade_enabled, true);
IPCManager::Get().PostConfigMessageToDashboardApp(configid_int_state_overlay_current_id_override, -1);
//Enable gaze fade if it isn't yet
OverlayManager::Get().GetConfigData(m_TargetOverlay).ConfigBool[configid_bool_overlay_gazefade_enabled] = true;
AuxUIWindow::Hide();
}
void WindowGazeFadeAutoHint::Update()
{
bool render_window = WindowUpdateBase() || m_Size.x == -1.0f;
if (!render_window)
return;
if (m_TickTime + 1.0 < ImGui::GetTime())
{
if (m_Countdown == 0)
{
//Hide and keep the old label during fade-out
Hide();
}
else //Update label
{
m_Label = TranslationManager::GetString((m_Countdown != 1) ? tstr_AuxUIGazeFadeAutoHint : tstr_AuxUIGazeFadeAutoHintSingular);
StringReplaceAll(m_Label, "%SECONDS%", std::to_string(m_Countdown));
m_Countdown--;
m_TickTime = ImGui::GetTime();
}
}
const bool desktop_mode = UIManager::Get()->IsInDesktopMode();
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize;
if (!m_Visible)
flags |= ImGuiWindowFlags_NoInputs;
//Center on screen in desktop mode
if (desktop_mode)
{
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, m_Alpha);
ImGui::SetNextWindowPos({ImGui::GetIO().DisplaySize.x / 2.0f, ImGui::GetIO().DisplaySize.y / 2.0f}, ImGuiCond_Always, {0.5f, 0.5f});
}
else
{
ImGui::SetNextWindowPos(m_Pos, ImGuiCond_Always);
}
ImGui::Begin("WindowGazeFadeAutoHint", nullptr, flags);
ImGui::TextUnformatted(m_Label.c_str());
if (desktop_mode)
DrawFullDimmedRectBehindWindow();
m_Size = ImGui::GetWindowSize();
ImGui::End();
if (desktop_mode)
ImGui::PopStyleVar();
if ((!desktop_mode) && (!m_IsTransitionFading))
{
UpdateOverlayPos();
}
if (m_AutoSizeFrames > 0)
{
m_AutoSizeFrames--;
if ((m_AutoSizeFrames == 0) && (!desktop_mode))
{
SetUpOverlay();
}
}
}
void WindowGazeFadeAutoHint::SetTargetOverlay(unsigned int overlay_id)
{
m_TargetOverlay = overlay_id;
}
//--WindowQuickStart
WindowQuickStart::WindowQuickStart() : AuxUIWindow(auxui_window_quickstart), m_CurrentPage(0)
{
//Leave 2 pixel padding around so interpolation doesn't cut off the pixel border
const DPRect& rect = UITextureSpaces::Get().GetRect(ui_texspace_aux_ui);
m_Pos = {float(rect.GetTL().x + 2), float(rect.GetTL().y + 2)};
m_Size = {-1.0f, -1.0f};
m_Transform.zero();
}
void WindowQuickStart::SetUpOverlay()
{
vr::VROverlayHandle_t overlay_handle = UIManager::Get()->GetOverlayHandleAuxUI();
vr::VROverlay()->SetOverlayWidthInMeters(overlay_handle, OVERLAY_WIDTH_METERS_AUXUI_WINDOW_QUICKSTART);
vr::VROverlay()->SetOverlaySortOrder(overlay_handle, 2);
vr::VROverlay()->SetOverlayAlpha(overlay_handle, 0.0f);
vr::VROverlay()->SetOverlayInputMethod(overlay_handle, vr::VROverlayInputMethod_Mouse);
SetUpTextureBounds();
UpdateOverlayPos();
vr::VROverlay()->ShowOverlay(overlay_handle);
}
void WindowQuickStart::UpdateOverlayPos()
{
const float auxui_ovrl_width = OVERLAY_WIDTH_METERS_AUXUI_WINDOW_QUICKSTART;
const float auxui_ovrl_height = OVERLAY_WIDTH_METERS_AUXUI_WINDOW_QUICKSTART * (m_Size.y / m_Size.x);
//Set initial transform to zoom in from the first time this is called while visible
if ((m_Visible) && (m_Transform.isZero()))
{
m_Transform = vr::IVRSystemEx::ComputeHMDFacingTransform(1.0);
Vector3 pos = m_Transform.getTranslation();
m_Transform.setTranslation({0.0f, 0.0f, 0.0f});
m_Transform.scale(0.0f);
m_Transform.setTranslation(pos);
return;
}
//Set target transform based on page (may update while animating)
switch (m_CurrentPage)
{
case pageid_Welcome:
{
m_TransformAnimationEnd = vr::IVRSystemEx::ComputeHMDFacingTransform(1.0f);
break;
}
case pageid_Overlays:
{
//Aim for top right side of overlay bar (doesn't obscure the overlay menu mentioned on the page)
const auto& window = UIManager::Get()->GetOverlayBarWindow();
const DPRect& rect_tex = UITextureSpaces::Get().GetRect(ui_texspace_overlay_bar);
//Default to top left of window relative to texspace (note that AuxUI overlay is centered on that spot)
Vector2 point_2d(window.GetPos().x - rect_tex.GetTL().x, window.GetPos().y - rect_tex.GetTL().y);
point_2d.x += window.GetSize().x;
m_TransformAnimationEnd = UIManager::Get()->GetOverlay2DPointTransform(point_2d, UIManager::Get()->GetOverlayHandleOverlayBar());
m_TransformAnimationEnd.translate_relative(0.0f, auxui_ovrl_height / 2.0f, 0.0f);
Vector3 pos = m_TransformAnimationEnd.getTranslation();
m_TransformAnimationEnd.setTranslation({0.0f, 0.0f, 0.0f});
m_TransformAnimationEnd.rotateY(-10.0f);
m_TransformAnimationEnd.setTranslation(pos);
m_TransformAnimationEnd.translate_relative(0.0f, 0.0f, 0.05f);
break;
}
case pageid_Overlays_2:
{
//Aim for top left side of overlay bar (doesn't obscure the add overlay menu mentioned on the page)
const auto& window = UIManager::Get()->GetOverlayBarWindow();
const DPRect& rect_tex = UITextureSpaces::Get().GetRect(ui_texspace_overlay_bar);
//Default to top left of window relative to texspace (note that AuxUI overlay is centered on that spot)
Vector2 point_2d(window.GetPos().x - rect_tex.GetTL().x, window.GetPos().y - rect_tex.GetTL().y);
m_TransformAnimationEnd = UIManager::Get()->GetOverlay2DPointTransform(point_2d, UIManager::Get()->GetOverlayHandleOverlayBar());
m_TransformAnimationEnd.translate_relative(0.0f, auxui_ovrl_height / 2.0f, 0.0f);
Vector3 pos = m_TransformAnimationEnd.getTranslation();
m_TransformAnimationEnd.setTranslation({0.0f, 0.0f, 0.0f});
m_TransformAnimationEnd.rotateY(12.5f);
m_TransformAnimationEnd.setTranslation(pos);
m_TransformAnimationEnd.translate_relative(0.0f, 0.0f, 0.05f);
break;
}
case pageid_OverlayProperties:
case pageid_OverlayProperties_2:
{
const auto& window = UIManager::Get()->GetOverlayPropertiesWindow();
const DPRect& rect_tex = UITextureSpaces::Get().GetRect(ui_texspace_overlay_properties);
//Top left of window relative to texspace
Vector2 point_2d(window.GetPos().x - rect_tex.GetTL().x, window.GetPos().y - rect_tex.GetTL().y);
point_2d.y += window.GetSize().y;
m_TransformAnimationEnd = UIManager::Get()->GetOverlay2DPointTransform(point_2d, window.GetOverlayHandle());
m_TransformAnimationEnd.translate_relative(auxui_ovrl_width * 1.52f, auxui_ovrl_height / 4.0f, 0.05f);
Vector3 pos = m_TransformAnimationEnd.getTranslation();
m_TransformAnimationEnd.setTranslation({0.0f, 0.0f, 0.0f});
m_TransformAnimationEnd.rotateY(-25.0f);
m_TransformAnimationEnd.setTranslation(pos);
m_TransformAnimationEnd.translate_relative(0.0f, 0.0f, 0.125f);
break;
}
case pageid_Settings:
case pageid_Profiles:
case pageid_Actions:
case pageid_Actions_2:
case pageid_OverlayTags:
case pageid_Settings_End:
{
const auto& window = UIManager::Get()->GetSettingsWindow();
const DPRect& rect_tex = UITextureSpaces::Get().GetRect(ui_texspace_settings);
//Top left of window relative to texspace
Vector2 point_2d(window.GetPos().x - rect_tex.GetTL().x, window.GetPos().y - rect_tex.GetTL().y);
point_2d.y += window.GetSize().y;
m_TransformAnimationEnd = UIManager::Get()->GetOverlay2DPointTransform(point_2d, UIManager::Get()->GetOverlayHandleSettings());
m_TransformAnimationEnd.translate_relative(auxui_ovrl_width * -1.02f, auxui_ovrl_height / 4.0f, 0.05f);
Vector3 pos = m_TransformAnimationEnd.getTranslation();
m_TransformAnimationEnd.setTranslation({0.0f, 0.0f, 0.0f});
m_TransformAnimationEnd.rotateY(25.0f);
m_TransformAnimationEnd.setTranslation(pos);
m_TransformAnimationEnd.translate_relative(0.0f, 0.0f, 0.125f);
break;
}
case pageid_FloatingUI:
{
const auto& window = UIManager::Get()->GetFloatingUI().GetActionBarWindow();
const DPRect& rect_tex = UITextureSpaces::Get().GetRect(ui_texspace_floating_ui);
//Top left of window relative to texspace
Vector2 point_2d(window.GetPos().x - rect_tex.GetTL().x, window.GetPos().y - rect_tex.GetTL().y);
point_2d.x += window.GetSize().x / 2.0f;
m_TransformAnimationEnd = UIManager::Get()->GetOverlay2DPointTransform(point_2d, UIManager::Get()->GetOverlayHandleFloatingUI());
m_TransformAnimationEnd.translate_relative(0.0f, (auxui_ovrl_height / 2.0f) + 0.05f, 0.0f);
m_TransformAnimationEnd.translate_relative(0.0f, 0.0f, 0.05f);
break;
}
case pageid_DesktopMode:
case pageid_ReadMe:
{
m_TransformAnimationEnd = vr::IVRSystemEx::ComputeHMDFacingTransform(1.0f);
break;
}
}
//Set start point if this animation is just starting
if (m_TransformAnimationProgress == 0.0f)
{
m_TransformAnimationStart = m_Transform;
}
//Smoothstep over each matrix component and set the result
float mat_array[16] = {};
for (int i = 0; i < 16; ++i)
{
mat_array[i] = smoothstep(m_TransformAnimationProgress, m_TransformAnimationStart.get()[i], m_TransformAnimationEnd.get()[i]);
}
m_Transform.set(mat_array);
//Progress animation step
const float time_step = ImGui::GetIO().DeltaTime * 3.0f;
m_TransformAnimationProgress += time_step;
if (m_TransformAnimationProgress > 1.0f)
{
m_TransformAnimationProgress = 1.0f;
}
//Set transform
vr::HmdMatrix34_t mat_ovr = m_Transform.toOpenVR34();
vr::VROverlay()->SetOverlayTransformAbsolute(UIManager::Get()->GetOverlayHandleAuxUI(), vr::TrackingUniverseStanding, &mat_ovr);
}
void WindowQuickStart::OnPageChange(int page_id)
{
//Set window visibility and scrolls as they should be for the page
WindowSettings& window_settings = UIManager::Get()->GetSettingsWindow();
WindowOverlayProperties& window_overlay_properties = UIManager::Get()->GetOverlayPropertiesWindow();
switch (page_id)
{
case pageid_Welcome:
case pageid_Overlays:
case pageid_Overlays_2:
{
window_settings.Hide();
window_overlay_properties.Hide();
break;
}
case pageid_OverlayProperties:
case pageid_OverlayProperties_2:
{
window_settings.Hide();
window_overlay_properties.SetActiveOverlayID(0);
window_overlay_properties.Show();
break;
}
case pageid_Settings:
case pageid_Profiles:
{
window_settings.Show();
window_settings.QuickStartGuideGoToPage(wndsettings_page_main);
window_overlay_properties.Hide();
break;
}
case pageid_Actions:
{
window_settings.Show();
window_settings.QuickStartGuideGoToPage(wndsettings_page_actions);
window_overlay_properties.Hide();
break;
}
case pageid_Actions_2:
case pageid_OverlayTags:
{
window_settings.Show();
window_settings.QuickStartGuideGoToPage(wndsettings_page_actions_edit);
window_overlay_properties.Hide();
break;
}
case pageid_Settings_End:
{
window_settings.Show();
window_settings.QuickStartGuideGoToPage(wndsettings_page_main);
window_overlay_properties.Hide();
break;
}
case pageid_FloatingUI:
case pageid_DesktopMode:
case pageid_ReadMe:
{
window_settings.Hide();
window_overlay_properties.Hide();
break;
}
}
m_TransformAnimationProgress = 0.0f;
m_TimeoutTickStart = 0;
}
bool WindowQuickStart::Show()
{
return AuxUIWindow::Show();
}
void WindowQuickStart::Hide()
{
m_TimeoutTickStart = 0;
AuxUIWindow::Hide();
}
void WindowQuickStart::Update()
{
if (!ConfigManager::GetValue(configid_bool_interface_quick_start_hidden))
{
//Temporarily hide when leaving dashboard tab
if (UIManager::Get()->IsOverlayBarOverlayVisible())
{
if (!m_Visible)
{
//We need to wait a bit or else we might get old overlay transform data when calling UpdateOverlayPos() after this
//Waiting two frames seems to be sufficient and isn't much of an issue
vr::VROverlay()->WaitFrameSync(100);
vr::VROverlay()->WaitFrameSync(100);
Show();
m_TransformAnimationProgress = 0.0f;
}
}
else if (m_Visible)
{
Hide();
}
//Hide automatically after a timeout if there doesn't seem to be any input device (using the dashboard without one is weird, but still)
if ((m_CurrentPage == 0) && (UIManager::Get()->IsOpenVRLoaded()))
{
const vr::TrackedDeviceIndex_t device_index = ConfigManager::Get().GetPrimaryLaserPointerDevice();
if ((device_index == vr::k_unTrackedDeviceIndexInvalid) || ((device_index == vr::k_unTrackedDeviceIndex_Hmd))) //HMD may or may not being able to click
{
//Don't do it if the overlay is being pointed at, however (potentially still no input possible, but user can point somewhere else then)
if (!ConfigManager::Get().IsLaserPointerTargetOverlay(UIManager::Get()->GetOverlayHandleAuxUI()))
{
const unsigned int timeout_ms = (device_index == vr::k_unTrackedDeviceIndex_Hmd) ? 20000 : 10000;
if (m_TimeoutTickStart == 0)
{
m_TimeoutTickStart = ::GetTickCount64();
}
else if (m_TimeoutTickStart + timeout_ms < ::GetTickCount64())
{
ConfigManager::SetValue(configid_bool_interface_quick_start_hidden, true);
Hide();
}
}
}
}
}
bool render_window = WindowUpdateBase() || m_Size.x == -1.0f;
if (!render_window)
return;
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar;
if (!m_Visible)
flags |= ImGuiWindowFlags_NoInputs;
ImGuiStyle& style = ImGui::GetStyle();
const DPRect& rect = UITextureSpaces::Get().GetRect(ui_texspace_aux_ui);
ImGui::SetNextWindowPos(m_Pos, ImGuiCond_Always);
ImGui::SetNextWindowSize({(float)rect.GetWidth() - 4.0f, (float)rect.GetHeight() * 0.85f});
ImGui::Begin("WindowQuickStart", nullptr, flags);
const float page_width = m_Size.x - style.WindowBorderSize - style.WindowPadding.x - style.WindowPadding.x;
//Page animation
if (m_PageAnimationDir != 0)
{
//Use the averaged framerate value instead of delta time for the first animation step
//This is to smooth over increased frame deltas that can happen when a new page needs to do initial larger computations or save/load files
const float progress_step = (m_PageAnimationProgress == 0.0f) ? (1.0f / ImGui::GetIO().Framerate) * 3.0f : ImGui::GetIO().DeltaTime * 3.0f;
m_PageAnimationProgress += progress_step;
if (m_PageAnimationProgress >= 1.0f)
{
m_PageAnimationProgress = 1.0f;
m_PageAnimationDir = 0;
m_CurrentPageAnimationMax = m_CurrentPageAnimation;
}
}
else if (m_CurrentPageAnimation != m_CurrentPage) //Only start new animation if none is running
{
m_PageAnimationDir = (m_CurrentPageAnimation < m_CurrentPage) ? -1 : 1;
m_CurrentPageAnimation = m_CurrentPage;
m_PageAnimationStartPos = m_PageAnimationOffset;
m_PageAnimationProgress = 0.0f;
m_CurrentPageAnimationMax = std::max(m_CurrentPage, m_CurrentPageAnimationMax);
}
const float target_x = (page_width + style.ItemSpacing.x) * -m_CurrentPageAnimation;
m_PageAnimationOffset = smoothstep(m_PageAnimationProgress, m_PageAnimationStartPos, target_x);
//Set up page offset and clipping
ImGui::SetCursorPosX( (ImGui::GetCursorPosX() + m_PageAnimationOffset) );
const ImVec2 child_size = {page_width, ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing() - style.ItemSpacing.y};
const int active_page_count = m_CurrentPageAnimationMax + 1;
for (int i = 0; i < active_page_count; ++i)
{
//Disable items when the page isn't active
const bool is_inactive_page = (i != m_CurrentPage);
if (is_inactive_page)
{
ImGui::PushItemDisabledNoVisual();
}
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.00f, 0.00f, 0.00f, 0.00f));
if ( (ImGui::BeginChild(ImGui::GetID((void*)(intptr_t)i), child_size, ImGuiChildFlags_NavFlattened)) )
{
ImGui::PopStyleColor(); //ImGuiCol_ChildBg
ImGui::PushTextWrapPos(ImGui::GetContentRegionAvail().x);
switch (i)
{
case pageid_Welcome:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartWelcomeHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartWelcomeBody));
break;
}
case pageid_Overlays:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartOverlaysHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartOverlaysBody));
break;
}
case pageid_Overlays_2:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartOverlaysHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartOverlaysBody2));
break;
}
case pageid_OverlayProperties:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartOverlayPropertiesHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartOverlayPropertiesBody));
break;
}
case pageid_OverlayProperties_2:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartOverlayPropertiesHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartOverlayPropertiesBody2));
break;
}
case pageid_Settings:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartSettingsHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartSettingsBody));
break;
}
case pageid_Profiles:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartProfilesHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartProfilesBody));
break;
}
case pageid_Actions:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartActionsHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartActionsBody));
break;
}
case pageid_Actions_2:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartActionsHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartActionsBody2));
break;
}
case pageid_OverlayTags:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartOverlayTagsHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartOverlayTagsBody));
break;
}
case pageid_Settings_End:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartSettingsHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartSettingsEndBody));
break;
}
case pageid_FloatingUI:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartFloatingUIHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartFloatingUIBody));
break;
}
case pageid_DesktopMode:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartDesktopModeHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartDesktopModeBody));
break;
}
case pageid_ReadMe:
{
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_AuxUIQuickStartEndHeader));
ImGui::Indent();
ImGui::TextUnformatted(TranslationManager::GetString(tstr_AuxUIQuickStartEndBody));
break;
}
}
ImGui::Unindent();
ImGui::PopTextWrapPos();
}
else
{
ImGui::PopStyleColor(); //ImGuiCol_ChildBg
}
if (is_inactive_page)
{
ImGui::PopItemDisabledNoVisual();
}
ImGui::EndChild();
if (i + 1 < active_page_count)
{
ImGui::SameLine();
}
}
//Bottom buttons
ImGui::Separator();
int current_page_prev = m_CurrentPage;
if (current_page_prev == 0)
ImGui::PushItemDisabled();
if (ImGui::Button(TranslationManager::GetString(tstr_AuxUIQuickStartButtonPrev)))
{
m_CurrentPage--;
OnPageChange(m_CurrentPage);
UIManager::Get()->RepeatFrame();
}
if (current_page_prev == 0)
ImGui::PopItemDisabled();
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
if (current_page_prev == pageid_MAX)
ImGui::PushItemDisabled();
if (ImGui::Button(TranslationManager::GetString(tstr_AuxUIQuickStartButtonNext)))
{
m_CurrentPage++;
OnPageChange(m_CurrentPage);
UIManager::Get()->RepeatFrame();
}
if (current_page_prev == pageid_MAX)
ImGui::PopItemDisabled();
ImGui::SameLine();
if (ImGui::Button(TranslationManager::GetString(tstr_AuxUIQuickStartButtonClose)))
{
ConfigManager::SetValue(configid_bool_interface_quick_start_hidden, true);
Hide();
}
m_Size = ImGui::GetWindowSize();
ImGui::End();
if ( (!UIManager::Get()->IsInDesktopMode()) && (m_Visible) && (!m_IsTransitionFading) && (m_TransformAnimationProgress != 1.0f) )
{
UpdateOverlayPos();
}
if (m_AutoSizeFrames > 0)
{
m_AutoSizeFrames--;
if ((m_AutoSizeFrames == 0) && (!UIManager::Get()->IsInDesktopMode()))
{
SetUpOverlay();
}
}
}
void WindowQuickStart::Reset()
{
Hide();
ConfigManager::SetValue(configid_bool_interface_quick_start_hidden, false);
UIManager::Get()->GetSettingsWindow().QuickStartGuideGoToPage(wndsettings_page_main);
m_Transform.zero();
m_CurrentPage = 0;
OnPageChange(m_CurrentPage);
if (m_Alpha == 0.0f)
{
m_PageAnimationProgress = 1.0f;
}
UIManager::Get()->RepeatFrame();
}
//--WindowCaptureWindowSelect
WindowCaptureWindowSelect::WindowCaptureWindowSelect() : AuxUIWindow(auxui_window_select), m_WindowLastClicked(nullptr), m_HoveredTickLast(0)
{
//Leave 2 pixel padding around so interpolation doesn't cut off the pixel border
const DPRect& rect = UITextureSpaces::Get().GetRect(ui_texspace_aux_ui);
m_Size = {float(rect.GetWidth() - 4), float(rect.GetHeight() - 4)};
m_Pos = {float(rect.GetTL().x + 2), float(rect.GetTL().y + 2)};
m_PosCenter = {(float)rect.GetCenter().x, (float)rect.GetCenter().y};
}
void WindowCaptureWindowSelect::SetUpOverlay()
{
vr::VROverlayHandle_t overlay_handle = UIManager::Get()->GetOverlayHandleAuxUI();
vr::VROverlay()->SetOverlayWidthInMeters(overlay_handle, OVERLAY_WIDTH_METERS_AUXUI_WINDOW_SELECT);
vr::VROverlay()->SetOverlaySortOrder(overlay_handle, 2);
vr::VROverlay()->SetOverlayAlpha(overlay_handle, 0.0f);
vr::VROverlay()->SetOverlayInputMethod(overlay_handle, vr::VROverlayInputMethod_Mouse);
//Take transform set by Overlay Bar and offset it a bit forward
Matrix4 mat = m_Transform;
mat.translate_relative(0.0f, 0.0f, 0.1f);
vr::HmdMatrix34_t mat_ovr = mat.toOpenVR34();
vr::VROverlay()->SetOverlayTransformAbsolute(overlay_handle, vr::TrackingUniverseStanding, &mat_ovr);
SetUpTextureBounds();
vr::VROverlay()->ShowOverlay(overlay_handle);
}
void WindowCaptureWindowSelect::ApplyPendingValues()
{
m_WindowLastClicked = nullptr;
}
bool WindowCaptureWindowSelect::Show()
{
//Limit window height to perfectly fit 14 list entries (not done in constructor since we need ImGui context)
m_Size.y = std::min( ((ImGui::GetTextLineHeight() + ImGui::GetStyle().ItemSpacing.y) * 14.0f) + (ImGui::GetStyle().WindowPadding.y * 2.0f), m_Size.y);
m_HoveredTickLast = ::GetTickCount64();
return AuxUIWindow::Show();
}
void WindowCaptureWindowSelect::Update()
{
bool render_window = WindowUpdateBase() || m_Size.x == -1.0f;
if (!render_window)
return;
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize;
if (!m_Visible)
flags |= ImGuiWindowFlags_NoInputs;
ImGui::SetNextWindowSizeConstraints({4.0f, 4.0f}, m_Size);
ImGui::SetNextWindowPos(m_PosCenter, ImGuiCond_Always, {0.5f, 0.5f});
ImGui::SetNextWindowScroll({0.0f, -1.0f}); //Prevent horizontal scrolling from happening on overflow
ImGui::Begin("WindowCaptureWindowSelect", nullptr, flags);
//Hide if clicked outside or not hovered for 3 seconds
if (!ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem))
{
if ((ImGui::IsAnyMouseClicked()) || (m_HoveredTickLast + 3000 < ::GetTickCount64()))
{
Hide();
}
}
else
{
m_HoveredTickLast = ::GetTickCount64();
}
//Adjust selectable colors to avoid flicker when fading the window out after activating
ImGui::PushStyleColor(ImGuiCol_Header, ImGui::GetStyleColorVec4(ImGuiCol_HeaderHovered));
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImGui::GetStyleColorVec4(ImGuiCol_HeaderHovered));
//List windows
ImVec2 img_size_line_height = {ImGui::GetTextLineHeight(), ImGui::GetTextLineHeight()};
ImVec2 img_size, img_uv_min, img_uv_max;
for (const auto& window_info : WindowManager::Get().WindowListGet())
{
ImGui::PushID(window_info.GetWindowHandle());
ImGui::Selectable("", (window_info.GetWindowHandle() == m_WindowLastClicked));
if (ImGui::IsItemActivated())
{
if (UIManager::Get()->IsOpenVRLoaded())
{
vr::TrackedDeviceIndex_t device_index = ConfigManager::Get().GetPrimaryLaserPointerDevice();
//If no dashboard device, try finding one
if (device_index == vr::k_unTrackedDeviceIndexInvalid)
{
device_index = vr::IVROverlayEx::FindPointerDeviceForOverlay(UIManager::Get()->GetOverlayHandleAuxUI());
}
//Try to get the pointer distance
float source_distance = 1.0f;
vr::VROverlayIntersectionResults_t results;
if (vr::IVROverlayEx::ComputeOverlayIntersectionForDevice(UIManager::Get()->GetOverlayHandleAuxUI(), device_index, vr::TrackingUniverseStanding, &results))
{
source_distance = results.fDistance;
}
//Set pointer hint in case dashboard app needs it
ConfigManager::SetValue(configid_int_state_laser_pointer_device_hint, (int)device_index);
IPCManager::Get().PostConfigMessageToDashboardApp(configid_int_state_laser_pointer_device_hint, (int)device_index);
//Add overlay
HWND window_handle = window_info.GetWindowHandle();
OverlayManager::Get().AddOverlay(ovrl_capsource_winrt_capture, -2, window_handle);
//Send to dashboard app
IPCManager::Get().PostConfigMessageToDashboardApp(configid_handle_state_arg_hwnd, (LPARAM)window_handle);
IPCManager::Get().PostMessageToDashboardApp(ipcmsg_action, ipcact_overlay_new_drag, MAKELPARAM(-2, (source_distance * 100.0f)));
}
//Store last clicked window to avoid Selectable flicker on fade out
m_WindowLastClicked = window_info.GetWindowHandle();
//We're done here, hide this window
Hide();
}
ImGui::SameLine(0.0f, 0.0f);
//Window icon and title
int icon_id = TextureManager::Get().GetWindowIconCacheID(window_info.GetIcon());
if (icon_id != -1)
{
TextureManager::Get().GetWindowIconTextureInfo(icon_id, img_size, img_uv_min, img_uv_max);
ImGui::Image(ImGui::GetIO().Fonts->TexID, img_size_line_height, img_uv_min, img_uv_max);
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemInnerSpacing.x);
}
ImGui::TextUnformatted(window_info.GetListTitle().c_str());
ImGui::PopID();
}
ImGui::PopStyleColor();
ImGui::PopStyleColor();
ImGui::End();
//Handle auto-size frames
if (m_AutoSizeFrames > 0)
{
m_AutoSizeFrames--;
if ((m_AutoSizeFrames == 0) && (!UIManager::Get()->IsInDesktopMode()))
{
SetUpOverlay();
}
}
}
void WindowCaptureWindowSelect::SetTransform(Matrix4 transform)
{
m_Transform = transform;
if (m_Visible)
{
StartTransitionFade();
}
}
//--AuxUI
AuxUI::AuxUI() : m_ActiveUIID(auxui_none), m_IsUIFadingOut(false)
{
//Nothing
}
void AuxUI::Update()
{
//All Aux UI windows are updated even when not active
m_WindowDragHint.Update();
m_WindowGazeFadeAutoHint.Update();
m_WindowCaptureWindowSelect.Update();
m_WindowQuickStart.Update();
}
bool AuxUI::IsActive() const
{
return (m_ActiveUIID != auxui_none);
}
void AuxUI::HideTemporaryWindows()
{
switch (m_ActiveUIID)
{
case auxui_window_select:
{
ClearActiveUI(m_ActiveUIID);
break;
}
default: break;
}
}
bool AuxUI::SetActiveUI(AuxUIID new_ui_id)
{
if (new_ui_id >= m_ActiveUIID)
{
if ( (m_ActiveUIID != auxui_none) && (new_ui_id != m_ActiveUIID) )
{
m_IsUIFadingOut = true;
}
m_ActiveUIID = new_ui_id;
return true;
}
return false;
}
AuxUIID AuxUI::GetActiveUI() const
{
return m_ActiveUIID;
}
void AuxUI::ClearActiveUI(AuxUIID current_ui_id)
{
if (m_ActiveUIID == current_ui_id)
{
m_ActiveUIID = auxui_none;
}
}
bool AuxUI::IsUIFadingOut() const
{
return m_IsUIFadingOut;
}
void AuxUI::SetFadeOutFinished()
{
m_IsUIFadingOut = false;
}
WindowDragHint& AuxUI::GetDragHintWindow()
{
return m_WindowDragHint;
}
WindowGazeFadeAutoHint& AuxUI::GetGazeFadeAutoHintWindow()
{
return m_WindowGazeFadeAutoHint;
}
WindowCaptureWindowSelect& AuxUI::GetCaptureWindowSelectWindow()
{
return m_WindowCaptureWindowSelect;
}
WindowQuickStart& AuxUI::GetQuickStartWindow()
{
return m_WindowQuickStart;
}
================================================
FILE: src/DesktopPlusUI/AuxUI.h
================================================
#pragma once
#define NOMINMAX
#include
#include "imgui.h"
#include "openvr.h"
#include "Matrices.h"
//Hosts and manages UI that is displayed on the DesktopPlusUIAux overlay
//Generally rarely and not parallel displayed things go here
//Order of UI ID also determines priority. A higher priority UI can force an active lower one to stop displaying
enum AuxUIID
{
auxui_none,
auxui_window_quickstart,
auxui_drag_hint,
auxui_gazefade_auto_hint,
auxui_window_select
};
class AuxUIWindow
{
protected:
const AuxUIID m_AuxUIID;
ImVec2 m_Pos; //Used for overlay texture bounds, always top-left pivot, should already be offset for 2x2 pixel padding
ImVec2 m_Size; //Used for overlay texture bounds, read as max size for ImGui window when using auto-sizing
bool m_Visible;
float m_Alpha;
bool m_IsTransitionFading; //If true, AuxUIWindow does not clear the active UI but instead shows the window again to transition to a different position with a fade
int m_AutoSizeFrames; //Overlay setup may need to be delayed when the window uses auto-sizing. In that case m_Alpha stays 0.0f while m_AutoSizeFrames is not 0
bool WindowUpdateBase(); //Handles alpha fading according to AuxUI state, but doesn't Begin() a window like FloatingWindow would. Returns true if window should be rendered
void DrawFullDimmedRectBehindWindow(); //Call before End() to draw a dimmed rect behind the window in desktop mode. Uses m_Alpha for fading. Messes with draw list, may break
virtual void SetUpTextureBounds();
virtual void StartTransitionFade();
virtual void ApplyPendingValues(); //Pending values when switching between states from a transition fade should be applied here, if any
public:
AuxUIWindow(AuxUIID ui_id);
virtual bool Show();
virtual void Hide();
bool IsVisible();
};
class WindowDragHint : public AuxUIWindow
{
public:
enum HintType
{
hint_none,
hint_docking,
hint_undocking,
hint_ovrl_locked,
hint_ovrl_theater_screen_blocked
};
private:
vr::TrackedDeviceIndex_t m_TargetDevice;
WindowDragHint::HintType m_HintType;
WindowDragHint::HintType m_HintTypePending;
void SetUpOverlay();
void UpdateOverlayPos();
virtual void ApplyPendingValues();
public:
WindowDragHint();
void Update();
void SetHintType(vr::TrackedDeviceIndex_t device_index, WindowDragHint::HintType hint_type);
};
class WindowGazeFadeAutoHint : public AuxUIWindow
{
private:
unsigned int m_TargetOverlay;
int m_Countdown;
double m_TickTime;
std::string m_Label;
void SetUpOverlay();
void UpdateOverlayPos();
public:
WindowGazeFadeAutoHint();
virtual bool Show();
virtual void Hide();
void Update();
void SetTargetOverlay(unsigned int overlay_id);
};
class WindowQuickStart : public AuxUIWindow
{
private:
enum PageID : int
{
pageid_Welcome,
pageid_Overlays,
pageid_Overlays_2,
pageid_OverlayProperties,
pageid_OverlayProperties_2,
pageid_Settings,
pageid_Profiles,
pageid_Actions,
pageid_Actions_2,
pageid_OverlayTags,
pageid_Settings_End,
pageid_FloatingUI,
pageid_DesktopMode,
pageid_ReadMe,
pageid_MAX = pageid_ReadMe,
};
int m_CurrentPage = 0;
int m_CurrentPageAnimation = 0;
int m_CurrentPageAnimationMax = 0;
int m_PageAnimationDir = 0;
float m_PageAnimationProgress = 0.0f;
float m_PageAnimationStartPos = 0.0f;
float m_PageAnimationOffset = 0.0f;
Matrix4 m_Transform;
Matrix4 m_TransformAnimationStart;
Matrix4 m_TransformAnimationEnd;
float m_TransformAnimationProgress = 0.0f;
ULONGLONG m_TimeoutTickStart = 0;
void SetUpOverlay();
void UpdateOverlayPos();
void OnPageChange(int page_id);
public:
WindowQuickStart();
virtual bool Show();
virtual void Hide();
void Update();
void Reset();
};
class WindowCaptureWindowSelect : public AuxUIWindow
{
private:
ImVec2 m_PosCenter;
Matrix4 m_Transform;
HWND m_WindowLastClicked;
ULONGLONG m_HoveredTickLast;
void SetUpOverlay();
virtual void ApplyPendingValues();
public:
WindowCaptureWindowSelect();
virtual bool Show();
void Update();
void SetTransform(Matrix4 transform);
};
class AuxUI
{
friend class AuxUIWindow;
private:
AuxUIID m_ActiveUIID;
bool m_IsUIFadingOut;
WindowDragHint m_WindowDragHint;
WindowGazeFadeAutoHint m_WindowGazeFadeAutoHint;
WindowCaptureWindowSelect m_WindowCaptureWindowSelect;
WindowQuickStart m_WindowQuickStart;
bool SetActiveUI(AuxUIID new_ui_id); //May return false if higher priority UI is active
AuxUIID GetActiveUI() const;
void ClearActiveUI(AuxUIID current_ui_id); //Sets active UI to auxui_none, but current ID has to match active one
bool IsUIFadingOut() const;
void SetFadeOutFinished();
public:
AuxUI();
void Update();
bool IsActive() const;
void HideTemporaryWindows(); //Called on certain OpenVR events to hide temporary windows when user interaction ended so they don't hang around needlessly
WindowDragHint& GetDragHintWindow();
WindowGazeFadeAutoHint& GetGazeFadeAutoHintWindow();
WindowCaptureWindowSelect& GetCaptureWindowSelectWindow();
WindowQuickStart& GetQuickStartWindow();
};
================================================
FILE: src/DesktopPlusUI/DesktopPlusUI.cpp
================================================
//This code belongs to the Desktop+ OpenVR overlay application, licensed under GPL 3.0
//
//Much of the code here is based on the Dear ImGui Win32 DirectX 11 sample
#define NOMINMAX
#include "imgui.h"
#include "imgui_impl_win32_openvr.h"
#include "imgui_impl_dx11_openvr.h"
#include "implot.h"
#include
#include
#define DIRECTINPUT_VERSION 0x0800
#include
#include
#include
#include
#include "resource.h"
#include "UIManager.h"
#include "TextureManager.h"
#include "InterprocessMessaging.h"
#include "WindowSettings.h"
#include "Util.h"
#include "OpenVRExt.h"
#include "ImGuiExt.h"
#include "DesktopPlusWinRT.h"
#include "DPBrowserAPIClient.h"
#include "WindowDesktopMode.h"
// Data
static Microsoft::WRL::ComPtr g_pd3dDevice;
static Microsoft::WRL::ComPtr g_pd3dDeviceContext;
static Microsoft::WRL::ComPtr g_pSwapChain;
static Microsoft::WRL::ComPtr g_desktopRenderTargetView;
static Microsoft::WRL::ComPtr g_vrTex;
static Microsoft::WRL::ComPtr g_vrRenderTargetView;
// Forward declarations of helper functions
bool CreateDeviceD3D(HWND hWnd, bool desktop_mode);
void CleanupDeviceD3D();
void CreateRenderTarget(bool desktop_mode);
void CleanupRenderTarget();
void RefreshOverlayTextureSharing();
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
void InitImGui(HWND hwnd, bool desktop_mode);
void ProcessCmdline(bool& force_desktop_mode, bool& open_keyboard_editor);
// Main code
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ INT nCmdShow)
{
DPLog_Init("DesktopPlusUI");
bool force_desktop_mode = false;
bool open_keyboard_editor = false;
ProcessCmdline(force_desktop_mode, open_keyboard_editor);
//Automatically use desktop mode if dashboard app isn't running
bool desktop_mode = ( (force_desktop_mode) || (!IPCManager::IsDashboardAppRunning()) );
LOG_F(INFO, "Desktop+ UI running in %s", (desktop_mode) ? ((open_keyboard_editor) ? "desktop mode (keyboard editor)" : "desktop mode") : "VR mode");
//Make sure only one instance is running
StopProcessByWindowClass(g_WindowClassNameUIApp);
//Enable DPI support for desktop mode
ImGui_ImplWin32_EnableDpiAwareness();
//Register application class
WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, WndProc, 0L, 0L, hInstance, nullptr, nullptr, nullptr, nullptr, g_WindowClassNameUIApp, nullptr };
wc.hIcon = (HICON)::LoadImage(hInstance, MAKEINTRESOURCE(IDI_DPLUS), IMAGE_ICON, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), LR_DEFAULTCOLOR);
wc.hIconSm = (HICON)::LoadImage(hInstance, MAKEINTRESOURCE(IDI_DPLUS), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR);
::RegisterClassEx(&wc);
//Create window
HWND hwnd;
if (desktop_mode)
hwnd = ::CreateWindow(wc.lpszClassName, L"Desktop+", WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, -1, -1, 100, 100, nullptr, nullptr, wc.hInstance, nullptr);
else
hwnd = ::CreateWindow(wc.lpszClassName, L"Desktop+ UI", 0, 0, 0, 1, 1, HWND_MESSAGE, nullptr, wc.hInstance, nullptr);
//Init UITextureSpaces
UITextureSpaces::Get().Init(desktop_mode, open_keyboard_editor);
//Init WinRT DLL
DPWinRT_Init();
LOG_F(INFO, "Loaded WinRT library");
//Init BrowserClientAPI (this doesn't start the browser process, only checks for presence)
DPBrowserAPIClient::Get().Init();
//Allow IPC messages even when elevated (though in normal operation, this process should not be elevated)
IPCManager::Get().DisableUIPForRegisteredMessages(hwnd);
//Init UIManager and load config
UIManager ui_manager(desktop_mode, open_keyboard_editor);
UIManager::IdleState& idle_state = ui_manager.GetIdleState();
ConfigManager::Get().LoadConfigFromFile();
IPCManager::Get().PostMessageToDashboardApp(ipcmsg_action, ipcact_sync_config_state);
ui_manager.SetWindowHandle(hwnd);
//Init OpenVR
//Don't try to init OpenVR without the dashboard app running since checking for active VR means launching SteamVR
if ( (!desktop_mode) || (IPCManager::IsDashboardAppRunning()) )
{
if (ui_manager.InitOverlay() != vr::VRInitError_None)
{
::UnregisterClass(wc.lpszClassName, wc.hInstance);
LOG_F(ERROR, "OpenVR init failed!");
//Try starting in desktop mode instead
if (!desktop_mode)
{
LOG_F(INFO, "Attempting to start in desktop mode instead...");
ui_manager.Restart(true);
}
return 2;
}
}
DPLog_SteamVR_SystemInfo();
// Initialize Direct3D
if (!CreateDeviceD3D(hwnd, desktop_mode))
{
CleanupDeviceD3D();
::UnregisterClass(wc.lpszClassName, wc.hInstance);
LOG_F(ERROR, "Direct3D init failed!");
return 1;
}
LOG_F(INFO, "Loaded Direct3D");
//Center window to the right monitor before setting real size for DPI detection to be correct
if (desktop_mode)
{
CenterWindowToMonitor(hwnd, true);
}
InitImGui(hwnd, desktop_mode);
ImGuiIO& io = ImGui::GetIO();
LOG_F(INFO, "Loaded Dear ImGui");
if (desktop_mode)
{
const DPRect& rect_total = UITextureSpaces::Get().GetRect(ui_texspace_total);
//Set real window size
RECT r;
r.left = 0;
r.top = 0;
r.right = int(rect_total.GetWidth() * ui_manager.GetUIScale());
r.bottom = int(rect_total.GetHeight() * ui_manager.GetUIScale());
::AdjustWindowRect(&r, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, FALSE);
::SetWindowPos(hwnd, NULL, 0, 0, r.right - r.left, r.bottom - r.top, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
//Center window on screen
CenterWindowToMonitor(hwnd, true);
::ShowWindow(hwnd, SW_SHOWDEFAULT);
::UpdateWindow(hwnd);
}
float clear_color[4] = {0.0f, 0.0f, 0.0f, 0.0f};
//Init notification icon if OpenVR is running (no need for it in pure desktop mode without switching back)
if ( (!ConfigManager::GetValue(configid_bool_interface_no_notification_icon)) && (ui_manager.IsOpenVRLoaded()) )
{
ui_manager.GetNotificationIcon().Init(hInstance);
}
ui_manager.OnInitDone();
LOG_F(INFO, "Finished startup");
//Main loop
MSG msg;
ZeroMemory(&msg, sizeof(msg));
while (msg.message != WM_QUIT)
{
//Poll and handle messages (inputs, window resize, etc.)
if (::PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
{
idle_state.OnWindowMessage(msg.message);
if (msg.message >= 0xC000) //Custom message from overlay process, handle in UI manager
{
ui_manager.HandleIPCMessage(msg);
}
else
{
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
continue;
}
if (!desktop_mode)
{
vr::VREvent_t vr_event;
bool do_quit = false;
//Handle OpenVR events for the dashboard UI
ImVec4 rect_v4 = UITextureSpaces::Get().GetRectAsVec4(ui_texspace_overlay_bar);
while (vr::VROverlay()->PollNextOverlayEvent(ui_manager.GetOverlayHandleOverlayBar(), &vr_event, sizeof(vr_event)))
{
idle_state.OnOpenVREvent(vr_event.eventType);
ImGui_ImplOpenVR_InputEventHandler(vr_event, &rect_v4);
switch (vr_event.eventType)
{
case vr::VREvent_FocusEnter:
{
//Adjust sort order so mainbar tooltips are displayed right
vr::VROverlay()->SetOverlaySortOrder(ui_manager.GetOverlayHandleOverlayBar(), 1);
break;
}
case vr::VREvent_FocusLeave:
case vr::VREvent_OverlayHidden:
{
//Reset adjustment so other overlays are not always behind the UI unless really needed
if (!ui_manager.GetOverlayBarWindow().IsAnyMenuVisible())
{
vr::VROverlay()->SetOverlaySortOrder(ui_manager.GetOverlayHandleOverlayBar(), 0);
}
break;
}
case vr::VREvent_TrackedDeviceActivated:
case vr::VREvent_TrackedDeviceDeactivated:
{
ui_manager.GetPerformanceWindow().RefreshTrackerBatteryList();
break;
}
case vr::VREvent_LeaveStandbyMode:
{
//Reset performance stats when leaving standby since it adds to the dropped frame count during that
ui_manager.GetPerformanceWindow().ResetCumulativeValues();
break;
}
case vr::VREvent_OverlaySharedTextureChanged:
{
//This should only happen during startup since the texture size never changes, but it has happened seemingly randomly before, so handle it
RefreshOverlayTextureSharing();
break;
}
case vr::VREvent_SceneApplicationChanged:
{
const bool loaded_overlay_profile = ConfigManager::Get().GetAppProfileManager().ActivateProfileForProcess(vr_event.data.process.pid);
if (loaded_overlay_profile)
{
ui_manager.OnProfileLoaded();
}
break;
}
case vr::VREvent_Quit:
{
do_quit = true;
break;
}
}
}
//Handle OpenVR events for the floating UI
rect_v4 = UITextureSpaces::Get().GetRectAsVec4(ui_texspace_floating_ui);
while (vr::VROverlay()->PollNextOverlayEvent(ui_manager.GetOverlayHandleFloatingUI(), &vr_event, sizeof(vr_event)))
{
idle_state.OnOpenVREvent(vr_event.eventType);
ImGui_ImplOpenVR_InputEventHandler(vr_event, &rect_v4);
switch (vr_event.eventType)
{
case vr::VREvent_FocusEnter:
{
//Adjust sort order so tooltips are displayed right
vr::VROverlay()->SetOverlaySortOrder(ui_manager.GetOverlayHandleFloatingUI(), 1);
break;
}
case vr::VREvent_FocusLeave:
{
//Reset adjustment so other overlays are not always behind the UI unless really needed
vr::VROverlay()->SetOverlaySortOrder(ui_manager.GetOverlayHandleFloatingUI(), 0);
break;
}
}
}
//Handle OpenVR events for the floating settings
rect_v4 = UITextureSpaces::Get().GetRectAsVec4(ui_texspace_settings);
while (vr::VROverlay()->PollNextOverlayEvent(ui_manager.GetOverlayHandleSettings(), &vr_event, sizeof(vr_event)))
{
idle_state.OnOpenVREvent(vr_event.eventType);
ImGui_ImplOpenVR_InputEventHandler(vr_event, &rect_v4);
}
//Handle OpenVR events for the overlay properties
rect_v4 = UITextureSpaces::Get().GetRectAsVec4(ui_texspace_overlay_properties);
while (vr::VROverlay()->PollNextOverlayEvent(ui_manager.GetOverlayHandleOverlayProperties(), &vr_event, sizeof(vr_event)))
{
idle_state.OnOpenVREvent(vr_event.eventType);
ImGui_ImplOpenVR_InputEventHandler(vr_event);
}
//Handle OpenVR events for the VR keyboard
rect_v4 = UITextureSpaces::Get().GetRectAsVec4(ui_texspace_keyboard);
while (vr::VROverlay()->PollNextOverlayEvent(ui_manager.GetOverlayHandleKeyboard(), &vr_event, sizeof(vr_event)))
{
idle_state.OnOpenVREvent(vr_event.eventType);
//Let the keyboard window handle events first for multi-laser support
if (!ui_manager.GetVRKeyboard().GetWindow().HandleOverlayEvent(vr_event))
{
ImGui_ImplOpenVR_InputEventHandler(vr_event, &rect_v4);
}
}
//Handle OpenVR events for the Aux UI
rect_v4 = UITextureSpaces::Get().GetRectAsVec4(ui_texspace_aux_ui);
while (vr::VROverlay()->PollNextOverlayEvent(ui_manager.GetOverlayHandleAuxUI(), &vr_event, sizeof(vr_event)))
{
idle_state.OnOpenVREvent(vr_event.eventType);
ImGui_ImplOpenVR_InputEventHandler(vr_event, &rect_v4);
switch (vr_event.eventType)
{
case vr::VREvent_DashboardActivated:
case vr::VREvent_DashboardDeactivated:
{
ui_manager.GetAuxUI().HideTemporaryWindows();
break;
}
}
}
ui_manager.PositionOverlay();
ui_manager.GetFloatingUI().UpdateUITargetState();
ui_manager.GetSettingsWindow().UpdateVisibility();
ui_manager.GetOverlayPropertiesWindow().UpdateVisibility();
ui_manager.GetVRKeyboard().GetWindow().UpdateVisibility();
if (do_quit)
{
break; //Breaks the message loop, causing clean shutdown
}
}
ui_manager.GetPerformanceWindow().UpdateVisibleState();
//Do texture reload now if it had been scheduled
//However, do not reload texture while an item is active as the ID changes on ImageButtons... but allow it if InputText is active to not have placeholder characters on user input
if ( (TextureManager::Get().GetReloadLaterFlag()) && ( (!ImGui::IsAnyItemActiveOrDeactivated()) || (ImGui::IsAnyInputTextActive()) ) )
{
TextureManager::Get().LoadAllTexturesAndBuildFonts();
}
//While we still need to poll, greatly reduce the rate and don't do any ImGui stuff to not waste resources (hopefully this does not mess up ImGui input state)
if ((idle_state.ShouldIdle()) && (!ui_manager.HasDelayedIPCMessages()))
{
::Sleep(64); //Could wait longer, but it doesn't really make much of a difference in load and we stay more responsive like this)
idle_state.DoIdleTimestep(); //Delta time won't be updated by ImGui while idle, but some parts of the app still rely on it so we do it ourselves
continue;
}
// Start the Dear ImGui frame
ImGui_ImplDX11_NewFrame();
if (desktop_mode)
ImGui_ImplWin32_NewFrame();
else
ImGui_ImplOpenVR_NewFrame();
idle_state.OnImGuiNewFrame();
ui_manager.GetVRKeyboard().OnImGuiNewFrame();
ImGui::NewFrame();
//Handle delayed ICP messages that need to be handled within an ImGui frame now
ui_manager.HandleDelayedIPCMessages();
if (!desktop_mode)
{
//Overlay dragging needs to happen within an ImGui frame for a valid scroll input state (ImGui::EndFrame() clears it by the time we'd loop again)
ui_manager.UpdateOverlayDrag();
//Make ImGui think the surface is smaller than it is (a poor man's multi-viewport hack)
//Overlay Bar (this and floating UI need no X adjustments)
io.DisplaySize.y = (float)UITextureSpaces::Get().GetRect(ui_texspace_overlay_bar).GetBR().y;
ImGui::GetMainViewport()->Size.y = io.DisplaySize.y;
ui_manager.GetOverlayBarWindow().Update();
//Floating UI
io.DisplaySize.y = (float)UITextureSpaces::Get().GetRect(ui_texspace_floating_ui).GetBR().y;
ImGui::GetMainViewport()->Size.y = io.DisplaySize.y;
ui_manager.GetFloatingUI().Update();
//Overlay Properties
io.DisplaySize.x = (float)UITextureSpaces::Get().GetRect(ui_texspace_overlay_properties).GetBR().x;
ImGui::GetMainViewport()->Size.x = io.DisplaySize.x;
io.DisplaySize.y = (float)UITextureSpaces::Get().GetRect(ui_texspace_overlay_properties).GetBR().y;
ImGui::GetMainViewport()->Size.y = io.DisplaySize.y;
ui_manager.GetOverlayPropertiesWindow().Update();
//Settings
io.DisplaySize.x = (float)UITextureSpaces::Get().GetRect(ui_texspace_settings).GetBR().x;
ImGui::GetMainViewport()->Size.x = io.DisplaySize.x;
io.DisplaySize.y = (float)UITextureSpaces::Get().GetRect(ui_texspace_settings).GetBR().y;
ImGui::GetMainViewport()->Size.y = io.DisplaySize.y;
ui_manager.GetSettingsWindow().Update();
//Keyboard
io.DisplaySize.x = (float)UITextureSpaces::Get().GetRect(ui_texspace_keyboard).GetBR().x;
ImGui::GetMainViewport()->Size.x = io.DisplaySize.x;
io.DisplaySize.y = (float)UITextureSpaces::Get().GetRect(ui_texspace_keyboard).GetBR().y;
ImGui::GetMainViewport()->Size.y = io.DisplaySize.y;
ui_manager.GetVRKeyboard().GetWindow().Update();
//Performance Monitor
io.DisplaySize.x = (float)UITextureSpaces::Get().GetRect(ui_texspace_performance_monitor).GetBR().x;
ImGui::GetMainViewport()->Size.x = io.DisplaySize.x;
io.DisplaySize.y = (float)UITextureSpaces::Get().GetRect(ui_texspace_performance_monitor).GetBR().y;
ImGui::GetMainViewport()->Size.y = io.DisplaySize.y;
ui_manager.GetPerformanceWindow().Update();
//Aux UI
io.DisplaySize.x = (float)UITextureSpaces::Get().GetRect(ui_texspace_aux_ui).GetBR().x;
ImGui::GetMainViewport()->Size.x = io.DisplaySize.x;
io.DisplaySize.y = (float)UITextureSpaces::Get().GetRect(ui_texspace_aux_ui).GetBR().y;
ImGui::GetMainViewport()->Size.y = io.DisplaySize.y;
ui_manager.GetAuxUI().Update();
}
else
{
if (open_keyboard_editor)
{
ui_manager.GetVRKeyboard().GetEditor().Update();
}
else
{
ui_manager.GetDesktopModeWindow().Update();
}
}
//Haptic feedback for hovered items, like the rest of the SteamVR UI
if ( (!desktop_mode) && (ImGui::HasHoveredNewItem()) )
{
for (const auto& overlay_handle : ui_manager.GetUIOverlayHandles())
{
if (ConfigManager::Get().IsLaserPointerTargetOverlay(overlay_handle))
{
ui_manager.TriggerLaserPointerHaptics(overlay_handle);
}
}
}
// Rendering
if (ui_manager.GetRepeatFrame()) //If frame repeat is enabled, don't actually render and skip vsync
{
ImGui::EndFrame();
ui_manager.DecreaseRepeatFrameCount();
}
else
{
ImGui::Render();
if (desktop_mode)
{
g_pd3dDeviceContext->OMSetRenderTargets(1, g_desktopRenderTargetView.GetAddressOf(), nullptr);
g_pd3dDeviceContext->ClearRenderTargetView(g_desktopRenderTargetView.Get(), clear_color);
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
const int sync_count = idle_state.GetFrameSkipValue() + 1;
HRESULT res = g_pSwapChain->Present(std::min(sync_count, 4), 0); //Present with vsync (and optionally wait longer with frameskip)
if (res == DXGI_STATUS_OCCLUDED) //When occluded, Present() will not wait for us
{
for (int i = 0; i < sync_count; ++i)
{
::DwmFlush(); //We could wait longer, but let's stay responsive
}
}
}
else
{
g_pd3dDeviceContext->OMSetRenderTargets(1, g_vrRenderTargetView.GetAddressOf(), nullptr);
g_pd3dDeviceContext->ClearRenderTargetView(g_vrRenderTargetView.Get(), clear_color);
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
//Set Overlay texture
ID3D11ShaderResourceView* ovrl_shader_rsv = vr::VROverlayEx()->GetOverlayTextureEx(ui_manager.GetOverlayHandleOverlayBar(), g_vrTex.Get());
if (ovrl_shader_rsv != nullptr)
{
//Do a partial update, only copying the active texture space into the overlay texture
Microsoft::WRL::ComPtr ovrl_tex;
ovrl_shader_rsv->GetResource(&ovrl_tex);
const DPRect active_rect = ui_manager.CalcRectForActiveTexspace();
if (active_rect.GetWidth() != 0)
{
D3D11_BOX box = {0};
box.left = active_rect.GetTL().x;
box.top = active_rect.GetTL().y;
box.front = 0;
box.right = active_rect.GetBR().x;
box.bottom = active_rect.GetBR().y;
box.back = 1;
g_pd3dDeviceContext->CopySubresourceRegion(ovrl_tex.Get(), 0, box.left, box.top, 0, g_vrTex.Get(), 0, &box);
}
//RSV is kept around by IVROverlayEx and not released here
}
else if ((ui_manager.GetOverlayHandleOverlayBar() != vr::k_ulOverlayHandleInvalid) && (g_vrTex))
{
vr::Texture_t vrtex = {};
vrtex.handle = g_vrTex.Get();
vrtex.eType = vr::TextureType_DirectX;
vrtex.eColorSpace = vr::ColorSpace_Gamma;
vr::VROverlayEx()->SetOverlayTextureEx(ui_manager.GetOverlayHandleOverlayBar(), &vrtex, {(int)io.DisplaySize.x, (int)io.DisplaySize.y});
}
//Set overlay intersection mask... there doesn't seem to be much overhead from doing this every frame, even though we only need to update this sometimes
auto overlay_handles = ui_manager.GetUIOverlayHandles();
std::vector mask_primitives;
ImGui_ImplOpenVR_SetIntersectionMaskFromWindows(overlay_handles.data(), overlay_handles.size(), &mask_primitives);
ui_manager.SendUIIntersectionMaskToDashboardApp(mask_primitives);
//Wait for VR frame sync, optionally repeatedly to achieve lower synched frame rates
g_pd3dDeviceContext->Flush();
const int sync_count = idle_state.GetFrameSkipValue() + 1;
for (int i = 0; i < sync_count; ++i)
{
vr::VROverlay()->WaitFrameSync(34); //(34 ms timeout so we don't go much below 30 fps worst case)
}
}
}
}
LOG_F(INFO, "Shutting down...");
// Cleanup
ui_manager.OnExit();
ImGui_ImplDX11_Shutdown();
ImGui_ImplWin32_Shutdown();
ImPlot::DestroyContext();
ImGui::DestroyContext();
CleanupDeviceD3D();
::DestroyWindow(hwnd);
::UnregisterClass(wc.lpszClassName, wc.hInstance);
return 0;
}
// Helper functions
bool CreateDeviceD3D(HWND hWnd, bool desktop_mode)
{
const UINT target_adapter_deviceid = (ConfigManager::GetValue(configid_int_misc_force_gpu_deviceid) == -1) ? 0 : (UINT)ConfigManager::GetValue(configid_int_misc_force_gpu_deviceid);
const UINT target_adapter_deviceid_vr = (ConfigManager::GetValue(configid_int_misc_force_gpu_vr_deviceid) == -1) ? 0 : (UINT)ConfigManager::GetValue(configid_int_misc_force_gpu_vr_deviceid);
//Get forced adapter if any and get the adapter recommended by OpenVR if it's loaded (needed for Performance Monitor even in desktop mode)
Microsoft::WRL::ComPtr adapter_ptr_vr;
Microsoft::WRL::ComPtr adapter_ptr_desktop; //stays nullptr unless forced
{
Microsoft::WRL::ComPtr factory_ptr;
int32_t vr_gpu_id = -1;
if (UIManager::Get()->IsOpenVRLoaded())
{
vr::VRSystem()->GetDXGIOutputInfo(&vr_gpu_id);
}
HRESULT hr = CreateDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&factory_ptr);
if (!FAILED(hr))
{
Microsoft::WRL::ComPtr adapter_ptr;
UINT i = 0;
while (factory_ptr->EnumAdapters(i, &adapter_ptr) != DXGI_ERROR_NOT_FOUND)
{
DXGI_ADAPTER_DESC adapter_desc;
adapter_ptr->GetDesc(&adapter_desc);
if ( ((target_adapter_deviceid_vr == 0) && (i == vr_gpu_id)) || (adapter_desc.DeviceId == target_adapter_deviceid_vr) )
{
adapter_ptr_vr = adapter_ptr;
}
if (adapter_desc.DeviceId == target_adapter_deviceid)
{
adapter_ptr_desktop = adapter_ptr;
}
++i;
}
}
if (adapter_ptr_vr != nullptr)
{
//Set target GPU and total VRAM for Performance Monitor
DXGI_ADAPTER_DESC adapter_desc;
if (adapter_ptr_vr->GetDesc(&adapter_desc) == S_OK)
{
UIManager::Get()->GetPerformanceWindow().GetPerformanceData().SetTargetGPU(adapter_desc.AdapterLuid, adapter_desc.DedicatedVideoMemory);
}
}
LOG_IF_F(WARNING, (adapter_ptr_desktop == nullptr) && (target_adapter_deviceid != 0), "GPU forced by config (DeviceID %u) was not found!", target_adapter_deviceid);
LOG_IF_F(WARNING, (adapter_ptr_vr == nullptr) && (target_adapter_deviceid_vr == 0) && UIManager::Get()->IsOpenVRLoaded(), "GPU requested by SteamVR (%u) was not found!", vr_gpu_id + 1);
LOG_IF_F(WARNING, (adapter_ptr_vr == nullptr) && (target_adapter_deviceid_vr != 0), "VR GPU forced by config (DeviceID %u) was not found!", target_adapter_deviceid_vr);
}
D3D_FEATURE_LEVEL featureLevel;
const D3D_FEATURE_LEVEL featureLevelArray[2] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0, };
if (desktop_mode)
{
//Setup swap chain
DXGI_SWAP_CHAIN_DESC sd;
ZeroMemory(&sd, sizeof(sd));
sd.BufferCount = 2;
sd.BufferDesc.Width = 0;
sd.BufferDesc.Height = 0;
sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
sd.BufferDesc.RefreshRate.Numerator = 60;
sd.BufferDesc.RefreshRate.Denominator = 1;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.OutputWindow = hWnd;
sd.SampleDesc.Count = 1;
sd.SampleDesc.Quality = 0;
sd.Windowed = TRUE;
sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; //DXGI_SWAP_EFFECT_FLIP_DISCARD also would work, but we still support Windows 8
if (D3D11CreateDeviceAndSwapChain(adapter_ptr_desktop.Get(), (adapter_ptr_desktop != nullptr) ? D3D_DRIVER_TYPE_UNKNOWN : D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, featureLevelArray, 2, D3D11_SDK_VERSION,
&sd, &g_pSwapChain, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext) != S_OK)
{
return false;
}
}
else
{
//No swap chain needed for VR
if (adapter_ptr_vr != nullptr)
{
if (D3D11CreateDevice(adapter_ptr_vr.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, 0, featureLevelArray, 2, D3D11_SDK_VERSION, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext) != S_OK)
{
return false;
}
}
else //Still try /something/, but it probably won't work
{
if (D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, featureLevelArray, 2, D3D11_SDK_VERSION, &g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext) != S_OK)
{
return false;
}
}
}
CreateRenderTarget(desktop_mode);
return true;
}
void CleanupDeviceD3D()
{
CleanupRenderTarget();
g_pSwapChain.Reset();
g_pd3dDeviceContext.Reset();
g_pd3dDevice.Reset();
}
void CreateRenderTarget(bool desktop_mode)
{
HRESULT hr;
const DPRect& texrect_total = UITextureSpaces::Get().GetRect(ui_texspace_total);
// Create overlay texture
D3D11_TEXTURE2D_DESC TexD;
RtlZeroMemory(&TexD, sizeof(D3D11_TEXTURE2D_DESC));
TexD.Width = texrect_total.GetWidth();
TexD.Height = texrect_total.GetHeight();
TexD.MipLevels = 1;
TexD.ArraySize = 1;
TexD.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
TexD.SampleDesc.Count = 1;
TexD.Usage = D3D11_USAGE_DEFAULT;
TexD.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
TexD.CPUAccessFlags = 0;
TexD.MiscFlags = D3D11_RESOURCE_MISC_SHARED;
hr = g_pd3dDevice->CreateTexture2D(&TexD, nullptr, &g_vrTex);
if (FAILED(hr))
{
//Cry
return;
}
UIManager::Get()->SetSharedTextureRef(g_vrTex.Get());
// Create render target view for overlay texture
D3D11_RENDER_TARGET_VIEW_DESC tex_rtv_desc = {};
tex_rtv_desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
tex_rtv_desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
tex_rtv_desc.Texture2D.MipSlice = 0;
g_pd3dDevice->CreateRenderTargetView(g_vrTex.Get(), &tex_rtv_desc, &g_vrRenderTargetView);
if (desktop_mode)
{
Microsoft::WRL::ComPtr pBackBuffer;
g_pSwapChain->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer));
if (pBackBuffer != nullptr)
{
g_pd3dDevice->CreateRenderTargetView(pBackBuffer.Get(), nullptr, &g_desktopRenderTargetView);
}
}
}
void CleanupRenderTarget()
{
g_desktopRenderTargetView.Reset();
g_vrRenderTargetView.Reset();
g_vrTex.Reset();
}
void RefreshOverlayTextureSharing()
{
//Set up advanced texture sharing between the overlays
vr::VROverlayEx()->SetSharedOverlayTexture(UIManager::Get()->GetOverlayHandleOverlayBar(), UIManager::Get()->GetOverlayHandleFloatingUI(), g_vrTex.Get());
vr::VROverlayEx()->SetSharedOverlayTexture(UIManager::Get()->GetOverlayHandleOverlayBar(), UIManager::Get()->GetOverlayHandleSettings(), g_vrTex.Get());
vr::VROverlayEx()->SetSharedOverlayTexture(UIManager::Get()->GetOverlayHandleOverlayBar(), UIManager::Get()->GetOverlayHandleOverlayProperties(), g_vrTex.Get());
vr::VROverlayEx()->SetSharedOverlayTexture(UIManager::Get()->GetOverlayHandleOverlayBar(), UIManager::Get()->GetOverlayHandleKeyboard(), g_vrTex.Get());
vr::VROverlayEx()->SetSharedOverlayTexture(UIManager::Get()->GetOverlayHandleOverlayBar(), UIManager::Get()->GetOverlayHandleAuxUI(), g_vrTex.Get());
//Also schedule for performance overlays, in case there are any
UIManager::Get()->GetPerformanceWindow().ScheduleOverlaySharedTextureUpdate();
}
// Win32 message handler
extern LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if ((UIManager::Get()) && (UIManager::Get()->IsInDesktopMode()))
{
ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam);
}
switch (msg)
{
case WM_SIZE:
{
if (g_pd3dDevice != nullptr && wParam != SIZE_MINIMIZED)
{
CleanupRenderTarget();
g_pSwapChain->ResizeBuffers(0, (UINT)LOWORD(lParam), (UINT)HIWORD(lParam), DXGI_FORMAT_UNKNOWN, 0);
CreateRenderTarget(UIManager::Get()->IsInDesktopMode());
}
return 0;
}
case WM_DPICHANGED:
{
UIManager::Get()->OnDPIChanged(HIWORD(wParam), *(RECT*)lParam);
return 0;
}
case WM_COPYDATA:
{
if (UIManager::Get())
{
MSG pmsg;
//Process all custom window messages posted before this
while (PeekMessage(&pmsg, nullptr, 0xC000, 0xFFFF, PM_REMOVE))
{
UIManager::Get()->HandleIPCMessage(pmsg);
}
MSG wmsg;
wmsg.hwnd = hWnd;
wmsg.message = msg;
wmsg.wParam = wParam;
wmsg.lParam = lParam;
UIManager::Get()->HandleIPCMessage(wmsg);
}
return 0;
}
case WM_SYSCOMMAND:
{
if ((wParam & 0xfff0) == SC_KEYMENU) // Disable ALT application menu
{
return 0;
}
break;
}
case WM_DESTROY:
{
::PostQuitMessage(0);
return 0;
}
}
return ::DefWindowProc(hWnd, msg, wParam, lParam);
}
void InitImGui(HWND hwnd, bool desktop_mode)
{
//Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
ImGuiIO& io = ImGui::GetIO();
//Enable keyboard controls when in desktop mode
if (desktop_mode)
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.IniFilename = nullptr; //We don't need any imgui.ini support
io.ConfigInputTrickleEventQueue = false; //Opt out of input trickling since it doesn't play well with VR scrolling (and lowers responsiveness on certain inputs)
ImGui::ConfigDisableCtrlTab();
//Use system double-click time in desktop mode, but set it to something VR-trigger-friendly otherwise
io.MouseDoubleClickTime = (desktop_mode) ? ::GetDoubleClickTime() / 1000.0f : 0.50f;
io.MouseDoubleClickMaxDist = 16.0f;
//Setup Platform/Renderer bindings
ImGui_ImplWin32_Init(hwnd);
ImGui_ImplDX11_Init(g_pd3dDevice.Get(), g_pd3dDeviceContext.Get());
UIManager::Get()->UpdateStyle();
}
void ProcessCmdline(bool& force_desktop_mode, bool& open_keyboard_editor)
{
//__argv and __argc are global vars set by system
for (UINT i = 0; i < static_cast(__argc); ++i)
{
if ((strcmp(__argv[i], "-DesktopMode") == 0) ||
(strcmp(__argv[i], "--DesktopMode") == 0) ||
(strcmp(__argv[i], "/DesktopMode") == 0))
{
force_desktop_mode = true;
}
else if ((strcmp(__argv[i], "-KeyboardEditor") == 0) ||
(strcmp(__argv[i], "--KeyboardEditor") == 0) ||
(strcmp(__argv[i], "/KeyboardEditor") == 0))
{
open_keyboard_editor = true;
force_desktop_mode = true;
}
}
}
================================================
FILE: src/DesktopPlusUI/DesktopPlusUI.vcxproj
================================================
Debug
x64
Release
x64
{14405CEC-DF3D-435E-8F11-B79BAC56D7D8}
DesktopPlusUI
DesktopPlusUI
Application
true
v142
Unicode
Application
false
v142
true
Unicode
true
..\DesktopPlus\DesktopPlus.ruleset
false
..\DesktopPlus\DesktopPlus.ruleset
Level3
Disabled
LOGURU_FILENAME_WIDTH=30;LOGURU_VERBOSE_SCOPE_ENDINGS=0;DPLUS_UI;DPLUS_SHA=$(DPLUS_SHA);_DEBUG;_WINDOWS;%(PreprocessorDefinitions)
.\imgui_win32_dx11_openvr;.\imgui;.\implot;$(OutDir);..\Shared;..\DesktopPlusUI;..\DesktopPlusWinRT;%(AdditionalIncludeDirectories)
MultiThreadedDebug
26812;%(DisableSpecificWarnings)
true
Windows
true
d3d11.lib;dxgi.lib;dwmapi.lib;openvr_api.lib;gdiplus.lib;shcore.lib;shlwapi.lib;pdh.lib;DesktopPlusWinRT.lib;%(AdditionalDependencies)
$(SolutionDir)Shared;$(OutputPath)
Level3
Full
true
true
LOGURU_FILENAME_WIDTH=30;LOGURU_VERBOSE_SCOPE_ENDINGS=0;DPLUS_UI;DPLUS_SHA=$(DPLUS_SHA);NDEBUG;_WINDOWS;%(PreprocessorDefinitions)
.\imgui_win32_dx11_openvr;.\imgui;.\implot;$(OutDir);..\Shared;..\DesktopPlusUI;..\DesktopPlusWinRT;%(AdditionalIncludeDirectories)
true
MultiThreaded
26812;%(DisableSpecificWarnings)
None
true
Speed
AnySuitable
Windows
true
true
false
d3d11.lib;dxgi.lib;dwmapi.lib;openvr_api.lib;gdiplus.lib;shcore.lib;shlwapi.lib;pdh.lib;DesktopPlusWinRT.lib;%(AdditionalDependencies)
$(SolutionDir)Shared;$(OutputPath)
Pixel
4.0_level_9_1
Pixel
$(OutDir)%(Filename).h
$(OutDir)%(Filename).h
PS
PS
Vertex
Vertex
4.0_level_9_1
$(OutDir)%(Filename).h
$(OutDir)%(Filename).h
VS
VS
================================================
FILE: src/DesktopPlusUI/DesktopPlusUI.vcxproj.filters
================================================
{cf9e2ac6-b93d-4ce4-be2b-2dceec48ea9c}
{02f38889-5b36-40a8-a60d-60d4016baec4}
{2fac41b3-89b4-4cdf-9924-e4d9ae01e9e3}
{accae70c-9225-413f-823d-dd068cbb1b5f}
ImGui
ImGui
ImGui
ImGui
ImGui Win32/DX11/OpenVR
ImGui Win32/DX11/OpenVR
Shared
Shared
Shared
Shared
Shared
Shared
Shared
ImPlot
ImPlot
ImGui
Shared
Shared
Shared
Shared
Shared
Shared
Shared
ImGui
ImGui
ImGui
ImGui
ImGui
ImGui
ImGui Win32/DX11/OpenVR
ImGui Win32/DX11/OpenVR
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
ImPlot
ImPlot
Shared
Shared
Shared
Shared
Shared
Shared
Shared
Shared
ImGui Win32/DX11/OpenVR
ImGui Win32/DX11/OpenVR
================================================
FILE: src/DesktopPlusUI/DesktopPlusUI.vcxproj.user
================================================
false
$(OutputPath)
WindowsLocalDebugger
-DesktopMode
$(OutputPath)
WindowsLocalDebugger
================================================
FILE: src/DesktopPlusUI/FloatingUI.cpp
================================================
#include "FloatingUI.h"
#include "UIManager.h"
#include "OverlayManager.h"
#include "InterprocessMessaging.h"
#include "Util.h"
#include "OpenVRExt.h"
FloatingUI::FloatingUI() : m_OvrlHandleCurrentUITarget(vr::k_ulOverlayHandleInvalid),
m_OvrlIDCurrentUITarget(0),
m_Width(1.0f),
m_Alpha(0.0f),
m_Visible(false),
m_IsSwitchingTarget(false),
m_FadeOutDelayCount(0.0f),
m_AutoFitFrames(0),
m_TheaterOffsetAnimationProgress(0.0f)
{
}
void FloatingUI::Update()
{
if ( (m_Alpha != 0.0f) || (m_Visible) )
{
vr::VROverlayHandle_t ovrl_handle_floating_ui = UIManager::Get()->GetOverlayHandleFloatingUI();
if ( (m_Alpha == 0.0f) && (m_AutoFitFrames == 0) ) //Overlay was hidden before
{
vr::VROverlay()->ShowOverlay(ovrl_handle_floating_ui);
OverlayConfigData& overlay_data = OverlayManager::Get().GetConfigData(m_OvrlIDCurrentUITarget);
//Instantly adjust action bar visibility to overlay state before fading in
if (overlay_data.ConfigBool[configid_bool_overlay_actionbar_enabled])
{
m_WindowActionBar.Show(true);
}
else
{
m_WindowActionBar.Hide(true);
}
//Give ImGui its two auto-fit frames it typically needs to rearrange widgets properly should they have changed from the last time FloatingUI was visible
m_AutoFitFrames = 2;
}
//Alpha fade animation
if ( (!UIManager::Get()->GetRepeatFrame()) && (m_AutoFitFrames == 0) )
{
const float alpha_step = ImGui::GetIO().DeltaTime * 6.0f;
m_Alpha += (m_Visible) ? alpha_step : -alpha_step;
}
if (m_Alpha > 1.0f)
m_Alpha = 1.0f;
else if (m_Alpha < 0.0f)
m_Alpha = 0.0f;
vr::VROverlay()->SetOverlayAlpha(ovrl_handle_floating_ui, m_Alpha);
if ( (m_Alpha == 0.0f) && (m_AutoFitFrames == 0) ) //Overlay was visible before
{
vr::VROverlay()->HideOverlay(ovrl_handle_floating_ui);
m_WindowActionBar.Hide(true);
//In case we were switching targets, reset switching state and target overlay
m_IsSwitchingTarget = false;
m_OvrlHandleCurrentUITarget = vr::k_ulOverlayHandleInvalid;
m_OvrlIDCurrentUITarget = 0;
//Request sync if drag-mode is still active while the UI is disappearing
if (ConfigManager::GetValue(configid_bool_state_overlay_dragmode))
{
IPCManager::Get().PostMessageToDashboardApp(ipcmsg_action, ipcact_overlay_transform_sync, -1);
}
}
else if (m_Visible) //Make sure the input method is always set when visible, even after partial fade-out
{
if (!ConfigManager::GetValue(configid_bool_state_overlay_dragmode_temp))
{
vr::VROverlay()->SetOverlayInputMethod(ovrl_handle_floating_ui, vr::VROverlayInputMethod_Mouse);
}
}
}
if ( (m_Alpha != 0.0f) || (m_AutoFitFrames != 0) )
{
OverlayConfigData& overlay_data = OverlayManager::Get().GetConfigData(m_OvrlIDCurrentUITarget);
//If action bar state was changed
if (overlay_data.ConfigBool[configid_bool_overlay_actionbar_enabled])
{
m_WindowActionBar.Show();
}
else
{
m_WindowActionBar.Hide();
}
m_WindowActionBar.Update(m_OvrlIDCurrentUITarget);
m_WindowMainBar.Update(m_WindowActionBar.GetSize().y, m_OvrlIDCurrentUITarget);
m_WindowOverlayStats.Update(m_WindowMainBar, m_WindowActionBar, m_OvrlIDCurrentUITarget);
if (m_AutoFitFrames > 0)
{
m_AutoFitFrames--;
UIManager::Get()->RepeatFrame();
if (m_AutoFitFrames == 0)
{
m_Alpha += ImGui::GetIO().DeltaTime * 6.0f;
}
}
}
}
void FloatingUI::UpdateUITargetState()
{
UIManager& ui_manager = *UIManager::Get();
vr::VROverlayHandle_t ovrl_handle_floating_ui = ui_manager.GetOverlayHandleFloatingUI();
//Find which overlay is being hovered
vr::VROverlayHandle_t ovrl_handle_hover_target = vr::k_ulOverlayHandleInvalid;
unsigned int ovrl_id_hover_target = k_ulOverlayID_None;
//Also find primary dashboard overlay as fallback to always display when available
vr::VROverlayHandle_t ovrl_handle_primary_dashboard = vr::k_ulOverlayHandleInvalid;
unsigned int ovrl_id_primary_dashboard = k_ulOverlayID_None;
const bool has_pointer_device = (ConfigManager::Get().GetPrimaryLaserPointerDevice() != vr::k_unTrackedDeviceIndexInvalid);
//If previous target overlay is no longer visible
if ( (m_OvrlHandleCurrentUITarget != vr::k_ulOverlayHandleInvalid) && (!vr::VROverlay()->IsOverlayVisible(m_OvrlHandleCurrentUITarget)) )
{
m_FadeOutDelayCount = 100.0f;
//Disable input so the pointer will no longer hit the UI window
vr::VROverlay()->SetOverlayInputMethod(ovrl_handle_floating_ui, vr::VROverlayInputMethod_None);
}
else if ( (has_pointer_device) && (ConfigManager::Get().IsLaserPointerTargetOverlay(ovrl_handle_floating_ui)) ) //Use as target Floating UI if it's hovered
{
ovrl_handle_hover_target = ovrl_handle_floating_ui;
}
//Don't show while reordering overlays since config changes may cause the UI to jump around while doing that
if ( (ovrl_handle_hover_target == vr::k_ulOverlayHandleInvalid) && (!ConfigManager::GetValue(configid_bool_state_overlay_dragmode_temp)) &&
(!UIManager::Get()->GetOverlayBarWindow().IsDraggingOverlayButtons()) )
{
if (has_pointer_device)
{
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i)
{
const OverlayConfigData& data = OverlayManager::Get().GetConfigData(i);
vr::VROverlayHandle_t ovrl_handle = data.ConfigHandle[configid_handle_overlay_state_overlay_handle];
if (ovrl_handle != vr::k_ulOverlayHandleInvalid)
{
//Check if this is the primary dashboard overlay
if ( (ovrl_id_primary_dashboard == k_ulOverlayID_None) && (data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_dashboard) &&
(data.ConfigInt[configid_int_overlay_display_mode] != ovrl_dispmode_scene) )
{
//First dashboard origin with non-scene display mode is considered to be the primary dashboard overlay, but only really use it if enabled with FloatingUI on and in dashboard
if ( (data.ConfigBool[configid_bool_overlay_enabled]) && (data.ConfigBool[configid_bool_overlay_floatingui_enabled]) && (UIManager::Get()->IsOverlayBarOverlayVisible()) &&
(vr::VROverlay()->IsOverlayVisible(ovrl_handle)) )
{
ovrl_handle_primary_dashboard = ovrl_handle;
ovrl_id_primary_dashboard = i;
}
}
//Check if this the hover target overlay
if ( (data.ConfigBool[configid_bool_overlay_floatingui_enabled]) && (ConfigManager::Get().IsLaserPointerTargetOverlay(ovrl_handle)) )
{
ovrl_handle_hover_target = ovrl_handle;
ovrl_id_hover_target = i;
//Break here unless we still need to find the primary dashboard overlay
if (ovrl_id_primary_dashboard != k_ulOverlayID_None)
{
break;
}
}
}
}
//Use fallback primary dashboard overlay if no hover target was found
if ( (ovrl_handle_hover_target == vr::k_ulOverlayHandleInvalid) && (m_FadeOutDelayCount == 0) &&
((m_OvrlHandleCurrentUITarget == vr::k_ulOverlayHandleInvalid) || (m_OvrlHandleCurrentUITarget == ovrl_handle_primary_dashboard)) )
{
ovrl_handle_hover_target = ovrl_handle_primary_dashboard;
ovrl_id_hover_target = ovrl_id_primary_dashboard;
}
}
}
bool is_newly_visible = false;
//Check if we're switching from another active overlay hover target, in which case we want to fade out completely first
if ( (m_OvrlHandleCurrentUITarget != vr::k_ulOverlayHandleInvalid) && (ovrl_handle_hover_target != vr::k_ulOverlayHandleInvalid) && (ovrl_handle_hover_target != ovrl_handle_floating_ui) &&
(ovrl_handle_hover_target != m_OvrlHandleCurrentUITarget) )
{
m_IsSwitchingTarget = true;
m_Visible = false;
UIManager::Get()->GetIdleState().AddActiveTime();
return;
}
else if ( (ovrl_handle_hover_target != ovrl_handle_floating_ui) && (ovrl_handle_hover_target != vr::k_ulOverlayHandleInvalid) )
{
if ( (!m_Visible) && (m_Alpha == 0.0f) )
{
is_newly_visible = true;
UIManager::Get()->GetIdleState().AddActiveTime();
}
m_OvrlHandleCurrentUITarget = ovrl_handle_hover_target;
m_OvrlIDCurrentUITarget = ovrl_id_hover_target;
m_Visible = true;
}
//If there is an active hover target overlay, position the floating UI
if (m_OvrlHandleCurrentUITarget != vr::k_ulOverlayHandleInvalid)
{
OverlayConfigData& overlay_data = OverlayManager::Get().GetConfigData(m_OvrlIDCurrentUITarget);
Matrix4 matrix = OverlayManager::Get().GetOverlayCenterBottomTransform(m_OvrlIDCurrentUITarget, m_OvrlHandleCurrentUITarget);
//If the Floating UI is just appearing, adjust overlay size based on the distance between HMD and overlay
if (is_newly_visible)
{
bool use_fixed_size = false;
//Use fixed size when using primary dashboard overlay fallback and distance to dashboard is lower than 0.25m
if (ovrl_handle_primary_dashboard == m_OvrlHandleCurrentUITarget)
{
//Use relative transform data here as dashboard transform can be unreliable during launch
const float distance = overlay_data.ConfigTransform.getTranslation().distance({0.0f, 0.0f, 0.0f});
use_fixed_size = (distance < 0.25f);
m_Width = 1.2f;
vr::VROverlay()->SetOverlayWidthInMeters(ovrl_handle_floating_ui, m_Width);
}
else if (overlay_data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_theater_screen) //Fixed size for theater screen too
{
m_Width = 3.0f;
vr::VROverlay()->SetOverlayWidthInMeters(ovrl_handle_floating_ui, m_Width);
}
else
{
vr::TrackedDevicePose_t poses[vr::k_unTrackedDeviceIndex_Hmd + 1];
vr::VRSystem()->GetDeviceToAbsoluteTrackingPose(vr::TrackingUniverseStanding, vr::IVRSystemEx::GetTimeNowToPhotons(), poses, vr::k_unTrackedDeviceIndex_Hmd + 1);
if (poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid)
{
Matrix4 mat_hmd = poses[vr::k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking;
float distance = matrix.getTranslation().distance(mat_hmd.getTranslation());
m_Width = 0.66f + (0.5f * distance);
vr::VROverlay()->SetOverlayWidthInMeters(ovrl_handle_floating_ui, m_Width);
}
}
}
//Move down by Floating UI height
const DPRect& rect_floating_ui = UITextureSpaces::Get().GetRect(ui_texspace_floating_ui);
const float floating_ui_height_m = m_Width * ((float)rect_floating_ui.GetHeight() / (float)rect_floating_ui.GetWidth());
matrix.translate_relative(0.0f, -floating_ui_height_m / 3.0f, 0.0f);
//Additional offset for theater screen
if (overlay_data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_theater_screen)
{
//SteamVR's control bar is only shown while system laser pointer is active, so only add offset if that's the case
const bool add_offset = vr::IVROverlayEx::IsSystemLaserPointerActive();
if (is_newly_visible)
{
m_TheaterOffsetAnimationProgress = (add_offset) ? 1.0f : 0.0f;
}
else //Also animate this
{
const float time_step = ImGui::GetIO().DeltaTime * 6.0f;
m_TheaterOffsetAnimationProgress += (add_offset) ? time_step : -time_step;
if (m_TheaterOffsetAnimationProgress > 1.0f)
m_TheaterOffsetAnimationProgress = 1.0f;
else if (m_TheaterOffsetAnimationProgress < 0.0f)
m_TheaterOffsetAnimationProgress = 0.0f;
}
matrix.translate_relative(0.0f, smoothstep(m_TheaterOffsetAnimationProgress, 0.0f, -0.29f), 0.0f);
}
//Don't update position if dummy transform is unstable unless it's target is not primary dashboard overlay or we're newly appearing
if ( (is_newly_visible) || (!UIManager::Get()->IsDummyOverlayTransformUnstable()) || (ovrl_id_hover_target != ovrl_id_primary_dashboard) )
{
//Only set transform and add active time if we actually need to move the overlay
if (matrix != m_TransformLast)
{
vr::HmdMatrix34_t hmd_matrix = matrix.toOpenVR34();
vr::VROverlay()->SetOverlayTransformAbsolute(ovrl_handle_floating_ui, vr::TrackingUniverseStanding, &hmd_matrix);
UIManager::Get()->GetIdleState().AddActiveTime(100);
m_TransformLast = matrix;
}
}
//Set floating UI curvature based on target overlay curvature
if (overlay_data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_theater_screen)
{
//Set curvature to 0 in this case, as theater screen curve is controlled by SteamVR and not query-able)
vr::VROverlay()->SetOverlayCurvature(ovrl_handle_floating_ui, 0.0f);
}
else
{
float curvature = 0.0f;
vr::VROverlay()->GetOverlayCurvature(m_OvrlHandleCurrentUITarget, &curvature);
float overlay_width = overlay_data.ConfigFloat[configid_float_overlay_width];
vr::VROverlay()->SetOverlayCurvature(ovrl_handle_floating_ui, curvature * (m_Width / overlay_width) );
}
}
if ( (ovrl_handle_hover_target == vr::k_ulOverlayHandleInvalid) || (m_OvrlHandleCurrentUITarget == vr::k_ulOverlayHandleInvalid) ) //If not even the UI itself is being hovered, fade out
{
if (m_Visible)
{
//Don't fade out if this is the theater screen overlay and the systemui is hovered
//This does have false positives when really trying (systemui is used for many things), but it's better not to fade out when SteamVR's overlay controls are hovered
bool blocked_by_systemui = false;
if (m_OvrlHandleCurrentUITarget != vr::k_ulOverlayHandleInvalid)
{
vr::VROverlayHandle_t ovrl_handle_systemui;
vr::VROverlay()->FindOverlay("system.systemui", &ovrl_handle_systemui);
if (ovrl_handle_systemui != vr::k_ulOverlayHandleInvalid)
{
const OverlayConfigData& overlay_data = OverlayManager::Get().GetConfigData(m_OvrlIDCurrentUITarget);
if (overlay_data.ConfigInt[configid_int_overlay_origin] == ovrl_origin_theater_screen)
{
blocked_by_systemui = ConfigManager::Get().IsLaserPointerTargetOverlay(ovrl_handle_systemui);
}
}
}
if (!blocked_by_systemui)
{
m_FadeOutDelayCount += ImGui::GetIO().DeltaTime;
//Delay normal fade in order to not flicker when switching hover target between mirror overlay and floating UI (or don't while reordering overlays)
if ((m_FadeOutDelayCount > 0.8f) || (UIManager::Get()->GetOverlayBarWindow().IsDraggingOverlayButtons()))
{
//Hide
m_Visible = false;
m_FadeOutDelayCount = 0.0f;
}
}
UIManager::Get()->GetIdleState().AddActiveTime();
}
else if (m_Alpha == 0.0f)
{
m_FadeOutDelayCount = 0.0f;
}
}
else
{
m_Visible = true;
}
//Update config state if it changed. This state is only set if the Floating UI itself is the hover target
const int target_overlay_id_new = ((m_OvrlIDCurrentUITarget != k_ulOverlayID_None) && (ovrl_handle_hover_target == ovrl_handle_floating_ui)) ? (int)m_OvrlIDCurrentUITarget : -1;
int& target_overlay_id_config = ConfigManager::GetRef(configid_int_state_interface_floating_ui_hovered_id);
if (target_overlay_id_config != target_overlay_id_new)
{
target_overlay_id_config = target_overlay_id_new;
IPCManager::Get().PostConfigMessageToDashboardApp(configid_int_state_interface_floating_ui_hovered_id, target_overlay_id_config);
}
}
bool FloatingUI::IsVisible() const
{
return ((m_Visible) || (m_Alpha != 0.0f));
}
float FloatingUI::GetAlpha() const
{
return m_Alpha;
}
WindowFloatingUIMainBar& FloatingUI::GetMainBarWindow()
{
return m_WindowMainBar;
}
WindowFloatingUIActionBar& FloatingUI::GetActionBarWindow()
{
return m_WindowActionBar;
}
================================================
FILE: src/DesktopPlusUI/FloatingUI.h
================================================
#pragma once
#include "WindowFloatingUIBar.h"
#include "openvr.h"
#include "Matrices.h"
class FloatingUI
{
private:
WindowFloatingUIMainBar m_WindowMainBar;
WindowFloatingUIActionBar m_WindowActionBar;
WindowFloatingUIOverlayStats m_WindowOverlayStats;
vr::VROverlayHandle_t m_OvrlHandleCurrentUITarget;
unsigned int m_OvrlIDCurrentUITarget;
float m_Width;
float m_Alpha;
bool m_Visible;
bool m_IsSwitchingTarget;
float m_FadeOutDelayCount;
int m_AutoFitFrames;
Matrix4 m_TransformLast;
float m_TheaterOffsetAnimationProgress;
public:
FloatingUI();
void Update();
void UpdateUITargetState();
bool IsVisible() const;
float GetAlpha() const;
WindowFloatingUIMainBar& GetMainBarWindow();
WindowFloatingUIActionBar& GetActionBarWindow();
};
================================================
FILE: src/DesktopPlusUI/FloatingWindow.cpp
================================================
#include "FloatingWindow.h"
#include "UIManager.h"
#include "OverlayManager.h"
#include "InterprocessMessaging.h"
#include "Util.h"
#include "OpenVRExt.h"
#include "ImGuiExt.h"
#include "imgui_internal.h"
FloatingWindow::FloatingWindow() : m_OvrlWidth(1.0f),
m_OvrlWidthMax(FLT_MAX),
m_Alpha(0.0f),
m_OvrlVisible(false),
m_IsTransitionFading(false),
m_OverlayStateCurrentID(floating_window_ovrl_state_dashboard_tab),
m_OverlayStateCurrent(&m_OverlayStateDashboardTab),
m_OverlayStatePending(&m_OverlayStateFading),
m_WindowTitleStrID(tstr_NONE),
m_WindowIcon(tmtex_icon_xsmall_desktop_none),
m_WindowIconWin32IconCacheID(-1),
m_AllowRoomUnpinning(false),
m_DragOrigin(ovrl_origin_dplus_tab),
m_TitleBarMinWidth(64.0f),
m_TitleBarTitleMaxWidth(-1.0f),
m_TitleBarTitleIconAlpha(1.0f),
m_IsTitleBarHovered(false),
m_IsTitleIconClicked(false),
m_HasAppearedOnce(false),
m_IsWindowAppearing(false),
m_CompactTableHeaderHeight(0.0f)
{
m_Pos.x = FLT_MIN;
m_WindowFlags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus;
m_WindowID = "###" + std::to_string((unsigned long long)this);
m_OverlayStateDashboardTab.TransformAbs.zero();
m_OverlayStateRoom.TransformAbs.zero();
}
void FloatingWindow::WindowUpdateBase()
{
if ( ((!UIManager::Get()->GetRepeatFrame()) || (m_Alpha == 0.0f)) && ((m_Alpha != 0.0f) || (m_OverlayStateCurrent->IsVisible) || (m_IsTransitionFading)) )
{
const float alpha_prev = m_Alpha;
const float alpha_step = ImGui::GetIO().DeltaTime * 6.0f;
//Alpha fade animation
m_Alpha += ((m_OverlayStateCurrent->IsVisible) && (!m_IsTransitionFading)) ? alpha_step : -alpha_step;
if (m_Alpha > 1.0f)
m_Alpha = 1.0f;
else if (m_Alpha < 0.0f)
m_Alpha = 0.0f;
//Use overlay alpha when not in desktop mode for better blending
if ( (!UIManager::Get()->IsInDesktopMode()) && (alpha_prev != m_Alpha) )
vr::VROverlay()->SetOverlayAlpha(GetOverlayHandle(), m_Alpha);
if (m_Alpha == 0.0f)
{
//Not the best spot, but it can be difficult to cancel an active overlay hightlight when disappearing since the window code isn't running anymore
//So instead we always cancel the current highlight here, which shouldn't be problematic
UIManager::Get()->HighlightOverlay(k_ulOverlayID_None);
//Finish transition fade if one's active
if (m_IsTransitionFading)
{
OverlayStateSwitchFinish();
}
}
}
if (m_Alpha == 0.0f)
return;
if (UIManager::Get()->IsInDesktopMode())
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, m_Alpha);
ImGuiIO& io = ImGui::GetIO();
ImGuiStyle& style = ImGui::GetStyle();
if (m_Pos.x == FLT_MIN)
{
//Expects m_Size to be padded 2 pixels around actual texture space, so -4 on each axis
m_Pos = {2.0f, (io.DisplaySize.y - m_Size.y) - 2.0f};
}
ImGui::SetNextWindowPos(m_Pos, ImGuiCond_Always, m_PosPivot);
ImGui::SetNextWindowSizeConstraints({m_TitleBarMinWidth, 4.0f}, m_Size);
ImGui::SetNextWindowScroll({0.0f, -1.0f}); //Prevent real horizontal scrolling from happening
ImGuiWindowFlags flags = m_WindowFlags;
if (!m_OverlayStateCurrent->IsVisible)
flags |= ImGuiWindowFlags_NoInputs;
ImGui::Begin(m_WindowID.c_str(), nullptr, flags);
m_IsTitleBarHovered = ((ImGui::IsItemHovered()) && (m_OverlayStateCurrent->IsVisible)); //Current item is the title bar (needs to be checked before BeginTitleBar())
m_IsWindowAppearing = ImGui::IsWindowAppearing();
//Title bar
ImVec4 title_rect = ImGui::BeginTitleBar();
//Icon and title text
ImVec2 img_size_line_height = {ImGui::GetTextLineHeight(), ImGui::GetTextLineHeight()};
ImVec2 img_size, img_uv_min, img_uv_max;
if (m_WindowIconWin32IconCacheID == -1)
{
TextureManager::Get().GetTextureInfo(m_WindowIcon, img_size, img_uv_min, img_uv_max);
}
else
{
TextureManager::Get().GetWindowIconTextureInfo(m_WindowIconWin32IconCacheID, img_size, img_uv_min, img_uv_max);
}
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, m_TitleBarTitleIconAlpha);
ImGui::Image(io.Fonts->TexID, img_size_line_height, img_uv_min, img_uv_max);
m_IsTitleIconClicked = ImGui::IsItemClicked();
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
ImVec2 clip_end = ImGui::GetCursorScreenPos();
clip_end.x += m_TitleBarTitleMaxWidth;
clip_end.y += ImGui::GetFrameHeight();
ImGui::PushClipRect(ImGui::GetCursorScreenPos(), clip_end, true);
ImGui::TextUnformatted( (m_WindowTitleStrID == tstr_NONE) ? m_WindowTitle.c_str() : TranslationManager::GetString(m_WindowTitleStrID) );
ImGui::PopClipRect();
ImGui::PopStyleVar();
float title_text_width = ImGui::GetItemRectSize().x;
//Right end of title bar
ImGui::PushStyleColor(ImGuiCol_Button, 0);
static float b_width = 0.0f;
ImGui::SetCursorScreenPos({title_rect.z - b_width - style.ItemInnerSpacing.x, title_rect.y + style.FramePadding.y});
//Buttons
TextureManager::Get().GetTextureInfo((m_OverlayStateCurrent->IsPinned) ? tmtex_icon_xxsmall_unpin : tmtex_icon_xxsmall_pin, img_size, img_uv_min, img_uv_max);
ImGui::BeginGroup();
const bool disable_pinning = ( (!m_AllowRoomUnpinning) && (m_OverlayStateCurrentID == floating_window_ovrl_state_room) );
if (disable_pinning)
ImGui::PushItemDisabled();
if (ImGui::ImageButton("PinButton", io.Fonts->TexID, img_size, img_uv_min, img_uv_max, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)))
{
SetPinned(!IsPinned());
OnWindowPinButtonPressed();
}
ImGui::SameLine(0.0f, 0.0f);
if (disable_pinning)
ImGui::PopItemDisabled();
TextureManager::Get().GetTextureInfo(tmtex_icon_xxsmall_close, img_size, img_uv_min, img_uv_max);
if (ImGui::ImageButton("CloseButton", io.Fonts->TexID, img_size, img_uv_min, img_uv_max, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)))
{
Hide();
OnWindowCloseButtonPressed();
}
ImGui::EndGroup();
m_IsTitleBarHovered = ( (m_IsTitleBarHovered) && (!ImGui::IsItemHovered()) ); //Title was hovered and no title bar element is hovered
b_width = ImGui::GetItemRectSize().x;
ImGui::PopStyleColor();
//Calculate title bar constraints
m_TitleBarMinWidth = img_size_line_height.x + b_width + (style.ItemSpacing.x * 2.0f);
m_TitleBarTitleMaxWidth = ImGui::GetWindowSize().x - m_TitleBarMinWidth;
m_TitleBarMinWidth += style.ItemSpacing.x * 2.0f;
//Shorten title bar string if it doesn't fit (this is destructive, but doesn't matter for the windows using this)
if ((m_WindowTitleStrID == tstr_NONE) && (title_text_width > std::max(ImGui::GetFontSize(), m_TitleBarTitleMaxWidth) ))
{
//Don't attempt to shorten the string during repeat frames as the size can still be adjusting in corner cases and throw us into a loop
if (!UIManager::Get()->GetRepeatFrame())
{
m_WindowTitle = ImGui::StringEllipsis(m_WindowTitle.c_str(), m_TitleBarTitleMaxWidth);
//Repeat frame to not make title shortening visible
UIManager::Get()->RepeatFrame();
}
}
//Title bar dragging
if ( (m_OverlayStateCurrent->IsVisible) && (m_IsTitleBarHovered) && (UIManager::Get()->IsOpenVRLoaded()) )
{
if ( (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) && (!UIManager::Get()->GetOverlayDragger().IsDragActive()) && (!UIManager::Get()->GetOverlayDragger().IsDragGestureActive()) )
{
UIManager::Get()->GetOverlayDragger().DragStart(GetOverlayHandle(), m_DragOrigin);
UIManager::Get()->GetOverlayDragger().DragSetMaxWidth(m_OvrlWidthMax);
}
else if ( (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) && (!UIManager::Get()->GetOverlayDragger().IsDragActive()) && (!UIManager::Get()->GetOverlayDragger().IsDragGestureActive()) )
{
UIManager::Get()->GetOverlayDragger().DragGestureStart(GetOverlayHandle(), m_DragOrigin);
UIManager::Get()->GetOverlayDragger().DragSetMaxWidth(m_OvrlWidthMax);
}
}
ImGui::EndTitleBar();
//To appease boundary extension error check
ImGui::Dummy({0.0f, 0.0f});
ImGui::SameLine(0.0f, 0.0f);
//Window content
WindowUpdate();
//Hack to work around ImGui's auto-sizing quirks. Just checking for ImGui::IsWindowAppearing() and using alpha 0 then doesn't help on its own so this is the next best thing
if (!m_HasAppearedOnce)
{
if (ImGui::IsWindowAppearing())
{
UIManager::Get()->RepeatFrame();
}
else
{
m_HasAppearedOnce = true;
}
}
//Blank space drag
if ( (ConfigManager::GetValue(configid_bool_interface_blank_space_drag_enabled)) && (m_OverlayStateCurrent->IsVisible) && (UIManager::Get()->IsOpenVRLoaded()) && (!ImGui::IsAnyItemHovered()) &&
(!IsVirtualWindowItemHovered()) && (ImGui::IsWindowHovered(ImGuiHoveredFlags_ChildWindows)) )
{
if ( (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) && (!UIManager::Get()->GetOverlayDragger().IsDragActive()) && (!UIManager::Get()->GetOverlayDragger().IsDragGestureActive()) )
{
UIManager::Get()->GetOverlayDragger().DragStart(GetOverlayHandle(), m_DragOrigin);
UIManager::Get()->GetOverlayDragger().DragSetMaxWidth(m_OvrlWidthMax);
}
else if ( (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) && (!UIManager::Get()->GetOverlayDragger().IsDragActive()) && (!UIManager::Get()->GetOverlayDragger().IsDragGestureActive()) )
{
UIManager::Get()->GetOverlayDragger().DragGestureStart(GetOverlayHandle(), m_DragOrigin);
UIManager::Get()->GetOverlayDragger().DragSetMaxWidth(m_OvrlWidthMax);
}
}
ImGui::End();
if (UIManager::Get()->IsInDesktopMode())
ImGui::PopStyleVar(); //ImGuiStyleVar_Alpha
}
void FloatingWindow::OverlayStateSwitchCurrent(bool use_dashboard_tab)
{
if (m_OverlayStateCurrent == &m_OverlayStateFading)
return;
//Use transition fade if the overlay position will change
m_OverlayStatePending = (use_dashboard_tab) ? &m_OverlayStateDashboardTab : &m_OverlayStateRoom;
if ( (m_OverlayStateCurrent->TransformAbs != m_OverlayStatePending->TransformAbs) || (!m_OverlayStatePending->IsVisible) )
{
m_IsTransitionFading = true;
m_OverlayStateFading = *m_OverlayStateCurrent;
m_OverlayStateCurrent = &m_OverlayStateFading;
}
else
{
OverlayStateSwitchFinish();
}
}
void FloatingWindow::OverlayStateSwitchFinish()
{
m_IsTransitionFading = false;
m_OverlayStateCurrent = m_OverlayStatePending;
m_OverlayStateCurrentID = (m_OverlayStateCurrent == &m_OverlayStateDashboardTab) ? floating_window_ovrl_state_dashboard_tab : floating_window_ovrl_state_room;
ApplyCurrentOverlayState();
if (m_OverlayStateCurrent->IsVisible)
{
Show();
}
}
void FloatingWindow::OnWindowPinButtonPressed()
{
//If pin button pressed from dashboard tab, also make it visible for the room state so it can be used there
if ( (m_OverlayStateCurrentID == floating_window_ovrl_state_dashboard_tab) && (m_OverlayStateDashboardTab.IsPinned) )
{
m_OverlayStateRoom.IsVisible = true;
}
}
void FloatingWindow::OnWindowCloseButtonPressed()
{
//Do nothing by default
}
bool FloatingWindow::IsVirtualWindowItemHovered() const
{
return false;
}
void FloatingWindow::HelpMarker(const char* desc, const char* marker_str) const
{
ImGui::TextDisabled(marker_str);
if (ImGui::IsItemHovered())
{
static float last_y_offset = FLT_MIN; //Try to avoid getting having the tooltip cover the marker... the way it's done here is a bit messy to be fair
const ImGuiStyle& style = ImGui::GetStyle();
float pos_y = ImGui::GetItemRectMax().y + style.ItemSpacing.y;
bool is_invisible = false;
if (last_y_offset == FLT_MIN) //Same as IsWindowAppearing except the former doesn't work before beginning the window which is too late for the position...
{
//We need to create the tooltip window for size calculations to happen but also don't want to see it... so alpha 0, even if wasteful
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.0f);
is_invisible = true;
}
else
{
ImGui::SetNextWindowPos(ImVec2(m_Pos.x, m_Pos.y + m_Size.y + last_y_offset), 0, {0.0f, 1.0f});
ImGui::SetNextWindowSize(ImVec2(m_Size.x, -1.0f));
}
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(m_Size.x - style.WindowPadding.x);
ImGui::TextUnformatted(desc);
ImGui::PopTextWrapPos();
if (ImGui::IsWindowAppearing()) //New tooltip, reset offset
{
//The window size isn't available in this frame yet, so we'll have to skip the having it visible for one more frame and then act on it
last_y_offset = FLT_MIN;
}
else
{
if (pos_y + ImGui::GetWindowSize().y > m_Pos.y + m_Size.y) //If it would cover the marker
{
if (UIManager::Get()->IsInDesktopMode())
{
last_y_offset = -m_Size.y + ImGui::GetWindowSize().y + UIManager::Get()->GetDesktopModeWindow().GetTitleBarRect().w;
}
else
{
last_y_offset = -m_Size.y + ImGui::GetWindowSize().y + ImGui::GetFontSize() + style.FramePadding.y * 2.0f;
}
}
else //Use normal pos
{
last_y_offset = 0.0f;
}
}
ImGui::EndTooltip();
if (is_invisible)
ImGui::PopStyleVar(); //ImGuiStyleVar_Alpha
}
}
void FloatingWindow::UpdateLimiterSetting(bool is_override) const
{
const ImGuiStyle& style = ImGui::GetStyle();
const ConfigID_Int configid_mode = (is_override) ? configid_int_overlay_update_limit_override_mode : configid_int_performance_update_limit_mode;
const ConfigID_Int configid_fps = (is_override) ? configid_int_overlay_update_limit_override_fps : configid_int_performance_update_limit_fps;
const ConfigID_Float configid_ms = (is_override) ? configid_float_overlay_update_limit_override_ms : configid_float_performance_update_limit_ms;
int& update_limit_mode = ConfigManager::GetRef(configid_mode);
bool limit_updates = (update_limit_mode != update_limit_mode_off);
if (ConfigManager::GetValue(configid_bool_interface_show_advanced_settings)) //Advanced view, choose limiter mode
{
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(TranslationManager::GetString((is_override) ? tstr_SettingsPerformanceUpdateLimiterModeOverride : tstr_SettingsPerformanceUpdateLimiterMode));
if (update_limit_mode == update_limit_mode_ms)
{
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
HelpMarker(TranslationManager::GetString(tstr_SettingsPerformanceUpdateLimiterModeMSTip));
}
else if (is_override)
{
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
HelpMarker(TranslationManager::GetString(tstr_SettingsPerformanceUpdateLimiterOverrideTip));
}
ImGui::NextColumn();
//Manually set up combo items here to support different first string for override setting
const TRMGRStrID combo_strings[] =
{
(is_override) ? tstr_SettingsPerformanceUpdateLimiterModeOffOverride : tstr_SettingsPerformanceUpdateLimiterModeOff,
tstr_SettingsPerformanceUpdateLimiterModeMS,
tstr_SettingsPerformanceUpdateLimiterModeFPS
};
update_limit_mode = clamp(update_limit_mode, 0, IM_ARRAYSIZE(combo_strings) - 1); //Avoid accessing past limits with invalid values
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::BeginComboAnimated("##ComboUpdateLimitMode", TranslationManager::GetString(combo_strings[update_limit_mode]) ))
{
int i = 0;
for (const auto& item : combo_strings)
{
if (ImGui::Selectable(TranslationManager::GetString(item), (update_limit_mode == i)))
{
update_limit_mode = i;
IPCManager::Get().PostConfigMessageToDashboardApp(configid_mode, update_limit_mode);
}
++i;
}
ImGui::EndCombo();
}
ImGui::NextColumn();
ImGui::NextColumn();
}
else //Simple view, only switch between off and fps mode (still shows ms below if previously set active)
{
if (ImGui::Checkbox(TranslationManager::GetString((is_override) ? tstr_SettingsPerformanceUpdateLimiterOverride : tstr_SettingsPerformanceUpdateLimiter), &limit_updates))
{
update_limit_mode = (limit_updates) ? update_limit_mode_fps : update_limit_mode_off;
IPCManager::Get().PostConfigMessageToDashboardApp(configid_mode, update_limit_mode);
}
if (is_override)
{
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
HelpMarker(TranslationManager::GetString(tstr_SettingsPerformanceUpdateLimiterOverrideTip));
}
ImGui::NextColumn();
}
if (update_limit_mode == update_limit_mode_ms)
{
VRKeyboard& vr_keyboard = UIManager::Get()->GetVRKeyboard();
float& update_limit_ms = ConfigManager::Get().GetRef(configid_ms);
vr_keyboard.VRKeyboardInputBegin( ImGui::SliderWithButtonsGetSliderID("UpdateLimitMS") );
if (ImGui::SliderWithButtonsFloat("UpdateLimitMS", update_limit_ms, 0.5f, 0.05f, 0.0f, 100.0f, "%.2f ms", ImGuiSliderFlags_Logarithmic))
{
if (update_limit_ms < 0.0f)
update_limit_ms = 0.0f;
IPCManager::Get().PostConfigMessageToDashboardApp(configid_ms, update_limit_ms);
}
vr_keyboard.VRKeyboardInputEnd();
}
else
{
if (!limit_updates)
ImGui::PushItemDisabled();
int& update_limit_fps = ConfigManager::Get().GetRef(configid_fps);
const int update_limit_fps_max = 9;
if (ImGui::SliderWithButtonsInt("UpdateLimitFPS", update_limit_fps, 1, 1, 0, update_limit_fps_max, "##%d", ImGuiSliderFlags_NoInput, nullptr,
TranslationManager::Get().GetFPSLimitString(update_limit_fps)))
{
update_limit_fps = clamp(update_limit_fps, 0, update_limit_fps_max);
IPCManager::Get().PostConfigMessageToDashboardApp(configid_fps, update_limit_fps);
}
if (!limit_updates)
ImGui::PopItemDisabled();
}
ImGui::NextColumn();
}
bool FloatingWindow::InputOverlayTags(const char* str_id, char* buffer_tags, size_t buffer_tags_size, FloatingWindowInputOverlayTagsState& state, int clip_parent_depth, bool show_auto_tags)
{
static const int single_tag_buffer_size = IM_ARRAYSIZE(state.TagEditBuffer);
ImGuiStyle& style = ImGui::GetStyle();
float widget_width = 0.0f;
bool is_widget_hovered = false;
float child_height = ImGui::GetTextLineHeight() + style.FramePadding.y * 2.0f;
child_height += ImGui::GetTextLineHeightWithSpacing() * (state.ChildHeightLines - 1.0f);
ImGui::PushID(str_id);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, style.FramePadding);
if (ImGui::BeginChild("InputOverlayTags", {-style.ChildBorderSize, child_height}, ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened))
{
ImGui::PopStyleVar();
state.ChildHeightLines = 1.0f;
widget_width = ImGui::GetWindowWidth();
is_widget_hovered = ImGui::IsWindowHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
//Split input string into individual tags and show a small button for each
const char* str = buffer_tags;
const char* str_end = str + strlen(str);
const char* tag_start = str;
const char* tag_end = nullptr;
char buffer_single_tag[single_tag_buffer_size] = "";
int tag_id = 0;
while (tag_start < str_end)
{
tag_end = (const char*)memchr(tag_start, ' ', str_end - tag_start);
if (tag_end == nullptr)
tag_end = str_end;
size_t length = tag_end - tag_start;
//Break if tag doesn't fit (would be comically long though)
if (length >= single_tag_buffer_size)
break;
if (length > 0)
{
memcpy(buffer_single_tag, tag_start, length);
buffer_single_tag[length] = '\0';
if (tag_id != 0)
{
ImVec2 text_size = ImGui::CalcTextSize(buffer_single_tag);
text_size.x += style.ItemSpacing.x;
if (text_size.x > ImGui::GetContentRegionAvail().x)
{
ImGui::NewLine();
state.ChildHeightLines += 1.0f;
}
}
ImGui::PushID(tag_id);
if (ImGui::SmallButton(buffer_single_tag))
{
//Copy tag into edit buffer
memcpy(state.TagEditBuffer, buffer_single_tag, single_tag_buffer_size);
//Also keep a copy to put back when canceling
state.TagEditOrigStr = state.TagEditBuffer;
//Remove tag from full string
std::string str_tags = buffer_tags;
str_tags.erase(tag_start - str, length);
StringReplaceAll(str_tags, " ", " "); //Clean up double whitespace separators, no matter where they came from
//Cleanup stray space at the beginning too
if ((!str_tags.empty()) && (str_tags[0] == ' '))
{
str_tags.erase(0, 1);
}
//Copy back to buffer
size_t copied_length = str_tags.copy(buffer_tags, buffer_tags_size - 1);
buffer_tags[copied_length] = '\0';
//Open popup to allow editing
state.PopupShow = true;
state.FocusTextInput = true;
//We modified the buffer we're looping over, get out of here
UIManager::Get()->RepeatFrame(5); //Prevent hover flicker from the button that take this one's place
ImGui::PopID();
ImGui::EndChild();
ImGui::PopID();
return true;
}
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
ImGui::PopID();
tag_id++;
}
tag_start = tag_end + 1;
}
if (tag_id != 0)
{
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
ImVec2 text_size = ImGui::CalcTextSize("+");
text_size.x += style.ItemSpacing.x;
if (text_size.x > ImGui::GetContentRegionAvail().x)
{
ImGui::NewLine();
state.ChildHeightLines += 1.0f;
}
}
//There are two plus-shaped buttons on the screen when the popup is up.
//This one will just clear the text input then and might be accidentally pressed instead of the bigger one. Disable it to prevent accidents
//Similar thing applies to the tag buttons themselves, but that behavior is at least useful for editing there
const bool disable_add = state.PopupShow;
if (disable_add)
{
ImGui::PushItemDisabledNoVisual();
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
}
//Add some frame padding in VR mode only to have a more square-ish button that is also easier to hit
//Due to how we do style-scaling this isn't necessary in desktop mode
if (!UIManager::Get()->IsInDesktopMode())
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, {style.FramePadding.x * 1.5f, 0.0f});
if (ImGui::SmallButton("+##AddTag"))
{
state.TagEditBuffer[0] = '\0';
state.TagEditOrigStr = state.TagEditBuffer;
state.PopupShow = true;
state.FocusTextInput = true;
}
if (!UIManager::Get()->IsInDesktopMode())
ImGui::PopStyleVar();
if (disable_add)
{
ImGui::PopStyleColor();
ImGui::PopItemDisabledNoVisual();
}
}
else
{
ImGui::PopStyleVar(); //ImGuiStyleVar_WindowPadding
}
//Avoid scrollbar flicker while figuring out child window size
if ((ImGui::IsAnyScrollBarVisible()) && (state.ChildHeightLines <= 3.0f))
{
UIManager::Get()->RepeatFrame();
}
//Show up to 3 lines before adding a scrollbar
state.ChildHeightLines = std::min(state.ChildHeightLines, 3.0f);
ImGui::EndChild();
//--Popup Window
if (!state.PopupShow)
{
ImGui::PopID();
return false;
}
ImGuiIO& io = ImGui::GetIO();
VRKeyboard& vr_keyboard = UIManager::Get()->GetVRKeyboard();
const float pos_y = ImGui::GetItemRectMin().y - style.ItemSpacing.y;
const float pos_y_down = ImGui::GetItemRectMax().y + style.ItemInnerSpacing.y;
const float pos_y_up = ImGui::GetItemRectMin().y - style.ItemSpacing.y - state.PopupHeight;
bool update_filter = false;
bool ret = false;
//Wait for window height to be known and stable before setting pos or animating fade/pos
if ((state.PopupHeight != FLT_MIN) && (state.PopupHeight == state.PopupHeightPrev))
{
ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMin().x, smoothstep(state.PosAnimationProgress, pos_y_down, pos_y_up) ));
const float time_step = ImGui::GetIO().DeltaTime * 6.0f;
state.PopupAlpha += (!state.IsFadingOut) ? time_step : -time_step;
if (state.PopupAlpha > 1.0f)
state.PopupAlpha = 1.0f;
else if (state.PopupAlpha < 0.0f)
state.PopupAlpha = 0.0f;
state.PosAnimationProgress += (state.PosDir == ImGuiDir_Up) ? time_step : -time_step;
if (state.PosAnimationProgress > 1.0f)
state.PosAnimationProgress = 1.0f;
else if (state.PosAnimationProgress < 0.0f)
state.PosAnimationProgress = 0.0f;
}
else if (state.PopupHeight == FLT_MIN) //Popup is appearing
{
state.KnownTagsList = OverlayManager::Get().GetKnownOverlayTagList();
update_filter = true;
}
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, state.PopupAlpha);
//Look up parent window (optionally digging deeper to avoid child windows) and get its clipping rect
ImGuiWindow* window_parent = ImGui::GetCurrentWindow();
ImGuiWindow* window_parent_lookup = window_parent;
while (clip_parent_depth > 0)
{
window_parent_lookup = window_parent_lookup->ParentWindow;
clip_parent_depth--;
if (window_parent_lookup != nullptr)
{
window_parent = window_parent_lookup;
}
else
{
break;
}
}
ImRect clip_rect = window_parent->ClipRect;
clip_rect.Max.y = window_parent->Size.y + clip_rect.Min.y; //Set Max.y from window content size as FLT_MAX values break drawlist commands for some reason
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoBackground;
ImGui::SetNextWindowSize(ImVec2(widget_width, ImGui::GetTextLineHeightWithSpacing() * 11.5f));
ImGui::Begin("##WindowAddTags", nullptr, flags);
//Transfer scroll input to parent window (which isn't a real parent window but just the one in the stack), so this window doesn't block scrolling
ImGui::ScrollBeginStackParentWindow();
//Use clipping rect of parent window
ImGui::PushClipRect(clip_rect.Min, clip_rect.Max, false);
//Draw background + border manually so it can be clipped properly
ImGuiWindow* window = ImGui::GetCurrentWindow();
ImRect window_rect = window->Rect();
window->DrawList->AddRectFilled(window_rect.Min, window_rect.Max, ImGui::GetColorU32(ImGuiCol_PopupBg), window->WindowRounding);
window->DrawList->AddRect(window_rect.Min, window_rect.Max, ImGui::GetColorU32(ImGuiCol_Border), window->WindowRounding, 0, window->WindowBorderSize);
//Disable inputs when fading out
const bool disable_items = state.IsFadingOut;
if (disable_items)
ImGui::PushItemDisabledNoVisual();
//-Window contents
static float buttons_width = 0.0f;
const bool is_input_text_active = ImGui::IsAnyInputTextActive(); //Need to get this before InputText is canceled in the same frame
bool add_current_input = false;
if (state.FocusTextInput)
{
ImGui::SetKeyboardFocusHere();
}
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - buttons_width - style.ItemInnerSpacing.x);
//Set up shortcut window so it does not block the tag listing itself either. It's a little bit awkward looking like this, but better than blocking the buttons
vr_keyboard.SetShortcutWindowDirectionHint(ImGuiDir_Up, (state.PosDir == ImGuiDir_Down) ? -child_height - style.ItemSpacing.y - style.ItemSpacing.y : -style.ItemInnerSpacing.y);
vr_keyboard.VRKeyboardInputBegin("##InputTagEdit");
if (ImGui::InputTextWithHint("##InputTagEdit", TranslationManager::GetString(tstr_DialogInputTagsHint), state.TagEditBuffer, single_tag_buffer_size,
ImGuiInputTextFlags_CharsNoBlank | ImGuiInputTextFlags_EnterReturnsTrue))
{
add_current_input = !state.IsTagAlreadyInBuffer;
}
vr_keyboard.VRKeyboardInputEnd();
//Wait until the actually have focus before turning the flag off
if (ImGui::IsItemActive())
{
state.FocusTextInput = false;
}
//Check if tag would be a duplicate and disable adding in that case
if (ImGui::IsItemEdited())
{
state.IsTagAlreadyInBuffer = OverlayManager::MatchOverlayTagSingle(buffer_tags, state.TagEditBuffer);
update_filter = true;
UIManager::Get()->AddFontBuilderStringIfAnyUnmappedCharacters(state.TagEditBuffer);
}
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
ImGui::BeginGroup();
ImVec2 b_size, b_uv_min, b_uv_max;
ImVec2 b_size_real = ImVec2(ImGui::GetTextLineHeight(), ImGui::GetTextLineHeight());
TextureManager::Get().GetTextureInfo(tmtex_icon_add, b_size, b_uv_min, b_uv_max);
if (state.IsTagAlreadyInBuffer)
ImGui::PushItemDisabled();
if (ImGui::ImageButton("AddButton", io.Fonts->TexID, b_size_real, b_uv_min, b_uv_max, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)))
{
add_current_input = true;
}
if (state.IsTagAlreadyInBuffer)
ImGui::PopItemDisabled();
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
TextureManager::Get().GetTextureInfo(tmtex_icon_small_close, b_size, b_uv_min, b_uv_max);
if (ImGui::ImageButton("DeleteButton", io.Fonts->TexID, b_size_real, b_uv_min, b_uv_max, ImVec4(0.0f, 0.0f, 0.0f, 0.0f)))
{
state.TagEditBuffer[0] = '\0';
state.TagEditOrigStr = "";
state.IsTagAlreadyInBuffer = true;
update_filter = true;
}
ImGui::EndGroup();
buttons_width = ImGui::GetItemRectSize().x;
ImGui::BeginChild("ChildKnownTags", ImVec2(0.0f, 0.0f), ImGuiChildFlags_NavFlattened, ImGuiWindowFlags_NoBackground);
for (const auto& list_entry : state.KnownTagsList)
{
if (state.KnownTagsFilter.PassFilter(list_entry.Tag.c_str()))
{
if ((list_entry.IsAutoTag) && (!show_auto_tags))
continue;
if (list_entry.IsAutoTag)
ImGui::PushStyleColor(ImGuiCol_Text, Style_ImGuiCol_TextNotification);
if (ImGui::Selectable(list_entry.Tag.c_str()))
{
size_t copied_length = list_entry.Tag.copy(state.TagEditBuffer, buffer_tags_size - 1);
state.TagEditBuffer[copied_length] = '\0';
add_current_input = true;
state.TagEditOrigStr = "";
}
if (list_entry.IsAutoTag)
ImGui::PopStyleColor();
}
}
ImGui::EndChild();
if (disable_items)
ImGui::PopItemDisabledNoVisual();
//Abandoned popup while editing existing tag, put it back
if ((state.IsFadingOut) && (!state.TagEditOrigStr.empty()))
{
size_t copied_length = state.TagEditOrigStr.copy(state.TagEditBuffer, buffer_tags_size - 1);
state.TagEditBuffer[copied_length] = '\0';
state.TagEditOrigStr = "";
add_current_input = true;
update_filter = true;
}
if (add_current_input)
{
//Check if tag is already in the string first and just don't add it then
if (!OverlayManager::MatchOverlayTagSingle(buffer_tags, state.TagEditBuffer))
{
std::string str_tags = buffer_tags;
if (!str_tags.empty())
{
str_tags += " ";
}
str_tags += state.TagEditBuffer;
size_t copied_length = str_tags.copy(buffer_tags, buffer_tags_size - 1);
buffer_tags[copied_length] = '\0';
}
state.TagEditOrigStr = "";
ret = true;
state.IsFadingOut = true;
UIManager::Get()->RepeatFrame();
}
if (update_filter)
{
//Update filter manually (we don't use its buffer directly as its size is fixed to 256)
size_t length = strlen(state.TagEditBuffer);
if (length < (size_t)IM_ARRAYSIZE(state.KnownTagsFilter.InputBuf))
{
memcpy(state.KnownTagsFilter.InputBuf, state.TagEditBuffer, length);
state.KnownTagsFilter.InputBuf[length] = '\0';
state.KnownTagsFilter.Build();
}
}
//Switch directions if there's no space in the default direction
if (state.PosDirDefault == ImGuiDir_Down)
{
state.PosDir = (pos_y_down + ImGui::GetWindowSize().y > clip_rect.Max.y) ? ImGuiDir_Up : ImGuiDir_Down;
}
else
{
//Not using pos_y_up here as it's not valid yet (state.PopupHeight not set)
state.PosDir = (pos_y - ImGui::GetWindowSize().y < clip_rect.Min.y) ? ImGuiDir_Down : ImGuiDir_Up;
}
if (state.PopupAlpha == 0.0f)
{
state.PosAnimationProgress = (state.PosDir == ImGuiDir_Down) ? 0.0f : 1.0f;
}
//Fade out on focus loss/cancel input
if ( ((!ImGui::IsWindowFocused(ImGuiFocusedFlags_ChildWindows)) && (!ImGui::IsAnyItemActive())) ||
((!is_input_text_active) && (ImGui::IsNavInputPressed(ImGuiNavInput_Cancel))) )
{
state.IsFadingOut = true;
}
//Cache window height so it's available on the next frame before beginning the window
state.PopupHeightPrev = state.PopupHeight;
state.PopupHeight = ImGui::GetWindowSize().y;
ImGui::End();
ImGui::PopStyleVar(); //ImGuiStyleVar_Alpha
//Reset when fade-out is done
if ( (state.IsFadingOut) && (state.PopupAlpha == 0.0f) )
{
state.IsFadingOut = false;
state.PopupHeight = FLT_MIN;
state.PosDir = state.PosDirDefault;
state.PosAnimationProgress = (state.PosDirDefault == ImGuiDir_Down) ? 0.0f : 1.0f;
state.PopupShow = false;
}
ImGui::PopID();
return ret;
}
bool FloatingWindow::ActionOrderList(ActionManager::ActionList& list_actions_target, bool is_appearing, bool is_returning, FloatingWindowActionOrderListState& state,
bool& go_add_actions, float height_offset)
{
static float list_buttons_width = 0.0f;
static ImVec2 no_actions_text_size;
const ImGuiStyle& style = ImGui::GetStyle();
ImGuiIO& io = ImGui::GetIO();
ActionManager& action_manager = ConfigManager::Get().GetActionManager();
if ((is_appearing) || (is_returning))
{
state.HasSavedChanges = false;
state.SelectedIndex = -1;
state.ActionsList.clear();
for (ActionUID uid : list_actions_target)
{
state.ActionsList.push_back({uid, action_manager.GetTranslatedName(uid)});
}
}
if (is_appearing)
{
state.ActionListOrig = list_actions_target;
}
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_SettingsActionsOrderHeader));
ImGui::Indent();
ImGui::SetNextItemWidth(-1.0f);
const float item_height = ImGui::GetFrameHeight() + style.ItemSpacing.y;
const float inner_padding = style.FramePadding.y + style.ItemInnerSpacing.y;
const float item_count = (UIManager::Get()->IsInDesktopMode()) ? 16.0f : 14.0f;
ImGui::BeginChild("ActionList", ImVec2(0.0f, (item_height * item_count) + inner_padding + height_offset), true);
if ((is_appearing) || (is_returning))
{
ImGui::SetScrollY(0.0f);
}
//Display error if there are no actions
if (state.ActionsList.size() == 0)
{
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x / 2.0f - (no_actions_text_size.x / 2.0f));
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetContentRegionAvail().y / 2.0f - (no_actions_text_size.y / 2.0f));
ImGui::TextUnformatted(TranslationManager::GetString(tstr_SettingsActionsOrderNoActions));
no_actions_text_size = ImGui::GetItemRectSize();
}
else
{
ActionUID hovered_action_prev = state.HoveredAction;
//List actions
int index = 0;
for (const auto& entry : state.ActionsList)
{
ImGui::PushID((void*)entry.UID);
//Set focus for nav if we previously re-ordered overlays via keyboard
if (state.KeyboardSwappedIndex == index)
{
ImGui::SetKeyboardFocusHere();
//Nav works against us here, so keep setting focus until ctrl isn't down anymore
if ((!io.KeyCtrl) || (!io.NavVisible))
{
state.KeyboardSwappedIndex = -1;
}
}
ImGui::SetNextItemAllowOverlap();
if (ImGui::Selectable(entry.Name.c_str(), (index == state.SelectedIndex), ImGuiSelectableFlags_AllowOverlap))
{
state.SelectedIndex = index;
}
if ( (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem | ImGuiHoveredFlags_AllowWhenOverlappedByItem)) || ((io.NavVisible) && (ImGui::IsItemFocused())) )
{
state.HoveredAction = entry.UID;
}
if (ImGui::IsItemVisible())
{
//Drag reordering
if ((ImGui::IsItemActive()) && (!ImGui::IsItemHovered()))
{
int index_swap = index + ((ImGui::GetMouseDragDelta(ImGuiMouseButton_Left).y < 0.0f) ? -1 : 1);
if ((state.HoveredAction != entry.UID) && (index_swap >= 0) && (index_swap < state.ActionsList.size()))
{
std::iter_swap(state.ActionsList.begin() + index, state.ActionsList.begin() + index_swap);
std::iter_swap(list_actions_target.begin() + index, list_actions_target.begin() + index_swap);
state.SelectedIndex = index_swap;
ImGui::ResetMouseDragDelta(ImGuiMouseButton_Left);
}
}
//Keyboard reordering
if ((io.NavVisible) && (io.KeyCtrl) && (state.HoveredAction == entry.UID))
{
int index_swap = index + ((ImGui::IsNavInputPressed(ImGuiNavInput_DpadDown, true)) ? 1 : (ImGui::IsNavInputPressed(ImGuiNavInput_DpadUp, true)) ? -1 : 0);
if ((index != index_swap) && (index_swap >= 0) && (index_swap < state.ActionsList.size()))
{
std::iter_swap(state.ActionsList.begin() + index, state.ActionsList.begin() + index_swap);
std::iter_swap(list_actions_target.begin() + index, list_actions_target.begin() + index_swap);
//Skip the rest of this frame to avoid double-swaps
state.KeyboardSwappedIndex = index_swap;
ImGui::PopID();
UIManager::Get()->RepeatFrame();
break;
}
}
}
ImGui::PopID();
index++;
}
//Reduce flicker from dragging and hovering
if (state.HoveredAction != hovered_action_prev)
{
UIManager::Get()->RepeatFrame();
}
}
ImGui::EndChild();
ImGui::Unindent();
const bool is_none = (state.SelectedIndex == -1);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - list_buttons_width);
ImGui::BeginGroup();
go_add_actions = ImGui::Button(TranslationManager::GetString(tstr_SettingsActionsOrderAdd));
ImGui::SameLine();
if (is_none)
ImGui::PushItemDisabled();
if ((ImGui::Button(TranslationManager::GetString(tstr_SettingsActionsOrderRemove))) || (ImGui::IsKeyPressed(ImGuiKey_Delete)))
{
if ((state.SelectedIndex >= 0) && (state.SelectedIndex < state.ActionsList.size()))
{
state.ActionsList.erase(state.ActionsList.begin() + state.SelectedIndex);
list_actions_target.erase(list_actions_target.begin() + state.SelectedIndex);
if (state.SelectedIndex >= (int)list_actions_target.size())
{
state.SelectedIndex--;
}
}
}
if (is_none)
ImGui::PopItemDisabled();
ImGui::EndGroup();
list_buttons_width = ImGui::GetItemRectSize().x + style.IndentSpacing;
ImGui::SetCursorPosY( ImGui::GetCursorPosY() + (ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing()) );
//Confirmation buttons
ImGui::Separator();
bool ret = false;
if (ImGui::Button(TranslationManager::GetString(tstr_DialogOk)))
{
ret = true;
state.HasSavedChanges = true;
}
ImGui::SameLine();
if (ImGui::Button(TranslationManager::GetString(tstr_DialogCancel)))
{
ret = true;
list_actions_target = state.ActionListOrig;
}
return ret;
}
bool FloatingWindow::ActionAddSelector(ActionManager::ActionList& list_actions_target, bool is_appearing, FloatingWindowActionAddSelectorState& state, float height_offset)
{
static float list_buttons_width = 0.0f;
static ImVec2 no_actions_text_size;
const ImGuiStyle& style = ImGui::GetStyle();
ActionManager& action_manager = ConfigManager::Get().GetActionManager();
if (is_appearing)
{
state.ActionsList = action_manager.GetActionNameList();
//Remove actions already in the existing list
auto it = std::remove_if(state.ActionsList.begin(), state.ActionsList.end(),
[&](const auto& entry) { return (std::find(list_actions_target.begin(), list_actions_target.end(), entry.UID) != list_actions_target.end()); } );
state.ActionsList.erase(it, state.ActionsList.end());
state.ActionsTickedList.resize(state.ActionsList.size(), 0);
std::fill(state.ActionsTickedList.begin(), state.ActionsTickedList.end(), 0);
}
ImGui::TextColoredUnformatted(ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered), TranslationManager::GetString(tstr_SettingsActionsAddSelectorHeader));
ImGui::Indent();
ImGui::SetNextItemWidth(-1.0f);
const float item_height = ImGui::GetFrameHeight() + style.ItemSpacing.y;
const float inner_padding = style.FramePadding.y + style.ItemInnerSpacing.y;
const float item_count = (UIManager::Get()->IsInDesktopMode()) ? 16.0f : 14.0f;
ImGui::BeginChild("ActionSelector", ImVec2(0.0f, (item_height * item_count) + inner_padding + height_offset), true);
//Reset scroll when appearing
if (is_appearing)
{
ImGui::SetScrollY(0.0f);
}
//Display error if there are no actions
if (state.ActionsList.size() == 0)
{
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x / 2.0f - (no_actions_text_size.x / 2.0f));
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::GetContentRegionAvail().y / 2.0f - (no_actions_text_size.y / 2.0f));
ImGui::TextUnformatted(TranslationManager::GetString(tstr_DialogActionPickerEmpty));
no_actions_text_size = ImGui::GetItemRectSize();
}
else
{
//List actions
const float cursor_x_past_checkbox = ImGui::GetCursorPosX() + ImGui::GetFrameHeightWithSpacing();
int index = 0;
for (const auto& entry : state.ActionsList)
{
ImGui::PushID(index);
//We're using a trick here to extend the checkbox interaction space to the end of the child window
//Checkbox() uses the item inner spacing if the label is not blank, so we increase that and use a space label
//Below we render a custom label after adjusting the cursor position to where it normally would be
ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, {ImGui::GetContentRegionAvail().x, style.ItemInnerSpacing.y});
if (ImGui::Checkbox(" ", (bool*)&state.ActionsTickedList[index]))
{
//Update any ticked status
state.IsAnyActionTicked = false;
for (auto is_ticked : state.ActionsTickedList)
{
if (is_ticked != 0)
{
state.IsAnyActionTicked = true;
break;
}
}
}
ImGui::PopStyleVar();
if (ImGui::IsItemVisible())
{
//Adjust cursor position to be after the checkbox
ImGui::SameLine();
float text_y = ImGui::GetCursorPosY();
ImGui::SetCursorPos({cursor_x_past_checkbox, text_y});
ImGui::TextUnformatted(entry.Name.c_str());
}
ImGui::PopID();
index++;
}
}
ImGui::EndChild();
ImGui::Unindent();
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ImGui::GetContentRegionAvail().x - list_buttons_width);
ImGui::BeginGroup();
if (ImGui::Button(TranslationManager::GetString(tstr_SettingsProfilesOverlaysProfileAddSelectAll)))
{
std::fill(state.ActionsTickedList.begin(), state.ActionsTickedList.end(), 1);
state.IsAnyActionTicked = true;
}
ImGui::SameLine();
if (ImGui::Button(TranslationManager::GetString(tstr_SettingsProfilesOverlaysProfileAddSelectNone)))
{
std::fill(state.ActionsTickedList.begin(), state.ActionsTickedList.end(), 0);
state.IsAnyActionTicked = false;
}
ImGui::EndGroup();
list_buttons_width = ImGui::GetItemRectSize().x + style.IndentSpacing;
ImGui::SetCursorPosY( ImGui::GetCursorPosY() + (ImGui::GetContentRegionAvail().y - ImGui::GetFrameHeightWithSpacing()) );
//Confirmation buttons
ImGui::Separator();
bool ret = false;
if (ImGui::Button(TranslationManager::GetString(tstr_SettingsActionsAddSelectorAdd)))
{
//Add ticked actions to the existing list
int index = 0;
for (const auto& entry : state.ActionsList)
{
if (state.ActionsTickedList[index])
{
list_actions_target.push_back(entry.UID);
}
index++;
}
ret = true;
}
ImGui::SameLine();
if (ImGui::Button(TranslationManager::GetString(tstr_DialogCancel)))
{
ret = true;
}
return ret;
}
bool FloatingWindow::BeginCompactTable(const char* str_id, int column, ImGuiTableFlags flags, const ImVec2& outer_size, float inner_width)
{
const ImGuiStyle style = ImGui::GetStyle();
//There's minor breakage at certain fractional scales, but the ones we care about (100%, 160% (VR), 200%) work fine with this
ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, {style.CellPadding.x, -1.0f});
ImGui::PushStyleVar(ImGuiStyleVar_ItemInnerSpacing, {style.ItemInnerSpacing.x, 0.0f});
flags = flags & (~ImGuiTableFlags_BordersOuter); //Remove border flag, we draw our own later
flags |= ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_PadOuterX;
bool ret = ImGui::BeginTable(str_id, column, flags, outer_size, inner_width);
if (ret)
{
m_CompactTableHeaderHeight = ImGui::GetCursorPosY();
}
else
{
ImGui::PopStyleVar(2);
}
return ret;
}
void FloatingWindow::CompactTableHeadersRow()
{
ImGui::PushItemDisabledNoVisual();
ImGui::TableHeadersRow();
ImGui::SetCursorPosY(ImGui::GetCursorPosY() - 1.0f);
ImGui::Dummy({0.0f, 0.0f}); //To appease parent boundary extension error check
ImGui::PopItemDisabledNoVisual();
m_CompactTableHeaderHeight = ImGui::GetCursorPosY() - m_CompactTableHeaderHeight - ImGui::GetStyle().ItemSpacing.y;
}
void FloatingWindow::EndCompactTable()
{
const ImGuiStyle style = ImGui::GetStyle();
ImGui::EndTable();
ImGui::PopStyleVar(2);
//Selectables cover parts of the default table border and the bottom border would be one pixel inside the last row, so we draw our own header and table border on top instead
ImVec2 rect_min = ImGui::GetItemRectMin(), rect_max = ImGui::GetItemRectMax();
ImGui::GetWindowDrawList()->AddRect(rect_min, {rect_max.x, rect_min.y + ceilf(m_CompactTableHeaderHeight - 1.0f)}, ImGui::GetColorU32(ImGuiCol_Border), 0.0f, 0, style.WindowBorderSize);
ImGui::GetWindowDrawList()->AddRect(rect_min, {rect_max.x, rect_max.y + 1.0f}, ImGui::GetColorU32(ImGuiCol_Border), 0.0f, 0, style.WindowBorderSize);
}
void FloatingWindow::Update()
{
WindowUpdateBase();
}
void FloatingWindow::UpdateVisibility()
{
//Set state depending on dashboard tab visibility
if ( (!UIManager::Get()->IsInDesktopMode()) && (!m_IsTransitionFading) )
{
const bool is_using_dashboard_state = (m_OverlayStateCurrentID == floating_window_ovrl_state_dashboard_tab);
if (is_using_dashboard_state != vr::VROverlay()->IsOverlayVisible(UIManager::Get()->GetOverlayHandleDPlusDashboard()))
{
OverlayStateSwitchCurrent(!is_using_dashboard_state);
}
}
//Overlay position and visibility
if (UIManager::Get()->IsOpenVRLoaded())
{
vr::VROverlayHandle_t overlay_handle = GetOverlayHandle();
if ( (m_OverlayStateCurrent->IsVisible) && (!m_OverlayStateCurrent->IsPinned) && (!UIManager::Get()->GetOverlayDragger().IsDragActive()) &&
(!UIManager::Get()->GetOverlayDragger().IsDragGestureActive()) )
{
//We don't update position when the dummy transform is unstable to avoid flicker, but we absolutely need to update it when the overlay is about to appear
if ( (!m_OvrlVisible) || (!UIManager::Get()->IsDummyOverlayTransformUnstable()) )
{
vr::TrackingUniverseOrigin origin = vr::TrackingUniverseStanding;
Matrix4 matrix_m4 = UIManager::Get()->GetOverlayDragger().GetBaseOffsetMatrix(ovrl_origin_dplus_tab) * m_OverlayStateCurrent->Transform;
vr::HmdMatrix34_t matrix_ovr = matrix_m4.toOpenVR34();
vr::VROverlay()->SetOverlayTransformAbsolute(overlay_handle, origin, &matrix_ovr);
m_OverlayStateCurrent->TransformAbs = matrix_m4;
}
}
if ((!m_OvrlVisible) && (m_OverlayStateCurrent->IsVisible))
{
vr::VROverlay()->ShowOverlay(overlay_handle);
m_OvrlVisible = true;
}
else if ((m_OvrlVisible) && (!m_OverlayStateCurrent->IsVisible) && (m_Alpha == 0.0f))
{
vr::VROverlay()->HideOverlay(overlay_handle);
m_OvrlVisible = false;
}
}
}
void FloatingWindow::Show(bool skip_fade)
{
m_OverlayStateCurrent->IsVisible = true;
if (skip_fade)
{
m_Alpha = 1.0f;
}
UIManager::Get()->GetIdleState().AddActiveTime();
}
void FloatingWindow::Hide(bool skip_fade)
{
m_OverlayStateCurrent->IsVisible = false;
if (skip_fade)
{
m_Alpha = 0.0f;
}
UIManager::Get()->GetIdleState().AddActiveTime();
}
void FloatingWindow::HideAll(bool skip_fade)
{
Hide(skip_fade);
m_OverlayStateRoom.IsVisible = false;
m_OverlayStateDashboardTab.IsVisible = false;
}
bool FloatingWindow::IsVisible() const
{
return m_OverlayStateCurrent->IsVisible;
}
bool FloatingWindow::IsVisibleOrFading() const
{
return ( (m_OverlayStateCurrent->IsVisible) || (m_Alpha != 0.0f) || (m_IsTransitionFading) );
}
float FloatingWindow::GetAlpha() const
{
return m_Alpha;
}
void FloatingWindow::ApplyUIScale()
{
m_Size.x = m_SizeUnscaled.x * UIManager::Get()->GetUIScale();
m_Size.y = m_SizeUnscaled.y * UIManager::Get()->GetUIScale();
}
bool FloatingWindow::CanUnpinRoom() const
{
return m_AllowRoomUnpinning;
}
bool FloatingWindow::IsPinned() const
{
return m_OverlayStateCurrent->IsPinned;
}
void FloatingWindow::SetPinned(bool is_pinned, bool no_state_copy)
{
m_OverlayStateCurrent->IsPinned = is_pinned;
if (!UIManager::Get()->IsOpenVRLoaded())
return;
if (!is_pinned)
{
RebaseTransform();
}
else if (!no_state_copy)
{
if (m_OverlayStateCurrentID == floating_window_ovrl_state_dashboard_tab)
{
m_OverlayStateRoom.IsPinned = m_OverlayStateDashboardTab.IsPinned;
m_OverlayStateRoom.Transform = m_OverlayStateDashboardTab.Transform;
m_OverlayStateRoom.TransformAbs = m_OverlayStateDashboardTab.TransformAbs;
}
//Reset transform if TransformAbs doesn't have anything of value yet
if (m_OverlayStateRoom.TransformAbs.isZero())
{
ResetTransform(floating_window_ovrl_state_room);
}
}
}
FloatingWindowOverlayState& FloatingWindow::GetOverlayState(FloatingWindowOverlayStateID id)
{
switch (id)
{
case floating_window_ovrl_state_room: return m_OverlayStateRoom;
case floating_window_ovrl_state_dashboard_tab: return m_OverlayStateDashboardTab;
}
return m_OverlayStateRoom;
}
FloatingWindowOverlayStateID FloatingWindow::GetOverlayStateCurrentID()
{
return m_OverlayStateCurrentID;
}
Matrix4& FloatingWindow::GetTransform()
{
return m_OverlayStateCurrent->Transform;
}
void FloatingWindow::SetTransform(const Matrix4& transform)
{
m_OverlayStateCurrent->Transform = transform;
//Store last absolute transform
vr::HmdMatrix34_t hmd_mat;
vr::TrackingUniverseOrigin universe_origin = vr::TrackingUniverseStanding;
vr::VROverlay()->GetOverlayTransformAbsolute(GetOverlayHandle(), &universe_origin, &hmd_mat);
m_OverlayStateCurrent->TransformAbs = hmd_mat;
//Store size multiplier
float current_width = m_OvrlWidth;
if (vr::VROverlay()->GetOverlayWidthInMeters(GetOverlayHandle(), ¤t_width) == vr::VROverlayError_None)
{
m_OverlayStateCurrent->Size = current_width / m_OvrlWidth;
}
}
void FloatingWindow::ApplyCurrentOverlayState()
{
if (!UIManager::Get()->IsOpenVRLoaded())
return;
if (m_OverlayStateCurrent->IsPinned)
{
vr::HmdMatrix34_t matrix_ovr = m_OverlayStateCurrent->TransformAbs.toOpenVR34();
vr::VROverlay()->SetOverlayTransformAbsolute(GetOverlayHandle(), vr::TrackingUniverseStanding, &matrix_ovr);
}
vr::VROverlay()->SetOverlayWidthInMeters(GetOverlayHandle(), m_OvrlWidth * m_OverlayStateCurrent->Size);
}
void FloatingWindow::RebaseTransform()
{
vr::HmdMatrix34_t hmd_mat;
vr::TrackingUniverseOrigin universe_origin = vr::TrackingUniverseStanding;
vr::VROverlay()->GetOverlayTransformAbsolute(GetOverlayHandle(), &universe_origin, &hmd_mat);
Matrix4 mat_abs = hmd_mat;
Matrix4 mat_origin_inverse = UIManager::Get()->GetOverlayDragger().GetBaseOffsetMatrix(ovrl_origin_dplus_tab);
mat_origin_inverse.invert();
m_OverlayStateCurrent->Transform = mat_origin_inverse * mat_abs;
}
void FloatingWindow::ResetTransformAll()
{
ResetTransform(floating_window_ovrl_state_dashboard_tab);
ResetTransform(floating_window_ovrl_state_room);
m_OverlayStateRoom.IsVisible = false;
m_OverlayStateRoom.IsPinned = false;
}
void FloatingWindow::ResetTransform(FloatingWindowOverlayStateID state_id)
{
GetOverlayState(state_id).Transform.identity();
//Set absolute transform to the dashboard tab one (as if pinning)
if (state_id == floating_window_ovrl_state_room)
{
if (!m_OverlayStateDashboardTab.TransformAbs.isZero())
{
m_OverlayStateRoom.TransformAbs = m_OverlayStateDashboardTab.TransformAbs;
}
else if ((UIManager::Get() != nullptr) && (UIManager::Get()->IsOpenVRLoaded())) //If the dashboard tab transform is still zero, generate a HMD facing transform instead (needs startup to be done though)
{
m_OverlayStateRoom.TransformAbs = vr::IVRSystemEx::ComputeHMDFacingTransform(1.25f);
UIManager::Get()->GetOverlayDragger().ApplyDashboardScale(m_OverlayStateRoom.TransformAbs);
}
}
}
void FloatingWindow::StartDrag()
{
if (UIManager::Get()->IsOpenVRLoaded())
{
OverlayDragger& overlay_dragger = UIManager::Get()->GetOverlayDragger();
if ( (!overlay_dragger.IsDragActive()) && (!overlay_dragger.IsDragGestureActive()) )
{
overlay_dragger.DragStart(GetOverlayHandle(), m_DragOrigin);
overlay_dragger.DragSetMaxWidth(m_OvrlWidthMax);
}
}
}
const ImVec2& FloatingWindow::GetPos() const
{
return m_Pos;
}
const ImVec2& FloatingWindow::GetSize() const
{
return m_Size;
}
bool FloatingWindow::TranslatedComboAnimated(const char* label, int& value, TRMGRStrID trstr_min, TRMGRStrID trstr_max)
{
bool ret = false;
const char* preview_value = ((trstr_min + value >= trstr_min) && (trstr_min + value <= trstr_max)) ? TranslationManager::GetString( (TRMGRStrID)(trstr_min + value) ) : "???";
if (ImGui::BeginComboAnimated(label, preview_value))
{
//Make use of the fact values and translation string IDs are laid out sequentially and shorten this to a nice loop
const int value_max = (trstr_max - trstr_min) + 1;
for (int i = 0; i < value_max; ++i)
{
ImGui::PushID(i);
if (ImGui::Selectable(TranslationManager::GetString( (TRMGRStrID)(trstr_min + i) ), (value == i)))
{
value = i;
ret = true;
}
ImGui::PopID();
}
ImGui::EndCombo();
}
return ret;
}
================================================
FILE: src/DesktopPlusUI/FloatingWindow.h
================================================
#pragma once
#include "OverlayDragger.h"
#include "TextureManager.h"
#include "TranslationManager.h"
#include "OverlayManager.h"
#include
enum FloatingWindowOverlayStateID
{
floating_window_ovrl_state_room,
floating_window_ovrl_state_dashboard_tab
};
struct FloatingWindowOverlayState
{
bool IsVisible = false;
bool IsPinned = false;
float Size = 1.0f;
Matrix4 Transform;
Matrix4 TransformAbs;
};
struct FloatingWindowInputOverlayTagsState
{
std::string TagEditOrigStr;
char TagEditBuffer[1024] = "";
float ChildHeightLines = 1.0f;
std::vector KnownTagsList;
ImGuiTextFilter KnownTagsFilter;
bool IsTagAlreadyInBuffer = false;
bool FocusTextInput = false;
bool PopupShow = false;
float PopupAlpha = 0.0f;
float PopupHeight = FLT_MIN;
float PopupHeightPrev = FLT_MIN;
ImGuiDir PosDir = ImGuiDir_Down;
ImGuiDir PosDirDefault = ImGuiDir_Down;
float PosAnimationProgress = 0.0f;
bool IsFadingOut = false;
};
struct FloatingWindowActionOrderListState
{
std::vector ActionsList;
ActionManager::ActionList ActionListOrig;
bool HasSavedChanges = false;
int KeyboardSwappedIndex = -1;
int SelectedIndex = -1;
ActionUID HoveredAction = k_ActionUID_Invalid;
};
struct FloatingWindowActionAddSelectorState
{
std::vector ActionsList;
std::vector ActionsTickedList;
bool IsAnyActionTicked = false;
};
//Base class for drag-able floating overlay windows, such as the Settings, Overlay Properties and Keyboard windows
class FloatingWindow
{
protected:
float m_OvrlWidth;
float m_OvrlWidthMax; //Maximum width passed to OverlayDragger
float m_Alpha;
bool m_OvrlVisible;
bool m_IsTransitionFading;
FloatingWindowOverlayStateID m_OverlayStateCurrentID;
FloatingWindowOverlayState m_OverlayStateRoom;
FloatingWindowOverlayState m_OverlayStateDashboardTab;
FloatingWindowOverlayState m_OverlayStateFading;
FloatingWindowOverlayState* m_OverlayStateCurrent;
FloatingWindowOverlayState* m_OverlayStatePending;
std::string m_WindowTitle;
std::string m_WindowID;
TRMGRStrID m_WindowTitleStrID;
TMNGRTexID m_WindowIcon;
int m_WindowIconWin32IconCacheID; //TextureManager Icon cache ID when using a Win32 window icon as the ImGui window icon
ImVec2 m_Pos;
ImVec2 m_PosPivot;
ImVec2 m_Size; //Set in derived constructor, 2 pixel-wide padding around actual texture space expected
ImVec2 m_SizeUnscaled; //Set in derived constructor, size before applying UI scale factor, so equal to m_Size initally
ImGuiWindowFlags m_WindowFlags;
bool m_AllowRoomUnpinning; //Set to enable pin button while room overlay state is active
OverlayOrigin m_DragOrigin; //Origin passed to OverlayDragger for window drags, doesn't affect overlay positioning (override relevant functions instead)
float m_TitleBarMinWidth;
float m_TitleBarTitleMaxWidth; //Width available for the title string without icon and buttons
float m_TitleBarTitleIconAlpha; //Alpha value applied to both window title text & icon
bool m_IsTitleBarHovered;
bool m_IsTitleIconClicked;
bool m_HasAppearedOnce;
bool m_IsWindowAppearing;
float m_CompactTableHeaderHeight;
void WindowUpdateBase(); //Sets up ImGui window with custom title bar, pinning and overlay-based dragging
virtual void WindowUpdate() = 0; //Window content, called within an ImGui Begin()'d window
void OverlayStateSwitchCurrent(bool use_dashboard_tab);
void OverlayStateSwitchFinish();
virtual void OnWindowPinButtonPressed(); //Called when the pin button is pressed, after updating overlay state
virtual void OnWindowCloseButtonPressed(); //Called when the close button is pressed, after updating overlay state
virtual bool IsVirtualWindowItemHovered() const; //Returns false by default, can be overridden to signal hover state of widgets that don't touch global ImGui state (used for blank space drag)
void HelpMarker(const char* desc, const char* marker_str = "(?)") const; //Help marker, but tooltip is fixed to top or bottom of the window
void UpdateLimiterSetting(bool is_override) const;
//Input widget for a collection of overlay tags. clip_parent_depth is the depth of parent window look up for popup's clipping rect, change when used in nested child windows
static bool InputOverlayTags(const char* str_id, char* buffer_tags, size_t buffer_tags_size, FloatingWindowInputOverlayTagsState& state, int clip_parent_depth = 0, bool show_auto_tags = true);
//Almost entire pages but implemented here to be shared between multiple windows
bool ActionOrderList(ActionManager::ActionList& list_actions_target, bool is_appearing, bool is_returning, FloatingWindowActionOrderListState& state,
bool& go_add_actions, float height_offset = 0.0f);
bool ActionAddSelector(ActionManager::ActionList& list_actions_target, bool is_appearing, FloatingWindowActionAddSelectorState& state, float height_offset = 0.0f);
//BeginTable(), but with some hacks to allow for compact, gap-less selectable + border around header (always set). Fairly specific, so not a generic ImGui extension
//Text has to be aligned to frame padding with this
bool BeginCompactTable(const char* str_id, int column, ImGuiTableFlags flags = 0, const ImVec2& outer_size = ImVec2(0.0f, 0.0f), float inner_width = 0.0f);
void CompactTableHeadersRow();
void EndCompactTable();
public:
FloatingWindow();
virtual ~FloatingWindow() = default;
void Update(); //Not called when idling (no windows visible)
virtual void UpdateVisibility(); //Only called in VR mode
virtual void Show(bool skip_fade = false);
virtual void Hide(bool skip_fade = false);
void HideAll(bool skip_fade = false); //Hide(), but applies to all overlay visibility states
bool IsVisible() const;
bool IsVisibleOrFading() const; //Returns true if m_Visible is true *or* m_Alpha isn't 0 yet
float GetAlpha() const;
virtual void ApplyUIScale();
bool CanUnpinRoom() const;
bool IsPinned() const;
void SetPinned(bool is_pinned, bool no_state_copy = false); //no_state_copy = don't copy dashboard state to room (default behavior for pin button)
FloatingWindowOverlayState& GetOverlayState(FloatingWindowOverlayStateID id);
FloatingWindowOverlayStateID GetOverlayStateCurrentID();
Matrix4& GetTransform();
void SetTransform(const Matrix4& transform);
virtual void ApplyCurrentOverlayState(); //Applies current absolute transform to the overlay if pinned and sets the width
virtual void RebaseTransform();
virtual void ResetTransformAll();
virtual void ResetTransform(FloatingWindowOverlayStateID state_id);
virtual void StartDrag(); //Starts a regular laser pointer drag of the window with the necessary parameters
const ImVec2& GetPos() const;
const ImVec2& GetSize() const;
virtual vr::VROverlayHandle_t GetOverlayHandle() const = 0;
static bool TranslatedComboAnimated(const char* label, int& value, TRMGRStrID trstr_min, TRMGRStrID trstr_max);
};
================================================
FILE: src/DesktopPlusUI/ImGuiExt.cpp
================================================
#include "ImGuiExt.h"
#include
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include "imgui_internal.h"
#include "UIManager.h"
#include "Util.h"
ImVec4 Style_ImGuiCol_TextNotification;
ImVec4 Style_ImGuiCol_TextWarning;
ImVec4 Style_ImGuiCol_TextError;
ImVec4 Style_ImGuiCol_TextOutline;
ImVec4 Style_ImGuiCol_ButtonPassiveToggled;
ImVec4 Style_ImGuiCol_SteamVRCursor;
ImVec4 Style_ImGuiCol_SteamVRCursorBorder;
namespace ImGui
{
//Like InputFloat()'s buttons but with a slider instead. Not quite as flexible, though. Always takes as much space as available.
bool SliderWithButtonsFloat(const char* str_id, float& value, float step, float step_small, float min, float max, const char* format, ImGuiSliderFlags flags, bool* used_button, const char* text_alt)
{
//Hacky solution to make right mouse enable text input on the slider while not touching ImGui code or generalizing it as ctrl press
ImGuiIO& io = ImGui::GetIO();
const bool mouse_left_clicked_old = io.MouseClicked[ImGuiMouseButton_Left];
const bool mouse_left_down_old = io.MouseDown[ImGuiMouseButton_Left];
const float mouse_left_down_duration_old = io.MouseDownDuration[ImGuiMouseButton_Left];
const bool key_ctrl_old = io.KeyCtrl;
if (io.MouseClicked[ImGuiMouseButton_Right])
{
io.MouseClicked[ImGuiMouseButton_Left] = true;
io.MouseDown[ImGuiMouseButton_Left] = true;
io.MouseDownDuration[ImGuiMouseButton_Left] = 0.0f;
io.KeyCtrl = true;
io.KeyMods |= ImGuiMod_Ctrl; //KeyMods needs to stay consistent with KeyCtrl
}
//Use small step value when shift is down
if (io.KeyShift)
{
step = step_small;
}
ImGuiStyle& style = ImGui::GetStyle();
const float value_old = value;
const ImVec2 button_size(ImGui::GetFrameHeight(), ImGui::GetFrameHeight());
ImGui::BeginGroup();
ImGui::PushID(str_id);
ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
//Calulate slider width (GetContentRegionAvail() returns 1 more than when using -1 width to fill)
ImGui::SetNextItemWidth((ImGui::GetContentRegionAvail().x - (ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x) * 2) - 1.0f);
ImGui::SliderFloat("##Slider", &value, min, max, format, flags);
if ( (text_alt != nullptr) && (ImGui::GetCurrentContext()->TempInputId != ImGui::GetID("##Slider")) )
{
ImGui::RenderTextClipped(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), text_alt, nullptr, nullptr, ImVec2(0.5f, 0.5f));
}
bool has_slider_deactivated = false;
if (ImGui::IsItemDeactivated())
{
has_slider_deactivated = true;
}
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
if (ImGui::Button("-", button_size))
{
//Round to the step value while we're at it. This may not be the most expected thing at first, but it helps a lot to get the usually preferred even values
int step_count = (int)ceilf(value / step);
value = step_count * step;
value -= step;
if ( int( ceilf(value / step) ) >= step_count ) //Welcome to floating point math, this can happen
value -= step / 10000.f; //This works for what we need, but not quite elegant indeed
if (used_button)
*used_button = true;
}
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
if (ImGui::Button("+", button_size))
{
int step_count = int(value / step);
value = step_count * step;
value += step;
if ( int(value / step) <= step_count ) //See above
value += step / 10000.f;
if (used_button)
*used_button = true;
}
ImGui::PopItemFlag(); //ImGuiItemFlag_ButtonRepeat
ImGui::PopID();
ImGui::EndGroup();
//Deactivated flag for the slider gets swallowed up somewhere, but we really need it for the VR keyboard, so we tape it back on here
if (has_slider_deactivated)
{
ImGui::GetCurrentContext()->LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDeactivated | ImGuiItemStatusFlags_Deactivated;
}
//We generally don't want -0.0 to be a thing, so prevent it
if (value == -0.0f)
value = 0.0f;
//Restore hack
io.MouseClicked[ImGuiMouseButton_Left] = mouse_left_clicked_old;
io.MouseDown[ImGuiMouseButton_Left] = mouse_left_down_old;
io.MouseDownDuration[ImGuiMouseButton_Left] = mouse_left_down_duration_old;
io.KeyCtrl = key_ctrl_old;
if (!io.KeyCtrl)
{
io.KeyMods &= ~ImGuiMod_Ctrl;
}
return (value != value_old);
}
bool SliderWithButtonsInt(const char* str_id, int& value, int step, int step_small, int min, int max, const char* format, ImGuiSliderFlags flags, bool* used_button, const char* text_alt)
{
//Hacky solution to make right mouse enable text input on the slider while not touching ImGui code or generalizing it as ctrl press
ImGuiIO& io = ImGui::GetIO();
const bool mouse_left_clicked_old = io.MouseClicked[ImGuiMouseButton_Left];
const bool mouse_left_down_old = io.MouseDown[ImGuiMouseButton_Left];
const float mouse_left_down_duration_old = io.MouseDownDuration[ImGuiMouseButton_Left];
const bool key_ctrl_old = io.KeyCtrl;
if (io.MouseClicked[ImGuiMouseButton_Right])
{
io.MouseClicked[ImGuiMouseButton_Left] = true;
io.MouseDown[ImGuiMouseButton_Left] = true;
io.MouseDownDuration[ImGuiMouseButton_Left] = 0.0f;
io.KeyCtrl = true;
io.KeyMods |= ImGuiMod_Ctrl; //KeyMods needs to stay consistent with KeyCtrl
}
//Use small step value when shift is down
if (io.KeyShift)
{
step = step_small;
}
ImGuiStyle& style = ImGui::GetStyle();
const int value_old = value;
const ImVec2 button_size(ImGui::GetFrameHeight(), ImGui::GetFrameHeight());
ImGui::BeginGroup();
ImGui::PushID(str_id);
ImGui::PushItemFlag(ImGuiItemFlags_ButtonRepeat, true);
//Calulate slider width (GetContentRegionAvail() returns 1 more than when using -1 width to fill)
ImGui::SetNextItemWidth((ImGui::GetContentRegionAvail().x - (ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x) * 2) - 1.0f);
ImGui::SliderInt("##Slider", &value, min, max, format, flags);
if ( (text_alt != nullptr) && (ImGui::GetCurrentContext()->TempInputId != ImGui::GetID("##Slider")) )
{
ImGui::RenderTextClipped(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), text_alt, nullptr, nullptr, ImVec2(0.5f, 0.5f));
}
bool has_slider_deactivated = false;
if (ImGui::IsItemDeactivated())
{
has_slider_deactivated = true;
}
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
if (ImGui::Button("-", button_size))
{
//Also rounding to steps here
value = (int)ceilf((float)value / step) * step;
value -= step;
if (used_button)
*used_button = true;
}
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
if (ImGui::Button("+", button_size))
{
value = (value / step) * step;
value += step;
if (used_button)
*used_button = true;
}
ImGui::PopItemFlag(); //ImGuiItemFlag_ButtonRepeat
ImGui::PopID();
ImGui::EndGroup();
//Deactivated flag for the slider gets swallowed up somewhere, but we really need it for the VR keyboard, so we tape it back on here
if (has_slider_deactivated)
{
ImGui::GetCurrentContext()->LastItemData.StatusFlags |= ImGuiItemStatusFlags_HasDeactivated | ImGuiItemStatusFlags_Deactivated;
}
//Restore hack
io.MouseClicked[ImGuiMouseButton_Left] = mouse_left_clicked_old;
io.MouseDown[ImGuiMouseButton_Left] = mouse_left_down_old;
io.MouseDownDuration[ImGuiMouseButton_Left] = mouse_left_down_duration_old;
io.KeyCtrl = key_ctrl_old;
if (!io.KeyCtrl)
{
io.KeyMods &= ~ImGuiMod_Ctrl;
}
return (value != value_old);
}
bool SliderWithButtonsFloatPercentage(const char* str_id, float& value, int step, int step_small, int min, int max, const char* format, ImGuiSliderFlags flags, bool* used_button, const char* text_alt)
{
int value_ui = int(value * 100.0f);
if (ImGui::SliderWithButtonsInt(str_id, value_ui, step, step_small, min, max, format, flags, used_button, text_alt))
{
value = value_ui / 100.0f;
//Floating point hell hacky fix (slider can get stuck when using + button otherwise)
int intvalue = int(value * 100.0f), intvalue_prev = intvalue;
while (intvalue < value_ui)
{
value += step / 10000.f;
intvalue_prev = intvalue;
intvalue = int(value * 100.0f);
if (intvalue == intvalue_prev) //Sanity check to avoid endless loop at big value + small step combinations
{
break;
}
}
return true;
}
return false;
}
ImGuiID SliderWithButtonsGetSliderID(const char* str_id)
{
ImGui::PushID(str_id);
ImGuiID id = ImGui::GetID("##Slider");
ImGui::PopID();
return id;
}
//Like imgui_demo's HelpMarker, but with a fixed position tooltip
void FixedHelpMarker(const char* desc, const char* marker_str)
{
ImGui::TextDisabled(marker_str);
if (ImGui::IsItemHovered())
{
static float last_y_offset = FLT_MIN; //Try to avoid getting the tooltip off-screen... the way it's done here is a bit messy to be fair
float pos_y = ImGui::GetItemRectMin().y;
bool is_invisible = false;
if (last_y_offset == FLT_MIN) //Same as IsWindowAppearing except the former doesn't work before beginning the window which is too late for the position...
{
//We need to create the tooltip window for size calculations to happen but also don't want to see it... so alpha 0, even if wasteful
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, 0.0f);
is_invisible = true;
}
else
{
ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMax().x + ImGui::GetStyle().ItemInnerSpacing.x, ImGui::GetItemRectMin().y + last_y_offset));
}
ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
ImGui::PushTextWrapPos(ImGui::GetIO().DisplaySize.x - ImGui::GetCursorScreenPos().x);
ImGui::TextUnformatted(desc);
ImGui::PopTextWrapPos();
ImGui::PopTextWrapPos();
if (ImGui::IsWindowAppearing()) //New tooltip, reset offset
{
//The window size isn't available in this frame yet, so we'll have to skip the having it visible for one more frame and then act on it
last_y_offset = FLT_MIN;
}
else
{
if (pos_y + ImGui::GetWindowSize().y > ImGui::GetIO().DisplaySize.y) //If it would be partially off-screen
{
last_y_offset = ImGui::GetIO().DisplaySize.y - (pos_y + ImGui::GetWindowSize().y);
}
else //Use normal pos
{
last_y_offset = 0.0f;
}
}
ImGui::EndTooltip();
if (is_invisible)
ImGui::PopStyleVar(); //ImGuiStyleVar_Alpha
}
}
bool ButtonWithWrappedLabel(const char* label, const ImVec2& size)
{
//This could probably be solved in a cleaner way, but it's not /that/ dirty
ImGui::PushID(label);
ImGui::BeginGroup();
ImGuiWindow* window = GetCurrentWindow();
const ImGuiStyle& style = ImGui::GetStyle();
bool ret = ImGui::Button("", ImVec2(size.x + (style.FramePadding.x * 2.0f), size.y + (style.FramePadding.y * 2.0f)) );
ImGui::SameLine(0.0f, 0.0f);
ImGui::SetCursorPosX(ImGui::GetCursorPosX() - size.x - style.FramePadding.x);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + style.FramePadding.y);
//Prevent child window moving line pos
float cursor_max_pos_prev_y = window->DC.CursorMaxPos.y;
ImGui::BeginChild("ButtonLabel", ImVec2(size.x + style.FramePadding.x, size.y), false, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoInputs);
ImVec2 text_size = ImGui::CalcTextSize(label, nullptr, false, size.x);
ImGui::SetCursorPosX( (size.x / 2.0f) - int(text_size.x / 2.0f) );
ImGui::SetCursorPosY( (size.x / 2.0f) - int(text_size.y / 2.0f) );
ImGui::PushTextWrapPos(size.x);
ImGui::TextUnformatted(label);
ImGui::PopTextWrapPos();
ImGui::EndChild();
window->DC.CursorMaxPos.y = cursor_max_pos_prev_y;
ImGui::EndGroup();
ImGui::PopID();
return ret;
}
void RenderButtonMultilineLabel(const char* label, float line_height_scale)
{
const ImGuiContext& g = *GImGui;
RenderButtonMultilineLabel(label, g.LastItemData.Rect, line_height_scale);
}
void RenderButtonMultilineLabel(const char* label, const ImRect& bb, float line_height_scale)
{
struct label_line_data
{
const char* text_start;
const char* text_end;
ImVec2 text_alignment;
};
const ImGuiStyle& style = ImGui::GetStyle();
const float line_height = ImGui::GetFontSize() * line_height_scale;
//Provided bounding box is intended to be for the button or whatever frame it's rendered on (also used for clipping), so apply inner padding here (vertical one not required)
ImRect bb_inner = bb;
bb_inner.Min.x += style.FramePadding.x;
bb_inner.Max.x -= style.FramePadding.x;
//Split label string into line segments (via offsets, no copying)
std::vector label_lines;
float total_height = ImGui::GetFontSize(); //First line is always set and needs to be equal with font size, while other lines add line_height
{
const char* line_start = label;
const char* s = label;
float h_align = 0.5f;
for (;;)
{
if (*s == '\0')
{
label_lines.push_back({line_start, s, {h_align, 0.0f}});
break;
}
else if (*s == '\n')
{
label_lines.push_back({line_start, s, {h_align, 0.0f}});
//total_height starts with first line height so only add on newline characters, not string end
total_height += line_height;
line_start = s + 1;
//Horizontal alignment defaults to center on line start
h_align = 0.5f;
}
else if ((*s == '#') && (s[1] == '#')) //Double # is already being eaten by ImGui but not used for IDs in this context. Let's use it as control mechanism for horizontal alignment
{
//Reminder this kind of advance access is fine because the string is always NUL-terminated and we didn't encounter any NULs
if (s[2] == 'L')
{
h_align = 0.0f;
}
else if (s[2] == 'R')
{
h_align = 1.0f;
}
}
++s;
}
}
//Render the lines one-by-one to get correct horizontally centered alignment for each
ImVec2 pos_line;
const float base_offset_y = ((bb.Max.y - bb.Min.y) / 2.0f) - (total_height / 2.0f);
for (int i = 0; i < label_lines.size(); ++i)
{
const label_line_data& label_line = label_lines[i];
pos_line.x = bb_inner.Min.x;
pos_line.y = bb_inner.Min.y + base_offset_y + (line_height * i);
RenderTextClippedUnclamped(pos_line, bb_inner.Max, label_line.text_start, label_line.text_end, nullptr, label_line.text_alignment, &bb);
}
}
bool BeginComboWithInputText(const char* str_id, char* str_buffer, size_t buffer_size, bool& out_buffer_changed, bool& persist_input_visible,
bool& persist_input_activated, bool& persist_mouse_released_once, bool no_preview_text)
{
ImGuiContext& g = *GImGui;
out_buffer_changed = false;
if (persist_input_visible)
{
ImGui::PushID("InputText");
g.NextItemData.Width = ImGui::CalcItemWidth();
g.NextItemData.Width -= ImGui::GetFrameHeight();
if ((ImGui::InputText(str_id, str_buffer, buffer_size)))
{
out_buffer_changed = true;
}
ImGuiID input_text_id = ImGui::GetItemID();
if ( (persist_input_activated) && (persist_mouse_released_once) && (ImGui::PopupContextMenuInputText(str_id, str_buffer, buffer_size)) )
{
out_buffer_changed = true;
}
if (!persist_input_activated)
{
ImGui::ActivateItemByID(ImGui::GetItemID());
persist_input_activated = true;
}
else if ( (!ImGui::IsPopupOpen(str_id)) && ( (ImGui::IsItemDeactivated()) || (g.ActiveId != input_text_id) ) )
{
persist_input_visible = false;
persist_input_activated = false;
persist_mouse_released_once = false;
UIManager::Get()->RepeatFrame();
}
if (ImGui::IsMouseReleased(ImGuiMouseButton_Right))
{
persist_mouse_released_once = true;
}
ImGui::SameLine(0.0f, 0.0f);
ImGui::PopID();
}
return (ImGui::BeginCombo(str_id, (no_preview_text) ? "" : str_buffer, (persist_input_visible) ? (ImGuiComboFlags_NoPreview | ImGuiComboFlags_PopupAlignLeft) : ImGuiComboFlags_None));
}
void ComboWithInputTextActivationCheck(bool& persist_input_visible)
{
ImGuiContext& g = *GImGui;
//Right-click or Ctrl+Left-click to edit
if ( (ImGui::IsItemClicked(ImGuiMouseButton_Right)) || ((ImGui::IsItemClicked(ImGuiMouseButton_Left)) && g.IO.KeyCtrl) )
{
persist_input_visible = true;
}
}
static float CalcMaxPopupHeightFromItemCount(int items_count)
{
ImGuiContext& g = *GImGui;
if (items_count <= 0)
return FLT_MAX;
return (g.FontSize + g.Style.ItemSpacing.y) * items_count - g.Style.ItemSpacing.y + (g.Style.WindowPadding.y * 2);
}
bool BeginComboAnimated(const char* label, const char* preview_value, ImGuiComboFlags flags)
{
//-
//Custom code sections marked with //-
//Otherwise exactly the same as ImGui::BeginCombo() of ImGui v1.82 (with compat patches)
static float animation_progress = 0.0f;
static float scrollbar_alpha = 0.0f;
float popup_expected_width = FLT_MAX;
float popup_height = 0.0f;
//-
// Always consume the SetNextWindowSizeConstraint() call in our early return paths
ImGuiContext& g = *GImGui;
ImGuiWindow* window = GetCurrentWindow();
ImGuiNextWindowDataFlags backup_next_window_data_flags = g.NextWindowData.HasFlags;
g.NextWindowData.ClearFlags(); // We behave like Begin() and need to consume those values
if (window->SkipItems)
return false;
IM_ASSERT((flags & (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)) != (ImGuiComboFlags_NoArrowButton | ImGuiComboFlags_NoPreview)); // Can't use both flags together
const ImGuiStyle& style = g.Style;
const ImGuiID id = window->GetID(label);
const float arrow_size = (flags & ImGuiComboFlags_NoArrowButton) ? 0.0f : GetFrameHeight();
const ImVec2 label_size = CalcTextSize(label, NULL, true);
const float expected_w = CalcItemWidth();
const float w = (flags & ImGuiComboFlags_NoPreview) ? arrow_size : expected_w;
const ImRect frame_bb(window->DC.CursorPos, window->DC.CursorPos + ImVec2(w, label_size.y + style.FramePadding.y * 2.0f));
const ImRect total_bb(frame_bb.Min, frame_bb.Max + ImVec2(label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f, 0.0f));
ItemSize(total_bb, style.FramePadding.y);
if (!ItemAdd(total_bb, id, &frame_bb))
return false;
bool hovered, held;
bool pressed = ButtonBehavior(frame_bb, id, &hovered, &held);
bool popup_open = IsPopupOpen(id, ImGuiPopupFlags_None);
const ImU32 frame_col = GetColorU32(hovered ? ImGuiCol_FrameBgHovered : ImGuiCol_FrameBg);
const float value_x2 = ImMax(frame_bb.Min.x, frame_bb.Max.x - arrow_size);
RenderNavCursor(frame_bb, id);
if (!(flags & ImGuiComboFlags_NoPreview))
window->DrawList->AddRectFilled(frame_bb.Min, ImVec2(value_x2, frame_bb.Max.y), frame_col, style.FrameRounding, (flags & ImGuiComboFlags_NoArrowButton) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersLeft);
if (!(flags & ImGuiComboFlags_NoArrowButton))
{
ImU32 bg_col = GetColorU32((popup_open || hovered) ? ImGuiCol_ButtonHovered : ImGuiCol_Button);
ImU32 text_col = GetColorU32(ImGuiCol_Text);
window->DrawList->AddRectFilled(ImVec2(value_x2, frame_bb.Min.y), frame_bb.Max, bg_col, style.FrameRounding, (w <= arrow_size) ? ImDrawFlags_RoundCornersAll : ImDrawFlags_RoundCornersRight);
if (value_x2 + arrow_size - style.FramePadding.x <= frame_bb.Max.x)
RenderArrow(window->DrawList, ImVec2(value_x2 + style.FramePadding.y, frame_bb.Min.y + style.FramePadding.y), text_col, ImGuiDir_Down, 1.0f);
}
RenderFrameBorder(frame_bb.Min, frame_bb.Max, style.FrameRounding);
if (preview_value != NULL && !(flags & ImGuiComboFlags_NoPreview))
{
ImVec2 preview_pos = frame_bb.Min + style.FramePadding;
if (g.LogEnabled)
LogSetNextTextDecoration("{", "}");
RenderTextClipped(preview_pos, ImVec2(value_x2, frame_bb.Max.y), preview_value, NULL, NULL, ImVec2(0.0f, 0.0f));
}
if (label_size.x > 0)
RenderText(ImVec2(frame_bb.Max.x + style.ItemInnerSpacing.x, frame_bb.Min.y + style.FramePadding.y), label);
if ((pressed || g.NavActivateId == id) && !popup_open)
{
if (window->DC.NavLayerCurrent == 0)
window->NavLastIds[0] = id;
OpenPopupEx(id, ImGuiPopupFlags_None);
popup_open = true;
//-
//Reset animation
animation_progress = 0.0f;
scrollbar_alpha = 0.0f;
//-
}
if (!popup_open)
return false;
//-
ImVec2 clip_min = ImGui::GetCursorScreenPos();
clip_min.y -= style.FramePadding.y + style.PopupBorderSize;
//-
g.NextWindowData.HasFlags = backup_next_window_data_flags;
// Set popup size
if (g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSizeConstraint)
{
g.NextWindowData.SizeConstraintRect.Min.x = ImMax(g.NextWindowData.SizeConstraintRect.Min.x, w);
}
else
{
if ((flags & ImGuiComboFlags_HeightMask_) == 0)
flags |= ImGuiComboFlags_HeightRegular;
IM_ASSERT(ImIsPowerOfTwo(flags & ImGuiComboFlags_HeightMask_)); // Only one
int popup_max_height_in_items = -1;
if (flags & ImGuiComboFlags_HeightRegular) popup_max_height_in_items = 8;
else if (flags & ImGuiComboFlags_HeightSmall) popup_max_height_in_items = 4;
else if (flags & ImGuiComboFlags_HeightLarge) popup_max_height_in_items = 20;
ImVec2 constraint_min(0.0f, 0.0f), constraint_max(FLT_MAX, FLT_MAX);
if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.x <= 0.0f) // Don't apply constraints if user specified a size
constraint_min.x = w;
if ((g.NextWindowData.HasFlags & ImGuiNextWindowDataFlags_HasSize) == 0 || g.NextWindowData.SizeVal.y <= 0.0f)
constraint_max.y = CalcMaxPopupHeightFromItemCount(popup_max_height_in_items);
SetNextWindowSizeConstraints(constraint_min, constraint_max);
}
char name[16];
ImFormatString(name, IM_ARRAYSIZE(name), "##Combo_%02d", g.BeginPopupStack.Size); // Recycle windows based on depth
// Position the window given a custom constraint (peak into expected window size so we can position it)
// This might be easier to express with an hypothetical SetNextWindowPosConstraints() function.
if (ImGuiWindow* popup_window = FindWindowByName(name))
if (popup_window->WasActive)
{
// Always override 'AutoPosLastDirection' to not leave a chance for a past value to affect us.
ImVec2 size_expected = CalcWindowNextAutoFitSize(popup_window);
if (flags & ImGuiComboFlags_PopupAlignLeft)
popup_window->AutoPosLastDirection = ImGuiDir_Left; // "Below, Toward Left"
else
popup_window->AutoPosLastDirection = ImGuiDir_Down; // "Below, Toward Right (default)"
ImRect r_outer = GetPopupAllowedExtentRect(popup_window);
ImVec2 pos = FindBestWindowPosForPopupEx(frame_bb.GetBL(), size_expected, &popup_window->AutoPosLastDirection, r_outer, frame_bb, ImGuiPopupPositionPolicy_ComboBox);
//-
//Animated popup scrolling out of the widget
if (animation_progress < 1.0f)
{
animation_progress += ImGui::GetIO().DeltaTime * 5.0f;
}
else
{
animation_progress = 1.0f;
scrollbar_alpha = std::min(scrollbar_alpha + (ImGui::GetIO().DeltaTime * 5.0f), 1.0f);
}
popup_height = IM_ROUND(smoothstep(animation_progress, 0.0f, size_expected.y)); //popup_height grows to full size in animation
//Offset position by animation progress
const bool pos_goes_down = (popup_window->AutoPosLastDirection == ImGuiDir_Down) || (popup_window->AutoPosLastDirection == ImGuiDir_Left);
pos.y += (pos_goes_down) ? -size_expected.y + popup_height : size_expected.y - popup_height;
popup_expected_width = size_expected.x;
//-
SetNextWindowPos(pos);
}
//-
else
{
//Expected width is not calculated in the first frame, having it default to larger than w is avoids flicker if it is the next frame. Kinda hacky to be fair
popup_expected_width = FLT_MAX;
}
//-
//-
//Hide background and border while animating since they don't get clipped (they're drawn manually below)
if (animation_progress != 1.0f)
{
ImGui::PushStyleColor(ImGuiCol_Border, 0);
ImGui::PushStyleColor(ImGuiCol_PopupBg, 0);
}
//Fade-in scrollbar colors
ImVec4 col;
for (int i = ImGuiCol_ScrollbarBg; i <= ImGuiCol_ScrollbarGrabActive; ++i)
{
col = style.Colors[i];
col.w *= scrollbar_alpha;
ImGui::PushStyleColor(i, col);
}
//-
// We don't use BeginPopupEx() solely because we have a custom name string, which we could make an argument to BeginPopupEx()
ImGuiWindowFlags window_flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_Popup | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoMove;
//-
if (scrollbar_alpha == 0.0f)
{
if (popup_expected_width <= w) //With popups wider than the widget, we run into resize issues when toggling the scrollbar. So we skip it there as it's the lesser evil
{
window_flags |= ImGuiWindowFlags_NoScrollbar; //Disable scrollbar when not visible so it can't be clicked accidentally
}
}
//-
// Horizontally align ourselves with the framed text
PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(style.FramePadding.x, style.WindowPadding.y));
bool ret = Begin(name, NULL, window_flags);
PopStyleVar();
if (!ret)
{
EndPopup();
IM_ASSERT(0); // This should never happen as we tested for IsPopupOpen() above
return false;
}
//-
ImGui::PopStyleColor(4); //Scrollbar colors
if (animation_progress != 1.0f)
{
ImGui::PopStyleColor(2); //Border and PopupBg
ImGuiWindow* popup_window = GetCurrentWindow();
clip_min.x = popup_window->Pos.x;
ImVec2 clip_max = clip_min;
clip_max.x += popup_window->Size.x;
//Popup open direction
if ((popup_window->AutoPosLastDirection == ImGuiDir_Down) || (popup_window->AutoPosLastDirection == ImGuiDir_Left)) //ImGuiDir_Left is left aligned but still down
{
clip_max.y += popup_height + style.PopupBorderSize;
ImGui::PushClipRect(clip_min, clip_max, false);
clip_max.y -= style.PopupBorderSize;
popup_window->DrawList->AddRectFilled(clip_min, clip_max, GetColorU32(ImGuiCol_PopupBg));
popup_window->DrawList->AddRect(clip_min, clip_max, GetColorU32(ImGuiCol_Border));
clip_min.y += 1;
}
else //ImGuiDir_Up
{
clip_min.y -= frame_bb.GetHeight() + popup_height + style.PopupBorderSize;
clip_max.y -= frame_bb.GetHeight();
ImGui::PushClipRect(clip_min, clip_max, false);
clip_min.y += style.PopupBorderSize;
popup_window->DrawList->AddRectFilled(clip_min, clip_max, GetColorU32(ImGuiCol_PopupBg));
popup_window->DrawList->AddRect(clip_min, clip_max, GetColorU32(ImGuiCol_Border));
}
ImGui::PopClipRect();
//Push the clip rect for the window content... this doesn't get popped anywhere, but that seems to be harmless
ImGui::PushClipRect(GetCurrentWindow()->InnerClipRect.Min, GetCurrentWindow()->InnerClipRect.Max, false);
ImGui::PushClipRect(clip_min, clip_max, true);
}
//-
return true;
}
void TextRight(float offset_x, float fixed_w, const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
TextRightV(offset_x, fixed_w, fmt, args);
va_end(args);
}
void TextRightV(float offset_x, float fixed_w, const char* fmt, va_list args)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return;
ImGuiContext& g = *GImGui;
const char* text, *text_end;
ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
TextRightUnformatted(offset_x, fixed_w, text, text_end);
}
void TextRightUnformatted(float offset_x, float fixed_w, const char* text, const char* text_end)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return;
ImVec2 size = ImGui::CalcTextSize(text, text_end);
const float available_width = (fixed_w == 0.0f) ? ImGui::GetContentRegionAvail().x : fixed_w;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (available_width - size.x - offset_x));
TextEx(text, text_end, ImGuiTextFlags_NoWidthForLargeClippedText);
}
void TextColoredUnformatted(const ImVec4& col, const char* text, const char* text_end)
{
ImGui::PushStyleColor(ImGuiCol_Text, col);
ImGui::TextUnformatted(text, text_end);
ImGui::PopStyleColor();
}
void TextOutlined(const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
TextOutlinedV(fmt, args);
va_end(args);
}
void TextOutlinedV(const char* fmt, va_list args)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return;
const char* text, *text_end;
ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
TextUnformattedOutlined(text, text_end);
}
void TextUnformattedOutlined(const char* text, const char* text_end)
{
//This is mostly ImGui::TextUnformatted()
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return;
ImGuiContext& g = *GImGui;
// Accept null ranges
if (text == text_end)
text = text_end = "";
// Calculate length
const char* text_begin = text;
if (text_end == NULL)
text_end = text + ImStrlen(text); // FIXME-OPT
const ImVec2 pos(window->DC.CursorPos.x, window->DC.CursorPos.y + window->DC.CurrLineTextBaseOffset);
// Common case
const ImVec2 text_size = CalcTextSize(text_begin, text_end);
ImRect bb(pos, pos + text_size);
ItemSize(text_size, 0.0f);
if (!ItemAdd(bb, 0))
return;
// Render
const bool hide_text_after_hash = true;
// Hide anything after a '##' string
const char* text_display_end;
if (hide_text_after_hash)
{
text_display_end = FindRenderedTextEnd(text, text_end);
}
else
{
if (!text_end)
text_end = text + ImStrlen(text); // FIXME-OPT
text_display_end = text_end;
}
if (text != text_display_end)
{
const ImU32 col = GetColorU32(ImGuiCol_Text);
const ImU32 col_outline = GetColorU32(Style_ImGuiCol_TextOutline);
//This does indeed do 9x the AddText() calls of normal text, which is a bit wasteful but not too bad with the short strings this gets used with
ImVec2 pos_outline = pos;
pos_outline.y -= 1;
window->DrawList->AddText(g.Font, g.FontSize, pos_outline, col_outline, text, text_display_end);
pos_outline.x += 1;
window->DrawList->AddText(g.Font, g.FontSize, pos_outline, col_outline, text, text_display_end);
pos_outline.y += 1;
window->DrawList->AddText(g.Font, g.FontSize, pos_outline, col_outline, text, text_display_end);
pos_outline.y += 1;
window->DrawList->AddText(g.Font, g.FontSize, pos_outline, col_outline, text, text_display_end);
pos_outline.x -= 1;
window->DrawList->AddText(g.Font, g.FontSize, pos_outline, col_outline, text, text_display_end);
pos_outline.x -= 1;
window->DrawList->AddText(g.Font, g.FontSize, pos_outline, col_outline, text, text_display_end);
pos_outline.y -= 1;
window->DrawList->AddText(g.Font, g.FontSize, pos_outline, col_outline, text, text_display_end);
pos_outline.y -= 1;
window->DrawList->AddText(g.Font, g.FontSize, pos_outline, col_outline, text, text_display_end);
window->DrawList->AddText(g.Font, g.FontSize, pos, col, text, text_display_end);
if (g.LogEnabled)
LogRenderedText(&pos, text, text_display_end);
}
}
void TextRightOutlined(float offset_x, float fixed_w, const char* fmt, ...)
{
va_list args;
va_start(args, fmt);
TextRightOutlinedV(offset_x, fixed_w, fmt, args);
va_end(args);
}
void TextRightOutlinedV(float offset_x, float fixed_w, const char* fmt, va_list args)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return;
const char* text, *text_end;
ImFormatStringToTempBufferV(&text, &text_end, fmt, args);
TextRightUnformattedOutlined(offset_x, fixed_w, text, text_end);
}
void TextRightUnformattedOutlined(float offset_x, float fixed_w, const char* text, const char* text_end)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return;
const ImVec2 size = ImGui::CalcTextSize(text, text_end);
const float available_width = (fixed_w == 0.0f) ? ImGui::GetContentRegionAvail().x : fixed_w;
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (available_width - size.x - offset_x));
TextUnformattedOutlined(text, text_end);
}
void RenderTextClippedUnclamped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect)
{
// Hide anything after a '##' string
const char* text_display_end = FindRenderedTextEnd(text, text_end);
const int text_len = (int)(text_display_end - text);
if (text_len == 0)
return;
ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow;
//Aside from this line, this function is identical to ImGui::RenderTextClipped()
RenderTextClippedUnclampedEx(window->DrawList, pos_min, pos_max, text, text_display_end, text_size_if_known, align, clip_rect);
if (g.LogEnabled)
LogRenderedText(&pos_min, text, text_display_end);
}
void RenderTextClippedUnclampedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_display_end, const ImVec2* text_size_if_known, const ImVec2& align, const ImRect* clip_rect)
{
// Perform CPU side clipping for single clipped element to avoid using scissor state
ImVec2 pos = pos_min;
const ImVec2 text_size = text_size_if_known ? *text_size_if_known : CalcTextSize(text, text_display_end, false, 0.0f);
const ImVec2* clip_min = clip_rect ? &clip_rect->Min : &pos_min;
const ImVec2* clip_max = clip_rect ? &clip_rect->Max : &pos_max;
bool need_clipping = (pos.x + text_size.x >= clip_max->x) || (pos.y + text_size.y >= clip_max->y);
if (clip_rect) // If we had no explicit clipping rectangle then pos==clip_min
need_clipping |= (pos.x < clip_min->x) || (pos.y < clip_min->y);
// Align whole block. We should defer that to the better rendering function when we'll have support for individual line alignment.
//Aside from these two lines, this function is identical to ImGui::RenderTextClippedEx()
if (align.x > 0.0f) pos.x = pos.x + (pos_max.x - pos.x - text_size.x) * align.x;
if (align.y > 0.0f) pos.y = pos.y + (pos_max.y - pos.y - text_size.y) * align.y;
// Render
if (need_clipping)
{
ImVec4 fine_clip_rect(clip_min->x, clip_min->y, clip_max->x, clip_max->y);
draw_list->AddText(nullptr, 0.0f, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, &fine_clip_rect);
}
else
{
draw_list->AddText(nullptr, 0.0f, pos, GetColorU32(ImGuiCol_Text), text, text_display_end, 0.0f, nullptr);
}
}
int g_Stretched_BeginIndex = 0;
void BeginStretched()
{
g_Stretched_BeginIndex = ImGui::GetWindowDrawList()->VtxBuffer.Size;
}
void EndStretched(float scale_x)
{
auto& buffer = ImGui::GetWindowDrawList()->VtxBuffer;
const float base_x = (buffer.size() > g_Stretched_BeginIndex) ? buffer[g_Stretched_BeginIndex].pos.x : 0.0f;
for (int i = g_Stretched_BeginIndex; i < buffer.Size; ++i)
{
buffer[i].pos.x = ((buffer[i].pos.x - base_x) * scale_x) + base_x;
}
}
//Takes a nicely adjustable function and bolts it down to the options we need after making a few modifications
bool ColorPicker4Simple(const char* str_id, float col[4], float ref_col[4], const char* label_color_current, const char* label_color_original, float scale)
{
ImGuiWindow* window = GetCurrentWindow();
if (window->SkipItems)
return false;
//Hacky solution to make right mouse enable text input on the slider while not touching ImGui code or generalizing it as ctrl press
ImGuiIO& io = ImGui::GetIO();
const bool mouse_left_clicked_old = io.MouseClicked[ImGuiMouseButton_Left];
const float mouse_left_down_duration_old = io.MouseDownDuration[ImGuiMouseButton_Left];
const bool key_ctrl_old = io.KeyCtrl;
if (io.MouseClicked[ImGuiMouseButton_Right])
{
io.MouseDown[ImGuiMouseButton_Left] = true;
io.MouseClicked[ImGuiMouseButton_Left] = true;
io.MouseDownDuration[ImGuiMouseButton_Left] = 0.0f;
io.KeyCtrl = true;
io.KeyMods |= ImGuiMod_Ctrl; //Mods needs to stay consistent with KeyCtrl
}
ImGuiContext& g = *GImGui;
const ImGuiStyle& style = g.Style;
//Fixed flags, but we still kept some checks below in case we need to adjust later
const ImGuiColorEditFlags flags = ImGuiColorEditFlags_DisplayRGB | ImGuiColorEditFlags_Float | ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_NoSidePreview |
ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop;
const float start_y = ImGui::GetCursorScreenPos().y;
const float square_sz = ImGui::GetFrameHeight() * scale;
const float picker_width = square_sz * 15.5f;
const float picker_bar_height = picker_width - (2.0f * (square_sz + style.ItemInnerSpacing.x));
g.NextItemData.ClearFlags();
ImGui::BeginGroup();
ImGui::PushID(str_id);
bool value_changed = false;
ImGuiColorEditFlags picker_flags_to_forward = ImGuiColorEditFlags_DataTypeMask_ | ImGuiColorEditFlags_PickerMask_ | ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR |
ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_AlphaBar | ImGuiColorEditFlags_NoTooltip | ImGuiColorEditFlags_NoDragDrop;
ImGuiColorEditFlags picker_flags = (flags & picker_flags_to_forward) | ImGuiColorEditFlags_DisplayMask_ | ImGuiColorEditFlags_NoLabel | ImGuiColorEditFlags_NoSidePreview |
ImGuiColorEditFlags_NoSmallPreview | ImGuiColorEditFlags_NoOptions;
ImGui::SetNextItemWidth(picker_width);
value_changed |= ImGui::ColorPicker4("##picker", col, picker_flags, &g.ColorPickerRef.x);
//Restore hack
io.MouseDown[ImGuiMouseButton_Left] = mouse_left_clicked_old;
io.MouseClicked[ImGuiMouseButton_Left] = mouse_left_clicked_old;
io.MouseDownDuration[ImGuiMouseButton_Left] = mouse_left_down_duration_old;
io.KeyCtrl = key_ctrl_old;
if (!io.KeyCtrl)
{
io.KeyMods &= ~ImGuiMod_Ctrl;
}
//Picker style switching without popup
if ( (ImGui::IsItemClicked(ImGuiMouseButton_Right)) && (ImGui::GetMousePos().y <= start_y + picker_bar_height) ) //(don't react to text input clicks)
{
ImGuiColorEditFlags picker_flags = (g.ColorEditOptions & ImGuiColorEditFlags_PickerHueBar) ? ImGuiColorEditFlags_PickerHueWheel : ImGuiColorEditFlags_PickerHueBar;
g.ColorEditOptions = (g.ColorEditOptions & ~ImGuiColorEditFlags_PickerMask_) | (picker_flags & ImGuiColorEditFlags_PickerMask_);
}
//Side previews, but translatable
ImGui::SameLine(0.0f, style.ItemInnerSpacing.x);
window->DC.CursorPos.y -= style.ItemSpacing.y;
ImGui::BeginGroup();
ImGui::PushItemFlag(ImGuiItemFlags_NoNavDefaultFocus, true);
ImVec4 col_v4(col[0], col[1], col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : col[3]);
if ((flags & ImGuiColorEditFlags_NoLabel))
ImGui::TextUnformatted(label_color_current);
ImGuiColorEditFlags sub_flags_to_forward = ImGuiColorEditFlags_InputMask_ | ImGuiColorEditFlags_HDR | ImGuiColorEditFlags_AlphaPreviewHalf | ImGuiColorEditFlags_NoTooltip |
ImGuiColorEditFlags_NoDragDrop;
ImGui::ColorButton("##current", col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2));
if (ref_col != nullptr)
{
ImGui::TextUnformatted(label_color_original);
ImVec4 ref_col_v4(ref_col[0], ref_col[1], ref_col[2], (flags & ImGuiColorEditFlags_NoAlpha) ? 1.0f : ref_col[3]);
if (ImGui::ColorButton("##original", ref_col_v4, (flags & sub_flags_to_forward), ImVec2(square_sz * 3, square_sz * 2)))
{
memcpy(col, ref_col, ((flags & ImGuiColorEditFlags_NoAlpha) ? 3 : 4) * sizeof(float));
value_changed = true;
}
}
ImGui::PopItemFlag(); //ImGuiItemFlags_NoNavDefaultFocus
ImGui::EndGroup();
ImGui::PopID();
ImGui::EndGroup();
if (value_changed)
ImGui::MarkItemEdited(g.LastItemData.ID);
return value_changed;
}
bool CollapsingHeaderPadded(const char* label, ImGuiTreeNodeFlags flags)
{
ImGuiWindow* window = GetCurrentWindow();
//Move back a pixel to fix CollapsingHeader()'s position being off by one for some reason
window->DC.CursorPos.x -= 1.0f;
//Temporarily modify window padding to fool CollapsingHeader to size differently
const float padding_x = ImGui::GetStyle().ItemInnerSpacing.x / 2.0f;
window->WindowPadding.x -= padding_x;
bool ret = ImGui::CollapsingHeader(label, flags);
window->WindowPadding.x += padding_x;
return ret;
}
struct CollapsingAreaState
{
ImGuiID WidgetID = 0;
float WidgetBeginY = 0.0f;
bool PushedClipRect = false;
bool PushedItemDisabled = false;
};
ImVector g_CollapsingArea_Stack;
void BeginCollapsingArea(const char* str_id, bool show_content, float& persist_animation_progress)
{
g_CollapsingArea_Stack.push_back(CollapsingAreaState());
CollapsingAreaState& state = g_CollapsingArea_Stack.back();
//Animate when changing between show_content state
const float animation_step = ImGui::GetIO().DeltaTime * 3.0f;
persist_animation_progress = clamp(persist_animation_progress + ((show_content) ? animation_step : -animation_step), 0.0f, 1.0f);
//Set clipping
state.WidgetID = ImGui::GetID(str_id);
//Don't push clip rect on full progress to allow for popups and such
if (persist_animation_progress != 1.0f)
{
const float content_height = ImGui::GetStateStorage()->GetFloat(state.WidgetID, 0.0f);
const float clip_height = smoothstep(persist_animation_progress, 0.0f, content_height);
ImVec2 clip_begin = ImGui::GetCursorScreenPos();
ImVec2 clip_end(clip_begin.x + ImGui::GetContentRegionAvail().x + ImGui::GetStyle().ItemSpacing.x, clip_begin.y + clip_height);
ImGui::PushClipRect(clip_begin, clip_end, true);
state.PushedClipRect = true;
//Pull cursor position further up to make widgets scroll down as they animate, keep start Y-pos in global to use in the next EndCollapsingArea() call
state.WidgetBeginY = (ImGui::GetCursorPosY() - content_height) + clip_height;
ImGui::SetCursorPosY(state.WidgetBeginY);
if (persist_animation_progress == 0.0f)
{
PushItemDisabledNoVisual();
state.PushedItemDisabled = true;
}
}
}
void EndCollapsingArea()
{
IM_ASSERT(!g_CollapsingArea_Stack.empty() && "Called EndCollapsingArea() before BeginCollapsingArea()");
CollapsingAreaState& state = g_CollapsingArea_Stack.back();
float& content_height = *ImGui::GetStateStorage()->GetFloatRef(state.WidgetID);
content_height = ImGui::GetCursorPosY() - state.WidgetBeginY;
if (state.PushedClipRect)
{
ImGui::PopClipRect();
}
if (state.PushedItemDisabled)
{
PopItemDisabledNoVisual();
}
g_CollapsingArea_Stack.pop_back();
}
void PushItemDisabled()
{
const ImGuiStyle& style = ImGui::GetStyle();
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, style.Alpha * style.DisabledAlpha);
}
void PopItemDisabled()
{
ImGui::PopItemFlag();
ImGui::PopStyleVar();
}
void PushItemDisabledNoVisual()
{
ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true);
}
void PopItemDisabledNoVisual()
{
ImGui::PopItemFlag();
}
void ConfigDisableCtrlTab()
{
GImGui->ConfigNavWindowingKeyNext = ImGuiKey_None;
GImGui->ConfigNavWindowingKeyPrev = ImGuiKey_None;
}
bool PopupContextMenuInputText(const char* str_id, char* str_buffer, size_t buffer_size, bool paste_remove_newlines)
{
bool ret = false;
if (ImGui::BeginPopupContextItem(str_id))
{
if (ImGui::MenuItem("Copy all"))
{
ImGui::SetClipboardText(str_buffer);
}
if (ImGui::MenuItem("Replace with Clipboard", nullptr, false, (ImGui::GetClipboardText() != nullptr) ))
{
std::string str(ImGui::GetClipboardText());
if (paste_remove_newlines)
{
//Remove newlines (all kinds, since you never know what might be in a clipboard)
std::string newlines[] = {"\r\n", "\n", "\r"};
for (auto& nline : newlines)
{
size_t start_pos = 0;
while ((start_pos = str.find(nline, start_pos)) != std::string::npos)
{
str.replace(start_pos, nline.length(), " ");
}
}
}
//Copy to buffer
size_t copied_length = str.copy(str_buffer, buffer_size - 1);
str_buffer[copied_length] = '\0';
ret = true;
}
ImGui::EndPopup();
}
return ret;
}
//ImGui got rid of direct nav input access, but we keep it around as an abstraction layer
ImGuiKey MapNavToKey(ImGuiNavInput nav_input, ImGuiInputSource input_source)
{
ImGuiKey imgui_key = ImGuiKey_None;
if (input_source == ImGuiInputSource_Keyboard)
{
switch (nav_input)
{
case ImGuiNavInput_Activate: imgui_key = ImGuiKey_Space; break;
case ImGuiNavInput_Cancel: imgui_key = ImGuiKey_Escape; break;
case ImGuiNavInput_Input: imgui_key = ImGuiKey_Enter; break;
case ImGuiNavInput_Menu: imgui_key = ImGuiMod_Alt; break;
case ImGuiNavInput_DpadLeft: imgui_key = ImGuiKey_LeftArrow; break;
case ImGuiNavInput_DpadRight: imgui_key = ImGuiKey_RightArrow; break;
case ImGuiNavInput_DpadUp: imgui_key = ImGuiKey_UpArrow; break;
case ImGuiNavInput_DpadDown: imgui_key = ImGuiKey_DownArrow; break;
case ImGuiNavInput_TweakSlow: imgui_key = ImGuiMod_Ctrl; break;
case ImGuiNavInput_TweakFast: imgui_key = ImGuiMod_Shift; break;
}
}
else if (input_source == ImGuiInputSource_Gamepad)
{
switch (nav_input)
{
case ImGuiNavInput_Activate: imgui_key = ImGuiKey_GamepadFaceDown; break;
case ImGuiNavInput_Cancel: imgui_key = ImGuiKey_GamepadFaceRight; break;
case ImGuiNavInput_Input: imgui_key = ImGuiKey_GamepadFaceLeft; break;
case ImGuiNavInput_Menu: imgui_key = ImGuiKey_GamepadFaceUp; break;
case ImGuiNavInput_DpadLeft: imgui_key = ImGuiKey_GamepadDpadLeft; break;
case ImGuiNavInput_DpadRight: imgui_key = ImGuiKey_GamepadDpadRight; break;
case ImGuiNavInput_DpadUp: imgui_key = ImGuiKey_GamepadDpadUp; break;
case ImGuiNavInput_DpadDown: imgui_key = ImGuiKey_GamepadDpadDown; break;
case ImGuiNavInput_TweakSlow: imgui_key = ImGuiKey_GamepadL1; break;
case ImGuiNavInput_TweakFast: imgui_key = ImGuiKey_GamepadR1; break;
}
}
return imgui_key;
}
bool IsNavInputDown(ImGuiNavInput nav_input)
{
return ImGui::IsKeyDown(MapNavToKey(nav_input, GImGui->NavInputSource));
}
bool ImGui::IsNavInputPressed(ImGuiNavInput nav_input, bool repeat)
{
return ImGui::IsKeyPressed(MapNavToKey(nav_input, GImGui->NavInputSource), repeat);
}
bool ImGui::IsNavInputReleased(ImGuiNavInput nav_input)
{
return ImGui::IsKeyReleased(MapNavToKey(nav_input, GImGui->NavInputSource));
}
float ImGui::GetPreviousLineHeight()
{
return GImGui->CurrentWindow->DC.PrevLineSize.y;
}
void SetPreviousLineHeight(float height)
{
GImGui->CurrentWindow->DC.PrevLineSize.y = height;
}
bool HasHoveredNewItem()
{
const ImGuiContext& g = *GImGui;
bool blocked_by_active_item = (g.ActiveId != 0 && !g.ActiveIdAllowOverlap);
return ( (g.HoveredId != g.HoveredIdPreviousFrame) && (g.HoveredId != 0) && (!g.HoveredIdIsDisabled) && (!blocked_by_active_item) );
}
bool IsAnyItemActiveOrDeactivated()
{
const ImGuiContext& g = *GImGui;
return ( (g.ActiveId != 0) || (g.ActiveIdPreviousFrame != 0) );
}
bool IsAnyItemDeactivated()
{
const ImGuiContext& g = *GImGui;
return ( (g.ActiveIdPreviousFrame != 0) && (g.ActiveId != g.ActiveIdPreviousFrame) );
}
bool IsAnyInputTextActive()
{
const ImGuiContext& g = *GImGui;
return ( (g.ActiveId != 0) && (g.ActiveId == g.InputTextState.ID) );
}
bool IsAnyTempInputTextActive()
{
const ImGuiContext& g = *GImGui;
return ( (g.ActiveId != 0) && (g.ActiveId == g.InputTextState.ID) && (g.TempInputId == g.InputTextState.ID) );
}
bool IsAnyMouseClicked()
{
const ImGuiContext& g = *GImGui;
for (int n = 0; n < IM_ARRAYSIZE(g.IO.MouseDown); n++)
if (g.IO.MouseClicked[n])
return true;
return false;
}
void BlockWidgetInput()
{
ImGuiContext& g = *GImGui;
ImGui::SetActiveID(ImGui::GetID("ImGuiExtInputBlock"), nullptr);
ImGui::SetKeyOwner(ImGuiKey_MouseWheelX, g.ActiveId);
ImGui::SetKeyOwner(ImGuiKey_MouseWheelY, g.ActiveId);
g.WheelingWindow = nullptr;
g.WheelingWindowReleaseTimer = 0.0f;
}
void HScrollWindowFromMouseWheelV()
{
ImGuiContext& g = *GImGui;
//Don't do anything is real hscroll is active
if ( (g.IO.MouseWheelH != 0.0f && !g.IO.KeyShift) || (g.IO.MouseWheel != 0.0f && g.IO.KeyShift) )
return;
ImGuiWindow* window = ImGui::GetCurrentWindow();
if (g.WheelingWindow != window)
return;
const float wheel_x = g.IO.MouseWheel;
float max_step = window->InnerRect.GetWidth() * 0.67f;
float scroll_step = ImTrunc(ImMin(2.0f * window->CalcFontSize(), max_step));
ImGui::SetScrollX(window, window->Scroll.x - wheel_x * scroll_step);
}
void ScrollBeginStackParentWindow()
{
ImGuiContext& g = *GImGui;
ImGuiWindow* window = ImGui::GetCurrentWindowRead();
if (g.WheelingWindow != window)
return;
ImGuiWindow* window_target = window->ParentWindowInBeginStack;
if (window_target == nullptr)
return;
const float wheel_x = g.IO.MouseWheelH;
const float wheel_y = g.IO.MouseWheel;
//HScroll
float max_step = window_target->InnerRect.GetWidth() * 0.67f;
float scroll_step = ImTrunc(ImMin(2.0f * window_target->CalcFontSize(), max_step));
ImGui::SetScrollX(window_target, window_target->Scroll.x - wheel_x * scroll_step);
//VScroll
max_step = window_target->InnerRect.GetHeight() * 0.67f;
scroll_step = ImTrunc(ImMin(5.0f * window_target->CalcFontSize(), max_step));
ImGui::SetScrollY(window_target, window_target->Scroll.y - wheel_y * scroll_step);
}
bool IsAnyScrollBarVisible()
{
ImGuiWindow* window = ImGui::GetCurrentWindowRead();
return ((window->ScrollbarX) || (window->ScrollbarY));
}
ImVec4 BeginTitleBar()
{
ImGuiStyle& style = ImGui::GetStyle();
ImRect rect = ImGui::GetCurrentWindowRead()->TitleBarRect();
ImGui::SetCursorScreenPos({rect.Min.x + style.WindowBorderSize + style.FramePadding.x, rect.Min.y + style.FramePadding.y});
ImGui::PushClipRect(rect.Min, rect.Max, false);
return ImVec4(rect.Min.x, rect.Min.y, rect.Max.x, rect.Max.y);
}
void EndTitleBar()
{
ImGui::PopClipRect();
ImGuiWindow* window = ImGui::GetCurrentWindow();
ImGui::SetCursorScreenPos(window->ContentRegionRect.Min);
window->DC.CursorMaxPos.x = 0; //Title bar does *not* affect overall window width as it messes with auto-resize
}
bool StringContainsUnmappedCharacter(const char* str)
{
if (GImGui == nullptr)
return true;
//Use current style font if possible, otherwise fall back to default font
ImFont* font = (GImGui->FontStack.empty()) ? ImGui::GetDefaultFont() : GImGui->FontStack.back();
if (font == nullptr)
return true;
const char* str_end = str + strlen(str);
ImWchar32 c;
int decoded_length;
while (str < str_end)
{
decoded_length = ImTextCharFromUtf8(&c, str, str_end);
if ( (c >= ' ') && (font->FindGlyphNoFallback((ImWchar)c) == nullptr) ) //Don't return true on unprintable characters being unmapped
{
return true;
}
str += decoded_length;
}
return false;
}
std::string StringEllipsis(const char* str, float width_max)
{
//Naive approach, but we're not supposed to use RenderTextEllipsis(), so this will do
const char* str_begin = str;
const char* str_end = str + strlen(str);
//Handle corner case of text fitting without ellipsis, but not with
if (ImGui::CalcTextSize(str, str_end).x <= width_max)
{
return str;
}
float ellipsis_width = ImGui::CalcTextSize("...").x;
bool is_full_string = true;
ImWchar32 c;
int decoded_length = 0;
while (str < str_end)
{
if (ImGui::CalcTextSize(str_begin, str).x + ellipsis_width >= width_max)
{
str -= decoded_length;
is_full_string = false;
break;
}
decoded_length = ImTextCharFromUtf8(&c, str, str_end);
str += decoded_length;
}
std::string str_out(str_begin, str - str_begin);
if (!is_full_string)
{
str_out.append("...");
}
return str_out;
}
bool DraggableRectArea(const char* str_id, const ImVec2& area_size, ImGuiDraggableRectAreaState& state)
{
ImGuiIO& io = ImGui::GetIO();
ImVec2& offset = state.RectPos;
ImVec2& size = state.RectSize;
ImVec2& offset_start = state.RectPosDragStart;
ImVec2& size_start = state.RectSizeDragStart;
ImVec2 offset_prev = offset;
ImVec2 size_prev = size;
ImGuiMouseButton& drag_mouse_button = state.DragMouseButton;
ImGuiDir& edge_drag_h_dir = state.EdgeDragHDir;
ImGuiDir& edge_drag_v_dir = state.EdgeDragVDir;
bool& is_edge_drag_active = state.EdgeDragActive;
bool& highlight_edge = state.EdgeDragHighlightVisible;
bool is_drag_active = false;
//This widget is mouse-only and interacts weirdly with the ImGui navigation.
//I've been unable to get ImGui to skip over it or bend it to work in sane manner, so I've resorted to disabling the items when nav is visible
const bool was_disabled_initially = (GImGui->CurrentItemFlags & ImGuiItemFlags_Disabled);
const bool disable_for_nav = (io.NavVisible && !ImGui::IsMouseClicked(ImGuiMouseButton_Left));
if (disable_for_nav)
ImGui::PushItemDisabledNoVisual();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {0.0f, 0.0f});
if (ImGui::BeginChild(str_id, area_size, ImGuiChildFlags_Borders | ImGuiChildFlags_NavFlattened, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse))
{
const float drag_threshold_prev = io.MouseDragThreshold;
io.MouseDragThreshold = 0.0f;
ImVec2 pos_area = ImGui::GetCursorScreenPos();
ImVec2 pos_button = pos_area;
pos_button.x += roundf(offset.x);
pos_button.y += roundf(offset.y);
const float drag_margin = ImGui::GetFontSize() / 2.0f;
//Draw rectangle manually
ImVec4 col_button = ImGui::GetStyleColorVec4(ImGuiCol_ButtonHovered);
ImVec4 col_fill = col_button;
col_fill.w = 0.25f;
ImGui::GetWindowDrawList()->AddRectFilled(pos_button, {roundf(pos_button.x + size.x), roundf(pos_button.y + size.y)}, ImGui::GetColorU32(col_fill));
ImGui::GetWindowDrawList()->AddRect(pos_button, {roundf(pos_button.x + size.x), roundf(pos_button.y + size.y)}, ImGui::GetColorU32(col_button));
if (highlight_edge)
{
if ((size.x > drag_margin * 2.0f) && (size.y > drag_margin * 2.0f))
{
col_fill.w = 0.10f;
//This highlights the inner part actually, but still does the trick
ImGui::GetWindowDrawList()->AddRectFilled({pos_button.x + drag_margin, pos_button.y + drag_margin},
{roundf(pos_button.x + size.x - drag_margin), roundf(pos_button.y + size.y - drag_margin)},
ImGui::GetColorU32(col_fill));
}
}
highlight_edge = false;
//Invisible button spanning the entire area to catch right clicks anywhere for relative drag
ImGui::SetNextItemAllowOverlap();
if (ImGui::InvisibleButton("DraggableRectArea", area_size, ImGuiButtonFlags_MouseButtonRight | ImGuiButtonFlags_PressedOnClick))
{
offset_start = offset;
size_start = size;
drag_mouse_button = ImGuiMouseButton_Right;
edge_drag_h_dir = ImGuiDir_None;
edge_drag_v_dir = ImGuiDir_None;
is_edge_drag_active = false;
}
else if (ImGui::IsItemActive())
{
is_drag_active = true;
}
//Pad the button size/pos to allow for off-edge grabs
ImVec2 pos_button_padded = pos_button;
pos_button_padded.x -= drag_margin / 2.0f;
pos_button_padded.y -= drag_margin / 2.0f;
ImVec2 size_button_padded = size;
size_button_padded.x += drag_margin;
size_button_padded.y += drag_margin;
ImGui::SetCursorScreenPos(pos_button_padded);
if (ImGui::InvisibleButton("DraggableRect", size_button_padded, ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_PressedOnClick | ImGuiButtonFlags_AllowOverlap))
{
offset_start = offset;
size_start = size;
drag_mouse_button = ImGuiMouseButton_Left;
const ImVec2 pos(io.MouseClickedPos[drag_mouse_button].x - pos_area.x, io.MouseClickedPos[drag_mouse_button].y - pos_area.y);
edge_drag_h_dir = ImGuiDir_None;
edge_drag_v_dir = ImGuiDir_None;
if (pos.x < offset.x + drag_margin)
edge_drag_h_dir = ImGuiDir_Left;
else if (pos.x > offset.x + size.x - drag_margin)
edge_drag_h_dir = ImGuiDir_Right;
if (pos.y < offset.y + drag_margin)
edge_drag_v_dir = ImGuiDir_Up;
else if (pos.y > offset.y + size.y - drag_margin)
edge_drag_v_dir = ImGuiDir_Down;
is_edge_drag_active = ((edge_drag_h_dir != ImGuiDir_None) || (edge_drag_v_dir != ImGuiDir_None));
}
else if (ImGui::IsItemActive())
{
is_drag_active = true;
}
if (is_drag_active)
{
if (is_edge_drag_active)
{
if (edge_drag_h_dir == ImGuiDir_Left)
{
size.x = size_start.x - ImGui::GetMouseDragDelta(drag_mouse_button).x;
offset.x = offset_start.x - (size.x - size_start.x);
if (offset.x < 0.0f)
{
size.x += offset.x;
offset.x = 0.0f;
}
else if (offset.x > area_size.x) //When dragged to opposite edge (width is negative)
{
size.x -= area_size.x - offset.x;
offset.x = area_size.x;
}
}
else if (edge_drag_h_dir == ImGuiDir_Right)
{
size.x = size_start.x + ImGui::GetMouseDragDelta(drag_mouse_button).x;
size.x = std::min(size.x, area_size.x - offset.x);
offset.x = offset_start.x;
if (size.x + offset.x < 0.0f) //When dragged to opposite edge (width is negative)
{
size.x -= size.x + offset.x;
}
}
if (edge_drag_v_dir == ImGuiDir_Up)
{
size.y = size_start.y - ImGui::GetMouseDragDelta(drag_mouse_button).y;
offset.y = offset_start.y - (size.y - size_start.y);
if (offset.y < 0.0f)
{
size.y += offset.y;
offset.y = 0.0f;
}
else if (offset.y > area_size.y) //When dragged to opposite edge (height is negative)
{
size.y -= area_size.y - offset.y;
offset.y = area_size.y;
}
}
else if (edge_drag_v_dir == ImGuiDir_Down)
{
size.y = size_start.y + ImGui::GetMouseDragDelta(drag_mouse_button).y;
size.y = std::min(size.y, area_size.y - offset.y);
offset.y = offset_start.y;
if (size.y + offset.y < 0.0f) //When dragged to opposite edge (height is negative)
{
size.y -= size.y + offset.y;
}
}
}
else //Normal drag
{
//Alt key scroll swapping is usually already done by ImGui but seems to be blocked by something down the line so we do it ourselves, whatever
if (io.KeyAlt)
{
io.MouseWheelH = -io.MouseWheel;
io.MouseWheel = 0.0f;
}
//Adjust size from wheel input (with deadzone for smooth scrolling input)
if (fabs(io.MouseWheelH) > 0.05f)
{
float size_diff = size.x;
size.x *= 1.0f + (io.MouseWheelH / -10.0f);
size_diff = size.x - size_diff;
offset_start.x -= size_diff / 2.0f;
}
if (fabs(io.MouseWheel) > 0.05f)
{
float size_diff = size.y;
size.y *= 1.0f + (io.MouseWheel / 10.0f);
size_diff = size.y - size_diff;
offset_start.y -= size_diff / 2.0f;
}
offset.x = offset_start.x + ImGui::GetMouseDragDelta(drag_mouse_button).x;
offset.y = offset_start.y + ImGui::GetMouseDragDelta(drag_mouse_button).y;
}
//Correct inverted rectangle
if (size.x < 0.0f)
{
offset.x += size.x;
size.x *= -1.0f;
}
if (size.y < 0.0f)
{
offset.y += size.y;
size.y *= -1.0f;
}
//Clamp to area
offset.x = clamp(offset.x, 0.0f, area_size.x - size.x);
offset.y = clamp(offset.y, 0.0f, area_size.y - size.y);
size.x = clamp(size.x, 1.0f, area_size.x);
size.y = clamp(size.y, 1.0f, area_size.y);
}
else
{
highlight_edge = ImGui::IsItemHovered( (was_disabled_initially) ? 0 : ImGuiHoveredFlags_AllowWhenDisabled );
}
io.MouseDragThreshold = drag_threshold_prev;
}
ImGui::EndChild();
ImGui::PopStyleVar();
if (disable_for_nav)
ImGui::PopItemDisabledNoVisual();
return ( (is_drag_active) && ((offset.x != offset_prev.x) || (offset.y != offset_prev.y) ||
(size.x != size_prev.x) || (size.y != size_prev.y)) );
}
ImGuiMouseState::ImGuiMouseState()
{
//Set everything to zero
memset(this, 0, sizeof(*this));
MousePos = MousePosPrev = ImVec2(-FLT_MAX,-FLT_MAX);
}
void ImGuiMouseState::SetFromGlobalState()
{
const ImGuiIO& io = ImGui::GetIO();
MousePos = io.MousePos;
std::copy(std::begin(io.MouseDown), std::end(io.MouseDown), MouseDown);
MouseWheel = io.MouseWheel;
MouseWheelH = io.MouseWheelH;
MouseDelta = io.MouseDelta;
MousePosPrev = io.MousePosPrev;
std::copy(std::begin(io.MouseClickedPos), std::end(io.MouseClickedPos), MouseClickedPos);
std::copy(std::begin(io.MouseClickedTime), std::end(io.MouseClickedTime), MouseClickedTime);
std::copy(std::begin(io.MouseClicked), std::end(io.MouseClicked), MouseClicked);
std::copy(std::begin(io.MouseDoubleClicked), std::end(io.MouseDoubleClicked), MouseDoubleClicked);
std::copy(std::begin(io.MouseClickedCount), std::end(io.MouseClickedCount), MouseClickedCount);
std::copy(std::begin(io.MouseClickedLastCount), std::end(io.MouseClickedLastCount), MouseClickedLastCount);
std::copy(std::begin(io.MouseReleased), std::end(io.MouseReleased), MouseReleased);
std::copy(std::begin(io.MouseDownOwned), std::end(io.MouseDownOwned), MouseDownOwned);
std::copy(std::begin(io.MouseDownOwnedUnlessPopupClose), std::end(io.MouseDownOwnedUnlessPopupClose), MouseDownOwnedUnlessPopupClose);
std::copy(std::begin(io.MouseDownDuration), std::end(io.MouseDownDuration), MouseDownDuration);
std::copy(std::begin(io.MouseDownDurationPrev), std::end(io.MouseDownDurationPrev), MouseDownDurationPrev);
std::copy(std::begin(io.MouseDragMaxDistanceSqr), std::end(io.MouseDragMaxDistanceSqr), MouseDragMaxDistanceSqr);
}
void ImGuiMouseState::ApplyToGlobalState()
{
ImGuiIO& io = ImGui::GetIO();
io.MousePos = MousePos;
std::copy(std::begin(MouseDown), std::end(MouseDown), io.MouseDown);
io.MouseWheel = MouseWheel;
io.MouseWheelH = MouseWheelH;
io.MouseDelta = MouseDelta;
io.MousePosPrev = MousePosPrev;
std::copy(std::begin(MouseClickedPos), std::end(MouseClickedPos), io.MouseClickedPos);
std::copy(std::begin(MouseClickedTime), std::end(MouseClickedTime), io.MouseClickedTime);
std::copy(std::begin(MouseClicked), std::end(MouseClicked), io.MouseClicked);
std::copy(std::begin(MouseDoubleClicked), std::end(MouseDoubleClicked), io.MouseDoubleClicked);
std::copy(std::begin(MouseClickedCount), std::end(MouseClickedCount), io.MouseClickedCount);
std::copy(std::begin(MouseClickedLastCount), std::end(MouseClickedLastCount), io.MouseClickedLastCount);
std::copy(std::begin(MouseReleased), std::end(MouseReleased), io.MouseReleased);
std::copy(std::begin(MouseDownOwned), std::end(MouseDownOwned), io.MouseDownOwned);
std::copy(std::begin(MouseDownOwnedUnlessPopupClose), std::end(MouseDownOwnedUnlessPopupClose), io.MouseDownOwnedUnlessPopupClose);
std::copy(std::begin(MouseDownDuration), std::end(MouseDownDuration), io.MouseDownDuration);
std::copy(std::begin(MouseDownDurationPrev), std::end(MouseDownDurationPrev), io.MouseDownDurationPrev);
std::copy(std::begin(MouseDragMaxDistanceSqr), std::end(MouseDragMaxDistanceSqr), io.MouseDragMaxDistanceSqr);
}
void ImGuiMouseState::Advance()
{
//This is almost just ImGui::UpdateMouseInputs(), but we don't touch any global state here
const ImGuiContext& g = *ImGui::GetCurrentContext();
// Round mouse position to avoid spreading non-rounded position (e.g. UpdateManualResize doesn't support them well)
if (IsMousePosValid(&MousePos))
MousePos = ImFloor(MousePos);
// If mouse just appeared or disappeared (usually denoted by -FLT_MAX components) we cancel out movement in MouseDelta
if (IsMousePosValid(&MousePos) && IsMousePosValid(&MousePosPrev))
MouseDelta = MousePos - MousePosPrev;
else
MouseDelta = ImVec2(0.0f, 0.0f);
MousePosPrev = MousePos;
for (int i = 0; i < IM_ARRAYSIZE(MouseDown); i++)
{
MouseClicked[i] = MouseDown[i] && MouseDownDuration[i] < 0.0f;
MouseClickedCount[i] = 0; // Will be filled below
MouseReleased[i] = !MouseDown[i] && MouseDownDuration[i] >= 0.0f;
MouseDownDurationPrev[i] = MouseDownDuration[i];
MouseDownDuration[i] = MouseDown[i] ? (MouseDownDuration[i] < 0.0f ? 0.0f : MouseDownDuration[i] + g.IO.DeltaTime) : -1.0f;
if (MouseClicked[i])
{
bool is_repeated_click = false;
if ((float)(g.Time - MouseClickedTime[i]) < g.IO.MouseDoubleClickTime)
{
ImVec2 delta_from_click_pos = IsMousePosValid(&MousePos) ? (MousePos - MouseClickedPos[i]) : ImVec2(0.0f, 0.0f);
if (ImLengthSqr(delta_from_click_pos) < g.IO.MouseDoubleClickMaxDist * g.IO.MouseDoubleClickMaxDist)
is_repeated_click = true;
}
if (is_repeated_click)
MouseClickedLastCount[i]++;
else
MouseClickedLastCount[i] = 1;
MouseClickedTime[i] = g.Time;
MouseClickedPos[i] = MousePos;
MouseClickedCount[i] = MouseClickedLastCount[i];
MouseDragMaxDistanceSqr[i] = 0.0f;
}
else if (MouseDown[i])
{
// Maintain the maximum distance we reaching from the initial click position, which is used with dragging threshold
float delta_sqr_click_pos = IsMousePosValid(&MousePos) ? ImLengthSqr(MousePos - MouseClickedPos[i]) : 0.0f;
MouseDragMaxDistanceSqr[i] = ImMax(MouseDragMaxDistanceSqr[i], delta_sqr_click_pos);
}
// We provide io.MouseDoubleClicked[] as a legacy service
MouseDoubleClicked[i] = (MouseClickedCount[i] == 2);
}
}
ActiveWidgetStateStorage::ActiveWidgetStateStorage()
{
//These need to be the same or things will explode
IM_ASSERT(sizeof(ImGuiDeactivatedItemDataInternal) == sizeof(DeactivatedItemData));
IM_ASSERT(sizeof(ActiveIdValueOnActivation) == sizeof(ActiveIdValueOnActivation));
IsInitialized = false;
HoveredId = 0;
HoveredIdPreviousFrame = 0;
HoveredIdPreviousFrameItemCount = 0;
HoveredIdTimer = 0.0f;
HoveredIdNotActiveTimer = 0.0f;
HoveredIdAllowOverlap = false;
HoveredIdIsDisabled = false;
ItemUnclipByLog = false;
ActiveId = 0;
ActiveIdIsAlive = 0;
ActiveIdTimer = 0.0f;
ActiveIdIsJustActivated = false;
ActiveIdAllowOverlap = false;
ActiveIdNoClearOnFocusLoss = false;
ActiveIdHasBeenPressedBefore = false;
ActiveIdHasBeenEditedBefore = false;
ActiveIdHasBeenEditedThisFrame = false;
ActiveIdFromShortcut = false;
ActiveIdUsingNavDirMask = 0x00;
ActiveIdClickOffset = ImVec2(-1, -1);
ActiveIdWindow = nullptr;
ActiveIdSource = ImGuiInputSource_None;
ActiveIdMouseButton = -1;
ActiveIdPreviousFrame = 0;
memset(&DeactivatedItemData, 0, sizeof(DeactivatedItemData));
memset(&ActiveIdValueOnActivation, 0, sizeof(ActiveIdValueOnActivation));
LastActiveId = 0;
LastActiveIdTimer = 0.0f;
ActiveIdUsingNavDirMask = 0;
ActiveIdUsingAllKeyboardKeys = false;
}
void ActiveWidgetStateStorage::StoreCurrentState()
{
IsInitialized = true;
ImGuiContext& g = *ImGui::GetCurrentContext();
HoveredId = g.HoveredId;
HoveredIdPreviousFrame = g.HoveredIdPreviousFrame;
HoveredIdPreviousFrameItemCount = g.HoveredIdPreviousFrameItemCount;
HoveredIdTimer = g.HoveredIdTimer;
HoveredIdNotActiveTimer = g.HoveredIdNotActiveTimer;
HoveredIdAllowOverlap = g.HoveredIdAllowOverlap;
HoveredIdIsDisabled = g.HoveredIdIsDisabled;
ItemUnclipByLog = g.ItemUnclipByLog;
ActiveId = g.ActiveId;
ActiveIdIsAlive = g.ActiveIdIsAlive;
ActiveIdTimer = g.ActiveIdTimer;
ActiveIdIsJustActivated = g.ActiveIdIsJustActivated;
ActiveIdAllowOverlap = g.ActiveIdAllowOverlap;
ActiveIdNoClearOnFocusLoss = g.ActiveIdNoClearOnFocusLoss;
ActiveIdHasBeenPressedBefore = g.ActiveIdHasBeenPressedBefore;
ActiveIdHasBeenEditedBefore = g.ActiveIdHasBeenEditedBefore;
ActiveIdHasBeenEditedThisFrame = g.ActiveIdHasBeenEditedThisFrame;
ActiveIdFromShortcut = g.ActiveIdFromShortcut;
ActiveIdMouseButton = g.ActiveIdMouseButton;
ActiveIdClickOffset = g.ActiveIdClickOffset;
ActiveIdWindow = g.ActiveIdWindow;
ActiveIdSource = g.ActiveIdSource;
ActiveIdPreviousFrame = g.ActiveIdPreviousFrame;
DeactivatedItemData = *(ImGuiDeactivatedItemDataInternal*)&g.DeactivatedItemData;
ActiveIdValueOnActivation = *(ImGuiDataTypeStorageInternal*)&g.ActiveIdValueOnActivation;
LastActiveId = g.LastActiveId;
LastActiveIdTimer = g.LastActiveIdTimer;
ActiveIdUsingNavDirMask = g.ActiveIdUsingNavDirMask;
ActiveIdUsingAllKeyboardKeys = g.ActiveIdUsingAllKeyboardKeys;
}
void ActiveWidgetStateStorage::ApplyState()
{
//Do nothing if not initialized. Calls to this are typically followed by storing the state afterwards, initializing it correctly then
if (!IsInitialized)
return;
ImGuiContext& g = *ImGui::GetCurrentContext();
g.HoveredId = HoveredId;
g.HoveredIdPreviousFrame = HoveredIdPreviousFrame;
g.HoveredIdPreviousFrameItemCount = HoveredIdPreviousFrameItemCount;
g.HoveredIdTimer = HoveredIdTimer;
g.HoveredIdNotActiveTimer = HoveredIdNotActiveTimer;
g.HoveredIdAllowOverlap = HoveredIdAllowOverlap;
g.HoveredIdIsDisabled = HoveredIdIsDisabled;
g.ItemUnclipByLog = ItemUnclipByLog;
g.ActiveId = ActiveId;
g.ActiveIdIsAlive = ActiveIdIsAlive;
g.ActiveIdTimer = ActiveIdTimer;
g.ActiveIdIsJustActivated = ActiveIdIsJustActivated;
g.ActiveIdAllowOverlap = ActiveIdAllowOverlap;
g.ActiveIdNoClearOnFocusLoss = ActiveIdNoClearOnFocusLoss;
g.ActiveIdHasBeenPressedBefore = ActiveIdHasBeenPressedBefore;
g.ActiveIdHasBeenEditedBefore = ActiveIdHasBeenEditedBefore;
g.ActiveIdHasBeenEditedThisFrame = ActiveIdHasBeenEditedThisFrame;
g.ActiveIdFromShortcut = ActiveIdFromShortcut;
g.ActiveIdMouseButton = ActiveIdMouseButton;
g.ActiveIdClickOffset = ActiveIdClickOffset;
g.ActiveIdWindow = (ImGuiWindow*)ActiveIdWindow;
g.ActiveIdSource = (ImGuiInputSource)ActiveIdSource;
g.ActiveIdPreviousFrame = ActiveIdPreviousFrame;
g.DeactivatedItemData = *(ImGuiDeactivatedItemData*)&DeactivatedItemData;
g.ActiveIdValueOnActivation = *(ImGuiDataTypeStorage*)&ActiveIdValueOnActivation;
g.LastActiveId = LastActiveId;
g.LastActiveIdTimer = LastActiveIdTimer;
g.ActiveIdUsingNavDirMask = ActiveIdUsingNavDirMask;
g.ActiveIdUsingAllKeyboardKeys = ActiveIdUsingAllKeyboardKeys;
}
void ActiveWidgetStateStorage::AdvanceState()
{
if (!IsInitialized)
return;
const ImGuiIO& io = ImGui::GetIO();
ImGuiContext& g = *ImGui::GetCurrentContext();
// Update HoveredId data
if (!HoveredIdPreviousFrame)
HoveredIdTimer = 0.0f;
if (!HoveredIdPreviousFrame || (HoveredId && ActiveId == HoveredId))
HoveredIdNotActiveTimer = 0.0f;
if (HoveredId)
HoveredIdTimer += io.DeltaTime;
if (HoveredId && ActiveId != HoveredId)
HoveredIdNotActiveTimer += io.DeltaTime;
HoveredIdPreviousFrame = HoveredId;
HoveredId = 0;
HoveredIdAllowOverlap = false;
HoveredIdIsDisabled = false;
// Clear ActiveID if the item is not alive anymore.
// In 1.87, the common most call to KeepAliveID() was moved from GetID() to ItemAdd().
// As a result, custom widget using ButtonBehavior() _without_ ItemAdd() need to call KeepAliveID() themselves.
if (ActiveId != 0 && ActiveIdIsAlive != ActiveId && ActiveIdPreviousFrame == ActiveId)
{
//ClearActiveID
ImGuiDeactivatedItemDataInternal* deactivated_data = &DeactivatedItemData;
deactivated_data->ID = ActiveId;
deactivated_data->ElapseFrame = (g.LastItemData.ID == ActiveId) ? g.FrameCount : g.FrameCount + 1;
deactivated_data->HasBeenEditedBefore = ActiveIdHasBeenEditedBefore;
deactivated_data->IsAlive = (ActiveIdIsAlive == ActiveId);
ActiveIdIsJustActivated = true;
ActiveIdTimer = 0.0f;
ActiveIdHasBeenPressedBefore = false;
ActiveIdHasBeenEditedBefore = false;
ActiveIdHasBeenEditedThisFrame = false;
ActiveIdFromShortcut = false;
ActiveIdMouseButton = -1;
ActiveId = 0;
ActiveIdAllowOverlap = false;
ActiveIdNoClearOnFocusLoss = false;
ActiveIdWindow = nullptr;
ActiveIdHasBeenEditedThisFrame = false;
}
// Update ActiveId data (clear reference to active widget if the widget isn't alive anymore)
if (ActiveIdIsAlive != ActiveId && ActiveIdPreviousFrame == ActiveId && ActiveId != 0)
{
ActiveIdIsJustActivated = true;
if (ActiveIdIsJustActivated)
{
ActiveIdTimer = 0.0f;
ActiveIdHasBeenPressedBefore = false;
ActiveIdHasBeenEditedBefore = false;
ActiveIdMouseButton = -1;
}
ActiveId = 0;
ActiveIdAllowOverlap = false;
ActiveIdNoClearOnFocusLoss = false;
ActiveIdWindow = nullptr;
ActiveIdHasBeenEditedThisFrame = false;
}
if (ActiveId)
ActiveIdTimer += io.DeltaTime;
LastActiveIdTimer += io.DeltaTime;
ActiveIdPreviousFrame = ActiveId;
ActiveIdIsAlive = 0;
ActiveIdHasBeenEditedThisFrame = false;
ActiveIdIsJustActivated = false;
if (ActiveId == 0)
{
ActiveIdUsingNavDirMask = 0x00;
ActiveIdUsingAllKeyboardKeys = false;
}
if (DeactivatedItemData.ElapseFrame < g.FrameCount)
DeactivatedItemData.ID = 0;
DeactivatedItemData.IsAlive = false;
}
}
================================================
FILE: src/DesktopPlusUI/ImGuiExt.h
================================================
//Some extra functions extending ImGui
//Hopefully nothing here breaks when updating ImGui
#pragma once
#include "imgui.h"
#include
//More colors
extern ImVec4 Style_ImGuiCol_TextNotification;
extern ImVec4 Style_ImGuiCol_TextWarning;
extern ImVec4 Style_ImGuiCol_TextError;
extern ImVec4 Style_ImGuiCol_TextOutline;
extern ImVec4 Style_ImGuiCol_ButtonPassiveToggled; //Toggled state for a button indicating a passive state, rather a full-on eye-catching active state
extern ImVec4 Style_ImGuiCol_SteamVRCursor; //Inner color used to mimic a SteamVR overlay cursor
extern ImVec4 Style_ImGuiCol_SteamVRCursorBorder; //Border color used to mimic a SteamVR overlay cursor
//Legacy ImGui enum, now provided by us instead
enum ImGuiNavInput
{
// Gamepad Mapping
ImGuiNavInput_Activate, // Activate / Open / Toggle / Tweak value // e.g. Cross (PS4), A (Xbox), A (Switch), Space (Keyboard)
ImGuiNavInput_Cancel, // Cancel / Close / Exit // e.g. Circle (PS4), B (Xbox), B (Switch), Escape (Keyboard)
ImGuiNavInput_Input, // Text input / On-Screen keyboard // e.g. Triang.(PS4), Y (Xbox), X (Switch), Return (Keyboard)
ImGuiNavInput_Menu, // Tap: Toggle menu / Hold: Focus, Move, Resize // e.g. Square (PS4), X (Xbox), Y (Switch), Alt (Keyboard)
ImGuiNavInput_DpadLeft, // Move / Tweak / Resize window (w/ PadMenu) // e.g. D-pad Left/Right/Up/Down (Gamepads), Arrow keys (Keyboard)
ImGuiNavInput_DpadRight, //
ImGuiNavInput_DpadUp, //
ImGuiNavInput_DpadDown, //
ImGuiNavInput_LStickLeft, // Scroll / Move window (w/ PadMenu) // e.g. Left Analog Stick Left/Right/Up/Down
ImGuiNavInput_LStickRight, //
ImGuiNavInput_LStickUp, //
ImGuiNavInput_LStickDown, //
ImGuiNavInput_FocusPrev, // Focus Next window (w/ PadMenu) // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch)
ImGuiNavInput_FocusNext, // Focus Prev window (w/ PadMenu) // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch)
ImGuiNavInput_TweakSlow, // Slower tweaks // e.g. L1 or L2 (PS4), LB or LT (Xbox), L or ZL (Switch)
ImGuiNavInput_TweakFast, // Faster tweaks // e.g. R1 or R2 (PS4), RB or RT (Xbox), R or ZL (Switch)
// [Internal] Don't use directly! This is used internally to differentiate keyboard from gamepad inputs for behaviors that require to differentiate them.
// Keyboard behavior that have no corresponding gamepad mapping (e.g. CTRL+TAB) will be directly reading from keyboard keys instead of io.NavInputs[].
ImGuiNavInput_KeyLeft_, // Move left // = Arrow keys
ImGuiNavInput_KeyRight_, // Move right
ImGuiNavInput_KeyUp_, // Move up
ImGuiNavInput_KeyDown_, // Move down
ImGuiNavInput_COUNT
};
//Forward declase ImRect as we have functions with overloads that take it, but it's not part of the public ImGui API
struct IMGUI_API ImRect;
namespace ImGui
{
//Like InputFloat()'s buttons but with a slider instead. Not quite as flexible, though. Always takes as much space as available.
//alt_text is unformatted text rendered on top if set (for unsanitized translation strings). format shouldn't render anything then (use ##)
bool SliderWithButtonsFloat(const char* str_id, float& value, float step, float step_small, float min, float max, const char* format, ImGuiSliderFlags flags = 0, bool* used_button = nullptr,
const char* text_alt = nullptr);
bool SliderWithButtonsInt(const char* str_id, int& value, int step, int step_small, int min, int max, const char* format, ImGuiSliderFlags flags = 0, bool* used_button = nullptr,
const char* text_alt = nullptr);
// format is for int
bool SliderWithButtonsFloatPercentage(const char* str_id, float& value, int step, int step_small, int min, int max, const char* format, ImGuiSliderFlags flags = 0, bool* used_button = nullptr,
const char* text_alt = nullptr);
ImGuiID SliderWithButtonsGetSliderID(const char* str_id);
//Like imgui_demo's HelpMarker, but with a fixed position tooltip
void FixedHelpMarker(const char* desc, const char* marker_str = "(?)");
//Button, but with wrapped, cropped and center aligned label
bool ButtonWithWrappedLabel(const char* label, const ImVec2& size);
//Per-line centered multiline label render function. Renders on top of the last button/item or inside explicit bounding box
void RenderButtonMultilineLabel(const char* label, float line_height_scale = 1.0f);
void RenderButtonMultilineLabel(const char* label, const ImRect& bb, float line_height_scale = 1.0f);
//BeginCombo() but with the option of turning the Combo into an Input text like the sliders. Little bit awkward with the state variables but works otherwise
//Uses PopupContextMenuInputText() with the InputText
//Use normal EndCombo() to finish
bool BeginComboWithInputText(const char* str_id, char* str_buffer, size_t buffer_size, bool& out_buffer_changed,
bool& persist_input_visible, bool& persist_input_activated, bool& persist_mouse_released_once, bool no_preview_text = false);
void ComboWithInputTextActivationCheck(bool& persist_input_visible); //Always call after BeginComboWithInputText(), outside the if statement
//BeginCombo(), but opening it is animated
bool BeginComboAnimated(const char* label, const char* preview_value, ImGuiComboFlags flags = 0);
//Right-alinged Text(). Use offset_x if it's not supposed to take all of the available space, non-zero fixed_w to explicitly set total width instead of using window available space
//Note that text may not always be pixel-perfectly aligned with this
void TextRight(float offset_x, float fixed_w, const char* fmt, ...) IM_FMTARGS(4);
void TextRightV(float offset_x, float fixed_w, const char* fmt, va_list args) IM_FMTLIST(4);
void TextRightUnformatted(float offset_x, float fixed_w, const char* text, const char* text_end = nullptr);
//Shortcut for unformatted colored text
void TextColoredUnformatted(const ImVec4& col, const char* text, const char* text_end = nullptr);
//Outlined Text(). Uses Style_ImGuiCol_TextOutline
void TextOutlined(const char* fmt, ...) IM_FMTARGS(2);
void TextOutlinedV(const char* fmt, va_list args) IM_FMTLIST(2);
void TextUnformattedOutlined(const char* text, const char* text_end = nullptr);
void TextRightOutlined(float offset_x, float fixed_w, const char* fmt, ...) IM_FMTARGS(4);
void TextRightOutlinedV(float offset_x, float fixed_w, const char* fmt, va_list args) IM_FMTLIST(4);
void TextRightUnformattedOutlined(float offset_x, float fixed_w, const char* text, const char* text_end = nullptr);
//RenderTextClipped, but does not limit rendered minimal position to alignment pos_min (so centered text will stay centered with left side being cut off instead)
void RenderTextClippedUnclamped(const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = nullptr);
void RenderTextClippedUnclampedEx(ImDrawList* draw_list, const ImVec2& pos_min, const ImVec2& pos_max, const char* text, const char* text_end, const ImVec2* text_size_if_known, const ImVec2& align = ImVec2(0, 0), const ImRect* clip_rect = nullptr);
//Stretches content added to drawlist between calls to BeginStretched() & EndStretched. Mostly for text.
void BeginStretched();
void EndStretched(float scale_x);
//ColorPicker simplified for embedded widget use instead having of popups + translation support
bool ColorPicker4Simple(const char* str_id, float col[4], float ref_col[4], const char* label_color_current = nullptr, const char* label_color_original = nullptr, float scale = 1.0f);
//CollapsingHeader() with padding hack applied for adjusted appearance within child windows
bool CollapsingHeaderPadded(const char* label, ImGuiTreeNodeFlags flags = 0);
//Collapsing area which animates the content sliding downwards. Always call content widget functions (for content height calculations)
//Uses external animation progress variable to allow overriding when needed
void BeginCollapsingArea(const char* str_id, bool show_content, float& persist_animation_progress);
void EndCollapsingArea();
//ImGuiItemFlags_Disabled is not exposed public API yet and has no styling, so here's something that does the job
//BeginDisabled()/EndDisabled() exists now, but behaves slightly differently and styling may change, so let's keep this for the time being
void PushItemDisabled();
void PopItemDisabled();
void PushItemDisabledNoVisual();
void PopItemDisabledNoVisual();
//Disables window switcher by setting internal config values
void ConfigDisableCtrlTab();
//Just straight up brought into the public API, use -1.0f on one axis to leave as-is
void SetNextWindowScroll(const ImVec2& scroll);
//Brought into the public API since we need only that one sometimes
void ClearActiveID();
//Allow checking for mapped nav inputs for implementing nav-related behavior
bool IsNavInputDown(ImGuiNavInput nav_input);
bool IsNavInputPressed(ImGuiNavInput nav_input, bool repeat = false);
bool IsNavInputReleased(ImGuiNavInput nav_input);
//Get and set previous line height. Useful on complex layouts where a widget may take more height while not being supposed to push the next line further down
float GetPreviousLineHeight();
void SetPreviousLineHeight(float height);
//Something loosely resembling an InputText context menu, except it doesn't operate on the current cursor position or selection at all. Returns if buffer was modified
bool PopupContextMenuInputText(const char* str_id, char* str_buffer, size_t buffer_size, bool paste_remove_newlines = true);
//Returns true if the hovered item has changed to a different one
bool HasHoveredNewItem();
//Returns true if any item is or was active in the previous frame
bool IsAnyItemActiveOrDeactivated();
bool IsAnyItemDeactivated();
//Returns true if any InputText is active
//Use IsAnyTempInputTextActive() to handle widgets creating temp InputTexts instead as the text state ID doesn't get cleared and it can't tell normal use and text input apart
bool IsAnyInputTextActive();
//Returns true if any temp InputText (created for sliders and such) is active
bool IsAnyTempInputTextActive();
//Returns true if any mouse button is clicked
bool IsAnyMouseClicked();
//Takes active input focus and prevents scrolling on any ImGui widget while leaving input state untouched otherwise
void BlockWidgetInput();
//Scroll window horizontally from vertical mouse wheel input
void HScrollWindowFromMouseWheelV();
//Scroll the parent window in begin stack from current mouse wheel input (real child windows can scroll their parents by default already)
void ScrollBeginStackParentWindow();
//Returns true if either scroll bar is visible
bool IsAnyScrollBarVisible();
//Adjusts cursor pos and clipping rect to enable custom title bar content (assumes default left-aligned title). Returns title bar screen rect as X, Y, X2, Y2 coordinates
ImVec4 BeginTitleBar();
void EndTitleBar();
//Returns true if a character in the string is mapped in the active font
bool StringContainsUnmappedCharacter(const char* str);
//Returns string shortened to fit width_max in the active font
std::string StringEllipsis(const char* str, float width_max);
//Fairly specific, yet general enough custom widget that contains an draggable & resizable rectangle in a fixed size area
struct ImGuiDraggableRectAreaState
{
ImVec2 RectPos;
ImVec2 RectSize;
ImVec2 RectPosDragStart;
ImVec2 RectSizeDragStart;
ImGuiMouseButton DragMouseButton = ImGuiMouseButton_Left;
ImGuiDir EdgeDragHDir = ImGuiDir_None;
ImGuiDir EdgeDragVDir = ImGuiDir_None;
bool EdgeDragActive = false;
bool EdgeDragHighlightVisible = false;
};
//Returns true if the rectangle was modified
bool DraggableRectArea(const char* str_id, const ImVec2& area_size, ImGuiDraggableRectAreaState& state);
//Stores an ImGui mouse state or something that would like to be one and can set from or apply to global state if needed.
//Somewhat hacky, but pretty unproblematic when used correctly.
class ImGuiMouseState
{
public:
ImVec2 MousePos;
bool MouseDown[5];
float MouseWheel;
float MouseWheelH;
ImVec2 MouseDelta;
ImVec2 MousePosPrev;
ImVec2 MouseClickedPos[5];
double MouseClickedTime[5];
bool MouseClicked[5];
bool MouseDoubleClicked[5];
ImU16 MouseClickedCount[5];
ImU16 MouseClickedLastCount[5];
bool MouseReleased[5];
bool MouseDownOwned[5];
bool MouseDownOwnedUnlessPopupClose[5];
float MouseDownDuration[5];
float MouseDownDurationPrev[5];
float MouseDragMaxDistanceSqr[5];
ImGuiMouseState();
void SetFromGlobalState();
void ApplyToGlobalState();
void Advance(); //Advance state similar to what happens in ImGui::NewFrame();
};
//Very dirty hack. Allows storing and restoring the active widget state. Works fine for a bunch of buttons not interrupting an InputText(), but likely breaks for anything complex.
//A separate ImGuiContext would be the sane option, but that comes with its own complications.
class ActiveWidgetStateStorage
{
private:
//Copies of structs declared in imgui_internal.h, since we don't want to pull that header in for everything that uses this one
//These need to be kept in sync
struct ImGuiDeactivatedItemDataInternal
{
ImGuiID ID;
int ElapseFrame;
bool HasBeenEditedBefore;
bool IsAlive;
};
struct ImGuiDataTypeStorageInternal
{
ImU8 Data[8];
};
bool IsInitialized;
ImGuiID HoveredId;
ImGuiID HoveredIdPreviousFrame;
int HoveredIdPreviousFrameItemCount;
float HoveredIdTimer;
float HoveredIdNotActiveTimer;
bool HoveredIdAllowOverlap;
bool HoveredIdIsDisabled;
bool ItemUnclipByLog;
ImGuiID ActiveId;
ImGuiID ActiveIdIsAlive;
float ActiveIdTimer;
bool ActiveIdIsJustActivated;
bool ActiveIdAllowOverlap;
bool ActiveIdNoClearOnFocusLoss;
bool ActiveIdHasBeenPressedBefore;
bool ActiveIdHasBeenEditedBefore;
bool ActiveIdHasBeenEditedThisFrame;
bool ActiveIdFromShortcut;
int ActiveIdMouseButton;
ImVec2 ActiveIdClickOffset;
void* ActiveIdWindow;
int ActiveIdSource;
ImGuiID ActiveIdPreviousFrame;
ImGuiDeactivatedItemDataInternal DeactivatedItemData;
ImGuiDataTypeStorageInternal ActiveIdValueOnActivation;
ImGuiID LastActiveId;
float LastActiveIdTimer;
//ImGuiKeyOwnerData is not part of this. Probably doesn't need to be
ImU32 ActiveIdUsingNavDirMask;
bool ActiveIdUsingAllKeyboardKeys;
public:
ActiveWidgetStateStorage();
void StoreCurrentState();
void ApplyState();
void AdvanceState(); //Advance state similar to what happens in ImGui::NewFrame();
};
}
================================================
FILE: src/DesktopPlusUI/NotificationIcon.cpp
================================================
#include "NotificationIcon.h"
#include
#include
#include
#include "UIManager.h"
#include "InterprocessMessaging.h"
#include "resource.h"
#undef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable : 4996)
#define WM_DPLUSUI_NOTIFICATIONICON WM_USER
static LPCWSTR const g_WindowClassNameNotificationIcon = L"elvdesktopUINotifIcon";
LRESULT __stdcall NotificationIcon::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (msg == WM_DPLUSUI_NOTIFICATIONICON)
{
if (UIManager::Get())
{
UIManager::Get()->GetNotificationIcon().OnCallbackMessage(wParam, lParam);
}
return 0;
}
return ::DefWindowProc(hWnd, msg, wParam, lParam);
}
NotificationIcon::NotificationIcon() : m_Instance(nullptr), m_PopupMenu(nullptr)
{
m_IconData = { sizeof(NOTIFYICONDATA) };
}
NotificationIcon::~NotificationIcon()
{
if (m_PopupMenu != nullptr)
{
DestroyMenu(m_PopupMenu);
}
::Shell_NotifyIcon(NIM_DELETE, &m_IconData);
if (m_IconData.hWnd != nullptr)
{
::DestroyWindow(m_IconData.hWnd);
::UnregisterClass(g_WindowClassNameNotificationIcon, m_Instance);
}
}
bool NotificationIcon::Init(HINSTANCE hinstance)
{
m_Instance = hinstance;
//Register window class
//Using an extra window allows us to have the popup menu not focus the main window and also prevent Windows from treating desktop and non-desktop mode as separate icons
WNDCLASSEX wc = { sizeof(WNDCLASSEX), CS_CLASSDC, NotificationIcon::WndProc, 0L, 0L, m_Instance, nullptr, nullptr, nullptr, nullptr, g_WindowClassNameNotificationIcon, nullptr };
::RegisterClassEx(&wc);
//Create notification icon
m_IconData.hWnd = ::CreateWindow(wc.lpszClassName, L"Desktop+ UI Notification Icon", 0, 0, 0, 1, 1, HWND_MESSAGE, nullptr, wc.hInstance, nullptr);
m_IconData.uID = 0;
m_IconData.uFlags = NIF_ICON | NIF_TIP | NIF_MESSAGE | NIF_SHOWTIP;
m_IconData.uCallbackMessage = WM_DPLUSUI_NOTIFICATIONICON;
m_IconData.hIcon = (HICON)::LoadImage(m_Instance, MAKEINTRESOURCE(IDI_DPLUS), IMAGE_ICON, ::GetSystemMetrics(SM_CXSMICON), ::GetSystemMetrics(SM_CYSMICON), LR_DEFAULTCOLOR);
wcscpy(m_IconData.szTip, L"Desktop+");
m_IconData.uVersion = NOTIFYICON_VERSION_4;
bool ret = ::Shell_NotifyIcon(NIM_ADD, &m_IconData);
::Shell_NotifyIcon(NIM_SETVERSION, &m_IconData);
RefreshPopupMenu();
return ret;
}
void NotificationIcon::RefreshPopupMenu()
{
//Destroy old menu if it exists
if (m_PopupMenu != nullptr)
{
DestroyMenu(m_PopupMenu);
}
//Create popup menu
m_PopupMenu = ::CreatePopupMenu();
if (m_PopupMenu != nullptr)
{
if (UIManager::Get()->IsInDesktopMode())
{
::InsertMenu(m_PopupMenu, 0, MF_BYPOSITION | MF_STRING, 1, WStringConvertFromUTF8(TranslationManager::GetString(tstr_NotificationIconRestoreVR)).c_str());
}
else
{
::InsertMenu(m_PopupMenu, 0, MF_BYPOSITION | MF_STRING, 1, WStringConvertFromUTF8(TranslationManager::GetString(tstr_NotificationIconOpenOnDesktop)).c_str());
}
::InsertMenu(m_PopupMenu, 1, MF_BYPOSITION | MF_STRING, 2, WStringConvertFromUTF8(TranslationManager::GetString(tstr_NotificationIconQuit)).c_str());
}
}
void NotificationIcon::OnCallbackMessage(WPARAM wparam, LPARAM lparam)
{
switch (LOWORD(lparam))
{
case NIN_SELECT:
case NIN_KEYSELECT:
case WM_CONTEXTMENU:
{
POINT const pt = { GET_X_LPARAM(wparam), GET_Y_LPARAM(wparam) };
//Respect menu drop alignment
UINT flags = TPM_BOTTOMALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD;
if (::GetSystemMetrics(SM_MENUDROPALIGNMENT) != 0)
{
flags |= TPM_RIGHTALIGN;
}
else
{
flags |= TPM_LEFTALIGN;
}
::SetForegroundWindow(m_IconData.hWnd);
BOOL sel = ::TrackPopupMenu(m_PopupMenu, flags, pt.x, pt.y, 0, m_IconData.hWnd, nullptr);
switch (sel)
{
case 1 /*Open Settings on Desktop / Restore VR Interface*/:
{
UIManager::Get()->Restart(!UIManager::Get()->IsInDesktopMode());
break;
}
case 2 /*Quit*/:
{
//Kindly ask dashboard process to quit
if (HWND window = ::FindWindow(g_WindowClassNameDashboardApp, nullptr))
{
::PostMessage(window, WM_QUIT, 0, 0);
}
UIManager::Get()->DisableRestartOnExit();
::PostMessage(UIManager::Get()->GetWindowHandle(), WM_QUIT, 0, 0);
break;
}
}
break;
}
break;
}
}
================================================
FILE: src/DesktopPlusUI/NotificationIcon.h
================================================
//Provides a notification area/tray icon
//Technically a singleton, but is designed to have its one instance live in UIManager for proper lifetime management
//Once intialized, it's fully self-contained
#pragma once
#define NOMINMAX
#include
class NotificationIcon
{
private:
HINSTANCE m_Instance;
NOTIFYICONDATA m_IconData;
HMENU m_PopupMenu;
static LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
public:
NotificationIcon();
~NotificationIcon();
bool Init(HINSTANCE hinstance);
void RefreshPopupMenu(); //Destroys the popup menu if it exists and recreates it. Called after Init() to update translation strings
void OnCallbackMessage(WPARAM wparam, LPARAM lparam);
};
================================================
FILE: src/DesktopPlusUI/TextureManager.cpp
================================================
#include "TextureManager.h"
#define NOMINMAX
#include
#include
#include
//Make GDI+ header work with NOMINMAX
namespace Gdiplus
{
using std::min;
using std::max;
}
#include
#include
#include "ConfigManager.h"
#include "Util.h"
#include "UIManager.h"
#include "OverlayManager.h"
#include "WindowManager.h"
#include "imgui_impl_dx11_openvr.h"
const wchar_t* TextureManager::s_TextureFilenames[tmtex_MAX] =
{
L"images/icons/desktop.png",
L"images/icons/desktop_all.png",
L"images/icons/desktop_1.png",
L"images/icons/desktop_2.png",
L"images/icons/desktop_3.png",
L"images/icons/desktop_4.png",
L"images/icons/desktop_5.png",
L"images/icons/desktop_6.png",
L"images/icons/desktop_next.png",
L"images/icons/desktop_previous.png",
L"images/icons/desktop_none.png",
L"images/icons/performance_monitor.png",
L"images/icons/browser.png",
L"images/icons/settings.png",
L"images/icons/keyboard.png",
L"images/icons/task_switch.png",
L"images/icons/add.png",
L"images/icons/window_overlay.png",
L"images/icons_small/small_app_icon.png",
L"images/icons_small/small_close.png",
L"images/icons_small/small_move.png",
L"images/icons_small/small_move_locked.png",
L"images/icons_small/small_add_window.png",
L"images/icons_small/small_actionbar.png",
L"images/icons_small/small_performance_monitor_reset.png",
L"images/icons_small/small_browser_back.png",
L"images/icons_small/small_browser_forward.png",
L"images/icons_small/small_browser_refresh.png",
L"images/icons_small/small_browser_stop.png",
L"images/icons_small/xsmall_desktop.png",
L"images/icons_small/xsmall_desktop_all.png",
L"images/icons_small/xsmall_desktop_1.png",
L"images/icons_small/xsmall_desktop_2.png",
L"images/icons_small/xsmall_desktop_3.png",
L"images/icons_small/xsmall_desktop_4.png",
L"images/icons_small/xsmall_desktop_5.png",
L"images/icons_small/xsmall_desktop_6.png",
L"images/icons_small/xsmall_desktop_none.png",
L"images/icons_small/xsmall_performance_monitor.png",
L"images/icons_small/xsmall_browser.png",
L"images/icons_small/xsmall_settings.png",
L"images/icons_small/xsmall_keyboard.png",
L"images/icons_small/xsmall_origin_playspace.png",
L"images/icons_small/xsmall_origin_hmd_pos.png",
L"images/icons_small/xsmall_origin_seated.png",
L"images/icons_small/xsmall_origin_dashboard.png",
L"images/icons_small/xsmall_origin_hmd.png",
L"images/icons_small/xsmall_origin_controller_left.png",
L"images/icons_small/xsmall_origin_controller_right.png",
L"images/icons_small/xsmall_origin_aux.png",
L"images/icons_small/xsmall_origin_theater_screen.png",
L"images/icons_small/xxsmall_close.png",
L"images/icons_small/xxsmall_pin.png",
L"images/icons_small/xxsmall_unpin.png",
L"images/icons_small/xxsmall_browser_back.png",
L"", //tmtex_icon_temp, blank
};
static TextureManager g_TextureManager;
TextureManager::TextureManager() : m_ReloadLater(false)
{
std::fill(std::begin(m_ImGuiRectIDs), std::end(m_ImGuiRectIDs), -1);
std::fill(std::begin(m_AtlasSizes), std::end(m_AtlasSizes), ImVec2(-1, -1));
std::fill(std::begin(m_AtlasUVs), std::end(m_AtlasUVs), ImVec4(0, 0, 0, 0));
}
TextureManager& TextureManager::Get()
{
return g_TextureManager;
}
bool TextureManager::LoadAllTexturesAndBuildFonts()
{
bool all_ok = true; //We don't need to abort when something fails, but let's not ignore it completely
ImGuiIO& io = ImGui::GetIO();
io.Fonts->Clear();
ImGui_ImplDX11_InvalidateDeviceObjects(); //I really feel like I shouldn't have to call a renderer-specific function to make reloading fonts work, but it seems necessary
//Clear arrays
std::fill(std::begin(m_ImGuiRectIDs), std::end(m_ImGuiRectIDs), -1);
std::fill(std::begin(m_AtlasSizes), std::end(m_AtlasSizes), ImVec2(-1, -1));
std::fill(std::begin(m_AtlasUVs), std::end(m_AtlasUVs), ImVec4(0, 0, 0, 0));
//Prepare font range to add more characters from action names/properties when needed
ImVector ranges;
ImFontGlyphRangesBuilder builder;
ActionManager& action_manager = ConfigManager::Get().GetActionManager();
for (ActionUID uid : action_manager.GetActionOrderListUI())
{
const Action action = action_manager.GetAction(uid);
builder.AddText(action.Name.c_str());
builder.AddText(action.Label.c_str());
for (const auto& command : action.Commands)
{
builder.AddText(command.StrMain.c_str());
builder.AddText(command.StrArg.c_str());
}
}
for (const std::string& str : ConfigManager::Get().GetOverlayProfileList()) //Also from overlay profiles
{
builder.AddText(str.c_str());
}
for (unsigned int i = 0; i < OverlayManager::Get().GetOverlayCount(); ++i) //And overlay names
{
builder.AddText(OverlayManager::Get().GetConfigData(i).ConfigNameStr.c_str());
}
for (const WindowInfo& window_info : WindowManager::Get().WindowListGet()) //And window list
{
builder.AddText(window_info.GetListTitle().c_str());
}
for (const std::string& str : m_FontBuilderExtraStrings) //And extra strings... yeah. This might not be the best way to tackle this issue
{
builder.AddText(str.c_str());
}
//Characters from current translation
TranslationManager::Get().AddStringsToFontBuilder(builder);
//Characters used by the VR Keyboard
builder.AddText(UIManager::Get()->GetVRKeyboard().GetKeyLabelsString().c_str());
//Extra characters used by the UI directly
builder.AddText(k_pch_bold_exclamation_mark);
builder.AddText(k_pch_degree_symbol);
builder.AddRanges(io.Fonts->GetGlyphRangesDefault());
builder.BuildRanges(&ranges);
ImFontConfig config_compact;
ImFontConfig config_large;
config_compact.GlyphOffset.y = -1; //Set offset to make it not look so bad
config_large.GlyphOffset.y = -1;
ImFontConfig* config = &config_compact;
//Try to load fonts
ImFont* font = nullptr;
ImFont* font_compact = nullptr;
ImFont* font_large = nullptr;
float font_base_size = 32.0f;
bool load_large_font = ( (ConfigManager::GetValue(configid_bool_interface_large_style)) && (!UIManager::Get()->IsInDesktopMode()) );
//Loop to do the same for the large font if needed
for (;;)
{
//Load preferred font first, if the translation has set one
const std::string& preferred_font_name = TranslationManager::Get().GetCurrentTranslationFontName();
const std::wstring preferred_font_name_wstr = WStringConvertFromUTF8(TranslationManager::Get().GetCurrentTranslationFontName().c_str());
if (!preferred_font_name.empty())
{
//AddFontFromFileTTF asserts when failing to load, so check for existence, though it's not really an issue in release mode
if (FileExists( (L"C:\\Windows\\Fonts\\" + preferred_font_name_wstr).c_str() ))
{
font = io.Fonts->AddFontFromFileTTF( ("C:\\Windows\\Fonts\\" + preferred_font_name).c_str(), font_base_size * UIManager::Get()->GetUIScale(), config, ranges.Data);
//Other fonts are still used as fallback
config->MergeMode = true;
}
else if (FileExists( (WStringConvertFromUTF8(ConfigManager::Get().GetApplicationPath().c_str()) + L"/lang/" + preferred_font_name_wstr).c_str() ))
{
//Also allow for a custom font from the application directory
font = io.Fonts->AddFontFromFileTTF( (ConfigManager::Get().GetApplicationPath() + "/lang/" + preferred_font_name).c_str(), font_base_size * UIManager::Get()->GetUIScale(),
config, ranges.Data);
config->MergeMode = true;
}
}
//Continue with the standard font selection
if (FileExists(L"C:\\Windows\\Fonts\\segoeui.ttf"))
{
font = io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\segoeui.ttf", font_base_size * UIManager::Get()->GetUIScale(), config, ranges.Data);
}
if (font != nullptr)
{
//Segoe UI doesn't have any CJK, use some fallbacks (loading this is actually pretty fast)
config->MergeMode = true;
//Prefer Meiryo over MS Gothic. The former isn't installed on non-japanese systems by default though
if (FileExists(L"C:\\Windows\\Fonts\\meiryo.ttc"))
io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\meiryo.ttc", font_base_size * UIManager::Get()->GetUIScale(), config, ranges.Data);
else if (FileExists(L"C:\\Windows\\Fonts\\msgothic.ttc"))
io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\msgothic.ttc", font_base_size * UIManager::Get()->GetUIScale(), config, ranges.Data);
if (FileExists(L"C:\\Windows\\Fonts\\malgun.ttf"))
io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\malgun.ttf", font_base_size * UIManager::Get()->GetUIScale(), config, ranges.Data);
if (FileExists(L"C:\\Windows\\Fonts\\msyh.ttc"))
io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\msyh.ttc", font_base_size * UIManager::Get()->GetUIScale(), config, ranges.Data);
//Thai font
if (FileExists(L"C:\\Windows\\Fonts\\LeelawUI.ttf"))
io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\LeelawUI.ttf", font_base_size * UIManager::Get()->GetUIScale(), config, ranges.Data);
//Also add some symbol support at least... yeah this is far from comprehensive all in all but should cover most uses
if (FileExists(L"C:\\Windows\\Fonts\\seguisym.ttf"))
io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\seguisym.ttf", font_base_size * UIManager::Get()->GetUIScale(), config, ranges.Data);
}
else
{
//Though we have the default as fallback if it isn't somehow
font = io.Fonts->AddFontDefault();
}
if (font_compact == nullptr)
{
font_compact = font;
}
else
{
font_large = font;
}
if ( (load_large_font) && (font_large == nullptr) )
{
font_base_size *= 1.5f;
config = &config_large;
}
else
{
break;
}
}
//Initialize GDI+.
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
if (GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr) != Gdiplus::Ok)
{
LOG_F(ERROR, "Initializing GDI+ failed! Icons will not be loaded");
//Still build the font so we can have text at least
const bool font_build_success = io.Fonts->Build();
//If building the font atlas failed, fall back to the internal default font and try again
if (!font_build_success)
{
LOG_F(ERROR, "Building font atlas failed (invalid font file?)! Falling back to internal font");
io.Fonts->Clear();
font = io.Fonts->AddFontDefault();
font_compact = font;
font_large = font;
io.Fonts->Build();
}
io.Fonts->ClearInputData(); //We don't need to keep this around, reduces RAM use a lot
UIManager::Get()->SetFonts(font_compact, font_large);
m_ReloadLater = false;
return false; //Everything below will fail as well if this did
}
//Load images and add custom rects for them
//Since we have to build the font before actually copying the image data, we have to keep the bitmaps loaded, which is a bit messy, especially with custom action icons
std::vector< std::unique_ptr > bitmaps;
//Load application icons
int icon_id = 0;
for (; icon_id < tmtex_MAX; ++icon_id)
{
std::unique_ptr bmp;
if (icon_id == tmtex_icon_temp)
{
bmp = std::unique_ptr( new Gdiplus::Bitmap(m_TextureFilenameIconTemp.c_str()) );
}
else
{
bmp = std::unique_ptr( new Gdiplus::Bitmap(s_TextureFilenames[icon_id]) );
}
if (bmp->GetLastStatus() == Gdiplus::Ok)
{
m_ImGuiRectIDs[icon_id] = io.Fonts->AddCustomRectRegular(bmp->GetWidth(), bmp->GetHeight());
if (io.Fonts->TexDesiredWidth <= (int)bmp->GetWidth())
{
//We could go smarter here, but let's be honest, we actually shouldn't load large images into the atlas in the first place!
//But well, I tried out of curiosity once and the result was a disaster without these checks.
//And yes, we need more space than the texture's width, unfortunately. Probably for that one white pixel in the atlas or something
io.Fonts->TexDesiredWidth = (bmp->GetWidth() >= 2048) ? 4096 : (bmp->GetWidth() >= 1024) ? 2048 : (bmp->GetWidth() >= 512) ? 1024 : 512;
}
}
bitmaps.push_back(std::move(bmp));
}
//Load custom action icons
struct ActionIconTextureData
{
int IconImGuiRectID = -1;
ImVec2 IconAtlasSize;
ImVec4 IconAtlasUV;
};
std::vector action_icon_tex_data;
action_icon_tex_data.reserve(action_manager.GetActionOrderListUI().size());
action_manager.ClearIconData();
for (ActionUID uid : action_manager.GetActionOrderListUI())
{
const Action& action = action_manager.GetAction(uid);
ActionIconTextureData tex_data;
if (!action.IconFilename.empty())
{
std::string icon_path = "images/icons/" + action.IconFilename;
std::unique_ptr bmp( new Gdiplus::Bitmap(WStringConvertFromUTF8(icon_path.c_str()).c_str()) );
if (bmp->GetLastStatus() == Gdiplus::Ok)
{
tex_data.IconImGuiRectID = io.Fonts->AddCustomRectRegular(bmp->GetWidth(), bmp->GetHeight());
if (io.Fonts->TexDesiredWidth <= (int)bmp->GetWidth())
{
//See above
io.Fonts->TexDesiredWidth = (bmp->GetWidth() >= 2048) ? 4096 : (bmp->GetWidth() >= 1024) ? 2048 : (bmp->GetWidth() >= 512) ? 1024 : 512;
}
}
bitmaps.push_back(std::move(bmp));
}
action_icon_tex_data.push_back(tex_data);
}
//Set up already loaded window icons
for (auto& window_icon : m_WindowIcons)
{
window_icon.ImGuiRectID = io.Fonts->AddCustomRectRegular((int)window_icon.Size.x, (int)window_icon.Size.y);
if (io.Fonts->TexDesiredWidth <= (int)window_icon.Size.x)
{
//See above
io.Fonts->TexDesiredWidth = (window_icon.Size.x >= 2048) ? 4096 : (window_icon.Size.x >= 1024) ? 2048 : (window_icon.Size.x >= 512) ? 1024 : 512;
}
}
//Build atlas
const bool font_build_success = io.Fonts->Build();
//If building the font atlas failed, fall back to the internal default font and try again
if (!font_build_success)
{
LOG_F(ERROR, "Building font atlas failed (invalid font file?)! Falling back to internal font");
ImVector custom_rects_back = io.Fonts->CustomRects;
int desired_width_back = io.Fonts->TexDesiredWidth;
io.Fonts->Clear();
font = io.Fonts->AddFontDefault();
font_compact = font;
font_large = font;
io.Fonts->CustomRects = custom_rects_back;
io.Fonts->TexDesiredWidth = desired_width_back;
io.Fonts->Build();
}
//Retrieve atlas texture in RGBA format
unsigned char* tex_pixels = nullptr;
int tex_width, tex_height;
io.Fonts->GetTexDataAsRGBA32(&tex_pixels, &tex_width, &tex_height);
//Actually do the copying now
icon_id = 0;
auto action_tex_data_it = action_icon_tex_data.begin();
for (auto& bmp : bitmaps)
{
if (bmp->GetLastStatus() == Gdiplus::Ok)
{
int* rect_id = nullptr;
ImVec2* atlas_size = nullptr;
ImVec4* atlas_uvs = nullptr;
if (icon_id < tmtex_MAX)
{
rect_id = &m_ImGuiRectIDs[icon_id];
atlas_size = &m_AtlasSizes[icon_id];
atlas_uvs = &m_AtlasUVs[icon_id];
}
else
{
//Get the next action, skipping the ones with no icon
do
{
if (action_tex_data_it == action_icon_tex_data.end())
{
break;
}
ActionIconTextureData& tex_data = *action_tex_data_it;
rect_id = &tex_data.IconImGuiRectID;
atlas_size = &tex_data.IconAtlasSize;
atlas_uvs = &tex_data.IconAtlasUV;
action_tex_data_it++;
}
while (*rect_id == -1);
}
if ( (rect_id != nullptr) && (*rect_id != -1) )
{
if (const ImFontAtlasCustomRect* rect = io.Fonts->GetCustomRectByIndex(*rect_id))
{
Gdiplus::BitmapData bitmapData;
Gdiplus::Rect gdirect(0, 0, rect->Width, rect->Height);
if (bmp->LockBits(&gdirect, Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, &bitmapData) == Gdiplus::Ok) //Access bitmap data from GDI+
{
for (int y = 0; y < rect->Height; ++y)
{
ImU32* p = (ImU32*)tex_pixels + (rect->Y + y) * tex_width + (rect->X);
UINT8* pgdi = (UINT8*)bitmapData.Scan0 + (y * bitmapData.Stride);
for (int x = 0; x < rect->Width; ++x)
{
//GDI+ order is BGRA, convert
*p++ = IM_COL32(*(pgdi + 2), *(pgdi + 1), *pgdi, *(pgdi + 3));
pgdi += 4;
}
}
bmp->UnlockBits(&bitmapData);
//Store UVs and size since we succeeded with copying
atlas_size->x = rect->Width;
atlas_size->y = rect->Height;
atlas_uvs->x = (float)rect->X * io.Fonts->TexUvScale.x; //Min U
atlas_uvs->y = (float)rect->Y * io.Fonts->TexUvScale.y; //Min V
atlas_uvs->z = (float)(rect->X + rect->Width) * io.Fonts->TexUvScale.x; //Max U
atlas_uvs->w = (float)(rect->Y + rect->Height) * io.Fonts->TexUvScale.y; //Max V
}
else
{
*rect_id = -1;
all_ok = false;
}
}
else
{
*rect_id = -1;
all_ok = false;
}
}
else
{
all_ok = false;
}
}
icon_id++;
}
//Copy cached window icons into atlas
for (auto& window_icon : m_WindowIcons)
{
if (const ImFontAtlasCustomRect* rect = io.Fonts->GetCustomRectByIndex(window_icon.ImGuiRectID))
{
UINT8* psrc = (UINT8*)window_icon.PixelData.get();
size_t stride = rect->Width * 4;
//Copy RGBA pixels line-by-line
for (int y = 0; y < rect->Height; ++y, psrc += stride)
{
ImU32* p = (ImU32*)tex_pixels + (rect->Y + y) * tex_width + (rect->X);
memcpy(p, psrc, stride);
}
//Store UVs
window_icon.AtlasUV.x = (float)rect->X * io.Fonts->TexUvScale.x; //Min U
window_icon.AtlasUV.y = (float)rect->Y * io.Fonts->TexUvScale.y; //Min V
window_icon.AtlasUV.z = (float)(rect->X + rect->Width) * io.Fonts->TexUvScale.x; //Max U
window_icon.AtlasUV.w = (float)(rect->Y + rect->Height) * io.Fonts->TexUvScale.y; //Max V
}
else
{
window_icon.ImGuiRectID = -1;
all_ok = false;
}
}
//Store action texture data in actual actions
IM_ASSERT(action_icon_tex_data.size() == action_manager.GetActionOrderListUI().size());
for (size_t i = 0; i < action_icon_tex_data.size(); ++i)
{
const ActionIconTextureData& tex_data = action_icon_tex_data[i];
//We can just skip this when the rect ID is still -1 since we cleared all texture data at beginning
if (tex_data.IconImGuiRectID != -1)
{
Action action = action_manager.GetAction( action_manager.GetActionOrderListUI()[i] );
action.IconImGuiRectID = tex_data.IconImGuiRectID;
action.IconAtlasSize = tex_data.IconAtlasSize;
action.IconAtlasUV = tex_data.IconAtlasUV;
action_manager.StoreAction(action);
}
}
//Delete Bitmaps before shutting down GDI+
bitmaps.clear();
//Shutdown GDI+, we won't need it again
Gdiplus::GdiplusShutdown(gdiplusToken);
m_ReloadLater = false;
//We don't need to keep this around, reduces RAM use a lot
io.Fonts->ClearInputData();
UIManager::Get()->SetFonts(font_compact, font_large);
return all_ok;
}
void TextureManager::ReloadAllTexturesLater()
{
m_ReloadLater = true;
}
bool TextureManager::GetReloadLaterFlag()
{
return m_ReloadLater;
}
const wchar_t* TextureManager::GetTextureFilename(TMNGRTexID texid) const
{
return (texid != tmtex_icon_temp) ? s_TextureFilenames[texid] : m_TextureFilenameIconTemp.c_str();
}
void TextureManager::SetTextureFilenameIconTemp(const wchar_t* filename)
{
m_TextureFilenameIconTemp = filename;
}
bool TextureManager::GetTextureInfo(TMNGRTexID texid, ImVec2& size, ImVec2& uv_min, ImVec2& uv_max) const
{
int rect_id = m_ImGuiRectIDs[texid];
if (rect_id != -1)
{
size = m_AtlasSizes[texid];
//Also set cached UV coordinates
uv_min.x = m_AtlasUVs[texid].x;
uv_min.y = m_AtlasUVs[texid].y;
uv_max.x = m_AtlasUVs[texid].z;
uv_max.y = m_AtlasUVs[texid].w;
return true;
}
return false;
}
bool TextureManager::GetTextureInfo(const Action& action, ImVec2& size, ImVec2& uv_min, ImVec2& uv_max) const
{
if (action.IconImGuiRectID != -1)
{
size = action.IconAtlasSize;
//Also set cached UV coordinates
uv_min.x = action.IconAtlasUV.x;
uv_min.y = action.IconAtlasUV.y;
uv_max.x = action.IconAtlasUV.z;
uv_max.y = action.IconAtlasUV.w;
return true;
}
return false;
}
int TextureManager::GetWindowIconCacheID(HWND window_handle)
{
WindowInfo const* info_ptr = WindowManager::Get().WindowListFindWindow(window_handle);
return (info_ptr != nullptr) ? GetWindowIconCacheID(info_ptr->GetIcon()) : -1;
}
int TextureManager::GetWindowIconCacheID(HWND window_handle, uint64_t& icon_handle_config)
{
WindowInfo const* info_ptr = WindowManager::Get().WindowListFindWindow(window_handle);
HICON icon_handle = (info_ptr != nullptr) ? info_ptr->GetIcon() : nullptr;
if (icon_handle != nullptr)
{
icon_handle_config = (uint64_t)icon_handle;
return GetWindowIconCacheID(icon_handle);
}
else if (icon_handle_config != 0)
{
return GetWindowIconCacheID((HICON)icon_handle_config);
}
return -1;
}
int TextureManager::GetWindowIconCacheID(HICON icon_handle)
{
//Look if the icon is already loaded
for (int i = 0; i < m_WindowIcons.size(); ++i)
{
if (m_WindowIcons[i].IconHandle == icon_handle)
{
return i;
}
}
//Icon not loaded yet, try to do that
int ret = -1;
ICONINFO icon_info = {0};
if (::GetIconInfo(icon_handle, &icon_info) != 0)
{
HDC hdc = ::GetDC(nullptr);
//Get bitmap info from icon bitmap
BITMAPINFO bmp_info = {0};
bmp_info.bmiHeader.biSize = sizeof(bmp_info.bmiHeader);
if (::GetDIBits(hdc, icon_info.hbmColor, 0, 0, nullptr, &bmp_info, DIB_RGB_COLORS) != 0)
{
TMNGRWindowIcon window_icon;
int icon_width = bmp_info.bmiHeader.biWidth;
int icon_height = abs(bmp_info.bmiHeader.biHeight);
const size_t icon_pixel_count = icon_width * icon_height;
auto PixelData = std::unique_ptr{new BYTE[icon_pixel_count * 4]};
bmp_info.bmiHeader.biSize = sizeof(bmp_info.bmiHeader);
bmp_info.bmiHeader.biBitCount = 32;
bmp_info.bmiHeader.biCompression = BI_RGB;
bmp_info.bmiHeader.biHeight = -icon_height; //Always use top-down order (negative height)
//Read the actual bitmap buffer into the pixel data array
if (::GetDIBits(hdc, icon_info.hbmColor, 0, bmp_info.bmiHeader.biHeight, (LPVOID)PixelData.get(), &bmp_info, DIB_RGB_COLORS) != 0)
{
//Even if we don't override biBitCount to 32, it's still returned as that for 24-bit and lower bit-depth icons (probably just the screen DC format)
//It seems the only way to check if the icon needs its mask applied is to see if the alpha channel is fully blank
//32-bit icons still come with masks, but applying them means to override the alpha channel with a 1-bit one (and doing so is also wasteful)
bool needs_mask = true;
BYTE* psrc = PixelData.get() + 3; //BGRA alpha pixel
const BYTE* const psrc_end = PixelData.get() + (icon_pixel_count * 4);
for (; psrc < psrc_end; psrc += 4)
{
if (*psrc != 0)
{
needs_mask = false;
break;
}
}
//Apply mask if we need to
if (needs_mask)
{
//Get bitmap info for the mask this time
BITMAPINFO bmp_info = {0};
bmp_info.bmiHeader.biSize = sizeof(bmp_info.bmiHeader);
if (::GetDIBits(hdc, icon_info.hbmMask, 0, 0, nullptr, &bmp_info, DIB_RGB_COLORS) != 0)
{
int mask_width = bmp_info.bmiHeader.biWidth;
int mask_height = abs(bmp_info.bmiHeader.biHeight);
//Only continue if icon and mask are really the same size (can be different for monochrome bitmap formats, which are not supported here)
if ( (icon_width == mask_width) && (icon_height == mask_height) )
{
auto PixelDataMask = std::unique_ptr{new BYTE[icon_pixel_count * 4]};
bmp_info.bmiHeader.biSize = sizeof(bmp_info.bmiHeader);
bmp_info.bmiHeader.biBitCount = 32;
bmp_info.bmiHeader.biCompression = BI_RGB;
bmp_info.bmiHeader.biHeight = -abs(bmp_info.bmiHeader.biHeight); //Always use top-down order (negative height)
//Read the mask bitmap buffer
if (::GetDIBits(hdc, icon_info.hbmMask, 0, bmp_info.bmiHeader.biHeight, (LPVOID)PixelDataMask.get(), &bmp_info, DIB_RGB_COLORS) != 0)
{
//Apply mask to color pixel data
psrc = PixelData.get() + 3; //BGRA alpha pixel
BYTE* pmsk = PixelDataMask.get(); //BGRA blue pixel (alpha channel is blank for the mask)
for (; psrc < psrc_end; psrc += 4, pmsk += 4)
{
*psrc = ~(*pmsk);
}
}
}
}
}
//Convert BGRA to RGBA for ImGui's texture atlas
window_icon.PixelData = std::unique_ptr{new BYTE[icon_pixel_count * 4]};
psrc = PixelData.get();
UINT32* pdst = (UINT32*)window_icon.PixelData.get();
for (; psrc < psrc_end; psrc += 4, ++pdst)
{
*pdst = IM_COL32(*(psrc + 2), *(psrc + 1), *psrc, *(psrc + 3));
}
//Fill out other data and move the icon to the cache
window_icon.IconHandle = icon_handle;
window_icon.Size = {(float)icon_width, (float)icon_height};
m_WindowIcons.push_back(std::move(window_icon));
//We succeeded, but the icon won't be ready until next frame, so schedule reload and skip rendering this frame
ReloadAllTexturesLater();
UIManager::Get()->RepeatFrame();
ret = (int)m_WindowIcons.size() - 1;
}
}
::DeleteObject(icon_info.hbmColor);
::DeleteObject(icon_info.hbmMask);
::ReleaseDC(nullptr, hdc);
}
return ret;
}
bool TextureManager::GetWindowIconTextureInfo(int icon_cache_id, ImVec2& size, ImVec2& uv_min, ImVec2& uv_max) const
{
if ( (icon_cache_id >= 0) && (icon_cache_id < m_WindowIcons.size()) )
{
const TMNGRWindowIcon& window_icon = m_WindowIcons[icon_cache_id];
size = window_icon.Size;
uv_min.x = window_icon.AtlasUV.x;
uv_min.y = window_icon.AtlasUV.y;
uv_max.x = window_icon.AtlasUV.z;
uv_max.y = window_icon.AtlasUV.w;
return true;
}
return false;
}
bool TextureManager::GetOverlayIconTextureInfo(OverlayConfigData& data, ImVec2& size, ImVec2& uv_min, ImVec2& uv_max, bool is_xsmall, bool* has_window_icon)
{
if ( (is_xsmall) && (data.ConfigInt[configid_int_overlay_capture_source] == ovrl_capsource_winrt_capture) && (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0) )
{
//XSmall returns the window icon itself
int cache_id = GetWindowIconCacheID((HWND)data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd], data.ConfigHandle[configid_handle_overlay_state_winrt_last_hicon]);
if (cache_id != -1)
{
if (has_window_icon != nullptr)
*has_window_icon = true;
return GetWindowIconTextureInfo(cache_id, size, uv_min, uv_max);
}
}
return GetTextureInfo(GetOverlayIconTextureID(data, is_xsmall, has_window_icon), size, uv_min, uv_max);
}
bool TextureManager::AddFontBuilderString(const std::string& str)
{
//Add only if it's not already in the extra string list. Avoids duplicates and unnecessary texture rebuilds if the requested character can't be found in the loaded fonts
if (std::find(m_FontBuilderExtraStrings.begin(), m_FontBuilderExtraStrings.end(), str) == m_FontBuilderExtraStrings.end())
{
m_FontBuilderExtraStrings.push_back(str);
return true;
}
return false;
}
TMNGRTexID TextureManager::GetOverlayIconTextureID(const OverlayConfigData& data, bool is_xsmall, bool* has_window_icon)
{
TMNGRTexID texture_id = (is_xsmall) ? tmtex_icon_xsmall_desktop_none : tmtex_icon_desktop_none;
int desktop_id = -2;
if (has_window_icon != nullptr)
*has_window_icon = false;
switch (data.ConfigInt[configid_int_overlay_capture_source])
{
case ovrl_capsource_desktop_duplication:
{
desktop_id = data.ConfigInt[configid_int_overlay_desktop_id];
break;
}
case ovrl_capsource_winrt_capture:
{
if (data.ConfigHandle[configid_handle_overlay_state_winrt_hwnd] != 0)
{
if (has_window_icon != nullptr)
*has_window_icon = true;
texture_id = (is_xsmall) ? tmtex_icon_xsmall_desktop_none : tmtex_icon_window_overlay;
}
else if (data.ConfigInt[configid_int_overlay_winrt_desktop_id] != -2)
{
desktop_id = data.ConfigInt[configid_int_overlay_winrt_desktop_id];
}
else
{
texture_id = (is_xsmall) ? tmtex_icon_xsmall_desktop_none : tmtex_icon_desktop_none;
}
break;
}
case ovrl_capsource_ui:
{
texture_id = (is_xsmall) ? tmtex_icon_xsmall_performance_monitor : tmtex_icon_performance_monitor;
break;
}
case ovrl_capsource_browser:
{
texture_id = (is_xsmall) ? tmtex_icon_xsmall_browser : tmtex_icon_browser;
break;
}
}
if (desktop_id != -2)
{
if (is_xsmall)
{
texture_id = (tmtex_icon_xsmall_desktop_1 + desktop_id <= tmtex_icon_xsmall_desktop_6) ? (TMNGRTexID)(tmtex_icon_xsmall_desktop_1 + desktop_id) : tmtex_icon_xsmall_desktop;
}
else
{
texture_id = (tmtex_icon_desktop_1 + desktop_id <= tmtex_icon_desktop_6) ? (TMNGRTexID)(tmtex_icon_desktop_1 + desktop_id) : tmtex_icon_desktop;
}
}
return texture_id;
}
================================================
FILE: src/DesktopPlusUI/TextureManager.h
================================================
//Desktop+UI loads all textures into Dear ImGui's font texture atlas
//Bigger texture sizes are well supported on VR-running GPUs and less texture switching is more efficient
//It's also more convenient in general
//This is using GDI+ to load PNGs. Seemed like the next best option without too much overhead and doesn't need any extra library to ship
#pragma once
#define NOMINMAX
#include
#include
#include
#include
#include "imgui.h"
struct Action;
enum TMNGRTexID
{
tmtex_icon_desktop,
tmtex_icon_desktop_all,
tmtex_icon_desktop_1,
tmtex_icon_desktop_2,
tmtex_icon_desktop_3,
tmtex_icon_desktop_4,
tmtex_icon_desktop_5,
tmtex_icon_desktop_6,
tmtex_icon_desktop_next,
tmtex_icon_desktop_prev,
tmtex_icon_desktop_none,
tmtex_icon_performance_monitor,
tmtex_icon_browser,
tmtex_icon_settings,
tmtex_icon_keyboard,
tmtex_icon_switch_task,
tmtex_icon_add,
tmtex_icon_window_overlay,
tmtex_icon_small_app_icon,
tmtex_icon_small_close,
tmtex_icon_small_move,
tmtex_icon_small_move_locked,
tmtex_icon_small_add_window,
tmtex_icon_small_actionbar,
tmtex_icon_small_performance_monitor_reset,
tmtex_icon_small_browser_back,
tmtex_icon_small_browser_forward,
tmtex_icon_small_browser_refresh,
tmtex_icon_small_browser_stop,
tmtex_icon_xsmall_desktop,
tmtex_icon_xsmall_desktop_all,
tmtex_icon_xsmall_desktop_1,
tmtex_icon_xsmall_desktop_2,
tmtex_icon_xsmall_desktop_3,
tmtex_icon_xsmall_desktop_4,
tmtex_icon_xsmall_desktop_5,
tmtex_icon_xsmall_desktop_6,
tmtex_icon_xsmall_desktop_none,
tmtex_icon_xsmall_performance_monitor,
tmtex_icon_xsmall_browser,
tmtex_icon_xsmall_settings,
tmtex_icon_xsmall_keyboard,
tmtex_icon_xsmall_origin_room,
tmtex_icon_xsmall_origin_hmd_floor,
tmtex_icon_xsmall_origin_seated_space,
tmtex_icon_xsmall_origin_dashboard,
tmtex_icon_xsmall_origin_hmd,
tmtex_icon_xsmall_origin_left_hand,
tmtex_icon_xsmall_origin_right_hand,
tmtex_icon_xsmall_origin_aux,
tmtex_icon_xsmall_origin_theater_screen,
tmtex_icon_xxsmall_close,
tmtex_icon_xxsmall_pin,
tmtex_icon_xxsmall_unpin,
tmtex_icon_xxsmall_browser_back,
tmtex_icon_temp, //This is an odd one to hack-ishly load one icon without associating it with anything. The file for this can be set freely by TextureManager
tmtex_MAX
};
struct TMNGRWindowIcon
{
HICON IconHandle = nullptr;
std::unique_ptr PixelData; //RGBA
ImVec2 Size = {0.0f, 0.0f};
int ImGuiRectID = -1; //-1 when no icon loaded (ID on ImGui end is not valid after building the font)
ImVec4 AtlasUV = {0.0f, 0.0f, 0.0f, 0.0f};
};
class OverlayConfigData;
class TextureManager
{
private:
static const wchar_t* s_TextureFilenames[tmtex_MAX];
int m_ImGuiRectIDs[tmtex_MAX]; //-1 when not loaded (ID on ImGui end is not valid after building the font as data is cleared to save memory)
ImVec2 m_AtlasSizes[tmtex_MAX];
ImVec4 m_AtlasUVs[tmtex_MAX];
std::wstring m_TextureFilenameIconTemp;
std::vector