Files
NotAlexNoyleandClaude Opus 5.5 554ed514dc T-505: the cause proven, the card measured; the card's frame wrapped without alpha
The owner's sequence: a Codex adversarial review (context/impl/impl-review-findings-t505-codex.md), then
"prove the cause first": JIHAD_GPU_NO_PUBLISH=1 (a diagnostic: frames made and read, never handed to the
card) shows the same daemon work making desktop Wikipedia 32 fps and the long page 53 while LunaSysMgr draws
nothing, 8.6 / 13.6 while the card moves: the card's redraw on the shared GPU and memory makes every daemon
stage ~3x slower. Then the card's paint measured (the adapter's [paint-stat] lines under its paint log): a
~10 ms software blit (Piranha's PSoftContext2D) of the whole view ~20 times a second, mostly repaints; a
PGSurface kept per frame changed nothing (not kept). The frame is now wrapped without alpha (every path
writes alpha 255): the blit 9 -> 5 ms, LunaSysMgr's card instance -15 % CPU while panning, the picture the
same; <state>/alphawrap reverts. The pan rate is unchanged; the owner's plan is the CPU/GPU split next.
tools/host/express-jobs.py also reads scripted pans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XtfxSpM1w2uiVKeLKoV3g3
2026-10-26 11:52:33 -04:00

6853 lines
248 KiB
C++

/* @@@LICENSE
*
* Copyright (c) 2012 Hewlett-Packard Development Company, L.P.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
LICENSE@@@ */
/* Modified by NotAlexNoyle (2026), per Apache-2.0 section 4(b):
* - MIME `application/x-jihad-browser` + YAP server name `jihad-browser` so this
* adapter (BrowserAdapterJihad.so) coexists with the stock browser.
* - handlePaint() replaced with a dstBuffer raw-blit path (+ rotation guard,
* pinch-zoom blit, scroll indicator) and single-tap -> asyncCmdClickAt dispatch,
* ported from the Atlas Browser project (Herrie82, Apache-2.0). See ../../NOTICE. */
#include <sys/time.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <math.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <regex.h>
#include <unistd.h>
#include <libgen.h>
#include <assert.h>
#include <memory>
#include <string.h>
#include <glib.h>
#include <syslog.h>
#include <sys/types.h>
#include <pwd.h>
#include "BrowserAdapter.h"
#include "BrowserAdapterManager.h"
#include "BrowserCenteredZoom.h"
#include "BrowserMetaViewport.h"
#include "BrowserOffscreen.h"
#include "PGContext.h" // webOS Piranha graphics context (rotation-aware composite; see NOTICE)
#include "PGSurface.h" // webOS Piranha surface (zero-copy wrap of our offscreen raster)
#include "BrowserRect.h"
#include <BufferLock.h>
#include "Debug.h"
#include <BrowserClientBase.h>
#include "UrlInfo.h"
#include "InteractiveInfo.h"
#include "ImageInfo.h"
#include "ElementInfo.h"
#include "JsonNPObject.h"
#include "NPObjectEvent.h"
#include <pbnjson.hpp>
#include <dlfcn.h> // dladdr: locate this .so, hence the app dir (jihadAppFile)
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
// ---- BUILD-TIME VARIANT IDENTITY (Jihad) --------------------------------------------
// The Enyo, Mochi, and Mojo packages are three fully independent browsers (cavekit-device-build.md
// R7), so each needs its own MIME type and its own YAP service name — otherwise a Mochi card gets
// served by the Enyo daemon, and removing one package breaks the other. Both values come in on the
// compiler command line (build/webos-oe/build-adapter-{pdk,arm}.sh) instead of forking this file
// three ways; context/plans/plan-variant-identity.md is the authoritative name table.
// JIHAD_VARIANT the variant token: "enyo" | "mochi" | "mojo". Matches the token the daemon
// derives at RUNTIME from $JIHAD_BS_NAME in render/goanna/JihadRuntimePaths.h —
// this adapter runs inside LunaSysMgr, not the daemon, so it has no such
// environment to read and takes the token at compile time instead. Both halves
// land on the same /var/palm/jihad/<variant>.
// JIHAD_MIME the BARE NPAPI MIME type — the same macro and the same value the shim is built
// with (BrowserAdapterShim.cpp). AdapterGetMIMEDescription appends the "::;"
// extension/description fields, so this string stays a plain type everywhere.
// JIHAD_YAP_NAME the BrowserClientBase service name -> /tmp/yapserver.<name>. ONLY the name is
// per-variant: the YAP command/message wire format is untouched and byte-identical,
// exactly as stock "browser" and "browsermojo" already differ by name alone.
// Defaults are the Enyo values — the one deployment known to work on-device keeps the unsuffixed
// names it already has. All-or-nothing, because a build that sets JIHAD_MIME but forgets
// JIHAD_YAP_NAME registers as Mochi and then connects to the Enyo daemon: a cross-variant hijack
// that looks perfectly healthy in the logs. That has to be a compile error, not a field report.
#if defined(JIHAD_VARIANT) || defined(JIHAD_MIME) || defined(JIHAD_YAP_NAME) || defined(JIHAD_STATE_DIR)
# if !defined(JIHAD_VARIANT) || !defined(JIHAD_MIME) || !defined(JIHAD_YAP_NAME)
# error "Jihad variant build is inconsistent: define JIHAD_VARIANT, JIHAD_MIME and JIHAD_YAP_NAME together (or none of them, for the enyo default). See context/plans/plan-variant-identity.md."
# endif
#else
# define JIHAD_VARIANT "enyo"
# define JIHAD_MIME "application/x-jihad-browser"
# define JIHAD_YAP_NAME "jihad-browser"
#endif
// Derived from the token by literal concatenation, so the state dir can never disagree with the
// identity (same construction as BrowserAdapterShim.cpp). Overridable only deliberately.
#ifndef JIHAD_STATE_DIR
# define JIHAD_STATE_DIR "/var/palm/jihad/" JIHAD_VARIANT
#endif
// what user does browserserver run as?
#define BROWSERVER_USER "luna"
/**
* Define this value to write out each update rectangle to a separate PNG file.
*/
#undef DEBUG_PAINTS
/**
* Define this value to show the flash rects
*/
#undef DEBUG_FLASH_RECTS
/** Define this to get more verbose logging. */
#undef VERBOSE_MESSAGESF
#ifdef VERBOSE_MESSAGES
#define VERBOSE_TRACE(...) TRACE(__VA_ARGS__)
#else
#define VERBOSE_TRACE(...) (void)0
#endif
const int ESC_KEY = 27;
// JIHAD: resolve a file that ships inside THIS app, next to the adapter's own .so.
//
// The adapter is an NPAPI plugin loaded by the card out of the app directory, so dladdr on
// any symbol in this library names that directory — no env var to set, no path to keep in
// sync with packaging, and correct for all three UI variants, which install to three
// different app ids. Returns an empty string if the file is not there, so callers can fall
// back to whatever the stock path was.
static QString jihadAppFile(const char* relPath)
{
Dl_info info;
// The address must be of something in THIS library; a static function is enough.
if (!dladdr(reinterpret_cast<void*>(&jihadAppFile), &info) || !info.dli_fname) {
return QString();
}
QString dir = QFileInfo(QString::fromUtf8(info.dli_fname)).absolutePath();
QString path = dir + QLatin1Char('/') + QString::fromUtf8(relPath);
return QFile::exists(path) ? path : QString();
}
static const float kFrozenSurfaceScale = 0.5f;
static const int kInvalidParam = -1;
static const double kDoubleEqualityTolerance = 0.00001;
static const double kMaxZoom = 4.0;
static const double kOverZoomFactor = 1.2;
static const int32_t kVirtualPageWidth= 960;
static const int32_t kVirtualPageHeight= 1400;
static const int32_t kDefaultLayoutWidth = kVirtualPageWidth;
static const int kHysteresis = 4;
static const int kMouseHoldInterval = 350;
static const int kDoubleClickInterval = 400;
static const int kTimerInterval = 16;
static const int kZoomAnimationSteps = 5;
static const int kNumRecordedGestures = 5;
static const int s_recordedGestureAvgWeights[] = { 1, 2, 4, 8, 16 };
static NPIdentifier gLoadProgressHandler = NULL;
static NPIdentifier gTitleURLChangeHandler = NULL;
static NPIdentifier gTitleChangeHandler = NULL;
static NPIdentifier gUrlChangeHandler = NULL;
static NPIdentifier gLinkClickedHandler = NULL;
static NPIdentifier gActionDataHandler = NULL;
static NPIdentifier gPageDimensionsHandler = NULL;
static NPIdentifier gCreatePageHandler = NULL;
static NPIdentifier gDownloadFinishedHandler = NULL;
static NPIdentifier gMimeHandoffUrlHandler = NULL;
static NPIdentifier gMimeNotSupportedHandler = NULL;
static NPIdentifier gDownloadErrorHandler = NULL;
static NPIdentifier gDownloadProgressHandler = NULL;
static NPIdentifier gDownloadStartHandler = NULL;
static NPIdentifier gDialogUserPasswordHandler = NULL;
static NPIdentifier gDialogAlertHandler = NULL;
static NPIdentifier gDialogConfirmHandler = NULL;
static NPIdentifier gDialogConfirmSSLHandler = NULL;
static NPIdentifier gDialogPromptHandler = NULL;
static NPIdentifier gLoadStartedHandler = NULL;
static NPIdentifier gLoadStoppedHandler = NULL;
static NPIdentifier gScrollToHandler = NULL;
static NPIdentifier gScrolledToHandler = NULL;
static NPIdentifier gClickRejectedHandler = NULL;
static NPIdentifier gPopupMenuShowHandler = NULL;
static NPIdentifier gPopupMenuHideHandler = NULL;
static NPIdentifier gSmartZoomCalculateResponseSimpleHandler = NULL;
static NPIdentifier gAdapterInitializedHandler = NULL;
static NPIdentifier gFailedLoadHandler = NULL;
static NPIdentifier gEditorFocusedHandler = NULL;
static NPIdentifier gReportErrorHandler = NULL;
static NPIdentifier gDidFinishDocumentLoadHandler = NULL;
static NPIdentifier gUpdateGlobalHistoryHandler = NULL;
static NPIdentifier gMainDocumentErrorHandler = NULL;
static NPIdentifier gBrowserServerDisconnectHandler = NULL;
static NPIdentifier gFirstPaintCompleteHandler = NULL;
static NPIdentifier gUrlRedirectedHandler = NULL;
static NPIdentifier gMetaViewportSetHandler = NULL;
static NPIdentifier gEval = NULL;
static NPIdentifier gPluginSpotlightCreate = NULL;
static NPIdentifier gPluginSpotlightRemove = NULL;
static NPIdentifier gMsgSpellingWidgetVisibleRectUpdate = NULL ;
static NPIdentifier gEventFiredHandler = NULL;
static NPIdentifier gMouseInFlashChangeHandler = NULL;
static NPIdentifier gFlashGestureLockHandler = NULL;
static NPIdentifier gMouseInInteractiveChangeHandler = NULL;
static NPIdentifier gShowPrintDialogHandler = NULL;
static NPIdentifier gServerConnectedHandler = NULL;
static char gDialogResponsePipe[50];
const char* kIconMaskFile = "/usr/lib/BrowserPlugins/BrowserAdapterData/launcher-bookmark-alpha.png";
const char* kIconOverlayFile = "/usr/lib/BrowserPlugins/BrowserAdapterData/launcher-bookmark-overlay.png";
const char* kSelectionReticleFile = "/usr/lib/BrowserPlugins/BrowserAdapterData/shift-tap-reticle.png";
//images for drop shadow
const char* kImgBotFile = "/usr/lib/BrowserPlugins/BrowserAdapterData/fl-win-shdw-bot.png";
const char* kImgBotLeftFile = "/usr/lib/BrowserPlugins/BrowserAdapterData/fl-win-shdw-bot-left-corner.png";
const char* kImgBotRightFile = "/usr/lib/BrowserPlugins/BrowserAdapterData/fl-win-shdw-bot-right-corner.png";
const char* kImgLeftFile= "/usr/lib/BrowserPlugins/BrowserAdapterData/fl-win-shdw-left.png";
const char* kImgRightFile= "/usr/lib/BrowserPlugins/BrowserAdapterData/fl-win-shdw-right.png";
const char* kImgTopFile= "/usr/lib/BrowserPlugins/BrowserAdapterData/fl-win-shdw-top.png";
const char* kImgTopRightFile= "/usr/lib/BrowserPlugins/BrowserAdapterData/fl-win-shdw-top-right-corner.png";
const char* kImgTopLeftFile= "/usr/lib/BrowserPlugins/BrowserAdapterData/fl-win-shdw-top-left-corner.png";
// -----------------------------------------------------------------------------------
// Mandatory Plugin Stub Implementations
// -----------------------------------------------------------------------------------
AdapterBase* AdapterCreate(NPP instance, GMainLoop* mainLoop, int16_t argc, char* argn[], char* argv[])
{
return new BrowserAdapter(instance, g_main_loop_get_context(mainLoop), argc, argn, argv);
}
NPError AdapterLibInitialize(void)// Called when proxy library is loaded by browser, may be empty. Returns NPERR_NO_ERROR on success.
{
// Look up identifiers for JavaScript side event handler methods:
gActionDataHandler = AdapterBase::NPN_GetStringIdentifier("actionData");
gCreatePageHandler = AdapterBase::NPN_GetStringIdentifier("createPage");
gDialogAlertHandler = AdapterBase::NPN_GetStringIdentifier("dialogAlert");
gDialogConfirmHandler = AdapterBase::NPN_GetStringIdentifier("dialogConfirm");
gDialogConfirmSSLHandler = AdapterBase::NPN_GetStringIdentifier("dialogSSLConfirm");
gDialogUserPasswordHandler = AdapterBase::NPN_GetStringIdentifier("dialogUserPassword");
gDialogPromptHandler = AdapterBase::NPN_GetStringIdentifier("dialogPrompt");
gDownloadErrorHandler = AdapterBase::NPN_GetStringIdentifier("downloadError");
gDownloadProgressHandler = AdapterBase::NPN_GetStringIdentifier("downloadProgress");
gDownloadFinishedHandler = AdapterBase::NPN_GetStringIdentifier("downloadFinished");
gDownloadStartHandler = AdapterBase::NPN_GetStringIdentifier("downloadStart");
gLinkClickedHandler = AdapterBase::NPN_GetStringIdentifier("linkClicked");
gLoadProgressHandler = AdapterBase::NPN_GetStringIdentifier("loadProgress");
gLoadStartedHandler = AdapterBase::NPN_GetStringIdentifier("loadStarted");
gLoadStoppedHandler = AdapterBase::NPN_GetStringIdentifier("loadStopped");
gMimeHandoffUrlHandler = AdapterBase::NPN_GetStringIdentifier("mimeHandoffUrl");
gMimeNotSupportedHandler = AdapterBase::NPN_GetStringIdentifier("mimeNotSupported");
gPageDimensionsHandler = AdapterBase::NPN_GetStringIdentifier("pageDimensions");
gScrollToHandler = AdapterBase::NPN_GetStringIdentifier("scrollTo");
gScrolledToHandler = AdapterBase::NPN_GetStringIdentifier("scrolledTo");
gPopupMenuShowHandler = AdapterBase::NPN_GetStringIdentifier("showPopupMenu");
gPopupMenuHideHandler = AdapterBase::NPN_GetStringIdentifier("hidePopupMenu");
gTitleChangeHandler = AdapterBase::NPN_GetStringIdentifier("titleChange");
gTitleURLChangeHandler = AdapterBase::NPN_GetStringIdentifier("titleURLChange");
gUrlChangeHandler = AdapterBase::NPN_GetStringIdentifier("urlChange");
gClickRejectedHandler = AdapterBase::NPN_GetStringIdentifier("clickRejected");
gSmartZoomCalculateResponseSimpleHandler = AdapterBase::NPN_GetStringIdentifier("smartZoomCalculateResponseSimple");
gAdapterInitializedHandler = AdapterBase::NPN_GetStringIdentifier("adapterInitialized");
gFailedLoadHandler = AdapterBase::NPN_GetStringIdentifier("failedLoad");
gEditorFocusedHandler = AdapterBase::NPN_GetStringIdentifier("editorFocused");
gReportErrorHandler = AdapterBase::NPN_GetStringIdentifier("reportError");
gDidFinishDocumentLoadHandler = AdapterBase::NPN_GetStringIdentifier("didFinishDocumentLoad");
gUpdateGlobalHistoryHandler = AdapterBase::NPN_GetStringIdentifier("updateGlobalHistory");
gMainDocumentErrorHandler = AdapterBase::NPN_GetStringIdentifier("setMainDocumentError");
gBrowserServerDisconnectHandler = AdapterBase::NPN_GetStringIdentifier("browserServerDisconnected");
gFirstPaintCompleteHandler = AdapterBase::NPN_GetStringIdentifier("firstPaintComplete");
gUrlRedirectedHandler = AdapterBase::NPN_GetStringIdentifier("urlRedirected");
gMetaViewportSetHandler = AdapterBase::NPN_GetStringIdentifier("metaViewportSet");
gEval = AdapterBase::NPN_GetStringIdentifier("eval");
gPluginSpotlightCreate = AdapterBase::NPN_GetStringIdentifier("pluginSpotlightCreate");
gPluginSpotlightRemove = AdapterBase::NPN_GetStringIdentifier("pluginSpotlightRemove");
gMsgSpellingWidgetVisibleRectUpdate = AdapterBase::NPN_GetStringIdentifier("spellingWidgetRectUpdate");
gEventFiredHandler = AdapterBase::NPN_GetStringIdentifier("eventFired");
gMouseInFlashChangeHandler = AdapterBase::NPN_GetStringIdentifier("mouseInFlashChange");
gFlashGestureLockHandler = AdapterBase::NPN_GetStringIdentifier("flashGestureLockChange");
gMouseInInteractiveChangeHandler = AdapterBase::NPN_GetStringIdentifier("mouseInInteractiveChange");
gShowPrintDialogHandler = AdapterBase::NPN_GetStringIdentifier("showPrintDialog");
gServerConnectedHandler = AdapterBase::NPN_GetStringIdentifier("serverConnected");
return NPERR_NO_ERROR;
}
const char* AdapterGetMIMEDescription(void)
{
TRACE;
// Self-contained: own MIME so the front-end loads THIS adapter, not the stock one. The app's
// engine override swaps its WebView plugin type to the matching JIHAD_MIME (per variant).
// Stock browser (application/x-palm-browser -> /tmp/yapserver.browser) untouched.
// "<mime>:<extensions>:<description>" — no extensions or description, as before.
return JIHAD_MIME "::;";
}
uint32_t AdapterGetMethods(const char*** outNames, const JSMethodPtr **outMethods, NPIdentifier **outIDs)
{
static const char* names[]= {
"clearHistory",
"deleteImage",
"generateIconFromFile",
"getHistoryState",
"goBack",
"goForward",
"interrogateClicks",
"openURL",
"pageScaleAndScroll",
"reloadPage",
"resizeImage",
"saveViewToFile",
"setMinFontSize",
"setPageIdentifier",
"smartZoom",
"setViewportSize",
"stopLoad",
"setMagnification",
"clickAt",
"selectPopupMenuItem",
"sendDialogResponse",
"gestureStart",
"gestureChange",
"gestureEnd",
"scrollStarting",
"scrollEnding",
"setEnableJavaScript",
"setBlockPopups",
"setHeaderHeight",
"setAcceptCookies",
"setShowClickedLink",
"mouseEvent",
"connectBrowserServer",
"disconnectBrowserServer",
"inspectUrlAtPoint",
"addUrlRedirect",
"pageFocused",
"addElementHighlight",
"removeElementHighlight",
"isEditing",
"insertStringAtCursor",
"enableSelectionMode",
"disableSelectionMode",
"clearSelection",
"saveImageAtPoint",
"selectAll",
"cut",
"copy",
"paste",
"getImageInfoAtPoint",
"interactiveAtPoint",
"elementInfoAtPoint",
"setMouseMode",
"startDeferWindowChange",
"stopDeferWindowChange",
"setSpotlight",
"removeSpotlight",
"setHTML",
"disableEnhancedViewport",
"ignoreMetaTags",
"setNetworkInterface",
"enableFastScaling",
"setDefaultLayoutWidth",
"printFrame",
"skipPaintHack",
"findInPage",
"mouseHoldAt",
"handleFlick",
"setVisibleSize",
"setDNSServers"
};
const size_t kExposedMethodCount = G_N_ELEMENTS(names);
static const JSMethodPtr methods[kExposedMethodCount]= {
BrowserAdapter::js_clearHistory,
BrowserAdapter::js_deleteImage,
BrowserAdapter::js_generateIconFromFile,
BrowserAdapter::js_getHistoryState,
BrowserAdapter::js_goBack,
BrowserAdapter::js_goForward,
BrowserAdapter::js_interrogateClicks,
BrowserAdapter::js_openURL,
BrowserAdapter::js_pageScaleAndScroll,
BrowserAdapter::js_reloadPage,
BrowserAdapter::js_resizeImage,
BrowserAdapter::js_saveViewToFile,
BrowserAdapter::js_setMinFontSize,
BrowserAdapter::js_setPageIdentifier,
BrowserAdapter::js_smartZoom,
BrowserAdapter::js_setViewportSize,
BrowserAdapter::js_stopLoad,
BrowserAdapter::js_setMagnification,
BrowserAdapter::js_clickAt,
BrowserAdapter::js_selectPopupMenuItem,
BrowserAdapter::js_sendDialogResponse,
BrowserAdapter::js_gestureStart,
BrowserAdapter::js_gestureChange,
BrowserAdapter::js_gestureEnd,
BrowserAdapter::js_scrollStarting,
BrowserAdapter::js_scrollEnding,
BrowserAdapter::js_setEnableJavaScript,
BrowserAdapter::js_setBlockPopups,
BrowserAdapter::js_setHeaderHeight,
BrowserAdapter::js_setAcceptCookies,
BrowserAdapter::js_setShowClickedLink,
BrowserAdapter::js_mouseEvent,
BrowserAdapter::js_connectBrowserServer,
BrowserAdapter::js_disconnectBrowserServer,
BrowserAdapter::js_inspectUrlAtPoint,
BrowserAdapter::js_addUrlRedirect,
BrowserAdapter::js_pageFocused,
BrowserAdapter::js_addElementHighlight,
BrowserAdapter::js_removeElementHighlight,
BrowserAdapter::js_isEditing,
BrowserAdapter::js_insertStringAtCursor,
BrowserAdapter::js_enableSelectionMode,
BrowserAdapter::js_disableSelectionMode,
BrowserAdapter::js_clearSelection,
BrowserAdapter::js_saveImageAtPoint,
BrowserAdapter::js_selectAll,
BrowserAdapter::js_cut,
BrowserAdapter::js_copy,
BrowserAdapter::js_paste,
BrowserAdapter::js_getImageInfoAtPoint,
BrowserAdapter::js_interactiveAtPoint,
BrowserAdapter::js_getElementInfoAtPoint,
BrowserAdapter::js_setMouseMode,
BrowserAdapter::js_startDeferWindowChange,
BrowserAdapter::js_stopDeferWindowChange,
BrowserAdapter::js_setSpotlight,
BrowserAdapter::js_removeSpotlight,
BrowserAdapter::js_setHTML,
BrowserAdapter::js_disableEnhancedViewport,
BrowserAdapter::js_ignoreMetaTags,
BrowserAdapter::js_setNetworkInterface,
BrowserAdapter::js_enableFastScaling,
BrowserAdapter::js_setDefaultLayoutWidth,
BrowserAdapter::js_printFrame,
BrowserAdapter::js_skipPaintHack,
BrowserAdapter::js_findInPage,
BrowserAdapter::js_mouseHoldAt,
BrowserAdapter::js_handleFlick,
BrowserAdapter::js_setVisibleSize,
BrowserAdapter::js_setDNSServers
};
static NPIdentifier ids[kExposedMethodCount] = {NULL};
*outNames = names;
*outMethods = methods;
*outIDs = ids;
return kExposedMethodCount;
}
// -----------------------------------------------------------------------------------
// BrowserAdapter Implementation
// -----------------------------------------------------------------------------------
static inline bool PrvIsEqual(double a, double b)
{
return (::fabs(a - b) < kDoubleEqualityTolerance);
}
// DOM touch forwarding is DEFAULT OFF and opted into with JIHAD_TOUCH_EVENTS=1 in the
// environment LunaSysMgr starts with. Env rather than a plugin param or a pref because the
// adapter already reads one switch that way (LOG_BROWSER_ALERTS) and because the thing being
// switched has to be decidable in the constructor, before any card JS has run. Only the exact
// string "1" enables it — "0"/"false"/empty must not turn a risky path on by accident.
static bool jihadTouchEventsRequested()
{
const char* env = ::getenv("JIHAD_TOUCH_EVENTS");
return env != NULL && ::strcmp(env, "1") == 0;
}
/**
* Constructor. The
*/
BrowserAdapter::BrowserAdapter(NPP instance, GMainContext *ctxt, int16_t argc, char* argn[], char* argv[])
: BrowserClientBase(JIHAD_YAP_NAME, ctxt) // -> /tmp/yapserver.<JIHAD_YAP_NAME>: this variant's OWN
// daemon (stock "browser" untouched, and no other Jihad
// variant's socket touched either — R7)
, AdapterBase(instance, true, true) // useGraphicsContext=true: WebKit hands a transform-aware
// Piranha PGContext (carries the card's rotation) — composite
// our offscreen THROUGH it so portrait<->landscape rotation is
// correct. Raw dstBuffer blit (below) is the no-context fallback.
// Rotation-aware path ported from Atlas Browser; see NOTICE.
, mScroller(0)
, mDirtyPattern(0)
, m_pageOffset(0, 0)
, m_touchPtDoc(0, 0)
, mViewportWidth(0)
, mViewportHeight(0)
, mZoomLevel(1.0)
, mZoomFit(true)
, mPageWidth(0)
, mPageHeight(0)
, mContentWidth(0)
, mContentHeight(0)
, mScrollPos(0, 0)
, mInGestureChange(false)
, mMetaViewport(0)
, mCenteredZoom(0)
, mOffscreen0(0)
, mOffscreen1(0)
, mOffscreen2(0)
, mOffscreenCurrent(0)
, mJihadLastPaintBuf(0)
, mJihadLastPaintSeq(0)
, mFrozenSurface(0)
, mFrozen(false)
, mFrozenRenderPos(0, 0)
, m_passInputEvents(false)
, mTouchEventsEnabled(jihadTouchEventsRequested())
, mFirstPaintComplete(false)
, mEnableJavaScript(true)
, mBlockPopups(true)
, mAcceptCookies(true)
, mShowClickedLink(true)
, m_defaultLayoutWidth(kDefaultLayoutWidth)
, mBrowserServerConnected(false)
, mNotifiedOfBrowserServerDisconnect(false)
, mSendFinishDocumentLoadNotification(false)
, mArgc(argc)
, mArgn(NULL)
, mArgv(NULL)
, mPaintEnabled(true)
, mInScroll(false)
, bEditorFocused(false)
, m_useFastScaling(false)
, mPageIdentifier(-1)
, mBsQueryNum(0)
, m_interrogateClicks(false)
, mMouseMode(0)
, mLogAlertMessages(getenv("LOG_BROWSER_ALERTS") != NULL)
, mPageFocused(true)
, mServerConnectedInvoked(false)
, mServerConnectedInvoking(false)
, mShowHighlight(false)
, mLastPointSentToFlash(-1, -1)
, mMouseInFlashRect(false)
, mFlashGestureLock(false)
, mMouseInInteractiveRect(false)
, m_spotlightHandle(0)
, m_ft(0)
, m_clickPt(0, 0)
, m_penDownDoc(0, 0)
, m_dragging(false)
, m_didDrag(false)
, m_didHold(false)
, m_sentToBS(false)
, m_clickTimerSource(0)
, m_mouseHoldTimerSource(0)
, m_gestureGen(0)
, m_zoomTimerSource(0)
, m_zoomPt(0,0)
, m_zoomTarget(0.0)
, m_zoomXInterval(0)
, m_zoomYInterval(0)
, m_zoomLevelInterval(0)
, m_headerHeight(0)
, mJihadPaintScrollX(-1)
, mJihadPaintScrollY(-1)
, mJihadPaintHeader(-1)
, mScrollbarOpacity(0)
, mScrollbarFadeSource(0)
, m_bufferLock(0)
, m_bufferLockName(0)
{
//openlog("browser-adapter", 0, LOG_USER);
g_message("%s: %p", __PRETTY_FUNCTION__, this);
TRACE;
mScroller = new KineticScroller(ctxt);
mScroller->setListener(this);
memset(gDialogResponsePipe, 0, G_N_ELEMENTS(gDialogResponsePipe));
resetScrollableLayerScrollSession();
// Duplicate the args for lazy connection to the browser server.
mArgn = new char*[argc];
mArgv = new char*[argc];
for (int16_t i = 0; i < argc; i++) {
mArgn[i] = strdup(argn[i]);
mArgv[i] = strdup(argv[i]);
}
/* Parse arguments */
for (int i = 0; i < mArgc; i++) {
if (0 == strcasecmp(mArgn[i], "usemouseevents")) {
m_passInputEvents = (strcasecmp(mArgv[i], "true") == 0);
} else if (0 == strcasecmp(mArgn[i], "viewportwidth")) {
mViewportWidth = atoi(mArgv[i]);
} else if (0 == strcasecmp(mArgn[i], "viewportheight")) {
mViewportHeight = atoi(mArgv[i]);
}
}
if ((mViewportWidth == 0) || (mViewportHeight == 0)) {
syslog(LOG_DEBUG, "viewport dimensions are unset");
setDefaultViewportSize();
}
// Freeze at startup
freeze();
// selection reticle setup
initSelectionReticleSurface();
BrowserAdapterManager::instance()->registerAdapter(this);
TRACEF("pass events %d viewport %dx%d", m_passInputEvents,
mViewportWidth, mViewportHeight);
// Touch delivery is opt-in on this platform: WebKit sends no npPalmTouch*Event to a plugin
// that has not set npPalmEnableTouchEvents to a non-NULL value (AdapterBase.h, above
// handleTouchStart). Without this call doTouchEvent below is unreachable no matter what
// shouldPassTouchEvents() says. Asked for only when the switch is on, so a default build
// does not even ask the host to generate the events.
if (mTouchEventsEnabled) {
NPError touchErr = NPN_SetValue((NPPVariable) npPalmEnableTouchEvents, (void*) this);
syslog(LOG_WARNING, "JIHAD_TOUCH_EVENTS=1: DOM touch forwarding ENABLED "
"(npPalmEnableTouchEvents rc=%d) — pen events are suppressed per "
"gesture by the daemon when the page consumes touchstart", (int) touchErr);
}
// Only at the end shall we inform our listener that we're initialized.
InvokeEventListener(gAdapterInitializedHandler, NULL, 0, NULL);
}
BrowserAdapter::~BrowserAdapter()
{
TRACE;
g_message("%s: %p", __PRETTY_FUNCTION__, this);
destroyBufferLock();
stopClickTimer();
stopMouseHoldTimer();
stopZoomAnimation();
delete mScroller;
delete mMetaViewport;
delete mCenteredZoom;
for (int16_t i = 0; i < mArgc; i++) {
free(mArgn[i]);
free(mArgv[i]);
}
delete [] mArgn;
delete [] mArgv;
delete mDirtyPattern;
mDirtyPattern = 0;
delete mFrozenSurface;
mFrozenSurface = 0;
delete mOffscreen0;
delete mOffscreen1;
delete mOffscreen2;
std::list<UrlRedirectInfo*>::iterator i;
for (i = m_urlRedirects.begin(); i != m_urlRedirects.end(); ++i) {
delete *i;
}
if (mSelectionReticle.timeoutSource != NULL) {
g_source_destroy(mSelectionReticle.timeoutSource);
g_source_unref(mSelectionReticle.timeoutSource);
mSelectionReticle.timeoutSource = NULL;
}
delete mSelectionReticle.surface;
mSelectionReticle.surface = NULL;
BrowserAdapterManager::instance()->unregisterAdapter(this);
stopFadeScrollbar();
closelog();
}
/**
* Is the directory that this file will be written to safe?
*/
bool BrowserAdapter::isSafeDir(const char* pszFileName)
{
return strncasecmp("/var/", pszFileName, 5) == 0 || strncasecmp("/tmp/", pszFileName, 5) == 0;
}
BrowserAdapter::InspectUrlAtPointArgs::InspectUrlAtPointArgs(int x, int y, NPObject* cb, int queryNum) :
BrowserServerCallArgs(queryNum), m_x(x), m_y(y), m_successCb(cb) {
if (m_successCb) {
AdapterBase::NPN_RetainObject(m_successCb);
}
}
BrowserAdapter::InspectUrlAtPointArgs::~InspectUrlAtPointArgs ()
{
if (m_successCb) {
AdapterBase::NPN_ReleaseObject(m_successCb);
}
}
BrowserAdapter::SaveImageAtPointArgs::SaveImageAtPointArgs(int x, int y, NPObject* cb, int queryNum) :
BrowserServerCallArgs(queryNum), m_x(x), m_y(y), m_callback(cb) {
if (m_callback) {
AdapterBase::NPN_RetainObject(m_callback);
}
}
BrowserAdapter::SaveImageAtPointArgs::~SaveImageAtPointArgs ()
{
if (m_callback) {
AdapterBase::NPN_ReleaseObject(m_callback);
}
}
BrowserAdapter::GetImageInfoAtPointArgs::GetImageInfoAtPointArgs(int x, int y, NPObject* cb, int queryNum) :
BrowserServerCallArgs(queryNum), m_x(x), m_y(y), m_callback(cb) {
if (m_callback) {
AdapterBase::NPN_RetainObject(m_callback);
}
}
BrowserAdapter::GetImageInfoAtPointArgs::~GetImageInfoAtPointArgs ()
{
if (m_callback) {
AdapterBase::NPN_ReleaseObject(m_callback);
}
}
BrowserAdapter::IsInteractiveAtPointArgs::IsInteractiveAtPointArgs(int x, int y, NPObject* cb, int queryNum) :
BrowserServerCallArgs(queryNum), m_x(x), m_y(y), m_callback(cb) {
if (m_callback) {
AdapterBase::NPN_RetainObject(m_callback);
}
}
BrowserAdapter::IsInteractiveAtPointArgs::~IsInteractiveAtPointArgs ()
{
if (m_callback) {
AdapterBase::NPN_ReleaseObject(m_callback);
}
}
BrowserAdapter::GetElementInfoAtPointArgs::GetElementInfoAtPointArgs(int x, int y, NPObject* cb, int queryNum) :
BrowserServerCallArgs(queryNum), m_x(x), m_y(y), m_callback(cb) {
if (m_callback) {
AdapterBase::NPN_RetainObject(m_callback);
}
}
BrowserAdapter::GetElementInfoAtPointArgs::~GetElementInfoAtPointArgs ()
{
if (m_callback) {
AdapterBase::NPN_ReleaseObject(m_callback);
}
}
BrowserAdapter::GetHistoryStateArgs::GetHistoryStateArgs(NPObject* cb, int queryNum) :
BrowserServerCallArgs(queryNum), m_successCb(cb) {
if (m_successCb) {
AdapterBase::NPN_RetainObject(m_successCb);
}
}
BrowserAdapter::GetHistoryStateArgs::~GetHistoryStateArgs ()
{
if (m_successCb) {
AdapterBase::NPN_ReleaseObject(m_successCb);
}
}
BrowserAdapter::IsEditingArgs::IsEditingArgs(NPObject* cb, int queryNum) :
BrowserServerCallArgs(queryNum), m_callback(cb) {
if (m_callback) {
AdapterBase::NPN_RetainObject(m_callback);
}
}
BrowserAdapter::IsEditingArgs::~IsEditingArgs ()
{
if (m_callback) {
AdapterBase::NPN_ReleaseObject(m_callback);
}
}
BrowserAdapter::CopySuccessCallbackArgs::CopySuccessCallbackArgs(NPObject* cb, int queryNum) :
BrowserServerCallArgs(queryNum), m_callback(cb) {
if (m_callback) {
AdapterBase::NPN_RetainObject(m_callback);
}
}
BrowserAdapter::CopySuccessCallbackArgs::~CopySuccessCallbackArgs ()
{
if (m_callback) {
AdapterBase::NPN_ReleaseObject(m_callback);
}
}
BrowserAdapter::HitTestArgs::HitTestArgs(const char *type, const Point& pt,
int modifiers, int queryNum) :
BrowserServerCallArgs(queryNum),
m_type(type),
m_pt(pt),
m_modifiers(modifiers),
m_gestureGen(0)
{
}
BrowserAdapter::HitTestArgs::~HitTestArgs()
{
}
BrowserAdapter::GetTextCaretArgs::GetTextCaretArgs(NPObject *cb, int queryNum) :
BrowserServerCallArgs(queryNum),
m_callback(cb)
{
if (m_callback) {
AdapterBase::NPN_RetainObject(m_callback);
}
}
BrowserAdapter::GetTextCaretArgs::~GetTextCaretArgs()
{
}
/**
* Send the adapter's state down the server (happens whenever we successfully connect).
*/
void BrowserAdapter::sendStateToServer()
{
int32_t virtualPageWidth = kVirtualPageWidth;
int32_t virtualPageHeight = kVirtualPageHeight;
for (int i=0; i<mArgc; i++) {
TRACEF("mArgn[%d]=%s, mArgv[%d]=%s", i, mArgn[i], i, mArgv[i]);
if (0==strcasecmp(mArgn[i], "virtualpagewidth")) {
virtualPageWidth = atoi(mArgv[i]);
} else if (0==strcasecmp(mArgn[i], "virtualpageheight")) {
virtualPageHeight = atoi(mArgv[i]);
}
}
asyncCmdConnect(virtualPageWidth, virtualPageHeight, mOffscreen0->key(),
mOffscreen1->key(), mOffscreen0->size(), mPageIdentifier);
// Connect carries exactly two keys and its wire shape is frozen, so the third arrives as a
// SEPARATE, ADDITIVE command. An adapter that never sends it leaves the daemon on two
// buffers, which is why this is safe to add to a contract otherwise held byte-identical.
if (mOffscreen2)
asyncCmdSetExtraBuffer(mOffscreen2->key(), mOffscreen2->size());
asyncCmdSetWindowSize(mViewportWidth, mViewportHeight);
asyncCmdPageFocused(mPageFocused);
asyncCmdSetMouseMode(mMouseMode);
asyncCmdInterrogateClicks(m_interrogateClicks);
asyncCmdSetEnableJavaScript(mEnableJavaScript);
asyncCmdSetBlockPopups(mBlockPopups);
asyncCmdSetAcceptCookies(mAcceptCookies);
asyncCmdSetShowClickedLink(mShowClickedLink);
asyncCmdSetZoomAndScroll(mZoomLevel, mScrollPos.x, serverScrollY());
const char* identifier = (const char*) NPN_GetValue((NPNVariable) npPalmApplicationIdentifier);
if (identifier)
asyncCmdSetAppIdentifier(identifier);
std::list<UrlRedirectInfo*>::const_iterator i;
for (i = m_urlRedirects.begin(); i != m_urlRedirects.end(); ++i) {
asyncCmdAddUrlRedirect((*i)->re.c_str(), (*i)->type, (*i)->redir, (*i)->udata.c_str());
}
}
/**
* Return the value of PalmSystem.isActivated.
*/
bool BrowserAdapter::isActivated()
{
// FIXME Need access to PalmSystem
#if PALM_SYSTEM_AVAILABLE
bool activated(false);
NPVariant jsCallResult, jsCallArgs;
STRINGZ_TO_NPVARIANT("PalmSystem.isActivated", jsCallArgs);
if (InvokeAdapter(gEval, &jsCallArgs, 1, &jsCallResult)) {
if (IsBooleanVariant(jsCallResult)) {
activated = VariantToBoolean(jsCallResult);
}
}
return activated;
#else
return true;
#endif
}
/**
* Complete the initialization of this object. Parses parameters and
* establishes connection to the BrowserServer.
*
* @return true if successful, false if not.
*/
bool BrowserAdapter::init()
{
if (!isActivated()) {
return false;
}
if (!initializeIpcBuffer()) {
serverDisconnected();
return false;
}
if (mBrowserServerConnected) {
serverConnected();
return true;
}
createBufferLock();
bool successful = connect();
if (successful) {
sendStateToServer();
serverConnected();
if (mFrozen)
BrowserAdapterManager::instance()->adapterActivated(this, true);
} else {
TRACEF("%s: unable to connect to BrowserServer.", __FUNCTION__);
serverDisconnected();
}
if (!mDirtyPattern) {
const int patternSize = 8;
mDirtyPattern = new QImage(patternSize * 2, patternSize * 2, QImage::Format_ARGB32_Premultiplied);
QPainter gc;
gc.begin(mDirtyPattern);
gc.setPen(QColor(0x00, 0x00, 0x00, 0x00));
gc.setBrush(QBrush(QColor(0xCC, 0xCC, 0xCC, 0xFF)));
gc.drawRect(QRect(0, 0, patternSize, patternSize));
gc.drawRect(QRect(patternSize, patternSize, patternSize * 2, patternSize * 2));
gc.setBrush(QBrush(QColor(0xEE, 0xEE, 0xEE, 0xFF)));
gc.drawRect(QRect(patternSize, 0, patternSize * 2, patternSize));
gc.drawRect(QRect(0, patternSize, patternSize, patternSize * 2));
gc.end();
}
return successful;
}
bool BrowserAdapter::initializeIpcBuffer()
{
if (!mOffscreen0)
mOffscreen0 = BrowserOffscreen::create();
if (!mOffscreen0)
return false;
if (!mOffscreen1)
mOffscreen1 = BrowserOffscreen::create();
if (!mOffscreen1) {
delete mOffscreen0;
mOffscreen0 = 0;
return false;
}
// Best-effort ONLY. A third buffer is a frame-rate optimisation, not a requirement, and it
// costs another full-page segment of SysV shm; on a device this size that allocation can
// legitimately fail. Failing it must leave a working two-buffer adapter rather than a dead
// one, so this deliberately does not undo the pair above.
if (!mOffscreen2)
mOffscreen2 = BrowserOffscreen::create();
return true;
}
void BrowserAdapter::setDefaultViewportSize()
{
NPVariant jsCallResult, jsCallArgs;
GetScreenResolution(mViewportWidth, mViewportHeight);
STRINGZ_TO_NPVARIANT("PalmSystem.deviceInfo.maximumCardWidth", jsCallArgs);
if (InvokeAdapter(gEval, &jsCallArgs, 1, &jsCallResult)) {
if (IsIntegerVariant(jsCallResult)) {
mViewportWidth = VariantToInteger(jsCallResult);
}
}
STRINGZ_TO_NPVARIANT("PalmSystem.deviceInfo.maximumCardHeight", jsCallArgs);
if (InvokeAdapter(gEval, &jsCallArgs, 1, &jsCallResult)) {
if (IsIntegerVariant(jsCallResult)) {
mViewportHeight = VariantToInteger(jsCallResult);
}
}
}
/**
* We lazily connect to the BrowserServer. This is just a handly routine
* that will cast and then initialize the BrowserAdapter.
*/
BrowserAdapter* BrowserAdapter::GetAndInitAdapter( AdapterBase* adapter )
{
BrowserAdapter* pAdapter = static_cast<BrowserAdapter*>(adapter);
pAdapter->init();// Safe to call multiple times.
return pAdapter;
}
static QColor colorNoOffscreen(0xff, 0xff, 0x00, 0xff);
static QColor colorNoConnection(0x00, 0xff, 0xff, 0xff);
static QColor colorOffscreenSurfEmpty(0xff, 0x00, 0xff, 0xff);
static QColor colorGenericBorder(0x00, 0x00, 0xff, 0xff);
//#define DRAW_DEBUG_COLORS
#if defined(DRAW_DEBUG_COLORS)
static void drawDebugBorder(QPainter* gc, NPWindow* window, QColor color)
{
gc->push();
gc->translate(window->x, window->y);
gc->addClipRect(0, 0, window->width, window->height);
gc->setFillColor(QColor(0,0,0,0));
gc->setStrokeThickness(2);
gc->setStrokeColor(color);
gc->drawRect(0, 0, window->width, window->height);
gc->pop();
}
static void drawDebugFill(QPainter* gc, NPWindow* window, QColor color)
{
gc->push();
gc->translate(window->x, window->y);
gc->addClipRect(0, 0, window->width, window->height);
gc->setFillColor(color);
gc->drawRect(0, 0, window->width, window->height);
gc->pop();
}
#else
static void drawDebugBorder(QPainter* gc, NPWindow* window, QColor color)
{
}
static void drawDebugFill(QPainter* gc, NPWindow* window, QColor color)
{
}
#endif
// Paint-path debug log (Jihad review F-166). Resolved AT MOST ONCE: only opens a stream when the
// gate file <state>/adapterlog exists, so in production the paint and msgPainted paths do ZERO
// per-frame filesystem I/O (the old code fopen/fprintf/fclose'd on every geometry change / every
// painted frame, which stalls the compositor and grows an unbounded log). Line-buffered so tails
// see each entry without an explicit flush. Off by default: no gate file, no stream, no writes.
//
// Both the gate and the log moved OUT of /media/internal/jihad/ into JIHAD_STATE_DIR
// (/var/palm/jihad/<variant>). /media/internal is the user's vfat USB mass-storage volume:
// cavekit-device-build.md R8 forbids this package writing anything there, it has no real
// permissions (everything reads as 777), and it disappears while the device is mounted as a USB
// drive on a PC. The daemon half of this made the same move (render/goanna/JihadRuntimePaths.h);
// it resolves the dir at runtime from $JIHAD_BS_NAME, we resolve it at compile time from
// JIHAD_VARIANT, and both land on the same /var/palm/jihad/<variant>.
//
// Nothing is created here but the log file itself, and never a world-readable one: no mkdir (the
// package owns the state dir and R8 forbids us widening it), O_NOFOLLOW so a planted symlink
// cannot redirect a write made with LunaSysMgr's root privileges, and mode 0600 instead of
// fopen()'s umask-dependent 0666. On a desktop/Ubuntu adapter build the gate simply never exists.
static FILE* jihadPaintLog()
{
static FILE* s_log = reinterpret_cast<FILE*>(-1); // -1 == not yet resolved
if (s_log == reinterpret_cast<FILE*>(-1)) {
s_log = NULL;
if (access(JIHAD_STATE_DIR "/adapterlog", F_OK) == 0) {
int fd = open(JIHAD_STATE_DIR "/adapter.log",
O_WRONLY | O_APPEND | O_CREAT | O_NOFOLLOW, S_IRUSR | S_IWUSR);
if (fd >= 0) {
s_log = fdopen(fd, "a");
if (s_log) setvbuf(s_log, NULL, _IOLBF, 0);
else close(fd);
}
}
}
return s_log;
}
// T-505 (the owner's choice, 2026-10-26): the frame is wrapped without alpha. The daemon's frames are opaque (every
// path writes alpha 255: the GPU readback forces it, the software readback and the region render enforce it), and
// Piranha's software blit (the card's PGContext is a PSoftContext2D) then copies instead of blending: 9 -> 5 ms a
// paint by finger, LunaSysMgr's card instance -15 % CPU while panning, the picture the same. <state>/alphawrap (read
// once per LunaSysMgr process) wraps it with alpha as before.
static bool jihadOpaqueOn()
{
static int s_on = -1;
if (s_on < 0) s_on = access(JIHAD_STATE_DIR "/alphawrap", F_OK) == 0 ? 0 : 1;
return s_on == 1;
}
static double jihadMsSince(const struct timeval& a, const struct timeval& b)
{
return (b.tv_sec - a.tv_sec) * 1000.0 + (b.tv_usec - a.tv_usec) / 1000.0;
}
// T-505: the GPU path's paints while the paint log is on, a line every 2 s: how many, the new frames' and the
// repaints' (the same frame, a scroll step) wrap, blit, hold (T-544) and release times, the pixels blitted
struct JihadPaintStat {
int n[2]; double wrap[2], blit[2], hold[2], rel[2], all[2], maxAll[2]; double px[2];
struct timeval since; bool started;
};
static void jihadPaintStat(bool same, double wrap, double blit, double hold, double rel, double all, double px)
{
FILE* lf = jihadPaintLog();
if (!lf) return;
static JihadPaintStat s;
struct timeval now;
gettimeofday(&now, NULL);
if (!s.started) { memset(&s, 0, sizeof(s)); s.since = now; s.started = true; }
const int k = same ? 1 : 0;
s.n[k]++; s.wrap[k] += wrap; s.blit[k] += blit; s.hold[k] += hold; s.rel[k] += rel; s.all[k] += all; s.px[k] += px;
if (all > s.maxAll[k]) s.maxAll[k] = all;
const double span = jihadMsSince(s.since, now);
if (span < 2000.0) return;
fprintf(lf, "[paint-stat] %.0f ms: %d paints (%d new frames, %d repaints of the same), %s\n",
span, s.n[0] + s.n[1], s.n[0], s.n[1], jihadOpaqueOn() ? "opaque" : "with alpha");
for (int c = 0; c < 2; c++) {
if (!s.n[c]) continue;
fprintf(lf, "[paint-stat] %s: wrap %.2f, blit %.2f, hold %.2f, release %.2f, all %.2f ms avg (max %.1f), %.0f kpx a paint\n",
c ? "repaints" : "new frames", s.wrap[c] / s.n[c], s.blit[c] / s.n[c], s.hold[c] / s.n[c], s.rel[c] / s.n[c],
s.all[c] / s.n[c], s.maxAll[c], s.px[c] / s.n[c] / 1000.0);
}
memset(&s, 0, sizeof(s)); s.since = now; s.started = true;
}
// T-544: between two frames of a pan the card shows the frame it holds moved by the scroll since, and
// the frame's fixed and stuck parts (desktop Wikipedia's sidebars) moved with the page and jumped back
// at the next frame (the owner, 2026-10-26: the column glides, the sidebars jitter and lag). Where the
// daemon says what is stuck in the frame (JihadFrameWindow.h), each such rect is drawn where the frame
// drew it, and what its moved copy covers outside it in the colour the frame had just past the rect on
// that side (the page's background there; white past the frame's edge).
// At the card's own zoom only, with the page's top at the window's top then and now (no Mojo header
// showing, no bounce at the page's top). The rects are the daemon's (JIHAD_GPU_NO_HOLD=1 there: none).
// where the hold draws (window px: left, top, right, bottom) and the window px to the context's offset
struct JihadHoldClip { int l, t, r, b, toX, toY; };
// a strip in one colour: the frame's pixel at (sx, sy), white past the frame (the PDK's gcc 4.3: no lambdas)
static void jihadHoldFill(PGContext* gc, const JihadHoldClip& c, const unsigned char* raster, const BrowserOffscreenInfo* gi,
int l, int t, int r, int b, int sx, int sy)
{
static uint32_t sWhite = 0xffffffffu;
if (l < c.l) l = c.l;
if (t < c.t) t = c.t;
if (r > c.r) r = c.r;
if (b > c.b) b = c.b;
if (r <= l || b <= t)
return;
const bool in = sx >= 0 && sy >= 0 && sx < gi->renderedWidth && sy < gi->renderedHeight;
const unsigned char* px = in ? raster + ((size_t)sy * gi->renderedWidth + sx) * 4 : (const unsigned char*)&sWhite;
if (PGSurface* one = PGSurface::wrap(1, 1, px, true)) {
gc->bitblt(one, 0, 0, 1, 1, l + c.toX, t + c.toY, r + c.toX, b + c.toY);
one->releaseRef();
}
}
static void jihadHoldStuck(PGContext* gc, PGSurface* surf, BrowserOffscreen* off, const BrowserOffscreenInfo* gi,
const NpPalmDrawEvent* ev, int scrollX, int pageScrollY, double zoom, int winW, int winH)
{
if (fabs(gi->contentZoom - zoom) > 1e-6 || pageScrollY < 0 || gi->renderedWidth != gi->bufferWidth)
return;
int vx = 0, vy = 0, r[kJihadStuckMax][4];
const int n = off->stuckRects(&vx, &vy, r, kJihadStuckMax);
if (n <= 0 || gi->renderedY + vy < 0)
return;
// the frame as drawn now, against where it was composed (window px)
const int dX = gi->renderedX + vx - scrollX, dY = gi->renderedY + vy - pageScrollY;
if (dX == 0 && dY == 0)
return;
// window px to the context's: the event's src rect (window px) lands on its dst rect 1:1; what is
// drawn stays in both and in the window
JihadHoldClip c;
c.toX = ev->dstLeft - ev->srcLeft; c.toY = ev->dstTop - ev->srcTop;
c.l = ev->srcLeft; c.t = ev->srcTop;
c.r = ev->srcLeft + (ev->dstRight - ev->dstLeft); c.b = ev->srcTop + (ev->dstBottom - ev->dstTop);
if (c.l < 0) c.l = 0;
if (c.t < 0) c.t = 0;
if (c.r > winW) c.r = winW;
if (c.b > winH) c.b = winH;
const unsigned char* raster = off->rasterBuffer();
// the moved copies' pixels outside the rects' own places
for (int k = 0; k < n; ++k) {
const int hl = r[k][0] - vx, ht = r[k][1] - vy, hr = r[k][2] - vx, hb = r[k][3] - vy;
const int ml = hl + dX, mt = ht + dY, mr = hr + dX, mb = hb + dY;
if (mr <= ml || mb <= mt)
continue;
const int mx = (r[k][0] + r[k][2]) / 2, my = (r[k][1] + r[k][3]) / 2; // frame px
jihadHoldFill(gc, c, raster, gi, ml, mt, mr, ht < mb ? (ht > mt ? ht : mt) : mb, mx, r[k][1] - 1); // above the rect's place
jihadHoldFill(gc, c, raster, gi, ml, hb > mt ? (hb < mb ? hb : mb) : mt, mr, mb, mx, r[k][3]); // below it
const int it = mt > ht ? mt : ht, ib = mb < hb ? mb : hb; // beside it
if (ib > it) {
jihadHoldFill(gc, c, raster, gi, ml, it, ml < hl ? (mr < hl ? mr : hl) : ml, ib, r[k][0] - 1, my);
jihadHoldFill(gc, c, raster, gi, mr > hr ? (ml > hr ? ml : hr) : mr, it, mr, ib, r[k][2], my);
}
}
// the rects where the frame drew them
for (int k = 0; k < n; ++k) {
int sl = r[k][0], st = r[k][1], sr = r[k][2], sb = r[k][3];
if (sl < 0) sl = 0;
if (st < 0) st = 0;
if (sr > gi->renderedWidth) sr = gi->renderedWidth;
if (sb > gi->renderedHeight) sb = gi->renderedHeight;
int l = sl - vx, t = st - vy, rr = sr - vx, b = sb - vy;
if (l < c.l) { sl += c.l - l; l = c.l; }
if (t < c.t) { st += c.t - t; t = c.t; }
if (rr > c.r) { sr -= rr - c.r; rr = c.r; }
if (b > c.b) { sb -= b - c.b; b = c.b; }
if (rr > l && b > t)
gc->bitblt(surf, sl, st, sr, sb, l + c.toX, t + c.toY, rr + c.toX, b + c.toY);
}
if (FILE* lf = jihadPaintLog()) {
static int sN = 0;
if (sN++ < 60)
fprintf(lf, "[hold] %d rects, moved %d,%d, view %d,%d, the first %d,%d..%d,%d\n",
n, dX, dY, vx, vy, r[0][0], r[0][1], r[0][2], r[0][3]);
}
}
void BrowserAdapter::handlePaint(NpPalmDrawEvent* event)
{
// Two paint modes (WebKit selects via npPalmUseGraphicsContext, which reads our
// useGraphicsContext=true):
// - GRAPHICS CONTEXT (device default): WebKit hands us a Piranha PGContext* (no buffer).
// It carries the card's rotation/scale transform, so we wrap our offscreen as a PGSurface
// and bitblt THROUGH it => the compositor maps logical->physical incl. portrait<->landscape
// rotation. This is the rotation-correct path (see the PGContext branch below).
// - RAW dstBuffer (fallback, only if no context is provided — e.g. desktop/Ubuntu builds):
// memcpy our offscreen ARGB tiles into the WebKit-provided buffer. NOTE the stock isis path
// cast event->graphicsContext to a QPainter* and segfaulted LunaSysMgr — the device type is
// a PGContext, which the graphics-context branch now uses correctly.
// Ported from the Atlas Browser project (Herrie82) BrowserAdapter: PGContext/PGSurface composite
// + commits 7b91ab8 (dstBuffer path), aefab6d (out-of-buffer white fill), dbc897c (tall-buffer
// pan), 27b54a5 (pinch-zoom scaled blit), 77ab724/01da087 (scroll indicator). See NOTICE.
if (FILE* lf = jihadPaintLog()) {
static int lastEH = -99; static void* lastOff = (void*)0x1;
int eh = event ? (event->dstBottom - event->dstTop) : -1;
if (eh != lastEH || (void*)mOffscreenCurrent != lastOff) {
lastEH = eh; lastOff = (void*)mOffscreenCurrent;
// gc is logged because it SELECTS the path: on device it is non-NULL and dstBuffer is
// legitimately NULL, so "dstBuf=(nil)" alone cannot distinguish "took the PGContext
// path" from "bailed at the dstBuffer guard" — which is exactly the ambiguity that made
// a blank card unreadable from this log.
fprintf(lf, "[hp-entry] dstH=%d mOff=%p dstBuf=%p rowBytes=%u gc=%p\n",
eh, (void*)mOffscreenCurrent, event?event->dstBuffer:0, event?event->dstRowBytes:0,
event?event->graphicsContext:0); } }
if (!event)
return;
// PRIMARY (device) PATH — composite through the transform-aware Piranha PGContext.
// With useGraphicsContext=true WebKit hands us a PGContext* that already carries the
// card's rotation/scale transform, and NO dstBuffer. We wrap our offscreen raster as a
// PGSurface (zero-copy) and bitblt THROUGH the context, so the compositor performs the
// logical->physical mapping — INCLUDING the portrait<->landscape rotation. This is the
// correct fix for the rotation shear/tiling: the raw-dstBuffer blit below is row-major in
// the card's logical space and cannot account for the rotation the compositor applies, so
// it sheared in landscape (the old white-frame "rotation guard" only masked it). Ported
// from Atlas Browser (Herrie82) BrowserAdapter; see NOTICE.
if (event->graphicsContext && mOffscreenCurrent) {
PGContext* gc = (PGContext*) event->graphicsContext;
BrowserOffscreenInfo* gi = mOffscreenCurrent->header();
if (gi && gi->renderedWidth > 0 && gi->renderedHeight > 0) {
// Stale-geometry guard (rotation transition): at unity zoom the buffer must match the
// current viewport WIDTH. If the daemon has not re-rendered at the new orientation yet
// after a rotate, HOLD the last composited frame rather than transform a wrong-size
// buffer (Codex review 2026-07-26, Finding 4). Pinch-zoom legitimately changes the
// width ratio and is exempt (inv != 1 -> scaled blit below).
{
double cz = (gi->contentZoom > 0.0) ? gi->contentZoom : 1.0;
double iv = (mZoomLevel > 0.0) ? (cz / mZoomLevel) : 1.0;
bool nearUnity = iv > 0.98 && iv < 1.02;
// bufferWidth, not renderedWidth: since 2026-10-05 the painted region may be
// WIDER than the viewport when zoomed in (sideways pan headroom), and what
// this guard asks is which viewport the frame was rendered for. The two are
// equal in every frame from a daemon that predates that.
if (nearUnity && mWindow.width > 0 && gi->bufferWidth != (int)mWindow.width)
return;
}
// T-505: timed while the paint log is on; a repaint is the same frame in the same buffer (a scroll step)
struct timeval tvA, tvB, tvC, tvD, tvE;
const bool timed = jihadPaintLog() != NULL;
if (timed) gettimeofday(&tvA, NULL);
const unsigned char* raster = (const unsigned char*)mOffscreenCurrent->rasterBuffer();
const uint32_t frameSeq = mOffscreenCurrent->frameSeq();
const bool sameFrame = frameSeq != 0 && frameSeq == mJihadLastPaintSeq && raster == mJihadLastPaintBuf;
mJihadLastPaintSeq = frameSeq; mJihadLastPaintBuf = raster;
// (a PGSurface kept for the frame across its repaints changed nothing: the card's context is Piranha's
// software one, and its blit reads the whole source every time; measured 2026-10-26, not kept)
PGSurface* surf = PGSurface::wrap((uint32_t)gi->renderedWidth, (uint32_t)gi->renderedHeight, raster, !jihadOpaqueOn());
if (timed) gettimeofday(&tvB, NULL);
if (!surf) { if (FILE* lf = jihadPaintLog()) fprintf(lf, "[hp-pg] PGSurface::wrap FAILED %dx%d\n",
gi->renderedWidth, gi->renderedHeight); }
if (surf) {
int dl = event->dstLeft, dt = event->dstTop, dr = event->dstRight, dbm = event->dstBottom;
double czoom = (gi->contentZoom > 0.0) ? gi->contentZoom : 1.0;
double inv = (mZoomLevel > 0.0) ? (czoom / mZoomLevel) : 1.0; // buffer px per screen px
int baseX = event->srcLeft - gi->renderedX; // buffer-x for content-x 0
int baseY = event->srcTop - gi->renderedY;
int sL = (int)(baseX + mScrollPos.x * inv);
// The page starts m_headerHeight below the window's top (the Mojo shell's sliding
// toolbar sits over that strip); the clamp below leaves rows above the page undrawn.
const int pageScrollY = mScrollPos.y - m_headerHeight;
int sT = (int)(baseY + pageScrollY * inv);
int sR = (int)(baseX + (mScrollPos.x + (dr - dl)) * inv);
int sB = (int)(baseY + (pageScrollY + (dbm - dt)) * inv);
// Clamp the SOURCE rect to the buffer on BOTH axes and shrink the DEST edges by the
// same scale (ceil = conservative), so a pan/zoom past the buffer edge leaves that
// region undrawn instead of feeding Piranha an out-of-bounds source (garbage/crash)
// or edge-replicating a smear. No-op when the viewport is inside the buffer -> normal
// composite is byte-identical (Codex review 2026-07-26, Finding 4).
if (inv > 0.0) {
int W = (int)gi->renderedWidth, H = (int)gi->renderedHeight;
if (sL < 0) { dl += (int)ceil((0 - sL) / inv); sL = 0; }
if (sT < 0) { dt += (int)ceil((0 - sT) / inv); sT = 0; }
if (sR > W) { dr -= (int)ceil((sR - W) / inv); sR = W; }
if (sB > H) { dbm -= (int)ceil((sB - H) / inv); sB = H; }
}
bool blitted = (sR > sL && sB > sT && dr > dl && dbm > dt);
if (FILE* lf = jihadPaintLog()) {
// T-502 device rounds: what this process READS from the shared buffer at blit time
static int sN = 0; static unsigned int sLastA = 1, sLastB = 1;
const unsigned int* words = (const unsigned int*)mOffscreenCurrent->rasterBuffer();
const int W = gi->renderedWidth, H = gi->renderedHeight;
unsigned int a = (W > 100 && H > 100) ? words[100 * W + 100] : 0;
unsigned int b = (W > 400 && H > 500) ? words[500 * W + 400] : 0;
unsigned int c = (W > 700 && H > 900) ? words[900 * W + 700] : 0;
if (sN++ < 40 || a != sLastA || b != sLastB) {
sLastA = a; sLastB = b;
fprintf(lf, "[hp-px] n=%d blit=%d key=%d px(100,100)=%08x px(400,500)=%08x px(700,900)=%08x frozen=%d\n",
sN, (int)blitted, mOffscreenCurrent->key(), a, b, c, mFrozen ? 1 : 0);
}
}
// Every way this branch can silently draw nothing (clamped-away rect, zero-area
// dst, a surface that wrapped but never blitted) looks identical on screen: a
// blank card. Log the decision and the numbers behind it, on change only.
if (FILE* lf = jihadPaintLog()) {
static int lS[4] = {-9,-9,-9,-9}, lD[4] = {-9,-9,-9,-9}; static int lB = -1;
if (lS[0]!=sL||lS[1]!=sT||lS[2]!=sR||lS[3]!=sB||
lD[0]!=dl||lD[1]!=dt||lD[2]!=dr||lD[3]!=dbm||lB!=(int)blitted) {
lS[0]=sL; lS[1]=sT; lS[2]=sR; lS[3]=sB;
lD[0]=dl; lD[1]=dt; lD[2]=dr; lD[3]=dbm; lB=(int)blitted;
fprintf(lf, "[hp-pg] blit=%d src[%d,%d %d,%d] dst[%d,%d %d,%d] inv=%.3f "
"base[%d,%d] scroll[%d,%d] rW=%d rH=%d rX=%d rY=%d\n",
(int)blitted, sL, sT, sR, sB, dl, dt, dr, dbm, inv, baseX, baseY,
mScrollPos.x, mScrollPos.y, gi->renderedWidth, gi->renderedHeight,
gi->renderedX, gi->renderedY); } }
if (timed) tvD = tvC = tvB;
if (blitted) {
gc->bitblt(surf, sL, sT, sR, sB, dl, dt, dr, dbm);
if (timed) gettimeofday(&tvC, NULL);
jihadHoldStuck(gc, surf, mOffscreenCurrent, gi, event, mScrollPos.x, pageScrollY,
mZoomLevel, (int)mWindow.width, (int)mWindow.height); // T-544
if (timed) gettimeofday(&tvD, NULL);
}
surf->releaseRef(); // PG ref-counted (starts at 1); frees the wrapper, not our data
if (timed) {
gettimeofday(&tvE, NULL);
jihadPaintStat(sameFrame, jihadMsSince(tvA, tvB), jihadMsSince(tvB, tvC), jihadMsSince(tvC, tvD),
jihadMsSince(tvD, tvE), jihadMsSince(tvA, tvE), blitted ? (double)(sR - sL) * (sB - sT) : 0.0);
}
}
}
return;
}
if (!event->dstBuffer || event->dstRowBytes == 0 || !mOffscreenCurrent)
return;
BrowserOffscreenInfo* info = mOffscreenCurrent->header();
if (!info || info->renderedWidth <= 0 || info->renderedHeight <= 0)
return;
// DEBUG geometry log: log whenever the dst box dimensions CHANGE (so a rotate to
// landscape is captured, not just the portrait launch paints) -> pull to see the real
// dst geometry when the on-screen composite is wrong (tiling/scanlines in landscape).
if (FILE* lf = jihadPaintLog()) {
static int lastW = -1, lastH = -1, lastRW = -1, lastRH = -1;
int curW = event->dstRight - event->dstLeft, curH = event->dstBottom - event->dstTop;
if (curW != lastW || curH != lastH ||
info->renderedWidth != lastRW || info->renderedHeight != lastRH) {
lastW = curW; lastH = curH; lastRW = info->renderedWidth; lastRH = info->renderedHeight;
fprintf(lf, "[hp] dstRowBytes=%u dst[l=%d r=%d t=%d b=%d] src[l=%d r=%d t=%d b=%d] "
"hdr[bufW=%d bufH=%d rW=%d rH=%d rX=%d rY=%d cz=%.3f] win[%ux%u] zoom=%.3f scroll[%d,%d]\n",
event->dstRowBytes, event->dstLeft, event->dstRight, event->dstTop, event->dstBottom,
event->srcLeft, event->srcRight, event->srcTop, event->srcBottom,
info->bufferWidth, info->bufferHeight, info->renderedWidth, info->renderedHeight,
info->renderedX, info->renderedY, info->contentZoom, mWindow.width, mWindow.height,
mZoomLevel, mScrollPos.x, mScrollPos.y); } }
unsigned char* dstBase = (unsigned char*) event->dstBuffer;
int dstStride = (int) event->dstRowBytes;
unsigned char* srcBase = mOffscreenCurrent->rasterBuffer();
int srcStride = info->renderedWidth * 4;
// Rotation guard: during a portrait<->landscape rotate the daemon has not yet re-rendered
// at the new orientation, so the stale buffer's srcStride (renderedWidth*4) does not match
// the new dst width and blitting it produces the 3x tiling + scanlines seen in landscape.
// The distinguishing signal of a rotation is that the dst WIDTH changes (768<->1024); when
// that happens while the buffer is still the old width, HOLD (return, next paint blits the
// re-rendered buffer). Do NOT key off portrait/landscape aspect alone: the VKB shrinks a
// portrait page to 768x602 (numerically "landscape", width>height) while the dst stays
// 768 wide — that is not a rotation, and holding there left the freshly-cleared dst white
// (Jihad review #7 P2 + VKB-whiteout fix). Width match => safe to blit (height mismatch is
// handled per-row below). Pinch-zoom keeps width==dst-scaled and uses the invScale path.
{
// A 1:1 (near-unity) blit requires the buffer's rendered width to equal the VIEWPORT
// (plugin window) width: the source is read at srcStride = renderedWidth*4, so if the
// buffer was rendered at a different width than the window it is being composited into,
// every row lands shifted and the frame shears/tiles. HOLD the last good frame until the
// daemon re-renders at the new width, rather than blit garbage. Compare against the WINDOW
// width, NOT the paint damage-rect (dstRight-dstLeft): a partial/clipped exposure paint has
// a narrow damage rect but a matching buffer, and keying off the damage width made those
// partial repaints look like a rotation mismatch and wrongly held -> stale/blank regions
// (Jihad review F-184). This width test also catches a same-aspect-class stale buffer during
// rotation (e.g. a 768x602 buffer vs a 1024x768 window — both "landscape", widths differ,
// F-167). Deliberate pinch-zoom (invScale != 1) legitimately changes widths and uses the
// scaled path below, so it is exempt. A VKB resize keeps width == window width.
double cz = (info->contentZoom > 0.0) ? info->contentZoom : 1.0;
double inv = (mZoomLevel > 0.0) ? (cz / mZoomLevel) : 1.0;
bool nearUnity = inv > 0.98 && inv < 1.02;
// bufferWidth is the viewport the frame was rendered for (see the PGContext path).
if (nearUnity && mWindow.width > 0 && info->bufferWidth != (int)mWindow.width)
return;
}
// Map dst box <- src box (content coords). The src content lives at (renderedX,renderedY)
// in the offscreen; dstBuffer points to the dst-box top-left.
int dl = event->dstLeft, dt = event->dstTop, dr = event->dstRight, dbm = event->dstBottom;
int sl = event->srcLeft, st = event->srcTop;
// The page starts m_headerHeight below the window's top (the card's toolbar sits over that
// strip and scrolls off with the page — the Mojo shell, 2026-10-07); rows above the page's
// top are not page and are left to the fill below.
int panY = mScrollPos.y - m_headerHeight, panX = mScrollPos.x; // pan within the tall buffer
const unsigned int FILL = 0xffffffff; // un-rendered area: white, not stale (ghosting)
// Pinch/zoom: buffer holds the page at info->contentZoom; display at mZoomLevel. Scale by
// invScale (nearest-neighbour, fixed-point 16.16). invScale==1 => exact 1:1 blit (normal browsing).
double czoom = (info->contentZoom > 0.0) ? info->contentZoom : 1.0;
double invScale = (mZoomLevel > 0.0) ? (czoom / mZoomLevel) : 1.0;
const int FP = 16;
int32_t stepFp = (int32_t)(invScale * (double)(1 << FP) + 0.5);
int baseX = sl - info->renderedX;
int baseY = st - info->renderedY;
for (int dy = dt; dy < dbm; ++dy) {
int64_t cyFp = (int64_t)((dy - dt) + panY) * stepFp;
int oy = baseY + (int)(cyFp >> FP);
unsigned int* drow = (unsigned int*)(dstBase + (dy - dt) * dstStride);
if (oy < 0 || oy >= info->renderedHeight) { // whole row outside the buffer -> fill
for (int dx = dl; dx < dr; ++dx) drow[dx - dl] = FILL;
continue;
}
const unsigned int* srow = (const unsigned int*)(srcBase + oy * srcStride);
int64_t cxFp = (int64_t)(panX) * stepFp;
for (int dx = dl; dx < dr; ++dx) {
int ox = baseX + (int)(cxFp >> FP);
drow[dx - dl] = (ox < 0 || ox >= info->renderedWidth) ? FILL : srow[ox];
cxFp += stepFp;
}
}
// Scroll indicator (the QPainter scrollbar is gone with the dstBuffer path): stock-style
// dark bar on the right edge, blended over the content, fading via mScrollbarOpacity.
if (mScrollbarOpacity > 0 && mContentHeight > (int)mWindow.height && (dbm - dt) > 0) {
int alpha = mScrollbarOpacity > 0xD0 ? 0xD0 : mScrollbarOpacity;
int vh = dbm - dt;
const int barW = 6, margin = 8;
int thumbH = (int)((double)mWindow.height / (double)mContentHeight * vh);
if (thumbH < 50) thumbH = 50;
int thumbY = (int)((double)mScrollPos.y / (double)mContentHeight * vh);
if (thumbY + thumbH > vh) thumbY = vh - thumbH;
if (thumbY < 0) thumbY = 0;
int x0 = (dr - dl) - barW - margin;
if (x0 < 0) x0 = 0;
for (int yy = thumbY; yy < thumbY + thumbH && yy < vh; ++yy) {
unsigned int* drow = (unsigned int*)(dstBase + yy * dstStride);
for (int xx = x0; xx < x0 + barW && xx < (dr - dl); ++xx) {
unsigned int p = drow[xx];
int r = (p >> 16) & 0xFF, g = (p >> 8) & 0xFF, b = p & 0xFF;
r = (r * (255 - alpha) + 0xAA * alpha) / 255;
g = (g * (255 - alpha) + 0xAA * alpha) / 255;
b = (b * (255 - alpha) + 0xAA * alpha) / 255;
drow[xx] = 0xff000000u | (r << 16) | (g << 8) | b;
}
}
}
}
void BrowserAdapter::handleWindowChange(NPWindow* window)
{
TRACEF("BrowserAdapter::handleWindowChange: %u, %u\n", mWindow.width, mWindow.height);
TRACEF("scroll %d,%d", mScrollPos.x, mScrollPos.y);
// Jihad: a 0x0 window is not a size. The Mojo WebView sizes this node to page x scale, and
// a window resize that lands between two pages (the on-screen keyboard coming up while the
// widget holds no page dimensions yet) makes that 0 x 0 — measured 2026-10-01,
// `windowChange 0x0 (vp was 768x705)`. Taking it as the viewport zeroed the fit-zoom inputs
// and told the server its window was empty; keep the last real geometry instead.
if (mWindow.width == 0 || mWindow.height == 0)
return;
m_pageOffset.set(window->x, window->y);
int oldWindowWidth = mViewportWidth;
int oldWindowHeight = mViewportHeight;
double oldZoom = mZoomLevel;
mViewportWidth = mWindow.width;
mViewportHeight = mWindow.height;
asyncCmdSetWindowSize(mViewportWidth, mViewportHeight);
mScroller->setViewportDimensions(mWindow.width, mWindow.height);
// If window size changes and adapter is zoom-fitting, need to update the zoomLevel.
// Compare old width to the NEW width (mViewportWidth), not mViewportHeight — on a
// 768x1024<->1024x768 rotation the old width equals the new height, so the height
// comparison skipped the fit-zoom recompute and landscape kept the portrait zoom
// (Jihad review #5 M-4; was a typo in the imported code).
if (mZoomFit &&
oldWindowWidth != mViewportWidth &&
mWindow.width != 0 &&
mPageWidth != 0) {
mZoomLevel = mWindow.width * 1.0 / mPageWidth;
mContentWidth = ::round(mZoomLevel * mPageWidth);
mContentHeight = ::round(mZoomLevel * mPageHeight);
mScroller->setContentDimensions(mContentWidth, mContentHeight + m_headerHeight);
asyncCmdSetZoomAndScroll(mZoomLevel, mScrollPos.x, serverScrollY());
}
scrollCaretIntoViewAfterResize(oldWindowWidth, oldWindowHeight, oldZoom);
}
bool BrowserAdapter::handleFocus(bool val)
{
const char* identifier = (const char*) NPN_GetValue((NPNVariable) npPalmApplicationIdentifier);
g_message("%s: %p: %s, %d", __PRETTY_FUNCTION__, this, identifier, val);
mPageFocused = val;
init();
asyncCmdPageFocused(val);
BrowserAdapterManager::instance()->adapterActivated(this, val);
if (!val)
mInGestureChange = false;
startFadeScrollbar();
return true;
}
/*
Event Handling
*/
bool BrowserAdapter::handlePenDown(NpPalmPenEvent *event)
{
mFrozen = false;
init();
Point eventPt(event->xCoord, event->yCoord - m_headerHeight);
// mouse hold timer
m_penDownDoc.set((mScrollPos.x + eventPt.x) / mZoomLevel,
(mScrollPos.y + eventPt.y) / mZoomLevel);
m_dragging = false;
EVENT_TRACEF("pen down (%d, %d) (%d, %d)\n", eventPt.x, eventPt.y, m_penDownDoc.x, m_penDownDoc.y);
bool doScroll = true;
bool passToFlash = false;
bool containsFlashPoint = flashRectContainsPoint(m_penDownDoc);
bool containsInteractivePoint = interactiveRectContainsPoint(m_penDownDoc);
detectScrollableLayerUnderMouseDown(m_penDownDoc, Point(eventPt.x / mZoomLevel, eventPt.y / mZoomLevel));
if (mFlashGestureLock) {
passToFlash = containsFlashPoint;
}
if (passToFlash || containsInteractivePoint || shouldPassInputEvents()) {
m_sentToBS = true;
doScroll = false;
if (passToFlash) {
mLastPointSentToFlash = Point(m_penDownDoc.x, m_penDownDoc.y);
}
gettimeofday(&m_lastPassedEventTime, NULL);
asyncCmdMouseEvent(0 /*mousedown*/, m_penDownDoc.x, m_penDownDoc.y, 1);
}
else {
startMouseHoldTimer();
if (!containsFlashPoint && !containsInteractivePoint) {
asyncCmdGetInteractiveNodeRects(m_penDownDoc.x, m_penDownDoc.y);
mShowHighlight = true;
}
}
if (doScroll) {
mScroller->handleMouseDown(eventPt.x, eventPt.y);
}
else
startFadeScrollbar();
if (!containsFlashPoint) {
updateFlashGestureLockStatus(false);
}
updateMouseInFlashStatus(containsFlashPoint);
updateMouseInInteractiveStatus(containsInteractivePoint);
return true;
}
bool BrowserAdapter::handlePenUp(NpPalmPenEvent *event)
{
init();
stopMouseHoldTimer();
m_didDrag = m_dragging;
m_dragging = false;
resetScrollableLayerScrollSession();
Point eventPt(event->xCoord, event->yCoord - m_headerHeight);
int cx = (mScrollPos.x + eventPt.x) / mZoomLevel;
int cy = (mScrollPos.y + eventPt.y) / mZoomLevel;
int dx = m_penDownDoc.x - cx;
int dy = m_penDownDoc.y - cy;
if ((dx * dx + dy * dy) >= kHysteresis * kHysteresis) {
stopMouseHoldTimer();
}
EVENT_TRACEF("pen up (%d, %d) (%d, %d)\n", eventPt.x, eventPt.y, cx, cy);
removeHighlight();
bool doScroll = true;
bool passToFlash = (mMouseInFlashRect && mFlashGestureLock);
if (passToFlash || mMouseInInteractiveRect || shouldPassInputEvents()) {
doScroll = false;
if (passToFlash) {
mLastPointSentToFlash = Point(-1, -1);
}
gettimeofday(&m_lastPassedEventTime, NULL);
asyncCmdMouseEvent(1 /*mouseup*/, cx, cy, 1);
}
if (doScroll) {
mScroller->handleMouseUp(eventPt.x, eventPt.y);
}
updateMouseInFlashStatus(false);
updateMouseInInteractiveStatus(false);
return true;
}
bool BrowserAdapter::handlePenMove(NpPalmPenEvent *event)
{
init();
removeHighlight();
m_sentMouseHoldEvent.reset();
Point eventPt(event->xCoord, event->yCoord - m_headerHeight);
int cx = (mScrollPos.x + eventPt.x) / mZoomLevel;
int cy = (mScrollPos.y + eventPt.y) / mZoomLevel;
int dx = m_penDownDoc.x - cx;
int dy = m_penDownDoc.y - cy;
if ((dx * dx + dy * dy) >= kHysteresis * kHysteresis) {
m_dragging = true;
stopMouseHoldTimer();
}
EVENT_TRACEF("pen move (%d, %d) (%d, %d) interactive %d\n", eventPt.x, eventPt.y, cx, cy, mMouseInInteractiveRect);
bool doScroll = true;
bool passToFlash = mMouseInFlashRect && mFlashGestureLock;
if (passToFlash || mMouseInInteractiveRect || shouldPassInputEvents()) {
if (passToFlash) {
mLastPointSentToFlash = Point(cx, cy);
doScroll = false;
}
gettimeofday(&m_lastPassedEventTime, NULL);
asyncCmdMouseEvent(2 /*mousemove*/, cx, cy, 1);
}
else if (mScrollableLayerScrollSession.layer) {
Point moveToDoc(eventPt.x / mZoomLevel, eventPt.y / mZoomLevel);
scrollLayerUnderMouse(moveToDoc);
if (mScrollableLayerScrollSession.isActive) {
mScroller->handleMouseDown(eventPt.x, eventPt.y);
doScroll = false;
}
else
resetScrollableLayerScrollSession();
}
if (doScroll) {
mScroller->handleMouseMove(eventPt.x, eventPt.y);
}
return true;
}
bool BrowserAdapter::handlePenClick(NpPalmPenEvent *event)
{
Point eventPt(event->xCoord, event->yCoord - m_headerHeight);
Point penDocPt((mScrollPos.x + eventPt.x) / mZoomLevel,
(mScrollPos.y + eventPt.y) / mZoomLevel);
EVENT_TRACEF("pen click (%d, %d) (%d, %d) %d %d %d\n", eventPt.x, eventPt.y, penDocPt.x, penDocPt.y, m_didDrag, m_sentToBS, m_didHold);
if (m_didDrag || m_sentToBS || m_didHold) {
// squelch the click after drag, hold, or sent to browserserver
m_didDrag = false;
m_sentToBS = false;
m_didHold = false;
return true;
}
// use timer to squelch this click if a double click is coming
m_clickPt = penDocPt;
startClickTimer();
// don't consume this to allow dblclick to arrive
return false;
}
bool BrowserAdapter::handlePenDoubleClick(NpPalmPenEvent *event)
{
mShowHighlight = true;
stopClickTimer();
Point eventPt(event->xCoord, event->yCoord - m_headerHeight);
Point penDocPt((mScrollPos.x + eventPt.x) / mZoomLevel,
(mScrollPos.y + eventPt.y) / mZoomLevel);
EVENT_TRACEF("pen double click (%d, %d) (%d, %d)\n", eventPt.x, eventPt.y, penDocPt.x, penDocPt.y);
bool inFlashRect = flashRectContainsPoint(penDocPt);
if (flashGestureLock()) {
if (!inFlashRect) {
updateFlashGestureLockStatus(false);
} else {
// ignore; taps will be sent through with handle* methods
return true;
}
}
else if (inFlashRect) {
updateFlashGestureLockStatus(true);
}
prvSmartZoom(penDocPt);
return true;
}
// ── WHO SWALLOWS A KEYSTROKE ────────────────────────────────────────────────────────────────
// The key is ALWAYS forwarded to the daemon (asyncCmdKeyDown above the return), so the engine
// and any plugin see it either way. The return value decides only whether the CARD also acts
// on it — so it is the difference between typing into a Flash game and typing into the
// address bar at the same time.
//
// Two conditions, for two different bugs:
//
// mPageFocused — ported from Atlas (BrowserAdapter commit ce199bc, Herman van Hazendonk,
// Apache-2.0; see NOTICE). bEditorFocused alone meant a page that auto-focuses an input on
// load left it true forever, so after the user tapped the chrome address bar the adapter
// kept eating every keystroke: "can't type an address after a site loaded".
//
// mFlashGestureLock — Jihad addition for R7. A Flash GAME focuses no HTML editor, so
// bEditorFocused is false throughout and every arrow key / WASD press leaked to the chrome
// while the game was being played. This latch is exactly the right signal: it is set when a
// tap lands inside a plugin rect and cleared by a tap outside one, i.e. it already means
// "the user has engaged the plugin", and it is the same latch that decides whether a DRAG
// goes to the plugin instead of panning the card.
bool BrowserAdapter::handleKeyDown(NpPalmKeyEvent *event)
{
EVENT_TRACEF("KeyDown %d/0x%08x\n", event->rawkeyCode, event->rawModifier);
asyncCmdKeyDown(event->rawkeyCode, event->rawModifier, event->chr);
return event->rawkeyCode != ESC_KEY && mPageFocused && (bEditorFocused || mFlashGestureLock);
}
bool BrowserAdapter::handleKeyUp(NpPalmKeyEvent *event)
{
EVENT_TRACEF("KeyUp %d/0x%08x\n", event->rawkeyCode, event->rawModifier);
asyncCmdKeyUp(event->rawkeyCode, event->rawModifier, event->chr);
return event->rawkeyCode != ESC_KEY && mPageFocused && (bEditorFocused || mFlashGestureLock);
}
bool BrowserAdapter::handleTouchStart(NpPalmTouchEvent *event)
{
return doTouchEvent(0, event);
}
bool BrowserAdapter::handleTouchMove(NpPalmTouchEvent *event)
{
return doTouchEvent(1, event);
}
bool BrowserAdapter::handleTouchEnd(NpPalmTouchEvent *event)
{
return doTouchEvent(2, event);
}
bool BrowserAdapter::handleTouchCancelled(NpPalmTouchEvent *event)
{
return doTouchEvent(3, event);
}
// TOUCH FORWARDING IS ON AS OF 2026-08-15 (T-120) BUT DEFAULT OFF AT RUNTIME. Read this whole
// note before touching the block; the fence it replaces was there for a real reason.
//
// What changed: the ABI mismatch described below is FIXED — the staged webkit/npapi/npapi.h (since
// 2026-10-14 build/webos-oe/adapter-include/npapi/webkit/npapi/npapi.h, in git) carries the 28-byte
// NpPalmTouchEvent (sha256 36b58a83…21c199), so the compile-time guard in doTouchEvent now passes
// and the body is compiled in. What has NOT changed: no device has ever run this. The runtime
// switch is JIHAD_TOUCH_EVENTS=1 (mTouchEventsEnabled), default off, and with it off neither the
// npPalmEnableTouchEvents opt-in in the constructor nor this body does anything at all.
//
// The historical measurement, kept because it is the evidence for the layout below.
// (2026-08-10, on the BUILD HOST from binaries and sources already in this tree. No device was
// used, and nothing here is a runtime result.)
//
// The block below was written against a DIFFERENT npapi.h than the one this adapter used to
// compile with, and the two disagree about NpPalmTouchEvent:
//
// header on our include path BEFORE the what the DEVICE actually pushes
// re-stage (adapter-deps/staging/include/ (libWebKitLuna.so, measured — and what the
// webkit/npapi/npapi.h, sha 462bcc89…1752b) re-staged header now declares)
// int32_t touchCount; // +0 NpPalmTouchList touches; // +0
// NpPalmTouchPoint* touches; // +4 NpPalmTouchList targetTouches; // +8
// int32_t modifiers; // +8 NpPalmTouchList changedTouches; // +16
// sizeof == 12 int32_t modifiers; // +24
// sizeof == 28
//
// How the right column was measured: WebCore::PluginView::handleTouchEvent at 0x4e913c in
// build/webos-oe/device-binaries/libWebKitLuna.so builds THREE variable-length point arrays out of
// TouchEvent+0x6c/+0x70/+0x74 (m_touches, m_targetTouches, m_changedTouches, in declaration order),
// stores each (points,length) pair at NPEvent data +0/+8/+16, writes modifiers=0 at +24, and hands
// that base to dispatchNPEvent. Three independent things agree with it: isis's own adapter
// (../ref-BrowserAdapter) and Atlas's both read event->changedTouches, and the npapi-headers
// checkout our header was staged from carries the 28-byte declaration in its debian INSTALL copy
// (third_party/npapi-headers/debian/npapiheaders/usr/include/npapi.h). The 12-byte declaration is
// the repo-root file of that SAME checkout, so the two copies are out of sync and staging took the
// wrong one.
//
// Enabling this block against the 12-byte header would therefore not merely misplace a touch. It
// reads the device's length word as a pointer and dereferences it INSIDE LunaSysMgr. That is also
// where the "coordinates in excess of 7,000,000" the code below apologises for came from — plus a
// second, independent cause, fixed below: the TouchEnd loop iterated changedTouches.length but
// indexed touches.points, and on a release the touches list is the SHORTER of the two (the lifted
// finger is gone from it), so it read past the end. The guard below turns a re-stage that reverts
// the header into a compile error instead of a shell crash.
//
// Three things that were listed as unsettled, and where they now stand:
// * DOUBLE ACTIVATION. A finger already produces pen events, hence asyncCmdMouseEvent/clickAt, so
// turning touch on hands the page BOTH. Gecko's own suppression cannot help, because our mouse
// events are not synthesized from touch; they arrive on a separate YAP command. HANDLED
// DAEMON-SIDE as of 2026-08-15: GoannaRenderPage::TouchEvent now RETURNS the preventDefault
// result of nsIDOMWindowUtils::sendTouchEventToWindow, and BrowserPageGoanna suppresses the
// synthesized mouse events of a gesture whose touchstart the page consumed. The correlation is
// positional + time-boxed, not by id (there is no shared id) — see the comment on
// BrowserPageGoanna::touchSuppressesMouse for the rule and its failure modes. The suppressor
// needs no switch of its own: it only ever arms when a touch command arrives, which only
// happens when this switch is on.
// * shouldPassTouchEvents() is m_passInputEvents || a non-user-scalable viewport, and
// m_passInputEvents comes from the "usemouseevents" plugin param, which NO variant passes
// (grepped app/, app-mochi/, app-mojo/). So even with JIHAD_TOUCH_EVENTS=1 this fires on
// user-scalable=no pages only. That gate is deliberately KEPT: it is the stock rule, and it
// keeps the untested path off ordinary pages.
// * Stock never did this either. isis AND Atlas both ship doTouchEvent under an
// #ifdef QT_FIXME, so this is new capability, not restored parity — which is the third reason
// the switch defaults to off.
// The per-point "state" written into the touches JSON. THESE VALUES ARE OURS, NOT THE FROZEN
// CONTRACT'S. The field is on the wire, but upstream never shipped the values: they came from
// Palm::TouchPointPalm::State in webkit-supplemental, a header that does not exist in this build.
// The ordering is WebCore::PlatformTouchPoint::State from the reference WebKit tree, which
// TouchPointPalm mirrored. Nothing reads the field today either, since BrowserPageGoanna::touchEvent
// parses only "x" and "y". Do not pass these numbers off as part of the contract.
enum JihadTouchState {
kJihadTouchReleased = 0,
kJihadTouchPressed = 1,
kJihadTouchMoved = 2,
kJihadTouchStationary = 3,
kJihadTouchCancelled = 4
};
// Fails the BUILD, not the device, if the include path still carries the 12-byte
// NpPalmTouchEvent described above. ARM EABI: 3 * 8 + 4, no tail padding. At namespace scope so
// it is checked even in a build where nothing calls doTouchEvent.
typedef char jihadTouchAbiGuard[(sizeof(NpPalmTouchEvent) == 28) ? 1 : -1];
bool BrowserAdapter::doTouchEvent(int32_t type, NpPalmTouchEvent *event)
{
if (!event)
return true;
if (mTouchEventsEnabled && shouldPassTouchEvents()) {
pbnjson::JValue arr = pbnjson::Array();
const int liveCount = event->touches.points ? event->touches.length : 0;
for (int ix = 0; ix < liveCount; ix++) {
Point touchPoint (event->touches.points[ix].xCoord, event->touches.points[ix].yCoord);
JihadTouchState touchState = kJihadTouchStationary;
if (isPointInList(touchPoint, event->changedTouches)) {
if (type == 0) // TouchStart
touchState = kJihadTouchPressed;
else if (type == 1) // TouchMove
touchState = kJihadTouchMoved;
}
Point eventPt(touchPoint.x - m_pageOffset.x, touchPoint.y - (m_pageOffset.y + m_headerHeight));
Point touchDocPt((mScrollPos.x + eventPt.x) / mZoomLevel,
(mScrollPos.y + eventPt.y) / mZoomLevel);
m_touchPtDoc = touchDocPt; // Save this point in case we need it for a TouchReleased event
pbnjson::JValue obj = pbnjson::Object();
obj.put("x", touchDocPt.x);
obj.put("y", touchDocPt.y);
obj.put("state", (int)touchState);
arr.append(obj);
}
// released or cancelled touches are not in the event->touches list
// so we need to add them from the event->changedTouches list
if (type == 2 || type == 3) { // TouchEnd or TouchCancelled
JihadTouchState touchState = type == 2 ? kJihadTouchReleased : kJihadTouchCancelled;
const int liftedCount = event->changedTouches.points ? event->changedTouches.length : 0;
for (int ix = 0; ix < liftedCount; ix++) {
// changedTouches, NOT touches: this loop is bounded by changedTouches.length and
// the point it wants is the lifted one, which is exactly what is missing from the
// touches list. Indexing touches.points here read past the end of the shorter
// array — the actual source of the "7,000,000" coordinates apologised for below.
Point touchPoint (event->changedTouches.points[ix].xCoord,
event->changedTouches.points[ix].yCoord);
Point eventPt(touchPoint.x - m_pageOffset.x, touchPoint.y - (m_pageOffset.y + m_headerHeight));
Point touchDocPt((mScrollPos.x + eventPt.x) / mZoomLevel,
(mScrollPos.y + eventPt.y) / mZoomLevel);
pbnjson::JValue obj = pbnjson::Object();
if ((touchDocPt.x < 0 || touchDocPt.x > mContentWidth)
|| (touchDocPt.y < 0 || touchDocPt.y > mContentHeight)) {
// In testing, it was observed that, for a TouchReleased event, the x and y
// coordinates where absurdly high values in excess of 7,000,000. Therefore,
// if the x and y values are out of range, just set them to the saved value
// in m_touchPtDoc which is more likely to be the point that the user actually
// released their touch of the screen.
touchDocPt = m_touchPtDoc;
}
obj.put("x", touchDocPt.x);
obj.put("y", touchDocPt.y);
obj.put("state", (int)touchState);
arr.append(obj);
}
}
pbnjson::JGenerator ser(NULL);
pbnjson::JSchemaFragment schema("{}");
std::string json;
if (!ser.toString(arr, schema, json)) {
syslog(LOG_DEBUG, "error generating json, dropping event");
} else {
EVENT_TRACEF("touches: %d, json: %s", arr.arraySize(), json.c_str());
asyncCmdTouchEvent(type, arr.arraySize(), event->modifiers, json.c_str());
}
}
// Always true: the card must not act on the touch itself, and returning false here would
// hand it back to WebKit, which would then re-derive the pen events the daemon is busy
// suppressing. Same value the fenced-off version returned.
return true;
}
bool BrowserAdapter::isPointInList(const Point &point, const NpPalmTouchList &list)
{
bool retVal = false;
if (!list.points)
return false;
for (int ix = 0; ix < list.length; ix++) {
if (point.x == list.points[ix].xCoord && point.y == list.points[ix].yCoord) {
retVal = true;
break;
}
}
return retVal;
}
bool BrowserAdapter::handleGesture(NpPalmGestureEvent *event)
{
//EVENT_TRACEF("window (%ld,%ld)\n", mWindow.x, mWindow.y);
EVENT_TRACEF("type %d (%d,%d) c (%d,%d) scale %f rot %f mod 0x%08x",
event->type, event->x, event->y, event->center_x, event->center_y,
event->scale, event->rotate, event->modifiers);
mShowHighlight = false;
stopMouseHoldTimer();
// Any gesture start, whoever handles it (passed to the page or zoomed here), overtakes a
// long-press already asked of the engine (doGestureStart does the same).
if (event->type == npPalmGestureStartEvent)
++m_gestureGen;
// do not handle gestures in page to page transition (page size is zero)
if (mPageWidth == 0 || mPageHeight == 0)
return true;
bool passGestureEvents = m_passInputEvents || (mMetaViewport && !mMetaViewport->userScalable);
switch (event->type) {
case npPalmGestureStartEvent:
if (passGestureEvents)
sendGestureStart(event->x, event->y,
event->scale, event->rotate,
event->center_x, event->center_y);
else
doGestureStart(event->x, event->y,
event->scale, event->rotate,
event->center_x, event->center_y);
break;
case npPalmGestureChangeEvent:
if (passGestureEvents)
sendGestureChange(event->x, event->y,
event->scale, event->rotate,
event->center_x, event->center_y);
else
doGestureChange(event->x, event->y,
event->scale, event->rotate,
event->center_x, event->center_y);
break;
case npPalmGestureEndEvent:
if (passGestureEvents)
sendGestureEnd(event->x, event->y,
event->scale, event->rotate,
event->center_x, event->center_y);
else
doGestureEnd(event->x, event->y,
event->scale, event->rotate,
event->center_x, event->center_y);
break;
case npPalmGestureSingleTapEvent:
// NO click from the gesture. A tap's click is the pen path's (isis's contract):
// handlePenClick starts the double-click timer, clickTimeoutCb asks the daemon for a
// hit test of type "click", msgHitTestResponse lets the card claim the tap through
// eventFired and sends asyncCmdClickAt otherwise. A clickAt dispatched HERE as well
// (added 2026-07-07, when the daemon answered no hit test and "single taps did
// nothing") made every tap TWO clicks once the hit test worked: a checkbox toggled
// twice (cb change=2 checked=0 for one tap, jihad-input-sweep.html), about:addons'
// Disable disabled and re-enabled, and its Preferences opened only when the second
// click lost the race with the view switch (one run in five). Measured 2026-10-25
// with the emulated finger; the Mojo shell had already stood down its own third
// sender for the same reason (2026-10-08). The tap's click now arrives once, after
// kDoubleClickInterval, as it did on the stock browser.
TRACEF("single tap gesture at %d,%d: the pen path's click timer owns it", event->x, event->y);
break;
}
return true;
}
void BrowserAdapter::doGestureStart(int cx, int cy, float scale, float rotate,
int centerX, int centerY)
{
cy -= m_headerHeight;
centerY -= m_headerHeight;
// A pinch starts as one finger down, which armed the long-press timer. The native gesture
// event path (handleGesture) stops it, but the Mojo shell drives a pinch through the JS
// entry points (js_gestureStart), which come straight here — so the timer ran on and the
// hold fired mid-pinch: the engine selected a word and Mojo put its long-press menu up over a
// dimmed page (device report 2026-10-06: "pinch to zoom is broken. the page turns grey and I
// get the copy/select all/reload/find on page menu instead"). Stop it here, for every
// caller, and drop a hold already asked of the engine (the per-request gesture generation).
stopMouseHoldTimer();
++m_gestureGen;
m_recordedGestures.clear();
recordGesture(scale, rotate, centerX, centerY);
// All gestures (scrolls, pinches, etc.) should hide the spelling widget.
asyncCmdHideSpellingWidget();
// break out of flash gesture lock if necessary
updateFlashGestureLockStatus(false);
if (!mCenteredZoom)
mCenteredZoom = new BrowserCenteredZoom;
mCenteredZoom->scale = scale;
mCenteredZoom->centerX = centerX;
mCenteredZoom->centerY = centerY;
mCenteredZoom->scrollX = mScrollPos.x;
// OUR scroll (header strip included): the centre has already had the header taken off
// above, so the point under the fingers is scrollY + centerY - header, as everywhere else
// (taking it off both counted it twice: every Mojo pinch drifted by (scale-1)*header —
// inspector 2026-10-08).
mCenteredZoom->scrollY = mScrollPos.y;
mCenteredZoom->zoomLevel = mZoomLevel;
mZoomFit = false;
mInGestureChange = true;
}
void BrowserAdapter::doGestureChange(int cx, int cy, float scale, float rotate,
int centerX, int centerY, bool isGestureEnd)
{
mInGestureChange = true;
// Scale down the scale value to create a sense of physical resistance
scale = 1.0f + (scale - 1.0f) * 0.6f;
// round to two decimal places to reduce jitter
scale = ::round(scale * 100) / 100;
if (!mCenteredZoom) {
doGestureStart(cx, cy, scale, rotate, centerX, centerY);
}
recordGesture(scale, rotate, centerX, centerY);
RecordedGestureEntry avgGesture = getAveragedGesture();
scale = avgGesture.scale;
rotate = avgGesture.rotate;
centerX = avgGesture.centerX;
centerY = avgGesture.centerY;
cy -= m_headerHeight;
centerY -= m_headerHeight;
mZoomLevel = mCenteredZoom->zoomLevel * scale;
double maxZoom = mMetaViewport ? mMetaViewport->maximumScale : kMaxZoom;
double minZoom = mMetaViewport ? mMetaViewport->minimumScale : 0.0;
double fitZoom = mWindow.width * 1.0 / mPageWidth;
minZoom = MAX(minZoom, fitZoom);
if (!isGestureEnd) {
// Friction for overzooming in either direction.
const double kDamping = 0.2;
if (mZoomLevel > maxZoom)
mZoomLevel = maxZoom + kDamping * (mZoomLevel - maxZoom);
if (mZoomLevel < minZoom)
mZoomLevel = minZoom - kDamping * (minZoom - mZoomLevel);
}
else {
if (mZoomLevel > maxZoom)
mZoomLevel = maxZoom;
if (mZoomLevel < minZoom)
mZoomLevel = minZoom;
}
scale = mZoomLevel / mCenteredZoom->zoomLevel;
// this is the offset after scaling
double x = (scale - 1) * mCenteredZoom->centerX;
// add the scaled scroll offset
x += scale * mCenteredZoom->scrollX;
// now account for the moving center
x += mCenteredZoom->centerX - centerX;
// do the y direction
double y = (scale - 1) * mCenteredZoom->centerY +
scale * mCenteredZoom->scrollY +
mCenteredZoom->centerY - centerY;
mZoomFit = false;
mContentWidth = ::round(mZoomLevel * mPageWidth);
mContentHeight = ::round(mZoomLevel * mPageHeight);
mScrollPos.x = ::round(x);
mScrollPos.y = ::round(y);
mScroller->setContentDimensions(mContentWidth, mContentHeight + m_headerHeight);
mScroller->scrollTo(-mScrollPos.x, -mScrollPos.y, false);
invalidate();
fireScrolledToEvent(-mScrollPos.x, -mScrollPos.y);
}
void BrowserAdapter::doGestureEnd(int cx, int cy, float scale, float rotate,
int centerX, int centerY)
{
doGestureChange(cx, cy, scale, rotate, centerX, centerY, true);
cy -= m_headerHeight;
centerY -= m_headerHeight;
mInGestureChange = false;
delete mCenteredZoom;
mCenteredZoom = 0;
if (mPageWidth) {
double fitZoom = mWindow.width * 1.0 / mPageWidth;
if (PrvIsEqual(mZoomLevel, fitZoom))
mZoomFit = true;
}
asyncCmdSetZoomAndScroll(mZoomLevel, mScrollPos.x, serverScrollY());
}
void BrowserAdapter::sendGestureStart(int cx, int cy, float scale, float rotate,
int center_x, int center_y)
{
// All gestures (scrolls, pinches, etc.) should hide the spelling widget.
asyncCmdHideSpellingWidget();
// break out of flash gesture lock if necessary
updateFlashGestureLockStatus(false);
// do not start pinch zoom in page to page transition (page size is zero)
if (mPageWidth == 0 || mPageHeight == 0) {
return;
}
if (m_passInputEvents || m_spotlightHandle) {
TRACEF("x: %d, y: %d, scale: %f, rotate: %f, centerX: %d, centerY: %d",
cx, cy, scale, rotate, center_x, center_y);
asyncCmdGestureEvent(0, // gesture start
cx, cy, scale, rotate, center_x, center_y);
}
}
void BrowserAdapter::sendGestureChange(int cx, int cy, float scale, float rotate,
int center_x, int center_y)
{
if (m_passInputEvents || m_spotlightHandle) {
TRACEF("x: %d, y: %d, scale: %f, rotate: %f, centerX: %d, centerY: %d",
cx, cy, scale, rotate, center_x, center_y);
asyncCmdGestureEvent(1, // gesture change
cx, cy, scale, rotate, center_x, center_y);
}
}
void BrowserAdapter::sendGestureEnd(int cx, int cy, float scale, float rotate,
int center_x, int center_y)
{
if (m_passInputEvents || m_spotlightHandle) {
TRACEF("x: %d, y: %d, scale: %f, rotate: %f, centerX: %d, centerY: %d",
cx, cy, scale, rotate, center_x, center_y);
asyncCmdGestureEvent(2, // gesture end
cx, cy, scale, rotate, center_x, center_y);
}
}
void BrowserAdapter::sendSingleTap(int cx, int cy, int modifiers)
{
Point docPt((cx + mScrollPos.x) / mZoomLevel,
(cy + mScrollPos.y) / mZoomLevel);
if (mBrowserServerConnected) {
if (flashRectContainsPoint(docPt)) {
// single taps for flash go through if we're not in flash lock mode
if (!flashGestureLock()) {
asyncCmdMouseEvent(0 /*mousedown*/, docPt.x, docPt.y, 1);
asyncCmdMouseEvent(1 /*mouseup*/, docPt.x, docPt.y, 1);
}
} else {
int q = mBsQueryNum++;
HitTestArgs *callArgs = new HitTestArgs("click", docPt, modifiers, q);
m_hitTestArgs.add(callArgs);
TRACEF("x: %d, y: %d, q: %d", docPt.x, docPt.y, q);
asyncCmdHitTest(q, docPt.x, docPt.y);
}
}
}
const char* BrowserAdapter::js_openURL(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 1 || NPVARIANT_IS_STRING(args[0]) == false)
{
return "BrowserAdapter::OpenURL(string): Bad arguments.";
}
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (a->mBrowserServerConnected) {
char *url = NPStringToString(NPVARIANT_TO_STRING(args[0]));
TRACEF("%s\n", url);
a->asyncCmdOpenUrl(url);
::free(url);
delete a->mFrozenSurface;
a->mFrozenSurface = 0;
return NULL;
}
else {
return "BrowserAdapter::openURL: Disconnected from server";
}
}
const char* BrowserAdapter::js_setPageIdentifier(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 1 || !NPVARIANT_IS_STRING(args[0]))
{
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::setPageIdentifier(string): Bad arguments.";
}
char *idStr = NPStringToString(NPVARIANT_TO_STRING(args[0]));
int32_t identifier = atoi(idStr);
TRACEF("%s\n", idStr);
::free(idStr);
BrowserAdapter* pAdapter = static_cast<BrowserAdapter*>(adapter);
pAdapter->setPageIdentifier(identifier);
return NULL;
}
const char* BrowserAdapter::js_pageScaleAndScroll(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if( argCount < 3 )
{
return "BrowserAdapter::pageScaleAndScroll(number, number, number): Bad Arguments";
}
double scaleFactor = VariantToDouble(args[0]);
int32_t scroll_x = VariantToInteger(args[1]);
int32_t scroll_y = VariantToInteger(args[2]);
TRACEF("%f, %d, %d\n", scaleFactor, scroll_x, scroll_y);
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->scaleAndScrollTo(scaleFactor, scroll_x, scroll_y);
return NULL;
}
// FIXME: Remove?
const char* BrowserAdapter::js_setMagnification(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if( argCount < 1 )
{
return "BrowserAdapter::setMagnification(number): Bad Arguments";
}
TRACEF("%f\n", VariantToDouble(args[0]));
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->scale( VariantToDouble(args[0]) );
return 0;
}
const char* BrowserAdapter::js_goBack(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 0) {
return "BrowserAdapter::GoBack(): Bad arguments, expected none.";
}
TRACE;
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
proxy->asyncCmdBack();
return NULL;
}
const char* BrowserAdapter::js_goForward(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 0) {
return "BrowserAdapter::GoForward(): Bad arguments, expected none.";
}
TRACE;
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
proxy->asyncCmdForward();
return NULL;
}
const char* BrowserAdapter::js_clearHistory(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 0) {
return "BrowserAdapter::clearHistory(): Bad arguments, expected none.";
}
TRACE;
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (NULL != proxy) {
proxy->asyncCmdClearHistory();
}
return NULL;
}
const char* BrowserAdapter::js_reloadPage(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 0) {
return "BrowserAdapter::ReloadPage(): Bad arguments, expected none.";
}
TRACE;
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
proxy->asyncCmdReload();
return NULL;
}
const char* BrowserAdapter::js_stopLoad(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 0) {
return "BrowserAdapter::StopLoad(): Bad arguments, expected none.";
}
TRACE;
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
proxy->asyncCmdStop();
return NULL;
}
const char* BrowserAdapter::js_addUrlRedirect(AdapterBase *adapterBase, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if (argCount != 4
|| !NPVARIANT_IS_STRING(args[0])
|| !IsBooleanVariant(args[1])
|| !NPVARIANT_IS_STRING(args[2])
|| !IsIntegerVariant(&args[3])) {
return "addUrlRedirect: invalid args (string, bool, string, int)";
}
int type = VariantToInteger(&args[3]);
if (type < 0 || type > 1) {
return "addUrlRedirect: incorrect type (0 or 1)";
}
char* re = NPStringToString(NPVARIANT_TO_STRING(args[0]));
char* userData = NPStringToString(NPVARIANT_TO_STRING(args[2]));
TRACEF("Redirecting '%s' -> '%s'\n", re, userData);
BrowserAdapter *adapter = GetAndInitAdapter(adapterBase);
regex_t reg = {0};
if (regcomp(&reg, re, REG_EXTENDED | REG_ICASE | REG_NOSUB) != 0) {
char* msg(NULL);
if ( -1 != ::asprintf(&msg, "addUrlRedirect: Can't compile RE '%s'", re) ) {
adapter->ThrowException(msg);
free(msg);
}
else {
adapter->ThrowException("Can't compile RE");
}
}
else {
regfree(&reg);
adapter->asyncCmdAddUrlRedirect(re, type, NPVARIANT_TO_BOOLEAN(args[1]), userData);
// Save for later so that if we reconnect to the server we can resend our redirect info.
adapter->m_urlRedirects.push_back( new UrlRedirectInfo(re, NPVARIANT_TO_BOOLEAN(args[1]), userData, type) );
}
free(re);
free(userData);
return NULL;
}
const char* BrowserAdapter::js_pageFocused(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
// Jihad: this was an empty stub, and three things were built on the belief that it was not.
// A request a card makes over Luna (a bookmark thumbnail, printing, the padlock's
// certificate) cannot say which card it comes from, so the card calls pageFocused(true)
// first and the daemon answers for the page that said so last. With the stub, "last" was
// whichever card last CONNECTED or was given the plugin focus — with two cards open, not
// necessarily the one asking, and the padlock would then have shown another page's
// certificate (review 2026-10-05). The message is the adapter's own existing
// asyncCmdPageFocused; nothing is added to the contract.
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected)
return NULL;
bool focused = true;
if (argCount >= 1 && IsBooleanVariant(args[0]))
focused = VariantToBoolean(args[0]);
a->asyncCmdPageFocused(focused);
return NULL;
}
// deprecated: remove me after change widget_webview.js
const char* BrowserAdapter::js_startDeferWindowChange(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
return NULL;
}
// deprecated: remove me after change widget_webview.js
const char* BrowserAdapter::js_stopDeferWindowChange(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
return NULL;
}
const char* BrowserAdapter::js_setSpotlight(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (argCount != 5 && argCount != 6) {
return "BrowserAdapter::js_setSpotlight(): Bad arguments";
}
int left = VariantToInteger(args[0]);
int top = VariantToInteger(args[1]);
int right = VariantToInteger(args[2]);
int bottom = VariantToInteger(args[3]);
int handle = VariantToInteger(args[4]);
TRACEF("%d, %d, %d, %d, %d\n", left, top, right, bottom, handle);
if (handle == 0) {
return "BrowserAdapter::js_setSpotlight(): Invalid parameter spotlight handle\n";
}
if (argCount == 6) {
proxy->m_spotlightAlpha = (int)NPVARIANT_TO_INT32(args[5]);
} else {
// scrim around spotlight is opaque if no alpha channel argument is provided
proxy->m_spotlightAlpha = 0xff;
}
if (handle != proxy->m_spotlightHandle) {
proxy->m_spotlightHandle = handle;
}
proxy->m_spotlightRect.setX(left);
proxy->m_spotlightRect.setY(top);
proxy->m_spotlightRect.setWidth((right - left));
proxy->m_spotlightRect.setHeight((bottom - top));
proxy->invalidate();
// notify BS/plugin
proxy->asyncCmdPluginSpotlightStart(left, top, right-left, bottom-top);
return NULL;
}
const char* BrowserAdapter::js_removeSpotlight(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (argCount != 0) {
return "BrowserAdapter::js_removeSpotlight(): Bad arguments";
}
TRACE;
if (proxy->m_spotlightHandle) {
proxy->m_spotlightHandle = 0;
proxy->invalidate();
}
// notify BS/plugin
proxy->asyncCmdPluginSpotlightEnd();
return NULL;
}
const char* BrowserAdapter::js_setHTML(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (argCount != 2
|| !NPVARIANT_IS_STRING(args[0])
|| !NPVARIANT_IS_STRING(args[1]))
{
return "BrowserAdapter::js_setHTML(): Bad arguments";
}
TRACE;
char* arg0 =NPStringToString(NPVARIANT_TO_STRING(args[0]));
char* arg1 =NPStringToString(NPVARIANT_TO_STRING(args[1]));
proxy->asyncCmdSetHtml(arg0,arg1);
delete proxy->mFrozenSurface;
proxy->mFrozenSurface = 0;
::free(arg0);
::free(arg1);
return NULL;
}
const char* BrowserAdapter::js_disableEnhancedViewport(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (argCount != 1
|| !IsBooleanVariant(args[0]))
{
return "BrowserAdapter::js_disableEnhancedViewport(): Bad arguments";
}
bool disable = VariantToBoolean(args[0]);
TRACEF("%d\n", disable);
proxy->asyncCmdDisableEnhancedViewport(disable);
return NULL;
}
/**
* See if there is a URL at a certain point and return information about it.
*/
const char* BrowserAdapter::js_inspectUrlAtPoint(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if ( argCount != 3
|| !IsIntegerVariant(args[0])
|| !IsIntegerVariant(args[1])
|| !NPVARIANT_IS_OBJECT(args[2])
) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::inspectUrlAtPoint(x, y, cb): Bad arguments.";
}
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (proxy->mBrowserServerConnected) {
int x = VariantToInteger(args[0]);
int y = VariantToInteger(args[1]);
int nQueryNum = proxy->mBsQueryNum++;
InspectUrlAtPointArgs* inspectArgs = new InspectUrlAtPointArgs(x, y,
NPVARIANT_TO_OBJECT(args[2]), nQueryNum);
TRACEF("%d, %d, %f\n", x, y, proxy->mZoomLevel);
x = (x + proxy->mScrollPos.x) / proxy->mZoomLevel;
y = (y + proxy->mScrollPos.y) / proxy->mZoomLevel;
proxy->m_inspectUrlArgs.add(inspectArgs);
proxy->asyncCmdInspectUrlAtPoint(nQueryNum, x, y);
return NULL;
}
else {
return "Not connected to server";
}
}
const char* BrowserAdapter::js_getImageInfoAtPoint(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if ( argCount != 3
|| !IsIntegerVariant(args[0])
|| !IsIntegerVariant(args[1])
|| !NPVARIANT_IS_OBJECT(args[2])
) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::getImageInfoAtPoint(x, y, cb): Bad arguments.";
}
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (proxy->mBrowserServerConnected) {
int x = VariantToInteger(args[0]);
int y = VariantToInteger(args[1]);
int nQueryNum = proxy->mBsQueryNum++;
GetImageInfoAtPointArgs* callArgs = new GetImageInfoAtPointArgs(x, y,
NPVARIANT_TO_OBJECT(args[2]), nQueryNum);
TRACEF("%d, %d, %f\n", x, y, proxy->mZoomLevel);
x = (x + proxy->mScrollPos.x) / proxy->mZoomLevel;
y = (y + proxy->mScrollPos.y) / proxy->mZoomLevel;
proxy->m_getImageInfoAtPointArgs.add(callArgs);
proxy->asyncCmdGetImageInfoAtPoint(nQueryNum, x, y);
return NULL;
}
else {
return "Not connected to server";
}
}
const char* BrowserAdapter::js_getElementInfoAtPoint(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if ( argCount != 3
|| !IsIntegerVariant(args[0])
|| !IsIntegerVariant(args[1])
|| !NPVARIANT_IS_OBJECT(args[2])
) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::getElementInfoAtPoint(x, y, cb): Bad arguments.";
}
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (proxy->mBrowserServerConnected) {
int x = VariantToInteger(args[0]);
int y = VariantToInteger(args[1]);
int nQueryNum = proxy->mBsQueryNum++;
GetElementInfoAtPointArgs* callArgs = new GetElementInfoAtPointArgs(x, y,
NPVARIANT_TO_OBJECT(args[2]), nQueryNum);
TRACEF("%d, %d, %f\n", x, y, proxy->mZoomLevel);
x = (x + proxy->mScrollPos.x) / proxy->mZoomLevel;
y = (y + proxy->mScrollPos.y) / proxy->mZoomLevel;
proxy->m_getElementInfoAtPointArgs.add(callArgs);
proxy->asyncCmdGetElementInfoAtPoint(nQueryNum, x, y);
return NULL;
}
else {
return "Not connected to server";
}
}
const char* BrowserAdapter::js_ignoreMetaTags(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
TRACE;
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (argCount != 1
|| !IsBooleanVariant(args[0]))
{
return "BrowserAdapter::js_disableEnhancedViewport(): Bad arguments";
}
bool ignore = VariantToBoolean(args[0]);
if (proxy->mBrowserServerConnected) {
proxy->asyncCmdIgnoreMetaTags(ignore);
return NULL;
}
else {
return "Not connected to server";
}
}
const char* BrowserAdapter::js_setNetworkInterface(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
TRACE;
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (argCount != 1
|| !NPVARIANT_IS_STRING(args[0]))
{
return "BrowserAdapter::js_setNetworkInterface(): Bad arguments";
}
char* interfaceName =NPStringToString(NPVARIANT_TO_STRING(args[0]));
proxy->asyncCmdSetNetworkInterface(interfaceName);
::free(interfaceName);
return NULL;
}
const char* BrowserAdapter::js_enableFastScaling(AdapterBase *adapter,
const NPVariant *args, uint32_t argCount, NPVariant *result)
{
TRACE;
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if ((argCount != 1) || !IsBooleanVariant(args[0])) {
return "BrowserAdapter::js_enableFastScaling(): Bad arguments";
}
bool enable = VariantToBoolean(args[0]);
proxy->enableFastScaling(enable);
return NULL;
}
const char* BrowserAdapter::js_setDefaultLayoutWidth(AdapterBase *adapter,
const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if ((argCount == 1 && !IsIntegerVariant(args[0]))
|| (argCount == 2 && (!IsIntegerVariant(args[0]) || !IsIntegerVariant(args[1]))) || argCount > 2) {
return "BrowserAdapter::setDefaultLayoutWidth(int): Bad arguments.";
}
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "Not connected to server.";
}
int layoutWidth = VariantToInteger(args[0]);
int layoutHeight = 0;
if (argCount == 2) {
layoutHeight = VariantToInteger(args[1]);
}
else {
layoutHeight = layoutWidth / a->mViewportWidth * a->mViewportHeight;
}
//TRACEF("%d -> %d", a->m_defaultLayoutWidth, layoutWidth);
//if (layoutWidth != a->m_defaultLayoutWidth) {
a->m_defaultLayoutWidth = layoutWidth;
TRACEF("%d -> %dx%d", a->m_defaultLayoutWidth, layoutWidth, layoutHeight);
a->asyncCmdSetVirtualWindowSize(layoutWidth, layoutHeight);
//}
return NULL;
}
void
BrowserAdapter::msgGetImageInfoAtPointResponse(int32_t queryNum, bool succeeded, const char* baseUri, const char* src,
const char* title, const char* altText, int32_t width, int32_t height, const char* mimeType)
{
TRACEF("%c, '%s'", succeeded ? 'T' : 'F', baseUri);
std::auto_ptr<GetImageInfoAtPointArgs> args(m_getImageInfoAtPointArgs.get(queryNum));
if (!args.get()) {
TRACEF("Can't find response for this call.");
return;
}
NPVariant jsCallResult, jsCallArgs;
NPObject* info = NPN_CreateObject(&ImageInfo::sImageInfoClass);
if (info) {
OBJECT_TO_NPVARIANT(info, jsCallArgs);
static_cast<ImageInfo*>(info)->initialize(succeeded, baseUri, src, title, altText, width, height, mimeType);
if (NPN_InvokeDefault(args->m_callback, &jsCallArgs, 1, &jsCallResult))
TRACEF("getImageInfoAtPoint response call success.");
else
TRACEF("getImageInfoAtPoint response call FAILED.");
AdapterBase::NPN_ReleaseVariantValue(&jsCallArgs);
}
else {
TRACEF("getImageInfoAtPoint: response obj NOT created.");
}
}
const char* BrowserAdapter::js_interactiveAtPoint(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if ( argCount != 3
|| !IsIntegerVariant(args[0])
|| !IsIntegerVariant(args[1])
|| !NPVARIANT_IS_OBJECT(args[2])
) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::isInteractiveAtPoint(x, y, cb): Bad arguments.";
}
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (proxy->mBrowserServerConnected) {
int x = VariantToInteger(args[0]);
int y = VariantToInteger(args[1]);
syslog(LOG_DEBUG, "isInteractiveAtPoint: (%d, %d)\n", x, y);
int nQueryNum = proxy->mBsQueryNum++;
IsInteractiveAtPointArgs* callArgs = new IsInteractiveAtPointArgs(x, y,
NPVARIANT_TO_OBJECT(args[2]), nQueryNum);
TRACEF("%d, %d, %f\n", x, y, proxy->mZoomLevel);
x = (x + proxy->mScrollPos.x) / proxy->mZoomLevel;
y = (y + proxy->mScrollPos.y) / proxy->mZoomLevel;
proxy->m_isInteractiveAtPointArgs.add(callArgs);
proxy->asyncCmdIsInteractiveAtPoint(nQueryNum, x, y);
return NULL;
}
else {
return "Not connected to server";
}
}
void BrowserAdapter::msgIsInteractiveAtPointResponse(int32_t queryNum, bool interactive)
{
TRACEF("'%c'", interactive ? 'T' : 'F');
std::auto_ptr<IsInteractiveAtPointArgs> args(m_isInteractiveAtPointArgs.get(queryNum));
if (!args.get()) {
TRACEF("Can't find response for this call.");
return;
}
NPVariant jsCallResult, jsCallArgs;
NPObject* info = NPN_CreateObject(&InteractiveInfo::sInteractiveInfoClass);
if (info) {
OBJECT_TO_NPVARIANT(info, jsCallArgs);
static_cast<InteractiveInfo*>(info)->initialize(interactive, args->m_x, args->m_y);
if (NPN_InvokeDefault(args->m_callback, &jsCallArgs, 1, &jsCallResult))
TRACEF("isInteractiveAtPoint response call success.");
else
TRACEF("isInteractiveAtPoint response call FAILED.");
AdapterBase::NPN_ReleaseVariantValue(&jsCallArgs);
}
else {
TRACEF("isInteractiveAtPoint: response obj NOT created.");
}
}
/**
* Get the state (canGoBack/canGoForward) for the session history.
*/
const char* BrowserAdapter::js_getHistoryState(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if ( argCount != 1 || !NPVARIANT_IS_OBJECT(args[0])
) {
return "BrowserAdapter::getHistoryState(stateCb): Bad arguments.";
}
TRACE;
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (proxy->mBrowserServerConnected) {
int nQueryNum = proxy->mBsQueryNum++;
GetHistoryStateArgs* callArgs = new GetHistoryStateArgs(NPVARIANT_TO_OBJECT(args[0]), nQueryNum);
proxy->m_historyStateArgs.add(callArgs);
proxy->asyncCmdGetHistoryState(nQueryNum);
return NULL;
}
else {
return "Not connected to server";
}
}
const char* BrowserAdapter::js_isEditing(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if ( argCount != 1 || !NPVARIANT_IS_OBJECT(args[0])) {
return "BrowserAdapter::isEditing(callback): Bad arguments.";
}
TRACE;
BrowserAdapter* proxy = GetAndInitAdapter(adapter);
if (!proxy->mBrowserServerConnected) {
return "Not connected to server";
}
int queryNum = proxy->mBsQueryNum;
proxy->mBsQueryNum++;
IsEditingArgs* callArgs = new IsEditingArgs(NPVARIANT_TO_OBJECT(args[0]), queryNum);
// save async request args with unique query id to avoid overlapping responses
proxy->m_isEditingArgs.add(callArgs);
// send message to BS with query number. BA will then match reply queryNum
// with request queryNum
proxy->asyncCmdIsEditing(queryNum);
return NULL;
}
const char* BrowserAdapter::js_insertStringAtCursor(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 1 || NPVARIANT_IS_STRING(args[0]) == false)
{
return "BrowserAdapter::insertStringAtCursor(string): Bad arguments.";
}
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "BrowserAdapter::insertStringAtCursor: Disconnected from server";
}
char* text = NPStringToString(NPVARIANT_TO_STRING(args[0]));
TRACEF("'%s'\n", text);
a->asyncCmdInsertStringAtCursor(text);
::free(text);
return NULL;
}
/**
* Save the current view to the supplied file in PNG format. Caller is responsible for deleting
* newly generated file.
*
* @param [0] Output file path.
* @param [1] Left view coordinate of clipping rectangle. (optional)
* @param [2] Top view coordinate of clipping rectangle. (optional)
* @param [3] Right view coordinate of clipping rectangle. (optional)
* @param [4] Bottom view coordinate of clipping rectangle. (optional)
*/
const char* BrowserAdapter::js_saveViewToFile(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter *a = GetAndInitAdapter(adapter);
int viewLeft = - a->mScrollPos.x; // raw coords scaled by QPainter in renderToFile
int viewTop = - a->mScrollPos.y;
// Source Rectangle is optional
if ( !((argCount == 1 && NPVARIANT_IS_STRING(args[0])) ||
(argCount == 5 && NPVARIANT_IS_STRING(args[0]) &&
IsIntegerVariant(args[1]) &&
IsIntegerVariant(args[2]) &&
IsIntegerVariant(args[3]) &&
IsIntegerVariant(args[4])))
) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::saveViewToFile(string [, left, top, right, bottom]): Bad arguments.";
}
char* pszOutFile = NPStringToString(NPVARIANT_TO_STRING(args[0]));
if (!isSafeDir(pszOutFile)) {
// A security measure to ensure that only files in /var can be created
::free(pszOutFile);
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::saveViewToFile - Invalid filename";
}
int nImageWidth;
int nImageHeight;
if (argCount == 5) {
viewLeft = VariantToInteger(args[1]);
viewTop = VariantToInteger(args[2]);
int viewRight = VariantToInteger(args[3]);
int viewBottom = VariantToInteger(args[4]);
nImageWidth = viewRight - viewLeft; // size of image
nImageHeight = viewBottom - viewTop;
} else {
// size of viewport if no dimensions are passed via npapi
nImageWidth = a->mViewportWidth;
nImageHeight = a->mViewportHeight;
}
char* pszDirName = ::strdup(pszOutFile);
if (NULL == pszDirName) {
INT32_TO_NPVARIANT( ENOMEM, *result );
return "BrowserAdapter::saveViewToFile - Insufficient space";
}
// Create the output directory (ignore error)
::g_mkdir_with_parents(::dirname(pszDirName), S_IRWXU );
// set the directory ownership and permission so non-root browserserver
// can write to it
{
struct passwd *pw;
pw = getpwnam(BROWSERVER_USER);
if (pw) {
::chown(pszDirName,pw->pw_uid,pw->pw_gid);
}
}
::free(pszDirName);
pszDirName = NULL;
int nErr;
a->syncCmdRenderToFile(pszOutFile, viewLeft, viewTop, nImageWidth, nImageHeight, nErr);
if (nErr) {
char msg[kExceptionMessageLength];
::snprintf(msg, G_N_ELEMENTS(msg), "BrowserAdapter::saveViewToFile - ERROR %d saving view to '%s'", nErr, pszOutFile);
a->ThrowException(msg);
}
::free( pszOutFile );
return NULL;
}
void
BrowserAdapter::msgGetElementInfoAtPointResponse(int32_t queryNum, bool succeeded, const char* element, const char* id,
const char* name, const char* cname, const char* type, int32_t left, int32_t top, int32_t right, int32_t bottom,
bool isEditable)
{
TRACEF("GetElementInfoAtPointResponse: succeeded: %c, element:'%s'", succeeded ? 'T' : 'F', element);
std::auto_ptr<GetElementInfoAtPointArgs> args(m_getElementInfoAtPointArgs.get(queryNum));
if (!args.get()) {
TRACEF("Can't find response for this call.");
return;
}
NPVariant jsCallResult, jsCallArgs;
NPObject* info = NPN_CreateObject(&ElementInfo::sElementInfoClass);
if (info) {
OBJECT_TO_NPVARIANT(info, jsCallArgs);
static_cast<ElementInfo*>(info)->initialize(succeeded, element, id, name, cname, type, left, top, right,
bottom, args->m_x, args->m_y, isEditable);
if (NPN_InvokeDefault(args->m_callback, &jsCallArgs, 1, &jsCallResult))
TRACEF("getElementInfoAtPoint response call success.");
else
TRACEF("getElementInfoAtPoint response call FAILED.");
AdapterBase::NPN_ReleaseVariantValue(&jsCallArgs);
}
else {
TRACEF("getElementInfoAtPoint: response obj NOT created.");
}
}
/**
* Generate an icon from the specified file. This routine will do the following:
*
* 1. Read in the source file.
* 2. Clip the source image using the supplied rectangle.
* 3. Resize the resultant image to 64x64 pixels.
* 4. "Embellish" icon to make it "pretty".
* 5. Write out the new icon to the supplied output file.
*
* @note Both files must be PNG files.
*
* @param [0] Source file path.
* @param [1] Output file path.
* @param [2] Left coordinate of clipping rectangle.
* @param [3] Top coordinate of clipping rectangle.
* @param [4] Right coordinate of clipping rectangle.
* @param [5] Bottom coordinate of clipping rectangle.
*/
const char* BrowserAdapter::js_generateIconFromFile(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 6 ||
!NPVARIANT_IS_STRING(args[0]) ||
!NPVARIANT_IS_STRING(args[1]) ||
!IsIntegerVariant(args[2]) ||
!IsIntegerVariant(args[3]) ||
!IsIntegerVariant(args[4]) ||
!IsIntegerVariant(args[5]) ) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::generateIconFromFile(string, string, number, number, number, number): Bad arguments.";
}
char* pszSrcFile = NPStringToString(NPVARIANT_TO_STRING(args[0]));
char* pszOutFile = NPStringToString(NPVARIANT_TO_STRING(args[1]));
int left = VariantToInteger(args[2]);
int top = VariantToInteger(args[3]);
int right = VariantToInteger(args[4]);
int bottom= VariantToInteger(args[5]);
if (NULL == pszSrcFile || '\0' == *pszSrcFile || NULL == strstr(pszSrcFile, ".png") ) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::generateIconFromFile - bad source file name";
}
if (NULL == pszOutFile || '\0' == *pszOutFile || NULL == strstr(pszOutFile, ".png") ) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::generateIconFromFile - bad output file name";
}
TRACEF("(l:%d, t:%d, r:%d, b:%d), '%s'->'%s'", left, top, right, bottom,
pszSrcFile, pszOutFile);
if (!isSafeDir(pszOutFile)) {
::free(pszOutFile);
::free(pszSrcFile);
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::generateIconFromFile: Invalid filename";
}
char* pszFileName = ::strdup(pszOutFile);
if (NULL == pszFileName) {
INT32_TO_NPVARIANT( ENOMEM, *result );
return "BrowserAdapter::generateIconFromFile: Insufficient space";
}
// Create the output directory (ignore error)
::g_mkdir_with_parents(::dirname(pszFileName), S_IRWXU );
::free(pszFileName);
// Read in the source file
QImage inputPixmap(pszSrcFile);
// nErr = readPngFile(pszSrcFile, context, pInputPixmap, nImageWidth, nImageHeight);
if (right < left)
right = left;
if (right > inputPixmap.width())
right = inputPixmap.width();
if (bottom < top)
bottom = top;
if (bottom > inputPixmap.height())
bottom = inputPixmap.height();
#ifdef QT_FIXME
const int nMargin = 4;// Must agree with pixel data in image files
const int nIconSize = 64;// Width & height of output image
const int nIconWidth = nIconSize-2*nMargin;// Width of icon image within file
const int nIconHeight = nIconSize-2*nMargin;
const PVertex2D iconTL(nMargin, nMargin);
const PVertex2D iconBR(nMargin + nIconWidth, nMargin + nIconHeight);
PPixmap* pFinalPixmap(NULL);
if (!nErr) {
pFinalPixmap = context.CreatePixmap(nIconSize, nIconSize);
if (NULL != pFinalPixmap) {
pFinalPixmap->Set (PFORMAT_8888, NULL, 0, true /*allow access*/);
pFinalPixmap->SetQuality(PSBilinear);
context.RenderToTexture(pFinalPixmap);
context.Clear(QColor(0x00,0x00,0x00,0x0));
const PVertex2D srcTL(left, top);
const PVertex2D srcBR(right, bottom);
context.DrawSubPixmap(inputPixmap, iconTL, iconBR, srcTL, srcBR);
}
else {
nErr = ENOMEM;
}
}
// Mask the icon to give it the rounded corners.
if (!nErr) {
PPixmap* pMaskPixmap(NULL);
int nWidth(0), nHeight(0);
nErr = readPngFile(kIconMaskFile, context, pMaskPixmap, nWidth, nHeight);
if (!nErr) {
assert( nWidth == nIconSize );
int nNumPixels = nWidth * nHeight;
uint32_t* pImgData = static_cast<uint32_t*>(pFinalPixmap->GetBuffer());
const uint32_t* pMaskData = static_cast<uint32_t*>(pMaskPixmap->GetBuffer());
while (nNumPixels--) {
*pImgData = (*pImgData & 0x00ffffff) | (*pMaskData & 0xff000000);
pImgData++;
pMaskData++;
}
}
if (NULL != pMaskPixmap)
context.DestroyPixmap(pMaskPixmap);
}
// Overlay the icon to give it a drop shadow and a slight 3D look.
if (!nErr) {
PPixmap* pOverlayPixmap(NULL);
int nWidth(0), nHeight(0);
nErr = readPngFile(kIconOverlayFile, context, pOverlayPixmap, nWidth, nHeight);
if (!nErr)
nErr = preScaleImage(pFinalPixmap, nWidth, nHeight);
if (!nErr)
nErr = preScaleImage(pOverlayPixmap, nWidth, nHeight);
if (!nErr)
context.DrawPixmap(pOverlayPixmap, PVertex2D(0,0), PVertex2D(nIconSize,nIconSize));
if (NULL != pOverlayPixmap)
context.DestroyPixmap(pOverlayPixmap);
}
if (!nErr) {
nErr = writePngFile(pszOutFile, static_cast<const uint32_t*>(pFinalPixmap->GetBuffer()), nIconSize, nIconSize);
}
if (nErr) {
char msg[kExceptionMessageLength];
::snprintf(msg, G_N_ELEMENTS(msg), "ERROR %d generating icon '%s'", nErr, pszOutFile);
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->ThrowException(msg);
}
::free( pszSrcFile );
::free( pszOutFile );
#endif
return NULL;
}
const char* BrowserAdapter::js_deleteImage(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if( !(argCount == 1 && NPVARIANT_IS_STRING(args[0])) ) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::deleteImage(string): Bad arguments.";
}
char* pszFile = NPStringToString(NPVARIANT_TO_STRING(args[0]));
// For security reasons I only delete PNG files.
if (NULL == pszFile || '\0' == *pszFile || NULL == strstr(pszFile, ".png") ) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
TRACEF("%s: bad file name '%s'", __FUNCTION__, pszFile);
return "BrowserAdapter::deleteImage - bad file name";
}
TRACEF("Deleting '%s'", pszFile);
int nErr = 0;
if (::unlink(pszFile) == -1) {
nErr = errno;
}
if (nErr) {
char msg[kExceptionMessageLength];
::snprintf(msg, G_N_ELEMENTS(msg), "ERROR %d deleting '%s'", nErr, pszFile);
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->ThrowException(msg);
}
::free(pszFile);
return NULL;
}
const char* BrowserAdapter::js_resizeImage(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
#ifdef QT_FIXME
if( !(argCount == 4 &&
NPVARIANT_IS_STRING(args[0]) &&
NPVARIANT_IS_STRING(args[1]) &&
IsIntegerVariant(args[2]) &&
IsIntegerVariant(args[3])) ) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::resizeImage(string, string, number, number): Bad arguments.";
}
int nErr = 0;
char* pszSrcFile = NPStringToString(NPVARIANT_TO_STRING(args[0]));
char* pszDstFile = NPStringToString(NPVARIANT_TO_STRING(args[1]));
int nDstWidth = VariantToInteger(args[2]);
int nDstHeight = VariantToInteger(args[3]);
TRACEF("Resizing '%s'->'%s' %d x %d\n", pszSrcFile, pszDstFile, nDstWidth, nDstHeight);
int nSrcWidth(0), nSrcHeight(0);
PPixmap* pSrcPixmap(NULL);
PContext2D context;
nErr = readPngFile(pszSrcFile, context, pSrcPixmap, nSrcWidth, nSrcHeight);
PPixmap* pFinalPixmap(NULL);
if (!nErr) {
pFinalPixmap = context.CreatePixmap(nDstWidth, nDstHeight);
if (NULL != pFinalPixmap) {
pFinalPixmap->Set (PFORMAT_8888, NULL, 0, true /*allow access*/);
PFilters filters;
filters.DownSample(pFinalPixmap, pSrcPixmap);
}
else {
nErr = ENOMEM;
}
}
if (!nErr) {
nErr = writePngFile(pszDstFile, static_cast<const uint32_t*>(pFinalPixmap->GetBuffer()), nDstWidth, nDstHeight);
}
if (NULL != pSrcPixmap) {
context.DestroyPixmap(pSrcPixmap);
}
if (NULL != pFinalPixmap) {
context.DestroyPixmap(pFinalPixmap);
}
if (nErr) {
char msg[kExceptionMessageLength];
::snprintf(msg, G_N_ELEMENTS(msg), "ERROR %d resizing '%s'", nErr, pszSrcFile);
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->ThrowException(msg);
}
::free(pszSrcFile);
::free(pszDstFile);
#endif
return NULL;
}
/**
* Calculate where to smart zoom into a page.
*/
const char* BrowserAdapter::js_smartZoom(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 2 || IsIntegerVariant(args[0]) == false || IsIntegerVariant(args[1]) == false) {
return "BrowserAdapter::smartZoom(int, int): Bad arguments.";
}
TRACEF("%d, %d\n", VariantToInteger(args[0]), VariantToInteger(args[1]));
BrowserAdapter *a = GetAndInitAdapter(adapter);
// The point is in the plugin's own pixels: add the scroll (and take off the header strip)
// before dividing by the zoom, as the native double-tap (handlePenDoubleClick) and clickAt
// do. It was divided straight, which only held while the Mojo WebView sized this plugin to
// the whole page (its scroll was then always 0).
Point docPt((a->mScrollPos.x + VariantToInteger(args[0])) / a->mZoomLevel,
(a->mScrollPos.y + VariantToInteger(args[1]) - a->m_headerHeight) / a->mZoomLevel);
bool inFlashRect = a->flashRectContainsPoint(docPt);
if (a->flashGestureLock()) {
if (!inFlashRect) {
a->updateFlashGestureLockStatus(false);
} else {
/* ignore; taps will be sent through with handle* methods */
return NULL;
}
} else if (inFlashRect) {
a->updateFlashGestureLockStatus(true);
}
a->prvSmartZoom(docPt);
return NULL;
}
const char* BrowserAdapter::js_setViewportSize(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if (argCount != 2 || IsIntegerVariant(args[0]) == false || IsIntegerVariant(args[1]) == false) {
return "BrowserAdapter::setViewportSize(int, int): Bad arguments.";
}
BrowserAdapter *a = GetAndInitAdapter(adapter);
int newWidth = VariantToInteger(args[0]);
int newHeight = VariantToInteger(args[1]);
if (newWidth == 0 || newHeight == 0) {
return "BrowserAdapter::setViewportSize(int, int): cannot set to 0";
}
TRACEF("%dx%d -> %dx%d", a->mViewportWidth, a->mViewportHeight,
newWidth, newHeight);
if (a->mViewportWidth != newWidth || a->mViewportHeight != newHeight) {
a->mViewportWidth = newWidth;
a->mViewportHeight = newHeight;
a->asyncCmdSetWindowSize(a->mViewportWidth, a->mViewportHeight);
}
return NULL;
}
/**
* Set the current list of DNS server to use for lookups
*
* @param [0] Comma seperated list of DNS servers.
*/
const char* BrowserAdapter::js_setDNSServers(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
TRACE;
if (argCount != 1 || !NPVARIANT_IS_STRING(args[0])) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::js_setDNSServers(string): Bad argument.";
}
char *inServers = NPStringToString(NPVARIANT_TO_STRING(args[0]));
TRACEF("Setting DNS servers: %s", inServers);
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->asyncCmdSetDNSServers(inServers);
::free(inServers);
return NULL;
}
const char* BrowserAdapter::js_setMinFontSize(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 1 || IsIntegerVariant(args[0]) == false) {
return "BrowserAdapter::setMinFontSize(int): Bad arguments.";
}
int32_t minFontSize = VariantToInteger(args[0]);
TRACEF("Setting minFontSize: %d", minFontSize);
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->asyncCmdSetMinFontSize(minFontSize);
return NULL;
}
const char* BrowserAdapter::js_interrogateClicks(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 1 || IsBooleanVariant(args[0]) == false) {
return "BrowserAdapter::interrogateClicks(int): Bad arguments.";
}
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->m_interrogateClicks = VariantToBoolean(args[0]);
TRACEF("Interrogate clicks: %c", a->m_interrogateClicks ? 'Y' : 'N');
a->asyncCmdInterrogateClicks(a->m_interrogateClicks);
return NULL;
}
const char* BrowserAdapter::js_clickAt(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if (argCount != 3 || IsIntegerVariant(args[0]) == false ||
IsIntegerVariant(args[1]) == false || IsIntegerVariant(args[2]) == false) {
return "BrowserAdapter::js_clickAt(int, int): Bad arguments.";
}
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (a->m_passInputEvents || a->m_spotlightHandle) {
return NULL;
}
Point clickPt(VariantToInteger(args[0]), VariantToInteger(args[1]) - a->m_headerHeight);
int counter = VariantToInteger(args[2]);
TRACEF("%d, %d, %d", clickPt.x, clickPt.y, counter);
struct timeval tv, elapsed;
gettimeofday(&tv, NULL);
timersub(&tv, &a->m_lastPassedEventTime, &elapsed);
if (elapsed.tv_sec == 0 && elapsed.tv_usec < (50 * 1000)) {
return NULL;
}
// contentX/contentY = (scroll + event) / zoom — the pen paths all add mScrollPos; this
// js-driven clickAt dropped it, so on a scrolled page it hit the wrong element (Jihad
// review #7). Add the scroll offset before dividing by zoom.
clickPt.x = (a->mScrollPos.x + clickPt.x) / a->mZoomLevel;
clickPt.y = (a->mScrollPos.y + clickPt.y) / a->mZoomLevel;
/* filter clicks out in flash rects when gesture lock enabled since we
* send the events directly via handlePen* */
if (a->flashGestureLock() && a->flashRectContainsPoint(clickPt)) {
return NULL;
}
if (a->m_sentMouseHoldEvent.sent &&
a->m_sentMouseHoldEvent.pt == clickPt) {
a->m_sentMouseHoldEvent.reset();
return NULL;
}
a->m_sentMouseHoldEvent.reset();
a->asyncCmdClickAt(clickPt.x, clickPt.y, 1, counter);
return NULL;
}
/**
* Send a response for a popup menu selection.
*
* @param param[0] Numerical index of selected menu item. Pass a negative value to indicate that no selection was
* made.
*/
const char* BrowserAdapter::js_selectPopupMenuItem(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
TRACE;
if (argCount != 2 || !NPVARIANT_IS_STRING(args[0]) || !IsIntegerVariant(args[1]))
return "BrowserAdapter::js_selectPopupMenuItem(string, double): Bad arguments.";
char *identifier = NPStringToString(NPVARIANT_TO_STRING(args[0]));
int32_t selIdx = VariantToInteger(args[1]);
TRACEF("Sending popup menu selection: %d\n", selIdx);
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->asyncCmdPopupMenuSelect(identifier, selIdx);
::free(identifier);
return NULL;
}
/**
* Send a response to a Javascript dialog. The first parameter is the sync pipe path followed by the
* return values.
*/
const char* BrowserAdapter::js_sendDialogResponse(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
bool paramsOkay(false);
uint32_t msgLen = 0, i;
TRACE;
// Verify the arguments
// 0 = "1" for Okay, "0" for "Cancel"
// 1, 2 (optional) = prompt response or username & password
if (argCount >= 1 && argCount <= 3) {
paramsOkay = true;
for (i=0; i< argCount; i++)
if (NPVARIANT_IS_STRING(args[i]) == false)
paramsOkay = false;
else {
msgLen += 1 + strlen(NPStringToString(NPVARIANT_TO_STRING(args[i])));
}
}
if (!paramsOkay) {
return "BrowserAdapter::js_sendDialogResponse(string, string, ...): Bad arguments";
}
if (strlen(gDialogResponsePipe) == 0) {
return "BrowserAdapter::js_sendDialogResponse(string, string, ...): Invalid state";
}
FILE* f = fopen(gDialogResponsePipe, "wb" );
if (f) {
// Write the length of the response (in FIFO format)
const char *buf = (const char *) &msgLen;
fwrite(buf+3, 1, 1, f);
fwrite(buf+2, 1, 1, f);
fwrite(buf+1, 1, 1, f);
fwrite(buf, 1, 1, f);
// Write the arguments out
for (i=0; i< argCount; i++) {
char *str = NPStringToString(NPVARIANT_TO_STRING(args[i]));
fwrite(str, sizeof(char), strlen(str)+1, f);
::free(str);
}
fclose(f);
}
memset(gDialogResponsePipe, 0, G_N_ELEMENTS(gDialogResponsePipe));
return NULL;
}
const char* BrowserAdapter::js_gestureStart(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (argCount != 6 ||
!IsIntegerVariant(args[0]) ||
!IsIntegerVariant(args[1]) ||
!IsDoubleVariant(args[2]) ||
!IsDoubleVariant(args[3]) ||
!IsIntegerVariant(args[4]) ||
!IsIntegerVariant(args[5])) {
return "BrowserAdapter::js_gestureStart(6*double): Bad arguments.";
}
TRACEF("contentX: %d, contentY: %d, zoomScale: %f, rotation: %f, centerX: %d, centerY: %d",
(int)(VariantToInteger(args[0]) / a->mZoomLevel), //contentX
(int)(VariantToInteger(args[1]) / a->mZoomLevel), //contentY
VariantToDouble(args[2]), // zoomScale
VariantToDouble(args[3]), // rotation
(int)(VariantToInteger(args[4]) / a->mZoomLevel), // centerX
(int)(VariantToInteger(args[5]) / a->mZoomLevel));
// The pinch centre is in the plugin's own (window) pixels, as the native gesture event's is
// (handleGesture passes event->center_x straight through): dividing it by the zoom, as this
// entry point used to, anchored a JS-driven pinch at the wrong point whenever the page was
// not at 1:1. The Mojo shell drives every pinch through here since 2026-10-07.
a->doGestureStart(VariantToInteger(args[0]), //x
VariantToInteger(args[1]), //y
VariantToDouble(args[2]), // zoomScale
VariantToDouble(args[3]), // rotation
VariantToInteger(args[4]), // centerX
VariantToInteger(args[5])); // centerY
return NULL;
}
const char* BrowserAdapter::js_gestureChange(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (argCount != 6 ||
!IsIntegerVariant(args[0]) ||
!IsIntegerVariant(args[1]) ||
!IsDoubleVariant(args[2]) ||
!IsDoubleVariant(args[3]) ||
!IsIntegerVariant(args[4]) ||
!IsIntegerVariant(args[5])) {
return "BrowserAdapter::js_gestureChange(6*double): Bad arguments.";
}
TRACEF("contentX: %d, contentY: %d, zoomScale: %f, rotation: %f, centerX: %d, centerY: %d",
(int)(VariantToInteger(args[0]) / a->mZoomLevel), //contentX
(int)(VariantToInteger(args[1]) / a->mZoomLevel), //contentY
VariantToDouble(args[2]), // zoomScale
VariantToDouble(args[3]), // rotation
(int)(VariantToInteger(args[4]) / a->mZoomLevel), // centerX
(int)(VariantToInteger(args[5]) / a->mZoomLevel));
a->doGestureChange(VariantToInteger(args[0]), //x
VariantToInteger(args[1]), //y
VariantToDouble(args[2]), // zoomScale
VariantToDouble(args[3]), // rotation
VariantToInteger(args[4]), // centerX (window px; see js_gestureStart)
VariantToInteger(args[5])); // centerY
return NULL;
}
const char* BrowserAdapter::js_gestureEnd(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (argCount != 6 ||
!IsIntegerVariant(args[0]) ||
!IsIntegerVariant(args[1]) ||
!IsDoubleVariant(args[2]) ||
!IsDoubleVariant(args[3]) ||
!IsIntegerVariant(args[4]) ||
!IsIntegerVariant(args[5])) {
return "BrowserAdapter::js_gestureChange(6*double): Bad arguments.";
}
TRACEF("contentX: %d, contentY: %d, zoomScale: %f, rotation: %f, centerX: %d, centerY: %d",
(int)(VariantToInteger(args[0]) / a->mZoomLevel), //contentX
(int)(VariantToInteger(args[1]) / a->mZoomLevel), //contentY
VariantToDouble(args[2]), // zoomScale
VariantToDouble(args[3]), // rotation
(int)(VariantToInteger(args[4]) / a->mZoomLevel), // centerX
(int)(VariantToInteger(args[5]) / a->mZoomLevel));
a->doGestureEnd(VariantToInteger(args[0]), //x
VariantToInteger(args[1]), //y
VariantToDouble(args[2]), // zoomScale
VariantToDouble(args[3]), // rotation
VariantToInteger(args[4]), // centerX (window px; see js_gestureStart)
VariantToInteger(args[5])); // centerY
return NULL;
}
const char* BrowserAdapter::js_scrollStarting(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter* a = static_cast<BrowserAdapter*>(adapter);
TRACE;
a->mInScroll = true;
return NULL;
}
const char* BrowserAdapter::js_scrollEnding(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter* a = static_cast<BrowserAdapter*>(adapter);
TRACE;
a->mInScroll = false;
return NULL;
}
const char* BrowserAdapter::js_setEnableJavaScript(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 1 || IsBooleanVariant(args[0]) == false) {
return "BrowserAdapter::js_setEnableJavaScript(bool): Bad arguments.";
}
bool enable = VariantToBoolean(args[0]);
TRACEF("Enable JavaScript: %c", enable ? 'Y' : 'N');
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->mEnableJavaScript = enable;
a->asyncCmdSetEnableJavaScript(enable);
return NULL;
}
const char* BrowserAdapter::js_setBlockPopups(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 1 || IsBooleanVariant(args[0]) == false) {
return "BrowserAdapter::js_setBlockPopups(bool): Bad arguments.";
}
bool enable = VariantToBoolean(args[0]);
TRACEF("Block Popups: %c", enable ? 'Y' : 'N');
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->mBlockPopups = enable;
a->asyncCmdSetBlockPopups(enable);
return NULL;
}
const char* BrowserAdapter::js_setHeaderHeight(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 1 || !IsIntegerVariant(args[0])) {
return "BrowserAdapter::js_setHeaderHeight(number): Bad arguments.";
}
int height = VariantToInteger(args[0]);
TRACEF("HeaderHeight: %d", height);
BrowserAdapter *a = GetAndInitAdapter(adapter);
const int old = a->m_headerHeight;
if (height < 0) height = 0;
a->m_headerHeight = height;
if (height != old) {
// The scroll range includes the strip, so it changes with it (the Mojo find bar opens
// and closes it — inspector 2026-10-08: the page's last strip-height of rows was out of
// reach with the bar open, and white past the end after it closed). A page shown below
// a visible header moves down with it; one scrolled under it stays where it is.
if (a->mScrollPos.y > old) a->mScrollPos.y += height - old;
const int maxY = MAX(a->mContentHeight + height - (int) a->mViewportHeight, 0);
a->mScrollPos.y = MAX(0, MIN(a->mScrollPos.y, maxY));
if (a->mScroller) {
a->mScroller->setContentDimensions(a->mContentWidth, a->mContentHeight + height);
a->mScroller->scrollTo(-a->mScrollPos.x, -a->mScrollPos.y, false);
}
if (a->mBrowserServerConnected)
a->asyncCmdSetScrollPosition(a->mScrollPos.x, a->serverScrollY(),
a->mScrollPos.x + a->mWindow.width,
a->serverScrollY() + a->mWindow.height);
a->fireScrolledToEvent(-a->mScrollPos.x, -a->mScrollPos.y);
}
a->invalidate();
return NULL;
}
const char* BrowserAdapter::js_setAcceptCookies(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 1 || IsBooleanVariant(args[0]) == false) {
return "BrowserAdapter::js_setAcceptCookies(bool): Bad arguments.";
}
bool enable = VariantToBoolean(args[0]);
TRACEF("Accept Cookies: %c", enable ? 'Y' : 'N');
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->mAcceptCookies = enable;
a->asyncCmdSetAcceptCookies(enable);
return NULL;
}
const char* BrowserAdapter::js_setShowClickedLink(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if(argCount != 1 || IsBooleanVariant(args[0]) == false) {
return "BrowserAdapter::js_setShowClickedLink(bool): Bad arguments.";
}
bool enable = VariantToBoolean(args[0]);
TRACEF("Show Clicked Link: %c", enable ? 'Y' : 'N');
BrowserAdapter *a = GetAndInitAdapter(adapter);
a->mShowClickedLink = enable;
a->asyncCmdSetShowClickedLink(enable);
return NULL;
}
/*
* @brief requests from webkit array of rectangles that cover an interactive
* node at given coordinates
*
*/
const char* BrowserAdapter::js_addElementHighlight(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
/* FIXME: RR
if (argCount !=2
|| !IsIntegerVariant(args[0])
|| !IsIntegerVariant(args[1])) {
return "BrowserAdapter::js_addElementHighlight(double, double): Bad arguments.";
}
BrowserAdapter* a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "BrowserAdapter::connectBrowserServer Cannot connect to server.";
}
int rawX = VariantToInteger(args[0]);
int rawY = VariantToInteger(args[1]);
int scaledX = (rawX - a->mJsScrollX) / a->mZoomLevel;
int scaledY = (rawY - a->mJsScrollY) / a->mZoomLevel;
TRACEF("%d, %d (jsscroll %d, %d) %f\n",
rawX, rawY, a->mJsScrollX, a->mJsScrollY, a->mZoomLevel);
// BS will get rectangles from webkit and will send them to BA as a json array
a->asyncCmdGetInteractiveNodeRects(scaledX, scaledY);
a->mShowHighlight = true;
*/
return NULL;
}
/**
* @brief calls removeHighlight() to clear vector of highlight rects and mark
* rect regions as dirty.
*
*/
const char* BrowserAdapter::js_removeElementHighlight(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
TRACE;
BrowserAdapter* a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "BrowserAdapter::connectBrowserServer Cannot connect to server.";
}
a->removeHighlight();
return NULL;
}
const char* BrowserAdapter::js_enableSelectionMode(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
/* FIXME: RR
if (argCount !=2
|| !IsIntegerVariant(args[0])
|| !IsIntegerVariant(args[1])) {
return "BrowserAdapter::js_enableSelectionMode(double, double): Bad arguments.";
}
BrowserAdapter* a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "BrowserAdapter::connectBrowserServer Cannot connect to server.";
}
int rawX = VariantToInteger(args[0]);
int rawY = VariantToInteger(args[1]);
int scaledX = (rawX - a->mJsScrollX) / a->mZoomLevel;
int scaledY = (rawY - a->mJsScrollY) / a->mZoomLevel;
TRACEF("%d, %d (jsscroll %d, %d) %f\n",
rawX, rawY, a->mJsScrollX, a->mJsScrollY, a->mZoomLevel);
a->mSelectionReticle.x = scaledX;
a->mSelectionReticle.y = scaledY;
a->mSelectionReticle.show = true;
a->invalidateSelectionReticleRegion();
a->asyncCmdEnableSelection(scaledX, scaledY);
*/
return NULL;
}
const char* BrowserAdapter::js_disableSelectionMode(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter* a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "BrowserAdapter::disableSelectionMode Cannot connect to server.";
}
TRACE;
a->asyncCmdDisableSelection();
return NULL;
}
const char* BrowserAdapter::js_selectAll(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter* a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "BrowserAdapter::selectAll Cannot connect to server.";
}
TRACE;
a->asyncCmdSelectAll();
return NULL;
}
const char* BrowserAdapter::js_cut(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter* a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "BrowserAdapter::cut Cannot connect to server.";
}
a->asyncCmdCut();
return NULL;
}
const char* BrowserAdapter::js_copy(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter* proxy = GetAndInitAdapter(adapter);
if (!proxy->mBrowserServerConnected) {
return "BrowserAdapter::copy Cannot connect to server.";
}
// if we have one argument, it must be a callback
if (argCount > 1 || (argCount == 1 && !NPVARIANT_IS_OBJECT(args[0]))) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::js_copy(callback): Bad arguments.";
}
// allow the user to not pass a callback, though we must still use the
// callback query mechanism. we will just use a null NPObject
// that is ignored by BA when BS sends it a copy complete message
int queryNum = proxy->mBsQueryNum;
proxy->mBsQueryNum++;
NPObject* callback = NULL;
if (argCount == 1) {
callback = NPVARIANT_TO_OBJECT(args[0]);
}
CopySuccessCallbackArgs* callbackArgs = new CopySuccessCallbackArgs(callback, queryNum);
proxy->m_copySuccessCallbackArgs.add(callbackArgs);
proxy->asyncCmdCopy(queryNum);
return NULL;
}
const char* BrowserAdapter::js_paste(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter* a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "BrowserAdapter::paste Cannot connect to server.";
}
a->asyncCmdPaste();
return NULL;
}
const char* BrowserAdapter::js_setMouseMode(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if (argCount != 1 || !IsIntegerVariant(&args[0]) ) {
return "setMouseMode(number): Bad arguments.";
}
int mode = VariantToInteger(&args[0]);
if (mode < 0 || mode > 1) {
return "invalid mode.";
}
BrowserAdapter* a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "Cannot connect to server.";
}
TRACEF("%d\n", mode);
a->asyncCmdSetMouseMode(mode);
a->mMouseMode = mode;
return NULL;
}
const char* BrowserAdapter::js_clearSelection(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter* a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "BrowserAdapter::clearSelection Cannot connect to server.";
}
TRACE;
a->asyncCmdClearSelection();
return NULL;
}
const char* BrowserAdapter::js_mouseEvent(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if (argCount != 4 ||
!IsIntegerVariant(args[0]) ||
!IsIntegerVariant(args[1]) ||
!IsIntegerVariant(args[2]) ||
!IsIntegerVariant(args[3])) {
return "BrowserAdapter::js_mouseEvent(number, number, number, number): Bad arguments.";
}
BrowserAdapter *a = GetAndInitAdapter(adapter);
TRACEF("%d, %d, %d, %d, %f\n",
VariantToInteger(args[0]),
VariantToInteger(args[1]),
VariantToInteger(args[2]),
VariantToInteger(args[3]), a->mZoomLevel);
gettimeofday(&a->m_lastPassedEventTime, NULL);
a->asyncCmdMouseEvent(VariantToInteger(args[0]), // up, down, drag
VariantToInteger(args[1]) / a->mZoomLevel,
VariantToInteger(args[2]) / a->mZoomLevel,
VariantToInteger(args[3]));
return NULL;
}
/**
* @brief Called by app when it believes BA is not connected to BS
*/
const char* BrowserAdapter::js_connectBrowserServer(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
TRACE;
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "BrowserAdapter::connectBrowserServer Cannot connect to server.";
}
return NULL; // success
}
/**
* @brief Called by our host when they want the adapter to disconnect from the BrowserServer.
*
* @note Does not fire the browserServerDisconnected event.
*/
const char* BrowserAdapter::js_disconnectBrowserServer(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
BrowserAdapter *a = GetAndInitAdapter(adapter);
TRACE;
a->asyncCmdDisconnect();
return NULL;
}
/**
* Prints the specified frame. WebKit will render the frame to image file(s) and
* notify the Print Manager Luna Service to send them to the printer.
*
* @param args[0] string
* The name of the frame to print. If not specified, WebKit will render the main frame.
* @param args[1] int
* The job ID associated with this print
* @param args[2] int
* The printable width of the page in pixels
* @param args[3] int
* The printable height of the page in pixels
* @param args[4] int
* The print Dpi
* @param args[5] bool
* The orientation of the page, TRUE=landscape, FALSE=portrait
* @param args[6] bool
* The order to print pages, TRUE=last-to-first, FALSE=first-to-last
*
*/
const char* BrowserAdapter::js_printFrame(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if (argCount != 7) {
return "BrowserAdapter::printFrame: Bad arguments.";
}
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (!a->mBrowserServerConnected) {
return "Not connected to server.";
}
char* frameName = NPStringToString(NPVARIANT_TO_STRING(args[0]));
int lpsJobId = VariantToInteger(args[1]);
int width = VariantToInteger(args[2]);
int height = VariantToInteger(args[3]);
int dpi = VariantToInteger(args[4]);
bool landscape = VariantToBoolean(args[5]);
bool reverseOrder = VariantToBoolean(args[6]);
a->asyncCmdPrintFrame(frameName,lpsJobId,width,height,dpi,landscape,reverseOrder);
::free(frameName);
return NULL;
}
const char* BrowserAdapter::js_skipPaintHack(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
return NULL;
}
const char* BrowserAdapter::js_findInPage(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if ((argCount != 1) || NPVARIANT_IS_STRING(args[0]) == false) {
return "BrowserAdapter::findInPage(string): Bad arguments.";
}
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (a->mBrowserServerConnected) {
char *str = NPStringToString(NPVARIANT_TO_STRING(args[0]));
TRACEF("%s\n", str);
a->asyncCmdFindString(str, true);
::free(str);
return NULL;
}
else {
return "BrowserAdapter::openURL: Disconnected from server";
}
}
const char* BrowserAdapter::js_mouseHoldAt(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if (argCount != 2 || IsIntegerVariant(args[0]) == false ||
IsIntegerVariant(args[1]) == false) {
return "BrowserAdapter::js_mouseHoldAt(int, int): Bad arguments.";
}
BrowserAdapter *a = GetAndInitAdapter(adapter);
if (a->m_passInputEvents || a->m_spotlightHandle) {
return NULL;
}
Point docPt(VariantToInteger(args[0]) / a->mZoomLevel,
VariantToInteger(args[1]) / a->mZoomLevel);
if (a->flashRectContainsPoint(docPt)) {
return NULL;
}
a->m_sentMouseHoldEvent.set(docPt.x, docPt.y);
a->asyncCmdHoldAt(docPt.x, docPt.y);
return NULL;
}
const char* BrowserAdapter::js_handleFlick(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if (argCount != 2 || !IsIntegerVariant(args[0]) || !IsIntegerVariant(args[1])) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::handleFlick(): Bad arguments.";
}
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (proxy && !proxy->mScrollableLayerScrollSession.isActive)
proxy->mScroller->handleMouseFlick(VariantToInteger(args[0]),
VariantToInteger(args[1]));
return NULL;
}
const char* BrowserAdapter::js_setVisibleSize(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if (argCount != 2 || !IsIntegerVariant(args[0]) || !IsIntegerVariant(args[1])) {
INT32_TO_NPVARIANT(kInvalidParam, *result);
return "BrowserAdapter::setVisibleSize(int, int): Bad arguments.";
}
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
int oldwidth = proxy->mViewportWidth;
int oldheight = proxy->mViewportHeight;
proxy->mViewportWidth = VariantToInteger(args[0]);
proxy->mViewportHeight = VariantToInteger(args[1]);
TRACEF("new size %dx%d", proxy->mViewportWidth, proxy->mViewportHeight);
proxy->asyncCmdSetWindowSize(proxy->mViewportWidth, proxy->mViewportHeight);
proxy->mScroller->setViewportDimensions(proxy->mViewportWidth, proxy->mViewportHeight);
proxy->scrollCaretIntoViewAfterResize(oldwidth, oldheight,
proxy->mZoomLevel);
return NULL;
}
void BrowserAdapter::scrollCaretIntoViewAfterResize(int oldwidth, int oldheight, double oldZoomLevel)
{
//int oldcaretx = m_textCaretRect.x() * oldZoomLevel;
int oldcarety = m_textCaretRect.y() * oldZoomLevel;
//int newcaretx = m_textCaretRect.x() * mZoomLevel;
int newcarety = m_textCaretRect.y() * mZoomLevel;
/*
TRACEF("caret %d,%d %d,%d old %dx%d vp %dx%d scroll %d,%d zoom %f %f",
oldcaretx, oldcarety, newcaretx, newcarety,
oldwidth, oldheight, mViewportWidth, mViewportHeight,
mScrollPos.x, mScrollPos.y, mZoomLevel, oldZoomLevel);
*/
if ((m_textCaretRect.x() != 0 || m_textCaretRect.y() != 0)
&& oldheight != mViewportHeight
&& oldcarety > mScrollPos.y && oldcarety < mScrollPos.y + oldheight
&& (newcarety > mScrollPos.y + mViewportHeight
|| newcarety < mScrollPos.y)) {
mScrollPos.y += (oldheight - mViewportHeight) + (newcarety - oldcarety);
TRACEF("scrolling to %d %d", mScrollPos.x, mScrollPos.y);
mScroller->scrollTo(-mScrollPos.x, -mScrollPos.y, false);
invalidate();
}
}
/**
* Invalidate the entire page forcing it to be redrawn.
*/
void BrowserAdapter::invalidate()
{
NPRect dirty;
dirty.top = 0;
dirty.left = 0;
dirty.bottom = mWindow.height;
dirty.right = mWindow.width;
NPN_InvalidateRect(&dirty);
}
/**
* Smart zoom the adapter view to indicated origin in page coordinates.
*
* @deprecated
*
* @param pt The point (in document coordinates) to calculate the smart zoom rectangle for.
*/
bool BrowserAdapter::prvSmartZoom(const Point& pt)
{
init();
// no double tap zoom if meta viewport sets user-scalable=false
if (mMetaViewport && !mMetaViewport->userScalable) {
return false;
}
asyncCmdZoomSmartCalculateRequest(pt.x, pt.y);
return true;
}
/*
Yapper connection related callbacks
*/
void BrowserAdapter::serverConnected()
{
TRACE;
if (mServerConnectedInvoking) return;
mBrowserServerConnected = true;
mNotifiedOfBrowserServerDisconnect = false;
if (mBrowserServerConnected && !mServerConnectedInvoked) {
mServerConnectedInvoking = true; // prevent reentry
mServerConnectedInvoked = InvokeEventListener(gServerConnectedHandler, NULL, 0, NULL);
mServerConnectedInvoking = false;
}
}
void BrowserAdapter::serverDisconnected()
{
TRACE;
m_useFastScaling = false;
bEditorFocused = false;
if (!mNotifiedOfBrowserServerDisconnect) { // notify once
TRACEF("%s: no connection with BrowserServer", __FUNCTION__);
if (InvokeEventListener(gBrowserServerDisconnectHandler, NULL, 0, NULL)) {
mNotifiedOfBrowserServerDisconnect = true;
}
else {
TRACEF("%s: Error informing WebView of disconnect!", __FUNCTION__);
}
}
mBrowserServerConnected = false;
mServerConnectedInvoked = false;
mSendFinishDocumentLoadNotification = false;
}
/*
Server Message Handlers
*/
/**
* The browser server just painted into it's buffer and we need to respond accordingly.
*
* All coordinates are scaled document with the origin at the TL of the page.
*
* @param rectX Left of painted rectangle.
* @param rectY Top of painted rectangle.
* @param rectWidth Width of painted rectangle.
* @param rectHeight Height of painted rectangle.
*/
void BrowserAdapter::msgPainted(int32_t sharedBufferKey)
{
VERBOSE_TRACE("BrowserAdapter::msgPaintedBuffer key: %d",
sharedBufferKey);
if (mFrozen) {
if (m_bufferLock)
sem_post(m_bufferLock);
return;
}
delete mFrozenSurface;
mFrozenSurface = NULL;
int receivedBuffer = -1;
if (mOffscreen0->ipcBuffer()->key() == sharedBufferKey)
receivedBuffer = 0;
else if (mOffscreen1->ipcBuffer()->key() == sharedBufferKey)
receivedBuffer = 1;
else if (mOffscreen2 && mOffscreen2->ipcBuffer()->key() == sharedBufferKey)
receivedBuffer = 2;
if (receivedBuffer < 0) {
g_warning("Received shared buffer key is not ours: %d", sharedBufferKey);
if (m_bufferLock)
sem_post(m_bufferLock);
return;
}
// T-527: the frame shown until now (its number and geometry), for a partial redraw below
uint32_t shownSeq = 0;
BrowserOffscreenInfo shown;
bool haveShown = false;
if (mOffscreenCurrent && mOffscreenCurrent->header()) {
shownSeq = mOffscreenCurrent->frameSeq();
shown = *mOffscreenCurrent->header();
haveShown = true;
}
if (mOffscreenCurrent) {
assert(mOffscreenCurrent->ipcBuffer()->key() != sharedBufferKey);
asyncCmdReturnBuffer(mOffscreenCurrent->key());
mOffscreenCurrent = 0;
}
mOffscreenCurrent = (receivedBuffer == 0) ? mOffscreen0
: (receivedBuffer == 1) ? mOffscreen1 : mOffscreen2;
if (FILE* lf = jihadPaintLog()) {
BrowserOffscreenInfo* pi = mOffscreenCurrent->header();
fprintf(lf, "[msgp] key=%d recv=%d frozen=%d rW=%d rH=%d win[%ux%u] -> invalidate\n",
sharedBufferKey, receivedBuffer, mFrozen?1:0, pi?pi->renderedWidth:-1, pi?pi->renderedHeight:-1,
mWindow.width, mWindow.height); }
// T-527: when the daemon says what differs from the frame on screen (JihadFrameWindow.h) and the
// frames are of one geometry at the card's own zoom, only that rect is redrawn: the card's blit of
// a whole frame for a changed strip (a WebGL canvas, an animation, typing) was a fifth of
// LunaSysMgr's time on the 480 px WebGL cube (r304). Anything else redraws the whole.
{
BrowserOffscreenInfo* ni = mOffscreenCurrent->header();
uint32_t base = 0;
int dl = 0, dt = 0, dr = 0, db = 0;
const bool partial = haveShown && ni && shownSeq != 0 &&
mOffscreenCurrent->dirtyRect(&base, &dl, &dt, &dr, &db) && base == shownSeq &&
ni->renderedX == shown.renderedX && ni->renderedY == shown.renderedY &&
ni->renderedWidth == shown.renderedWidth && ni->renderedHeight == shown.renderedHeight &&
ni->contentZoom == shown.contentZoom && fabs(ni->contentZoom - mZoomLevel) < 1e-6 &&
mScrollPos.x == mJihadPaintScrollX && mScrollPos.y == mJihadPaintScrollY && m_headerHeight == mJihadPaintHeader;
mJihadPaintScrollX = mScrollPos.x; mJihadPaintScrollY = mScrollPos.y; mJihadPaintHeader = m_headerHeight;
if (FILE* lf = jihadPaintLog())
fprintf(lf, "[msgp-dirty] seq=%u shown=%u base=%u partial=%d rect[%d,%d %d,%d]\n",
mOffscreenCurrent->frameSeq(), shownSeq, base, (int)partial, dl, dt, dr, db);
if (partial) {
// buffer px to the plugin's (the blit's mapping at a scale of 1)
const int pageScrollY = mScrollPos.y - m_headerHeight;
int l = dl + ni->renderedX - mScrollPos.x, r = dr + ni->renderedX - mScrollPos.x;
int t = dt + ni->renderedY - pageScrollY, b = db + ni->renderedY - pageScrollY;
if (l < 0) l = 0;
if (t < 0) t = 0;
if (r > (int)mWindow.width) r = mWindow.width;
if (b > (int)mWindow.height) b = mWindow.height;
if (r > l && b > t) {
NPRect dirty;
dirty.left = l; dirty.top = t; dirty.right = r; dirty.bottom = b;
NPN_InvalidateRect(&dirty);
}
} else {
invalidate();
}
}
if (m_bufferLock)
sem_post(m_bufferLock);
if (!mFirstPaintComplete) {
InvokeEventListener(gFirstPaintCompleteHandler, NULL, 0, NULL);
mFirstPaintComplete = true;
}
}
void BrowserAdapter::msgReportError(const char* url, int32_t code, const char* msg)
{
TRACEF("ReportErrorMsg: Url: %s, Code: %d Msg: %s", url, code, msg);
NPVariant args[3];
STRINGZ_TO_NPVARIANT(url, args[0]);
INT32_TO_NPVARIANT(code, args[1]);
STRINGZ_TO_NPVARIANT(msg, args[2]);
InvokeEventListener(gReportErrorHandler, args, G_N_ELEMENTS(args), 0 );
}
void BrowserAdapter::msgEditorFocused(bool focused, int32_t fieldType, int32_t fieldActions)
{
TRACEF("EditorFocused: %c", focused ? 'T' : 'F');
bEditorFocused = focused;
NPVariant args[3];
BOOLEAN_TO_NPVARIANT(focused, args[0]);
INT32_TO_NPVARIANT(fieldType, args[1]);
INT32_TO_NPVARIANT(fieldActions, args[2]);
InvokeEventListener(gEditorFocusedHandler, args, G_N_ELEMENTS(args), 0 );
}
void BrowserAdapter::msgFailedLoad(const char* domain, int errorCode,
const char* failingURL, const char* localizedDescription)
{
if(domain==NULL)
domain="";
if(failingURL ==NULL)
failingURL="";
if(localizedDescription==NULL)
localizedDescription="";
TRACEF("%s, %d, %s, %s", domain, errorCode, failingURL, localizedDescription);
NPVariant args[4];
STRINGZ_TO_NPVARIANT(domain, args[0]);
INT32_TO_NPVARIANT(errorCode, args[1]);
STRINGZ_TO_NPVARIANT(failingURL, args[2]);
STRINGZ_TO_NPVARIANT(localizedDescription, args[3]);
InvokeEventListener(gFailedLoadHandler, args, G_N_ELEMENTS(args), 0 );
}
void BrowserAdapter::msgSetMainDocumentError(const char* domain, int errorCode,
const char* failingURL, const char* localizedDescription)
{
TRACEF("%s, %d, %s, %s", domain, errorCode, failingURL, localizedDescription);
NPVariant args[4];
STRINGZ_TO_NPVARIANT(domain, args[0]);
INT32_TO_NPVARIANT(errorCode, args[1]);
STRINGZ_TO_NPVARIANT(failingURL, args[2]);
STRINGZ_TO_NPVARIANT(localizedDescription, args[3]);
InvokeEventListener(gMainDocumentErrorHandler, args, G_N_ELEMENTS(args), 0 );
}
/**
* The size of the contents (page) changed.
*
* @param width Scaled page width.
* @param height Scaled page height.
*/
void BrowserAdapter::msgContentsSizeChanged(int32_t width, int32_t height)
{
TRACEF("%s: Content size: %dx%d zoom %f", __FUNCTION__, width, height, mZoomLevel);
if (mPageWidth == width && mPageHeight == height)
return;
if (width == 0 && height == 0) {
// First content size after a page load. Zoom fit
delete mMetaViewport;
mMetaViewport = 0;
resetScrollableLayerScrollSession();
mScrollableLayers.clear();
mZoomFit = true;
mZoomLevel = 1.0;
mScroller->scrollTo(0, 0, false);
mScrollPos.x = 0;
mScrollPos.y = 0;
// return any buffers we own, since they are stale at this point
if (mOffscreenCurrent) {
asyncCmdReturnBuffer(mOffscreenCurrent->key());
mOffscreenCurrent = 0;
}
}
if (mZoomFit && mWindow.width != 0 && width != 0) {
mZoomLevel = mWindow.width * 1.0 / width;
asyncCmdSetZoomAndScroll(mZoomLevel, mScrollPos.x, serverScrollY());
}
mPageWidth = width;
mPageHeight = height;
mContentWidth = ::round(mZoomLevel * width);
mContentHeight = ::round(mZoomLevel * height);
if (mMetaViewport && !mMetaViewport->userScalable
&& mContentHeight > mViewportHeight && mContentHeight <= (mViewportHeight + 2)) {
// In testing, it was observed that for some sites, like maps.google.com, the mContentHeight
// would initially equal the mViewportHeight and then slightly increase by 2 pixels. When
// mContentHeight > mViewportHeight, input events are not passed (see shouldPassInputEvents()),
// along with a variety of other state changes within BrowserAdapter that negatively effected
// sites that use touch events.
mContentHeight = mViewportHeight;
}
mScroller->setContentDimensions(mContentWidth, mContentHeight + m_headerHeight);
if (mShowHighlight) {
removeHighlight();
}
// FIXME: RR this should not be needed anymore
// Let listeners know about the size change
NPVariant args[2];
INT32_TO_NPVARIANT(width, args[0]);
INT32_TO_NPVARIANT(height, args[1]);
InvokeEventListener(gPageDimensionsHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgSmartZoomCalculateResponseSimple(int32_t inCenterX, int32_t inCenterY,
int32_t inLeft, int32_t inTop,
int32_t inRight, int32_t inBottom,
int inSpotlightHandle)
{
if (mPageWidth == 0 || mPageHeight == 0 ||
mWindow.width == 0 || mWindow.height == 0)
return;
int viewportWidth = mWindow.width;
int viewportHeight = mWindow.height;
const int kMargin = 10;
// fit by width
double zoom = viewportWidth * 1.0 / (inRight - inLeft + kMargin * 2);
// Special handling for flash double tap zoom. We want to fit the entire content
// and so may need to adjust the zoom
if (inSpotlightHandle) {
double zoom2 = viewportHeight * 1.0 / (inBottom - inTop + kMargin * 2);
if (zoom > zoom2) {
zoom = zoom2;
}
}
// zoom cap
double maxZoom = mMetaViewport ? mMetaViewport->maximumScale : kMaxZoom;
double minZoom = mMetaViewport ? mMetaViewport->minimumScale : 0.0;
double fitZoom = viewportWidth * 1.0 / mPageWidth;
minZoom = MAX(minZoom, fitZoom);
if (zoom < minZoom)
zoom = minZoom;
if (zoom > maxZoom)
zoom = maxZoom;
int left = 0;
int top = 0;
if (PrvIsEqual(zoom, mZoomLevel)) {
// zoom back to fit width
zoom = fitZoom;
left = 0;
top = inCenterY * zoom - viewportHeight / 2;
}
else {
// Center the rectangle
// Horizontal center is in middle of rectangle
int centerX = ((inLeft + inRight) / 2) * zoom;
// Vertical center is at centerY
int centerY = inCenterY * zoom;
// But if the rectangle will fit height-wise within the viewport Height
// we center in vertically in the middle of the rectangle
if ((inBottom - inTop) * zoom < viewportHeight) {
centerY = ((inTop + inBottom) / 2) * zoom;
}
left = centerX - viewportWidth / 2;
top = centerY - viewportHeight / 2;
}
// Clamp on edges
int maxLeft = mPageWidth * zoom - viewportWidth;
int maxTop = mPageHeight * zoom - viewportHeight;
left = MIN(maxLeft, left);
top = MIN(maxTop, top);
left = MAX(left, 0);
top = MAX(top, 0);
// Our scroll includes the header strip above the page (m_headerHeight, the Mojo shell's
// sliding toolbar): a block below the page's top is reached with the strip scrolled away.
startZoomAnimation(zoom, left, top > 0 ? top + m_headerHeight : 0);
}
void BrowserAdapter::msgMakePointVisible(int32_t x, int32_t y)
{
TRACEF("%d %d", x, y);
/* FIXME: RR
int scaledX = x * mZoomLevel + mJsScrollX;
int scaledY = y * mZoomLevel + mJsScrollY;
TRACEF("MSG: MakePointVisible %d, %d\n", x, y);
int scrollAlongX = 0;
if (scaledX < 0) {
scrollAlongX = scaledX; // negative number that will make us scroll left
} else if (scaledX > mViewportWidth) {
scrollAlongX = scaledX - mViewportWidth; // positive number to scroll right
}
int scrollAlongY = 0;
if (scaledY < 0) {
scrollAlongY = scaledY; // negative will move us up
} else if (scaledY > (mViewportHeight - 25)) { // 25 gives us some breathing room
scrollAlongY = scaledY - (mViewportHeight - 25);
}
if (scrollAlongX != 0 || scrollAlongY != 0) {
//printf("%s: I want to scroll along X: %d, along Y: %d\n", __FUNCTION__,
// scrollAlongX, scrollAlongY);
mWkScrollX += scrollAlongX;
mWkScrollY += scrollAlongY;
msgScrolledTo(mWkScrollX, mWkScrollY);
}
*/
}
/**
* The browser just scrolled to a new position. Will notify the offscreen
* buffer of the scroll change and invalidate the screen to get it to redraw.
*
* @param scrollx The scaled left coordinate.
* @param scrolly The scaled top coordinate.
*/
void BrowserAdapter::msgScrolledTo(int32_t scrollx, int32_t scrolly)
{
mScrollPos.x = MIN(::round(scrollx * mZoomLevel), MAX(mContentWidth - mViewportWidth, 0));;
// The engine's scroll is the PAGE's; ours includes the header strip above it (m_headerHeight,
// 0 unless the card has a scrolling toolbar). At the page's top keep however much of the
// header was showing; anywhere else the header has scrolled away.
{
int pageY = ::round(scrolly * mZoomLevel);
int y = (pageY > 0) ? pageY + m_headerHeight : MIN(mScrollPos.y, m_headerHeight);
mScrollPos.y = MIN(y, MAX(mContentHeight + m_headerHeight - mViewportHeight, 0));
}
TRACEF("(%d, %d) (%d, %d) c %dx%d vp %dx%d", scrollx, scrolly, mScrollPos.x, mScrollPos.y, mContentWidth, mContentHeight, mViewportWidth, mViewportHeight);
mScroller->scrollTo(-mScrollPos.x, -mScrollPos.y, false);
/* FIXME: RR
TRACEF("(%d, %d)",scrollx, scrolly);
mWkScrollX = scrollx;
mWkScrollY = scrolly;
scrollx = lround(mWkScrollX * mZoomLevel);
scrolly = lround(mWkScrollY * mZoomLevel);
if( gScrollToHandler )
{
NPVariant args[2];
INT32_TO_NPVARIANT(scrollx,args[0]);
INT32_TO_NPVARIANT(scrolly,args[1]);
InvokeEventListener(gScrollToHandler, args, G_N_ELEMENTS(args), 0 );
}
*/
}
void BrowserAdapter::msgLoadStarted()
{
TRACE;
mFirstPaintComplete = false;
// A new page does not inherit the last one's meta viewport. It was freed only on a (0,0)
// contents-size message, which this daemon never sends, and the daemon sends nothing for a
// page without the tag — so after one user-scalable=no page every later page refused pinch
// and double-tap (inspector 2026-10-08). The new page's tag, if any, arrives after this.
delete mMetaViewport;
mMetaViewport = 0;
// And it starts with the card's header (the Mojo toolbar) showing. The daemon's navigation
// reset reports page scroll 0, which msgScrolledTo keeps as "header partly hidden" when the
// old page had been scrolled, so a link followed from lower down opened with the address bar
// hidden (inspector 2026-10-08).
if (m_headerHeight > 0 && mScrollPos.y > 0 && mScrollPos.y <= m_headerHeight) {
mScrollPos.y = 0;
if (mScroller) mScroller->scrollTo(-mScrollPos.x, 0, false);
invalidate();
fireScrolledToEvent(-mScrollPos.x, 0);
}
if ( NULL != gLoadStartedHandler ) {
InvokeEventListener(gLoadStartedHandler, NULL, 0, NULL);
}
}
void BrowserAdapter::msgLoadStopped()
{
TRACE;
if ( NULL != gLoadStoppedHandler ) {
InvokeEventListener(gLoadStoppedHandler, NULL, 0, NULL);
}
}
void BrowserAdapter::jsonToRects(const char* rectsArrayJson)
{
if (!rectsArrayJson) {
TRACEF("%s: invoked with invalid parameter, rectsArrayJson = %s\n.", __FUNCTION__, rectsArrayJson);
return;
}
int numRects = 0;
// parse out rectangle coordinates
pbnjson::JValue rectsArray;
pbnjson::JDomParser parser(NULL);
// JIHAD: resolve the schema inside THIS app first, and only then fall back to the stock
// system path. Two reasons, both learned the hard way:
// - /etc/palm/browser/ on a webOS 3.0.5 device ships HitTest.schema and NOTHING else.
// InteractiveWidgetRect.schema is simply not there, and a missing schema file makes
// the parse below fail, which silently drops EVERY rect in the message — the feature
// looks correct all the way to the wire and then does nothing.
// - /etc is the read-only system rootfs, shared with the stock browser, and outside
// this variant's own footprint (device-build R8). Installing into it would need a
// remount and would leave a file behind that our uninstall never removes.
// The wire bytes are untouched by this: the schema is how the adapter validates the
// payload, not part of the frozen contract.
// Flat, not conf/: the loaded library is the ROOT-OWNED copy in /usr/lib/jihad/<variant>/
// (the app-bundle copy is only a delivery vehicle), and postinst puts the schema there
// beside it. dladdr therefore names that directory, for whichever variant is running.
QString schemaFile = jihadAppFile("InteractiveWidgetRect.schema");
if (schemaFile.isEmpty()) {
#ifdef ISIS_DESKTOP
schemaFile = QString("%1/.isis/conf/InteractiveWidgetRect.schema").arg(QDir::homePath());
#else
schemaFile = QString("/etc/palm/browser/InteractiveWidgetRect.schema");
#endif
}
pbnjson::JSchemaFile schema(qPrintable(schemaFile));
if (!parser.parse(rectsArrayJson, schema, NULL)) {
TRACEF("%s: unable to parse '%s' against schema '%s'\n", __FUNCTION__,
rectsArrayJson, qPrintable(schemaFile));
goto Done;
}
rectsArray = parser.getDom();
numRects = (int) rectsArray.arraySize();
for (int i = 0; i < numRects; ++i)
{
pbnjson::JValue rectObj = rectsArray[i];
int left, top, right, bottom;
uintptr_t id;
InteractiveRectType type;
id = (uintptr_t) rectObj["id"].asNumber<int64_t>();
left = rectObj["left"].asNumber<int>();
top = rectObj["top"].asNumber<int>();
right = rectObj["right"].asNumber<int>();
bottom = rectObj["bottom"].asNumber<int>();
type = (InteractiveRectType) rectObj["type"].asNumber<int>();
BrowserRect rect(left, top, right-left, bottom-top);
switch (type) {
case InteractiveRectDefault:
mDefaultInteractiveRects.insert(RectMapItem(id, rect));
TRACEF("inserting rect: type: %d, id: %d, left: %d, top: %d, width: %d, height: %d, new count: %d",
(int)type, (int)id, rect.x(), rect.y(), rect.w(), rect.h(), (int)mDefaultInteractiveRects.size());
break;
case InteractiveRectPlugin:
mFlashRects.insert(RectMapItem(id, rect));
TRACEF("inserting rect: type: %d, id: %d, left: %d, top: %d, width: %d, height: %d, new count: %d",
(int)type, (int)id, rect.x(), rect.y(), rect.w(), rect.h(), (int)mFlashRects.size());
break;
default:
g_debug("Unrecognized rect type: %d", type);
break;
}
}
Done:
return;
}
void BrowserAdapter::msgAddFlashRects(const char* rectsArrayJson)
{
TRACEF("ADD RECTS!: %s", rectsArrayJson);
jsonToRects(rectsArrayJson);
}
void BrowserAdapter::msgRemoveFlashRects(const char* rectIdJson)
{
TRACEF("REMOVE RECTS!: %s", rectIdJson);
// numeric id and type are passed
pbnjson::JValue rectId;
pbnjson::JDomParser parser(NULL);
pbnjson::JSchemaFragment schema("{}");
if (!parser.parse(rectIdJson, schema, NULL)) {
TRACEF("%s: unable to parse string '%s'\n", __FUNCTION__, rectIdJson);
return;
}
rectId = parser.getDom();
uintptr_t id = (uintptr_t) rectId["id"].asNumber<int64_t>();
InteractiveRectType type = (InteractiveRectType) rectId["type"].asNumber<int>();
switch (type) {
case InteractiveRectDefault:
mDefaultInteractiveRects.erase(id);
TRACEF("Removing rect type: %d, id: %d, new count: %d", type, (int)id, (int)mDefaultInteractiveRects.size());
break;
case InteractiveRectPlugin:
mFlashRects.erase(id);
TRACEF("Removing rect type: %d, id: %d, new count: %d", type, (int)id, (int)mFlashRects.size());
break;
default:
g_debug("Unrecognized rect type: %d", type);
break;
}
return;
}
void BrowserAdapter::msgShowPrintDialog()
{
InvokeEventListener(gShowPrintDialogHandler, NULL, 0, NULL);
}
void BrowserAdapter::msgGetTextCaretBoundsResponse(int32_t queryNum, int32_t left, int32_t top, int32_t right, int32_t bottom)
{
TRACEF("%d %d %d %d", left, top, right, bottom);
if (left && top && right && bottom
&& left != -1 && top != -1 && right != -1 && bottom != -1) {
m_textCaretRect.setX(left);
m_textCaretRect.setY(top);
m_textCaretRect.setWidth(right - left);
m_textCaretRect.setHeight(bottom - top);
} else {
m_textCaretRect.setX(0);
m_textCaretRect.setY(0);
m_textCaretRect.setWidth(0);
m_textCaretRect.setHeight(0);
}
}
void BrowserAdapter::msgUpdateGlobalHistory(const char* url, bool reload)
{
NPVariant args[2];
STRINGZ_TO_NPVARIANT(url, args[0]);
BOOLEAN_TO_NPVARIANT(reload,args[1]);
InvokeEventListener(gUpdateGlobalHistoryHandler, args, G_N_ELEMENTS(args), 0 );
}
void BrowserAdapter::msgDidFinishDocumentLoad()
{
TRACE;
InvokeEventListener(gDidFinishDocumentLoadHandler, NULL, 0, NULL);
mSendFinishDocumentLoadNotification = false;
}
void BrowserAdapter::msgLoadProgress(int32_t progress)
{
NPVariant arg;
INT32_TO_NPVARIANT(progress, arg);
if ( NULL != gLoadProgressHandler ) {
InvokeEventListener(gLoadProgressHandler, &arg, 1, NULL);
}
}
void BrowserAdapter::msgLocationChanged(const char* uri, bool canGoBack, bool canGoForward)
{
TRACEF("%s", uri);
NPVariant args[3];
STRINGZ_TO_NPVARIANT(uri, args[0]);
BOOLEAN_TO_NPVARIANT(canGoBack, args[1]);
BOOLEAN_TO_NPVARIANT(canGoForward, args[2]);
InvokeEventListener(gUrlChangeHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgTitleChanged(const char* title)
{
TRACEF("%s", title);
NPVariant args[1];
STRINGZ_TO_NPVARIANT(title, args[0]);
InvokeEventListener(gTitleChangeHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgTitleAndUrlChanged(const char* title, const char* url, bool canGoBack, bool canGoForward)
{
TRACEF("Title&Url Change: %s, %s", title, url);
NPVariant args[4];
if( !url )
url = "";
if( !title )
title = "";
STRINGZ_TO_NPVARIANT(title, args[0]);
STRINGZ_TO_NPVARIANT(url, args[1]);
BOOLEAN_TO_NPVARIANT(canGoBack, args[2]);
BOOLEAN_TO_NPVARIANT(canGoForward, args[3]);
InvokeEventListener(gTitleURLChangeHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgDialogAlert(const char* syncPipePath, const char* msg)
{
TRACEF("Password Dialog: %s", msg);
if (mLogAlertMessages) {
FILE* file = fopen("/tmp/browser-alert_message.txt", "w+");
if (file != NULL) {
if (msg == NULL)
fputs("<NULL>", file);// just to be safe.
else
fputs(msg, file);
fclose(file);
}
}
strncpy(gDialogResponsePipe, syncPipePath, G_N_ELEMENTS(gDialogResponsePipe));
gDialogResponsePipe[G_N_ELEMENTS(gDialogResponsePipe)-1] = '\0';
NPVariant args[1];
STRINGZ_TO_NPVARIANT(msg, args[0]);
InvokeEventListener(gDialogAlertHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgDialogConfirm(const char* syncPipePath, const char* msg)
{
TRACEF("Confirm Dialog: %s", msg);
strncpy(gDialogResponsePipe, syncPipePath, G_N_ELEMENTS(gDialogResponsePipe));
gDialogResponsePipe[G_N_ELEMENTS(gDialogResponsePipe)-1] = '\0';
NPVariant args[1];
STRINGZ_TO_NPVARIANT(msg, args[0]);
InvokeEventListener(gDialogConfirmHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgDialogSSLConfirm(const char* syncPipePath, const char* host,int32_t code, const char* certFile)
{
TRACEF("Confirm SSL Dialog: host:%s, code:%d, certFile:%s", host, code, certFile);
strncpy(gDialogResponsePipe, syncPipePath, G_N_ELEMENTS(gDialogResponsePipe));
gDialogResponsePipe[G_N_ELEMENTS(gDialogResponsePipe)-1] = '\0';
NPVariant args[3];
STRINGZ_TO_NPVARIANT(host, args[0]);
INT32_TO_NPVARIANT(code,args[1]);
STRINGZ_TO_NPVARIANT(certFile, args[2]);
InvokeEventListener(gDialogConfirmSSLHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgDialogPrompt(const char* syncPipePath, const char* msg, const char* defaultValue)
{
TRACEF("Prompt Dialog: %s (%s)", msg, defaultValue);
strncpy(gDialogResponsePipe, syncPipePath, G_N_ELEMENTS(gDialogResponsePipe));
gDialogResponsePipe[G_N_ELEMENTS(gDialogResponsePipe)-1] = '\0';
NPVariant args[2];
STRINGZ_TO_NPVARIANT(msg, args[0]);
STRINGZ_TO_NPVARIANT(defaultValue, args[1]);
InvokeEventListener(gDialogPromptHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgDialogUserPassword(const char* syncPipePath, const char* msg)
{
TRACEF("Password Dialog: %s", msg);
strncpy(gDialogResponsePipe, syncPipePath, G_N_ELEMENTS(gDialogResponsePipe));
gDialogResponsePipe[G_N_ELEMENTS(gDialogResponsePipe)-1] = '\0';
NPVariant args[1];
STRINGZ_TO_NPVARIANT(msg, args[0]);
InvokeEventListener(gDialogUserPasswordHandler, args, G_N_ELEMENTS(args), NULL);
}
/**
* Sent by the browser server when the client needs to display a popoup menu.
*/
void BrowserAdapter::msgPopupMenuShow(const char* menuId, const char* menuDataFileName)
{
TRACEF("showPopupMenu: %s", menuId);
FILE* file = ::fopen(menuDataFileName, "rb");
if (NULL != file) {
::fseek(file, 0, SEEK_END);
long fsize = ::ftell(file);
::fseek(file, 0, SEEK_SET);
char* menuData = new char[fsize+1];
if (NULL != menuData) {
if (static_cast<size_t>(fsize) == ::fread(menuData, sizeof(char), fsize, file)) {
menuData[fsize] = '\0';
NPVariant args[2];
STRINGZ_TO_NPVARIANT(menuId, args[0]);
STRINGZ_TO_NPVARIANT(menuData, args[1]);
InvokeEventListener(gPopupMenuShowHandler, args, G_N_ELEMENTS(args), NULL);
}
else {
TRACEF("ERROR reading menu data from file.");
// If this fails then it's either a program error or something horrible going
// wrong on the device. Not responding to this message won't cause WebKit to hang.
}
delete [] menuData;
}
::fclose(file);
::unlink(menuDataFileName);
}
}
/**
* Sent by the browser server when the client needs to hide a popoup menu.
*/
void BrowserAdapter::msgPopupMenuHide(const char* menuId)
{
TRACEF("hidePopupMenu: %s", menuId);
NPVariant args[1];
STRINGZ_TO_NPVARIANT(menuId, args[0]);
InvokeEventListener(gPopupMenuHideHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgActionData(const char* dataType, const char* data)
{
NPVariant args[2];
TRACEF("ActionData: dataType: %s data: %s", dataType, data);
STRINGZ_TO_NPVARIANT(dataType, args[0]);
STRINGZ_TO_NPVARIANT(data, args[1]);
InvokeEventListener(gActionDataHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgDownloadStart(const char* url)
{
TRACEF("DownloadStartMsg: %s", url);
NPVariant args[1];
STRINGZ_TO_NPVARIANT(url, args[0]);
InvokeEventListener(gDownloadStartHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgDownloadProgress(const char* url, int32_t totalSizeSoFar, int32_t totalSize)
{
TRACEF("DownloadProgressMsg: Url: %s totalSizeSoFar %d totalSize %d",
url, totalSizeSoFar, totalSize);
NPVariant args[3];
STRINGZ_TO_NPVARIANT(url, args[0]);
INT32_TO_NPVARIANT(totalSizeSoFar, args[1]);
INT32_TO_NPVARIANT(totalSize, args[2]);
InvokeEventListener(gDownloadProgressHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgDownloadError(const char* url, const char* errorMsg)
{
TRACEF("DownloadErrorMsg: Url: %s errorMsg: %s",
url, errorMsg);
NPVariant args[2];
STRINGZ_TO_NPVARIANT(url, args[0]);
STRINGZ_TO_NPVARIANT(errorMsg, args[1]);
InvokeEventListener(gDownloadErrorHandler, args, G_N_ELEMENTS(args), NULL);
}
/**
* @brief creates vector of BrowserAdapterRectangleS that cover an interactive
* element that will be highlighted at the next paint.
*
*/
void BrowserAdapter::msgHighlightRects(const char* rectsArrayJson)
{
#ifdef FIXME_QT
TRACEF("%s: rects array: %s", __FUNCTION__, rectsArrayJson);
// clear out previous rectangles
removeHighlight();
if (!rectsArrayJson)
{
// ignore the message if the parameter is invalid. This may happen if the list
// of json objects was too long to fit into the Yap buffers, like when selecting a very
// long URL (displayed in several lines) on the screen.
TRACEF("%s: invoked with invalid parameter, rectsArrayJson = %s\n.", __FUNCTION__, rectsArrayJson);
return;
}
bool success = false;
int numRects = 0;
// parse out rectangle coordinates
json_object* rectsArrayRoot = json_tokener_parse(rectsArrayJson);
array_list* rectsArray;
if (!rectsArrayRoot || is_error(rectsArrayRoot)) {
rectsArrayRoot = NULL;
TRACEF("%s: unable to parse string '%s'\n", __FUNCTION__, rectsArrayJson);
goto Done;
}
rectsArray = json_object_get_array(rectsArrayRoot);
if (!rectsArray || is_error(rectsArray)) {
TRACEF("%s: unable to get array from '%s'\n", __FUNCTION__, json_object_get_string(rectsArrayRoot));
goto Done;
}
numRects = array_list_length(rectsArray);
for (int i = 0; i < numRects; ++i)
{
json_object* rectJson = (json_object*) array_list_get_idx(rectsArray, i);
int left, top, right, bottom;
json_object* coord = json_object_object_get(rectJson, "left");
left = json_object_get_int(coord);
coord = json_object_object_get(rectJson, "top");
if (!coord || is_error(coord)) {
goto Done;
}
top = json_object_get_int(coord);
coord = json_object_object_get(rectJson, "right");
if (!coord || is_error(coord)) {
goto Done;
}
right = json_object_get_int(coord);
coord = json_object_object_get(rectJson, "bottom");
if (!coord || is_error(coord)) {
goto Done;
}
bottom = json_object_get_int(coord);
BrowserRect rect(left, top, right-left, bottom-top);
mHighlightRects.push_back(rect);
}
success = true;
Done:
if (rectsArrayRoot) {
json_object_put(rectsArrayRoot);
}
if (success) {
for (std::vector<BrowserRect>::const_iterator rect_iter = mHighlightRects.begin();
rect_iter != mHighlightRects.end();
++rect_iter)
{
TRACEF("%s: will draw rect with left: %d, top: %d, right: %d, bottom: %d\n",
__FUNCTION__, (*rect_iter).x(), (*rect_iter).y(), (*rect_iter).r(), (*rect_iter).b());
}
invalidateHighlightRectsRegion(); // force area under new rectangles to be redrawn
mShowHighlight = true;
} else {
TRACEF("%s: some json failure\n", __FUNCTION__);
removeHighlight();
}
#endif /* FIXME_QT */
}
void BrowserAdapter::msgLinkClicked(const char* url)
{
TRACEF("LinkClickedMsg: Url: %s", url);
NPVariant args[1];
STRINGZ_TO_NPVARIANT(url, args[0]);
InvokeEventListener(gLinkClickedHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgDownloadFinished(const char* url, const char* mimeType, const char* tmpFilePath)
{
TRACEF("DownloadFinishedMsg: Url: %s mimeType: %s tmpFilePath: %s",url, mimeType, tmpFilePath);
NPVariant args[3];
STRINGZ_TO_NPVARIANT(url, args[0]);
STRINGZ_TO_NPVARIANT(mimeType, args[1]);
STRINGZ_TO_NPVARIANT(tmpFilePath, args[2]);
InvokeEventListener(gDownloadFinishedHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgMimeHandoffUrl(const char* mimeType, const char* url)
{
TRACEF("MimeHandoffUrlMsg: mimeType: %s Url: %s", mimeType, url);
NPVariant args[2];
STRINGZ_TO_NPVARIANT(mimeType, args[0]);
STRINGZ_TO_NPVARIANT(url, args[1]);
InvokeEventListener(gMimeHandoffUrlHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgMimeNotSupported(const char* mimeType, const char* url)
{
TRACEF("MimeNotSupportedMsg: mimeType: %s Url: %s", mimeType, url);
NPVariant args[2];
STRINGZ_TO_NPVARIANT(mimeType, args[0]);
STRINGZ_TO_NPVARIANT(url, args[1]);
InvokeEventListener(gMimeNotSupportedHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgCreatePage(int32_t identifier)
{
TRACEF("CreatePageMsg: identifier: %d\n", identifier);
char idBuff[24]; // 32-bit %d will not be longer than 11 decimal places, 64-bit < 21 places
::snprintf(idBuff, G_N_ELEMENTS(idBuff), "%d", identifier);
NPVariant args[1];
STRINGZ_TO_NPVARIANT(idBuff, args[0]);
InvokeEventListener(gCreatePageHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgClickRejected(int32_t counter)
{
TRACEF("ClickRejected: %d", counter);
NPVariant args[1];
INT32_TO_NPVARIANT(counter, args[0]);
InvokeEventListener(gClickRejectedHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::scale(double zoom)
{
TRACEF("Scale: %g", zoom);
mZoomLevel = zoom;
}
void BrowserAdapter::scaleAndScrollTo(double zoom, int x, int y)
{
// I believe this can be safely deprectated. Should do so after Browser's WebView
// widget is merged into the framework.
}
/**
* BrowserServer has requested that we purge this page. We're expected to do
* so if this page is not focused.
*/
void BrowserAdapter::msgPurgePage()
{
bool active = isActivated();
g_message("Requested to purge this adapter. Active=%c", active ? 'Y' : 'N');
if (active) {
g_warning("Not disconnecting because adapter is active.");
// Sending down a focused message will raise the page priority and then another
// inactive adapter will be told to purge its DOM next time.
asyncCmdPageFocused(true);
}
else {
g_warning("Disconnecting as requested");
asyncCmdDisconnect();
}
}
void BrowserAdapter::setPageIdentifier(int32_t identifier)
{
// Jihad: the identifier only ever travelled inside Connect, and the adapter connects by
// itself as soon as it is activated. A card that hands the identifier over AFTER that —
// any card whose plugin node is not scriptable until adapterInitialized fires, which is how
// the Mochi shell's view comes up — therefore connected as an ordinary card (-1), and the
// page the engine had asked a new card for (msgCreatePage, i.e. window.open) was never
// claimed: the card came up empty. Measured 2026-10-01: the card's
// setPageIdentifier("5") and connectBrowserServer() both returned success and the daemon
// logged `connect ... id=-1`.
// A real identifier (> 0; an ordinary card passes its DOM id, which atoi()s to 0) arriving
// on an already-connected adapter re-sends the connect state. Same command, same wire: the
// server replaces this proxy's page, exactly as it does for any second Connect.
bool late = mBrowserServerConnected && identifier > 0 && identifier != mPageIdentifier;
mPageIdentifier = identifier;
if (late) {
g_message("jihad: page identifier %d arrived after connect - reconnecting with it", identifier);
sendStateToServer();
}
}
void BrowserAdapter::enableFastScaling(bool enable)
{
if (enable != m_useFastScaling) {
m_useFastScaling = enable;
// trigger a repaint when we disable fast scaling
if (!m_useFastScaling) {
// FIXME: RR
} else {
m_startingZoomLevel = mZoomLevel;
}
}
}
void BrowserAdapter::msgInspectUrlAtPointResponse(int32_t queryNum, bool succeeded,
const char* url, const char* desc, int32_t rectWidth,
int32_t rectHeight, int32_t rectX, int32_t rectY)
{
TRACEF("Got inspectUrlAtPoint response qn=%d success=%c url='%s' desc='%s' (%d, %d) %d x %d.\n",
queryNum, succeeded ? 'T' : 'F', url, desc, rectX, rectY, rectWidth, rectHeight);
std::auto_ptr<InspectUrlAtPointArgs> args( m_inspectUrlArgs.get(queryNum) );
if (NULL != args.get()) {
NPVariant jsCallResult, jsCallArgs;
NPObject* info = NPN_CreateObject(&UrlInfo::sUrlInfoClass);
if (info) {
OBJECT_TO_NPVARIANT(info, jsCallArgs);
static_cast<UrlInfo*>(info)->initialize(succeeded, url, desc, rectX, rectY, rectX + rectWidth, rectY + rectHeight);
if (NPN_InvokeDefault(args->m_successCb, &jsCallArgs, 1, &jsCallResult))
TRACEF("inspectUrlAtPoint response call success.");
else
TRACEF("inspectUrlAtPoint response call FAILED.");
AdapterBase::NPN_ReleaseVariantValue(&jsCallArgs);
}
else {
TRACEF("InspectUrlAtPointResponse: response obj NOT created.");
}
}
else {
TRACEF("Can't find args");
}
}
void BrowserAdapter::msgUrlRedirected(const char* url, const char* userData)
{
TRACEF("Got URL redirect: '%s' -> '%s'\n", url, userData);
NPVariant args[2];
STRINGZ_TO_NPVARIANT(url, args[0]);
STRINGZ_TO_NPVARIANT(userData, args[1]);
InvokeEventListener(gUrlRedirectedHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgIsEditing(int32_t queryNum, bool isEditing)
{
TRACEF("is editable field in focus? %s\n", isEditing? "True" : "False");
std::auto_ptr<IsEditingArgs> args(m_isEditingArgs.get(queryNum));
if (!args.get()) {
TRACEF("Args requests are overlapping");
return;
}
NPVariant jsCallResult;
NPVariant jsCallArgs[1];
BOOLEAN_TO_NPVARIANT(isEditing, jsCallArgs[0]);
if (NPN_InvokeDefault(args->m_callback, jsCallArgs, G_N_ELEMENTS(jsCallArgs), &jsCallResult)) {
TRACEF("isEditing response call success.");
}
else {
TRACEF("isEditing response call FAILED.");
}
}
void BrowserAdapter::msgGetHistoryStateResponse(int32_t queryNum, bool canGoBack, bool canGoForward)
{
TRACEF("Got historyState response back=%c, forward=%c\n", canGoBack ? 'Y' : 'N',
canGoForward ? 'Y' : 'N' );
std::auto_ptr<GetHistoryStateArgs> args( m_historyStateArgs.get(queryNum) );
if (NULL != args.get()) {
NPVariant jsCallResult;
NPVariant jsCallArgs[2];
BOOLEAN_TO_NPVARIANT(canGoBack, jsCallArgs[0]);
BOOLEAN_TO_NPVARIANT(canGoForward, jsCallArgs[1]);
if (NPN_InvokeDefault(args->m_successCb, jsCallArgs, G_N_ELEMENTS(jsCallArgs), &jsCallResult))
TRACEF("getHistoryState response call success.");
else
TRACEF("getHistoryState response call FAILED.");
}
else {
TRACEF("Can't find args");
}
}
void BrowserAdapter::msgMetaViewportSet(double initialScale, double minimumScale, double maximumScale,
int32_t width, int32_t height, bool userScalable)
{
TRACEF("Got metaViewportSet: scale: %g, %g, %g, dimensions: %d, %d, userScalable: %d\n",
initialScale, minimumScale, maximumScale, width, height, userScalable);
if (!mMetaViewport)
mMetaViewport = new BrowserMetaViewport;
mMetaViewport->initialScale = initialScale;
mMetaViewport->minimumScale = minimumScale;
mMetaViewport->maximumScale = maximumScale;
mMetaViewport->userScalable = userScalable;
NPVariant args[6];
DOUBLE_TO_NPVARIANT(initialScale, args[0]);
DOUBLE_TO_NPVARIANT(minimumScale, args[1]);
DOUBLE_TO_NPVARIANT(maximumScale, args[2]);
INT32_TO_NPVARIANT(width, args[3]);
INT32_TO_NPVARIANT(height, args[4]);
BOOLEAN_TO_NPVARIANT(userScalable, args[5]);
InvokeEventListener(gMetaViewportSetHandler, args, G_N_ELEMENTS(args), NULL);
}
void BrowserAdapter::msgCopySuccessResponse(int32_t queryNum, bool isEditing)
{
std::auto_ptr<CopySuccessCallbackArgs> args(m_copySuccessCallbackArgs.get(queryNum));
if (!args.get()) {
TRACEF("Args requests are overlapping");
return;
}
NPVariant jsCallResult;
NPVariant jsCallArgs[1];
BOOLEAN_TO_NPVARIANT(isEditing, jsCallArgs[0]);
if (args->m_callback) { // a callback was provided
if (NPN_InvokeDefault(args->m_callback, jsCallArgs, G_N_ELEMENTS(jsCallArgs), &jsCallResult)) {
TRACEF("copy response call success.");
}
else {
TRACEF("copy response call FAILED.");
}
}
}
bool BrowserAdapter::isFrozen()
{
return mFrozen;
}
void BrowserAdapter::freeze()
{
const char* identifier = (const char*) NPN_GetValue((NPNVariable) npPalmApplicationIdentifier);
g_message("%s: %p: %s", __PRETTY_FUNCTION__, this, identifier);
if (mFrozen)
return;
mFrozen = true;
delete mFrozenSurface;
mFrozenSurface = 0;
if (mOffscreenCurrent) {
BrowserOffscreenInfo* info = mOffscreenCurrent->header();
QImage offscreenSurf = mOffscreenCurrent->surface();
int width = offscreenSurf.width() * kFrozenSurfaceScale;
int height = offscreenSurf.height() * kFrozenSurfaceScale;
mFrozenSurface = new QImage(offscreenSurf.scaled(width, height));
mFrozenRenderPos.x = info->renderedX;
mFrozenRenderPos.y = info->renderedY;
mFrozenRenderWidth = info->renderedWidth;
mFrozenRenderHeight = info->renderedHeight;
mFrozenZoomFactor = info->contentZoom;
}
// ---------------------------------------------------------------
delete mOffscreen0;
mOffscreen0 = 0;
delete mOffscreen1;
mOffscreen1 = 0;
delete mOffscreen2;
mOffscreen2 = 0;
mOffscreenCurrent = 0;
asyncCmdFreeze();
}
void BrowserAdapter::thaw()
{
const char* identifier = (const char*) NPN_GetValue((NPNVariable) npPalmApplicationIdentifier);
g_message("%s: %p: %s", __PRETTY_FUNCTION__, this, identifier);
if (!mFrozen)
return;
TRACEF("BrowserAdapter::thaw %p\n", this);
mFrozen = false;
if (!init()) {
g_critical("%s: failed to initialize adapter", __PRETTY_FUNCTION__);
return;
}
asyncCmdThaw(mOffscreen0->key(), mOffscreen1->key(),
mOffscreen0->size());
// Thaw carries two keys for the same frozen-shape reason as Connect, and init() above may
// have allocated a NEW third segment, so it has to be re-announced or the daemon would keep
// rotating onto a key that no longer exists.
if (mOffscreen2)
asyncCmdSetExtraBuffer(mOffscreen2->key(), mOffscreen2->size());
// don't release frozen at this point, wait msgPainted event coming back!
}
void BrowserAdapter::handlePaintInFrozenState(NpPalmDrawEvent* event)
{
if (!mFrozenSurface) {
TRACEF("Frozen surface null");
return;
}
QPainter* gc = (QPainter*) event->graphicsContext;
gc->save();
gc->translate(mWindow.x, mWindow.y);
gc->setClipRect(QRect(0, 0, mWindow.width, mWindow.height));
gc->translate(-mScrollPos.x, -mScrollPos.y);
// -- Checkerboard ---------------------------------------------------
#ifdef QT_FIXME
BrowserRect windowRect(mScrollPos.x,
mScrollPos.y + m_headerHeight,
mWindow.width,
mWindow.height - m_headerHeight);
int pw = mDirtyPattern->width();
int ph = mDirtyPattern->height();
int dstX = windowRect.x() % pw - pw;
int dstY = windowRect.y() % ph - ph;
int dstR = windowRect.r() % pw + pw + windowRect.w();
int dstB = windowRect.b() % ph + ph + windowRect.h();
gc->drawPattern(mDirtyPattern,
windowRect.x() % pw,
windowRect.y() % ph,
dstX, dstY, dstR, dstB);
#endif
// -- Scaled draw of frozen surface ----------------------------------
int centerOfSurfX = mFrozenRenderPos.x + mFrozenRenderWidth / 2;
int centerOfSurfY = mFrozenRenderPos.y + mFrozenRenderHeight / 2;
float zoomFactor = mZoomLevel / mFrozenZoomFactor;
centerOfSurfX *= zoomFactor;
centerOfSurfY *= zoomFactor;
gc->translate(centerOfSurfX, centerOfSurfY);
gc->scale(zoomFactor, zoomFactor);
gc->drawImage(QRect(- mFrozenRenderWidth / 2,
- mFrozenRenderHeight / 2,
mFrozenRenderWidth / 2,
mFrozenRenderHeight / 2),
*mFrozenSurface);
gc->restore();
if (mShowHighlight) {
showHighlight(gc);
}
}
/**
* @brief invalidates highlight rectangle regions and clears vector of rectangles
*
*/
void BrowserAdapter::removeHighlight()
{
invalidateHighlightRectsRegion();
mHighlightRects.clear();
mShowHighlight = false;
}
void BrowserAdapter::invalidateHighlightRectsRegion()
{
// remove rectangles by invalidating their areas
/*
for (std::vector<BrowserAdapterRectangle>::const_iterator rect_iter = mHighlightRects.begin();
rect_iter != mHighlightRects.end();
++rect_iter)
{
BrowserAdapterRectangle r = *rect_iter;
NPRect dirty;
dirty.left = r.left() * mZoomLevel + mJsScrollX;
dirty.top = r.top() * mZoomLevel + mJsScrollY;
dirty.right = r.right() * mZoomLevel + mJsScrollX;
dirty.bottom = r.bottom() * mZoomLevel + mJsScrollY;
NPN_InvalidateRect(&dirty);
}
*/
invalidate();
}
int BrowserAdapter::showSpotlight(QPainter* gc)
{
/* FIXME: RR
BrowserRect scaledSpotlightRect(ceil(m_spotlightRect.x()*mZoomLevel) + mJsScrollX,
ceil(m_spotlightRect.y()*mZoomLevel) + mJsScrollY,
floor(m_spotlightRect.r()*mZoomLevel) - ceil(m_spotlightRect.x()*mZoomLevel),
floor(m_spotlightRect.b()*mZoomLevel) - ceil(m_spotlightRect.y()*mZoomLevel) );
//draw a grey drop shadow
if(!m_spotlightHandle)
return 0;
//draw a grey drop shadow
gc->push();
//gc->setStrokeColor(QColor(0x69, 0x69, 0x69, 0x69));
// gc->setFillColor(QColor(0x69, 0x69, 0x69, 0x69));
int x1=scaledSpotlightRect.x();
int y1=scaledSpotlightRect.y();
int x2=scaledSpotlightRect.r();
int y2=scaledSpotlightRect.b();
PGSurface* imgSurfaceTop=PGSurface::createFromPNGFile(kImgTopFile);
PGSurface* imgSurfaceBot=PGSurface::createFromPNGFile(kImgBotFile);
if(m_spotlightHandle==2) //when drop shadow is completely around the rect
{
//draw completely around the flash window
PGSurface* imgSurfaceLeft=PGSurface::createFromPNGFile(kImgLeftFile);
PGSurface* imgSurfaceLeftBotCorner=PGSurface::createFromPNGFile(kImgBotLeftFile);
PGSurface* imgSurfaceRightBotCorner=PGSurface::createFromPNGFile(kImgBotRightFile);
PGSurface* imgSurfaceRight=PGSurface::createFromPNGFile(kImgRightFile);
PGSurface* imgSurfaceRightTop=PGSurface::createFromPNGFile(kImgTopLeftFile);
PGSurface* imgSurfaceLeftTop=PGSurface::createFromPNGFile(kImgTopRightFile);
//draw the left
if(imgSurfaceLeft!=NULL)
{
gc->bitblt(imgSurfaceLeft,
0, 0,
imgSurfaceLeft->width()-1, imgSurfaceLeft->height()-1,
x1-20, y1, x1, y2);
}
//draw left bottom corner
if(imgSurfaceLeftBotCorner!=NULL)
{
gc->bitblt(imgSurfaceLeftBotCorner,
0, 0,
imgSurfaceLeftBotCorner->width()-1, imgSurfaceLeftBotCorner->height()-1,
x1-20, y2-20, x1, y2);
}
//draw the bottom
if(imgSurfaceBot!=NULL)
{
gc->bitblt(imgSurfaceBot,
0, 0,
imgSurfaceBot->width()-1, imgSurfaceBot->height()-1,
x1, y2, x2, y2+20);
}
//draw the bottom right
if(imgSurfaceRightBotCorner!=NULL)
{
gc->bitblt(imgSurfaceRightBotCorner,
0, 0,
imgSurfaceRightBotCorner->width()-1, imgSurfaceRightBotCorner->height()-1,
x2, y2, x2+20, y2+20);
}
//draw the right
if(imgSurfaceRight!=NULL)
{
//TRACE("drawDropShadow:imgSurfaceRight");
gc->bitblt(imgSurfaceRight,
0, 0,
imgSurfaceRight->width()-1, imgSurfaceRight->height()-1,
x2, y1, x2+20, y2);
}
//draw the right top
if(imgSurfaceRightTop!=NULL)
{
gc->bitblt(imgSurfaceRightTop,
0, 0,
imgSurfaceRightTop->width()-1, imgSurfaceRightTop->height()-1,
x2, y1-20, x2+20, y1);
}
//draw the left top
if(imgSurfaceLeftTop!=NULL)
{
gc->bitblt(imgSurfaceLeftTop,
0, 0,
imgSurfaceLeftTop->width()-1, imgSurfaceLeftTop->height()-1,
x2, y2, x1+20, y1-20);
}
//draw the top
if(imgSurfaceTop!=NULL)
{
gc->bitblt(imgSurfaceTop,
0, 0,
imgSurfaceTop->width()-1, imgSurfaceTop->height()-1,
x1, y1-20, x2, y1);
}
}
else if(m_spotlightHandle==1) //only when dropshadow on top and bottom of rect
{
//draw only below the flash window and above flash window
if(imgSurfaceBot != NULL)
{
//draw it around the flash area
gc->bitblt(imgSurfaceBot,
0, 0,
imgSurfaceBot->width()-1, imgSurfaceBot->height()-1,
x1, y2, x2, y2+20);
}
if(imgSurfaceTop!=NULL)
{
gc->bitblt(imgSurfaceTop,
0, 0,
imgSurfaceTop->width()-1, imgSurfaceTop->height()-1,
x1, y1-20, x2, y1);
}
}
gc->pop();
*/
return 1;
}
/**
* @brief draws highlight rectangles in current context
*
*
*/
int BrowserAdapter::showHighlight(QPainter* gc)
{
gc->save();
gc->setPen(QColor(0, 0, 0, 0)); // no border around rects
gc->setBrush(QBrush(QColor(0, 0, 0, 60)));
gc->translate(mWindow.x, mWindow.y);
gc->setClipRect(QRect(0, 0, mWindow.width, mWindow.height));
gc->translate(-mScrollPos.x, m_headerHeight-mScrollPos.y);
for (std::vector<BrowserRect>::const_iterator rect_iter = mHighlightRects.begin();
rect_iter != mHighlightRects.end();
++rect_iter)
{
BrowserRect r = *rect_iter;
BrowserRect d[4];
int count = 0;
for (RectMap::const_iterator i = mFlashRects.begin();
i != mFlashRects.end();
++i)
{
BrowserRect f = i->second;
if (r.intersects(f)) {
// just do one for now
/*
TRACEF("rect %d,%d,%d,%d intersects with flash rect %d,%d,%d,%d", r.x(), r.y(), r.r(), r.b(), f.x(), f.y(), f.r(), f.b());
*/
count = r.subtract(f, d);
break;
}
}
for (RectMap::const_iterator i = mDefaultInteractiveRects.begin();
i != mDefaultInteractiveRects.end();
++i)
{
BrowserRect f = i->second;
if (r.intersects(f)) {
// just do one for now
/*
TRACEF("rect %d,%d,%d,%d intersects with irect %d,%d,%d,%d", r.x(), r.y(), r.r(), r.b(), f.x(), f.y(), f.r(), f.b());
*/
count = r.subtract(f, d);
break;
}
}
if (count == 0) {
gc->drawRect(QRect(r.x() * mZoomLevel,
r.y() * mZoomLevel,
r.r() * mZoomLevel,
r.b() * mZoomLevel));
} else {
for (int j = 0; j < count; j++) {
gc->drawRect(QRect(d[j].x() * mZoomLevel,
d[j].y() * mZoomLevel,
d[j].r() * mZoomLevel,
d[j].b() * mZoomLevel));
}
}
/*
TRACEF("DREW rect with left: %d, top: %d, right: %d, bottom: %d",
int(d.x() * mZoomLevel + mJsScrollX),
int(d.y() * mZoomLevel + mJsScrollY),
int(d.r() * mZoomLevel + mJsScrollX),
int(d.b() * mZoomLevel + mJsScrollY));
*/
}
gc->restore();
return mHighlightRects.size();
}
void BrowserAdapter::updateMouseInFlashStatus(bool inFlashRect)
{
if (inFlashRect != mMouseInFlashRect) {
mMouseInFlashRect = inFlashRect;
// let BA know of change
TRACEF("Updating mouseInFlash status to : %d\n", inFlashRect);
NPVariant arg;
BOOLEAN_TO_NPVARIANT(inFlashRect, arg);
InvokeEventListener(gMouseInFlashChangeHandler, &arg, 1, 0);
}
}
void BrowserAdapter::updateMouseInInteractiveStatus(bool inInteractiveRect)
{
if (inInteractiveRect != mMouseInInteractiveRect) {
mMouseInInteractiveRect = inInteractiveRect;
// let BA know of change
TRACEF("Updating mouseInInteractive status to : %d\n", inInteractiveRect);
NPVariant arg;
BOOLEAN_TO_NPVARIANT(inInteractiveRect, arg);
InvokeEventListener(gMouseInInteractiveChangeHandler, &arg, 1, 0);
}
}
void BrowserAdapter::updateFlashGestureLockStatus(bool gestureLockEnabled)
{
if (gestureLockEnabled != mFlashGestureLock) {
mFlashGestureLock = gestureLockEnabled;
// Jihad: this is the ONLY place the latch changes, and the keyboard-arbitration gate
// (handleKeyDown/Up) reads it. TRACEF below compiles to nothing, so until this line the
// latch's runtime value was unobservable on a device — and the open arbitration
// criterion (cavekit-input-bridging.md R7) is precisely "is it true during play".
// g_message reaches the system log, readable with palm-log / /var/log/messages.
g_message("jihad: flashGestureLock -> %d (mouseInFlashRect=%d)",
(int)gestureLockEnabled, (int)mMouseInFlashRect);
// Jihad (cavekit-input-bridging.md R7): the keyboard for a plugin. The VKB is driven by
// the card's editorFocused handler, which only an HTML editor ever triggers
// (msgEditorFocused) — a plugin never does, so on a device with no hardware keyboard a
// plugin that takes keys (a game, a Flash text field) could be engaged and still not be
// typed into. Nothing in this NPAPI tells the host that a plugin WANTS text, so the
// signal is the user's own: the latch, i.e. the double tap that engages the plugin
// (handlePenDoubleClick, and the card's smartZoom; a single tap does not set it). Engaging raises the keyboard through
// the same handler an HTML field uses; releasing the latch (a tap outside the plugin)
// lowers it again unless an HTML editor still has focus.
{
NPVariant kb[3];
BOOLEAN_TO_NPVARIANT(gestureLockEnabled || bEditorFocused, kb[0]);
INT32_TO_NPVARIANT(0, kb[1]);
INT32_TO_NPVARIANT(0, kb[2]);
InvokeEventListener(gEditorFocusedHandler, kb, G_N_ELEMENTS(kb), 0);
}
// Send a pen up if necessary since we get a penDown before gestureStart
if (!gestureLockEnabled && mLastPointSentToFlash.x != -1 && mLastPointSentToFlash.y != -1) {
gettimeofday(&m_lastPassedEventTime, NULL);
asyncCmdMouseEvent(1 /*mouseup */, mLastPointSentToFlash.x, mLastPointSentToFlash.y, 1);
mLastPointSentToFlash = Point(-1, -1);
}
// let BA know of change
TRACEF("Updating flashGestureLock status to : %d\n", gestureLockEnabled);
NPVariant arg;
BOOLEAN_TO_NPVARIANT(gestureLockEnabled, arg);
InvokeEventListener(gFlashGestureLockHandler, &arg, 1, 0);
}
}
bool BrowserAdapter::flashRectContainsPoint(const Point& pt)
{
bool ret = rectContainsPoint(mFlashRects, pt);
if (ret) {
TRACEF("point x: %d, y: %d in flash rect\n", pt.x, pt.y);
} else {
TRACEF("point x: %d, y: %d NOT in rect\n", pt.x, pt.y);
}
return ret;
}
bool BrowserAdapter::interactiveRectContainsPoint(const Point& pt)
{
bool ret = rectContainsPoint(mDefaultInteractiveRects, pt);
if (ret) {
TRACEF("point x: %d, y: %d in interactive rect\n", pt.x, pt.y);
} else {
TRACEF("point x: %d, y: %d NOT in interactive rect\n", pt.x, pt.y);
}
return ret;
}
bool BrowserAdapter::rectContainsPoint(RectMap &rectMap, const Point& pt)
{
if (rectMap.size() == 0) {
return false;
}
BrowserRect point(pt.x, pt.y, 1, 1);
for (RectMap::const_iterator i = rectMap.begin();
i != rectMap.end();
++i)
{
BrowserRect r = i->second;
if (r.overlaps(point)) {
return true;
}
}
return false;
}
const char* BrowserAdapter::js_saveImageAtPoint(AdapterBase *adapter, const NPVariant *args, uint32_t argCount, NPVariant *result)
{
if ( argCount != 4
|| !IsIntegerVariant(args[0])
|| !IsIntegerVariant(args[1])
|| !NPVARIANT_IS_STRING(args[2])
|| !NPVARIANT_IS_OBJECT(args[3])
) {
INT32_TO_NPVARIANT( kInvalidParam, *result );
return "BrowserAdapter::saveImageAtPoint(x, y, outDir, cb): Bad arguments.";
}
BrowserAdapter *proxy = GetAndInitAdapter(adapter);
if (proxy->mBrowserServerConnected) {
int x = VariantToInteger(args[0]);
int y = VariantToInteger(args[1]);
char* outDir = NPStringToString(NPVARIANT_TO_STRING(args[2]));
int nQueryNum = proxy->mBsQueryNum++;
SaveImageAtPointArgs* callArgs = new SaveImageAtPointArgs(x, y,
NPVARIANT_TO_OBJECT(args[3]), nQueryNum);
proxy->m_saveImageAtPointArgs.add(callArgs);
x = (x + proxy->mScrollPos.x) / proxy->mZoomLevel;
y = (y + proxy->mScrollPos.y) / proxy->mZoomLevel;
proxy->asyncCmdSaveImageAtPoint(nQueryNum, x, y, outDir);
::free(outDir);
return NULL;
}
else {
return "BrowserAdapter::saveImageAtPoint() - Not connected to server";
}
}
void
BrowserAdapter::msgSaveImageAtPointResponse(int32_t queryNum, bool succeeded, const char* filepath)
{
TRACEF("SaveImageAtPointResponse: %c, '%s'", succeeded ? 'T' : 'F', filepath);
std::auto_ptr<SaveImageAtPointArgs> args(m_saveImageAtPointArgs.get(queryNum));
if (!args.get()) {
TRACEF("Can't find response for this call.");
return;
}
NPVariant jsCallResult;
NPVariant jsCallArgs[2];
BOOLEAN_TO_NPVARIANT(succeeded, jsCallArgs[0]);
if (filepath == NULL)
filepath = "";
STRINGZ_TO_NPVARIANT(filepath, jsCallArgs[1]);
if (NPN_InvokeDefault(args->m_callback, jsCallArgs, G_N_ELEMENTS(jsCallArgs), &jsCallResult)) {
TRACEF("saveImageAtPoint response call success.");
}
else {
TRACEF("saveImageAtPoint response call FAILED.");
}
}
void
BrowserAdapter::msgCopiedToClipboard()
{
NPVariant jsCallResult, jsCallArgs;
STRINGZ_TO_NPVARIANT("PalmSystem.copiedToClipboard()", jsCallArgs);
InvokeAdapter(gEval, &jsCallArgs, 1, &jsCallResult);
}
void
BrowserAdapter::msgPastedFromClipboard()
{
// stub for sending notifications when paste succeeds
}
void
BrowserAdapter::initSelectionReticleSurface()
{
mSelectionReticle.surface = NULL;
mSelectionReticle.timeoutSource = NULL;
mSelectionReticle.x = 0;
mSelectionReticle.y = 0;
mSelectionReticle.centerOffsetX = 0;
mSelectionReticle.centerOffsetY = 0;
mSelectionReticle.show = false;
mSelectionReticle.surface = new QImage(kSelectionReticleFile);
if (mSelectionReticle.surface) {
mSelectionReticle.centerOffsetX = mSelectionReticle.surface->width()/2;
mSelectionReticle.centerOffsetY = mSelectionReticle.surface->height()/2;
}
}
void
BrowserAdapter::showSelectionReticle(QPainter* gc)
{
if (!mSelectionReticle.surface) {
return;
}
/* FIXME: RR
gc->bitblt(mSelectionReticle.surface,
mSelectionReticle.x * mZoomLevel + mJsScrollX - mSelectionReticle.centerOffsetX,
mSelectionReticle.y * mZoomLevel + mJsScrollY - mSelectionReticle.centerOffsetY,
mSelectionReticle.x * mZoomLevel + mJsScrollX + mSelectionReticle.centerOffsetX,
mSelectionReticle.y * mZoomLevel + mJsScrollY + mSelectionReticle.centerOffsetY);
*/
}
void BrowserAdapter::invalidateSelectionReticleRegion()
{
if (!mSelectionReticle.surface) {
return;
}
invalidate();
}
void
BrowserAdapter::msgRemoveSelectionReticle()
{
if (mSelectionReticle.timeoutSource) {
return;
}
mSelectionReticle.timeoutSource = g_timeout_source_new(250); // in ms
g_source_set_callback(mSelectionReticle.timeoutSource, &BrowserAdapter::removeSelectionReticleCallback, this, NULL);
g_source_attach(mSelectionReticle.timeoutSource, g_main_loop_get_context(mMainLoop));
}
gboolean
BrowserAdapter::removeSelectionReticleCallback(gpointer arg)
{
BrowserAdapter* adapter = (BrowserAdapter*)arg;
adapter->mSelectionReticle.show = false;
adapter->invalidateSelectionReticleRegion();
if (adapter->mSelectionReticle.timeoutSource != NULL) {
g_source_destroy(adapter->mSelectionReticle.timeoutSource);
g_source_unref(adapter->mSelectionReticle.timeoutSource);
adapter->mSelectionReticle.timeoutSource = NULL;
}
return FALSE;
}
void
BrowserAdapter::msgPluginFullscreenSpotlightCreate(int32_t spotlightHandle,
int32_t rectX, int32_t rectY, int32_t rectWidth, int32_t rectHeight)
{
NPVariant args[6];
DOUBLE_TO_NPVARIANT((double)rectX, args[0]);
DOUBLE_TO_NPVARIANT((double)rectY, args[1]);
DOUBLE_TO_NPVARIANT((double)rectX+rectWidth, args[2]);
DOUBLE_TO_NPVARIANT((double)rectY+rectHeight, args[3]);
STRINGZ_TO_NPVARIANT("fullscreen", args[4]);
BOOLEAN_TO_NPVARIANT(true, args[5]);
InvokeEventListener(gPluginSpotlightCreate, args, G_N_ELEMENTS(args), NULL);
}
void
BrowserAdapter::msgPluginFullscreenSpotlightRemove()
{
NPVariant args[0];
InvokeEventListener(gPluginSpotlightRemove, args, G_N_ELEMENTS(args), NULL);
}
void
BrowserAdapter::msgSpellingWidgetVisibleRectUpdate(int32_t rectX, int32_t rectY, int32_t rectWidth, int32_t rectHeight)
{
TRACEF("**** BrowserAdapter msgSpellingWidgetVisibleRectUpdate %d,%d,%d,%d",rectX,rectY,rectWidth,rectHeight);
NPVariant args[4];
DOUBLE_TO_NPVARIANT((double)rectX, args[0]);
DOUBLE_TO_NPVARIANT((double)rectY, args[1]);
DOUBLE_TO_NPVARIANT((double)rectWidth, args[2]);
DOUBLE_TO_NPVARIANT((double)rectHeight, args[3]);
InvokeEventListener(gMsgSpellingWidgetVisibleRectUpdate, args, G_N_ELEMENTS(args), NULL);
}
// The hit-test answer's schema: isis's HitTest.schema fields, every one optional, nothing
// else. The adapter carries it instead of reading the device's /etc/palm/browser/HitTest.schema:
// that file does not compile in the pbnjson this adapter links (measured 2026-10-08 — it rejected
// even "{}"), and a rejected parse leaves the hit-test object EMPTY, so the cards saw no link and no
// image under any long-press and only ever offered the page's menu. Written in the dialect the
// device's pbnjson reads ("optional", as packaging/conf/InteractiveWidgetRect.schema).
static const char kJihadHitTestSchema[] =
"{\"type\":\"object\",\"properties\":{"
"\"isNull\":{\"type\":\"boolean\",\"optional\":true},"
"\"isLink\":{\"type\":\"boolean\",\"optional\":true},"
"\"isImage\":{\"type\":\"boolean\",\"optional\":true},"
"\"x\":{\"type\":\"number\",\"optional\":true},"
"\"y\":{\"type\":\"number\",\"optional\":true},"
"\"bounds\":{\"type\":\"object\",\"optional\":true,\"properties\":{"
"\"left\":{\"type\":\"number\",\"optional\":true},"
"\"top\":{\"type\":\"number\",\"optional\":true},"
"\"right\":{\"type\":\"number\",\"optional\":true},"
"\"bottom\":{\"type\":\"number\",\"optional\":true}}},"
"\"element\":{\"type\":\"string\",\"optional\":true},"
"\"title\":{\"type\":\"string\",\"optional\":true},"
"\"linkText\":{\"type\":\"string\",\"optional\":true},"
"\"linkUrl\":{\"type\":\"string\",\"optional\":true},"
"\"linkTitle\":{\"type\":\"string\",\"optional\":true},"
"\"altText\":{\"type\":\"string\",\"optional\":true},"
"\"imageUrl\":{\"type\":\"string\",\"optional\":true},"
"\"editable\":{\"type\":\"boolean\",\"optional\":true},"
"\"selected\":{\"type\":\"boolean\",\"optional\":true}},"
"\"additionalProperties\":false}";
void BrowserAdapter::msgHitTestResponse(int32_t queryNum,
const char *hitTestResultJson)
{
TRACEF("json: %s", hitTestResultJson);
std::auto_ptr<HitTestArgs> args(m_hitTestArgs.get(queryNum));
if ((args.get() != NULL)) {
NPVariant jsCallResult, jsCallArgs[2];
NPObject *event = NPN_CreateObject(&NPObjectEvent::sNPObjectEventClass);
if (event) {
NPObject *hitTest =
NPN_CreateObject(&JsonNPObject::sJsonNPObjectClass);
if (hitTest) {
OBJECT_TO_NPVARIANT(event, jsCallArgs[0]);
OBJECT_TO_NPVARIANT(hitTest, jsCallArgs[1]);
static_cast<NPObjectEvent*>(event)->initialize(args->m_type,
args->m_pt.x * mZoomLevel - mScrollPos.x,
args->m_pt.y * mZoomLevel - mScrollPos.y, args->m_modifiers);
// Validated against the adapter's own copy of the answer's schema (above);
// an answer it rejects leaves the object empty and the hold falls back to
// the page's menu.
{
pbnjson::JSchemaFragment schema(kJihadHitTestSchema);
if (!static_cast<JsonNPObject *>(hitTest)->initialize(schema, hitTestResultJson)) {
if (FILE* lf = jihadPaintLog())
fprintf(lf, "[hit-test] answer rejected by the schema: %.200s\n",
hitTestResultJson ? hitTestResultJson : "(null)");
}
}
InvokeEventListener(gEventFiredHandler, jsCallArgs, 2,
&jsCallResult);
TRACEF("result %d %s", !VariantToBoolean(jsCallResult), args->m_type);
if (!VariantToBoolean(jsCallResult)) {
if (!strcmp(args->m_type, "click")) {
asyncCmdClickAt(args->m_pt.x, args->m_pt.y, 1, 1);
} else if (!strcmp(args->m_type, "mousehold")) {
if (args->m_gestureGen != m_gestureGen) {
// A pinch began after this hold was asked for (see doGestureStart).
TRACEF("hold at %d,%d dropped: a gesture began since", args->m_pt.x, args->m_pt.y);
} else {
m_didHold = true;
asyncCmdHoldAt(args->m_pt.x, args->m_pt.y);
}
}
}
AdapterBase::NPN_ReleaseVariantValue(&jsCallArgs[0]);
AdapterBase::NPN_ReleaseVariantValue(&jsCallArgs[1]);
}
}
}
}
bool BrowserAdapter::shouldPassInputEvents()
{
return m_passInputEvents
|| (mContentWidth <= mViewportWidth
&& mContentHeight <= mViewportHeight
&& mMetaViewport && !mMetaViewport->userScalable);
}
// Touch events can be safely passed even if the content dimensions exceed the viewport dimensions
bool BrowserAdapter::shouldPassTouchEvents()
{
return m_passInputEvents || (mMetaViewport && !mMetaViewport->userScalable);
}
// What the daemon is told of our vertical scroll: the PAGE's, i.e. ours less the header strip
// the card keeps above the page (m_headerHeight; 0 in the Enyo and Mochi shells).
int BrowserAdapter::serverScrollY() const
{
int y = mScrollPos.y - m_headerHeight;
return y > 0 ? y : 0;
}
void BrowserAdapter::fireScrolledToEvent(int x, int y)
{
if( gScrolledToHandler ) {
NPVariant args[2];
INT32_TO_NPVARIANT(x, args[0]);
INT32_TO_NPVARIANT(y, args[1]);
InvokeEventListener(gScrolledToHandler, args, G_N_ELEMENTS(args), 0 );
}
}
void BrowserAdapter::scrollTo(int x, int y)
{
// ignore any callbacks from kinetic scroller if we are in the middle
// of a zoom animation or in a gesture change
if (m_zoomTimerSource || mInGestureChange)
return;
mScrollPos.x = (mContentWidth > (int) mWindow.width) ? -x : 0;
mScrollPos.y = -y;
asyncCmdSetScrollPosition(mScrollPos.x, serverScrollY(),
mScrollPos.x + mWindow.width,
serverScrollY() + mWindow.height);
invalidate();
fireScrolledToEvent(x, y);
}
gboolean BrowserAdapter::clickTimeoutCb(gpointer arg)
{
BrowserAdapter *a = (BrowserAdapter *)arg;
if (a->mBrowserServerConnected) {
int q = a->mBsQueryNum++;
HitTestArgs *callArgs = new HitTestArgs("click", a->m_clickPt, 0, q);
a->m_hitTestArgs.add(callArgs);
TRACEF("x: %d, y: %d, q: %d", a->m_clickPt.x, a->m_clickPt.y, q);
a->asyncCmdHitTest(q, a->m_clickPt.x, a->m_clickPt.y);
}
a->stopClickTimer();
return FALSE;
}
void BrowserAdapter::startClickTimer()
{
if (m_clickTimerSource)
stopClickTimer();
m_clickTimerSource = g_timeout_source_new(kDoubleClickInterval);
g_source_set_callback(m_clickTimerSource, clickTimeoutCb, this /*data*/, NULL);
g_source_attach(m_clickTimerSource, g_main_loop_get_context(mMainLoop));
}
void BrowserAdapter::stopClickTimer()
{
if (m_clickTimerSource) {
g_source_destroy(m_clickTimerSource);
g_source_unref(m_clickTimerSource);
m_clickTimerSource = NULL;
}
}
gboolean BrowserAdapter::mouseHoldTimeoutCb(gpointer arg)
{
BrowserAdapter *a = (BrowserAdapter *)arg;
if (a->mBrowserServerConnected
&& !a->flashRectContainsPoint(a->m_penDownDoc)) {
int q = a->mBsQueryNum++;
HitTestArgs *callArgs = new HitTestArgs("mousehold", a->m_penDownDoc, 0, q);
callArgs->m_gestureGen = a->m_gestureGen;
a->m_hitTestArgs.add(callArgs);
TRACEF("x: %d, y: %d, q: %d", a->m_penDownDoc.x, a->m_penDownDoc.y, q);
a->asyncCmdHitTest(q, a->m_penDownDoc.x, a->m_penDownDoc.y);
}
a->stopMouseHoldTimer();
return FALSE;
}
void BrowserAdapter::startMouseHoldTimer()
{
if (m_mouseHoldTimerSource)
stopMouseHoldTimer();
m_mouseHoldTimerSource = g_timeout_source_new(kMouseHoldInterval);
g_source_set_callback(m_mouseHoldTimerSource, mouseHoldTimeoutCb, this /*data*/, NULL);
g_source_attach(m_mouseHoldTimerSource, g_main_loop_get_context(mMainLoop));
}
void BrowserAdapter::stopMouseHoldTimer()
{
if (m_mouseHoldTimerSource) {
g_source_destroy(m_mouseHoldTimerSource);
g_source_unref(m_mouseHoldTimerSource);
m_mouseHoldTimerSource = NULL;
}
}
gboolean BrowserAdapter::zoomTimerTimeoutCb(gpointer arg)
{
BrowserAdapter *a = (BrowserAdapter *)arg;
return a->animateZoom();
}
bool BrowserAdapter::animateZoom()
{
if (PrvIsEqual(mZoomLevel, m_zoomTarget)) {
if (mPageWidth) {
double fitZoom = mWindow.width * 1.0 / mPageWidth;
if (PrvIsEqual(mZoomLevel, fitZoom))
mZoomFit = true;
else
mZoomFit = false;
}
asyncCmdSetZoomAndScroll(mZoomLevel, mScrollPos.x, serverScrollY());
stopZoomAnimation();
} else {
if (::fabs(m_zoomTarget - mZoomLevel) < ::fabs(m_zoomLevelInterval)) {
mZoomLevel = m_zoomTarget;
mContentWidth = ::round(mZoomLevel * mPageWidth);
mContentHeight = ::round(mZoomLevel * mPageHeight);
mScrollPos.x = m_zoomPt.x;
mScrollPos.y = m_zoomPt.y;
} else {
mZoomLevel += m_zoomLevelInterval;
mContentWidth = ::round(mZoomLevel * mPageWidth);
mContentHeight = ::round(mZoomLevel * mPageHeight);
mScrollPos.x += m_zoomXInterval;
mScrollPos.y += m_zoomYInterval;
}
mScroller->setContentDimensions(mContentWidth, mContentHeight + m_headerHeight);
mScroller->scrollTo(-mScrollPos.x, -mScrollPos.y, false);
invalidate();
}
return TRUE;
}
void BrowserAdapter::startZoomAnimation(double zoom, int x, int y)
{
if (m_zoomTimerSource) {
stopZoomAnimation();
}
m_zoomTarget = zoom;
m_zoomPt.set(x, y);
m_zoomXInterval = (x - mScrollPos.x) / kZoomAnimationSteps;
m_zoomYInterval = (y - mScrollPos.y) / kZoomAnimationSteps;
m_zoomLevelInterval = (zoom - mZoomLevel) / kZoomAnimationSteps;
m_zoomTimerSource = g_timeout_source_new(kTimerInterval);
g_source_set_callback(m_zoomTimerSource, zoomTimerTimeoutCb, this /*data*/, NULL);
g_source_attach(m_zoomTimerSource, g_main_loop_get_context(mMainLoop));
}
void BrowserAdapter::stopZoomAnimation()
{
if (m_zoomTimerSource) {
g_source_destroy(m_zoomTimerSource);
g_source_unref(m_zoomTimerSource);
m_zoomTimerSource = NULL;
}
}
void BrowserAdapter::msgUpdateScrollableLayers(const char* json)
{
#ifdef FIXME_QT
if (!json)
return;
mScrollableLayers.clear();
resetScrollableLayerScrollSession();
json_object* root = json_tokener_parse(json);
if (!root || is_error(root)) {
return;
}
array_list* array = json_object_get_array(root);
if (!array || is_error(array)) {
json_object_put(root);
return;
}
for (int i = 0; i < array_list_length(array); i++) {
BrowserScrollableLayer layer;
json_object* obj = (json_object*) array_list_get_idx(array, i);
json_object* l = 0;
int left, right, top, bottom;
l = json_object_object_get(obj, "id");
layer.id = json_object_get_int(l);
l = json_object_object_get(obj, "left");
left = json_object_get_int(l);
l = json_object_object_get(obj, "right");
right = json_object_get_int(l);
l = json_object_object_get(obj, "top");
top = json_object_get_int(l);
l = json_object_object_get(obj, "bottom");
bottom = json_object_get_int(l);
layer.bounds.set(left, top, right, bottom);
l = json_object_object_get(obj, "contentX");
layer.contentX = json_object_get_int(l);
l = json_object_object_get(obj, "contentY");
layer.contentY = json_object_get_int(l);
l = json_object_object_get(obj, "contentWidth");
layer.contentWidth = json_object_get_int(l);
l = json_object_object_get(obj, "contentHeight");
layer.contentHeight = json_object_get_int(l);
mScrollableLayers[layer.id] = layer;
}
json_object_put(root);
#endif /* FIXME_QT */
}
bool BrowserAdapter::detectScrollableLayerUnderMouseDown(const Point& pagePt, const Point& mousePt)
{
resetScrollableLayerScrollSession();
for (BrowserScrollableLayerMap::iterator it = mScrollableLayers.begin();
it != mScrollableLayers.end(); ++it) {
BrowserRect pt(pagePt.x, pagePt.y, 1, 1);
if (it->second.bounds.overlaps(pt)) {
mScrollableLayerScrollSession.layer = it->first;
mScrollableLayerScrollSession.contentXAtMouseDown = it->second.contentX;
mScrollableLayerScrollSession.contentYAtMouseDown = it->second.contentY;
mScrollableLayerScrollSession.mouseDownX = mousePt.x;
mScrollableLayerScrollSession.mouseDownY = mousePt.y;
break;
}
}
return (mScrollableLayerScrollSession.layer != 0);
}
bool BrowserAdapter::scrollLayerUnderMouse(const Point& currentMouseDoc)
{
if (mScrollableLayerScrollSession.layer == 0)
return false;
BrowserScrollableLayerMap::iterator it = mScrollableLayers.find(mScrollableLayerScrollSession.layer);
if (it == mScrollableLayers.end()) {
resetScrollableLayerScrollSession();
return false;
}
BrowserScrollableLayer& layer = it->second;
int dx = - (currentMouseDoc.x - mScrollableLayerScrollSession.mouseDownX);
int dy = - (currentMouseDoc.y - mScrollableLayerScrollSession.mouseDownY);
int contentXMin = 0;
int contentXMax = layer.contentWidth - (layer.bounds.r() - layer.bounds.x());
int contentYMin = 0;
int contentYMax = layer.contentHeight - (layer.bounds.b() - layer.bounds.y());
if (contentXMax <= contentXMin)
dx = 0;
if (contentYMax <= contentYMin)
dy = 0;
// Not scrollable
if (dx == 0 && dy == 0) {
resetScrollableLayerScrollSession();
return false;
}
int newContentX = mScrollableLayerScrollSession.contentXAtMouseDown + dx;
int newContentY = mScrollableLayerScrollSession.contentYAtMouseDown + dy;
newContentX = MAX(contentXMin, newContentX);
newContentX = MIN(contentXMax, newContentX);
newContentY = MAX(contentYMin, newContentY);
newContentY = MIN(contentYMax, newContentY);
if (newContentX == layer.contentX && newContentY == layer.contentY)
return false;
mScrollableLayerScrollSession.isActive = true;
dx = newContentX - layer.contentX;
dy = newContentY - layer.contentY;
layer.contentX = newContentX;
layer.contentY = newContentY;
asyncCmdScrollLayer(mScrollableLayerScrollSession.layer, dx, dy);
return true;
}
void BrowserAdapter::resetScrollableLayerScrollSession()
{
mScrollableLayerScrollSession.isActive = false;
mScrollableLayerScrollSession.layer = 0;
mScrollableLayerScrollSession.contentXAtMouseDown = 0;
mScrollableLayerScrollSession.contentYAtMouseDown = 0;
mScrollableLayerScrollSession.mouseDownX = 0;
mScrollableLayerScrollSession.mouseDownY = 0;
}
void BrowserAdapter::showActiveScrollableLayer(QPainter* gc)
{
if (!mScrollableLayerScrollSession.isActive ||
!mScrollableLayerScrollSession.layer)
return;
BrowserScrollableLayerMap::const_iterator it =
mScrollableLayers.find(mScrollableLayerScrollSession.layer);
if (it == mScrollableLayers.end()) {
resetScrollableLayerScrollSession();
return;
}
const BrowserScrollableLayer& layer = it->second;
gc->save();
gc->setBrush(QColor(0, 0, 0, 0));
gc->setPen(QColor(0x88, 0x88, 0x88, 0x80));
gc->translate(mWindow.x, mWindow.y);
gc->setClipRect(QRect(0, 0, mWindow.width, mWindow.height));
gc->translate(-mScrollPos.x, -mScrollPos.y);
gc->drawRect(layer.bounds.x() * mZoomLevel,
layer.bounds.y() * mZoomLevel,
layer.bounds.r() * mZoomLevel,
layer.bounds.b() * mZoomLevel);
gc->restore();
}
void BrowserAdapter::recordGesture(float s, float r, int cX, int cY)
{
m_recordedGestures.push_back(RecordedGestureEntry(s, r, cX, cY));
if (m_recordedGestures.size() > (size_t) kNumRecordedGestures)
m_recordedGestures.pop_front();
}
BrowserAdapter::RecordedGestureEntry BrowserAdapter::getAveragedGesture() const
{
RecordedGestureEntry avg(0, 0, 0, 0);
if (m_recordedGestures.empty()) {
avg.scale = 1.0;
return avg;
}
int index = 0;
int denom = 0;
int weight = 0;
for (std::list<RecordedGestureEntry>::const_iterator it = m_recordedGestures.begin();
it != m_recordedGestures.end(); ++it, ++index) {
const RecordedGestureEntry& e = (*it);
weight = s_recordedGestureAvgWeights[index];
avg.scale += e.scale * weight;
avg.rotate += e.rotate * weight;
avg.centerX += e.centerX * weight;
avg.centerY += e.centerY * weight;
denom += weight;
}
avg.scale /= denom;
avg.rotate /= denom;
avg.centerX /= denom;
avg.centerY /= denom;
return avg;
}
bool BrowserAdapter::destroyBufferLock()
{
bool result(false);
if (m_bufferLock) {
sem_post(m_bufferLock);
sem_close(m_bufferLock);
sem_unlink(m_bufferLockName);
delete []m_bufferLockName;
m_bufferLockName = 0;
m_bufferLock = 0;
result = true;
}
return result;
}
bool BrowserAdapter::createBufferLock()
{
if (destroyBufferLock())
incrementPostfix();
m_bufferLockName = createBufferLockName(postfix());
if (m_bufferLockName)
m_bufferLock = sem_open(m_bufferLockName, O_CREAT, S_IRUSR | S_IWUSR, 0);
// sem_open returns SEM_FAILED ((sem_t*)-1), which is non-zero — a bare `!= 0` check
// treats failure as success, then sem_post/sem_close operate on (sem_t*)-1 (EINVAL/UB)
// and the shared-framebuffer lock (IPC R2) is silently absent (Jihad review #5 M-5).
if (m_bufferLock == SEM_FAILED) {
m_bufferLock = 0;
return false;
}
return m_bufferLock != 0;
}
void BrowserAdapter::showScrollbar()
{
mScrollbarOpacity = 0xFF * 2;
stopFadeScrollbar();
}
void BrowserAdapter::startFadeScrollbar()
{
if (mScrollbarFadeSource)
return;
if (mScrollbarOpacity <= 0)
return;
mScrollbarFadeSource = g_timeout_source_new(40);
g_source_set_callback(mScrollbarFadeSource, &BrowserAdapter::scrollbarFadeoutCb, this, NULL);
g_source_attach(mScrollbarFadeSource, g_main_loop_get_context(mMainLoop));
}
void BrowserAdapter::stopFadeScrollbar()
{
if (mScrollbarFadeSource) {
g_source_destroy(mScrollbarFadeSource);
g_source_unref(mScrollbarFadeSource);
mScrollbarFadeSource = NULL;
}
}
#define SCALE_DELTA(val, div) \
if (val > 0) \
val = MAX(val/div, 1); \
else if (val < 0) \
val = MIN(val/div, -1);
gboolean BrowserAdapter::scrollbarFadeoutCb(gpointer data)
{
BrowserAdapter *a = (BrowserAdapter*)data;
a->invalidate();
int delta = a->mScrollbarOpacity;
SCALE_DELTA(delta, 2);
a->mScrollbarOpacity -= delta;
if (a->mScrollbarOpacity <= 0) {
a->mScrollbarOpacity = 0;
a->stopFadeScrollbar();
return FALSE;
}
return TRUE;
}
void BrowserAdapter::startedAnimating()
{
showScrollbar();
}
void BrowserAdapter::stoppedAnimating()
{
startFadeScrollbar();
}