# Python code coverage for Tools/demo/ss1.py

# | count | content |
---|---|---|

1 | n/a | #!/usr/bin/env python3 |

2 | n/a | |

3 | n/a | """ |

4 | n/a | SS1 -- a spreadsheet-like application. |

5 | n/a | """ |

6 | n/a | |

7 | n/a | import os |

8 | n/a | import re |

9 | n/a | import sys |

10 | n/a | from xml.parsers import expat |

11 | n/a | from xml.sax.saxutils import escape |

12 | n/a | |

13 | n/a | LEFT, CENTER, RIGHT = "LEFT", "CENTER", "RIGHT" |

14 | n/a | |

15 | n/a | def ljust(x, n): |

16 | n/a | return x.ljust(n) |

17 | n/a | def center(x, n): |

18 | n/a | return x.center(n) |

19 | n/a | def rjust(x, n): |

20 | n/a | return x.rjust(n) |

21 | n/a | align2action = {LEFT: ljust, CENTER: center, RIGHT: rjust} |

22 | n/a | |

23 | n/a | align2xml = {LEFT: "left", CENTER: "center", RIGHT: "right"} |

24 | n/a | xml2align = {"left": LEFT, "center": CENTER, "right": RIGHT} |

25 | n/a | |

26 | n/a | align2anchor = {LEFT: "w", CENTER: "center", RIGHT: "e"} |

27 | n/a | |

28 | n/a | def sum(seq): |

29 | n/a | total = 0 |

30 | n/a | for x in seq: |

31 | n/a | if x is not None: |

32 | n/a | total += x |

33 | n/a | return total |

34 | n/a | |

35 | n/a | class Sheet: |

36 | n/a | |

37 | n/a | def __init__(self): |

38 | n/a | self.cells = {} # {(x, y): cell, ...} |

39 | n/a | self.ns = dict( |

40 | n/a | cell = self.cellvalue, |

41 | n/a | cells = self.multicellvalue, |

42 | n/a | sum = sum, |

43 | n/a | ) |

44 | n/a | |

45 | n/a | def cellvalue(self, x, y): |

46 | n/a | cell = self.getcell(x, y) |

47 | n/a | if hasattr(cell, 'recalc'): |

48 | n/a | return cell.recalc(self.ns) |

49 | n/a | else: |

50 | n/a | return cell |

51 | n/a | |

52 | n/a | def multicellvalue(self, x1, y1, x2, y2): |

53 | n/a | if x1 > x2: |

54 | n/a | x1, x2 = x2, x1 |

55 | n/a | if y1 > y2: |

56 | n/a | y1, y2 = y2, y1 |

57 | n/a | seq = [] |

58 | n/a | for y in range(y1, y2+1): |

59 | n/a | for x in range(x1, x2+1): |

60 | n/a | seq.append(self.cellvalue(x, y)) |

61 | n/a | return seq |

62 | n/a | |

63 | n/a | def getcell(self, x, y): |

64 | n/a | return self.cells.get((x, y)) |

65 | n/a | |

66 | n/a | def setcell(self, x, y, cell): |

67 | n/a | assert x > 0 and y > 0 |

68 | n/a | assert isinstance(cell, BaseCell) |

69 | n/a | self.cells[x, y] = cell |

70 | n/a | |

71 | n/a | def clearcell(self, x, y): |

72 | n/a | try: |

73 | n/a | del self.cells[x, y] |

74 | n/a | except KeyError: |

75 | n/a | pass |

76 | n/a | |

77 | n/a | def clearcells(self, x1, y1, x2, y2): |

78 | n/a | for xy in self.selectcells(x1, y1, x2, y2): |

79 | n/a | del self.cells[xy] |

80 | n/a | |

81 | n/a | def clearrows(self, y1, y2): |

82 | n/a | self.clearcells(0, y1, sys.maxsize, y2) |

83 | n/a | |

84 | n/a | def clearcolumns(self, x1, x2): |

85 | n/a | self.clearcells(x1, 0, x2, sys.maxsize) |

86 | n/a | |

87 | n/a | def selectcells(self, x1, y1, x2, y2): |

88 | n/a | if x1 > x2: |

89 | n/a | x1, x2 = x2, x1 |

90 | n/a | if y1 > y2: |

91 | n/a | y1, y2 = y2, y1 |

92 | n/a | return [(x, y) for x, y in self.cells |

93 | n/a | if x1 <= x <= x2 and y1 <= y <= y2] |

94 | n/a | |

95 | n/a | def movecells(self, x1, y1, x2, y2, dx, dy): |

96 | n/a | if dx == 0 and dy == 0: |

97 | n/a | return |

98 | n/a | if x1 > x2: |

99 | n/a | x1, x2 = x2, x1 |

100 | n/a | if y1 > y2: |

101 | n/a | y1, y2 = y2, y1 |

102 | n/a | assert x1+dx > 0 and y1+dy > 0 |

103 | n/a | new = {} |

104 | n/a | for x, y in self.cells: |

105 | n/a | cell = self.cells[x, y] |

106 | n/a | if hasattr(cell, 'renumber'): |

