Compare commits

...
Sign in to create a new pull request.

18 commits

Author SHA1 Message Date
mr. m
2a64d867cc
feat: Prevent folders from being removed if there are no tabs, b=(no-bug), c=folders 2025-05-24 21:59:34 +02:00
mr. m
1b68d6a2ef
chore: Merge branches, b=(no-bug), c=folders 2025-05-24 21:32:26 +02:00
mr. m
5fb4ff3400
Discard changes to src/browser/components/tabbrowser/content/tabbrowser-js.patch 2025-05-24 21:30:50 +02:00
mr. m
4e9afda720
Merge branch 'dev' of https://github.com/zen-browser/desktop into tab-folders 2025-05-24 21:29:01 +02:00
mr. m
242029c34c
feat: Make zen folders inherit from firefox tabgroups, b=(no-bug), c=tabs, folders 2025-05-24 21:24:20 +02:00
Mr. M
3fddb37485
feat: Added new icons and improved styles, b=(no-bug), c=common, folders 2025-05-18 01:26:26 +02:00
Mr. M
c3f9fe99a5
chore: Formatted, b=(no-bug), c=folders 2025-05-17 14:32:46 +02:00
Mr. M
7291b07a09
chore: Merged branch, b=(no-bug), c=no-component 2025-05-17 14:23:29 +02:00
Mr. M
d9c8968468
Merge branch 'dev' into tab-folders 2025-05-17 14:23:10 +02:00
Mr. M
a926c1949b Refactor tab group handling to improve child element appending and event listener attachment 2025-04-09 18:34:52 +02:00
mr. m
6a23d198a4
Started working on nested groups 2025-04-08 15:48:36 +02:00
mr. m
225d17eb88
Merge branch 'dev' into tab-folders 2025-04-08 15:02:03 +02:00
mr. m
565df0ea49
Merge branch 'dev' into tab-folders 2025-04-07 02:03:31 -07:00
mr. M
4a02ea11b1
fix: update tab group click handling and improve folder icon SVG dimensions 2025-04-06 12:30:28 +02:00
mr. M
a77d2cfed3
refactor: improve tab group handling and adjust overflow styles 2025-04-06 00:49:06 +02:00
mr. m
b4703baf23
fix: adjust animation duration for tab group collapse and expand 2025-04-05 18:40:39 +02:00
mr. M
2970e9b3cc
feat: add folder icons and animations for tab groups 2025-04-05 17:35:06 +02:00
mr. M
b4575de026
Initial development for folders 2025-04-05 01:26:14 +02:00
10 changed files with 361 additions and 55 deletions

View file

