libtextworker 0.1.4
Cross-platform, free and open library for Python projects
dirctrl.py
1 """
2 @package libtextworker.interface.tk.dirctrl
3 @brief Directory tree for Tkinter
4 """
5 
6 # A cross-platform library for Python apps.
7 # Copyright (C) 2023-2024 Le Bao Nguyen and contributors.
8 # This is a part of the libtextworker project.
9 # Licensed under the GNU General Public License version 3.0 or later.
10 
11 import os
12 import time
13 
14 from tkinter import TclError, ttk, Misc
15 
16 from ..base.dirctrl import *
17 from ... import _
18 from ...general import CraftItems
19 
20 from watchdog.events import FileSystemEvent, FileSystemEventHandler
21 from watchdog.observers import Observer
22 
23 # File system events
24 # I leave them here and you just do what you want
25 FileEditedEvent = "<<FileEdited>>"
26 FileCreatedEvent = "<<FileCreated>>"
27 FileDeletedEvent = "<<FileDeleted>>"
28 FileOpenedEvent = "<<FileOpened>>"
29 FileClosedEvent = "<<FileClosed>>"
30 FileMovedEvent = "<<FileMoved>>"
31 
32 DirEditedEvent = "<<DirEdited>>"
33 DirCreatedEvent = "<<DirCreated>>"
34 DirMovedEvent = "<<DirMoved>>"
35 DirDeletedEvent = "<<DirDeleted>>"
36 
37 class FSEventHandler(FileSystemEventHandler):
38  """
39  A file system events handler derived from watchdog's FileSystemEventHandler.
40 
41  On both wx and Tk, a new event will be generated.
42  Set the Target attribute which the handler sends the event to.
43 
44  On Tk, since a new event is just a new event: no custom value allowed. However
45  you can do a bind_class to connect the event to all widgets with your specified
46  class:
47 
48  ```python
49  # Binds FileDeletedEvent to all StyledTextControl instances
50  root.bind_class('StyledTextControl', FileDeletedEvent, callback)
51  # Or this (add=True won't replace the current callback if any)
52  widget.bind(FileDeletedEvent, callback, add=True)
53  ```
54 
55  Or, if you use this for your own widget, derive this class like any other classes
56  you use for that widget, and set TargetIsSelf = True instead of Target.
57  This class does not use __init__.
58 
59  Currently only one target is supported. You need to do something else to handle the
60  events generated here.
61  """
62 
63  Target: Misc
64  TargetIsSelf: bool
65 
66  def evtIsDir(this, event: FileSystemEvent): return "Dir" if event.is_directory else "File"
67  def getTarget(this): return this.Target if not this.TargetIsSelf else this
68 
69  # It sucks when I can't use __dict__ (module variable) to access
70  # class easier (= less code used)
71 
72  def on_moved(this, event: FileSystemEvent):
73  if this.evtIsDir(event) == "Dir": what_to_use = DirMovedEvent
74  else: what_to_use = FileMovedEvent
75  this.getTarget().event_generate(what_to_use, path=event.src_path)
76 
77  def on_created(this, event: FileSystemEvent):
78  if this.evtIsDir(event) == "Dir": what_to_use = DirCreatedEvent
79  else: what_to_use = FileCreatedEvent
80  this.getTarget().event_generate(what_to_use, path=event.src_path)
81 
82  def on_deleted(this, event: FileSystemEvent):
83  if this.evtIsDir(event) == "Dir": what_to_use = DirDeletedEvent
84  else: what_to_use = FileDeletedEvent
85  this.getTarget().event_generate(what_to_use, path=event.src_path)
86 
87  def on_modified(this, event: FileSystemEvent):
88  if this.evtIsDir(event) == "Dir": what_to_use = DirEditedEvent
89  else: what_to_use = FileEditedEvent
90  this.getTarget().event_generate(what_to_use, path=event.src_path)
91 
92  def on_closed(this, event: FileSystemEvent):
93  this.getTarget().event_generate(FileClosedEvent, path=event.src_path)
94 
95  def on_opened(this, event: FileSystemEvent):
96  this.getTarget().event_generate(FileOpenedEvent, path=event.src_path)
97 
98 class DirCtrl(ttk.Treeview, FSEventHandler, DirCtrlBase):
99  Parent_ArgName = "master"
100  TargetIsSelf = True
101  Observers: dict[str, Observer] = {}
102  _Frame = ttk.Frame
103 
104  def __init__(this, *args, **kwds):
105  """
106  A ttkTreeview customized to show folder list using os.listdir.
107  Multiple roots is supported, but only adding new for now.
108  Lacks label editing, DND, right-click menu, item icon.
109 
110  DirCtrl's custom styles can be defined via the "w_styles" keyword.
111  """
112  args, kwds = DirCtrlBase.__init__(this, *args, **kwds)
113  ttk.Treeview.__init__(this, this.Frame, *args, **kwds)
114 
115  ysb = ttk.Scrollbar(this.Frame, orient="vertical", command=this.yview)
116  xsb = ttk.Scrollbar(this.Frame, orient="horizontal", command=this.xview)
117  this.configure(yscroll=ysb.set, xscroll=xsb.set)
118 
119  # ysb.pack(fill="y", expand=True, side="right")
120  # xsb.pack(fill="x", expand=True, side="bottom")
121  # this.pack(expand=True, fill="both")
122 
123  this.grid(column=0, row=0)
124  ysb.grid(column=1, row=0, sticky="ns")
125  xsb.grid(column=0, row=1, sticky="ew")
126 
127  def destroy(this):
128  if this.Observers:
129  for key in this.Observers:
130  this.Observers[key].stop()
131  this.Observers[key].join()
132  this.Observers.clear()
133  ttk.Treeview.destroy(this)
134 
135  def SetFolder(this, path: str, newroot: bool = False):
136  def insert_node(node: str, folderpath: str):
137  for item in os.listdir(folderpath):
138  # craftedpath = CraftItems(fullpath, item)
139  new = this.insert(node, "end", text=item, open=False)
140  if os.path.isdir(os.path.join(folderpath, item)):
141  this.insert(new, "end")
142 
143  # "Lazy" expand
144  # Only load the folder content when the user open
145 
146  def Expand(evt):
147  path = this.focus()
148  this.item(path, open=True)
149  fullpath = os.path.normpath(this.GetFullPath())
150  iter = os.listdir(fullpath)
151 
152  if len(iter) == 0:
153  return
154  try:
155  this.delete(this.get_children(path))
156  except TclError:
157  pass
158 
159  insert_node(path, fullpath)
160 
161  DirCtrlBase.SetFolder(this, path, newroot)
162 
163  first = this.insert("", "end", text=path)
164  insert_node(first, path)
165 
166  this.Observers[path] = Observer()
167  this.Observers[path].schedule(this, path, recursive=True)
168  this.Observers[path].start()
169 
170  this.bind("<<TreeviewOpen>>", Expand)
171 
172  def GetFullPath(this, item: str | None = None) -> str:
173  # Like wx, ttkTreeView handles items by IDs
174  if not item:
175  item = this.focus()
176 
177  parent = this.parent(item)
178  node = []
179 
180  def getroot():
181  nonlocal parent
182  text = this.item(parent, "text")
183  node.append(text)
184  if parent != "":
185  parent = this.parent(parent)
186  getroot()
187 
188  getroot()
189  node.pop()
190  node.reverse()
191  # print(node)
192 
193  if node:
194  return CraftItems(*tuple(node), this.item(item, "text"))
195 
196 
197 class DirList(ttk.Treeview, DirCtrlBase):
198  Parent_ArgName = "master"
199  _Frame = ttk.Frame
200 
201  def __init__(this, *args, **kwds):
202  """
203  A directory items list.
204  By default contains these columns:
205  * Name
206  * Item type (file, folder)
207  * Last modified time
208  * Item size
209  Navigate history support. Not much customizable for now.
210  No libtextworker custom style support for now.
211  """
212 
213  args, kwds = DirCtrlBase.__init__(this, *args, **kwds)
214  ttk.Treeview.__init__(this, this.Frame, show="headings",
215  columns=[_("Name"), _("Item type"), _("Last modified"), _("Size")],
216  *args, **kwds)
217 
218  ysb = ttk.Scrollbar(this.Frame, orient="vertical", command=this.yview)
219  xsb = ttk.Scrollbar(this.Frame, orient="horizontal", command=this.xview)
220  this.configure(yscroll=ysb.set, xscroll=xsb.set)
221 
222  # ysb.pack(fill="y", expand=True, side="right")
223  # xsb.pack(fill="x", expand=True, side="bottom")
224  # this.pack(expand=True, fill="both")
225 
226  this.grid(column=0, row=0, sticky="nsew")
227  ysb.grid(column=1, row=0, sticky="nse")
228  xsb.grid(column=0, row=1, sticky="ews")
229 
230  def SetFolder(this, path: str):
231  """
232  Navigate to the specified folder.
233  """
234  DirCtrlBase.SetFolder(this, path, False)
235  this.delete(*this.get_children())
236 
237  for it in os.listdir(path):
238  fullpath = os.path.join(path, it)
239  statinfo = os.stat(fullpath)
240 
241  if os.path.isdir(os.path.join(path, it)):
242  it_type = _("Folder")
243  it_size = ""
244  elif DC_DIRONLY not in this.Styles:
245  it_type = _("File")
246  it_size = this.sizeof_fmt(statinfo.st_size)
247 
248  lastmod = time.strftime(
249  "%d %b %Y, %H:%M:%S", time.localtime(statinfo.st_mtime)
250  )
251 
252  this.insert("", "end", values=(it, it_type, lastmod, it_size))
def __init__(this, *args, **kwds)
Definition: dirctrl.py:104
def SetFolder(this, str path, bool newroot=False)
Definition: dirctrl.py:135
def __init__(this, *args, **kwds)
Definition: dirctrl.py:201