107 | n/a | cell = cell.renumber(x1, y1, x2, y2, dx, dy) |

108 | n/a | if x1 <= x <= x2 and y1 <= y <= y2: |

109 | n/a | x += dx |

110 | n/a | y += dy |

111 | n/a | new[x, y] = cell |

112 | n/a | self.cells = new |

113 | n/a | |

114 | n/a | def insertrows(self, y, n): |

115 | n/a | assert n > 0 |

116 | n/a | self.movecells(0, y, sys.maxsize, sys.maxsize, 0, n) |

117 | n/a | |

118 | n/a | def deleterows(self, y1, y2): |

119 | n/a | if y1 > y2: |

120 | n/a | y1, y2 = y2, y1 |

121 | n/a | self.clearrows(y1, y2) |

122 | n/a | self.movecells(0, y2+1, sys.maxsize, sys.maxsize, 0, y1-y2-1) |

123 | n/a | |

124 | n/a | def insertcolumns(self, x, n): |

125 | n/a | assert n > 0 |

126 | n/a | self.movecells(x, 0, sys.maxsize, sys.maxsize, n, 0) |

127 | n/a | |

128 | n/a | def deletecolumns(self, x1, x2): |

129 | n/a | if x1 > x2: |

130 | n/a | x1, x2 = x2, x1 |

131 | n/a | self.clearcells(x1, x2) |

132 | n/a | self.movecells(x2+1, 0, sys.maxsize, sys.maxsize, x1-x2-1, 0) |

133 | n/a | |

134 | n/a | def getsize(self): |

135 | n/a | maxx = maxy = 0 |

136 | n/a | for x, y in self.cells: |

137 | n/a | maxx = max(maxx, x) |

138 | n/a | maxy = max(maxy, y) |

139 | n/a | return maxx, maxy |

140 | n/a | |

141 | n/a | def reset(self): |

142 | n/a | for cell in self.cells.values(): |

143 | n/a | if hasattr(cell, 'reset'): |

144 | n/a | cell.reset() |

145 | n/a | |

146 | n/a | def recalc(self): |

147 | n/a | self.reset() |

148 | n/a | for cell in self.cells.values(): |

149 | n/a | if hasattr(cell, 'recalc'): |

150 | n/a | cell.recalc(self.ns) |

151 | n/a | |

152 | n/a | def display(self): |

153 | n/a | maxx, maxy = self.getsize() |

154 | n/a | width, height = maxx+1, maxy+1 |

155 | n/a | colwidth = [1] * width |

156 | n/a | full = {} |

157 | n/a | # Add column heading labels in row 0 |

158 | n/a | for x in range(1, width): |

159 | n/a | full[x, 0] = text, alignment = colnum2name(x), RIGHT |

160 | n/a | colwidth[x] = max(colwidth[x], len(text)) |

161 | n/a | # Add row labels in column 0 |

162 | n/a | for y in range(1, height): |

163 | n/a | full[0, y] = text, alignment = str(y), RIGHT |

164 | n/a | colwidth[0] = max(colwidth[0], len(text)) |

165 | n/a | # Add sheet cells in columns with x>0 and y>0 |

166 | n/a | for (x, y), cell in self.cells.items(): |

167 | n/a | if x <= 0 or y <= 0: |

168 | n/a | continue |

169 | n/a | if hasattr(cell, 'recalc'): |

170 | n/a | cell.recalc(self.ns) |

171 | n/a | if hasattr(cell, 'format'): |

172 | n/a | text, alignment = cell.format() |

173 | n/a | assert isinstance(text, str) |

174 | n/a | assert alignment in (LEFT, CENTER, RIGHT) |

175 | n/a | else: |

176 | n/a | text = str(cell) |

177 | n/a | if isinstance(cell, str): |

178 | n/a | alignment = LEFT |

179 | n/a | else: |

180 | n/a | alignment = RIGHT |

181 | n/a | full[x, y] = (text, alignment) |

182 | n/a | colwidth[x] = max(colwidth[x], len(text)) |

183 | n/a | # Calculate the horizontal separator line (dashes and dots) |

184 | n/a | sep = "" |

185 | n/a | for x in range(width): |

186 | n/a | if sep: |

187 | n/a | sep += "+" |

188 | n/a | sep += "-"*colwidth[x] |

189 | n/a | # Now print The full grid |

190 | n/a | for y in range(height): |

191 | n/a | line = "" |

192 | n/a | for x in range(width): |

193 | n/a | text, alignment = full.get((x, y)) or ("", LEFT) |

194 | n/a | text = align2action[alignment](text, colwidth[x]) |

195 | n/a | if line: |

196 | n/a | line += '|' |

197 | n/a | line += text |

198 | n/a | print(line) |

199 | n/a | if y == 0: |

200 | n/a | print(sep) |

201 | n/a | |

202 | n/a | def xml(self): |

203 | n/a | out = ['<spreadsheet>'] |

204 | n/a | for (x, y), cell in self.cells.items(): |

205 | n/a | if hasattr(cell, 'xml'): |

206 | n/a | cellxml = cell.xml() |

207 | n/a | else: |

