@@ -71,6 +71,7 @@ class WeaveroPlugin {
7171 this . _sidebarHandler = ( event ) => this . _sidebarHandlerImpl ( event ) ;
7272 this . _contextHandler = ( event ) => this . _contextHandlerImpl ( event ) ;
7373 this . _viewContextHandler = ( event ) => this . _viewContextHandlerImpl ( event ) ;
74+ this . _toolbarHandler = ( event ) => this . _toolbarHandlerImpl ( event ) ;
7475 }
7576
7677 // ---- Utilities --------------------------------------------------------
@@ -672,6 +673,24 @@ class WeaveroPlugin {
672673 return this . _getEnableMarkdown ( ) || this . _getEnableCommentMarkdown ( ) ;
673674 }
674675
676+ /** Compact title bar — hide the menubar row in the main window and move
677+ * the window controls into the tab strip (Firefox-style "tabs in
678+ * titlebar"). Default OFF: existing users keep the standard menubar
679+ * unless they explicitly opt in. Mac-excluded by the apply method.
680+ *
681+ * Reads tolerate the pref being stored as either a bool (Prefs.set
682+ * with a boolean — the normal path) OR a string (some external tools
683+ * write `"true"` / `"false"`). Without the string check, `!!"false"`
684+ * evaluates to `true` and the feature silently stays on. */
685+ _getCompactTitleBar ( ) {
686+ try {
687+ const v = Zotero . Prefs . get ( "weavero.compactTitleBar" ) ;
688+ if ( v === undefined ) return false ;
689+ if ( typeof v === "string" ) return v . toLowerCase ( ) === "true" ;
690+ return ! ! v ;
691+ } catch ( e ) { return false ; }
692+ }
693+
675694 // ====================================================================
676695 // Per-feature toggles introduced in v0.8.1.
677696 // Pattern: each group has a master pref + per-feature children.
@@ -1730,13 +1749,23 @@ class WeaveroPlugin {
17301749 this . _resetStaleMarkers ( win && win . document ) ;
17311750 } catch ( e ) { }
17321751
1733- // 2. Reader event listeners
1752+ // 2. Reader event listeners.
1753+ // The pluginID MUST be the full addon ID ("weavero@mjthoraval"),
1754+ // not a short slug. Zotero's `Plugins.addObserver({shutdown})`
1755+ // hook calls `_unregisterEventListenerByPluginID(id)` with the
1756+ // addon ID, filtering listeners by `pluginID !== id`. A short
1757+ // slug never matches, so prior-version listeners survive plugin
1758+ // upgrades and a second registration in the new init() leaves
1759+ // TWO live listeners — visible to the user as duplicate toolbar
1760+ // buttons in standalone reader windows.
17341761 Zotero . Reader . registerEventListener (
1735- "renderSidebarAnnotationHeader" , this . _sidebarHandler , "weavero" ) ;
1762+ "renderSidebarAnnotationHeader" , this . _sidebarHandler , "weavero@mjthoraval " ) ;
17361763 Zotero . Reader . registerEventListener (
1737- "createAnnotationContextMenu" , this . _contextHandler , "weavero" ) ;
1764+ "createAnnotationContextMenu" , this . _contextHandler , "weavero@mjthoraval " ) ;
17381765 Zotero . Reader . registerEventListener (
1739- "createViewContextMenu" , this . _viewContextHandler , "weavero" ) ;
1766+ "createViewContextMenu" , this . _viewContextHandler , "weavero@mjthoraval" ) ;
1767+ Zotero . Reader . registerEventListener (
1768+ "renderToolbar" , this . _toolbarHandler , "weavero@mjthoraval" ) ;
17401769
17411770 // 3. Notifier: new reader tabs
17421771 this . _notifierIDs . push ( Zotero . Notifier . registerObserver ( {
@@ -2201,6 +2230,50 @@ class WeaveroPlugin {
22012230 this . _applyTreeIconPref ( this . _getShowTreeIcon ( ) ) ;
22022231 this . _applyInlineLinksPref ( this . _getInlineLinks ( ) ) ;
22032232 this . _applyCommentMarkdownPref ( ) ;
2233+ // Defensive: prune dead entries from `Zotero.Reader._readers`.
2234+ // Earlier dev builds occasionally left a ReaderWindow in the list
2235+ // after its chrome window was destroyed; the dead entry then
2236+ // broke the next `Zotero.Reader.open` call ("can't access dead
2237+ // object" at reader.js:73). Even with the unload-cleanup we
2238+ // added in `_applyReaderCompactMenubar`, this self-heal at every
2239+ // init keeps the user unblocked if a leak slips through.
2240+ try {
2241+ const readers = ( Zotero as any ) . Reader ?. _readers ;
2242+ if ( Array . isArray ( readers ) ) {
2243+ let pruned = 0 ;
2244+ for ( let i = readers . length - 1 ; i >= 0 ; i -- ) {
2245+ try {
2246+ const _ = readers [ i ] . tabID ; // throws if dead
2247+ } catch ( e ) {
2248+ readers . splice ( i , 1 ) ;
2249+ pruned ++ ;
2250+ }
2251+ }
2252+ if ( pruned ) Zotero . debug ( "[Weavero] init: pruned " + pruned + " dead reader(s) from Zotero.Reader._readers" ) ;
2253+ }
2254+ } catch ( e ) { }
2255+
2256+ // Compact title bar — apply to any already-open main windows.
2257+ // `onMainWindowLoad` only fires for NEW windows, so on Zotero
2258+ // restart the existing window won't trigger it; we apply here
2259+ // from init() too. Idempotent: `_applyCompactTitleBar` early-
2260+ // returns when a stash already exists, so onMainWindowLoad's
2261+ // call later (for fresh windows) doesn't double-apply.
2262+ try {
2263+ if ( this . _getCompactTitleBar ( ) ) {
2264+ const wins = Zotero . getMainWindows ? Zotero . getMainWindows ( ) : [ Zotero . getMainWindow ( ) ] . filter ( Boolean ) ;
2265+ for ( const w of wins ) {
2266+ try { this . _applyCompactTitleBar ( w ) ; } catch ( e ) { }
2267+ }
2268+ // Same for any already-open standalone reader windows.
2269+ // Newly opened readers are handled by the renderToolbar
2270+ // event path in `_toolbarHandlerImpl`.
2271+ const readers = ( Zotero . Reader . _readers || [ ] ) . filter ( r => ! r . tabID && r . _window ) ;
2272+ for ( const r of readers ) {
2273+ try { this . _applyReaderCompactMenubar ( r ) ; } catch ( e ) { }
2274+ }
2275+ }
2276+ } catch ( e ) { Zotero . debug ( "[Weavero] init compactTitleBar err: " + e ) ; }
22042277 this . _applyUIThemeClass ( ) ;
22052278 // Sync per-scheme `network.protocol-handler.warn-external.<x>`
22062279 // prefs with the user's "Open without confirmation" choice.
@@ -2287,6 +2360,27 @@ class WeaveroPlugin {
22872360 if ( data === "extensions.zotero.weavero.enableReaderViewIcons" ) {
22882361 this . _applySurfacePref ( "readerView" ) ;
22892362 }
2363+ // Compact title bar — apply or revert across every main
2364+ // window AND every standalone reader window so the
2365+ // change is visible without a restart.
2366+ if ( data === "extensions.zotero.weavero.compactTitleBar" ) {
2367+ try {
2368+ const on = this . _getCompactTitleBar ( ) ;
2369+ const wins = Zotero . getMainWindows ? Zotero . getMainWindows ( ) : [ Zotero . getMainWindow ( ) ] . filter ( Boolean ) ;
2370+ for ( const w of wins ) {
2371+ if ( on ) this . _applyCompactTitleBar ( w ) ;
2372+ else this . _revertCompactTitleBar ( w ) ;
2373+ }
2374+ // Reader windows — iterate every window-mode
2375+ // ReaderInstance and apply/revert the menubar
2376+ // collapse.
2377+ const readers = ( Zotero . Reader . _readers || [ ] ) . filter ( r => ! r . tabID && r . _window ) ;
2378+ for ( const r of readers ) {
2379+ if ( on ) this . _applyReaderCompactMenubar ( r ) ;
2380+ else this . _revertReaderCompactMenubar ( r ) ;
2381+ }
2382+ } catch ( e ) { Zotero . debug ( "[Weavero] compactTitleBar toggle err: " + e ) ; }
2383+ }
22902384 // Tags column auto-count toggle — (1) resize the
22912385 // column to fit one or two numbers, (2) re-render
22922386 // the items view so already-painted Tags cells pick
@@ -2713,6 +2807,12 @@ class WeaveroPlugin {
27132807 this . _applyInlineLinksPref ( this . _getInlineLinks ( ) ) ;
27142808 this . _applyCommentMarkdownPref ( ) ;
27152809 this . _applyUIThemeClass ( ) ;
2810+ // Apply compact-title-bar mode if the pref is on (default off).
2811+ // Runs after the window is fully laid out so the buttonbox move
2812+ // doesn't race against Zotero's own titlebar init.
2813+ try {
2814+ if ( this . _getCompactTitleBar ( ) ) this . _applyCompactTitleBar ( _window ) ;
2815+ } catch ( e ) { Zotero . debug ( "[Weavero] _applyCompactTitleBar onLoad err: " + e ) ; }
27162816 // Refresh sidebar icons across any open readers. The
27172817 // renderSidebarAnnotationHeader event won't re-fire for rows
27182818 // that were already mounted before the plugin (re-)started,
@@ -2821,9 +2921,43 @@ class WeaveroPlugin {
28212921 this . _uiThemeObserver = null ;
28222922 }
28232923
2824- try { ( Zotero . Reader as any ) . unregisterEventListener ( "renderSidebarAnnotationHeader" , "weavero" ) ; } catch ( e ) { }
2825- try { ( Zotero . Reader as any ) . unregisterEventListener ( "createAnnotationContextMenu" , "weavero" ) ; } catch ( e ) { }
2826- try { ( Zotero . Reader as any ) . unregisterEventListener ( "createViewContextMenu" , "weavero" ) ; } catch ( e ) { }
2924+ // Reader event listeners are cleaned up by Zotero's plugin-
2925+ // shutdown observer (`_unregisterEventListenerByPluginID`,
2926+ // wired in xpcom/reader.js), which fires on plugin disable /
2927+ // upgrade and filters listeners by pluginID. We DON'T call
2928+ // `unregisterEventListener(type, handler)` manually because
2929+ // Zotero's implementation is inverted — it does
2930+ // `filter(x => x.type === type && x.handler === handler)`,
2931+ // which KEEPS the matching listener and discards all others,
2932+ // i.e. it would wipe other plugins' listeners on any actual
2933+ // match (and our prior code passed a string where a handler
2934+ // was expected, so the predicate never matched and it merely
2935+ // wiped everything). Registering with the correct full plugin
2936+ // ID is enough — the shutdown observer handles teardown.
2937+ // Revert compact-title-bar across every main window AND every
2938+ // standalone reader window so unloading the plugin doesn't leave
2939+ // the DOM mutilated (buttonbox moved, icon hidden, menubar
2940+ // collapsed). SKIPPED during app shutdown — the windows are
2941+ // about to be destroyed anyway, and reverting here was found
2942+ // to interfere with Zotero's session save (Session.save reads
2943+ // `Zotero.getZoteroPanes()` from `quit-application-granted`;
2944+ // if our long synchronous DOM revert delays things, the panes
2945+ // are gone by save time and the old tab state gets restored,
2946+ // making closed tabs reappear on next startup).
2947+ try {
2948+ if ( ! Services . startup . shuttingDown ) {
2949+ const wins = Zotero . getMainWindows ? Zotero . getMainWindows ( ) : [ Zotero . getMainWindow ( ) ] . filter ( Boolean ) ;
2950+ for ( const w of wins ) {
2951+ try { this . _revertCompactTitleBar ( w ) ; } catch ( e ) { }
2952+ }
2953+ const readers = ( Zotero . Reader . _readers || [ ] ) . filter ( r => ! r . tabID && r . _window ) ;
2954+ for ( const r of readers ) {
2955+ try { this . _revertReaderCompactMenubar ( r ) ; } catch ( e ) { }
2956+ }
2957+ } else {
2958+ Zotero . debug ( "[Weavero] destroy: app shutting down, skipping compact-title-bar revert" ) ;
2959+ }
2960+ } catch ( e ) { }
28272961 this . _unregisterItemTreeColumns ( ) ;
28282962 try { this . _unpatchAnnotationRow ( ) ; } catch ( e ) { }
28292963
0 commit comments