2 @package libtextworker.interface.wx.dirctrl
13 import wx.lib.newevent
15 from libtextworker
import _
20 from typing
import Callable
22 from watchdog.observers
import Observer
26 imgs = wx.ImageList(16, 16)
28 def addImg(name: str) -> int:
30 wx.ArtProvider.GetBitmap(
31 getattr(wx, f
"ART_{name.upper()}"), wx.ART_OTHER, (16, 16)
35 folderidx = addImg(
"folder")
36 fileidx = addImg(
"normal_file")
37 openfolderidx = addImg(
"folder_open")
43 FileEditedEvent, EVT_FILE_EDITED = wx.lib.newevent.NewEvent()
44 FileCreatedEvent, EVT_FILE_CREATED = wx.lib.newevent.NewEvent()
45 FileDeletedEvent, EVT_FILE_DELETED = wx.lib.newevent.NewEvent()
46 FileOpenedEvent, EVT_FILE_OPEN = wx.lib.newevent.NewEvent()
47 FileClosedEvent, EVT_FILE_CLOSED = wx.lib.newevent.NewEvent()
48 FileMovedEvent, EVT_FILE_MOVED = wx.lib.newevent.NewEvent()
50 DirEditedEvent, EVT_DIR_EDITED = wx.lib.newevent.NewEvent()
51 DirCreatedEvent, EVT_DIR_CREATED = wx.lib.newevent.NewEvent()
52 DirMovedEvent, EVT_DIR_MOVED = wx.lib.newevent.NewEvent()
53 DirDeletedEvent, EVT_DIR_DELETED = wx.lib.newevent.NewEvent()
57 A file system event handler derived from watchdog's FileSystemEventHandler.
59 On both wx and Tk, a new event will be generated.
60 Set the Target attribute which the handler sends the event to.
62 Or, if you use this for your own widget, derive this class like any other classes
63 you use for that widget, and set TargetIsSelf = True instead of Target.
64 This class does not use __init__.
66 Currently only one target is supported.
70 from watchdog.observers import Observer
73 def on_close(evt): # On window close
79 # Do anything with evt.path!
81 wind = wx.(...) # A wxWindow
82 wind.Bind("put your wanted event here", func)
83 path = os.path.expanduser('~/')
84 evt_handler = FSEventHandler()
85 evt_handler.Target = wind
87 observer.schedule(evt_handler, path, recursive=True)
93 TargetIsSelf: bool =
True
95 def evtIsDir(this, event: FileSystemEvent):
return "Dir" if event.is_directory
else "File"
96 def getTarget(this):
return this.Target
if not this.TargetIsSelf
else this
101 def on_moved(this, event: FileSystemEvent):
102 if this.evtIsDir(event) ==
"Dir": cls_to_use = DirMovedEvent
103 else: cls_to_use = FileMovedEvent
104 wx.PostEvent(this.getTarget(), cls_to_use(path=event.src_path))
106 def on_created(this, event: FileSystemEvent):
107 if this.evtIsDir(event) ==
"Dir": cls_to_use = DirCreatedEvent
108 else: cls_to_use = FileCreatedEvent
109 wx.PostEvent(this.getTarget(), cls_to_use(path=event.src_path))
111 def on_deleted(this, event: FileSystemEvent):
112 if this.evtIsDir(event) ==
"Dir": cls_to_use = DirDeletedEvent
113 else: cls_to_use = FileDeletedEvent
114 wx.PostEvent(this.getTarget(), cls_to_use(path=event.src_path))
116 def on_modified(this, event: FileSystemEvent):
117 if this.evtIsDir(event) ==
"Dir": cls_to_use = DirEditedEvent
118 else: cls_to_use = FileEditedEvent
119 wx.PostEvent(this.getTarget(), cls_to_use(path=event.src_path))
121 def on_closed(this, event: FileSystemEvent):
122 wx.PostEvent(this.getTarget(), FileClosedEvent(path=event.src_path))
124 def on_opened(this, event: FileSystemEvent):
125 wx.PostEvent(this.getTarget(), FileOpenedEvent(path=event.src_path))
132 Parent_ArgName =
"parent"
134 Observers: dict[str] = {}
138 A directory list made from wxTreeCtrl.
139 This is WIP, and lacks lots of features:
141 * Copy-paste + right-click menu
143 * Hidden files detect (quite hard, may need to use C/C++)
147 * DC_EDIT = wx.TR_EDIT_LABELS
148 * DC_HIDEROOT = hide the root node
149 * DC_ONEROOT: only use one root node
150 * DC_MULTIPLE = wx.TR_MULTIPLE (multiple selections)
151 * No flag at all = wx.TR_DEFAULT_STYLE
153 If you want to add more than one folder to this control,
154 use DC_HIDEROOT and disable DC_ONEROOT (default).
156 args, kw = DirCtrlBase.__init__(this, *args, **kw)
159 if not "style" in kw:
162 if not len(args) >= 5:
163 styles = wx.TR_DEFAULT_STYLE
172 if DC_EDIT
in this.Styles:
173 styles |= wx.TR_EDIT_LABELS
175 if DC_HIDEROOT
in this.Styles:
176 if not DC_ONEROOT
in this.Styles:
177 styles |= wx.TR_HIDE_ROOT
181 if DC_MULTIPLE
in this.Styles:
182 styles |= wx.TR_MULTIPLE
189 wx.TreeCtrl.__init__(this, *args, **kw)
190 this.AssignImageList(imgs)
192 this.Bind(wx.EVT_TREE_ITEM_EXPANDED, this.LazyExpand)
193 this.Bind(wx.EVT_TREE_SEL_CHANGED, this.LazyExpand)
195 def AddItem(evt: FileCreatedEvent | DirCreatedEvent):
196 this.AppendItem(this.MatchItem(os.path.dirname(evt.path)),
197 os.path.basename(evt.path),
198 fileidx
if isinstance(evt, FileCreatedEvent)
else folderidx)
201 def DeleteItem(evt: FileDeletedEvent | DirDeletedEvent):
202 wx.TreeCtrl.Delete(this, this.MatchItem(evt.path))
205 this.Bind(EVT_FILE_CREATED, AddItem)
206 this.Bind(EVT_DIR_CREATED, AddItem)
207 this.Bind(EVT_FILE_DELETED, DeleteItem)
208 this.Bind(EVT_DIR_DELETED, DeleteItem)
212 for item
in this.Observers:
213 this.Observers[item].stop()
214 this.Observers[item].join()
215 return wx.TreeCtrl.Destroy(this)
219 Expand the given/currently selected item.
221 Explain: if the target item has childs inside, that means
222 the item has been opened before. This can be done by checking
223 whether the item full path is a directory and has items inside.
225 if isinstance(what, wx.TreeItemId):
228 path = this.GetSelection()
230 fullpath = os.path.normpath(this.GetFullPath(path))
232 if os.path.isdir(fullpath)
and this.ItemHasChildren(path):
233 wx.TreeCtrl.DeleteChildren(this, path)
234 this.SetItemImage(path, openfolderidx, wx.TreeItemIcon_Expanded)
235 ls = os.listdir(fullpath)
237 craftedpath = CraftItems(fullpath, item)
238 if os.path.isfile(craftedpath)
and DC_DIRONLY
in this.Styles:
240 icon = folderidx
if os.path.isdir(craftedpath)
else fileidx
242 newitem = this.AppendItem(path, item, icon)
244 if os.path.isdir(craftedpath)
and len(os.listdir(craftedpath)) >= 1:
245 this.SetItemHasChildren(newitem)
247 if isinstance(what, wx.PyEvent):
252 Make DirCtrl to open (a) directory.
253 @param path (str): Target path
254 @since 0.1.4: Code description
257 DirCtrlBase.SetFolder(this, path,
False)
259 if not DC_ONEROOT
in this.Styles
and DC_HIDEROOT
in this.Styles:
260 root = this.GetRootItem()
262 if not root: root = this.AddRoot(
"Hidden root")
264 for x
in this.GetNodeChildren(root):
265 if this.GetItemText(x) == path:
267 if not kickstart: kickstart = this.AppendItem(root, path)
269 elif DC_ONEROOT
in this.Styles:
270 this.DeleteAllItems()
271 kickstart = this.AddRoot(path)
273 raise libTewException(
"The tree cannot determine whether to delete everything"
274 " and start from scratch or just add a new one while keeping"
275 " the old root node. Ask the app developer for this.")
277 this.SetItemHasChildren(kickstart)
278 this.SetItemImage(kickstart, folderidx)
280 this.Observers[path] = Observer()
281 this.Observers[path].schedule(this, path, recursive=
True)
282 this.Observers[path].start()
288 def MatchItem(this, path: str, start: wx.TreeItemId |
None =
None) -> wx.TreeItemId:
290 Find for an item in the tree by the specified path.
291 "Item" here is a wx.TreeItemId object.
293 parent = this.GetRootItem()
if not start
else start
294 item, cookie = this.GetFirstChild(parent)
296 if this.GetFullPath(item) == path:
298 if this.ItemHasChildren(item):
299 check = this.MatchItem(path, item)
300 if check:
return check
301 item, cookie = this.GetNextChild(parent, cookie)
305 Get all subitems of a tree node.
308 if isinstance(item, str):
309 node = this.MatchItem(item)
313 it, cookie = this.GetFirstChild(node)
316 it, cookie = this.GetNextChild(it, cookie)
321 Delete the specified item, but also delete the item on the
323 wx.TreeCtrl.Delete() exists (original method, use with care).
326 fullpath = this.GetFullPath(item)
327 if os.path.isdir(fullpath)
and fullpath
in this.Observers:
328 this.Observers[fullpath].stop()
329 this.Observers[fullpath].join()
330 this.Observers.pop(fullpath)
333 if os.path.isdir(fullpath): shutil.rmtree(fullpath)
334 else: os.remove(fullpath)
335 wx.TreeCtrl.Delete(this, item)
339 Remove all subitems of the specified item, except the item itself.
340 Also remove them from the file system.
341 wx.TreeCtrl.DeleteChildren() exists (original method, use with care).
343 if not this.ItemHasChildren(item):
return
344 for child
in this.GetNodeChildren(item):
349 Get the full path of an item.
350 The same work as wxGenericDirCtrl.GetPath.
353 parent = this.GetSelection()
359 if parent == this.GetRootItem():
360 return this.GetItemText(parent)
364 text = this.GetItemText(parent)
365 result.insert(0, text)
366 if parent != this.GetRootItem():
367 if DC_HIDEROOT
in this.Styles
and DC_ONEROOT
not in this.Styles \
368 and parent
in this.GetNodeChildren(this.GetRootItem()):
return
369 parent = this.GetItemParent(parent)
375 return CraftItems(*tuple(result))
382 PatchedDirCtrl = DirCtrl
387 Unlike wxDirCtrl, wxDirList lists not only files + folders, but also their
388 last modified and item type, size. You will see it most in your file explorer,
390 Of couse this is wxLC_REPORT will be used.
391 This comes with check buttons, which is optional.
393 libtextworker flags to be ignored:
394 * DC_HIDEROOT (where's the root node in this list control?)
395 * DC_ONEROOT (one root by default so this is useless)
404 def __init__(this, parent: wx.Window, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
405 style=wx.LC_REPORT, validator=wx.DefaultValidator, name=wx.ListCtrlNameStr, w_styles: auto = DC_USEICON):
407 if DC_EDIT
in w_styles:
408 style |= wx.LC_EDIT_LABELS
410 for i
in [wx.LC_ICON, wx.LC_SMALL_ICON, wx.LC_LIST]:
414 DirCtrlBase.__init__(this)
415 wx.ListCtrl.__init__(this, parent, id, pos, size, style, validator, name)
417 this.InsertColumn(0, _(
"Name"), width=246)
418 this.InsertColumn(1, _(
"Item type"))
419 this.InsertColumn(2, _(
"Last modified"), width=150)
420 this.InsertColumn(3, _(
"Size"))
422 this.AssignImageList(imgs, wx.IMAGE_LIST_SMALL)
424 this.Bind(wx.EVT_LIST_ITEM_ACTIVATED, this.SetFolder)
430 wx.ListCtrl.Destroy(this)
432 def DrawItems(this, path: str = os.path.expanduser(
"~/")):
434 Fill the list control with items;)
437 this.DeleteAllItems()
438 for item
in os.listdir(path):
439 crafted = os.path.join(path, item)
440 statinfo = os.stat(crafted)
443 if os.path.isdir(crafted):
445 this.InsertItem(0, item, folderidx)
446 this.SetItem(0, 1, _(
"Folder"))
447 elif DC_DIRONLY
not in this.Styles:
448 it_size = statinfo.st_size
449 this.InsertItem(0, item, fileidx)
450 this.SetItem(0, 1, _(
"File"))
452 m_time = statinfo.st_mtime
453 lastmod = time.strftime(
"%d %b %Y, %H:%M:%S", time.localtime(m_time))
455 this.SetItem(0, 2, lastmod)
457 if isinstance(it_size, int):
458 it_size = this.sizeof_fmt(it_size)
460 this.SetItem(0, 3, str(it_size))
465 Make this control show the content of a folder.
466 @param evt = None: wxListCtrl event
467 @param path (str): Target path (if not specified but evt will use the current item instead)
471 name = this.GetItemText(pos)
472 item_type = this.GetItemText(pos, 1)
474 if item_type == _(
"Folder"):
475 path = os.path.join(this.currpath, name)
478 raise Exception(
"Who the hell call DirList.SetFolder with no directory to go???")
480 DirCtrlBase.SetFolder(this, path,
False)
482 this.Watcher = Observer()
483 this.Watcher.schedule(this, path,
True)
485 this.PostSetDir(path,
"go")
488 this.SetFolder(path=os.path.dirname(this.currpath))
491 this, item: str | Callable |
None =
None, event: Callable |
None =
None
494 Never be implemented.
495 Find the way yourself.
497 raise NotImplementedError(
"Why do you calling this? Don't be too lazy!")
list[wx.TreeItemId] GetNodeChildren(this, wx.TreeItemId|str item)
def DeleteChildren(this, wx.TreeItemId item)
wx.TreeItemId MatchItem(this, str path, wx.TreeItemId|None start=None)
def SetFolder(this, str path)
def __init__(this, *args, **kw)
def Delete(this, wx.TreeItemId item)
def LazyExpand(this, wx.PyEvent|wx.TreeItemId what)
str GetFullPath(this, wx.TreeItemId|None item=None)
def SetFolder(this, evt=None, str path="")
def GetFullPath(this, str|Callable|None item=None, Callable|None event=None)
def DrawItems(this, str path=os.path.expanduser("~/"))