208 | n/a | cellxml = '<value>%s</value>' % escape(cell) |

209 | n/a | out.append('<cell row="%s" col="%s">\n %s\n</cell>' % |

210 | n/a | (y, x, cellxml)) |

211 | n/a | out.append('</spreadsheet>') |

212 | n/a | return '\n'.join(out) |

213 | n/a | |

214 | n/a | def save(self, filename): |

215 | n/a | text = self.xml() |

216 | n/a | with open(filename, "w", encoding='utf-8') as f: |

217 | n/a | f.write(text) |

218 | n/a | if text and not text.endswith('\n'): |

219 | n/a | f.write('\n') |

220 | n/a | |

221 | n/a | def load(self, filename): |

222 | n/a | with open(filename, 'rb') as f: |

223 | n/a | SheetParser(self).parsefile(f) |

224 | n/a | |

225 | n/a | class SheetParser: |

226 | n/a | |

227 | n/a | def __init__(self, sheet): |

228 | n/a | self.sheet = sheet |

229 | n/a | |

230 | n/a | def parsefile(self, f): |

231 | n/a | parser = expat.ParserCreate() |

232 | n/a | parser.StartElementHandler = self.startelement |

233 | n/a | parser.EndElementHandler = self.endelement |

234 | n/a | parser.CharacterDataHandler = self.data |

235 | n/a | parser.ParseFile(f) |

236 | n/a | |

237 | n/a | def startelement(self, tag, attrs): |

238 | n/a | method = getattr(self, 'start_'+tag, None) |

239 | n/a | if method: |

240 | n/a | method(attrs) |

241 | n/a | self.texts = [] |

242 | n/a | |

243 | n/a | def data(self, text): |

244 | n/a | self.texts.append(text) |

245 | n/a | |

246 | n/a | def endelement(self, tag): |

247 | n/a | method = getattr(self, 'end_'+tag, None) |

248 | n/a | if method: |

249 | n/a | method("".join(self.texts)) |

250 | n/a | |

251 | n/a | def start_cell(self, attrs): |

252 | n/a | self.y = int(attrs.get("row")) |

253 | n/a | self.x = int(attrs.get("col")) |

254 | n/a | |

255 | n/a | def start_value(self, attrs): |

256 | n/a | self.fmt = attrs.get('format') |

257 | n/a | self.alignment = xml2align.get(attrs.get('align')) |

258 | n/a | |

259 | n/a | start_formula = start_value |

260 | n/a | |

261 | n/a | def end_int(self, text): |

262 | n/a | try: |

263 | n/a | self.value = int(text) |

264 | n/a | except (TypeError, ValueError): |

265 | n/a | self.value = None |

266 | n/a | |

267 | n/a | end_long = end_int |

268 | n/a | |

269 | n/a | def end_double(self, text): |

270 | n/a | try: |

271 | n/a | self.value = float(text) |

272 | n/a | except (TypeError, ValueError): |

273 | n/a | self.value = None |

274 | n/a | |

275 | n/a | def end_complex(self, text): |

276 | n/a | try: |

277 | n/a | self.value = complex(text) |

278 | n/a | except (TypeError, ValueError): |

279 | n/a | self.value = None |

280 | n/a | |

281 | n/a | def end_string(self, text): |

282 | n/a | self.value = text |

283 | n/a | |

284 | n/a | def end_value(self, text): |

285 | n/a | if isinstance(self.value, BaseCell): |

286 | n/a | self.cell = self.value |

287 | n/a | elif isinstance(self.value, str): |

288 | n/a | self.cell = StringCell(self.value, |

289 | n/a | self.fmt or "%s", |

290 | n/a | self.alignment or LEFT) |

291 | n/a | else: |

292 | n/a | self.cell = NumericCell(self.value, |

293 | n/a | self.fmt or "%s", |

294 | n/a | self.alignment or RIGHT) |

295 | n/a | |

296 | n/a | def end_formula(self, text): |

297 | n/a | self.cell = FormulaCell(text, |

298 | n/a | self.fmt or "%s", |

299 | n/a | self.alignment or RIGHT) |

300 | n/a | |

301 | n/a | def end_cell(self, text): |

302 | n/a | self.sheet.setcell(self.x, self.y, self.cell) |

303 | n/a | |

304 | n/a | class BaseCell: |

305 | n/a | __init__ = None # Must provide |

306 | n/a | """Abstract base class for sheet cells. |

307 | n/a | |

308 | n/a | Subclasses may but needn't provide the following APIs: |

309 | n/a | |

310 | n/a | cell.reset() -- prepare for recalculation |

311 | n/a | cell.recalc(ns) -> value -- recalculate formula |

312 | n/a | cell.format() -> (value, alignment) -- return formatted value |

313 | n/a | cell.xml() -> string -- return XML |

314 | n/a | """ |

315 | n/a | |

316 | n/a | class NumericCell(BaseCell): |

317 | n/a | |

318 | n/a | def __init__(self, value, fmt="%s", alignment=RIGHT): |

319 | n/a | assert isinstance(value, (int, float, complex)) |

