From 3f3c6ca8b51e4fbf7beab5a1da038500568d9b8a Mon Sep 17 00:00:00 2001 From: Billy Date: Tue, 19 Mar 2024 02:23:03 +0530 Subject: [PATCH 1/4] feat: Open-editors tree using native Treeview --- .../views/sidebar/explorer/__init__.py | 4 ++ .../views/sidebar/explorer/open_editors.py | 66 +++++++++++++++++++ .../components/views/sidebar/sidebarview.py | 2 +- .../layout/base/content/editors/__init__.py | 14 ++-- 4 files changed, 75 insertions(+), 11 deletions(-) create mode 100644 biscuit/core/components/views/sidebar/explorer/open_editors.py diff --git a/biscuit/core/components/views/sidebar/explorer/__init__.py b/biscuit/core/components/views/sidebar/explorer/__init__.py index 0c78a730..c5df02f0 100644 --- a/biscuit/core/components/views/sidebar/explorer/__init__.py +++ b/biscuit/core/components/views/sidebar/explorer/__init__.py @@ -1,10 +1,12 @@ import os +import tkinter as tk from biscuit.core.components.floating.palette import ActionSet from ..sidebarview import SidebarView from .directorytree import DirectoryTree from .menu import ExplorerMenu +from .open_editors import OpenEditors class Explorer(SidebarView): @@ -20,6 +22,8 @@ def __init__(self, master, *args, **kwargs) -> None: self.menu.add_item("Search", self.base.events.show_file_search_palette) self.add_button('ellipsis', self.menu.show) + self.open_editors = OpenEditors(self) + self.open_editors.pack(fill=tk.X) self.directory = DirectoryTree(self, observe_changes=True) self.add_widget(self.directory) diff --git a/biscuit/core/components/views/sidebar/explorer/open_editors.py b/biscuit/core/components/views/sidebar/explorer/open_editors.py new file mode 100644 index 00000000..46359769 --- /dev/null +++ b/biscuit/core/components/views/sidebar/explorer/open_editors.py @@ -0,0 +1,66 @@ +import os +import platform +import shutil +import subprocess +import threading +import tkinter as tk + +import pyperclip + +from biscuit.core.components.floating.palette.actionset import ActionSet +from biscuit.core.components.utils import Tree + +from ..item import SidebarViewItem + + +class OpenEditors(SidebarViewItem): + def __init__(self, master, startpath=None, itembar=True, *args, **kwargs) -> None: + self.title = 'Open Editors' + self.__buttons__ = (('new-file', lambda: self.base.palette.show('newfile:')),) + super().__init__(master, itembar, *args, **kwargs) + + self.tree = Tree(self.content, startpath, singleclick=self.openfile, *args, **kwargs) + self.tree.bind("", self.adjust_frame_size) + self.tree.grid(row=0, column=0, sticky=tk.NSEW) + self.tree.bind("<>", self.openfile) + + self.path = startpath + + def adjust_frame_size(self, *_): + if items := self.tree.get_children(): + frame_height = len(items) * 25 + 5 + else: + frame_height = 1 + self.tree.config(height=frame_height) + self.update() + + def add_item(self, editor): + self.tree.insert('', 'end', text=editor.filename, values=(editor.path, 'file')) + self.adjust_frame_size() + + def remove_item(self, editor): + for child in self.tree.get_children(): + if self.tree.item_fullpath(child) == editor.path: + self.tree.delete(child) + self.adjust_frame_size() + if not len(self.base.editorsmanager.active_editors): + self.disable() + return + + def set_active(self, editor): + for child in self.tree.get_children(): + if self.tree.item_fullpath(child) == editor.path: + self.tree.tree.selection_set(child) + self.adjust_frame_size() + return + + def clear(self): + self.tree.delete(*self.tree.get_children()) + self.adjust_frame_size() + + def openfile(self, _) -> None: + if self.tree.selected_type() != 'file': + return + + path = self.tree.selected_path() + self.base.open_editor(path) diff --git a/biscuit/core/components/views/sidebar/sidebarview.py b/biscuit/core/components/views/sidebar/sidebarview.py index d57a559d..f7179dc0 100644 --- a/biscuit/core/components/views/sidebar/sidebarview.py +++ b/biscuit/core/components/views/sidebar/sidebarview.py @@ -11,7 +11,7 @@ class SidebarView(View): def __init__(self, master, *args, **kwargs) -> None: super().__init__(master, *args, **kwargs) - self.__buttons__: list[tuple(str, typing.Callable)] = [] + self.__buttons__: list[tuple(str, typing.Callable)] = [] # type: ignore self.__icon__ = 'preview' self.name = "View" self.__name__ = self.__class__.__name__ diff --git a/biscuit/core/layout/base/content/editors/__init__.py b/biscuit/core/layout/base/content/editors/__init__.py index 7915b5a2..81a83c34 100644 --- a/biscuit/core/layout/base/content/editors/__init__.py +++ b/biscuit/core/layout/base/content/editors/__init__.py @@ -1,15 +1,5 @@ """ Holds the editors and provides an interface to manage the editor tabs. - -+---------------------------------+ -| File1.txt | File2.py | | -+---------------------------------+ -| \ \ \ \ \ \ \ | -| \ \ \ \ \ \ \| -| \ \ \ \ \ \ | -| \ \ \ \ \ \ | -|\ \ \ \ \ \ \ | -+---------------------------------+ """ from __future__ import annotations @@ -88,6 +78,7 @@ def add_editor(self, editor: Union[Editor,BaseEditor]) -> Editor | BaseEditor: if editor.content: editor.content.create_buttons(self.editorsbar.container) self.tabs.add_tab(editor) + self.base.explorer.open_editors.add_item(editor) self.refresh() return editor @@ -146,6 +137,7 @@ def close_editor(self, editor: Editor) -> None: self.closed_editors[editor.path] = editor else: editor.destroy() + self.base.explorer.open_editors.remove_item(editor) self.refresh() def get_editor(self, path: str) -> Editor: @@ -166,6 +158,7 @@ def delete_editor(self, editor: Editor) -> None: self.closed_editors.remove(editor) editor.destroy() + self.base.explorer.open_editors.remove_item(editor) self.refresh() def set_active_editor(self, editor: Editor) -> Editor: @@ -173,6 +166,7 @@ def set_active_editor(self, editor: Editor) -> Editor: for tab in self.tabs.tabs: if tab.editor == editor: self.tabs.set_active_tab(tab) + self.base.explorer.open_editors.set_active(editor) return editor From d0369a18951ea439e2286e5bb4763afb5a999f44 Mon Sep 17 00:00:00 2001 From: Billy Date: Tue, 19 Mar 2024 04:42:33 +0530 Subject: [PATCH 2/4] feat: Custom list widget for open editors --- biscuit/core/components/utils/__init__.py | 1 + .../core/components/utils/closelistitem.py | 90 +++++++++++++++++++ .../views/sidebar/explorer/open_editors.py | 67 ++++++-------- .../layout/base/content/editors/__init__.py | 4 + 4 files changed, 122 insertions(+), 40 deletions(-) create mode 100644 biscuit/core/components/utils/closelistitem.py diff --git a/biscuit/core/components/utils/__init__.py b/biscuit/core/components/utils/__init__.py index b0a5c42c..78c507bb 100644 --- a/biscuit/core/components/utils/__init__.py +++ b/biscuit/core/components/utils/__init__.py @@ -5,6 +5,7 @@ from .buttonsentry import ButtonsEntry from .callername import caller_name from .canvas import Canvas +from .closelistitem import CloseListItem from .codicon import get_codicon from .colorizer import colorize from .entry import Entry diff --git a/biscuit/core/components/utils/closelistitem.py b/biscuit/core/components/utils/closelistitem.py new file mode 100644 index 00000000..0fbeb9ca --- /dev/null +++ b/biscuit/core/components/utils/closelistitem.py @@ -0,0 +1,90 @@ +import tkinter as tk + +from biscuit.core.components.utils.iconbutton import IconButton + +from .codicon import get_codicon +from .frame import Frame +from .icon import Icon + + +class CloseListItem(Frame): + """ + A varient of IconLabelButton that has a close icon on the right side of the button. + """ + def __init__(self, master, text=None, icon=None, function=lambda *_: None, closefn=lambda *_:None, iconside=tk.LEFT, padx=5, pady=1, *args, **kwargs) -> None: + super().__init__(master, padx=padx, pady=pady, *args, **kwargs) + self.function = function + self.closefn = closefn + + self.bg, self.fg, self.hbg, self.hfg = self.base.theme.utils.iconlabelbutton.values() + self.config(bg=self.bg) + self.text = text + self.icon = icon + + if icon: + self.icon_label = tk.Label(self, text=get_codicon(self.icon), anchor=tk.CENTER, + bg=self.bg, fg=self.fg, font=("codicon", 14)) + self.icon_label.pack(side=iconside, fill=tk.BOTH, expand=True) + + if text: + self.text_label = tk.Label(self, text=self.text, anchor=tk.CENTER, pady=2, + bg=self.bg, fg=self.fg, font=("Segoe UI", 10)) + self.text_label.pack(side=iconside, fill=tk.BOTH, expand=True) + + self.close_btn = IconButton(self, 'close', self.closefn) + self.close_btn.config(bg=self.bg, fg=self.fg) + self.close_btn.pack(side=tk.RIGHT, fill=tk.BOTH) + + self.config_bindings() + self.visible = False + + def config_bindings(self) -> None: + self.bind("", self.on_enter) + self.bind("", self.on_leave) + + self.bind("", self.on_click) + if self.text: + self.text_label.bind("", self.on_click) + if self.icon: + self.icon_label.bind("", self.on_click) + + def on_enter(self, *_) -> None: + self.config(bg=self.hbg) + if self.text: + self.text_label.config(bg=self.hbg, fg=self.hfg) + if self.icon: + self.icon_label.config(bg=self.hbg, fg=self.hfg) + self.close_btn.config(bg=self.hbg, fg=self.hfg) + + def on_leave(self, *_) -> None: + self.config(bg=self.bg) + if self.text: + self.text_label.config(bg=self.bg, fg=self.fg) + if self.icon: + self.icon_label.config(bg=self.bg, fg=self.fg) + self.close_btn.config(bg=self.bg, fg=self.fg) + + def on_click(self, *_) -> None: + self.function() + + def change_text(self, text) -> None: + self.text_label.config(text=text) + + def change_icon(self, icon) -> None: + self.icon_label.config(text=icon) + + def set_pack_data(self, **kwargs) -> None: + self.pack_data = kwargs + + def get_pack_data(self): + return self.pack_data + + def show(self) -> None: + if not self.visible: + self.visible = True + self.pack(**self.get_pack_data()) + + def hide(self) -> None: + if self.visible: + self.visible = False + self.pack_forget() diff --git a/biscuit/core/components/views/sidebar/explorer/open_editors.py b/biscuit/core/components/views/sidebar/explorer/open_editors.py index 46359769..8f417053 100644 --- a/biscuit/core/components/views/sidebar/explorer/open_editors.py +++ b/biscuit/core/components/views/sidebar/explorer/open_editors.py @@ -1,14 +1,7 @@ -import os -import platform -import shutil -import subprocess -import threading import tkinter as tk -import pyperclip - from biscuit.core.components.floating.palette.actionset import ActionSet -from biscuit.core.components.utils import Tree +from biscuit.core.components.utils import CloseListItem, Menubutton from ..item import SidebarViewItem @@ -18,49 +11,43 @@ def __init__(self, master, startpath=None, itembar=True, *args, **kwargs) -> Non self.title = 'Open Editors' self.__buttons__ = (('new-file', lambda: self.base.palette.show('newfile:')),) super().__init__(master, itembar, *args, **kwargs) - - self.tree = Tree(self.content, startpath, singleclick=self.openfile, *args, **kwargs) - self.tree.bind("", self.adjust_frame_size) - self.tree.grid(row=0, column=0, sticky=tk.NSEW) - self.tree.bind("<>", self.openfile) - self.path = startpath + self.nodes = {} - def adjust_frame_size(self, *_): - if items := self.tree.get_children(): - frame_height = len(items) * 25 + 5 + def refresh(self): + if not self.nodes: + self.itembar.hide_content() else: - frame_height = 1 - self.tree.config(height=frame_height) + self.itembar.show_content() self.update() def add_item(self, editor): - self.tree.insert('', 'end', text=editor.filename, values=(editor.path, 'file')) - self.adjust_frame_size() + temp = CloseListItem(self.content, editor.filename, function=lambda p=editor.path: self.openfile(p), + closefn=lambda p=editor.path: self.closefile(p), padx=10) + temp.text_label.config(anchor=tk.W) + temp.pack(fill=tk.X, expand=True) + + self.nodes[editor.path] = temp + self.refresh() def remove_item(self, editor): - for child in self.tree.get_children(): - if self.tree.item_fullpath(child) == editor.path: - self.tree.delete(child) - self.adjust_frame_size() - if not len(self.base.editorsmanager.active_editors): - self.disable() - return + e = self.nodes.pop(editor.path) + e.pack_forget() + e.destroy() + self.refresh() def set_active(self, editor): - for child in self.tree.get_children(): - if self.tree.item_fullpath(child) == editor.path: - self.tree.tree.selection_set(child) - self.adjust_frame_size() - return + # TODO: set highlight, clear highlight on others + ... def clear(self): - self.tree.delete(*self.tree.get_children()) - self.adjust_frame_size() + for node in self.nodes.values(): + node.destroy() + self.nodes = {} - def openfile(self, _) -> None: - if self.tree.selected_type() != 'file': - return + def openfile(self, path) -> None: + self.base.editorsmanager.set_active_editor_by_path(path) - path = self.tree.selected_path() - self.base.open_editor(path) + def closefile(self, path) -> None: + self.base.editorsmanager.close_editor_by_path(path) + \ No newline at end of file diff --git a/biscuit/core/layout/base/content/editors/__init__.py b/biscuit/core/layout/base/content/editors/__init__.py index 81a83c34..a0037e28 100644 --- a/biscuit/core/layout/base/content/editors/__init__.py +++ b/biscuit/core/layout/base/content/editors/__init__.py @@ -140,6 +140,10 @@ def close_editor(self, editor: Editor) -> None: self.base.explorer.open_editors.remove_item(editor) self.refresh() + def close_editor_by_path(self, path: str) -> None: + "removes an editor by path, keeping it in cache." + self.close_editor(self.get_editor(path)) + def get_editor(self, path: str) -> Editor: "Get editor by path" for editor in self.active_editors: From d24679af2a1c2b8c7e33698fd353ae50ecf83958 Mon Sep 17 00:00:00 2001 From: Billy Date: Tue, 19 Mar 2024 05:01:15 +0530 Subject: [PATCH 3/4] feat: Close files from "open editors" list --- .../views/sidebar/explorer/open_editors.py | 7 ++++--- .../core/layout/base/content/editors/__init__.py | 8 ++++++-- biscuit/core/layout/base/content/editors/tabs.py | 13 ++++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/biscuit/core/components/views/sidebar/explorer/open_editors.py b/biscuit/core/components/views/sidebar/explorer/open_editors.py index 8f417053..5939bafe 100644 --- a/biscuit/core/components/views/sidebar/explorer/open_editors.py +++ b/biscuit/core/components/views/sidebar/explorer/open_editors.py @@ -44,10 +44,11 @@ def clear(self): for node in self.nodes.values(): node.destroy() self.nodes = {} + self.refresh() def openfile(self, path) -> None: - self.base.editorsmanager.set_active_editor_by_path(path) + self.base.editorsmanager.tabs.switch_tabs(path) def closefile(self, path) -> None: - self.base.editorsmanager.close_editor_by_path(path) - \ No newline at end of file + e = self.base.editorsmanager.close_editor_by_path(path) + self.base.editorsmanager.tabs.close_tab_helper(e) diff --git a/biscuit/core/layout/base/content/editors/__init__.py b/biscuit/core/layout/base/content/editors/__init__.py index e8f89533..fa199eee 100644 --- a/biscuit/core/layout/base/content/editors/__init__.py +++ b/biscuit/core/layout/base/content/editors/__init__.py @@ -90,6 +90,7 @@ def delete_all_editors(self) -> None: self.editorsbar.clear() self.tabs.clear_all_tabs() self.active_editors.clear() + self.base.explorer.open_editors.clear() self.refresh() def reopen_active_editor(self) -> None: @@ -130,13 +131,14 @@ def open_game(self, name: str) -> None: def close_editor(self, editor: Editor) -> None: "removes an editor, keeping it in cache." self.active_editors.remove(editor) + editor.grid_forget() if editor.content and editor.content.editable: self.base.language_server_manager.tab_closed(editor.content.text) # not keeping diff/games in cache if not editor.diff and editor.content: - self.closed_editors[editor.path] = editor + self.closed_editors[editor.path] = editor else: editor.destroy() self.base.explorer.open_editors.remove_item(editor) @@ -144,7 +146,9 @@ def close_editor(self, editor: Editor) -> None: def close_editor_by_path(self, path: str) -> None: "removes an editor by path, keeping it in cache." - self.close_editor(self.get_editor(path)) + e = self.get_editor(path) + self.close_editor(e) + return e def get_editor(self, path: str) -> Editor: "Get editor by path" diff --git a/biscuit/core/layout/base/content/editors/tabs.py b/biscuit/core/layout/base/content/editors/tabs.py index 885efaae..ffcb6128 100644 --- a/biscuit/core/layout/base/content/editors/tabs.py +++ b/biscuit/core/layout/base/content/editors/tabs.py @@ -32,9 +32,6 @@ def close_active_tab(self) -> None: self.close_tab(self.active_tab) def close_tab(self, tab: Tab) -> None: - tab.editor.grid_forget() - tab.destroy() - try: i = self.tabs.index(tab) except ValueError: @@ -42,6 +39,7 @@ def close_tab(self, tab: Tab) -> None: return self.tabs.remove(tab) + tab.destroy() self.master.master.close_editor(tab.editor) if self.tabs: @@ -53,6 +51,15 @@ def close_tab(self, tab: Tab) -> None: self.active_tab = None self.master.master.refresh() + def close_tab_helper(self, editor: str) -> None: + for tab in self.tabs: + if tab.editor == editor: + try: + self.tabs.remove(tab) + tab.destroy() + except ValueError: + return + def delete_tab(self, editor): for tab in self.tabs: if tab.editor == editor: From 594a63bb39e88d73c654ae78029a77b759bf2a2c Mon Sep 17 00:00:00 2001 From: Billy Date: Tue, 19 Mar 2024 05:05:23 +0530 Subject: [PATCH 4/4] feat: Toggle "Open Editors" from explorer context menu --- .../core/components/views/sidebar/explorer/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/biscuit/core/components/views/sidebar/explorer/__init__.py b/biscuit/core/components/views/sidebar/explorer/__init__.py index c5df02f0..af17d31c 100644 --- a/biscuit/core/components/views/sidebar/explorer/__init__.py +++ b/biscuit/core/components/views/sidebar/explorer/__init__.py @@ -17,11 +17,12 @@ def __init__(self, master, *args, **kwargs) -> None: self.name = 'Explorer' self.menu = ExplorerMenu(self, 'files') - self.menu.add_item("Open Editors", self.base.events.show_active_editors) + self.menu.add_item("Open Editors", self.toggle_active_editors) self.menu.add_separator(10) self.menu.add_item("Search", self.base.events.show_file_search_palette) self.add_button('ellipsis', self.menu.show) + self.active_editors_visible = True self.open_editors = OpenEditors(self) self.open_editors.pack(fill=tk.X) self.directory = DirectoryTree(self, observe_changes=True) @@ -44,6 +45,13 @@ def __init__(self, master, *args, **kwargs) -> None: ) self.base.palette.register_actionset(lambda: self.rename_actionset) + def toggle_active_editors(self): + if self.active_editors_visible: + self.open_editors.pack_forget() + else: + self.open_editors.pack(fill=tk.X, before=self.directory) + self.active_editors_visible = not self.active_editors_visible + def get_actionset(self, term: str) -> ActionSet: self.filesearch_actionset.update(self.filesearch(term)) return self.filesearch_actionset