@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js
index f43ab1cf6257ff1a9c9aa522a0180fd9bbfe4036..d9714c46de860243b06af7e8343d36b107efb855 100644
index f43ab1cf6257ff1a9c9aa522a0180fd9bbfe4036..75d6f7e08f70a1a26e24c4e2005af9225f193e68 100644
--- a/browser/components/tabbrowser/content/tab.js
+++ b/browser/components/tabbrowser/content/tab.js
@@ -21,6 +21,7 @@
@ -48,6 +48,17 @@ index f43ab1cf6257ff1a9c9aa522a0180fd9bbfe4036..d9714c46de860243b06af7e8343d36b1
}
get lastAccessed() {
@@ -364,8 +367,8 @@
}
get group() {
- if (this.parentElement?.tagName == "tab-group") {
- return this.parentElement;
+ if (gBrowser.isTabGroup(this.parentElement?.parentElement)) {
+ return this.parentElement.parentElement;
}
return null;
}
@@ -459,6 +462,8 @@
this.style.MozUserFocus = "ignore";
} else if (

View file

@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabbrowser.js b/browser/components/tabbrowser/content/tabbrowser.js
index d5aa64842a35c6697263c63fd3a0571b64b01344..6943dc7f1d3d95ab1da315cbdbda0b032cf200f1 100644
index d5aa64842a35c6697263c63fd3a0571b64b01344..475838b0fcfab3074116111d971d77c209d70f44 100644
--- a/browser/components/tabbrowser/content/tabbrowser.js
+++ b/browser/components/tabbrowser/content/tabbrowser.js
@@ -413,11 +413,41 @@
@ -9,7 +9,7 @@ index d5aa64842a35c6697263c63fd3a0571b64b01344..6943dc7f1d3d95ab1da315cbdbda0b03
+ get _numVisiblePinTabsWithoutCollapsed() {
+ let i = 0;
+ for (let item of this.tabContainer.ariaFocusableItems) {
+ if (!!item?.classList?.contains("tab-group-label") && item.closest("tab-group").pinned) {
+ if (!!item?.classList?.contains("tab-group-label") && item.closest("tab-folder")) {
+ i += 1;
+ continue;
+ }
@ -546,6 +546,15 @@ index d5aa64842a35c6697263c63fd3a0571b64b01344..6943dc7f1d3d95ab1da315cbdbda0b03
aTab.selected ||
aTab.closing ||
// Tabs that are sharing the screen, microphone or camera cannot be hidden.
@@ -5909,7 +6035,7 @@
* `true` if element is a `<tab-group>`
*/
isTabGroup(element) {
- return !!(element?.tagName == "tab-group");
+ return !!(element?.tagName == "tab-group" || element?.tagName == "zen-folder");
}
/**
@@ -5986,7 +6112,7 @@
// Don't allow mixing pinned and unpinned tabs.

View file

@ -0,0 +1,89 @@
diff --git a/browser/components/tabbrowser/content/tabgroup.js b/browser/components/tabbrowser/content/tabgroup.js
index 6dc774ea335b0c5dba7dcf76cdb23728faae1343..f6a5742423342dec9394ce9d98011b5f388e5174 100644
--- a/browser/components/tabbrowser/content/tabgroup.js
+++ b/browser/components/tabbrowser/content/tabgroup.js
@@ -13,10 +13,13 @@
class MozTabbrowserTabGroup extends MozXULElement {
static markup = `
- <vbox class="tab-group-label-container" pack="center">
- <label class="tab-group-label" role="button"/>
- </vbox>
- <html:slot/>
+ <hbox class="tab-group-label-container" pack="center">
+ <image class="tab-group-folder-icon"/>
+ <label class="tab-group-label" role="button"/>
+ </hbox>
+ <html:div class="tab-group-container">
+ <html:div class="zen-tab-group-start" />
+ </html:div>
`;
/** @type {string} */
@@ -68,7 +71,7 @@
this.#labelElement.container = gBrowser.tabContainer;
this.#labelElement.group = this;
- this.#labelElement.addEventListener("click", this);
+ this.querySelector(".tab-group-label-container").addEventListener("click", this);
this.#labelElement.addEventListener("contextmenu", e => {
e.preventDefault();
gBrowser.tabGroupMenu.openEditModal(this);
@@ -93,6 +96,9 @@
// claim that a tab group was created by adoption the first time it
// mounts after getting created by `Tabbrowser.adoptTabGroup`.
this.#wasCreatedByAdoption = false;
+ this.appendChild = function (child) {
+ this.querySelector('.tab-group-container').appendChild(child);
+ }
}
disconnectedCallback() {
@@ -133,7 +139,7 @@
}
});
}
- if (!this.tabs.length) {
+ if (!this.tabs.length && !this.pinned) {
this.dispatchEvent(
new CustomEvent("TabGroupRemoved", { bubbles: true })
);
@@ -275,7 +281,21 @@
}
get tabs() {
- return Array.from(this.children).filter(node => node.matches("tab"));
+ // add other group tabs if they are under this group
+ let childs = Array.from(this.querySelector('.tab-group-container')?.children ?? []);
+ for (let item of childs) {
+ if (gBrowser.isTabGroup(item)) {
+ childs = childs.concat(item.tabs);
+ }
+ }
+ return childs.filter(node => node.matches("tab"));
+ }
+
+ get group() {
+ if (gBrowser.isTabGroup(this.parentElement)) {
+ return this.parentElement.parentElement;
+ }
+ return null;
}
/**
@@ -301,7 +321,7 @@
*/
addTabs(tabs, metricsContext) {
for (let tab of tabs) {
- if (tab.pinned) {
+ if (tab.pinned != this.pinned) {
tab.ownerGlobal.gBrowser.unpinTab(tab);
}
let tabToMove =
@@ -395,5 +415,6 @@
}
}
+ window.MozTabbrowserTabGroup = MozTabbrowserTabGroup;
customElements.define("tab-group", MozTabbrowserTabGroup);
}

View file

@ -0,0 +1 @@
<svg width="28" height="19" viewBox="-5 0 30 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 3.5C1 2.39543 1.89543 1.5 3 1.5H8L10 3.5H15C16.1046 3.5 17 4.39543 17 5.5V15.5C17 16.6046 16.1046 17.5 15 17.5H3C1.89543 17.5 1 16.6046 1 15.5V3.5Z" fill="context-stroke"></path><path d="M1 3.5C1 2.39543 1.89543 1.5 3 1.5H8L10 3.5H15C16.1046 3.5 17 4.39543 17 5.5V15.5C17 16.6046 16.1046 17.5 15 17.5H3C1.89543 17.5 1 16.6046 1 15.5V3.5Z" fill="context-fill" fill-opacity="0.7"></path><path d="M1 3.5C1 2.39543 1.89543 1.5 3 1.5H8L10 3.5H15C16.1046 3.5 17 4.39543 17 5.5V15.5C17 16.6046 16.1046 17.5 15 17.5H3C1.89543 17.5 1 16.6046 1 15.5V3.5Z" stroke="context-stroke" stroke-width="1.5"></path><path d="M1 8.5C1 7.39543 1.89543 6.5 3 6.5H15C16.1046 6.5 17 7.39543 17 8.5V15.5C17 16.6046 16.1046 17.5 15 17.5H3C1.89543 17.5 1 16.6046 1 15.5V8.5Z" fill="context-fill" stroke="context-stroke" stroke-width="1.5" style="stroke-opacity: 1;"></path></svg>

After

Width:  |  Height:  |  Size: 960 B

View file

@ -0,0 +1 @@
<svg width="28" height="19" viewBox="-3 0 30 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1.44411 3.86858C1.21333 2.63775 2.15758 1.5 3.40985 1.5H7.81325C8.40681 1.5 8.96971 1.76365 9.34969 2.21963L9.40031 2.28037C9.7803 2.73635 10.3432 3 10.9367 3H15.3153C16.2888 3 17.1208 3.70088 17.2862 4.66019L19.0966 15.1602C19.3073 16.3826 18.3661 17.5 17.1257 17.5H5.65985C4.69742 17.5 3.87147 16.8145 3.69411 15.8686L1.44411 3.86858Z" fill="context-stroke" fill-opacity="1"></path><path d="M1.44411 3.86858C1.21333 2.63775 2.15758 1.5 3.40985 1.5H7.81325C8.40681 1.5 8.96971 1.76365 9.34969 2.21963L9.40031 2.28037C9.7803 2.73635 10.3432 3 10.9367 3H15.3153C16.2888 3 17.1208 3.70088 17.2862 4.66019L19.0966 15.1602C19.3073 16.3826 18.3661 17.5 17.1257 17.5H5.65985C4.69742 17.5 3.87147 16.8145 3.69411 15.8686L1.44411 3.86858Z" fill="context-fill" fill-opacity="0.7"></path><path d="M1.44411 3.86858C1.21333 2.63775 2.15758 1.5 3.40985 1.5H7.81325C8.40681 1.5 8.96971 1.76365 9.34969 2.21963L9.40031 2.28037C9.7803 2.73635 10.3432 3 10.9367 3H15.3153C16.2888 3 17.1208 3.70088 17.2862 4.66019L19.0966 15.1602C19.3073 16.3826 18.3661 17.5 17.1257 17.5H5.65985C4.69742 17.5 3.87147 16.8145 3.69411 15.8686L1.44411 3.86858Z" fill="url(#paint0_linear_70_12)" fill-opacity="0.15"></path><path d="M1.44411 3.86858C1.21333 2.63775 2.15758 1.5 3.40985 1.5H7.81325C8.40681 1.5 8.96971 1.76365 9.34969 2.21963L9.40031 2.28037C9.7803 2.73635 10.3432 3 10.9367 3H15.3153C16.2888 3 17.1208 3.70088 17.2862 4.66019L19.0966 15.1602C19.3073 16.3826 18.3661 17.5 17.1257 17.5H5.65985C4.69742 17.5 3.87147 16.8145 3.69411 15.8686L1.44411 3.86858Z" stroke="context-stroke" stroke-width="1.5"></path><path d="M5.59806 7.97376C5.83537 7.10364 6.62569 6.5 7.52759 6.5H19.3815C20.7002 6.5 21.658 7.75396 21.311 9.02623L19.4019 16.0262C19.1646 16.8964 18.3743 17.5 17.4724 17.5H5.6185C4.29975 17.5 3.34199 16.246 3.68897 14.9738L5.59806 7.97376Z" fill="context-fill" fill-opacity="1"></path><path d="M5.59806 7.97376C5.83537 7.10364 6.62569 6.5 7.52759 6.5H19.3815C20.7002 6.5 21.658 7.75396 21.311 9.02623L19.4019 16.0262C19.1646 16.8964 18.3743 17.5 17.4724 17.5H5.6185C4.29975 17.5 3.34199 16.246 3.68897 14.9738L5.59806 7.97376Z" fill="url(#paint1_linear_70_12)" fill-opacity="0.15"></path><path d="M5.59806 7.97376C5.83537 7.10364 6.62569 6.5 7.52759 6.5H19.3815C20.7002 6.5 21.658 7.75396 21.311 9.02623L19.4019 16.0262C19.1646 16.8964 18.3743 17.5 17.4724 17.5H5.6185C4.29975 17.5 3.34199 16.246 3.68897 14.9738L5.59806 7.97376Z" stroke="context-stroke" stroke-width="1.5"></path><defs><linearGradient id="paint0_linear_70_12" x1="13.5" y1="5.8125" x2="13.5" y2="17.5" gradientUnits="userSpaceOnUse"><stop stop-opacity="0"></stop><stop offset="1" stop-color="black" stop-opacity="1"></stop></linearGradient><linearGradient id="paint1_linear_70_12" x1="12.5" y1="5.49404" x2="12.5" y2="17.5" gradientUnits="userSpaceOnUse"><stop stop-opacity="0"></stop><stop offset="1" stop-color="black" stop-opacity="1"></stop></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -1239,3 +1239,12 @@ menupopup > menuitem:is([type='checkbox']) .menu-iconic-left {
#zen-media-pip-button {
list-style-image: url('chrome://global/skin/media/picture-in-picture-open.svg');
}
/* Tab folders */
.tab-group-folder-icon {
list-style-image: url('folder-open.svg');
tab-group[collapsed] > .tab-group-label-container & {
list-style-image: url('folder-closed.svg');
}
}

View file

@ -413,5 +413,7 @@
skin/classic/browser/zen-icons/zoom-control.svg (../shared/zen-icons/lin/zoom-control.svg)
skin/classic/browser/zen-icons/zoom-out.svg (../shared/zen-icons/lin/zoom-out.svg)
#endif
skin/classic/browser/zen-icons/folder-closed.svg (../shared/zen-icons/common/folder-closed.svg)
skin/classic/browser/zen-icons/folder-open.svg (../shared/zen-icons/common/folder-open.svg)
skin/classic/browser/zen-icons/urlbar-arrow.svg (../shared/zen-icons/common/urlbar-arrow.svg)
skin/classic/browser/zen-icons/icons.css (../shared/zen-icons/icons.css)

View file

@ -22,7 +22,7 @@ do_icons() {
do_common_icons() {
for filename in common/*.svg; do
# remove the os/ prefix
# remove the common/ prefix
filename=$(basename $filename)
echo "Working on $filename"
echo " skin/classic/browser/zen-icons/$filename (../shared/zen-icons/common/$filename) " >> jar.inc.mn

View file

@ -1,16 +1,56 @@
{
class ZenFolders {
class ZenFolder extends MozTabbrowserTabGroup {
constructor() {
super();
}
connectedCallback() {
super.connectedCallback();
}
/**
* Returns the group this folder belongs to.
* @returns {MozTabbrowserTabGroup|null} The group this folder belongs to, or null if it is not part of a group.
**/
get group() {
if (gBrowser.isTabGroup(this.parentElement?.parentElement)) {
return this.parentElement.parentElement;
}
return null;
}
get isZenFolder() {
return true;
}
}
customElements.define('zen-folder', ZenFolder);
class ZenFolders extends ZenPreloadedFeature {
init() {
this.#initContextMenu();
this.#initEventListeners();
}
#initEventListeners() {
document.addEventListener('TabGrouped', this.#onTabGrouped.bind(this));
document.addEventListener('TabUngrouped', this.#onTabUngrouped.bind(this));
document.addEventListener('TabGroupRemoved', this.#onTabGroupRemoved.bind(this));
document.addEventListener('TabGroupCreate', this.#onTabGroupCreate.bind(this));
document.addEventListener('TabPinned', this.#onTabPinned.bind(this));
document.addEventListener('TabUnpinned', this.#onTabUnpinned.bind(this));
window.addEventListener('TabGrouped', this.#onTabGrouped.bind(this));
window.addEventListener('TabUngrouped', this.#onTabUngrouped.bind(this));
window.addEventListener('TabGroupRemoved', this.#onTabGroupRemoved.bind(this));
window.addEventListener('TabGroupCreate', this.#onTabGroupCreate.bind(this));
window.addEventListener('TabPinned', this.#onTabPinned.bind(this));
window.addEventListener('TabUnpinned', this.#onTabUnpinned.bind(this));
window.addEventListener('TabGroupExpand', this.#onTabGroupExpand.bind(this));
window.addEventListener('TabGroupCollapse', this.#onTabGroupCollapse.bind(this));
document
.getElementById('zen-context-menu-new-folder')
.addEventListener('command', this.#onNewFolder.bind(this));
}
#initContextMenu() {
const contextMenuItems = window.MozXULElement.parseXULToFragment(`
<menuitem id="zen-context-menu-new-folder" data-l10n-id="zen-toolbar-context-new-folder"/>
`);
document.getElementById('toolbarNavigatorItemsMenuSeparator').before(contextMenuItems);
}
#onTabGrouped(event) {
@ -118,6 +158,107 @@
}
return this._piningFolder;
}
#onNewFolder(event) {
const tabs = gBrowser.selectedTabs;
this.createFolder(tabs);
}
createFolder(tabs, options = {}) {
for (const tab of tabs) {
gBrowser.pinTab(tab);
}
const insertBefore =
options.insertBefore ||
gZenWorkspaces.pinnedTabsContainer.querySelector(
'.vertical-pinned-tabs-container-separator'
);
const label = options.label || 'New Folder';
const folder = document.createXULElement('zen-folder', { is: 'zen-folder' });
let id = options.id;
if (!id) {
// Note: If this changes, make sure to also update the
// getExtTabGroupIdForInternalTabGroupId implementation in
// browser/components/extensions/parent/ext-browser.js.
// See: Bug 1960104 - Improve tab group ID generation in addTabGroup
id = `${Date.now()}-${Math.round(Math.random() * 100)}`;
}
folder.id = id;
folder.label = label;
folder.collapsed = !!options.collapsed;
folder.pinned = true;
folder.addTabs(tabs);
insertBefore.before(folder);
// Fixes bug1953801 and bug1954689
// Ensure that the tab state cache is updated immediately after creating
// a group. This is necessary because we consider group creation a
// deliberate user action indicating the tab has importance for the user.
// Without this, it is not possible to save and close a tab group with
// a short lifetime.
folder.tabs.forEach((tab) => {
gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
});
return folder;
}
async #onTabGroupCollapse(event) {
const ANIMATION_DURATION = 0.15;
const group = event.target;
const tabsContainer = group.querySelector('.tab-group-container');
const animations = [];
const groupStart = group.querySelector('.zen-tab-group-start');
let heightUntilSelected = 0;
let selectedItem = null;
let itemsAfterSelected = [];
for (const item of tabsContainer.children) {
const rect = item.getBoundingClientRect();
if (item.hasAttribute('visuallyselected')) {
selectedItem = item;
} else if (!selectedItem) {
heightUntilSelected += rect.height;
} else {
itemsAfterSelected.push(item);
}
}
animations.push(
gZenUIManager.motion.animate(
groupStart,
{
marginTop: [0, -heightUntilSelected],
},
{
duration: ANIMATION_DURATION,
ease: 'linear',
}
)
);
// TODO: Do the rest of the items after the selected item
await Promise.all(animations);
}
async #onTabGroupExpand(event) {
const group = event.target;
const tabsContainer = group.querySelector('.tab-group-container');
const groupStart = group.querySelector('.zen-tab-group-start');
const animations = [];
tabsContainer.style.overflow = 'hidden';
animations.push(
gZenUIManager.motion.animate(
groupStart,
{
marginTop: 0,
},
{
duration: 0.15,
ease: 'linear',
}
)
);
await Promise.all(animations);
tabsContainer.style.overflow = '';
}
}
window.gZenFolders = new ZenFolders();

View file

@ -12,6 +12,11 @@ tab-group {
tab-group[split-view-group] {
display: flex;
}
tab-group[split-view-group] > .tab-group-container {
display: flex;
flex: 1;
flex-wrap: nowrap;
border-radius: var(--border-radius-medium);
padding: 0 2px;
@ -151,50 +156,88 @@ tab-group[split-view-group] .tab-group-line {
background: transparent;
}
tab-group:not([split-view-group]) {
& .tab-group-label-container {
min-width: fit-content;
max-width: 100%;
height: fit-content !important;
display: flex;
justify-content: start;
margin-top: 10px;
margin-bottom: 10px;
}
& .tab-group-label {
text-align: start;
flex-grow: 1 !important;
min-width: fit-content;
max-width: 100%;
font-size: 14px !important;
display: block !important;
padding-right: 8px;
}
& .tab-group-line {
display: none !important;
}
&[collapsed] .tabbrowser-tab {
display: none !important;
}
&:not([collapsed]) .tabbrowser-tab {
margin-left: 10px;
}
&:not([collapsed]) .tabbrowser-tab::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 2px;
height: 100%;
background-color: var(--tab-group-color);
}
}
.tab-group-line {
display: none !important;
}
zen-folder {
display: flex !important;
flex-direction: column !important;
@media not (-moz-bool-pref: 'zen.view.sidebar-expanded') {
width: var(--tab-min-width) !important;
margin: var(--tab-block-margin) auto !important;
}
& > .tab-group-container {
padding-top: var(--tab-block-margin);
padding-left: var(--zen-folder-indent, 0.5em);
& > tab::before {
background: none !important;
}
}
margin: var(--tab-block-margin);
& > .tab-group-label-container {
flex: 0 0 auto !important;
position: sticky !important;
top: 0 !important;
z-index: 1000 !important;
--tab-group-color-pale: transparent !important;
--tab-group-color: transparent !important;
padding-block-end: 0 !important;
margin: 0 !important;
height: 36px !important;
border-radius: var(--border-radius-medium) !important;
transition: transform 0.2s ease;
transition:
background-color 0.2s ease,
border-radius 0.2s ease-in-out;
padding-inline: var(--tab-group-label-padding);
align-items: center;
@media not (-moz-bool-pref: 'zen.view.sidebar-expanded') {
width: var(--tab-min-width) !important;
}
.tab-group-folder-icon {
width: 28px;
height: 28px;
margin-inline-end: calc(var(--toolbarbutton-inner-padding) / 3) !important;
-moz-context-properties: fill, stroke, fill-opacity, stroke-opacity;
stroke: currentColor;
fill: var(--zen-colors-primary);
}
&:hover {
background-color: var(--tab-hover-background-color) !important;
}
&:after {
display: none;
}
& > label {
width: 100% !important;
background: transparent !important;
border: none !important;
color: var(--sidebar-text-color) !important;
margin: 0 !important;
padding-left: 34.5px !important;
font-weight: 500;
padding: 0 !important;
align-self: center !important;
text-align: start;
}
}
&[collapsed] {
& > .tabbrowser-tab:not([hidden]) {
display: flex;
}
& > .tab-group-container {
overflow-y: hidden;
}
}
}