320 | n/a | assert alignment in (LEFT, CENTER, RIGHT) |

321 | n/a | self.value = value |

322 | n/a | self.fmt = fmt |

323 | n/a | self.alignment = alignment |

324 | n/a | |

325 | n/a | def recalc(self, ns): |

326 | n/a | return self.value |

327 | n/a | |

328 | n/a | def format(self): |

329 | n/a | try: |

330 | n/a | text = self.fmt % self.value |

331 | n/a | except: |

332 | n/a | text = str(self.value) |

333 | n/a | return text, self.alignment |

334 | n/a | |

335 | n/a | def xml(self): |

336 | n/a | method = getattr(self, '_xml_' + type(self.value).__name__) |

337 | n/a | return '<value align="%s" format="%s">%s</value>' % ( |

338 | n/a | align2xml[self.alignment], |

339 | n/a | self.fmt, |

340 | n/a | method()) |

341 | n/a | |

342 | n/a | def _xml_int(self): |

343 | n/a | if -2**31 <= self.value < 2**31: |

344 | n/a | return '<int>%s</int>' % self.value |

345 | n/a | else: |

346 | n/a | return '<long>%s</long>' % self.value |

347 | n/a | |

348 | n/a | def _xml_float(self): |

349 | n/a | return '<double>%r</double>' % self.value |

350 | n/a | |

351 | n/a | def _xml_complex(self): |

352 | n/a | return '<complex>%r</complex>' % self.value |

353 | n/a | |

354 | n/a | class StringCell(BaseCell): |

355 | n/a | |

356 | n/a | def __init__(self, text, fmt="%s", alignment=LEFT): |

357 | n/a | assert isinstance(text, str) |

358 | n/a | assert alignment in (LEFT, CENTER, RIGHT) |

359 | n/a | self.text = text |

360 | n/a | self.fmt = fmt |

361 | n/a | self.alignment = alignment |

362 | n/a | |

363 | n/a | def recalc(self, ns): |

364 | n/a | return self.text |

365 | n/a | |

366 | n/a | def format(self): |

367 | n/a | return self.text, self.alignment |

368 | n/a | |

369 | n/a | def xml(self): |

370 | n/a | s = '<value align="%s" format="%s"><string>%s</string></value>' |

371 | n/a | return s % ( |

372 | n/a | align2xml[self.alignment], |

373 | n/a | self.fmt, |

374 | n/a | escape(self.text)) |

375 | n/a | |

376 | n/a | class FormulaCell(BaseCell): |

377 | n/a | |

378 | n/a | def __init__(self, formula, fmt="%s", alignment=RIGHT): |

379 | n/a | assert alignment in (LEFT, CENTER, RIGHT) |

380 | n/a | self.formula = formula |

381 | n/a | self.translated = translate(self.formula) |

382 | n/a | self.fmt = fmt |

383 | n/a | self.alignment = alignment |

384 | n/a | self.reset() |

385 | n/a | |

386 | n/a | def reset(self): |

387 | n/a | self.value = None |

388 | n/a | |

389 | n/a | def recalc(self, ns): |

390 | n/a | if self.value is None: |

391 | n/a | try: |

392 | n/a | self.value = eval(self.translated, ns) |

393 | n/a | except: |

394 | n/a | exc = sys.exc_info()[0] |

395 | n/a | if hasattr(exc, "__name__"): |

396 | n/a | self.value = exc.__name__ |

397 | n/a | else: |

398 | n/a | self.value = str(exc) |

399 | n/a | return self.value |

400 | n/a | |

401 | n/a | def format(self): |

402 | n/a | try: |

403 | n/a | text = self.fmt % self.value |

404 | n/a | except: |

405 | n/a | text = str(self.value) |

406 | n/a | return text, self.alignment |

407 | n/a | |

408 | n/a | def xml(self): |

409 | n/a | return '<formula align="%s" format="%s">%s</formula>' % ( |

410 | n/a | align2xml[self.alignment], |

411 | n/a | self.fmt, |

412 | n/a | escape(self.formula)) |

413 | n/a | |

414 | n/a | def renumber(self, x1, y1, x2, y2, dx, dy): |

415 | n/a | out = [] |

416 | n/a | for part in re.split(r'(\w+)', self.formula): |

417 | n/a | m = re.match('^([A-Z]+)([1-9][0-9]*)$', part) |

418 | n/a | if m is not None: |

419 | n/a | sx, sy = m.groups() |

420 | n/a | x = colname2num(sx) |

421 | n/a | y = int(sy) |

422 | n/a | if x1 <= x <= x2 and y1 <= y <= y2: |

423 | n/a | part = cellname(x+dx, y+dy) |

424 | n/a | out.append(part) |

425 | n/a | return FormulaCell("".join(out), self.fmt, self.alignment) |

426 | n/a | |

427 | n/a | def translate(formula): |

428 | n/a | """Translate a formula containing fancy cell names to valid Python code. |

429 | n/a | |

430 | n/a | Examples: |

431 | n/a | B4 -> cell(2, 4) |

432 | n/a | B4:Z100 -> cells(2, 4, 26, 100) |

433 | n/a | """ |

434 | n/a | out = [] |

435 | n/a | for part in re.split(r"(\w+(?::\w+)?)", formula): |

436 | n/a | m = re.match(r"^([A-Z]+)([1-9][0-9]*)(?::([A-Z]+)([1-9][0-9]*))?$", part) |

437 | n/a | if m is None: |

438 | n/a | out.append(part) |

439 | n/a | else: |

440 | n/a | x1, y1, x2, y2 = m.groups() |

441 | n/a | x1 = colname2num(x1) |

442 | n/a | if x2 is None: |

443 | n/a | s = "cell(%s, %s)" % (x1, y1) |

444 | n/a | else: |

445 | n/a | x2 = colname2num(x2) |

446 | n/a | s = "cells(%s, %s, %s, %s)" % (x1, y1, x2, y2) |

447 | n/a | out.append(s) |

448 | n/a | return "".join(out) |

449 | n/a | |

450 | n/a | def cellname(x, y): |

451 | n/a | "Translate a cell coordinate to a fancy cell name (e.g. (1, 1)->'A1')." |

452 | n/a | assert x > 0 # Column 0 has an empty name, so can't use that |

453 | n/a | return colnum2name(x) + str(y) |

454 | n/a | |

455 | n/a | def colname2num(s): |

456 | n/a | "Translate a column name to number (e.g. 'A'->1, 'Z'->26, 'AA'->27)." |

457 | n/a | s = s.upper() |

458 | n/a | n = 0 |

459 | n/a | for c in s: |

460 | n/a | assert 'A' <= c <= 'Z' |

461 | n/a | n = n*26 + ord(c) - ord('A') + 1 |

462 | n/a | return n |

463 | n/a | |

464 | n/a | def colnum2name(n): |

465 | n/a | "Translate a column number to name (e.g. 1->'A', etc.)." |

466 | n/a | assert n > 0 |

467 | n/a | s = "" |

468 | n/a | while n: |

469 | n/a | n, m = divmod(n-1, 26) |

470 | n/a | s = chr(m+ord('A')) + s |

471 | n/a | return s |

472 | n/a | |

473 | n/a | import tkinter as Tk |

474 | n/a | |

475 | n/a | class SheetGUI: |

476 | n/a | |

477 | n/a | """Beginnings of a GUI for a spreadsheet. |

478 | n/a | |

479 | n/a | TO DO: |

480 | n/a | - clear multiple cells |

481 | n/a | - Insert, clear, remove rows or columns |

482 | n/a | - Show new contents while typing |

483 | n/a | - Scroll bars |

484 | n/a | - Grow grid when window is grown |

485 | n/a | - Proper menus |

486 | n/a | - Undo, redo |

487 | n/a | - Cut, copy and paste |

488 | n/a | - Formatting and alignment |

489 | n/a | """ |

490 | n/a | |

491 | n/a | def __init__(self, filename="sheet1.xml", rows=10, columns=5): |

492 | n/a | """Constructor. |

493 | n/a | |

494 | n/a | Load the sheet from the filename argument. |

495 | n/a | Set up the Tk widget tree. |

496 | n/a | """ |

497 | n/a | # Create and load the sheet |

498 | n/a | self.filename = filename |

499 | n/a | self.sheet = Sheet() |

500 | n/a | if os.path.isfile(filename): |

501 | n/a | self.sheet.load(filename) |

502 | n/a | # Calculate the needed grid size |

503 | n/a | maxx, maxy = self.sheet.getsize() |

504 | n/a | rows = max(rows, maxy) |

505 | n/a | columns = max(columns, maxx) |

506 | n/a | # Create the widgets |

507 | n/a | self.root = Tk.Tk() |

508 | n/a | self.root.wm_title("Spreadsheet: %s" % self.filename) |

509 | n/a | self.beacon = Tk.Label(self.root, text="A1", |

510 | n/a | font=('helvetica', 16, 'bold')) |

511 | n/a | self.entry = Tk.Entry(self.root) |

512 | n/a | self.savebutton = Tk.Button(self.root, text="Save", |

513 | n/a | command=self.save) |

514 | n/a | self.cellgrid = Tk.Frame(self.root) |

515 | n/a | # Configure the widget lay-out |

516 | n/a | self.cellgrid.pack(side="bottom", expand=1, fill="both") |

517 | n/a | self.beacon.pack(side="left") |

518 | n/a | self.savebutton.pack(side="right") |

519 | n/a | self.entry.pack(side="left", expand=1, fill="x") |

520 | n/a | # Bind some events |

521 | n/a | self.entry.bind("<Return>", self.return_event) |

522 | n/a | self.entry.bind("<Shift-Return>", self.shift_return_event) |

523 | n/a | self.entry.bind("<Tab>", self.tab_event) |

524 | n/a | self.entry.bind("<Shift-Tab>", self.shift_tab_event) |

525 | n/a | self.entry.bind("<Delete>", self.delete_event) |

526 | n/a | self.entry.bind("<Escape>", self.escape_event) |

527 | n/a | # Now create the cell grid |

528 | n/a | self.makegrid(rows, columns) |

529 | n/a | # Select the top-left cell |

530 | n/a | self.currentxy = None |

531 | n/a | self.cornerxy = None |

532 | n/a | self.setcurrent(1, 1) |

533 | n/a | # Copy the sheet cells to the GUI cells |

534 | n/a | self.sync() |

535 | n/a | |

536 | n/a | def delete_event(self, event): |

537 | n/a | if self.cornerxy != self.currentxy and self.cornerxy is not None: |

538 | n/a | self.sheet.clearcells(*(self.currentxy + self.cornerxy)) |

539 | n/a | else: |

540 | n/a | self.sheet.clearcell(*self.currentxy) |

541 | n/a | self.sync() |

542 | n/a | self.entry.delete(0, 'end') |

543 | n/a | return "break" |

544 | n/a | |

545 | n/a | def escape_event(self, event): |

546 | n/a | x, y = self.currentxy |

547 | n/a | self.load_entry(x, y) |

548 | n/a | |

549 | n/a | def load_entry(self, x, y): |

550 | n/a | cell = self.sheet.getcell(x, y) |

551 | n/a | if cell is None: |

552 | n/a | text = "" |

553 | n/a | elif isinstance(cell, FormulaCell): |

554 | n/a | text = '=' + cell.formula |

555 | n/a | else: |

556 | n/a | text, alignment = cell.format() |

557 | n/a | self.entry.delete(0, 'end') |

558 | n/a | self.entry.insert(0, text) |

559 | n/a | self.entry.selection_range(0, 'end') |

560 | n/a | |

561 | n/a | def makegrid(self, rows, columns): |

562 | n/a | """Helper to create the grid of GUI cells. |

563 | n/a | |

564 | n/a | The edge (x==0 or y==0) is filled with labels; the rest is real cells. |

565 | n/a | """ |

566 | n/a | self.rows = rows |

567 | n/a | self.columns = columns |

568 | n/a | self.gridcells = {} |

569 | n/a | # Create the top left corner cell (which selects all) |

570 | n/a | cell = Tk.Label(self.cellgrid, relief='raised') |

571 | n/a | cell.grid_configure(column=0, row=0, sticky='NSWE') |

572 | n/a | cell.bind("<ButtonPress-1>", self.selectall) |

573 | n/a | # Create the top row of labels, and configure the grid columns |

574 | n/a | for x in range(1, columns+1): |

575 | n/a | self.cellgrid.grid_columnconfigure(x, minsize=64) |

576 | n/a | cell = Tk.Label(self.cellgrid, text=colnum2name(x), relief='raised') |

577 | n/a | cell.grid_configure(column=x, row=0, sticky='WE') |

578 | n/a | self.gridcells[x, 0] = cell |

579 | n/a | cell.__x = x |

580 | n/a | cell.__y = 0 |

581 | n/a | cell.bind("<ButtonPress-1>", self.selectcolumn) |

582 | n/a | cell.bind("<B1-Motion>", self.extendcolumn) |

583 | n/a | cell.bind("<ButtonRelease-1>", self.extendcolumn) |

584 | n/a | cell.bind("<Shift-Button-1>", self.extendcolumn) |

585 | n/a | # Create the leftmost column of labels |

586 | n/a | for y in range(1, rows+1): |

587 | n/a | cell = Tk.Label(self.cellgrid, text=str(y), relief='raised') |

588 | n/a | cell.grid_configure(column=0, row=y, sticky='WE') |

589 | n/a | self.gridcells[0, y] = cell |

590 | n/a | cell.__x = 0 |

591 | n/a | cell.__y = y |

592 | n/a | cell.bind("<ButtonPress-1>", self.selectrow) |

593 | n/a | cell.bind("<B1-Motion>", self.extendrow) |

594 | n/a | cell.bind("<ButtonRelease-1>", self.extendrow) |

595 | n/a | cell.bind("<Shift-Button-1>", self.extendrow) |

596 | n/a | # Create the real cells |

597 | n/a | for x in range(1, columns+1): |

598 | n/a | for y in range(1, rows+1): |

599 | n/a | cell = Tk.Label(self.cellgrid, relief='sunken', |

600 | n/a | bg='white', fg='black') |

601 | n/a | cell.grid_configure(column=x, row=y, sticky='NSWE') |

602 | n/a | self.gridcells[x, y] = cell |

603 | n/a | cell.__x = x |

604 | n/a | cell.__y = y |

605 | n/a | # Bind mouse events |

606 | n/a | cell.bind("<ButtonPress-1>", self.press) |

607 | n/a | cell.bind("<B1-Motion>", self.motion) |

608 | n/a | cell.bind("<ButtonRelease-1>", self.release) |

609 | n/a | cell.bind("<Shift-Button-1>", self.release) |

610 | n/a | |

611 | n/a | def selectall(self, event): |

612 | n/a | self.setcurrent(1, 1) |

613 | n/a | self.setcorner(sys.maxsize, sys.maxsize) |

614 | n/a | |

615 | n/a | def selectcolumn(self, event): |

616 | n/a | x, y = self.whichxy(event) |

617 | n/a | self.setcurrent(x, 1) |

618 | n/a | self.setcorner(x, sys.maxsize) |

619 | n/a | |

620 | n/a | def extendcolumn(self, event): |

621 | n/a | x, y = self.whichxy(event) |

622 | n/a | if x > 0: |

623 | n/a | self.setcurrent(self.currentxy[0], 1) |

624 | n/a | self.setcorner(x, sys.maxsize) |

625 | n/a | |

626 | n/a | def selectrow(self, event): |

627 | n/a | x, y = self.whichxy(event) |

628 | n/a | self.setcurrent(1, y) |

629 | n/a | self.setcorner(sys.maxsize, y) |

630 | n/a | |

631 | n/a | def extendrow(self, event): |

632 | n/a | x, y = self.whichxy(event) |

633 | n/a | if y > 0: |

634 | n/a | self.setcurrent(1, self.currentxy[1]) |

635 | n/a | self.setcorner(sys.maxsize, y) |

636 | n/a | |

637 | n/a | def press(self, event): |

638 | n/a | x, y = self.whichxy(event) |

639 | n/a | if x > 0 and y > 0: |

640 | n/a | self.setcurrent(x, y) |

641 | n/a | |

642 | n/a | def motion(self, event): |

643 | n/a | x, y = self.whichxy(event) |

644 | n/a | if x > 0 and y > 0: |

645 | n/a | self.setcorner(x, y) |

646 | n/a | |

647 | n/a | release = motion |

648 | n/a | |

649 | n/a | def whichxy(self, event): |

650 | n/a | w = self.cellgrid.winfo_containing(event.x_root, event.y_root) |

651 | n/a | if w is not None and isinstance(w, Tk.Label): |

652 | n/a | try: |

653 | n/a | return w.__x, w.__y |

654 | n/a | except AttributeError: |

655 | n/a | pass |

656 | n/a | return 0, 0 |

657 | n/a | |

658 | n/a | def save(self): |

659 | n/a | self.sheet.save(self.filename) |

660 | n/a | |

661 | n/a | def setcurrent(self, x, y): |

662 | n/a | "Make (x, y) the current cell." |

663 | n/a | if self.currentxy is not None: |

664 | n/a | self.change_cell() |

665 | n/a | self.clearfocus() |

666 | n/a | self.beacon['text'] = cellname(x, y) |

667 | n/a | self.load_entry(x, y) |

668 | n/a | self.entry.focus_set() |

669 | n/a | self.currentxy = x, y |

670 | n/a | self.cornerxy = None |

671 | n/a | gridcell = self.gridcells.get(self.currentxy) |

672 | n/a | if gridcell is not None: |

673 | n/a | gridcell['bg'] = 'yellow' |

674 | n/a | |

675 | n/a | def setcorner(self, x, y): |

676 | n/a | if self.currentxy is None or self.currentxy == (x, y): |

677 | n/a | self.setcurrent(x, y) |

678 | n/a | return |

679 | n/a | self.clearfocus() |

680 | n/a | self.cornerxy = x, y |

681 | n/a | x1, y1 = self.currentxy |

682 | n/a | x2, y2 = self.cornerxy or self.currentxy |

683 | n/a | if x1 > x2: |

684 | n/a | x1, x2 = x2, x1 |

685 | n/a | if y1 > y2: |

686 | n/a | y1, y2 = y2, y1 |

687 | n/a | for (x, y), cell in self.gridcells.items(): |

688 | n/a | if x1 <= x <= x2 and y1 <= y <= y2: |

689 | n/a | cell['bg'] = 'lightBlue' |

690 | n/a | gridcell = self.gridcells.get(self.currentxy) |

691 | n/a | if gridcell is not None: |

692 | n/a | gridcell['bg'] = 'yellow' |

693 | n/a | self.setbeacon(x1, y1, x2, y2) |

694 | n/a | |

695 | n/a | def setbeacon(self, x1, y1, x2, y2): |

696 | n/a | if x1 == y1 == 1 and x2 == y2 == sys.maxsize: |

697 | n/a | name = ":" |

698 | n/a | elif (x1, x2) == (1, sys.maxsize): |

699 | n/a | if y1 == y2: |

700 | n/a | name = "%d" % y1 |

701 | n/a | else: |

702 | n/a | name = "%d:%d" % (y1, y2) |

703 | n/a | elif (y1, y2) == (1, sys.maxsize): |

704 | n/a | if x1 == x2: |

705 | n/a | name = "%s" % colnum2name(x1) |

706 | n/a | else: |

707 | n/a | name = "%s:%s" % (colnum2name(x1), colnum2name(x2)) |

708 | n/a | else: |

709 | n/a | name1 = cellname(*self.currentxy) |

710 | n/a | name2 = cellname(*self.cornerxy) |

711 | n/a | name = "%s:%s" % (name1, name2) |

712 | n/a | self.beacon['text'] = name |

713 | n/a | |

714 | n/a | |

715 | n/a | def clearfocus(self): |

716 | n/a | if self.currentxy is not None: |

717 | n/a | x1, y1 = self.currentxy |

718 | n/a | x2, y2 = self.cornerxy or self.currentxy |

719 | n/a | if x1 > x2: |

720 | n/a | x1, x2 = x2, x1 |

721 | n/a | if y1 > y2: |

722 | n/a | y1, y2 = y2, y1 |

723 | n/a | for (x, y), cell in self.gridcells.items(): |

724 | n/a | if x1 <= x <= x2 and y1 <= y <= y2: |

725 | n/a | cell['bg'] = 'white' |

726 | n/a | |

727 | n/a | def return_event(self, event): |

728 | n/a | "Callback for the Return key." |

729 | n/a | self.change_cell() |

730 | n/a | x, y = self.currentxy |

731 | n/a | self.setcurrent(x, y+1) |

732 | n/a | return "break" |

733 | n/a | |

734 | n/a | def shift_return_event(self, event): |

735 | n/a | "Callback for the Return key with Shift modifier." |

736 | n/a | self.change_cell() |

737 | n/a | x, y = self.currentxy |

738 | n/a | self.setcurrent(x, max(1, y-1)) |

739 | n/a | return "break" |

740 | n/a | |

741 | n/a | def tab_event(self, event): |

742 | n/a | "Callback for the Tab key." |

743 | n/a | self.change_cell() |

744 | n/a | x, y = self.currentxy |

745 | n/a | self.setcurrent(x+1, y) |

746 | n/a | return "break" |

747 | n/a | |

748 | n/a | def shift_tab_event(self, event): |

749 | n/a | "Callback for the Tab key with Shift modifier." |

750 | n/a | self.change_cell() |

751 | n/a | x, y = self.currentxy |

752 | n/a | self.setcurrent(max(1, x-1), y) |

753 | n/a | return "break" |

754 | n/a | |

755 | n/a | def change_cell(self): |

756 | n/a | "Set the current cell from the entry widget." |

757 | n/a | x, y = self.currentxy |

758 | n/a | text = self.entry.get() |

759 | n/a | cell = None |

760 | n/a | if text.startswith('='): |

761 | n/a | cell = FormulaCell(text[1:]) |

762 | n/a | else: |

763 | n/a | for cls in int, float, complex: |

764 | n/a | try: |

765 | n/a | value = cls(text) |

766 | n/a | except (TypeError, ValueError): |

767 | n/a | continue |

768 | n/a | else: |

769 | n/a | cell = NumericCell(value) |

770 | n/a | break |

771 | n/a | if cell is None and text: |

772 | n/a | cell = StringCell(text) |

773 | n/a | if cell is None: |

774 | n/a | self.sheet.clearcell(x, y) |

775 | n/a | else: |

776 | n/a | self.sheet.setcell(x, y, cell) |

777 | n/a | self.sync() |

778 | n/a | |

779 | n/a | def sync(self): |

780 | n/a | "Fill the GUI cells from the sheet cells." |

781 | n/a | self.sheet.recalc() |

782 | n/a | for (x, y), gridcell in self.gridcells.items(): |

783 | n/a | if x == 0 or y == 0: |

784 | n/a | continue |

785 | n/a | cell = self.sheet.getcell(x, y) |

786 | n/a | if cell is None: |

787 | n/a | gridcell['text'] = "" |

788 | n/a | else: |

789 | n/a | if hasattr(cell, 'format'): |

790 | n/a | text, alignment = cell.format() |

791 | n/a | else: |

792 | n/a | text, alignment = str(cell), LEFT |

793 | n/a | gridcell['text'] = text |

794 | n/a | gridcell['anchor'] = align2anchor[alignment] |

795 | n/a | |

796 | n/a | |

797 | n/a | def test_basic(): |

798 | n/a | "Basic non-gui self-test." |

799 | n/a | a = Sheet() |

800 | n/a | for x in range(1, 11): |

801 | n/a | for y in range(1, 11): |

802 | n/a | if x == 1: |

803 | n/a | cell = NumericCell(y) |

804 | n/a | elif y == 1: |

805 | n/a | cell = NumericCell(x) |

806 | n/a | else: |

807 | n/a | c1 = cellname(x, 1) |

808 | n/a | c2 = cellname(1, y) |

809 | n/a | formula = "%s*%s" % (c1, c2) |

810 | n/a | cell = FormulaCell(formula) |

811 | n/a | a.setcell(x, y, cell) |

812 | n/a | ## if os.path.isfile("sheet1.xml"): |

813 | n/a | ## print "Loading from sheet1.xml" |

814 | n/a | ## a.load("sheet1.xml") |

815 | n/a | a.display() |

816 | n/a | a.save("sheet1.xml") |

817 | n/a | |

818 | n/a | def test_gui(): |

819 | n/a | "GUI test." |

820 | n/a | if sys.argv[1:]: |

821 | n/a | filename = sys.argv[1] |

822 | n/a | else: |

823 | n/a | filename = "sheet1.xml" |

824 | n/a | g = SheetGUI(filename) |

825 | n/a | g.root.mainloop() |

826 | n/a | |

827 | n/a | if __name__ == '__main__': |

828 | n/a | #test_basic() |

829 | n/a | test_gui() |