file_browser.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. #!/usr/bin/python3
  2. """
  3. File Browser
  4. """
  5. import os
  6. import math
  7. import stat
  8. import sys
  9. import subprocess
  10. import cairo
  11. import yutani
  12. import text_region
  13. import toaru_fonts
  14. from menu_bar import MenuBarWidget, MenuEntryAction, MenuEntrySubmenu, MenuEntryDivider, MenuWindow
  15. from icon_cache import get_icon
  16. from about_applet import AboutAppletWindow
  17. from input_box import TextInputWindow
  18. from dialog import DialogWindow
  19. import yutani_mainloop
  20. app_name = "File Browser"
  21. version = "1.0.0"
  22. _description = f"<b>{app_name} {version}</b>\n© 2017-2018 K. Lange\n\nFile system navigator.\n\n<color 0x0000FF>http://github.com/klange/toaruos</color>"
  23. class File(object):
  24. def __init__(self, path, window):
  25. self.path = path
  26. self.name = os.path.basename(path)
  27. self.stat = os.stat(path)
  28. self.hilight = False
  29. self.window = window
  30. self.tr = text_region.TextRegion(0,0,100,20)
  31. self.tr.set_alignment(2)
  32. self.tr.set_ellipsis()
  33. self.tr.set_text(self.name)
  34. self.x = 0
  35. self.y = 0
  36. @property
  37. def is_directory(self):
  38. return stat.S_ISDIR(self.stat.st_mode)
  39. @property
  40. def is_executable(self):
  41. return stat.S_IXUSR & self.stat.st_mode and not self.is_directory
  42. @property
  43. def icon(self):
  44. if self.is_directory: return get_icon('folder',48)
  45. if self.is_executable: return get_icon(self.name,48)
  46. return get_icon('file',48) # Need file icon
  47. def do_action(self):
  48. if self.is_directory:
  49. self.window.load_directory(self.path)
  50. self.window.draw()
  51. elif self.is_executable:
  52. subprocess.Popen([self.path])
  53. elif self.name.endswith('.png'):
  54. subprocess.Popen(['painting.py',self.path])
  55. elif self.name.endswith('.pdf') and os.path.exists('/usr/bin/pdfviewer.py'):
  56. subprocess.Popen(['pdfviewer.py',self.path])
  57. elif self.name.endswith('.pdf') and os.path.exists('/usr/bin/pdfviewer'):
  58. subprocess.Popen(['pdfviewer',self.path])
  59. # Nothing to do.
  60. @property
  61. def sortkey(self):
  62. if self.is_directory: return "___" + self.name
  63. else: return "zzz" + self.name
  64. class FileBrowserWindow(yutani.Window):
  65. base_width = 400
  66. base_height = 300
  67. def __init__(self, decorator, path):
  68. super(FileBrowserWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=app_name, icon="folder", doublebuffer=True)
  69. self.move(100,100)
  70. self.decorator = decorator
  71. def exit_app(action):
  72. menus = [x for x in self.menus.values()]
  73. for x in menus:
  74. x.definitely_close()
  75. self.close()
  76. sys.exit(0)
  77. def about_window(action):
  78. AboutAppletWindow(self.decorator,f"About {app_name}","/usr/share/icons/48/folder.png",_description,"folder")
  79. def help_browser(action):
  80. subprocess.Popen(["help-browser.py","file_browser.trt"])
  81. def input_path(action):
  82. def input_callback(input_window):
  83. text = input_window.tr.text
  84. input_window.close()
  85. self.load_directory(text)
  86. TextInputWindow(self.decorator,"Open directory...","open",text=self.path,callback=input_callback,window=self)
  87. menus = [
  88. ("File", [
  89. MenuEntryAction("Exit","exit",exit_app,None),
  90. ]),
  91. ("Go", [
  92. MenuEntryAction("Path...","open",input_path,None),
  93. MenuEntryDivider(),
  94. MenuEntryAction("Home","home",self.load_directory,os.environ.get("HOME")),
  95. MenuEntryAction("File System",None,self.load_directory,"/"),
  96. MenuEntryAction("Up","up",self.go_up,None),
  97. ]),
  98. ("Help", [
  99. MenuEntryAction("Contents","help",help_browser,None),
  100. MenuEntryDivider(),
  101. MenuEntryAction(f"About {app_name}","star",about_window,None),
  102. ]),
  103. ]
  104. self.menubar = MenuBarWidget(self,menus)
  105. self.hover_widget = None
  106. self.down_button = None
  107. self.menus = {}
  108. self.hovered_menu = None
  109. self.buf = None
  110. self.load_directory(path)
  111. self.hilighted = None
  112. def go_up(self, action):
  113. self.load_directory(os.path.abspath(os.path.join(self.path,'..')))
  114. self.draw()
  115. def load_directory(self, path):
  116. if not os.path.exists(path):
  117. DialogWindow(self.decorator,app_name,f"The path <mono>{path}</mono> could not be opened. (Not found)",window=self,icon='folder')
  118. return
  119. if not os.path.isdir(path):
  120. DialogWindow(self.decorator,app_name,f"The path <mono>{path}</mono> could not be opened. (Not a directory)",window=self,icon='folder')
  121. return
  122. path = os.path.normpath(path)
  123. self.path = path
  124. title = "/" if path == "/" else os.path.basename(path)
  125. self.set_title(f"{title} - {app_name}",'folder')
  126. self.files = sorted([File(os.path.join(path,f), self) for f in os.listdir(path)], key=lambda x: x.sortkey)
  127. self.scroll_y = 0
  128. self.hilighted = None
  129. self.redraw_buf()
  130. def redraw_buf(self,icons=None):
  131. if self.buf:
  132. self.buf.destroy()
  133. w = self.width - self.decorator.width(self)
  134. files_per_row = int(w / 100)
  135. self.buf = yutani.GraphicsBuffer(w,math.ceil(len(self.files)/files_per_row)*100)
  136. surface = self.buf.get_cairo_surface()
  137. ctx = cairo.Context(surface)
  138. if icons:
  139. for icon in icons:
  140. ctx.rectangle(icon.x,icon.y,100,100)
  141. ctx.clip()
  142. ctx.rectangle(0,0,surface.get_width(),surface.get_height())
  143. ctx.set_source_rgb(1,1,1)
  144. ctx.fill()
  145. offset_x = 0
  146. offset_y = 0
  147. for f in self.files:
  148. if not icons or f in icons:
  149. x_, y_ = ctx.user_to_device(0,0)
  150. f.tr.move(offset_x,offset_y+60)
  151. f.tr.draw(self.buf)
  152. ctx.set_source_surface(f.icon,offset_x + 26,offset_y+10)
  153. ctx.paint_with_alpha(1.0 if not f.hilight else 0.7)
  154. f.x = offset_x
  155. f.y = offset_y
  156. offset_x += 100
  157. if offset_x + 100 > surface.get_width():
  158. offset_x = 0
  159. offset_y += 100
  160. def draw(self):
  161. surface = self.get_cairo_surface()
  162. WIDTH, HEIGHT = self.width - self.decorator.width(self), self.height - self.decorator.height(self)
  163. ctx = cairo.Context(surface)
  164. ctx.translate(self.decorator.left_width(self), self.decorator.top_height(self))
  165. ctx.rectangle(0,0,WIDTH,HEIGHT)
  166. ctx.set_source_rgb(1,1,1)
  167. ctx.fill()
  168. ctx.save()
  169. ctx.translate(0,self.menubar.height)
  170. text = self.buf.get_cairo_surface()
  171. ctx.set_source_surface(text,0,self.scroll_y)
  172. ctx.paint()
  173. ctx.restore()
  174. self.menubar.draw(ctx,0,0,WIDTH)
  175. self.decorator.render(self)
  176. self.flip()
  177. def finish_resize(self, msg):
  178. """Accept a resize."""
  179. if msg.width < 120 or msg.height < 120:
  180. self.resize_offer(max(msg.width,120),max(msg.height,120))
  181. return
  182. self.resize_accept(msg.width, msg.height)
  183. self.reinit()
  184. self.redraw_buf()
  185. self.draw()
  186. self.resize_done()
  187. self.flip()
  188. def scroll(self, amount):
  189. w,h = self.width - self.decorator.width(self), self.height - self.decorator.height(self)
  190. files_per_row = int(w / 100)
  191. rows_total = math.ceil(len(self.files) / files_per_row)
  192. rows_visible = int((h - 24) / 100)
  193. rows = rows_total - rows_visible
  194. if rows < 0: rows = 0
  195. self.scroll_y += amount
  196. if self.scroll_y > 0:
  197. self.scroll_y = 0
  198. if self.scroll_y < -100 * rows:
  199. self.scroll_y = -100 * rows
  200. def mouse_event(self, msg):
  201. decor_event = d.handle_event(msg)
  202. if decor_event == yutani.Decor.EVENT_CLOSE:
  203. window.close()
  204. sys.exit(0)
  205. elif decor_event == yutani.Decor.EVENT_RIGHT:
  206. d.show_menu(self, msg)
  207. x,y = msg.new_x - self.decorator.left_width(self), msg.new_y - self.decorator.top_height(self)
  208. w,h = self.width - self.decorator.width(self), self.height - self.decorator.height(self)
  209. if x >= 0 and x < w and y >= 0 and y < self.menubar.height:
  210. self.menubar.mouse_event(msg, x, y)
  211. return
  212. if x >= 0 and x < w and y >= self.menubar.height and y < h:
  213. if msg.buttons & yutani.MouseButton.SCROLL_UP:
  214. self.scroll(30)
  215. self.draw()
  216. return
  217. elif msg.buttons & yutani.MouseButton.SCROLL_DOWN:
  218. self.scroll(-30)
  219. self.draw()
  220. return
  221. if msg.buttons & yutani.MouseButton.BUTTON_RIGHT:
  222. if not self.menus:
  223. menu_entries = [
  224. MenuEntryAction("Up","up",self.go_up,None),
  225. ]
  226. menu = MenuWindow(menu_entries,(self.x+msg.new_x,self.y+msg.new_y),root=self)
  227. return
  228. if y < 0: return
  229. offset_x = 0
  230. offset_y = self.scroll_y + self.menubar.height
  231. redraw = []
  232. files_per_row = int(w / 100)
  233. rows_total = math.ceil(len(self.files) / files_per_row)
  234. skip_files = files_per_row * (int(-offset_y / 100))
  235. offset_y += int(-offset_y/100) * 100
  236. hit = False
  237. for f in self.files[skip_files:]:
  238. if offset_y > h: break
  239. if offset_y > -100:
  240. if x >= offset_x and x < offset_x + 100 and y >= offset_y and y < offset_y + 100:
  241. if not f.hilight:
  242. redraw.append(f)
  243. if self.hilighted:
  244. redraw.append(self.hilighted)
  245. self.hilighted.hilight = False
  246. f.hilight = True
  247. self.hilighted = f
  248. hit = True
  249. break
  250. offset_x += 100
  251. if offset_x + 100 > w:
  252. offset_x = 0
  253. offset_y += 100
  254. if not hit:
  255. if self.hilighted:
  256. redraw.append(self.hilighted)
  257. self.hilighted.hilight = False
  258. self.hilighted = None
  259. if self.hilighted:
  260. if msg.command == yutani.MouseEvent.DOWN:
  261. self.hilighted.do_action()
  262. if redraw:
  263. self.redraw_buf(redraw)
  264. self.draw()
  265. def keyboard_event(self, msg):
  266. if msg.event.action != yutani.KeyAction.ACTION_DOWN:
  267. return # Ignore anything that isn't a key down.
  268. if msg.event.key == b"q":
  269. self.close()
  270. sys.exit(0)
  271. if __name__ == '__main__':
  272. yutani.Yutani()
  273. d = yutani.Decor()
  274. window = FileBrowserWindow(d,os.environ.get('HOME','/') if len(sys.argv) < 2 else sys.argv[1])
  275. window.draw()
  276. yutani_mainloop.mainloop()