Unverified Commit c4db278a authored by Alovchin91's avatar Alovchin91 Committed by GitHub

Add Windows Jump Lists support in AWT (#943)

This PR adds Windows Jump Lists support to AWT and fixes compilation on
Visual Studio 2022.
parent 576deebf
package org.jetbrains.skiko.sample package org.jetbrains.skiko.sample
import org.jetbrains.skiko.* import org.jetbrains.skiko.*
import org.jetbrains.skiko.windows.*
import java.awt.Dimension import java.awt.Dimension
import javax.swing.* import javax.swing.*
fun main() { fun main(args: Array<String>) {
val skiaLayer = SkiaLayer() val skiaLayer = SkiaLayer()
parseArgs(args)
val app = run { val app = run {
//EmojiStory() //EmojiStory()
AwtClocks(skiaLayer) AwtClocks(skiaLayer)
...@@ -20,5 +22,70 @@ fun main() { ...@@ -20,5 +22,70 @@ fun main() {
skiaLayer.needRedraw() skiaLayer.needRedraw()
window.pack() window.pack()
window.isVisible = true window.isVisible = true
createWindowsJumpList()
}
}
/**
* The Jump List sample requires a separate executable for two reasons:
* 1. The java.exe process somehow blocks the Jump Lists so to enable them we would have to provide
* an AppUserModelID for the sample's window, and
* 2. In the current implementation, the Jump List items launch the calling process when clicked with
* command line arguments provided to the JumpListItem. Without a separate executable,
* java.exe will be launched.
*/
private fun createWindowsJumpList() {
// Check if Jump Lists are supported
if (JumpList.isSupported()) {
// Start a Jump List building transaction
JumpList.build {
// If the process has an explicit AppUserModelID, it should be set for the Jump List
// before calling [beginList]
// setAppID("org.jetbrains.skiko.SkiaMultiplatformSample.awt")
// Create a new Jump List
beginList()
// Collect elements to add to the Jump List, e.g. some recent documents
val recentItems = listOf(
JumpListItem("Recent item 1", "path\\to\\recent\\item\\1").apply {
attributes = JumpListItemAttributes().apply {
description = "The first recent item"
icon = JumpListItemIcon("explorer.exe", 0)
}
}
)
// TODO: Exclude [removedItems] from [recentItems]
val removedItems = getRemovedItems()
// Add recent items to the Jump List
addCategory("Recent items", recentItems)
// Add a user task to the Jump List
addUserTask(JumpListItem("User Task 1", "--user-task-1"))
// Commit the Jump List and finish the transaction
try {
commit().onFailure {
println("The Jump List has been committed but appending categories failed. Please make sure Jump Lists are enabled for the user.")
}
}
catch (e: JumpListException) {
println("Couldn't build the Jump List, most likely because the sample is run via java.exe. ${e.message}")
}
}
}
}
// When a Jump List item is clicked, the app is re-launched with an argument
// passed to the JumpListItem's constructor
private fun parseArgs(args: Array<String>) {
for(arg in args) {
when (arg) {
"--user-task-1" -> println("User Task 1 in the Jump List was clicked!")
"path\\to\\recent\\item\\1" -> println("Recent Item 1 in the Jump List was clicked!")
else -> println("Unknown argument: ($arg)")
}
} }
} }
...@@ -24,6 +24,7 @@ import org.gradle.api.tasks.TaskProvider ...@@ -24,6 +24,7 @@ import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Jar import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.withType import org.gradle.kotlin.dsl.withType
import org.gradle.util.internal.VersionNumber
import projectDirs import projectDirs
import registerOrGetSkiaDirProvider import registerOrGetSkiaDirProvider
import registerSkikoTask import registerSkikoTask
...@@ -295,6 +296,16 @@ fun SkikoProjectContext.createLinkJvmBindings( ...@@ -295,6 +296,16 @@ fun SkikoProjectContext.createLinkJvmBindings(
"/ignore:4217" "/ignore:4217"
) )
) )
// workaround for VS Build Tools 2022 (17.2+) change
// https://developercommunity.visualstudio.com/t/-imp-std-init-once-complete-unresolved-external-sy/1684365#T-N10041864
if (windowsSdkPaths.toolchainVersion >= VersionNumber.parse("14.32")) {
addAll(
arrayOf(
"/ALTERNATENAME:__imp___std_init_once_begin_initialize=__imp_InitOnceBeginInitialize",
"/ALTERNATENAME:__imp___std_init_once_complete=__imp_InitOnceComplete"
)
)
}
addAll( addAll(
arrayOf( arrayOf(
"/NOLOGO", "/NOLOGO",
...@@ -302,6 +313,8 @@ fun SkikoProjectContext.createLinkJvmBindings( ...@@ -302,6 +313,8 @@ fun SkikoProjectContext.createLinkJvmBindings(
"Advapi32.lib", "Advapi32.lib",
"gdi32.lib", "gdi32.lib",
"Dwmapi.lib", "Dwmapi.lib",
"ole32.lib",
"Propsys.lib",
"shcore.lib", "shcore.lib",
"user32.lib", "user32.lib",
) )
......
...@@ -2,8 +2,10 @@ import org.gradle.api.invocation.Gradle ...@@ -2,8 +2,10 @@ import org.gradle.api.invocation.Gradle
import org.gradle.kotlin.dsl.support.serviceOf import org.gradle.kotlin.dsl.support.serviceOf
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.* import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.*
import org.gradle.nativeplatform.platform.internal.NativePlatformInternal import org.gradle.nativeplatform.platform.internal.NativePlatformInternal
import org.gradle.nativeplatform.toolchain.internal.EmptySystemLibraries
import org.gradle.nativeplatform.toolchain.internal.SystemLibraries import org.gradle.nativeplatform.toolchain.internal.SystemLibraries
import org.gradle.nativeplatform.toolchain.internal.msvcpp.* import org.gradle.nativeplatform.toolchain.internal.msvcpp.*
import org.gradle.util.internal.VersionNumber
import java.io.File import java.io.File
import kotlin.math.abs import kotlin.math.abs
...@@ -12,6 +14,7 @@ data class WindowsSdkPaths( ...@@ -12,6 +14,7 @@ data class WindowsSdkPaths(
val linker: File, val linker: File,
val includeDirs: Collection<File>, val includeDirs: Collection<File>,
val libDirs: Collection<File>, val libDirs: Collection<File>,
val toolchainVersion: VersionNumber,
) )
private const val ENV_SKIKO_VSBT_PATH = "SKIKO_VSBT_PATH" private const val ENV_SKIKO_VSBT_PATH = "SKIKO_VSBT_PATH"
...@@ -26,12 +29,14 @@ fun findWindowsSdkPaths(gradle: Gradle, arch: Arch): WindowsSdkPaths { ...@@ -26,12 +29,14 @@ fun findWindowsSdkPaths(gradle: Gradle, arch: Arch): WindowsSdkPaths {
val visualCpp = finder.findVisualCpp() val visualCpp = finder.findVisualCpp()
val windowsSdk = finder.findWindowsSdk() val windowsSdk = finder.findWindowsSdk()
val ucrt = finder.findUcrt() val ucrt = finder.findUcrt()
val systemLibraries = listOf(visualCpp, windowsSdk, ucrt) val winrt = finder.findWinrt()
val systemLibraries = listOf(visualCpp, windowsSdk, ucrt, winrt)
return WindowsSdkPaths( return WindowsSdkPaths(
compiler = visualCpp.compilerExecutable.fixPathFor(arch), compiler = visualCpp.compilerExecutable.fixPathFor(arch),
linker = visualCpp.linkerExecutable.fixPathFor(arch), linker = visualCpp.linkerExecutable.fixPathFor(arch),
includeDirs = systemLibraries.flatMap { it.includeDirs }.map { it.fixPathFor(arch) }, includeDirs = systemLibraries.flatMap { it.includeDirs }.map { it.fixPathFor(arch) },
libDirs = systemLibraries.flatMap { it.libDirs }.map { it.fixPathFor(arch) }, libDirs = systemLibraries.flatMap { it.libDirs }.map { it.fixPathFor(arch) },
toolchainVersion = visualCpp.implementationVersion,
) )
} }
...@@ -92,6 +97,15 @@ private class GradleWindowsComponentFinderWrapper( ...@@ -92,6 +97,15 @@ private class GradleWindowsComponentFinderWrapper(
?: error("UCRT component for host platform '$hostPlatform' is null") ?: error("UCRT component for host platform '$hostPlatform' is null")
} }
fun findWinrt(): SystemLibraries {
// Gradle doesn't have a Locator for WinRT, so we take the UCRT one and fix the path
return object : EmptySystemLibraries() {
override fun getIncludeDirs(): List<File> {
return findUcrt().includeDirs.map { File(it.path.replace("ucrt", "winrt")) }
}
}
}
private fun <T : Any> List<T>.chooseComponentByPreferredVersion( private fun <T : Any> List<T>.chooseComponentByPreferredVersion(
componentType: String, componentType: String,
preferredVersionEnvVar: String, preferredVersionEnvVar: String,
......
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <string>
#include <jni.h> #include <jni.h>
template<typename T> template<typename T>
...@@ -8,3 +9,10 @@ T inline fromJavaPointer(jlong ptr) { return reinterpret_cast<T>(static_cast<uin ...@@ -8,3 +9,10 @@ T inline fromJavaPointer(jlong ptr) { return reinterpret_cast<T>(static_cast<uin
template<typename T> template<typename T>
jlong inline toJavaPointer(T ptr) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(ptr)); } jlong inline toJavaPointer(T ptr) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(ptr)); }
std::wstring inline toStdString(JNIEnv *env, jstring jstr) {
const jchar* jchars = env->GetStringCritical(jstr, NULL);
std::wstring wstr = reinterpret_cast<const wchar_t*>(jchars);
env->ReleaseStringCritical(jstr, jchars);
return wstr;
}
#if SK_BUILD_FOR_WIN
#include <windows.h>
#include <wrl/client.h>
#include <shobjidl.h>
#include <propkey.h>
#include <propvarutil.h>
#include <optional>
#include "jni_helpers.h"
template <typename T>
using ComPtr = Microsoft::WRL::ComPtr<T>;
#define THROW_IF_FAILED(action, message, ret) \
do { \
HRESULT hr { action }; \
if (FAILED(hr)) { \
throwJumpListException(env, __FUNCTION__, DWORD(hr), message); \
return ret; \
} \
} while ((void)0, 0)
namespace org::jetbrains::skiko::windows {
namespace JumpListInteropItem {
jclass cls;
jmethodID init;
jmethodID getTitle;
jmethodID getArguments;
jmethodID getDescription;
jmethodID getIconPath;
jmethodID getIconNum;
}
void ensure(JNIEnv* env) {
static jclass _cls = JumpListInteropItem::cls = (jclass) env->NewGlobalRef(env->FindClass("org/jetbrains/skiko/windows/JumpListBuilder$JumpListInteropItem"));
static jmethodID _init = JumpListInteropItem::init = env->GetMethodID(_cls, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V");
static jmethodID _getTitle = JumpListInteropItem::getTitle = env->GetMethodID(_cls, "getTitle", "()Ljava/lang/String;");
static jmethodID _getArguments = JumpListInteropItem::getArguments = env->GetMethodID(_cls, "getArguments", "()Ljava/lang/String;");
static jmethodID _getDescription = JumpListInteropItem::getDescription = env->GetMethodID(_cls, "getDescription", "()Ljava/lang/String;");
static jmethodID _getIconPath = JumpListInteropItem::getIconPath = env->GetMethodID(_cls, "getIconPath", "()Ljava/lang/String;");
static jmethodID _getIconNum = JumpListInteropItem::getIconNum = env->GetMethodID(_cls, "getIconNum", "()I");
}
}
/**
* Convenience wrapper for PROPVARIANTs that need to be cleared.
*/
class PropVariantWrapper {
PROPVARIANT _propvar;
HRESULT _hr;
public:
PropVariantWrapper(PROPVARIANT propvar) : _propvar(propvar), _hr(S_OK) { }
PropVariantWrapper(PCWSTR pszValue) {
_hr = InitPropVariantFromString(pszValue, &_propvar);
}
~PropVariantWrapper() {
if (SUCCEEDED(_hr)) {
PropVariantClear(&_propvar);
}
}
operator HRESULT() {
return _hr;
}
operator PROPVARIANT() {
return _propvar;
}
};
void throwJumpListException(JNIEnv *env, const char * function, DWORD code, const char * context) {
char fullMsg[1024];
char *msg = 0;
FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_MAX_WIDTH_MASK,
NULL, code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &msg, 0, NULL
);
int result = snprintf(fullMsg, sizeof(fullMsg) - 1,
"Native exception in [%s], code %lu: %s\nContext: %s", function, code, msg, context);
LocalFree(msg);
static jclass cls = static_cast<jclass>(env->NewGlobalRef(env->FindClass("org/jetbrains/skiko/windows/JumpListException")));
static jmethodID init = env->GetMethodID(cls, "<init>", "(Ljava/lang/String;I)V");
jthrowable throwable = (jthrowable) env->NewObject(cls, init, env->NewStringUTF(fullMsg), code);
env->Throw(throwable);
}
/**
* Creates a CLSID_ShellLink to insert into the Jump List.
*/
void createShellLink(
JNIEnv* env,
const std::wstring& title,
const std::wstring& arguments,
const std::optional<std::wstring>& description,
const std::optional<std::pair<std::wstring, int>>& icon,
IShellLinkW **ppsl)
{
ComPtr<IShellLinkW> psl;
THROW_IF_FAILED(
CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&psl)),
"Failed to create a shell link.",
);
// Use current executable for the shell link. Can contain a verbatim (\\?\) path so MAX_PATH might not be sufficient.
WCHAR szAppPath[1024];
if (0 == GetModuleFileNameW(NULL, szAppPath, ARRAYSIZE(szAppPath))) {
THROW_IF_FAILED(HRESULT_FROM_WIN32(GetLastError()), "Failed to get current module's path.",);
}
THROW_IF_FAILED(psl->SetPath(szAppPath), "Failed to set shell link's path.",);
THROW_IF_FAILED(psl->SetArguments(arguments.c_str()), "Failed to set shell link's arguments.",);
if (description.has_value()) {
THROW_IF_FAILED(psl->SetDescription(description.value().c_str()), "Failed to set shell link's description.",);
}
if (icon.has_value()) {
const auto& [iconPath, iconNum] = icon.value();
THROW_IF_FAILED(psl->SetIconLocation(iconPath.c_str(), iconNum), "Failed to set shell link's icon.",);
}
// The title property is required on Jump List items provided as an IShellLink instance.
// This value is used as the display name in the Jump List.
ComPtr<IPropertyStore> pps;
THROW_IF_FAILED(psl->QueryInterface(IID_PPV_ARGS(&pps)), "Failed to cast shell link to IPropertyStore.",);
PropVariantWrapper propvar(title.c_str());
THROW_IF_FAILED(propvar, "Failed to create a PropVariant.",);
THROW_IF_FAILED(pps->SetValue(PKEY_Title, propvar), "Failed to set shell link's title.",);
THROW_IF_FAILED(pps->Commit(), "Failed to commit shell link's title.",);
THROW_IF_FAILED(psl->QueryInterface(IID_PPV_ARGS(ppsl)), "Failed to return the shell link.",);
}
void createShellLinkFromInteropObject(JNIEnv* env, jobject obj, IShellLinkW **ppsl) {
org::jetbrains::skiko::windows::ensure(env);
std::wstring title = toStdString(env, (jstring) env->CallObjectMethod(obj, org::jetbrains::skiko::windows::JumpListInteropItem::getTitle));
std::wstring arguments = toStdString(env, (jstring) env->CallObjectMethod(obj, org::jetbrains::skiko::windows::JumpListInteropItem::getArguments));
jstring jDescription = (jstring) env->CallObjectMethod(obj, org::jetbrains::skiko::windows::JumpListInteropItem::getDescription);
jstring jIconPath = (jstring) env->CallObjectMethod(obj, org::jetbrains::skiko::windows::JumpListInteropItem::getIconPath);
int jIconNum = (int) env->CallIntMethod(obj, org::jetbrains::skiko::windows::JumpListInteropItem::getIconNum);
std::optional<std::wstring> description =
env->IsSameObject(jDescription, NULL) ? std::nullopt : std::make_optional(toStdString(env, jDescription));
std::optional<std::pair<std::wstring, int>> icon =
env->IsSameObject(jIconPath, NULL) ? std::nullopt : std::make_optional(std::make_pair(toStdString(env, jIconPath), jIconNum));
return createShellLink(env, title, arguments, description, icon, ppsl);
}
jobject createJumpListInteropItemObject(JNIEnv* env, IShellLinkW *psl)
{
org::jetbrains::skiko::windows::ensure(env);
ComPtr<IPropertyStore> propStore;
if (SUCCEEDED(psl->QueryInterface(IID_PPV_ARGS(&propStore)))) {
PROPVARIANT propvar;
if (SUCCEEDED(propStore->GetValue(PKEY_Title, &propvar))) {
PropVariantWrapper propvarWrapper(propvar);
WCHAR szTitle[512];
if (SUCCEEDED(PropVariantToString(propvar, szTitle, ARRAYSIZE(szTitle)))) {
WCHAR szArguments[1024];
if (SUCCEEDED(psl->GetArguments(szArguments, ARRAYSIZE(szArguments)))) {
WCHAR szIconPath[512];
int numIcon;
if (SUCCEEDED(psl->GetIconLocation(szIconPath, ARRAYSIZE(szIconPath), &numIcon))) {
WCHAR szDescription[1024];
if (SUCCEEDED(psl->GetDescription(szDescription, ARRAYSIZE(szDescription)))) {
return env->NewObject(
org::jetbrains::skiko::windows::JumpListInteropItem::cls,
org::jetbrains::skiko::windows::JumpListInteropItem::init,
env->NewString(reinterpret_cast<const jchar*>(szTitle), wcsnlen_s(szTitle, ARRAYSIZE(szTitle))),
env->NewString(reinterpret_cast<const jchar*>(szArguments), wcsnlen_s(szArguments, ARRAYSIZE(szArguments))),
env->NewString(reinterpret_cast<const jchar*>(szDescription), wcsnlen_s(szDescription, ARRAYSIZE(szDescription))),
env->NewString(reinterpret_cast<const jchar*>(szIconPath), wcsnlen_s(szIconPath, ARRAYSIZE(szIconPath))),
numIcon
);
}
}
}
}
}
}
return NULL;
}
bool createObjectArray(JNIEnv* env, jobjectArray jobjArray, IObjectArray **ppoa) {
jsize jobjArraySize = env->GetArrayLength(jobjArray);
if (0 == jobjArraySize) {
return false;
}
ComPtr<IObjectCollection> poc;
THROW_IF_FAILED(
CoCreateInstance(CLSID_EnumerableObjectCollection, NULL, CLSCTX_INPROC, IID_PPV_ARGS(&poc)),
"Failed to create an instance of EnumerableObjectCollection.", false
);
for (jsize i = 0; i < jobjArraySize; i++) {
jobject obj = env->GetObjectArrayElement(jobjArray, i);
ComPtr<IShellLinkW> psl;
createShellLinkFromInteropObject(env, obj, &psl);
THROW_IF_FAILED(poc->AddObject(psl.Get()), "Failed to add a shell link to the collection.", false);
}
THROW_IF_FAILED(poc->QueryInterface(IID_PPV_ARGS(ppoa)), "Failed to create an object array.", false);
return true;
}
extern "C"
{
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1init(JNIEnv* env, jobject obj) {
THROW_IF_FAILED(CoInitializeEx(NULL, COINIT_MULTITHREADED), "Failed to initialize COM apartment.", 0L);
ComPtr<ICustomDestinationList> pcdl;
THROW_IF_FAILED(
CoCreateInstance(CLSID_DestinationList, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pcdl)),
"Failed to create CLSID_DestinationList.", 0L
);
return toJavaPointer(pcdl.Detach());
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1dispose(
JNIEnv* env, jobject obj, jlong ptr)
{
ICustomDestinationList* pcdl = fromJavaPointer<ICustomDestinationList*>(ptr);
pcdl->Release();
CoUninitialize();
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1setAppID(JNIEnv *env, jobject obj, jlong ptr, jstring appID) {
ComPtr<ICustomDestinationList> pcdl { fromJavaPointer<ICustomDestinationList*>(ptr) };
if (pcdl.Get() == NULL) {
throwJumpListException(env, __FUNCTION__, E_POINTER, "Native pointer is null.");
return;
}
std::wstring strAppID = toStdString(env, appID);
THROW_IF_FAILED(pcdl->SetAppID(strAppID.c_str()), "Failed to set AppUserModelID for the Jump List.",);
}
JNIEXPORT jobjectArray JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1beginList(JNIEnv *env, jobject obj, jlong ptr) {
ComPtr<ICustomDestinationList> pcdl { fromJavaPointer<ICustomDestinationList*>(ptr) };
if (pcdl.Get() == NULL) {
throwJumpListException(env, __FUNCTION__, E_POINTER, "Native pointer is null.");
return NULL;
}
UINT uMaxSlots;
ComPtr<IObjectArray> poaRemoved;
THROW_IF_FAILED(
pcdl->BeginList(&uMaxSlots, IID_PPV_ARGS(&poaRemoved)),
"Failed to BeginList.", NULL
);
UINT cItems;
THROW_IF_FAILED(poaRemoved->GetCount(&cItems), "Failed to get removed destinations count.", NULL);
org::jetbrains::skiko::windows::ensure(env);
jobjectArray itemsArray = env->NewObjectArray(cItems, org::jetbrains::skiko::windows::JumpListInteropItem::cls, NULL);
for (UINT i = 0; i < cItems; i++) {
ComPtr<IShellLinkW> pslRemoved;
if (SUCCEEDED(poaRemoved->GetAt(i, IID_PPV_ARGS(&pslRemoved)))) {
jobject interopItem = createJumpListInteropItemObject(env, pslRemoved.Get());
if (interopItem != NULL) {
env->SetObjectArrayElement(itemsArray, i, interopItem);
}
}
}
return itemsArray;
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1addUserTasks(
JNIEnv* env, jobject obj, jlong ptr, jobjectArray tasks)
{
ComPtr<ICustomDestinationList> pcdl { fromJavaPointer<ICustomDestinationList*>(ptr) };
if (pcdl.Get() == NULL) {
throwJumpListException(env, __FUNCTION__, E_POINTER, "Native pointer is null.");
return;
}
ComPtr<IObjectCollection> poc;
THROW_IF_FAILED(
CoCreateInstance(CLSID_EnumerableObjectCollection, NULL, CLSCTX_INPROC, IID_PPV_ARGS(&poc)),
"Failed to create an instance of EnumerableObjectCollection.",
);
ComPtr<IObjectArray> poa;
if (!createObjectArray(env, tasks, &poa)) {
return;
}
THROW_IF_FAILED(pcdl->AddUserTasks(poa.Get()), "Failed to add user tasks.",);
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1addCategory(
JNIEnv* env, jobject obj, jlong ptr, jstring category, jobjectArray itemsArray)
{
ComPtr<ICustomDestinationList> pcdl { fromJavaPointer<ICustomDestinationList*>(ptr) };
if (pcdl.Get() == NULL) {
throwJumpListException(env, __FUNCTION__, E_POINTER, "Native pointer is null.");
return;
}
ComPtr<IObjectArray> poa;
if (!createObjectArray(env, itemsArray, &poa)) {
return;
}
std::wstring strCategory = toStdString(env, category);
// Items listed in the removed list may not be re-added to the Jump List during this
// list-building transaction. They should not be re-added to the Jump List until
// the user has used the item again. The AppendCategory call will fail if
// an attempt to add an item in the removed list is made.
THROW_IF_FAILED(pcdl->AppendCategory(strCategory.c_str(), poa.Get()), "Failed to append category.",);
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1commit(
JNIEnv* env, jobject obj, jlong ptr)
{
ComPtr<ICustomDestinationList> pcdl { fromJavaPointer<ICustomDestinationList*>(ptr) };
if (pcdl.Get() == NULL) {
throwJumpListException(env, __FUNCTION__, E_POINTER, "Native pointer is null.");
return;
}
THROW_IF_FAILED(pcdl->CommitList(), "Failed to commit the Jump List.",);
}
}
#endif
package org.jetbrains.skiko.windows
import org.jetbrains.skiko.hostOs
/**
* Provides access to Windows Jump List features
*/
object JumpList {
/**
* Returns a value that indicates whether Jump Lists are supported.
*/
fun isSupported(): Boolean = when {
hostOs.isWindows -> true
else -> false
}
/**
* Wraps a single Jump List building transaction.
*
* The transaction starts with a call to [JumpListBuilder.beginList] and is finalised
* by a call to [JumpListBuilder.commit].
*/
fun <R> build(block: JumpListBuilder.() -> R): R = when {
hostOs.isWindows -> JumpListBuilder().use { builder ->
builder.initialize()
block(builder)
}
else -> error("Jump List is only supported on Windows")
}
/**
* Wraps a single Jump List building transaction.
*
* The transaction starts with a call to [JumpListBuilder.beginList] and is finalised
* by a call to [JumpListBuilder.commit].
*/
suspend fun <R> buildAsync(block: suspend JumpListBuilder.() -> R): R = when {
hostOs.isWindows -> JumpListBuilder().use { builder ->
builder.initialize()
block(builder)
}
else -> error("Jump List is only supported on Windows")
}
}
/**
* Represents an item in the Jump List.
*/
class JumpListItem(val title: String, val arguments: String) {
var attributes: JumpListItemAttributes? = null
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as JumpListItem
if (title != other.title) return false
if (arguments != other.arguments) return false
return true
}
override fun hashCode(): Int {
var result = title.hashCode()
result = 31 * result + arguments.hashCode()
return result
}
}
/**
* Represents optional attributes that can be added to a Jump List item.
*/
class JumpListItemAttributes {
var description: String? = null
var icon: JumpListItemIcon? = null
}
/**
* Represents an icon that can be assigned to a Jump List item.
*
* @param path A path to the file containing the icon.
* @param index An index of the icon in the specified file.
*/
class JumpListItemIcon(val path: String, val index: Int) {
companion object {
internal fun fromParts(path: String?, num: Int): JumpListItemIcon? {
if (path.isNullOrEmpty()) return null
return JumpListItemIcon(path, num)
}
}
}
/**
* Provides functions to build the Jump List.
*
* This is a wrapper for ICustomDestinationList Windows COM interface.
*/
class JumpListBuilder internal constructor() : AutoCloseable {
private var jumpListPointer: Long = 0L
private var removedItems: List<JumpListItem> = listOf()
private val userTasks: MutableList<JumpListInteropItem> = mutableListOf()
private val categories: MutableMap<String, List<JumpListInteropItem>> = mutableMapOf()
/**
* Starts the Jump List building transaction.
*/
internal fun initialize() {
jumpListPointer = jumpList_init().also { ptr ->
check(ptr != 0L) { "Failed to initialize Windows jump list" }
}
}
/**
* Specifies an optional AppUserModelID for the Jump List.
*
* This function must be called if the application has an explicit AppUserModelID.
*
* @throws JumpListException [setAppID] must be called before [beginList]
*/
fun setAppID(appID: String) {
check(jumpListPointer != 0L) { "The jump list pointer is invalid" }
jumpList_setAppID(jumpListPointer, appID)
}
/**
* Creates a new list.
*/
fun beginList() {
check(jumpListPointer != 0L) { "The jump list pointer is invalid" }
jumpList_beginList(jumpListPointer).let { interopItems ->
removedItems = interopItems.map { it.fromInterop() }
}
}
/**
* Returns a list of items the user has chosen to remove from their Jump List.
*/
fun getRemovedItems(): List<JumpListItem> = removedItems
/**
* Adds a task to the Jump List. Tasks always appear in the canonical "Tasks" category
* that is displayed at the bottom of the Jump List, after all other categories.
*/
fun addUserTask(task: JumpListItem) {
check(jumpListPointer != 0L) { "The jump list pointer is invalid" }
userTasks.add(task.toInterop())
}
/**
* Adds a category to the Jump List. If there are more categories, they will appear
* from top to bottom in the order they are appended.
*
* Make sure to exclude removed items returned by the [beginList] call as they may not be
* re-added to the list during the same list-building transaction.
*/
fun addCategory(category: String, items: List<JumpListItem>) {
check(jumpListPointer != 0L) { "The jump list pointer is invalid" }
items.map { it.toInterop() }.let { interopItems ->
categories.merge(category, interopItems) { existing, new -> existing + new }
}
}
/**
* Finalises the Jump List building transaction by committing the list.
*
* @return [Result.success] if the list has been committed successfully. [Result.failure] if there was
* a recoverable error when committing the list. In the latter case, the list was still committed, but
* might lack some or all categories.
*
* @throws JumpListException An attempt to add an item in the removed list was made (see [addCategory])
*/
fun commit(): Result<Unit> {
check(jumpListPointer != 0L) { "The jump list pointer is invalid" }
jumpList_addUserTasks(jumpListPointer, userTasks.toTypedArray())
val result = try {
categories.forEach { (name, items) ->
jumpList_addCategory(jumpListPointer, name, items.toTypedArray())
}
Result.success(Unit)
}
catch (e: JumpListException) {
// According to the documentation, in case if AppendCategory fails with E_ACCESSDENIED,
// the application should continue to update tasks and call CommitList.
if (e.code != -2147024891) {
throw e
}
Result.failure(e)
}
jumpList_commit(jumpListPointer)
return result
}
/**
* Frees the Jump List's native resources.
*/
override fun close() {
if (jumpListPointer != 0L) {
jumpList_dispose(jumpListPointer)
}
jumpListPointer = 0L
}
private external fun jumpList_init(): Long
private external fun jumpList_dispose(ptr: Long)
private external fun jumpList_setAppID(ptr: Long, appID: String)
private external fun jumpList_beginList(ptr: Long): Array<JumpListInteropItem>
private external fun jumpList_addUserTasks(ptr: Long, tasks: Array<JumpListInteropItem>)
private external fun jumpList_addCategory(ptr: Long, category: String, itemsArray: Array<JumpListInteropItem>)
private external fun jumpList_commit(ptr: Long)
private class JumpListInteropItem(val title: String,
val arguments: String,
val description: String?,
val iconPath: String?,
val iconNum: Int)
private fun JumpListItem.toInterop() = JumpListInteropItem(
title, arguments, attributes?.description, attributes?.icon?.path, attributes?.icon?.index ?: 0
)
private fun JumpListInteropItem.fromInterop() = JumpListItem(title, arguments)
.also { item ->
item.attributes = when {
iconPath.isNullOrEmpty() && description.isNullOrEmpty() -> null
else -> JumpListItemAttributes().also { attr ->
attr.description = description
attr.icon = JumpListItemIcon.fromParts(iconPath, iconNum)
}
}
}
}
class JumpListException(message: String, val code: Int) : RuntimeException(message)
...@@ -79,6 +79,38 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_Direct3DRedrawer_initFe ...@@ -79,6 +79,38 @@ JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_Direct3DRedrawer_initFe
JNIEnv *env, jobject redrawer, jlong devicePtr) { JNIEnv *env, jobject redrawer, jlong devicePtr) {
skikoUnimplemented("Java_org_jetbrains_skiko_redrawer_Direct3DRedrawer_initFence"); skikoUnimplemented("Java_org_jetbrains_skiko_redrawer_Direct3DRedrawer_initFence");
} }
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1init(JNIEnv *env, jobject obj) {
skikoUnimplemented("Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1init");
return 0;
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1dispose(JNIEnv *env, jobject obj, jlong ptr) {
skikoUnimplemented("Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1dispose");
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1setAppID(JNIEnv *env, jobject obj, jlong ptr, jstring appID) {
skikoUnimplemented("Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1setAppID");
}
JNIEXPORT jobjectArray JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1beginList(JNIEnv *env, jobject obj, jlong ptr) {
skikoUnimplemented("Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1beginList");
return 0;
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1addUserTasks(
JNIEnv *env, jobject obj, jlong ptr, jobjectArray tasks) {
skikoUnimplemented("Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1addUserTasks");
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1addCategory(
JNIEnv *env, jobject obj, jlong ptr, jstring category, jobjectArray itemsArray) {
skikoUnimplemented("Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1addCategory");
}
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1commit(JNIEnv *env, jobject obj, jlong ptr) {
skikoUnimplemented("Java_org_jetbrains_skiko_windows_JumpListBuilder_jumpList_1commit");
}
#endif #endif
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment