class BarMenu extends Menu { constructor(id, structure, options = {}) { super(id, structure, options) MenuBar.menus[id] = this this.type = 'bar_menu' this.id = id this.children = []; this.condition = options.condition this.node = document.createElement('ul'); this.node.className = 'contextMenu menu_bar_menu'; this.node.style.minHeight = '8px'; this.node.style.minWidth = '150px'; this.name = tl(options.name || `menu.${id}`); this.label = Interface.createElement('li', {class: 'menu_bar_point'}, this.name); this.label.addEventListener('click', (event) => { if (open_menu === this) { this.hide() } else { this.open() } }) this.label.addEventListener('mouseenter', (event) => { if (MenuBar.open && MenuBar.open !== this) { this.open() } }) this.structure = structure; this.highlight_action = null; } open(...args) { super.open(...args); Blockbench.dispatchEvent('open_bar_menu', {menu: this}); } hide() { super.hide(); $(this.label).removeClass('opened'); MenuBar.open = undefined; this.highlight_action = null; this.label.classList.remove('highlighted'); return this; } highlight(action) { this.highlight_action = action; this.label.classList.add('highlighted'); } } const MenuBar = { menus: {}, open: undefined, setup() { MenuBar.menues = MenuBar.menus; new BarMenu('file', [ new MenuSeparator('file_options'), 'project_window', new MenuSeparator('open'), {name: 'menu.file.new', id: 'new', icon: 'insert_drive_file', children: function() { let arr = []; let redact = settings.streamer_mode.value; for (let key in Formats) { let format = Formats[key]; if (!format.show_in_new_list) continue; arr.push({ id: format.id, name: (redact && format.confidential) ? `[${tl('generic.redacted')}]` : format.name, icon: format.icon, description: format.description, click: (e) => { format.new() } }) } arr.push(new MenuSeparator('loaders')); for (let key in ModelLoader.loaders) { let loader = ModelLoader.loaders[key]; arr.push({ id: loader.id, name: (redact && loader.confidential) ? `[${tl('generic.redacted')}]` : loader.name, icon: loader.icon, description: loader.description, click: (e) => { loader.new() } }) } return arr; } }, {name: 'menu.file.recent', id: 'recent', icon: 'history', condition() {return isApp && recent_projects.length}, searchable: true, children() { var arr = [] let redact = settings.streamer_mode.value; for (let p of recent_projects) { if (arr.length > 12) break; arr.push({ name: redact ? `[${tl('generic.redacted')}]` : p.name, path: p.path, description: redact ? '' : p.path, icon: p.icon, click(c, event) { Blockbench.read([p.path], {}, files => { loadModelFile(files[0]); }) } }) } if (recent_projects.length > 12) { arr.push('_', { name: 'menu.file.recent.more', icon: 'read_more', always_show: true, click(c, event) { ActionControl.select('recent: '); } }) } if (arr.length) { arr.push('_', { name: 'menu.file.recent.clear', icon: 'clear', always_show: true, click(c, event) { recent_projects.empty(); updateRecentProjects(); } }) } return arr } }, 'open_model', 'open_from_link', 'new_window', new MenuSeparator('project'), 'save_project', 'save_project_as', 'convert_project', 'close_project', new MenuSeparator('import_export'), {name: 'menu.file.import', id: 'import', icon: 'insert_drive_file', condition: () => Format && !Format.pose_mode, children: [ { id: 'import_open_project', name: 'menu.file.import.import_open_project', icon: 'input', condition: () => Project && ModelProject.all.length > 1, children() { let projects = []; ModelProject.all.forEach(project => { if (project == Project) return; projects.push({ name: project.getDisplayName(), icon: project.format.icon, description: project.path, click() { let current_project = Project; project.select(); let bbmodel = Codecs.project.compile(); current_project.select(); Codecs.project.merge(JSON.parse(bbmodel)); } }) }) return projects; } }, 'import_project', 'import_java_block_model', 'import_optifine_part', 'import_obj', 'extrude_texture' ]}, {name: 'generic.export', id: 'export', icon: 'insert_drive_file', condition: () => Project, children: [ 'export_blockmodel', 'export_bedrock', 'export_entity', 'export_class_entity', 'export_optifine_full', 'export_optifine_part', 'export_minecraft_skin', 'export_gltf', 'export_obj', 'export_fbx', 'export_collada', 'export_modded_animations', 'upload_sketchfab', 'share_model', ]}, 'export_over', 'export_asset_archive', new MenuSeparator('options'), {name: 'menu.file.preferences', id: 'preferences', icon: 'tune', children: [ 'settings_window', 'keybindings_window', 'theme_window', { id: 'profiles', name: 'data.settings_profile', icon: 'manage_accounts', condition: () => SettingsProfile.all.findIndex(p => p.condition.type == 'selectable') != -1, children: () => { let list = [ { name: 'generic.none', icon: SettingsProfile.selected ? 'far.fa-circle' : 'far.fa-dot-circle', click: () => { SettingsProfile.unselect(); } }, '_' ]; SettingsProfile.all.forEach(profile => { if (profile.condition.type != 'selectable') return; list.push({ name: profile.name, icon: profile.selected ? 'far.fa-dot-circle' : 'far.fa-circle', color: markerColors[profile.color].standard, click: () => { profile.select(); } }) }) return list; } } ]}, 'plugins_window', 'edit_session' ]) new BarMenu('edit', [ new MenuSeparator('undo'), 'undo', 'redo', 'edit_history', new MenuSeparator('add_element'), 'add_cube', 'add_mesh', 'add_group', 'add_locator', 'add_null_object', 'add_texture_mesh', new MenuSeparator('modify_elements'), 'duplicate', 'rename', 'find_replace', 'unlock_everything', 'delete', new MenuSeparator('mesh_specific'), {name: 'data.mesh', id: 'mesh', icon: 'fa-gem', children: [ 'extrude_mesh_selection', 'inset_mesh_selection', 'loop_cut', 'create_face', 'invert_face', 'switch_face_crease', 'merge_vertices', 'dissolve_edges', 'solidify_mesh_selection', 'apply_mesh_rotation', 'split_mesh', 'merge_meshes', ]}, new MenuSeparator('editing_mode'), 'proportional_editing', 'mirror_modeling', new MenuSeparator('selection'), 'select_window', 'select_all', 'unselect_all', 'invert_selection' ]) new BarMenu('transform', [ 'scale', {name: 'menu.transform.rotate', id: 'rotate', icon: 'rotate_90_degrees_ccw', children: [ 'rotate_x_cw', 'rotate_x_ccw', 'rotate_y_cw', 'rotate_y_ccw', 'rotate_z_cw', 'rotate_z_ccw' ]}, {name: 'menu.transform.flip', id: 'flip', icon: 'flip', children: [ 'flip_x', 'flip_y', 'flip_z' ]}, {name: 'menu.transform.center', id: 'center', icon: 'filter_center_focus', children: [ 'center_x', 'center_y', 'center_z', 'center_lateral' ]}, {name: 'menu.transform.properties', id: 'properties', icon: 'navigate_next', children: [ 'toggle_visibility', 'toggle_locked', 'toggle_export', 'toggle_autouv', 'toggle_shade', 'toggle_mirror_uv' ]} ], { condition: {modes: ['edit']} }) new BarMenu('uv', UVEditor.menu.structure, { condition: {modes: ['edit']}, onOpen() { setActivePanel('uv'); } }) new BarMenu('image', [ new MenuSeparator('adjustment'), 'adjust_brightness_contrast', 'adjust_saturation_hue', 'adjust_opacity', 'invert_colors', 'adjust_curves', new MenuSeparator('filters'), 'limit_to_palette', 'clear_unused_texture_space', new MenuSeparator('transform'), 'flip_texture_x', 'flip_texture_y', 'rotate_texture_cw', 'rotate_texture_ccw', 'resize_texture', 'crop_texture_to_selection' ], { condition: {modes: ['paint']} }) new BarMenu('animation', [ new MenuSeparator('edit_options'), 'animation_onion_skin', 'animation_onion_skin_selective', 'lock_motion_trail', new MenuSeparator('edit'), 'add_marker', 'select_effect_animator', 'flip_animation', 'optimize_animation', 'bake_ik_animation', 'bake_animation_into_model', new MenuSeparator('file'), 'load_animation_file', 'save_all_animations', 'export_animation_file' ], { condition: {modes: ['animate']} }) new BarMenu('keyframe', [ new MenuSeparator('copypaste'), 'copy', 'paste', new MenuSeparator('edit'), 'add_keyframe', 'keyframe_column_create', 'select_all', 'keyframe_column_select', 'reverse_keyframes', {name: 'menu.animation.flip_keyframes', id: 'flip_keyframes', condition: () => Timeline.selected.length, icon: 'flip', children: [ 'flip_x', 'flip_y', 'flip_z' ]}, 'keyframe_uniform', 'reset_keyframe', 'resolve_keyframe_expressions', 'delete', ], { condition: {modes: ['animate']} }) new BarMenu('timeline', Timeline.menu.structure, { name: 'panel.timeline', condition: {modes: ['animate'], method: () => !AnimationController.selected}, onOpen() { setActivePanel('timeline'); } }) new BarMenu('display', [ new MenuSeparator('copypaste'), 'copy', 'paste', new MenuSeparator('presets'), 'add_display_preset', 'apply_display_preset' ], { condition: {modes: ['display']} }) new BarMenu('tools', [ new MenuSeparator('overview'), {id: 'main_tools', icon: 'construction', name: 'Toolbox', condition: () => Project, children() { let tools = Toolbox.children.filter(tool => tool instanceof Tool && tool.condition !== false); tools.forEach(tool => { let old_condition = tool.condition; tool.condition = () => { tool.condition = old_condition; return true; } }) let modes = Object.keys(Modes.options); tools.sort((a, b) => { return (a.modes ? modes.indexOf(a.modes[0]) : -1) - (b.modes ? modes.indexOf(b.modes[0]) : -1); }) let mode = tools[0].modes?.[0]; for (let i = 0; i < tools.length; i++) { if (tools[i].modes?.[0] !== mode) { mode = tools[i].modes?.[0]; tools.splice(i, 0, '_'); i++; } } return tools; }}, 'swap_tools', 'action_control', new MenuSeparator('tools'), 'predicate_overrides', 'convert_to_mesh', 'auto_set_cullfaces', 'remove_blank_faces', ]) MenuBar.menus.filter = MenuBar.menus.tools; new BarMenu('view', [ new MenuSeparator('viewport'), 'fullscreen', new MenuSeparator('viewport'), 'view_mode', 'toggle_shading', 'toggle_motion_trails', 'toggle_all_grids', 'toggle_ground_plane', 'preview_checkerboard', 'pixel_grid', 'painting_grid', new MenuSeparator('references'), 'preview_scene', 'edit_reference_images', new MenuSeparator('interface'), 'toggle_sidebars', 'split_screen', new MenuSeparator('model'), 'hide_everything_except_selection', 'focus_on_selection', {name: 'menu.view.screenshot', id: 'screenshot', icon: 'camera_alt', children: []}, new MenuSeparator('media'), 'screenshot_model', 'screenshot_app', 'advanced_screenshot', 'record_model_gif', 'timelapse', ]) new BarMenu('help', [ new MenuSeparator('search'), {name: 'menu.help.search_action', description: BarItems.action_control.description, keybind: BarItems.action_control.keybind, id: 'search_action', icon: 'search', click: ActionControl.select}, new MenuSeparator('links'), {name: 'menu.help.quickstart', id: 'quickstart', icon: 'fas.fa-directions', click: () => { Blockbench.openLink('https://blockbench.net/quickstart/'); }}, {name: 'menu.help.discord', id: 'discord', icon: 'fab.fa-discord', click: () => { Blockbench.openLink('http://discord.blockbench.net'); }}, {name: 'menu.help.wiki', id: 'wiki', icon: 'menu_book', click: () => { Blockbench.openLink('https://blockbench.net/wiki/'); }}, {name: 'menu.help.report_issue', id: 'report_issue', icon: 'bug_report', click: () => { Blockbench.openLink('https://github.com/JannisX11/blockbench/issues'); }}, new MenuSeparator('backups'), 'view_backups', new MenuSeparator('about'), {name: 'menu.help.developer', id: 'developer', icon: 'fas.fa-wrench', children: [ 'reload_plugins', {name: 'menu.help.plugin_documentation', id: 'plugin_documentation', icon: 'fa-book', click: () => { Blockbench.openLink('https://www.blockbench.net/wiki/docs/plugin'); }}, 'open_dev_tools', {name: 'Error Log', condition: () => window.ErrorLog.length, icon: 'error', color: 'red', keybind: {toString: () => window.ErrorLog.length.toString()}, click() { let lines = window.ErrorLog.slice(0, 64).map((error) => { return Interface.createElement('p', {style: 'word-break: break-word;'}, `${error.message}\n - In .${error.file.split(location.origin).join('')} : ${error.line}`); }) new Dialog({ id: 'error_log', title: 'Error Log', lines, singleButton: true }).show(); }}, 'reset_layout', {name: 'menu.help.developer.reset_storage', icon: 'fas.fa-hdd', click: () => { factoryResetAndReload(); }}, {name: 'menu.help.developer.unlock_projects', id: 'unlock_projects', icon: 'vpn_key', condition: () => ModelProject.all.find(project => project.locked), click() { ModelProject.all.forEach(project => project.locked = false); }}, {name: 'menu.help.developer.cache_reload', id: 'cache_reload', icon: 'cached', condition: !isApp, click: () => { if('caches' in window){ caches.keys().then((names) => { names.forEach(async (name) => { await caches.delete(name) }) }) } window.location.reload(true) }}, 'reload', ]}, 'about_window' ]) MenuBar.update(); if (Blockbench.isMobile) { let header = document.querySelector('header'); document.getElementById('menu_bar').remove(); document.getElementById('header_free_bar').remove(); document.getElementById('corner_logo').remove(); let menu_button = Interface.createElement('div', {class: 'tool'}, Blockbench.getIconNode('menu')); menu_button.addEventListener('click', event => { MenuBar.openMobile(menu_button, event); }) let search_button = Interface.createElement('div', {class: 'tool'}, Blockbench.getIconNode('search')); search_button.addEventListener('click', event => { ActionControl.select() }) let undo_button = Interface.createElement('div', {class: 'tool'}, Blockbench.getIconNode('undo')); undo_button.addEventListener('click', event => { BarItems.undo.trigger() }) let redo_button = Interface.createElement('div', {class: 'tool'}, Blockbench.getIconNode('redo')); redo_button.addEventListener('click', event => { BarItems.redo.trigger() }) let mode_switcher = Interface.createElement('div', {class: 'tool hidden', style: 'margin-left: auto'}, Blockbench.getIconNode('settings')); mode_switcher.addEventListener('click', event => { Modes.mobileModeMenu(mode_switcher, event); }) MenuBar.mode_switcher_button = mode_switcher; let home_button = document.getElementById('title_bar_home_button'); let buttons = [menu_button, search_button, home_button, undo_button, redo_button, mode_switcher]; buttons.forEach(button => { header.append(button); }) } }, openMobile(button, event) { let entries = []; for (let id in MenuBar.menus) { if (id == 'filter') continue; let menu = MenuBar.menus[id]; let entry = { id, icon: menu.icon || '', name: menu.name, children: menu.structure, condition: menu.condition, }; entries.push(entry); } let menu = new Menu(entries).open(button); return menu; }, update() { if (!Blockbench.isMobile) { let bar = $(document.getElementById('menu_bar')); bar.children().detach(); this.keys = []; for (var menu in MenuBar.menus) { if (MenuBar.menus.hasOwnProperty(menu)) { if (MenuBar.menus[menu].conditionMet()) { bar.append(MenuBar.menus[menu].label) this.keys.push(menu); } } } } }, addAction(action, path) { if (path) { path = path.split('.') var menu = MenuBar.menus[path.splice(0, 1)[0]] if (menu) { menu.addAction(action, path.join('.')) } } }, removeAction(path) { if (path) { path = path.split('.') var menu = MenuBar.menus[path.splice(0, 1)[0]] if (menu) { menu.removeAction(path.join('.')) } } } }