dialog.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. #!/usr/bin/python3
  2. """
  3. Simple okay/cancel dialog.
  4. """
  5. import os
  6. import re
  7. import stat
  8. import sys
  9. import time
  10. import fnmatch
  11. import cairo
  12. import yutani
  13. import text_region
  14. import toaru_fonts
  15. from button import Button
  16. from icon_cache import get_icon
  17. import yutani_mainloop
  18. _default_text = "This is a dialog. <b>Formatting is available.</b>"
  19. hilight_border_top = (54/255,128/255,205/255)
  20. hilight_gradient_top = (93/255,163/255,236/255)
  21. hilight_gradient_bottom = (56/255,137/255,220/55)
  22. hilight_border_bottom = (47/255,106/255,167/255)
  23. class DialogButtons(object):
  24. OKAY_CANCEL = 1
  25. YES_NO_CANCEL = 2
  26. class DialogWindow(yutani.Window):
  27. base_width = 500
  28. base_height = 150
  29. text_offset = 40
  30. okay_label = "Okay"
  31. cancel_label = "Cancel"
  32. def __init__(self, decorator, title, text, icon='help', buttons=DialogButtons.OKAY_CANCEL, callback=None, cancel_callback=None,window=None,cancel_label=True,close_is_cancel=True):
  33. super(DialogWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=title, icon=icon, doublebuffer=True)
  34. if window:
  35. # Center window
  36. self.move(window.x+int((window.width-self.width)/2),window.y+int((window.height-self.height)/2))
  37. else:
  38. # Center screen
  39. self.move(int((yutani.yutani_ctx._ptr.contents.display_width-self.width)/2),int((yutani.yutani_ctx._ptr.contents.display_height-self.height)/2))
  40. self.decorator = decorator
  41. self.logo = get_icon(icon,48,'help')
  42. self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, 13, 0xFF000000)
  43. self.tr = text_region.TextRegion(0,0,self.base_width-60,self.base_height-self.text_offset,font=self.font)
  44. self.tr.set_richtext(text)
  45. if cancel_label is not True:
  46. self.cancel_label = cancel_label
  47. self.button_ok = Button(self.okay_label,self.ok_click)
  48. self.button_cancel = Button(self.cancel_label,self.cancel_click)
  49. self.buttons = [self.button_ok]
  50. if self.cancel_label:
  51. self.buttons.append(self.button_cancel)
  52. self.close_is_cancel = close_is_cancel
  53. self.hover_widget = None
  54. self.down_button = None
  55. self.callback = callback
  56. self.cancel_callback = cancel_callback
  57. self.draw()
  58. def ok_click(self, button):
  59. self.close()
  60. if self.callback:
  61. self.callback()
  62. return False
  63. def cancel_click(self, button):
  64. self.close()
  65. if self.cancel_callback:
  66. self.cancel_callback()
  67. return False
  68. def draw(self):
  69. surface = self.get_cairo_surface()
  70. WIDTH, HEIGHT = self.width - self.decorator.width(self), self.height - self.decorator.height(self)
  71. ctx = cairo.Context(surface)
  72. ctx.translate(self.decorator.left_width(self), self.decorator.top_height(self))
  73. ctx.rectangle(0,0,WIDTH,HEIGHT)
  74. ctx.set_source_rgb(204/255,204/255,204/255)
  75. ctx.fill()
  76. ctx.set_source_surface(self.logo,30,30)
  77. ctx.paint()
  78. self.tr.resize(WIDTH-90,HEIGHT-self.text_offset)
  79. self.tr.move(self.decorator.left_width(self) + 90,self.decorator.top_height(self)+self.text_offset)
  80. self.tr.draw(self)
  81. self.button_ok.draw(self,ctx,WIDTH-130,HEIGHT-60,100,30)
  82. if self.cancel_label:
  83. self.button_cancel.draw(self,ctx,WIDTH-240,HEIGHT-60,100,30)
  84. self.decorator.render(self)
  85. self.flip()
  86. def finish_resize(self, msg):
  87. """Accept a resize."""
  88. self.resize_accept(msg.width, msg.height)
  89. self.reinit()
  90. self.draw()
  91. self.resize_done()
  92. self.flip()
  93. def mouse_event(self, msg):
  94. decor_event = self.decorator.handle_event(msg)
  95. if decor_event == yutani.Decor.EVENT_CLOSE:
  96. if self.close_is_cancel:
  97. self.cancel_click(None)
  98. else:
  99. self.ok_click(None)
  100. return
  101. elif decor_event == yutani.Decor.EVENT_RIGHT:
  102. self.decorator.show_menu(self, msg)
  103. x,y = msg.new_x - self.decorator.left_width(self), msg.new_y - self.decorator.top_height(self)
  104. w,h = self.width - self.decorator.width(self), self.height - self.decorator.height(self)
  105. redraw = False
  106. if self.down_button:
  107. if msg.command == yutani.MouseEvent.RAISE or msg.command == yutani.MouseEvent.CLICK:
  108. if not (msg.buttons & yutani.MouseButton.BUTTON_LEFT):
  109. if x >= self.down_button.x and \
  110. x < self.down_button.x + self.down_button.width and \
  111. y >= self.down_button.y and \
  112. y < self.down_button.y + self.down_button.height:
  113. self.down_button.focus_enter()
  114. if self.down_button.callback(self.down_button):
  115. redraw = True
  116. self.down_button = None
  117. else:
  118. self.down_button.focus_leave()
  119. self.down_button = None
  120. redraw = True
  121. else:
  122. button = None
  123. for b in self.buttons:
  124. if x >= b.x and x < b.x + b.width and y >= b.y and y < b.y + b.height:
  125. button = b
  126. break
  127. if button != self.hover_widget:
  128. if button:
  129. button.focus_enter()
  130. redraw = True
  131. if self.hover_widget:
  132. self.hover_widget.focus_leave()
  133. redraw = True
  134. self.hover_widget = button
  135. if msg.command == yutani.MouseEvent.DOWN:
  136. if button:
  137. button.hilight = 2
  138. self.down_button = button
  139. redraw = True
  140. if not button:
  141. if self.hover_widget:
  142. self.hover_widget.focus_leave()
  143. redraw = True
  144. self.hover_widget = None
  145. if redraw:
  146. self.draw()
  147. def keyboard_event(self, msg):
  148. if msg.event.action == yutani.KeyAction.ACTION_DOWN:
  149. if msg.event.key == b'\n':
  150. self.ok_click(None)
  151. class File(object):
  152. def __init__(self, path,name=None):
  153. if not name:
  154. self.name = os.path.basename(path)
  155. else:
  156. self.name = name
  157. self.path = os.path.normpath(path)
  158. self.stat = os.stat(path)
  159. self.y = 0
  160. self.x = 0
  161. self.hilight = False
  162. self.tr = text_region.TextRegion(0,0,400,20)
  163. self.tr.set_one_line()
  164. self.tr.set_ellipsis()
  165. self.tr.set_text(self.name)
  166. def do_action(self, dialog):
  167. if self.is_directory:
  168. dialog.load_directory(self.path)
  169. dialog.redraw_buf()
  170. return True
  171. else:
  172. dialog.path = self.path
  173. dialog.ok_click(None)
  174. return False
  175. @property
  176. def is_directory(self):
  177. return stat.S_ISDIR(self.stat.st_mode)
  178. @property
  179. def is_executable(self):
  180. return stat.S_IXUSR & self.stat.st_mode and not self.is_directory
  181. @property
  182. def icon(self):
  183. if self.is_directory: return get_icon('folder',16)
  184. if self.is_executable: return get_icon('applications-generic',16)
  185. return get_icon('file',16) # Need file icon
  186. @property
  187. def sortkey(self):
  188. if self.is_directory: return "___" + self.name
  189. else: return "zzz" + self.name
  190. class OpenFileDialog(DialogWindow):
  191. base_width = 500
  192. base_height = 450
  193. okay_label = "Open"
  194. buf = None
  195. path = None
  196. icon_width = 16
  197. unit_height = 20
  198. def __init__(self, decorator, title, glob=None, callback=None, cancel_callback=None,window=None):
  199. self.buf = None
  200. self.path = None
  201. if glob:
  202. self.matcher = re.compile(fnmatch.translate(glob))
  203. else:
  204. self.matcher = None
  205. self.tr = None
  206. self.load_directory(os.getcwd())
  207. self.redraw_buf()
  208. self.hilighted = None
  209. super(OpenFileDialog, self).__init__(decorator,title,"Open...",icon="open",callback=callback,cancel_callback=cancel_callback,window=window)
  210. self.tr.set_text(self.directory)
  211. def ok_click(self, button):
  212. self.close()
  213. if self.callback:
  214. self.callback(self.path)
  215. return False
  216. def load_directory(self, directory):
  217. self.directory = os.path.normpath(directory)
  218. if self.tr:
  219. self.tr.set_text(self.directory)
  220. self.files = sorted([File(os.path.join(self.directory,f)) for f in os.listdir(self.directory)],key=lambda x: x.sortkey)
  221. if self.matcher:
  222. self.files = [x for x in self.files if x.is_directory or self.matcher.match(x.name)]
  223. if directory != '/':
  224. self.files.insert(0,File(os.path.join(self.directory,'..'),'(Go up)'))
  225. self.scroll_y = 0
  226. def redraw_buf(self,clips=None):
  227. if self.buf:
  228. self.buf.destroy()
  229. w = 450
  230. height = self.unit_height
  231. self.buf = yutani.GraphicsBuffer(w,len(self.files)*height)
  232. surface = self.buf.get_cairo_surface()
  233. ctx = cairo.Context(surface)
  234. if clips:
  235. for clip in clips:
  236. ctx.rectangle(clip.x,clip.y,w,height)
  237. ctx.clip()
  238. ctx.rectangle(0,0,surface.get_width(),surface.get_height())
  239. ctx.set_source_rgb(1,1,1)
  240. ctx.fill()
  241. offset_y = 0
  242. i = 0
  243. for f in self.files:
  244. f.y = offset_y
  245. if not clips or f in clips:
  246. tr = f.tr
  247. tr.move(26,offset_y+2)
  248. if f.hilight:
  249. gradient = cairo.LinearGradient(0,0,0,height-2)
  250. gradient.add_color_stop_rgba(0.0,*hilight_gradient_top,1.0)
  251. gradient.add_color_stop_rgba(1.0,*hilight_gradient_bottom,1.0)
  252. ctx.rectangle(0,offset_y,w,1)
  253. ctx.set_source_rgb(*hilight_border_top)
  254. ctx.fill()
  255. ctx.rectangle(0,offset_y+height-1,w,1)
  256. ctx.set_source_rgb(*hilight_border_bottom)
  257. ctx.fill()
  258. ctx.save()
  259. ctx.translate(0,offset_y+1)
  260. ctx.rectangle(0,0,w,height-2)
  261. ctx.set_source(gradient)
  262. ctx.fill()
  263. ctx.restore()
  264. tr.font.font_color = 0xFFFFFFFF
  265. else:
  266. ctx.rectangle(0,offset_y,w,height)
  267. if i % 2:
  268. ctx.set_source_rgb(0.9,0.9,0.9)
  269. else:
  270. ctx.set_source_rgb(1,1,1)
  271. ctx.fill()
  272. tr.font.font_color = 0xFF000000
  273. ctx.set_source_surface(f.icon,4,offset_y+2)
  274. ctx.paint()
  275. tr.draw(self.buf)
  276. offset_y += height
  277. i += 1
  278. def draw(self):
  279. surface = self.get_cairo_surface()
  280. WIDTH, HEIGHT = self.width - self.decorator.width(self), self.height - self.decorator.height(self)
  281. ctx = cairo.Context(surface)
  282. ctx.translate(self.decorator.left_width(self), self.decorator.top_height(self))
  283. ctx.rectangle(0,0,WIDTH,HEIGHT)
  284. ctx.set_source_rgb(204/255,204/255,204/255)
  285. ctx.fill()
  286. #ctx.set_source_surface(self.logo,30,30)
  287. #ctx.paint()
  288. self.tr.resize(WIDTH,30)
  289. self.tr.move(self.decorator.left_width(self),self.decorator.top_height(self)+10)
  290. self.tr.set_alignment(2)
  291. self.tr.draw(self)
  292. ctx.save()
  293. ctx.translate(20, 40)
  294. ctx.rectangle(0,0,450,HEIGHT-130)
  295. ctx.set_line_width(2)
  296. ctx.set_source_rgb(0.7,0.7,0.7)
  297. ctx.stroke_preserve()
  298. ctx.set_source_rgb(1,1,1)
  299. ctx.fill()
  300. ctx.rectangle(0,0,450,HEIGHT-130)
  301. ctx.clip()
  302. text = self.buf.get_cairo_surface()
  303. ctx.set_source_surface(text,0,self.scroll_y)
  304. ctx.paint()
  305. ctx.restore()
  306. self.button_cancel.draw(self,ctx,WIDTH-130,HEIGHT-60,100,30)
  307. self.button_ok.draw(self,ctx,WIDTH-240,HEIGHT-60,100,30)
  308. self.decorator.render(self)
  309. self.flip()
  310. def scroll(self, amount):
  311. w,h = self.width - self.decorator.width(self), self.height - self.decorator.height(self)
  312. self.scroll_y += amount
  313. if self.scroll_y > 0:
  314. self.scroll_y = 0
  315. top = min(-(self.buf.height - (h-130)),0)
  316. if self.scroll_y < top:
  317. self.scroll_y = top
  318. def mouse_event(self, msg):
  319. super(OpenFileDialog,self).mouse_event(msg)
  320. x,y = msg.new_x - self.decorator.left_width(self), msg.new_y - self.decorator.top_height(self)
  321. w,h = self.width - self.decorator.width(self), self.height - self.decorator.height(self)
  322. if y < 0: return
  323. if x < 0 or x >= w: return
  324. if msg.buttons & yutani.MouseButton.SCROLL_UP:
  325. self.scroll(30)
  326. self.draw()
  327. return
  328. elif msg.buttons & yutani.MouseButton.SCROLL_DOWN:
  329. self.scroll(-30)
  330. self.draw()
  331. return
  332. offset_y = self.scroll_y + 40
  333. redraw = []
  334. hit = False
  335. if x >= 20 and x < 450+20:
  336. for f in self.files:
  337. if offset_y > h: break
  338. if y >= offset_y and y < offset_y + self.unit_height:
  339. if not f.hilight:
  340. redraw.append(f)
  341. if self.hilighted:
  342. redraw.append(self.hilighted)
  343. self.hilighted.hilight = False
  344. f.hilight = True
  345. self.hilighted = f
  346. hit = True
  347. break
  348. offset_y += self.unit_height
  349. if not hit:
  350. if self.hilighted:
  351. redraw.append(self.hilighted)
  352. self.hilighted.hilight = False
  353. self.hilighted = None
  354. if self.hilighted:
  355. if msg.command == yutani.MouseEvent.DOWN:
  356. if self.hilighted.do_action(self):
  357. redraw = []
  358. self.redraw_buf()
  359. self.draw()
  360. if redraw:
  361. self.redraw_buf(redraw)
  362. self.draw()
  363. if __name__ == '__main__':
  364. yutani.Yutani()
  365. d = yutani.Decor()
  366. def okay(path):
  367. print("You hit Okay!",path)
  368. sys.exit(0)
  369. def cancel():
  370. print("You hit Cancel!")
  371. sys.exit(0)
  372. #window = DialogWindow(d,"Okay/Cancel Dialog","A thing happend!",cancel_callback=cancel,callback=okay)
  373. window = OpenFileDialog(d,"Open...",glob="*.png",cancel_callback=cancel,callback=okay)
  374. yutani_mainloop.mainloop()