Coverage for tree.py: 15%

359 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 13:22 -0700

1# XXX TO DO: 

2# - popup menu 

3# - support partial or total redisplay 

4# - key bindings (instead of quick-n-dirty bindings on Canvas): 

5# - up/down arrow keys to move focus around 

6# - ditto for page up/down, home/end 

7# - left/right arrows to expand/collapse & move out/in 

8# - more doc strings 

9# - add icons for "file", "module", "class", "method"; better "python" icon 

10# - callback for selection??? 

11# - multiple-item selection 

12# - tooltips 

13# - redo geometry without magic numbers 

14# - keep track of object ids to allow more careful cleaning 

15# - optimize tree redraw after expand of subnode 

16 

17import os 

18 

19from tkinter import * 

20from tkinter.ttk import Frame, Scrollbar 

21 

22from idlelib.config import idleConf 

23from idlelib import zoomheight 

24 

25ICONDIR = "Icons" 

26 

27# Look for Icons subdirectory in the same directory as this module 

28try: 

29 _icondir = os.path.join(os.path.dirname(__file__), ICONDIR) 

30except NameError: 

31 _icondir = ICONDIR 

32if os.path.isdir(_icondir): 32 ↛ 34line 32 didn't jump to line 34, because the condition on line 32 was never false

33 ICONDIR = _icondir 

34elif not os.path.isdir(ICONDIR): 

35 raise RuntimeError("can't find icon directory (%r)" % (ICONDIR,)) 

36 

37def listicons(icondir=ICONDIR): 

38 """Utility to display the available icons.""" 

39 root = Tk() 

40 import glob 

41 list = glob.glob(os.path.join(glob.escape(icondir), "*.gif")) 

42 list.sort() 

43 images = [] 

44 row = column = 0 

45 for file in list: 

46 name = os.path.splitext(os.path.basename(file))[0] 

47 image = PhotoImage(file=file, master=root) 

48 images.append(image) 

49 label = Label(root, image=image, bd=1, relief="raised") 

50 label.grid(row=row, column=column) 

51 label = Label(root, text=name) 

52 label.grid(row=row+1, column=column) 

53 column = column + 1 

54 if column >= 10: 

55 row = row+2 

56 column = 0 

57 root.images = images 

58 

59def wheel_event(event, widget=None): 

60 """Handle scrollwheel event. 

61 

62 For wheel up, event.delta = 120*n on Windows, -1*n on darwin, 

63 where n can be > 1 if one scrolls fast. Flicking the wheel 

64 generates up to maybe 20 events with n up to 10 or more 1. 

65 Macs use wheel down (delta = 1*n) to scroll up, so positive 

66 delta means to scroll up on both systems. 

67 

68 X-11 sends Control-Button-4,5 events instead. 

69 

70 The widget parameter is needed so browser label bindings can pass 

71 the underlying canvas. 

72 

73 This function depends on widget.yview to not be overridden by 

74 a subclass. 

75 """ 

76 up = {EventType.MouseWheel: event.delta > 0, 

77 EventType.ButtonPress: event.num == 4} 

78 lines = -5 if up[event.type] else 5 

79 widget = event.widget if widget is None else widget 

80 widget.yview(SCROLL, lines, 'units') 

81 return 'break' 

82 

83 

84class TreeNode: 

85 

86 def __init__(self, canvas, parent, item): 

87 self.canvas = canvas 

88 self.parent = parent 

89 self.item = item 

90 self.state = 'collapsed' 

91 self.selected = False 

92 self.children = [] 

93 self.x = self.y = None 

94 self.iconimages = {} # cache of PhotoImage instances for icons 

95 

96 def destroy(self): 

97 for c in self.children[:]: 

98 self.children.remove(c) 

99 c.destroy() 

100 self.parent = None 

101 

102 def geticonimage(self, name): 

103 try: 

104 return self.iconimages[name] 

105 except KeyError: 

106 pass 

107 file, ext = os.path.splitext(name) 

108 ext = ext or ".gif" 

109 fullname = os.path.join(ICONDIR, file + ext) 

110 image = PhotoImage(master=self.canvas, file=fullname) 

111 self.iconimages[name] = image 

112 return image 

113 

114 def select(self, event=None): 

115 if self.selected: 

116 return 

117 self.deselectall() 

118 self.selected = True 

119 self.canvas.delete(self.image_id) 

120 self.drawicon() 

121 self.drawtext() 

122 

123 def deselect(self, event=None): 

124 if not self.selected: 

125 return 

126 self.selected = False 

127 self.canvas.delete(self.image_id) 

128 self.drawicon() 

129 self.drawtext() 

130 

131 def deselectall(self): 

132 if self.parent: 

133 self.parent.deselectall() 

134 else: 

135 self.deselecttree() 

136 

137 def deselecttree(self): 

138 if self.selected: 

139 self.deselect() 

140 for child in self.children: 

141 child.deselecttree() 

142 

143 def flip(self, event=None): 

144 if self.state == 'expanded': 

145 self.collapse() 

146 else: 

147 self.expand() 

148 self.item.OnDoubleClick() 

149 return "break" 

150 

151 def expand(self, event=None): 

152 if not self.item._IsExpandable(): 

153 return 

154 if self.state != 'expanded': 

155 self.state = 'expanded' 

156 self.update() 

157 self.view() 

158 

159 def collapse(self, event=None): 

160 if self.state != 'collapsed': 

161 self.state = 'collapsed' 

162 self.update() 

163 

164 def view(self): 

165 top = self.y - 2 

166 bottom = self.lastvisiblechild().y + 17 

167 height = bottom - top 

168 visible_top = self.canvas.canvasy(0) 

169 visible_height = self.canvas.winfo_height() 

170 visible_bottom = self.canvas.canvasy(visible_height) 

171 if visible_top <= top and bottom <= visible_bottom: 

172 return 

173 x0, y0, x1, y1 = self.canvas._getints(self.canvas['scrollregion']) 

174 if top >= visible_top and height <= visible_height: 

175 fraction = top + height - visible_height 

176 else: 

177 fraction = top 

178 fraction = float(fraction) / y1 

179 self.canvas.yview_moveto(fraction) 

180 

181 def lastvisiblechild(self): 

182 if self.children and self.state == 'expanded': 

183 return self.children[-1].lastvisiblechild() 

184 else: 

185 return self 

186 

187 def update(self): 

188 if self.parent: 

189 self.parent.update() 

190 else: 

191 oldcursor = self.canvas['cursor'] 

192 self.canvas['cursor'] = "watch" 

193 self.canvas.update() 

194 self.canvas.delete(ALL) # XXX could be more subtle 

195 self.draw(7, 2) 

196 x0, y0, x1, y1 = self.canvas.bbox(ALL) 

197 self.canvas.configure(scrollregion=(0, 0, x1, y1)) 

198 self.canvas['cursor'] = oldcursor 

199 

200 def draw(self, x, y): 

201 # XXX This hard-codes too many geometry constants! 

202 dy = 20 

203 self.x, self.y = x, y 

204 self.drawicon() 

205 self.drawtext() 

206 if self.state != 'expanded': 

207 return y + dy 

208 # draw children 

209 if not self.children: 

210 sublist = self.item._GetSubList() 

211 if not sublist: 

212 # _IsExpandable() was mistaken; that's allowed 

213 return y+17 

214 for item in sublist: 

215 child = self.__class__(self.canvas, self, item) 

216 self.children.append(child) 

217 cx = x+20 

218 cy = y + dy 

219 cylast = 0 

220 for child in self.children: 

221 cylast = cy 

222 self.canvas.create_line(x+9, cy+7, cx, cy+7, fill="gray50") 

223 cy = child.draw(cx, cy) 

224 if child.item._IsExpandable(): 

225 if child.state == 'expanded': 

226 iconname = "minusnode" 

227 callback = child.collapse 

228 else: 

229 iconname = "plusnode" 

230 callback = child.expand 

231 image = self.geticonimage(iconname) 

232 id = self.canvas.create_image(x+9, cylast+7, image=image) 

233 # XXX This leaks bindings until canvas is deleted: 

234 self.canvas.tag_bind(id, "<1>", callback) 

235 self.canvas.tag_bind(id, "<Double-1>", lambda x: None) 

236 id = self.canvas.create_line(x+9, y+10, x+9, cylast+7, 

237 ##stipple="gray50", # XXX Seems broken in Tk 8.0.x 

238 fill="gray50") 

239 self.canvas.tag_lower(id) # XXX .lower(id) before Python 1.5.2 

240 return cy 

241 

242 def drawicon(self): 

243 if self.selected: 

244 imagename = (self.item.GetSelectedIconName() or 

245 self.item.GetIconName() or 

246 "openfolder") 

247 else: 

248 imagename = self.item.GetIconName() or "folder" 

249 image = self.geticonimage(imagename) 

250 id = self.canvas.create_image(self.x, self.y, anchor="nw", image=image) 

251 self.image_id = id 

252 self.canvas.tag_bind(id, "<1>", self.select) 

253 self.canvas.tag_bind(id, "<Double-1>", self.flip) 

254 

255 def drawtext(self): 

256 textx = self.x+20-1 

257 texty = self.y-4 

258 labeltext = self.item.GetLabelText() 

259 if labeltext: 

260 id = self.canvas.create_text(textx, texty, anchor="nw", 

261 text=labeltext) 

262 self.canvas.tag_bind(id, "<1>", self.select) 

263 self.canvas.tag_bind(id, "<Double-1>", self.flip) 

264 x0, y0, x1, y1 = self.canvas.bbox(id) 

265 textx = max(x1, 200) + 10 

266 text = self.item.GetText() or "<no text>" 

267 try: 

268 self.entry 

269 except AttributeError: 

270 pass 

271 else: 

272 self.edit_finish() 

273 try: 

274 self.label 

275 except AttributeError: 

276 # padding carefully selected (on Windows) to match Entry widget: 

277 self.label = Label(self.canvas, text=text, bd=0, padx=2, pady=2) 

278 theme = idleConf.CurrentTheme() 

279 if self.selected: 

280 self.label.configure(idleConf.GetHighlight(theme, 'hilite')) 

281 else: 

282 self.label.configure(idleConf.GetHighlight(theme, 'normal')) 

283 id = self.canvas.create_window(textx, texty, 

284 anchor="nw", window=self.label) 

285 self.label.bind("<1>", self.select_or_edit) 

286 self.label.bind("<Double-1>", self.flip) 

287 self.label.bind("<MouseWheel>", lambda e: wheel_event(e, self.canvas)) 

288 self.label.bind("<Button-4>", lambda e: wheel_event(e, self.canvas)) 

289 self.label.bind("<Button-5>", lambda e: wheel_event(e, self.canvas)) 

290 self.text_id = id 

291 

292 def select_or_edit(self, event=None): 

293 if self.selected and self.item.IsEditable(): 

294 self.edit(event) 

295 else: 

296 self.select(event) 

297 

298 def edit(self, event=None): 

299 self.entry = Entry(self.label, bd=0, highlightthickness=1, width=0) 

300 self.entry.insert(0, self.label['text']) 

301 self.entry.selection_range(0, END) 

302 self.entry.pack(ipadx=5) 

303 self.entry.focus_set() 

304 self.entry.bind("<Return>", self.edit_finish) 

305 self.entry.bind("<Escape>", self.edit_cancel) 

306 

307 def edit_finish(self, event=None): 

308 try: 

309 entry = self.entry 

310 del self.entry 

311 except AttributeError: 

312 return 

313 text = entry.get() 

314 entry.destroy() 

315 if text and text != self.item.GetText(): 

316 self.item.SetText(text) 

317 text = self.item.GetText() 

318 self.label['text'] = text 

319 self.drawtext() 

320 self.canvas.focus_set() 

321 

322 def edit_cancel(self, event=None): 

323 try: 

324 entry = self.entry 

325 del self.entry 

326 except AttributeError: 

327 return 

328 entry.destroy() 

329 self.drawtext() 

330 self.canvas.focus_set() 

331 

332 

333class TreeItem: 

334 

335 """Abstract class representing tree items. 

336 

337 Methods should typically be overridden, otherwise a default action 

338 is used. 

339 

340 """ 

341 

342 def __init__(self): 1ab

343 """Constructor. Do whatever you need to do.""" 

344 

345 def GetText(self): 

346 """Return text string to display.""" 

347 

348 def GetLabelText(self): 

349 """Return label text string to display in front of text (if any).""" 

350 

351 expandable = None 

352 

353 def _IsExpandable(self): 

354 """Do not override! Called by TreeNode.""" 

355 if self.expandable is None: 

356 self.expandable = self.IsExpandable() 

357 return self.expandable 

358 

359 def IsExpandable(self): 

360 """Return whether there are subitems.""" 

361 return 1 

362 

363 def _GetSubList(self): 

364 """Do not override! Called by TreeNode.""" 

365 if not self.IsExpandable(): 

366 return [] 

367 sublist = self.GetSubList() 

368 if not sublist: 

369 self.expandable = 0 

370 return sublist 

371 

372 def IsEditable(self): 

373 """Return whether the item's text may be edited.""" 

374 

375 def SetText(self, text): 

376 """Change the item's text (if it is editable).""" 

377 

378 def GetIconName(self): 

379 """Return name of icon to be displayed normally.""" 

380 

381 def GetSelectedIconName(self): 

382 """Return name of icon to be displayed when selected.""" 

383 

384 def GetSubList(self): 

385 """Return list of items forming sublist.""" 

386 

387 def OnDoubleClick(self): 

388 """Called on a double-click on the item.""" 

389 

390 

391# Example application 

392 

393class FileTreeItem(TreeItem): 

394 

395 """Example TreeItem subclass -- browse the file system.""" 

396 

397 def __init__(self, path): 

398 self.path = path 

399 

400 def GetText(self): 

401 return os.path.basename(self.path) or self.path 

402 

403 def IsEditable(self): 

404 return os.path.basename(self.path) != "" 

405 

406 def SetText(self, text): 

407 newpath = os.path.dirname(self.path) 

408 newpath = os.path.join(newpath, text) 

409 if os.path.dirname(newpath) != os.path.dirname(self.path): 

410 return 

411 try: 

412 os.rename(self.path, newpath) 

413 self.path = newpath 

414 except OSError: 

415 pass 

416 

417 def GetIconName(self): 

418 if not self.IsExpandable(): 

419 return "python" # XXX wish there was a "file" icon 

420 

421 def IsExpandable(self): 

422 return os.path.isdir(self.path) 

423 

424 def GetSubList(self): 

425 try: 

426 names = os.listdir(self.path) 

427 except OSError: 

428 return [] 

429 names.sort(key = os.path.normcase) 

430 sublist = [] 

431 for name in names: 

432 item = FileTreeItem(os.path.join(self.path, name)) 

433 sublist.append(item) 

434 return sublist 

435 

436 

437# A canvas widget with scroll bars and some useful bindings 

438 

439class ScrolledCanvas: 

440 

441 def __init__(self, master, **opts): 

442 if 'yscrollincrement' not in opts: 

443 opts['yscrollincrement'] = 17 

444 self.master = master 

445 self.frame = Frame(master) 

446 self.frame.rowconfigure(0, weight=1) 

447 self.frame.columnconfigure(0, weight=1) 

448 self.canvas = Canvas(self.frame, **opts) 

449 self.canvas.grid(row=0, column=0, sticky="nsew") 

450 self.vbar = Scrollbar(self.frame, name="vbar") 

451 self.vbar.grid(row=0, column=1, sticky="nse") 

452 self.hbar = Scrollbar(self.frame, name="hbar", orient="horizontal") 

453 self.hbar.grid(row=1, column=0, sticky="ews") 

454 self.canvas['yscrollcommand'] = self.vbar.set 

455 self.vbar['command'] = self.canvas.yview 

456 self.canvas['xscrollcommand'] = self.hbar.set 

457 self.hbar['command'] = self.canvas.xview 

458 self.canvas.bind("<Key-Prior>", self.page_up) 

459 self.canvas.bind("<Key-Next>", self.page_down) 

460 self.canvas.bind("<Key-Up>", self.unit_up) 

461 self.canvas.bind("<Key-Down>", self.unit_down) 

462 self.canvas.bind("<MouseWheel>", wheel_event) 

463 self.canvas.bind("<Button-4>", wheel_event) 

464 self.canvas.bind("<Button-5>", wheel_event) 

465 #if isinstance(master, Toplevel) or isinstance(master, Tk): 

466 self.canvas.bind("<Alt-Key-2>", self.zoom_height) 

467 self.canvas.focus_set() 

468 def page_up(self, event): 

469 self.canvas.yview_scroll(-1, "page") 

470 return "break" 

471 def page_down(self, event): 

472 self.canvas.yview_scroll(1, "page") 

473 return "break" 

474 def unit_up(self, event): 

475 self.canvas.yview_scroll(-1, "unit") 

476 return "break" 

477 def unit_down(self, event): 

478 self.canvas.yview_scroll(1, "unit") 

479 return "break" 

480 def zoom_height(self, event): 

481 zoomheight.zoom_height(self.master) 

482 return "break" 

483 

484 

485def _tree_widget(parent): # htest # 

486 top = Toplevel(parent) 

487 x, y = map(int, parent.geometry().split('+')[1:]) 

488 top.geometry("+%d+%d" % (x+50, y+175)) 

489 sc = ScrolledCanvas(top, bg="white", highlightthickness=0, takefocus=1) 

490 sc.frame.pack(expand=1, fill="both", side=LEFT) 

491 item = FileTreeItem(ICONDIR) 

492 node = TreeNode(sc.canvas, None, item) 

493 node.expand() 

494 

495if __name__ == '__main__': 495 ↛ 496line 495 didn't jump to line 496, because the condition on line 495 was never true

496 from unittest import main 

497 main('idlelib.idle_test.test_tree', verbosity=2, exit=False) 

498 

499 from idlelib.idle_test.htest import run 

500 run(_tree_widget)