view release on metacpan or search on metacpan
- Fix headless Linux test failures to satisfy cpantesters
0.15 2026-04-06
- Add Chandra::Splash - For splash screen / loading state
- Add Chandra::Log - For structured logging framework
- Add Chandra::Socket::Token - For token management with rotation and expiry
- More test fixes for Strawberry Perl
- Makefile fix for darwin intel arch
0.14 2026-04-06
- Add Chandra::ContextMenu - Context menus for Chandra applications
- A few more fixes to try and get Strawberry to build
0.13 2026-04-05
- Add Chandra::Window - Multi-window management
- Add Chandra::Assets - Asset bundling and resource loading for Chandra apps
- Add Chandra::Clipboard - System clipboard access (text, HTML, images)
- Add Chandra::DragDrop - File drops, text drops, intra-app drag and drop
- Auto-detect headless environments in Makefile.PL for CPAN Testers
- Fix Windows/Strawberry Perl builds (MULTIPLICITY-safe Win32 APIs)
include/webview-win32.c
include/webview.h
lib/Chandra.pm
lib/Chandra/App.pm
lib/Chandra/Assets.pm
lib/Chandra/Bind.pm
lib/Chandra/Bridge.pm
lib/Chandra/Bridge/Extension.pm
lib/Chandra/Canvas.pm
lib/Chandra/Clipboard.pm
lib/Chandra/ContextMenu.pm
lib/Chandra/DevTools.pm
lib/Chandra/Dialog.pm
lib/Chandra/DragDrop.pm
lib/Chandra/Element.pm
lib/Chandra/Error.pm
lib/Chandra/Event.pm
lib/Chandra/Form.pm
lib/Chandra/HotReload.pm
lib/Chandra/Log.pm
lib/Chandra/Notify.pm
examples/contextmenu_example.pl view on Meta::CPAN
#!/usr/bin/env perl
#
# Example: Context Menus
#
# Right-click context menus with static items, dynamic items,
# submenus, icons, checkable items, and keyboard shortcut hints.
#
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../blib/lib", "$FindBin::Bin/../blib/arch";
use Chandra::App;
my $app = Chandra::App->new(
title => 'Context Menu Example',
width => 600,
height => 450,
debug => 1,
);
# ---- Static context menu on the editor area ----
$app->context_menu('#editor', [
{ label => 'Select All', action => sub {
$app->eval("var e=document.getElementById('editor');var r=document.createRange();r.selectNodeContents(e);var s=window.getSelection();s.removeAllRanges();s.addRange(r);");
examples/contextmenu_example.pl view on Meta::CPAN
white-space: pre-wrap; background: #fff; overflow-y: auto;
line-height: 1.6; min-height: 200px; }
#log { height: 120px; overflow-y: auto; background: #2d2d2d; color: #0f0;
font-family: monospace; font-size: 12px; padding: 8px; border-top: 2px solid #444; }
.log-entry { padding: 2px 0; }
.hint { color: #888; font-size: 12px; padding: 8px 16px; background: #fafafa;
border-top: 1px solid #eee; }
</style>
</head>
<body>
<div class="header">Context Menu Example</div>
<div class="main">
<div class="sidebar">
<div class="list-item" id="file-readme">README.md</div>
<div class="list-item" id="file-main">main.pl</div>
<div class="list-item" id="file-config">config.yml</div>
<div class="list-item" id="file-test">test.t</div>
<div class="list-item" id="file-lib">lib/App.pm</div>
</div>
<div id="editor">use strict;
use warnings;
include/chandra/chandra.h view on Meta::CPAN
call_method("_dispatch", G_DISCARD | G_EVAL);
if (SvTRUE(ERRSV))
warn("DragDrop dispatch error: %s", SvPV_nolen(ERRSV));
FREETMPS; LEAVE;
}
ST(0) = &PL_sv_undef;
XSRETURN(1);
}
/* ---- ContextMenu static XS callback ---- */
static XS(XS_Chandra__ContextMenu__dispatch_trampoline)
{
dXSARGS;
SV *cm_self = (SV *)CvXSUBANY(cv).any_ptr;
SV *json_sv = (items > 0) ? ST(0) : &PL_sv_undef;
if (!cm_self || !SvOK(cm_self) || !SvROK(cm_self)) {
ST(0) = &PL_sv_undef;
XSRETURN(1);
}
{
dSP;
ENTER; SAVETMPS;
PUSHMARK(SP);
XPUSHs(cm_self);
XPUSHs(json_sv);
PUTBACK;
call_method("_dispatch", G_DISCARD | G_EVAL);
if (SvTRUE(ERRSV))
warn("ContextMenu dispatch error: %s", SvPV_nolen(ERRSV));
FREETMPS; LEAVE;
}
ST(0) = &PL_sv_undef;
XSRETURN(1);
}
#endif /* CHANDRA_XS_IMPLEMENTATION */
#endif /* CHANDRA_H */
include/chandra/chandra_contextmenu.h view on Meta::CPAN
#ifndef CHANDRA_CONTEXTMENU_H
#define CHANDRA_CONTEXTMENU_H
/* Static C helper functions for Chandra::ContextMenu */
static IV
_cm_next_id(pTHX_ HV *hv)
{
SV **id_svp = hv_fetchs(hv, "_next_id", 0);
IV id = SvIV(*id_svp);
sv_setiv(*id_svp, id + 1);
return id;
}
include/webview-cocoa.c view on Meta::CPAN
#define WKUserScriptInjectionTimeAtDocumentStart 0
#define NSApplicationActivationPolicyRegular 0
static id get_nsstring(const char *c_str) {
return ((id(*)(id, SEL, const char *c_str))objc_msgSend)((id)objc_getClass("NSString"),
sel_registerName("stringWithUTF8String:"), c_str);
}
static id create_menu_item(id title, const char *action, const char *key) {
id item = ((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSMenuItem"), sel_registerName("alloc"));
((void(*)(id, SEL, id, SEL, id))objc_msgSend)(item, sel_registerName("initWithTitle:action:keyEquivalent:"), title, sel_registerName(action), get_nsstring(key));
((void(*)(id, SEL))objc_msgSend)(item, sel_registerName("autorelease"));
return item;
}
static void webview_window_will_close(id self, SEL cmd, id notification) {
struct webview *w = (struct webview *)objc_getAssociatedObject(self, "webview");
include/webview-cocoa.c view on Meta::CPAN
NSApplicationActivationPolicyRegular);
((void(*)(id, SEL))objc_msgSend)(((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSApplication"),
sel_registerName("sharedApplication")),
sel_registerName("finishLaunching"));
((void(*)(id, SEL, int))objc_msgSend)(((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSApplication"),
sel_registerName("sharedApplication")),
sel_registerName("activateIgnoringOtherApps:"), 1);
id menubar = ((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSMenu"), sel_registerName("alloc"));
((void(*)(id, SEL, id))objc_msgSend)(menubar, sel_registerName("initWithTitle:"), get_nsstring(""));
((void(*)(id, SEL))objc_msgSend)(menubar, sel_registerName("autorelease"));
id appName = ((id(*)(id, SEL))objc_msgSend)(((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSProcessInfo"),
sel_registerName("processInfo")),
sel_registerName("processName"));
id appMenuItem = ((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSMenuItem"), sel_registerName("alloc"));
((void(*)(id, SEL, id, void *, id))objc_msgSend)(appMenuItem,
sel_registerName("initWithTitle:action:keyEquivalent:"), appName,
NULL, get_nsstring(""));
id appMenu = ((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSMenu"), sel_registerName("alloc"));
((void(*)(id, SEL, id))objc_msgSend)(appMenu, sel_registerName("initWithTitle:"), appName);
((void(*)(id, SEL))objc_msgSend)(appMenu, sel_registerName("autorelease"));
((void(*)(id, SEL, id))objc_msgSend)(appMenuItem, sel_registerName("setSubmenu:"), appMenu);
((void(*)(id, SEL, id))objc_msgSend)(menubar, sel_registerName("addItem:"), appMenuItem);
id title =
((id(*)(id, SEL, id))objc_msgSend)(get_nsstring("Hide "),
sel_registerName("stringByAppendingString:"), appName);
id item = create_menu_item(title, "hide:", "h");
((void(*)(id, SEL, id))objc_msgSend)(appMenu, sel_registerName("addItem:"), item);
item = create_menu_item(get_nsstring("Hide Others"),
"hideOtherApplications:", "h");
((void(*)(id, SEL, int))objc_msgSend)(item, sel_registerName("setKeyEquivalentModifierMask:"),
(NSEventModifierFlagOption | NSEventModifierFlagCommand));
((void(*)(id, SEL, id))objc_msgSend)(appMenu, sel_registerName("addItem:"), item);
item =
create_menu_item(get_nsstring("Show All"), "unhideAllApplications:", "");
((void(*)(id, SEL, id))objc_msgSend)(appMenu, sel_registerName("addItem:"), item);
((void(*)(id, SEL, id))objc_msgSend)(appMenu, sel_registerName("addItem:"),
((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSMenuItem"),
sel_registerName("separatorItem")));
title = ((id(*)(id, SEL, id))objc_msgSend)(get_nsstring("Quit "),
sel_registerName("stringByAppendingString:"), appName);
item = create_menu_item(title, "terminate:", "q");
((void(*)(id, SEL, id))objc_msgSend)(appMenu, sel_registerName("addItem:"), item);
((void(*)(id, SEL, id))objc_msgSend)(((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSApplication"),
sel_registerName("sharedApplication")),
sel_registerName("setMainMenu:"), menubar);
w->priv.should_exit = 0;
return 0;
}
WEBVIEW_API int webview_loop(struct webview *w, int blocking) {
id until = (blocking ? ((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSDate"),
sel_registerName("distantFuture"))
: ((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSDate"),
sel_registerName("distantPast")));
include/webview-edge.c view on Meta::CPAN
HRESULT (STDMETHODCALLTYPE *get_IsScriptEnabled)(ICoreWebView2Settings*, BOOL*);
HRESULT (STDMETHODCALLTYPE *put_IsScriptEnabled)(ICoreWebView2Settings*, BOOL);
HRESULT (STDMETHODCALLTYPE *get_IsWebMessageEnabled)(ICoreWebView2Settings*, BOOL*);
HRESULT (STDMETHODCALLTYPE *put_IsWebMessageEnabled)(ICoreWebView2Settings*, BOOL);
HRESULT (STDMETHODCALLTYPE *get_AreDefaultScriptDialogsEnabled)(ICoreWebView2Settings*, BOOL*);
HRESULT (STDMETHODCALLTYPE *put_AreDefaultScriptDialogsEnabled)(ICoreWebView2Settings*, BOOL);
HRESULT (STDMETHODCALLTYPE *get_IsStatusBarEnabled)(ICoreWebView2Settings*, BOOL*);
HRESULT (STDMETHODCALLTYPE *put_IsStatusBarEnabled)(ICoreWebView2Settings*, BOOL);
HRESULT (STDMETHODCALLTYPE *get_AreDevToolsEnabled)(ICoreWebView2Settings*, BOOL*);
HRESULT (STDMETHODCALLTYPE *put_AreDevToolsEnabled)(ICoreWebView2Settings*, BOOL);
HRESULT (STDMETHODCALLTYPE *get_AreDefaultContextMenusEnabled)(ICoreWebView2Settings*, BOOL*);
HRESULT (STDMETHODCALLTYPE *put_AreDefaultContextMenusEnabled)(ICoreWebView2Settings*, BOOL);
HRESULT (STDMETHODCALLTYPE *get_AreHostObjectsAllowed)(ICoreWebView2Settings*, BOOL*);
HRESULT (STDMETHODCALLTYPE *put_AreHostObjectsAllowed)(ICoreWebView2Settings*, BOOL);
HRESULT (STDMETHODCALLTYPE *get_IsZoomControlEnabled)(ICoreWebView2Settings*, BOOL*);
HRESULT (STDMETHODCALLTYPE *put_IsZoomControlEnabled)(ICoreWebView2Settings*, BOOL);
HRESULT (STDMETHODCALLTYPE *get_IsBuiltInErrorPageEnabled)(ICoreWebView2Settings*, BOOL*);
HRESULT (STDMETHODCALLTYPE *put_IsBuiltInErrorPageEnabled)(ICoreWebView2Settings*, BOOL);
} ICoreWebView2SettingsVtbl;
struct ICoreWebView2Settings {
const ICoreWebView2SettingsVtbl *lpVtbl;
include/webview-edge.c view on Meta::CPAN
}
wv2->webview = webview;
/* Configure settings */
ICoreWebView2Settings *settings = NULL;
webview->lpVtbl->get_Settings(webview, &settings);
if (settings) {
settings->lpVtbl->put_IsScriptEnabled(settings, TRUE);
settings->lpVtbl->put_IsWebMessageEnabled(settings, TRUE);
settings->lpVtbl->put_AreDefaultContextMenusEnabled(settings, h->w->debug ? TRUE : FALSE);
settings->lpVtbl->put_AreDevToolsEnabled(settings, h->w->debug ? TRUE : FALSE);
settings->lpVtbl->Release(settings);
}
/* Add web message handler */
WebMessageHandler *wmh = (WebMessageHandler *)GlobalAlloc(GPTR, sizeof(WebMessageHandler));
if (wmh) {
wmh->lpVtbl = &wmh_vtbl;
wmh->ref = 1;
wmh->w = h->w;
include/webview-tray-cocoa.c view on Meta::CPAN
/* NSStatusBar constants */
#define NSVariableStatusItemLength (-1.0)
#define NSSquareStatusItemLength (-2.0)
/* Forward: tray menu item action handler */
static void tray_menu_item_action(id self, SEL cmd, id sender);
/* Private data for the cocoa tray */
struct webview_tray_cocoa {
id status_item; /* NSStatusItem */
id menu; /* NSMenu */
id delegate_class; /* Registered ObjC class for click handling */
id delegate; /* Instance */
};
static void tray_build_menu(struct webview_tray *t, id menu,
struct webview_tray_item *items, int count);
static id tray_create_menu_for_items(struct webview_tray *t,
struct webview_tray_item *items, int count) {
id menu = ((id(*)(id, SEL))objc_msgSend)(
((id(*)(id, SEL))objc_msgSend)((id)objc_getClass("NSMenu"),
sel_registerName("alloc")),
sel_registerName("init"));
((void(*)(id, SEL, BOOL))objc_msgSend)(menu,
sel_registerName("setAutoenablesItems:"), NO);
tray_build_menu(t, menu, items, count);
return menu;
}
static void tray_build_menu(struct webview_tray *t, id menu,
struct webview_tray_item *items, int count) {
struct webview_tray_cocoa *priv = (struct webview_tray_cocoa *)t->_priv;
for (int i = 0; i < count; i++) {
struct webview_tray_item *it = &items[i];
if (it->is_separator) {
id sep = ((id(*)(id, SEL))objc_msgSend)(
(id)objc_getClass("NSMenuItem"),
sel_registerName("separatorItem"));
((void(*)(id, SEL, id))objc_msgSend)(menu,
sel_registerName("addItem:"), sep);
continue;
}
id title = get_nsstring(it->label ? it->label : "");
id item = ((id(*)(id, SEL))objc_msgSend)(
(id)objc_getClass("NSMenuItem"), sel_registerName("alloc"));
((void(*)(id, SEL, id, SEL, id))objc_msgSend)(item,
sel_registerName("initWithTitle:action:keyEquivalent:"),
title,
it->submenu_count > 0 ? (SEL)NULL : sel_registerName("trayMenuAction:"),
get_nsstring(""));
if (it->submenu_count <= 0) {
((void(*)(id, SEL, id))objc_msgSend)(item,
sel_registerName("setTarget:"), priv->delegate);
/* Store item id as tag */
((void(*)(id, SEL, long))objc_msgSend)(item,
sel_registerName("setTag:"), (long)it->id);
}
include/webview-tray-cocoa.c view on Meta::CPAN
WEBVIEW_API int webview_tray_create(struct webview_tray *t) {
struct webview_tray_cocoa *priv = (struct webview_tray_cocoa *)calloc(1, sizeof(*priv));
if (!priv) return -1;
t->_priv = priv;
/* Create a delegate class for handling menu actions */
priv->delegate_class = objc_allocateClassPair(
(Class)objc_getClass("NSObject"), "ChandraTrayDelegate", 0);
if (priv->delegate_class) {
class_addMethod(priv->delegate_class, sel_registerName("trayMenuAction:"),
(IMP)tray_menu_item_action, "v@:@");
objc_registerClassPair(priv->delegate_class);
}
priv->delegate = ((id(*)(id, SEL))objc_msgSend)(
((id(*)(id, SEL))objc_msgSend)(priv->delegate_class,
sel_registerName("alloc")),
sel_registerName("init"));
/* Store tray pointer as associated object on delegate */
objc_setAssociatedObject(priv->delegate, "webview_tray",
include/webview-tray-cocoa.c view on Meta::CPAN
if (button) {
((void(*)(id, SEL, id))objc_msgSend)(button,
sel_registerName("setTitle:"),
get_nsstring(t->tooltip ? t->tooltip : "App"));
}
}
/* Build and attach menu */
priv->menu = tray_create_menu_for_items(t, t->items, t->item_count);
((void(*)(id, SEL, id))objc_msgSend)(priv->status_item,
sel_registerName("setMenu:"), priv->menu);
return 0;
}
WEBVIEW_API void webview_tray_update(struct webview_tray *t) {
if (!t || !t->_priv) return;
struct webview_tray_cocoa *priv = (struct webview_tray_cocoa *)t->_priv;
/* Update tooltip */
if (t->tooltip) {
include/webview-tray-cocoa.c view on Meta::CPAN
if (button) {
((void(*)(id, SEL, id))objc_msgSend)(button,
sel_registerName("setImage:"), icon);
}
}
}
/* Rebuild menu */
id new_menu = tray_create_menu_for_items(t, t->items, t->item_count);
((void(*)(id, SEL, id))objc_msgSend)(priv->status_item,
sel_registerName("setMenu:"), new_menu);
priv->menu = new_menu;
}
WEBVIEW_API void webview_tray_destroy(struct webview_tray *t) {
if (!t || !t->_priv) return;
struct webview_tray_cocoa *priv = (struct webview_tray_cocoa *)t->_priv;
/* Remove status item from status bar */
id status_bar = ((id(*)(id, SEL))objc_msgSend)(
(id)objc_getClass("NSStatusBar"),
include/webview-tray-gtk.c view on Meta::CPAN
/*
* webview-tray-gtk.c â Linux system tray (GtkStatusIcon) backend
*
* Uses GtkStatusIcon (deprecated but widely supported) with GtkMenu.
* Falls back gracefully if no system tray is available.
*/
struct webview_tray_gtk {
GtkStatusIcon *icon;
GtkWidget *menu;
};
/* Forward */
static void tray_gtk_activate(GtkStatusIcon *icon, gpointer user_data);
static void tray_gtk_popup(GtkStatusIcon *icon, guint button,
guint activate_time, gpointer user_data);
/* Menu item activation callback */
static void tray_gtk_menu_item_activated(GtkMenuItem *menuitem,
gpointer user_data) {
int *item_data = (int *)user_data;
/* item_data[0] = item_id, item_data[1..] = pointer to tray */
/* We pack item_id and tray pointer together */
(void)menuitem;
/* This approach is fragile. Instead, use g_object_set_data. */
}
/* Alternative: use g_object_set_data to attach callback info */
struct tray_gtk_cb_data {
struct webview_tray *tray;
int item_id;
};
static void tray_gtk_item_activated(GtkMenuItem *menuitem, gpointer data) {
(void)menuitem;
struct tray_gtk_cb_data *cb = (struct tray_gtk_cb_data *)data;
if (cb && cb->tray && cb->tray->menu_cb) {
cb->tray->menu_cb(cb->tray->w, cb->item_id);
}
}
static GtkWidget *tray_gtk_build_menu(struct webview_tray *t,
struct webview_tray_item *items,
int count) {
include/webview-tray-win32.c view on Meta::CPAN
HWND msg_hwnd;
};
/* Forward */
static LRESULT CALLBACK tray_wndproc(HWND hwnd, UINT msg, WPARAM wParam,
LPARAM lParam);
static HMENU tray_win32_build_menu(struct webview_tray *t,
struct webview_tray_item *items,
int count) {
HMENU menu = CreatePopupMenu();
for (int i = 0; i < count; i++) {
struct webview_tray_item *it = &items[i];
if (it->is_separator) {
AppendMenuA(menu, MF_SEPARATOR, 0, NULL);
continue;
}
UINT flags = MF_STRING;
if (it->is_disabled) flags |= MF_GRAYED;
if (it->is_checked) flags |= MF_CHECKED;
if (it->submenu_count > 0 && it->submenu) {
HMENU sub = tray_win32_build_menu(t, it->submenu, it->submenu_count);
AppendMenuA(menu, flags | MF_POPUP, (UINT_PTR)sub,
it->label ? it->label : "");
} else {
AppendMenuA(menu, flags, (UINT_PTR)it->id,
it->label ? it->label : "");
}
}
return menu;
}
static LRESULT CALLBACK tray_wndproc(HWND hwnd, UINT msg, WPARAM wParam,
LPARAM lParam) {
struct webview_tray *t = (struct webview_tray *)
GetWindowLongPtr(hwnd, GWLP_USERDATA);
if (msg == WM_TRAYICON) {
if (LOWORD(lParam) == WM_RBUTTONUP || LOWORD(lParam) == WM_LBUTTONUP) {
struct webview_tray_win32 *priv = (struct webview_tray_win32 *)t->_priv;
POINT pt;
GetCursorPos(&pt);
SetForegroundWindow(hwnd);
int cmd = TrackPopupMenu(priv->menu,
TPM_RETURNCMD | TPM_NONOTIFY,
pt.x, pt.y, 0, hwnd, NULL);
SendMessage(hwnd, WM_NULL, 0, 0);
if (cmd > 0 && t->menu_cb) {
t->menu_cb(t->w, cmd);
}
}
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
include/webview-tray-win32.c view on Meta::CPAN
LR_LOADFROMFILE | LR_DEFAULTSIZE);
if (newicon) {
priv->nid.hIcon = newicon;
}
}
Shell_NotifyIconA(NIM_MODIFY, &priv->nid);
/* Rebuild menu */
if (priv->menu) {
DestroyMenu(priv->menu);
}
priv->menu = tray_win32_build_menu(t, t->items, t->item_count);
}
WEBVIEW_API void webview_tray_destroy(struct webview_tray *t) {
if (!t || !t->_priv) return;
struct webview_tray_win32 *priv = (struct webview_tray_win32 *)t->_priv;
Shell_NotifyIconA(NIM_DELETE, &priv->nid);
if (priv->menu) {
DestroyMenu(priv->menu);
}
if (priv->msg_hwnd) {
DestroyWindow(priv->msg_hwnd);
}
free(priv);
t->_priv = NULL;
}
include/webview-win32.c view on Meta::CPAN
}
static HRESULT STDMETHODCALLTYPE ipf_RequestBorderSpace(IOleInPlaceFrame *This, LPCBORDERWIDTHS b) {
(void)This; (void)b; return E_NOTIMPL;
}
static HRESULT STDMETHODCALLTYPE ipf_SetBorderSpace(IOleInPlaceFrame *This, LPCBORDERWIDTHS b) {
(void)This; (void)b; return S_OK;
}
static HRESULT STDMETHODCALLTYPE ipf_SetActiveObject(IOleInPlaceFrame *This, IOleInPlaceActiveObject *a, LPCOLESTR s) {
(void)This; (void)a; (void)s; return S_OK;
}
static HRESULT STDMETHODCALLTYPE ipf_InsertMenus(IOleInPlaceFrame *This, HMENU h, LPOLEMENUGROUPWIDTHS m) {
(void)This; (void)h; (void)m; return E_NOTIMPL;
}
static HRESULT STDMETHODCALLTYPE ipf_SetMenu(IOleInPlaceFrame *This, HMENU a, HOLEMENU b, HWND c) {
(void)This; (void)a; (void)b; (void)c; return S_OK;
}
static HRESULT STDMETHODCALLTYPE ipf_RemoveMenus(IOleInPlaceFrame *This, HMENU h) {
(void)This; (void)h; return E_NOTIMPL;
}
static HRESULT STDMETHODCALLTYPE ipf_SetStatusText(IOleInPlaceFrame *This, LPCOLESTR s) {
(void)This; (void)s; return S_OK;
}
static HRESULT STDMETHODCALLTYPE ipf_EnableModeless(IOleInPlaceFrame *This, BOOL f) {
(void)This; (void)f; return S_OK;
}
static HRESULT STDMETHODCALLTYPE ipf_TranslateAccelerator(IOleInPlaceFrame *This, LPMSG m, WORD w) {
(void)This; (void)m; (void)w; return E_NOTIMPL;
}
static IOleInPlaceFrameVtbl inplace_frame_vtbl = {
ipf_QueryInterface, ipf_AddRef, ipf_Release,
(HRESULT(STDMETHODCALLTYPE *)(IOleInPlaceFrame *, HWND *))ipf_GetWindow,
ipf_ContextSensitiveHelp,
ipf_GetBorder, ipf_RequestBorderSpace, ipf_SetBorderSpace,
ipf_SetActiveObject, ipf_InsertMenus, ipf_SetMenu, ipf_RemoveMenus,
ipf_SetStatusText, ipf_EnableModeless, ipf_TranslateAccelerator
};
/* ---- IDocHostUIHandler ---- */
static HRESULT STDMETHODCALLTYPE uih_QueryInterface(IDocHostUIHandler *This, REFIID riid, void **ppv) {
WebviewSite *s = (WebviewSite *)((char *)This - offsetof(WebviewSite, ui_handler));
return site_QueryInterface(&s->client_site, riid, ppv);
}
static ULONG STDMETHODCALLTYPE uih_AddRef(IDocHostUIHandler *This) {
WebviewSite *s = (WebviewSite *)((char *)This - offsetof(WebviewSite, ui_handler));
return site_AddRef(&s->client_site);
}
static ULONG STDMETHODCALLTYPE uih_Release(IDocHostUIHandler *This) {
WebviewSite *s = (WebviewSite *)((char *)This - offsetof(WebviewSite, ui_handler));
return site_Release(&s->client_site);
}
static HRESULT STDMETHODCALLTYPE uih_ShowContextMenu(IDocHostUIHandler *This, DWORD id,
POINT *pt, IUnknown *pcmdtReserved, IDispatch *pdispReserved) {
(void)This; (void)id; (void)pt; (void)pcmdtReserved; (void)pdispReserved;
return S_OK; /* Suppress default context menu */
}
static HRESULT STDMETHODCALLTYPE uih_GetHostInfo(IDocHostUIHandler *This, DOCHOSTUIINFO *pInfo) {
(void)This;
pInfo->cbSize = sizeof(DOCHOSTUIINFO);
pInfo->dwFlags = DOCHOSTUIFLAG_NO3DBORDER;
pInfo->dwDoubleClick = DOCHOSTUIDBLCLK_DEFAULT;
return S_OK;
include/webview-win32.c view on Meta::CPAN
}
static HRESULT STDMETHODCALLTYPE uih_TranslateUrl(IDocHostUIHandler *This, DWORD f, OLECHAR *url, OLECHAR **purl) {
(void)This; (void)f; (void)url; (void)purl; return S_FALSE;
}
static HRESULT STDMETHODCALLTYPE uih_FilterDataObject(IDocHostUIHandler *This, IDataObject *d, IDataObject **pd) {
(void)This; (void)d; (void)pd; return S_FALSE;
}
static IDocHostUIHandlerVtbl ui_handler_vtbl = {
uih_QueryInterface, uih_AddRef, uih_Release,
uih_ShowContextMenu, uih_GetHostInfo, uih_ShowUI,
uih_HideUI, uih_UpdateUI, uih_EnableModeless,
uih_OnDocWindowActivate, uih_OnFrameWindowActivate,
uih_ResizeBorder, uih_TranslateAccelerator,
uih_GetOptionKeyPath, uih_GetDropTarget,
uih_GetExternal, uih_TranslateUrl, uih_FilterDataObject
};
/* ---- QueryInterface / AddRef / Release ---- */
static HRESULT STDMETHODCALLTYPE site_QueryInterface(IOleClientSite *This, REFIID riid, void **ppv) {
lib/Chandra/ContextMenu.pm view on Meta::CPAN
package Chandra::ContextMenu;
use strict;
use warnings;
use Chandra ();
our $VERSION = '0.24';
1;
__END__
=head1 NAME
Chandra::ContextMenu - Context menus for Chandra applications
=head1 SYNOPSIS
use Chandra::App;
my $app = Chandra::App->new(title => 'My App');
# Quick static context menu
$app->context_menu('#editor', [
{ label => 'Cut', action => sub { cut() }, shortcut => 'Ctrl+X' },
lib/Chandra/ContextMenu.pm view on Meta::CPAN
# Advanced usage via instance
my $menu = $app->context_menu_instance;
$menu->attach_global;
$menu->add_item({ label => 'About', action => sub { show_about() } });
$app->run;
=head1 DESCRIPTION
Chandra::ContextMenu provides HTML-based right-click context menus for
Chandra applications. Menus support nested submenus, separators, disabled
items, checkable items, icons, and keyboard shortcut hints.
Menus can be attached to specific CSS selectors or globally to the
entire document. Items can be static (defined at creation) or dynamic
(generated per right-click via a callback).
=head1 METHODS
=head2 new
my $menu = Chandra::ContextMenu->new(
app => $app,
items => \@items,
);
Create a new context menu. C<items> is an arrayref of item hashrefs.
=head2 attach
$menu->attach('#selector');
$menu->attach('.class', sub { my ($target) = @_; return \@items });
t/35_tray.t view on Meta::CPAN
{
my $tray = Chandra::Tray->new;
my $click_called = 0;
$tray->on_click(sub { $click_called = 1 });
my $cb = $tray->_make_dispatch_callback;
$cb->(-1);
is($click_called, 1, 'on_click handler called with id -1');
}
# --- Menu JSON output ---
{
my $tray = Chandra::Tray->new;
$tray->add_item('Show' => sub {});
$tray->add_separator;
$tray->add_item('Quit' => sub {});
my $menu_json = $tray->_menu_json;
ok(defined $menu_json, 'menu_json produced');
require Cpanel::JSON::XS;
t/51_contextmenu.t view on Meta::CPAN
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;
use Chandra::ContextMenu;
# ---- API exists ----
{
can_ok('Chandra::ContextMenu', qw(
new
attach detach attach_global detach_global
show_at set_item add_item remove_item
items get_items attachments
enable disable is_enabled
inject js_code
_dispatch
));
}
# ---- Constructor defaults ----
{
my $cm = Chandra::ContextMenu->new;
isa_ok($cm, 'Chandra::ContextMenu');
ok($cm->is_enabled, 'enabled by default');
my $items = $cm->items;
is(ref $items, 'ARRAY', 'items returns arrayref');
is(scalar @$items, 0, 'empty items by default');
}
# ---- Constructor with items ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'Cut', action => sub {} },
{ label => 'Copy', action => sub {} },
{ separator => 1 },
{ label => 'Paste', action => sub {} },
],
);
my $items = $cm->items;
is(scalar @$items, 4, '4 items stored');
is($items->[0]{label}, 'Cut', 'first item label');
ok($items->[2]{separator}, 'separator item');
}
# ---- Enable / disable ----
{
my $cm = Chandra::ContextMenu->new;
ok($cm->is_enabled, 'initially enabled');
$cm->disable;
ok(!$cm->is_enabled, 'disabled');
$cm->enable;
ok($cm->is_enabled, 're-enabled');
}
# ---- Chaining ----
{
my $cm = Chandra::ContextMenu->new;
is($cm->enable, $cm, 'enable chains');
is($cm->disable, $cm, 'disable chains');
is($cm->attach('#foo'), $cm, 'attach chains');
is($cm->detach('#foo'), $cm, 'detach chains');
is($cm->attach_global, $cm, 'attach_global chains');
is($cm->detach_global, $cm, 'detach_global chains');
}
# ---- Attach / detach ----
{
my $cm = Chandra::ContextMenu->new;
my @att = $cm->attachments;
is(scalar @att, 0, 'no attachments initially');
$cm->attach('#editor');
$cm->attach('.sidebar');
@att = sort $cm->attachments;
is(scalar @att, 2, 'two attachments');
is($att[0], '#editor', 'first selector');
is($att[1], '.sidebar', 'second selector');
$cm->detach('#editor');
@att = $cm->attachments;
is(scalar @att, 1, 'one after detach');
is($att[0], '.sidebar', 'remaining selector');
}
# ---- Attach with dynamic callback ----
{
my $cm = Chandra::ContextMenu->new;
my $called = 0;
$cm->attach('.item', sub { $called++; return [] });
my @att = $cm->attachments;
is(scalar @att, 1, 'dynamic attach stored');
}
# ---- attach_global / detach_global ----
{
my $cm = Chandra::ContextMenu->new;
$cm->attach_global;
# just verify no crash; _global flag is internal
$cm->detach_global;
ok(1, 'global attach/detach works');
}
# ---- add_item / remove_item ----
{
my $cm = Chandra::ContextMenu->new;
$cm->add_item({ label => 'Alpha', action => sub {} });
$cm->add_item({ label => 'Beta', action => sub {} });
my $items = $cm->items;
is(scalar @$items, 2, '2 items after add');
is($items->[0]{label}, 'Alpha', 'first added');
is($items->[1]{label}, 'Beta', 'second added');
$cm->remove_item('Alpha');
$items = $cm->items;
is(scalar @$items, 1, '1 item after remove');
is($items->[0]{label}, 'Beta', 'correct item remains');
}
# ---- set_item ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'Delete', action => sub {}, disabled => 1 },
],
);
ok($cm->items->[0]{disabled}, 'initially disabled');
$cm->set_item('Delete', disabled => 0);
ok(!$cm->items->[0]{disabled}, 'enabled after set_item');
}
# ---- Separator items ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'A', action => sub {} },
{ separator => 1 },
{ label => 'B', action => sub {} },
],
);
ok($cm->items->[1]{separator}, 'separator present');
is(scalar @{$cm->items}, 3, 'three items total');
}
# ---- Submenu nesting ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'File', submenu => [
{ label => 'New', action => sub {} },
{ label => 'Open', action => sub {} },
{ label => 'Recent', submenu => [
{ label => 'file1.txt', action => sub {} },
]},
]},
],
);
my $items = $cm->items;
is(scalar @$items, 1, 'one top-level item');
is(ref $items->[0]{submenu}, 'ARRAY', 'submenu is arrayref');
is(scalar @{$items->[0]{submenu}}, 3, 'submenu has 3 items');
is(ref $items->[0]{submenu}[2]{submenu}, 'ARRAY', 'nested submenu');
}
# ---- Checkable items ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'Word Wrap', checkable => 1, checked => 1, action => sub {} },
],
);
my $it = $cm->items->[0];
ok($it->{checkable}, 'checkable flag');
ok($it->{checked}, 'initially checked');
}
# ---- Icon and shortcut ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'Copy', icon => "\x{1f4cb}", shortcut => 'Ctrl+C', action => sub {} },
],
);
my $it = $cm->items->[0];
is($it->{shortcut}, 'Ctrl+C', 'shortcut stored');
ok($it->{icon}, 'icon stored');
}
# ---- show_at stores coords ----
{
my $cm = Chandra::ContextMenu->new;
is($cm->show_at(100, 200), $cm, 'show_at chains');
}
# ---- js_code generates code ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'Cut', action => sub {} },
{ separator => 1 },
{ label => 'Paste', action => sub {} },
],
);
$cm->attach('#editor');
my $js = $cm->js_code;
like($js, qr/__chandraCtxMenu/, 'guard variable present');
like($js, qr/#editor/, 'selector in JS');
like($js, qr/contextmenu/, 'contextmenu event');
like($js, qr/closeAll/, 'close function');
like($js, qr/buildMenu/, 'build function');
like($js, qr/l:'Cut'/, 'item label in JS');
like($js, qr/sep:1/, 'separator in JS');
}
# ---- _dispatch action ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'Test', action => sub { $_[0] } },
],
);
$cm->attach('#x');
# Find the action ID
my $id = $cm->items->[0]{_id};
ok(defined $id, 'action ID assigned');
t/51_contextmenu.t view on Meta::CPAN
# so we test with the original action stored in _actions
$cm->_dispatch('{"type":"action","id":' . $id . '}');
# The action in _actions is the original sub, not our replacement
# Just verify no crash
ok(1, '_dispatch action no crash');
}
# ---- _dispatch with disabled ----
{
my $cm = Chandra::ContextMenu->new;
$cm->disable;
eval { $cm->_dispatch('{"type":"action","id":1}') };
ok(!$@, 'dispatch while disabled is no-op');
}
# ---- _dispatch invalid JSON ----
{
my $cm = Chandra::ContextMenu->new;
eval { $cm->_dispatch('not json') };
ok(!$@, 'invalid JSON is no-op');
}
# ---- _dispatch checkable toggle ----
{
my $toggled_to;
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'Wrap', checkable => 1, checked => 1,
action => sub { $toggled_to = $_[0] } },
],
);
$cm->attach('#x');
my $id = $cm->items->[0]{_id};
$cm->_dispatch('{"type":"action","id":' . $id . ',"chk":false}');
is($cm->items->[0]{checked}, 0, 'checked toggled to 0');
}
# ---- get_items alias ----
{
my $cm = Chandra::ContextMenu->new(
items => [{ label => 'A', action => sub {} }],
);
my $a = $cm->items;
my $b = $cm->get_items;
is_deeply($a, $b, 'items and get_items return same ref');
}
done_testing;
t/52_contextmenu_edge.t view on Meta::CPAN
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;
use Chandra::ContextMenu;
# ---- Empty menu ----
{
my $cm = Chandra::ContextMenu->new;
my $js = $cm->js_code;
like($js, qr/items=\[\]/, 'empty items in JS');
}
# ---- Deeply nested submenus ----
{
my $deep = { label => 'L5', action => sub {} };
for my $i (reverse 1..4) {
$deep = { label => "L$i", submenu => [$deep] };
}
my $cm = Chandra::ContextMenu->new(items => [$deep]);
$cm->attach('#x');
my $js = $cm->js_code;
like($js, qr/sub:/, 'nested submenus in JS');
ok(length($js) > 100, 'JS code generated for deep nesting');
}
# ---- Very long label ----
{
my $long = 'A' x 500;
my $cm = Chandra::ContextMenu->new(
items => [{ label => $long, action => sub {} }],
);
my $it = $cm->items->[0];
is(length($it->{label}), 500, 'long label stored');
$cm->attach('#x');
my $js = $cm->js_code;
like($js, qr/A{100}/, 'long label in JS');
}
# ---- Attach to nonexistent selector (no crash) ----
{
my $cm = Chandra::ContextMenu->new;
eval { $cm->attach('#does-not-exist') };
ok(!$@, 'attach nonexistent selector no error');
my @att = $cm->attachments;
is(scalar @att, 1, 'selector stored even if nonexistent');
}
# ---- Duplicate labels ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'Dup', action => sub { 'first' } },
{ label => 'Dup', action => sub { 'second' } },
],
);
is(scalar @{$cm->items}, 2, 'duplicate labels allowed');
# set_item only modifies first match
$cm->set_item('Dup', disabled => 1);
ok($cm->items->[0]{disabled}, 'first dup modified');
ok(!$cm->items->[1]{disabled}, 'second dup not modified');
}
# ---- Unicode labels ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => "\x{2603} Snowman", action => sub {} },
{ label => "Caf\x{e9}", action => sub {} },
],
);
like($cm->items->[0]{label}, qr/Snowman/, 'unicode label stored');
$cm->attach('#x');
my $js = $cm->js_code;
like($js, qr/Snowman/, 'unicode in JS output');
}
# ---- Many items ----
{
my @items = map { { label => "Item $_", action => sub {} } } 1..100;
my $cm = Chandra::ContextMenu->new(items => \@items);
is(scalar @{$cm->items}, 100, '100 items stored');
$cm->attach('#x');
my $js = $cm->js_code;
like($js, qr/Item 100/, 'all items in JS');
}
# ---- Remove nonexistent item (no crash) ----
{
my $cm = Chandra::ContextMenu->new(
items => [{ label => 'A', action => sub {} }],
);
eval { $cm->remove_item('NOPE') };
ok(!$@, 'remove nonexistent no crash');
is(scalar @{$cm->items}, 1, 'items unchanged');
}
# ---- set_item nonexistent (no crash) ----
{
my $cm = Chandra::ContextMenu->new(
items => [{ label => 'A', action => sub {} }],
);
eval { $cm->set_item('NOPE', disabled => 1) };
ok(!$@, 'set_item nonexistent no crash');
}
# ---- Add then remove ----
{
my $cm = Chandra::ContextMenu->new;
$cm->add_item({ label => 'X', action => sub {} });
$cm->add_item({ label => 'Y', action => sub {} });
$cm->add_item({ label => 'Z', action => sub {} });
is(scalar @{$cm->items}, 3, '3 items');
$cm->remove_item('Y');
is(scalar @{$cm->items}, 2, '2 items after remove middle');
is($cm->items->[0]{label}, 'X', 'X remains');
is($cm->items->[1]{label}, 'Z', 'Z remains');
}
# ---- Separator-only menu ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ separator => 1 },
{ separator => 1 },
],
);
$cm->attach('#x');
my $js = $cm->js_code;
like($js, qr/sep:1/, 'separator-only menu JS ok');
}
# ---- Dispatch malformed JSON ----
{
my $cm = Chandra::ContextMenu->new;
eval { $cm->_dispatch('') };
ok(!$@, 'empty string dispatch no crash');
eval { $cm->_dispatch('{}') };
ok(!$@, 'empty object dispatch no crash');
eval { $cm->_dispatch('{"type":"unknown"}') };
ok(!$@, 'unknown type dispatch no crash');
}
# ---- Dispatch action with nonexistent ID ----
{
my $cm = Chandra::ContextMenu->new;
eval { $cm->_dispatch('{"type":"action","id":9999}') };
ok(!$@, 'nonexistent action ID no crash');
}
# ---- Multiple attach/detach cycles ----
{
my $cm = Chandra::ContextMenu->new;
for my $i (1..10) {
$cm->attach("#sel-$i");
}
my @att = $cm->attachments;
is(scalar @att, 10, '10 attachments');
for my $i (1..5) {
$cm->detach("#sel-$i");
}
@att = $cm->attachments;
is(scalar @att, 5, '5 remaining');
}
# ---- Items without action (display-only) ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'Info', disabled => 1 },
],
);
$cm->attach('#x');
my $js = $cm->js_code;
like($js, qr/dis:1/, 'disabled no-action item in JS');
}
# ---- Action error handling ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'Boom', action => sub { die "boom" } },
],
);
$cm->attach('#x');
my $id = $cm->items->[0]{_id};
my $warned = '';
local $SIG{__WARN__} = sub { $warned = $_[0] };
eval { $cm->_dispatch('{"type":"action","id":' . $id . '}') };
like($warned, qr/boom/, 'action error warned');
}
# ---- Global with dynamic callback ----
{
my $cm = Chandra::ContextMenu->new;
my $cb_called = 0;
$cm->attach_global(sub { $cb_called++; return [{ label => 'Dyn' }] });
# Can't fully test without app, just verify no crash
ok(1, 'attach_global with dynamic callback ok');
$cm->detach_global;
ok(1, 'detach_global after dynamic ok');
}
# ---- js_code with all features ----
{
my $cm = Chandra::ContextMenu->new(
items => [
{ label => 'Edit', icon => "\x{270f}", shortcut => 'Ctrl+E',
action => sub {} },
{ separator => 1 },
{ label => 'Toggle', checkable => 1, checked => 0,
action => sub {} },
{ label => 'More', submenu => [
{ label => 'Sub1', action => sub {} },
]},
{ label => 'Disabled', disabled => 1, action => sub {} },
PUSHMARK(SP);
XPUSHs(*dd_svp);
PUTBACK;
call_method("inject", G_DISCARD);
FREETMPS; LEAVE;
}
}
}
}
/* ContextMenu */
{
SV **cm_svp = hv_fetchs(hv, "_contextmenu", 0);
if (cm_svp && SvOK(*cm_svp) && SvROK(*cm_svp)) {
HV *cm_hv = (HV *)SvRV(*cm_svp);
SV **att_svp = hv_fetchs(cm_hv, "_attachments", 0);
SV **gl_svp = hv_fetchs(cm_hv, "_global", 0);
int has_attach = (att_svp && SvROK(*att_svp)
&& HvUSEDKEYS((HV *)SvRV(*att_svp)) > 0);
int is_global = (gl_svp && SvTRUE(*gl_svp));
if (has_attach || is_global) {
XPUSHs(callback);
PUTBACK;
call_method("add_drop_zone", G_DISCARD);
FREETMPS; LEAVE;
RETVAL = SvREFCNT_inc(self);
}
OUTPUT:
RETVAL
# ---- context_menu_instance() â lazy Chandra::ContextMenu accessor ----
SV *
context_menu_instance(self)
SV *self
CODE:
{
HV *hv = (HV *)SvRV(self);
SV **cached_svp = hv_fetchs(hv, "_contextmenu", 0);
if (cached_svp && SvOK(*cached_svp)) {
RETVAL = SvREFCNT_inc(*cached_svp);
} else {
dSP;
int count;
load_module(PERL_LOADMOD_NOIMPORT,
newSVpvs("Chandra::ContextMenu"), NULL);
ENTER; SAVETMPS;
PUSHMARK(SP);
XPUSHs(sv_2mortal(newSVpvs("Chandra::ContextMenu")));
XPUSHs(sv_2mortal(newSVpvs("app")));
XPUSHs(self);
PUTBACK;
count = call_method("new", G_SCALAR);
SPAGAIN;
RETVAL = (count > 0) ? SvREFCNT_inc(POPs) : &PL_sv_undef;
PUTBACK; FREETMPS; LEAVE;
(void)hv_stores(hv, "_contextmenu", SvREFCNT_inc(RETVAL));
}
xs/contextmenu.xs view on Meta::CPAN
MODULE = Chandra PACKAGE = Chandra::ContextMenu
PROTOTYPES: DISABLE
SV *
new(class, ...)
const char *class
CODE:
{
HV *self_hv = newHV();
AV *items_av = newAV();
xs/contextmenu.xs view on Meta::CPAN
SV **att_svp = hv_fetchs(hv, "_attachments", 0);
SV **items_svp = hv_fetchs(hv, "_items", 0);
SV **global_svp = hv_fetchs(hv, "_global", 0);
HV *att_hv = (HV *)SvRV(*att_svp);
AV *items_av = (AV *)SvRV(*items_svp);
int is_global = (global_svp && SvTRUE(*global_svp));
SV *js;
js = newSVpvs(
"(function(){\n"
"if(window.__chandraCtxMenu)return;\n"
"window.__chandraCtxMenu=1;\n"
"var sels=["
);
/* Emit selectors */
{
HE *entry;
int first = 1;
hv_iterinit(att_hv);
while ((entry = hv_iternext(att_hv)) != NULL) {
I32 klen;
xs/contextmenu.xs view on Meta::CPAN
".chandra-ctx-menu{background:#2d2d2d;border-color:#555;color:#e0e0e0;}"
".chandra-ctx-item:hover{background:#404040;}"
".chandra-ctx-sep{background:#555;}"
".chandra-ctx-sc,.chandra-ctx-sub-arrow{color:#888;}"
".chandra-ctx-item.disabled{color:#666;}"
"}"
"';\n"
"document.head.appendChild(style);\n"
);
/* Menu rendering and event handling JS */
sv_catpvs(js,
"var openMenus=[];\n"
"function closeAll(){"
"openMenus.forEach(function(m){m.remove()});"
"openMenus=[];"
"}\n"
"function buildMenu(items,x,y,depth){"
"var m=document.createElement('div');"
"m.className='chandra-ctx-menu';"
"items.forEach(function(it){"
"if(it.sep){"
"var s=document.createElement('div');"
"s.className='chandra-ctx-sep';"
"m.appendChild(s);return;"
"}"
"var row=document.createElement('div');"
xs/contextmenu.xs view on Meta::CPAN
"sh.textContent=it.sc;"
"row.appendChild(sh);"
"}"
/* Submenu arrow + hover */
"if(it.sub){"
"var ar=document.createElement('span');"
"ar.className='chandra-ctx-sub-arrow';"
"ar.textContent='\\u25b6';"
"row.appendChild(ar);"
"var subTimer=null,subMenu=null;"
"row.addEventListener('mouseenter',function(){"
"clearTimeout(subTimer);"
"if(!subMenu){"
"subTimer=setTimeout(function(){"
"var r=row.getBoundingClientRect();"
"subMenu=buildMenu(it.sub,r.right,r.top,(depth||0)+1);"
"subMenu.addEventListener('mouseleave',function(ev){"
"if(ev.relatedTarget&&row.contains(ev.relatedTarget))return;"
"subMenu.remove();"
"var idx=openMenus.indexOf(subMenu);"
"if(idx>=0)openMenus.splice(idx,1);"
"subMenu=null;"
"});"
"},150);"
"}"
"});"
"row.addEventListener('mouseleave',function(ev){"
"if(ev.relatedTarget&&subMenu&&subMenu.contains(ev.relatedTarget))return;"
"clearTimeout(subTimer);"
"if(subMenu){subMenu.remove();"
"var idx=openMenus.indexOf(subMenu);"
"if(idx>=0)openMenus.splice(idx,1);"
"subMenu=null;}"
"});"
"}"
/* Click handler */
"if(it.id!=null&&!it.dis){"
"row.addEventListener('click',function(e){"
"e.stopPropagation();"
"var payload=JSON.stringify({type:'action',id:it.id"
",chk:it.chk?!it.ckd:undefined});"
"if(it.chk){it.ckd=!it.ckd;"
"var cm=row.querySelector('.chandra-ctx-check');"
"if(cm)cm.textContent=it.ckd?'\\u2713':'';}"
"closeAll();"
"window.chandra.invoke('__chandraContextMenuBridge',[payload]);"
"});"
"}"
"m.appendChild(row);"
"});\n"
"m.style.left=x+'px';m.style.top=y+'px';"
"document.body.appendChild(m);"
"openMenus.push(m);"
/* Reposition if off-screen */
"var br=m.getBoundingClientRect();"
"if(br.right>window.innerWidth)m.style.left=(x-br.width)+'px';"
"if(br.bottom>window.innerHeight)m.style.top=(y-br.height)+'px';"
"requestAnimationFrame(function(){m.classList.add('show');});"
"return m;"
"}\n"
xs/contextmenu.xs view on Meta::CPAN
"e.preventDefault();"
"closeAll();"
/* Send dynamic request or show static menu */
"var tdata={id:t.id||'',class:t.className||'',tag:t.tagName||''"
",sel:matched?sels[i]:''};\n"
/* Check if we need dynamic items */
"var payload=JSON.stringify({type:'contextmenu'"
",x:e.clientX,y:e.clientY,target:tdata});\n"
"window.chandra.invoke('__chandraContextMenuBridge',[payload]);\n"
"});\n"
/* Expose show function for dynamic response */
"window.__chandraCtxShow=function(x,y,dynItems){"
"closeAll();"
"buildMenu(dynItems||items,x,y,0);"
"};\n"
"})();\n"
);
RETVAL = js;
}
OUTPUT:
RETVAL
xs/contextmenu.xs view on Meta::CPAN
{
dSP;
ENTER; SAVETMPS;
PUSHMARK(SP);
if (chk_svp && SvOK(*chk_svp))
XPUSHs(*chk_svp);
PUTBACK;
call_sv(*cb_svp, G_DISCARD | G_EVAL);
if (SvTRUE(ERRSV))
warn("ContextMenu action error: %s", SvPV_nolen(ERRSV));
FREETMPS; LEAVE;
}
}
}
}
/* Context menu request â dynamic items or show static */
else if (strEQ(type, "contextmenu")) {
SV **x_svp = hv_fetchs(event_hv, "x", 0);
SV **y_svp = hv_fetchs(event_hv, "y", 0);
SV **target_svp = hv_fetchs(event_hv, "target", 0);
xs/contextmenu.xs view on Meta::CPAN
dSP;
int count;
ENTER; SAVETMPS;
PUSHMARK(SP);
if (target_svp && SvOK(*target_svp))
XPUSHs(*target_svp);
PUTBACK;
count = call_sv(dynamic_cb, G_SCALAR | G_EVAL);
SPAGAIN;
if (SvTRUE(ERRSV)) {
warn("ContextMenu dynamic callback error: %s", SvPV_nolen(ERRSV));
FREETMPS; LEAVE;
SvREFCNT_dec(event_sv);
return;
}
dyn_items = (count > 0) ? newSVsv(POPs) : NULL;
PUTBACK;
FREETMPS; LEAVE;
}
if (dyn_items && SvROK(dyn_items) && SvTYPE(SvRV(dyn_items)) == SVt_PVAV) {
xs/contextmenu.xs view on Meta::CPAN
SV **injected_svp;
injected_svp = hv_fetchs(hv, "_injected", 0);
if (injected_svp && SvTRUE(*injected_svp)) {
RETVAL = SvREFCNT_inc(self);
} else {
SV *js;
(void)hv_stores(hv, "_injected", newSViv(1));
/* Bind __chandraContextMenuBridge dispatch if not already done */
{
SV **db_svp = hv_fetchs(hv, "_dispatch_bound", 0);
if (!db_svp || !SvTRUE(*db_svp)) {
SV **app_svp = hv_fetchs(hv, "app", 0);
if (app_svp && SvOK(*app_svp)) {
SV *cm_self_ref = newSVsv(self);
CV *wrapper_cv;
sv_rvweaken(cm_self_ref);
wrapper_cv = newXS(NULL, XS_Chandra__ContextMenu__dispatch_trampoline, __FILE__);
CvXSUBANY(wrapper_cv).any_ptr = (void *)cm_self_ref;
{
dSP;
ENTER; SAVETMPS;
PUSHMARK(SP);
XPUSHs(*app_svp);
XPUSHs(sv_2mortal(newSVpvs("__chandraContextMenuBridge")));
XPUSHs(sv_2mortal(newRV_noinc((SV *)wrapper_cv)));
PUTBACK;
call_method("bind", G_DISCARD);
FREETMPS; LEAVE;
}
(void)hv_stores(hv, "_dispatch_bound", newSViv(1));
}
}
}