From 8af0c8e3d4784d1b39806c1a33373e33274e1c4a Mon Sep 17 00:00:00 2001 From: JannisX11 Date: Fri, 28 Oct 2022 22:56:39 +0200 Subject: [PATCH] Detachable project tabs --- css/window.css | 23 ++++++++ js/desktop.js | 13 +++++ js/io/formats/bbmodel.js | 56 ++++++++++++++++++++ js/io/project.js | 111 +++++++++++++++++++++++++++++++-------- main.js | 52 +++++++++++++++--- 5 files changed, 225 insertions(+), 30 deletions(-) diff --git a/css/window.css b/css/window.css index 66d8de43..5b9a2bf1 100644 --- a/css/window.css +++ b/css/window.css @@ -675,6 +675,29 @@ opacity: 1; } } + #drag_out_window_helper { + width: 200px; + height: 120px; + position: absolute; + margin: -14px -40px; + background-color: var(--color-ui); + border: 3px solid var(--color-frame); + box-shadow: 1px 1px 10px rgb(0 0 0 / 40%); + background-image: url('../assets/logo_cutout.svg'); + background-position: center 48px; + background-size: 40px; + background-repeat: no-repeat; + z-index: 200; + cursor: pointer; + } + #drag_out_window_helper > div { + width: 100%; + height: 26px; + text-align: center; + overflow: hidden; + white-space: nowrap; + background-color: var(--color-frame); + } /*Status Bar*/ diff --git a/js/desktop.js b/js/desktop.js index af3d14ac..34a2e9fc 100644 --- a/js/desktop.js +++ b/js/desktop.js @@ -92,6 +92,19 @@ function loadOpenWithBlockbenchFile() { ipcRenderer.on('open-model', (event, path) => { load(path); }) + ipcRenderer.on('load-tab', (event, model) => { + let fake_file = { + path: model.editor_state?.save_path || '' + }; + Codecs.project.load(model, fake_file); + if (model.detached_uuid) { + ipcRenderer.send('close-detached-project', model.detached_window_id, model.detached_uuid); + } + }) + ipcRenderer.on('close-detached-project', (event, uuid) => { + let tab = ModelProject.all.find(project => project.uuid == uuid && project.detached); + if (tab) tab.close(true); + }) if (electron.process.argv.length >= 2) { let path = electron.process.argv.last(); load(path); diff --git a/js/io/formats/bbmodel.js b/js/io/formats/bbmodel.js index 483b0bec..1cc61c3a 100644 --- a/js/io/formats/bbmodel.js +++ b/js/io/formats/bbmodel.js @@ -122,6 +122,29 @@ var codec = new Codec('project', { if (options.flag) { model.flag = options.flag; } + + if (options.editor_state) { + Project.saveEditorState(); + model.editor_state = { + save_path: Project.save_path, + export_path: Project.export_path, + saved: Project.saved, + added_models: Project.added_models, + mode: Project.mode, + tool: Project.tool, + display_uv: Project.display_uv, + exploded_view: Project.exploded_view, + uv_viewport: Project.uv_viewport, + previews: JSON.parse(JSON.stringify(Project.previews)), + + selected_elements: Project.selected_elements.map(e => e.uuid), + selected_group: Project.selected_group?.uuid, + selected_vertices: JSON.parse(JSON.stringify(Project.selected_vertices)), + selected_faces: Project.selected_faces, + selected_texture: Project.selected_texture?.uuid, + }; + } + model.elements = [] elements.forEach(el => { var obj = el.getSaveCopy(model.meta) @@ -335,6 +358,39 @@ var codec = new Codec('project', { Canvas.updateAllPositions() Validator.validate() this.dispatchEvent('parsed', {model}) + + if (model.editor_state) { + let state = model.editor_state; + Merge.string(Project, state, 'save_path') + Merge.string(Project, state, 'export_path') + Merge.boolean(Project, state, 'saved') + Merge.number(Project, state, 'added_models') + Merge.string(Project, state, 'mode') + Merge.string(Project, state, 'tool') + Merge.string(Project, state, 'display_uv') + Merge.boolean(Project, state, 'exploded_view') + if (state.uv_viewport) { + Merge.number(Project.uv_viewport, state.uv_viewport, 'zoom') + Merge.arrayVector2(Project.uv_viewport = state.uv_viewport, 'offset'); + } + if (state.previews) { + for (let id in state.previews) { + Project.previews[id] = state.previews[id]; + } + } + state.selected_elements.forEach(uuid => { + let el = Outliner.elements.find(el2 => el2.uuid == uuid); + Project.selected_elements.push(el); + }) + Group.selected = (state.selected_group && Group.all.find(g => g.uuid == state.selected_group)); + for (let key in state.selected_vertices) { + Project.selected_vertices[key] = state.selected_vertices[key]; + } + Project.selected_faces.replace(state.selected_faces); + (state.selected_texture && Texture.all.find(t => t.uuid == state.selected_texture))?.select(); + + Project.loadEditorState(); + } }, merge(model, path) { diff --git a/js/io/project.js b/js/io/project.js index 8f10796a..2d80da3e 100644 --- a/js/io/project.js +++ b/js/io/project.js @@ -142,15 +142,25 @@ class ModelProject { this.on_next_upen.push(callback); } } - select() { - if (this === Project) return true; - if (this.locked || Project.locked) return false; - if (Project) { - Project.unselect(); - Blockbench.addFlag('switching_project'); - } else { - Interface.tab_bar.new_tab.visible = false; - } + saveEditorState() { + this.tool = Toolbox.selected.id; + + UVEditor.saveViewportOffset(); + + Preview.all.forEach(preview => { + this.previews[preview.id] = { + position: preview.camera.position.toArray(), + target: preview.controls.target.toArray(), + orthographic: preview.isOrtho, + zoom: preview.camOrtho.zoom, + angle: preview.angle, + } + }) + + Blockbench.dispatchEvent('save_editor_state', {project: this}); + return this; + } + loadEditorState() { Project = this; Undo = this.undo; this.selected = true; @@ -209,10 +219,25 @@ class ModelProject { if (data.zoom) preview.camOrtho.zoom = data.zoom; if (data.angle) preview.setLockedAngle(data.angle); } else if (preview.default_angle !== undefined) { - setTimeout(() => preview.loadAnglePreset(preview.default_angle), 0); + preview.loadAnglePreset(preview.default_angle); } }) + Blockbench.dispatchEvent('load_editor_state', {project: this}); + return this; + } + select() { + if (this === Project) return true; + if (this.locked || Project.locked) return false; + if (Project) { + Project.unselect(); + Blockbench.addFlag('switching_project'); + } else { + Interface.tab_bar.new_tab.visible = false; + } + + this.loadEditorState(); + if (this.EditSession) { Interface.Panels.chat.inside_vue.chat_history = this.EditSession.chat_history; this.EditSession.catchUp(); @@ -248,25 +273,15 @@ class ModelProject { this.thumbnail = Texture.getDefault()?.source; } - this.tool = Toolbox.selected.id; - UVEditor.saveViewportOffset(); + this.saveEditorState(); } + Interface.tab_bar.last_opened_project = this.uuid; if (Format && typeof Format.onDeactivation == 'function') { Format.onDeactivation() } - Preview.all.forEach(preview => { - this.previews[preview.id] = { - position: preview.camera.position.toArray(), - target: preview.controls.target.toArray(), - orthographic: preview.isOrtho, - zoom: preview.camOrtho.zoom, - angle: preview.angle, - } - }) - this.undo.closeAmendEditMenu(); Preview.all.forEach(preview => { if (preview.movingBackground) preview.stopMovingBackground(); @@ -639,6 +654,8 @@ onVueSetup(() => { let active = false; let timeout; let last_event = e1; + let outside_tab_bar = false; + let drag_out_window_helper; let tab_node = e1.target; if (!tab_node.classList.contains('project_tab') || ModelProject.all.indexOf(tab) < 0) return; @@ -674,6 +691,27 @@ onVueSetup(() => { let index_offset = Math.trunc((e2.clientX - e1.clientX) / tab_node.clientWidth); scope.drag_position_index = scope.drag_target_index + index_offset; + + // Detach tab + let outside_tab_bar_before = outside_tab_bar; + outside_tab_bar = isApp && Math.abs(e2.clientY - 42) > 60 || e2.clientX < 2 || e2.clientX > window.innerWidth; + + if (outside_tab_bar !== outside_tab_bar_before) { + //setStartScreen(outside_tab_bar); + if (!drag_out_window_helper) { + drag_out_window_helper = Interface.createElement('div', {id: 'drag_out_window_helper'}, Interface.createElement('div', {}, tab.name)); + } + if (outside_tab_bar) { + document.body.append(drag_out_window_helper); + } else { + document.body.removeChild(drag_out_window_helper); + } + tab_node.style.visibility = outside_tab_bar ? 'hidden' : 'visible'; + } + if (outside_tab_bar) { + drag_out_window_helper.style.left = `${e2.clientX}px`; + drag_out_window_helper.style.top = `${e2.clientY}px`; + } } last_event = e2; } @@ -688,7 +726,34 @@ onVueSetup(() => { if (Blockbench.isTouch) clearTimeout(timeout); - if (active && !open_menu) { + + if (isApp && outside_tab_bar && !tab.EditSession) { + let project = Codecs.project.compile({editor_state: true, history: true, uuids: true, bitmaps: true, raw: true}) + let pos = currentwindow.getPosition() + project.detached_uuid = Project.uuid; + project.detached_window_id = currentwindow.id; + ipcRenderer.send('new-window', JSON.stringify(project), JSON.stringify({ + offset: [ + pos[0] + e2.clientX, + pos[1] + e2.clientY, + ] + })); + drag_out_window_helper.remove(); + //setStartScreen(false); + tab_node.style.visibility = null; + //tab.select(); + tab.detached = true; + /*Blockbench.showMessageBox({ + title: 'Project detached', + message: 'The tab was moved to another window. Do you want to close it here?', + buttons: ['dialog.ok', 'dialog.cancel'] + }, val => { + if (val == 0) { + tab.close(true) + } + })*/ + + } else if (active && !open_menu) { convertTouchEvent(e2); let index_offset = Math.trunc((e2.clientX - e1.clientX) / tab_node.clientWidth); if (index_offset) { diff --git a/main.js b/main.js index 472fee20..549aae6f 100644 --- a/main.js +++ b/main.js @@ -8,6 +8,7 @@ require('@electron/remote/main').initialize() let orig_win; let all_wins = []; +let load_project_data; const LaunchSettings = { path: path.join(app.getPath('userData'), 'launch_settings.json'), @@ -35,19 +36,21 @@ if (LaunchSettings.get('hardware_acceleration') == false) { app.disableHardwareAcceleration(); } -function createWindow(second_instance) { +function createWindow(second_instance, options = {}) { if (app.requestSingleInstanceLock && !app.requestSingleInstanceLock()) { app.quit() return; } - let win = new BrowserWindow({ - icon:'icon.ico', + let win_options = { + icon: 'icon.ico', show: false, backgroundColor: '#21252b', frame: LaunchSettings.get('native_window_frame') === true, titleBarStyle: 'hidden', minWidth: 640, minHeight: 480, + width: 1080, + height: 720, webPreferences: { webgl: true, webSecurity: true, @@ -55,7 +58,12 @@ function createWindow(second_instance) { contextIsolation: false, enableRemoteModule: true } - }) + }; + if (options.position) { + win_options.x = options.position[0] - 300; + win_options.y = Math.max(options.position[1] - 100, 0); + } + let win = new BrowserWindow(win_options) if (!orig_win) orig_win = win; all_wins.push(win); @@ -133,7 +141,7 @@ function createWindow(second_instance) { win.setMenu(null); } - win.maximize() + if (options.maximize !== false) win.maximize() win.show() win.loadURL(url.format({ @@ -184,8 +192,33 @@ ipcMain.on('edit-launch-setting', (event, arg) => { ipcMain.on('add-recent-project', (event, path) => { app.addRecentDocument(path); }) -ipcMain.on('new-window', (event, path) => { - createWindow(true); +ipcMain.on('new-window', (event, data, position) => { + if (typeof data == 'string') load_project_data = JSON.parse(data); + if (position) { + position = JSON.parse(position) + let place_in_window = all_wins.find(win => { + if (win.isDestroyed() || win.webContents == event.sender || win.isMinimized()) return false; + let pos = win.getPosition(); + let size = win.getSize(); + return (position.offset[0] >= pos[0] && position.offset[0] <= pos[0] + size[0] + && position.offset[1] >= pos[1] && position.offset[1] <= pos[1] + size[1]); + }) + if (place_in_window) { + place_in_window.send('load-tab', load_project_data); + load_project_data = null; + } else { + createWindow(true, { + maximize: false, + position: position.offset + }); + } + } else { + createWindow(true); + } +}) +ipcMain.on('close-detached-project', async (event, window_id, uuid) => { + let window = all_wins.find(win => win.id == window_id); + if (window) window.send('close-detached-project', uuid); }) ipcMain.on('request-color-picker', async (event, arg) => { const color = await getColorHexRGB().catch((error) => { @@ -207,6 +240,11 @@ app.on('ready', () => { let app_was_loaded = false; ipcMain.on('app-loaded', () => { + if (load_project_data) { + all_wins[all_wins.length-1].send('load-tab', load_project_data); + load_project_data = null; + } + if (app_was_loaded) { console.log('[Blockbench] App reloaded') return;