feat(tabs): detach SQL and data tabs into separate windows
This commit is contained in:
1 parent
717dcc1294
commit
d38be39e03
17 files changed
+957
-21
No files matched your search
@@ -30,6 +30,8 @@ const props = defineProps<{
|
||||
settingsPageOpen?: boolean;
|
||||
settingsPageActive?: boolean;
|
||||
agentDriverUpdateCount?: number;
|
||||
detachedDropTarget?: boolean;
|
||||
canDetachTabs?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -45,6 +47,7 @@ const emit = defineEmits<{
|
||||
"save-all-tab-close": [];
|
||||
"discard-all-tab-close": [];
|
||||
"cancel-tab-close": [];
|
||||
"detach-tab": [tab: QueryTab, position?: { x: number; y: number }];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
@@ -52,16 +55,25 @@ const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { toast } = useToast();
|
||||
const tabDrag = useTabDrag(
|
||||
(draggedId, targetId, position) => {
|
||||
return queryStore.reorderTab(draggedId, targetId, position);
|
||||
},
|
||||
(draggedId, position) => {
|
||||
if (!props.canDetachTabs) return false;
|
||||
const tab = queryStore.tabs.find((item) => item.id === draggedId);
|
||||
if (!tab || !isDetachableTab(tab)) return false;
|
||||
emit("detach-tab", tab, position);
|
||||
return true;
|
||||
},
|
||||
() => (isVerticalLayout.value ? "vertical" : "horizontal"),
|
||||
);
|
||||
const editingTabId = ref<string | null>(null);
|
||||
const editingTitle = ref("");
|
||||
const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic");
|
||||
const isVerticalLayout = computed(() => settingsStore.editorSettings.tabPlacement === "left" || settingsStore.editorSettings.tabPlacement === "right");
|
||||
const isWrapLayout = computed(() => !isVerticalLayout.value && settingsStore.editorSettings.tabLayout === "wrap");
|
||||
const isManualTabOrder = computed(() => settingsStore.editorSettings.tabSortMode === "manual");
|
||||
const tabDrag = useTabDrag(
|
||||
(draggedId, targetId, position) => queryStore.reorderTab(draggedId, targetId, position),
|
||||
() => (isVerticalLayout.value ? "vertical" : "horizontal"),
|
||||
);
|
||||
|
||||
function tabGroupKey(tab: QueryTab) {
|
||||
const connection = connectionStore.getConfig(tab.connectionId);
|
||||
@@ -153,6 +165,7 @@ function scheduleCloseConfirmListClose() {
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
tabDrag.setDetachBoundsProvider(null);
|
||||
if (closeConfirmListCloseTimer) {
|
||||
clearTimeout(closeConfirmListCloseTimer);
|
||||
closeConfirmListCloseTimer = null;
|
||||
@@ -174,6 +187,10 @@ function canRenameTab(tab: QueryTab) {
|
||||
return tab.mode === "query";
|
||||
}
|
||||
|
||||
function isDetachableTab(tab: QueryTab) {
|
||||
return tab.mode === "query" || tab.mode === "data";
|
||||
}
|
||||
|
||||
function startRenameTab(tab: QueryTab) {
|
||||
if (!canRenameTab(tab)) return;
|
||||
editingTabId.value = tab.id;
|
||||
@@ -389,6 +406,12 @@ function getTabMenuItems(tab: QueryTab): ContextMenuItem[] {
|
||||
},
|
||||
icon: Copy,
|
||||
},
|
||||
{
|
||||
label: t("tabs.openInNewWindow"),
|
||||
action: () => emit("detach-tab", tab),
|
||||
icon: Maximize2,
|
||||
visible: !!props.canDetachTabs && isDetachableTab(tab),
|
||||
},
|
||||
{
|
||||
label: t("sidebar.locateActiveTab"),
|
||||
action: () => emit("locate-tab", tab),
|
||||
@@ -468,6 +491,13 @@ const {
|
||||
onTabsWheel: onFixedTabsWheel,
|
||||
startScrollbarDrag: startFixedScrollbarDrag,
|
||||
} = useTabScroll(fixedTabsContainerRef);
|
||||
tabDrag.setDetachBoundsProvider(() => {
|
||||
const regular = tabsContainerRef.value?.getBoundingClientRect();
|
||||
const fixed = fixedTabsContainerRef.value?.getBoundingClientRect();
|
||||
if (!regular) return fixed;
|
||||
if (!fixed) return regular;
|
||||
return new DOMRect(Math.min(regular.left, fixed.left), Math.min(regular.top, fixed.top), Math.max(regular.right, fixed.right) - Math.min(regular.left, fixed.left), Math.max(regular.bottom, fixed.bottom) - Math.min(regular.top, fixed.top));
|
||||
});
|
||||
const tabScrollBehavior = ref<ScrollBehavior>("smooth");
|
||||
|
||||
function updateAllScrollButtons() {
|
||||
@@ -773,7 +803,7 @@ function onOverflowItemKeydown(event: KeyboardEvent, tabId: string, kind: "regul
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="queryStore.tabs.length > 0 || driverStoreOpen || settingsPageOpen" class="app-tab-bar relative flex min-w-0 shrink-0 overflow-hidden" :class="tabBarClass">
|
||||
<div v-if="queryStore.tabs.length > 0 || driverStoreOpen || settingsPageOpen" data-main-tab-bar class="app-tab-bar relative flex w-full min-w-0 shrink-0 overflow-hidden" :class="[tabBarClass, { 'ring-2 ring-primary ring-inset': detachedDropTarget }]">
|
||||
<div v-if="isVerticalLayout" class="relative shrink-0 border-b p-2">
|
||||
<Search class="pointer-events-none absolute left-4 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input v-model="tabSearchQuery" type="search" :placeholder="t('tabs.searchOpenTabs')" class="h-8 w-full pl-7 text-sm" />
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowLeft, Minus, Square, Copy, X, Grip } from "@lucide/vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useWindowControls } from "@/composables/useWindowControls";
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
dirty?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
return: [];
|
||||
close: [];
|
||||
"drag-start": [position: { x: number; y: number }];
|
||||
dragging: [position: { x: number; y: number }];
|
||||
"drag-end": [position: { x: number; y: number }];
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const { isMac, isMaximized, minimize, toggleMaximize } = useWindowControls();
|
||||
|
||||
let dragging = false;
|
||||
|
||||
async function cursorPosition(event: PointerEvent) {
|
||||
// PointerEvent.screenX/Y are global screen coordinates. Convert the
|
||||
// browser's logical pixels to physical pixels so they can be compared with
|
||||
// Tauri's PhysicalPosition on the main window, including mixed-DPI setups.
|
||||
const scale = typeof window !== "undefined" && Number.isFinite(window.devicePixelRatio) && window.devicePixelRatio > 0 ? window.devicePixelRatio : 1;
|
||||
return { x: event.screenX * scale, y: event.screenY * scale };
|
||||
}
|
||||
|
||||
async function handleDragStart(event: PointerEvent) {
|
||||
if (event.button !== 0 || event.pointerType === "touch") return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
dragging = true;
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
target.setPointerCapture?.(event.pointerId);
|
||||
emit("drag-start", await cursorPosition(event));
|
||||
}
|
||||
|
||||
async function handleDragging(event: PointerEvent) {
|
||||
if (!dragging) return;
|
||||
event.preventDefault();
|
||||
emit("dragging", await cursorPosition(event));
|
||||
}
|
||||
|
||||
async function handleDragEnd(event: PointerEvent) {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
event.preventDefault();
|
||||
emit("drag-end", await cursorPosition(event));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-10 shrink-0 items-center border-b bg-background/95 text-xs" :class="isMac ? 'pl-20' : ''">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-2 px-3" data-tauri-drag-region>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-7 w-6 shrink-0 cursor-grab items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground active:cursor-grabbing"
|
||||
:title="t('tabs.dragBackToMain')"
|
||||
@pointerdown.stop="handleDragStart"
|
||||
@pointermove.stop="handleDragging"
|
||||
@pointerup.stop="handleDragEnd"
|
||||
@pointercancel.stop="handleDragEnd"
|
||||
>
|
||||
<Grip class="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<span class="min-w-0 flex-1 truncate font-medium" data-tauri-drag-region> <span v-if="dirty" class="mr-0.5">*</span>{{ title }} </span>
|
||||
</div>
|
||||
<button type="button" class="mr-1 inline-flex h-7 items-center gap-1 rounded px-2 text-muted-foreground hover:bg-accent hover:text-foreground" :title="t('tabs.returnToMainWindow')" @pointerdown.stop @click="emit('return')">
|
||||
<ArrowLeft class="h-3.5 w-3.5" />
|
||||
<span>{{ t("tabs.returnToMainWindow") }}</span>
|
||||
</button>
|
||||
<template v-if="!isMac">
|
||||
<button type="button" class="inline-flex h-10 w-10 items-center justify-center hover:bg-foreground/10" :title="t('tabs.minimizeWindow')" @click="minimize"><Minus class="h-4 w-4" /></button>
|
||||
<button type="button" class="inline-flex h-10 w-10 items-center justify-center hover:bg-foreground/10" :title="t('tabs.maximizeWindow')" @click="toggleMaximize"><Copy v-if="isMaximized" class="h-3.5 w-3.5" /><Square v-else class="h-3.5 w-3.5" /></button>
|
||||
<button type="button" class="inline-flex h-10 w-10 items-center justify-center hover:bg-red-500 hover:text-white" :title="t('tabs.closeWindow')" @click="emit('close')"><X class="h-4 w-4" /></button>
|
||||
</template>
|
||||
<button v-else type="button" class="mr-2 inline-flex h-7 w-7 items-center justify-center rounded hover:bg-red-500 hover:text-white" :title="t('tabs.closeWindow')" @click="emit('close')"><X class="h-4 w-4" /></button>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user