diff --git a/ipygee/asset.py b/ipygee/asset.py
index 379ad46..aaeb348 100644
--- a/ipygee/asset.py
+++ b/ipygee/asset.py
@@ -1,7 +1,8 @@
"""The asset manager widget code and functionalities."""
from __future__ import annotations
-from typing import List
+from pathlib import Path
+from typing import List, Optional
import ee
import geetools # noqa
@@ -59,6 +60,18 @@ class AssetManager(v.Flex, HasSideCar):
w_card: v.Card
"The card hosting the list of items"
+ w_delete_dialog: v.Dialog
+ "The dialog to confirm the deletion of an asset"
+
+ w_move_dialog: v.Dialog
+ "The dialog to confirm the move of an asset"
+
+ w_asset_dialog: v.Dialog
+ "The dialog to view an asset"
+
+ w_create_dialog: v.Dialog
+ "The dialog to create a new folder"
+
def __init__(self):
"""Initialize the class."""
# start by defining al the widgets
@@ -66,17 +79,17 @@ def __init__(self):
# fmt: off
# add a line of buttons to reload and add new projects
- self.w_new = v.Btn(color="error", children="NEW", elevation=2, class_="ma-1")
+ self.w_new = v.Btn(color="error", children="NEW", elevation=2, class_="ma-1", disabled=True)
self.w_reload = v.Btn(children=[v.Icon(color="primary", children="mdi-reload")], elevation=2, class_="ma-1")
- self.w_search = v.Btn(children=[v.Icon(color="primary", children="mdi-magnify")], elevation=2, class_="ma-1")
+ self.w_search = v.Btn(children=[v.Icon(color="primary", children="mdi-magnify")], elevation=2, class_="ma-1", disabled=True)
w_main_line = v.Flex(children=[self.w_new, self.w_reload, self.w_search])
# generate the asset selector and the CRUD buttons
self.w_selected = v.TextField(readonly=True, placeholder="Selected item", v_model="", clearable=True, outlined=True, class_="ma-1")
- self.w_view = v.Btn(children=[v.Icon(color="primary", children="mdi-eye")])
- self.w_copy = v.Btn(children=[v.Icon(color="primary", children="mdi-content-copy")])
- self.w_move = v.Btn(children=[v.Icon(color="primary", children="mdi-file-move")])
- self.w_delete = v.Btn(children=[v.Icon(color="primary", children="mdi-trash-can")])
+ self.w_view = v.Btn(children=[v.Icon(color="primary", children="mdi-eye")], disabled=True)
+ self.w_copy = v.Btn(children=[v.Icon(color="primary", children="mdi-content-copy")], disabled=True)
+ self.w_move = v.Btn(children=[v.Icon(color="primary", children="mdi-file-move")], disabled=True)
+ self.w_delete = v.Btn(children=[v.Icon(color="primary", children="mdi-trash-can")], disabled=True)
w_btn_list = v.ItemGroup(class_="ma-1 v-btn-toggle",children=[self.w_view, self.w_copy, self.w_move, self.w_delete])
w_selected_line = v.Layout(row=True, children=[w_btn_list, self.w_selected], class_="ma-1")
@@ -85,13 +98,34 @@ def __init__(self):
self.w_list = v.List(dense=True, v_model=True, children=[w_group], outlined=True)
self.w_card = v.Card(children=[self.w_list], outlined=True, class_="ma-1")
- super().__init__(children=[w_main_line, w_selected_line, self.w_card], v_model="", class_="ma-1")
+ # create the hidden dialogs
+ self.w_delete_dialog = DeleteAssetDialog()
+ self.w_move_dialog = MoveAssetDialog()
+ self.w_asset_dialog = AssetDialog()
+ self.w_create_dialog = CreateFolderDialog()
+
+ super().__init__(children=[
+ self.w_delete_dialog, self.w_move_dialog, self.w_asset_dialog, self.w_create_dialog,
+ w_main_line, w_selected_line, self.w_card
+ ], v_model="", class_="ma-1")
# fmt: on
+ # update the template of the DOM object to add a js method to copy to clipboard
+ # template with js behaviour
+ js_dir = Path(__file__).parent / "js"
+ clip = (js_dir / "jupyter_clip.js").read_text()
+ self.template = "" "" % clip
+
# add JS behaviour
t.link((self, "selected_item"), (self, "v_model"))
self.w_list.children[0].observe(self.on_item_select, "v_model")
self.w_reload.on_event("click", self.on_reload)
+ self.w_copy.on_event("click", self.on_copy)
+ self.w_delete.on_event("click", self.on_delete)
+ self.w_selected.observe(self.activate_buttons, "v_model")
+ self.w_move.on_event("click", self.on_move)
+ self.w_view.on_event("click", self.on_view)
+ self.w_new.on_event("click", self.on_new)
def get_items(self) -> List[v.ListItem]:
"""Create the list of items inside a folder."""
@@ -169,3 +203,341 @@ def on_item_select(self, change: dict):
def on_reload(self, *args):
"""Reload the current folder."""
self.on_item_select(change={"new": self.folder})
+
+ def on_copy(self, *args):
+ """Copy the selected item to clipboard."""
+ self.send({"method": "clip", "args": [self.w_selected.v_model]})
+ self.w_copy.children[0].children = ["mdi-check"]
+
+ @switch("loading", "disabled", member="w_card")
+ def on_delete(self, *args):
+ """Delete the selected item.
+
+ Ask for confirmation before deleting via a dialog window.
+ """
+ # make sure the current item is deletable. We can only delete assets i.e.
+ # files and folders. Projects and buckets are not deletable.
+ selected = self.w_selected.v_model
+ if selected in [".", ""] or ee.Asset(selected).is_project():
+ return
+
+ # open the delete dialog with the current file
+ self.w_delete_dialog.reload(ee.Asset(selected))
+ self.w_delete_dialog.value = True
+
+ @switch("loading", "disabled", member="w_card")
+ def on_move(self, *args):
+ """Copy the selected item.
+
+ Ask for confirmation before moving via a dialog window.
+ """
+ # make sure the current item is moveable. We can only move assets i.e.
+ # files and folders. Projects and buckets are not deletable.
+ selected = self.w_selected.v_model
+ if selected in [".", ""] or ee.Asset(selected).is_project():
+ return
+
+ # open the delete dialog with the current file
+ self.w_move_dialog.reload(ee.Asset(selected))
+ self.w_move_dialog.value = True
+
+ @switch("loading", "disabled", member="w_card")
+ def on_view(self, *args):
+ """Open the view dialog."""
+ # make sure the current item is moveable. We can only move assets i.e.
+ # files and folders. Projects and buckets are not deletable.
+ selected = ee.Asset(self.w_selected.v_model)
+ if self.w_selected.v_model in [".", ""] or selected.is_project() or selected.is_folder():
+ return
+
+ # open the delete dialog with the current file
+ self.w_asset_dialog.reload(ee.Asset(selected))
+ self.w_asset_dialog.value = True
+
+ @switch("loading", "disabled", member="w_card")
+ def on_new(self, *args):
+ """Create a new folder cia the dialog."""
+ # We need to be at least in a project to be able to create a new folder
+ selected = self.w_selected.v_model
+ if selected in [".", ""]:
+ return
+
+ self.w_create_dialog.reload(ee.Asset(self.folder))
+ self.w_create_dialog.value = True
+
+ def activate_buttons(self, change: dict):
+ """Activate the appropriate buttons whenever the selected item changes."""
+ # reset everything
+ self.w_new.disabled = True
+ self.w_view.disabled = True
+ self.w_move.disabled = True
+ self.w_delete.disabled = True
+ self.w_copy.disabled = True
+ self.w_copy.children[0].children = ["mdi-content-copy"]
+
+ # We can activate the new button for projects
+ asset = ee.Asset(change["new"])
+ if asset.is_absolute():
+ self.w_new.disabled = False
+
+ # we need to exit if the selected item is a project or a root
+ if change["new"] in [".", ""] or asset.is_project():
+ return
+
+ # reactivate delete move and copy for assets
+ if asset.exists():
+ self.w_delete.disabled = False
+ self.w_move.disabled = False
+ self.w_copy.disabled = False
+
+ # we can only view files
+ if not asset.is_folder():
+ self.w_view.disabled = False
+
+
+class DeleteAssetDialog(v.Dialog):
+ """A dialog to confirm the deletion of an asset."""
+
+ # -- Variables -------------------------------------------------------------
+
+ asset: ee.Asset
+ "The asset to delete"
+
+ # -- Widgets ---------------------------------------------------------------
+ w_confirm: v.Btn
+ "The confirm button"
+
+ w_cancel: v.Btn
+ "The cancel button"
+
+ def __init__(self, asset: Optional[ee.Asset] = None):
+ """Initialize the class."""
+ # start by defining all the widgets
+ self.w_confirm = v.Btn(children=[v.Icon(children="mdi-check"), "Confirm"], color="primary")
+ self.w_cancel = v.Btn(children=[v.Icon(children=["mdi-times"]), "Cancel"])
+ w_title = v.CardTitle(children=["Delete the assets"])
+ disclaimer = 'Clicking on "confirm" will definitively delete all the following asset. This action is definitive.' # fmt: skip
+ option = 'Click on "cancel" to abort the deletion.'
+
+ self.ul = v.Html(tag="ul", children=[])
+ w_content = v.CardText(children=[disclaimer, option, self.ul])
+
+ w_actions = v.CardActions(children=[v.Spacer(), self.w_cancel, self.w_confirm])
+
+ self.w_card = v.Card(children=[w_title, w_content, w_actions])
+
+ super().__init__(children=[self.w_card], max_width="50%", persistent=True)
+
+ # js interaction with the btns
+ self.w_confirm.on_event("click", self.on_confirm)
+ self.w_cancel.on_event("click", self.on_cancel)
+
+ def reload(self, asset: ee.Asset):
+ """Reload the dialog with a new asset."""
+ # We should never arrive here with a non asset
+ # but to avoid catastrophic destruction we will empty the list first
+ if asset is None or str(asset) == ".":
+ self.ul.children = []
+
+ # save the asset as a member and read it
+ self.asset = asset
+ assets = asset.iterdir(recursive=True) if asset.is_folder() else [asset]
+ self.ul.children = [v.Html(tag="li", children=[str(a)]) for a in assets]
+
+ @switch("loading", "disabled", member="w_card")
+ def on_confirm(self, *args):
+ """Confirm the deletion."""
+ # delete the asset and close the dialog
+ if self.asset.is_folder():
+ self.asset.rmdir(recursive=True, dry_run=False)
+ else:
+ self.asset.delete()
+ self.value = False
+
+ @switch("loading", "disabled", member="w_card")
+ def on_cancel(self, *args):
+ """Exit without doing anything."""
+ self.value = False
+
+
+class MoveAssetDialog(v.Dialog):
+ """A dialog to confirm the move of an asset."""
+
+ # -- Variables -------------------------------------------------------------
+
+ asset: ee.Asset
+ "The asset to delete"
+
+ # -- Widgets ---------------------------------------------------------------
+ w_asset: v.TextField
+ "The destination to move"
+
+ w_confirm: v.Btn
+ "The confirm button"
+
+ w_cancel: v.Btn
+ "The cancel button"
+
+ def __init__(self, asset: Optional[ee.Asset] = None):
+ """Initialize the class."""
+ # start by defining all the widgets
+ # fmt: off
+ self.w_asset = v.TextField(placeholder="Destination", v_model="", clearable=True, outlined=True, class_="ma-1")
+ self.w_confirm = v.Btn(children=[v.Icon(children="mdi-check"), "Confirm"], color="primary")
+ self.w_cancel = v.Btn(children=[v.Icon(children=["mdi-times"]), "Cancel"])
+ w_title = v.CardTitle(children=["Delete the assets"])
+ disclaimer = 'Clicking on "confirm" will move the following asset to the destination. This initial asset is not deleted.'
+ option = 'Click on "cancel" to abort the move.'
+ self.ul = v.Html(tag="ul", children=[])
+ w_content = v.CardText(children=[self.w_asset, disclaimer, option, self.ul])
+ w_actions = v.CardActions(children=[v.Spacer(), self.w_cancel, self.w_confirm])
+ self.w_card = v.Card(children=[w_title, w_content, w_actions])
+ # fmt: on
+
+ super().__init__(children=[self.w_card], max_width="50%", persistent=True)
+
+ # js interaction with the btns
+ self.w_confirm.on_event("click", self.on_confirm)
+ self.w_cancel.on_event("click", self.on_cancel)
+
+ def reload(self, asset: ee.Asset):
+ """Reload the dialog with a new asset."""
+ # We should never arrive here with a non asset
+ # but to avoid catastrophic destruction we will empty the list first
+ if asset is None or str(asset) == ".":
+ self.ul.children = []
+
+ # save the asset as a member and read it
+ self.asset = asset
+ assets = asset.iterdir(recursive=True) if asset.is_folder() else [asset]
+ self.ul.children = [v.Html(tag="li", children=[str(a)]) for a in assets]
+
+ @switch("loading", "disabled", member="w_card")
+ def on_confirm(self, *args):
+ """Confirm the deletion."""
+ # remove the warnings
+ self.w_asset.error_messages = []
+
+ # delete the asset and close the dialog
+ try:
+ self.asset.move(ee.Asset(self.w_asset.v_model))
+ self.value = False
+ except Exception as e:
+ self.w_asset.error_messages = [str(e)]
+
+ @switch("loading", "disabled", member="w_card")
+ def on_cancel(self, *args):
+ """Exit without doing anything."""
+ self.value = False
+
+
+class AssetDialog(v.Dialog):
+ """A dialog to view an asset."""
+
+ # -- Variables -------------------------------------------------------------
+
+ asset: ee.Asset
+ "The asset to delete"
+
+ # -- Widgets ---------------------------------------------------------------
+
+ w_exit: v.Btn
+ "The exit button"
+
+ def __init__(self, asset: Optional[ee.Asset] = None):
+ """Initialize the class."""
+ # start by defining all the widgets
+ # fmt: off
+ self.w_exit = v.Btn(children=[v.Icon(children="mdi-check"), "Exit"])
+ self.w_title = v.CardTitle(children=["Delete the assets"])
+ w_content = v.CardText(children=[""])
+ w_actions = v.CardActions(children=[v.Spacer(), self.w_exit])
+ self.w_card = v.Card(children=[self.w_title, w_content, w_actions])
+ # fmt: on
+
+ super().__init__(children=[self.w_card], max_width="50%", persistent=True)
+
+ # js interaction with the btns
+ self.w_exit.on_event("click", self.on_exit)
+
+ def reload(self, asset: ee.Asset):
+ """Reload the dialog with a new asset."""
+ # We should never arrive here with a non asset
+ # but to avoid catastrophic destruction we will empty the list first
+ if asset is None or str(asset) == ".":
+ self.ul.children = []
+
+ # save the asset as a member and read it
+ self.asset = asset
+ self.w_title.children = [f"Viewing {asset.name}"]
+
+ @switch("loading", "disabled", member="w_card")
+ def on_exit(self, *args):
+ """Exit without doing anything."""
+ self.value = False
+
+
+class CreateFolderDialog(v.Dialog):
+ """A dialog to create a new folder asset."""
+
+ # -- Variables -------------------------------------------------------------
+
+ folder: ee.Asset
+ "The current folder where to create the new folder."
+
+ # -- Widgets ---------------------------------------------------------------
+ w_asset: v.TextField
+ "The destination to move"
+
+ w_confirm: v.Btn
+ "The confirm button"
+
+ w_cancel: v.Btn
+ "The cancel button"
+
+ def __init__(self, asset: Optional[ee.Asset] = None):
+ """Initialize the class."""
+ # start by defining all the widgets
+ # fmt: off
+ self.w_asset = v.TextField(placeholder="Folder name", v_model="", clearable=True, outlined=True, class_="ma-1")
+ self.w_confirm = v.Btn(children=[v.Icon(children="mdi-check"), "Confirm"], color="primary")
+ self.w_cancel = v.Btn(children=[v.Icon(children=["mdi-times"]), "Cancel"])
+ w_title = v.CardTitle(children=["Create a new folder"])
+ w_content = v.CardText(children=[self.w_asset])
+ w_actions = v.CardActions(children=[v.Spacer(), self.w_cancel, self.w_confirm])
+ self.w_card = v.Card(children=[w_title, w_content, w_actions])
+ # fmt: on
+
+ super().__init__(children=[self.w_card], max_width="50%", persistent=True)
+
+ # js interaction with the btns
+ self.w_confirm.on_event("click", self.on_confirm)
+ self.w_cancel.on_event("click", self.on_cancel)
+
+ def reload(self, folder: ee.Asset):
+ """Reload the dialog with a new asset."""
+ # check the new destination is at least a project
+ if not ee.Asset(folder).is_absolute():
+ return
+
+ self.folder = folder
+ self.w_asset.prefix = f"{folder}/"
+ self.w_asset.v_model = ""
+
+ @switch("loading", "disabled", member="w_card")
+ def on_confirm(self, *args):
+ """Confirm the deletion."""
+ # remove the warnings
+ self.w_asset.error_messages = []
+
+ # crezte the folder and close the dialog
+ try:
+ (self.folder / self.w_asset.v_model).mkdir(exist_ok=True, parents=True)
+ self.value = False
+ except Exception as e:
+ self.w_asset.error_messages = [str(e)]
+
+ @switch("loading", "disabled", member="w_card")
+ def on_cancel(self, *args):
+ """Exit without doing anything."""
+ self.value = False
diff --git a/ipygee/js/jupyter_clip.js b/ipygee/js/jupyter_clip.js
new file mode 100644
index 0000000..02605b6
--- /dev/null
+++ b/ipygee/js/jupyter_clip.js
@@ -0,0 +1,7 @@
+var tempInput = document.createElement("input");
+tempInput.value = _txt;
+document.body.appendChild(tempInput);
+tempInput.focus();
+tempInput.select();
+document.execCommand("copy");
+document.body.removeChild(tempInput);