1#!/usr/bin/env python 2# -*-coding:utf8-*- 3 4import os 5import time 6import sys 7import pygame 8import subprocess 9 10# this was standard for pygame back in the day 11from pygame.locals import * 12 13try: 14 import psyco 15 psyco.full() 16except ImportError: 17 pass 18 19 20class List: 21 22 def __init__(self, screen, top, left, width, height, itemlist, selectednr, fg, bg, fontsize=32, fontface="superfrog.ttf"): 23 self.screen = screen 24 self.top = top 25 self.left = left 26 self.width = width 27 self.height = height 28 self.set(itemlist, selectednr) 29 self.fg = fg 30 self.bg = bg 31 self.linewidth = 2 32 self.margin = 10 33 self.fontface = fontface 34 self.fontsize = fontsize 35 self.font = pygame.font.Font(self.fontface, self.fontsize) 36 self.fontheight = self.font.size("Blabla")[1] 37 self.rows = self.height / self.fontheight 38 self.drawwidth = self.width - (self.linewidth * 2) - (self.margin * 2) 39 self.selected_color = (255, 40, 20) 40 self.clear_read_ratio_for_long_text = 0.7 41 self.too_long_text_color = (80, 80, 75) 42 43 def set(self, itemlist, selectednr): 44 self.itemlist = itemlist 45 self.selectednr = selectednr 46 self.listlength = len(self.itemlist) 47 if self.listlength == 0: 48 print("Can't draw an empty list!") 49 sys.exit(1) 50 51 def draw(self): 52 self.clear() 53 54 gfx_rows = self.rows 55 if len(self.itemlist) <= gfx_rows: 56 # print "case 1: short list" 57 startrow = 0 58 endrow = self.listlength 59 elif self.selectednr <= int(gfx_rows / 2): 60 # print "case 2: selected at the top" 61 startrow = 0 62 endrow = startrow + gfx_rows 63 elif self.selectednr > (self.listlength - int(gfx_rows / 2) - 1): 64 # print "case 3: selected at the bottom" 65 startrow = self.selectednr - int(gfx_rows / 2) 66 endrow = len(self.itemlist) 67 else: 68 # print "case 4: selected in a long list" 69 startrow = self.selectednr - int(gfx_rows / 2) 70 endrow = startrow + gfx_rows 71 72 selected_surface = None 73 selected_pos = None 74 for list_index in range(startrow, endrow): 75 # Draw the text in red if it's selected, self.fg otherwise 76 if list_index == self.selectednr: 77 fg = self.selected_color 78 else: 79 fg = self.fg 80 81 # Generate the graphics for for the list_index we're at 82 gfx_index = list_index - startrow 83 y = self.linewidth + self.top + gfx_index * self.fontheight 84 text = self.font.render(self.itemlist[list_index], True, fg, self.bg) 85 86 # If the text is too wide, use the left part of the image and scale down the rest 87 if text.get_width() > self.drawwidth: 88 leftsize = int(self.drawwidth * self.clear_read_ratio_for_long_text) 89 90 # Try rendering until we have enough characters for the left part 91 s = self.itemlist[list_index] 92 numchars = len(s) 93 lefttext = text 94 while lefttext.get_width() > leftsize: 95 numchars -= 1 96 lefttext = self.font.render(s[:numchars-1], True, fg, self.bg) 97 leftsize = lefttext.get_width() 98 rightsize = self.drawwidth - leftsize 99 100 # Now we have the left part in lefttext 101 # Render the right part, and scale it down 102 103 righttext = self.font.render(s[numchars-1:], True, self.too_long_text_color, self.bg) 104 righttext = pygame.transform.scale(righttext, (rightsize, text.get_height())) 105 106 # Now, blit the left and right part into a blank text-surface 107 text.fill(self.bg) 108 text = pygame.transform.scale(text, (leftsize+rightsize, text.get_height())) 109 text.blit(lefttext, (0, 0)) 110 text.blit(righttext, (leftsize, 0)) 111 112 # Save the graphics if we're at the selected item, and draw the graphics 113 if list_index == self.selectednr: 114 selected_surface = text 115 selected_pos = (self.left + self.linewidth + self.margin, y) 116 self.screen.blit(text, selected_pos) 117 else: 118 self.screen.blit(text, (self.left + self.linewidth + self.margin, y)) 119 120 # Return the surface of the selected item, together with the rectangle 121 # This is useful if some other function wishes to animate the text 122 return selected_surface, selected_pos 123 124 def __next__(self): 125 self.selectednr += 1 126 if self.selectednr == self.listlength: 127 self.selectednr = 0 128 129 def prev(self): 130 self.selectednr -= 1 131 if self.selectednr == -1: 132 self.selectednr = self.listlength - 1 133 134 def jumptoletter(self, letter): 135 """Jump to the next item in the list starting with a certain letter. Returns True if we actually moved.""" 136 137 # items that starts with that letter 138 itemnumbers = [] 139 for i, item in enumerate(self.itemlist): 140 if item[0] == letter: 141 itemnumbers.append(i) 142 143 if itemnumbers: 144 # If we're past the last item of that letter, or before the first one... 145 if (self.selectednr > itemnumbers[-1]) or (self.selectednr < itemnumbers[0]): 146 # go to the first number of that letter 147 self.selectednr = itemnumbers[0] 148 return True 149 else: 150 # go to the next instance of that letter 151 for i in itemnumbers: 152 if i > self.selectednr: 153 self.selectednr = i 154 return True 155 self.selectednr = itemnumbers[0] 156 return True 157 else: 158 # Select the last letter on the list that is 159 # still earlier in the alphabet (or else 0) 160 chosen = 0 161 for i, item in enumerate(self.itemlist): 162 if ord(item[0]) < ord(letter): 163 chosen = i 164 if self.selectednr == chosen: 165 return False 166 else: 167 self.selectednr = chosen 168 return True 169 170 def selected(self): 171 return self.itemlist[self.selectednr] 172 173 def pageup(self): 174 numup = min(self.rows / 2, self.selectednr) 175 for x in range(numup): 176 self.prev() 177 178 def pagedown(self): 179 numdown = min(self.rows / 2, self.listlength - self.selectednr - 1) 180 for x in range(numdown): 181 next(self) 182 183 def home(self): 184 self.selectednr = 0 185 186 def end(self): 187 self.selectednr = self.listlength - 1 188 189 def clear(self): 190 pygame.draw.rect(self.screen, self.bg, (self.left, self.top, self.width, self.height), 0) 191 pygame.draw.rect(self.screen, self.fg, (self.left, self.top, self.width, self.height), self.linewidth) 192 193 194class MenuProgram: 195 196 def __init__(self, width, height, bg, fg, layout=[0.2, 0.2, 0.6], splashtime=0.9, fullscreen=False): 197 self.width = width 198 self.height = height 199 self.bg = bg 200 self.fg = fg 201 202 self.menu = Menu() 203 self.layout = layout 204 if sum(layout) != 1: 205 print("layout must be 1 in total!") 206 sys.exit(1) 207 208 pygame.display.init() 209 pygame.font.init() 210 211 if fullscreen: 212 self.screen = pygame.display.set_mode((self.width, self.height), FULLSCREEN) 213 else: 214 self.screen = pygame.display.set_mode((self.width, self.height)) 215 216 pygame.display.set_caption("Superfrog") 217 pygame.mouse.set_visible(1) 218 219 self.clock = pygame.time.Clock() 220 221 # --- Splash screen --- 222 self.splashimage = None 223 self.splash() 224 pygame.display.flip() 225 time.sleep(splashtime) 226 227 # --- Menu screen --- 228 # self.screen.fill(self.bg) 229 230 self.lwidth = int(self.width * layout[0]) + 10 231 self.l = List(self.screen, top=10, left=10, width=self.lwidth, height=self.height - 232 20, itemlist=self.menu.lmenu(), selectednr=0, fg=self.fg, bg=self.bg) 233 self.l.draw() 234 235 self.rwidth = int(self.width * layout[1]) + 10 236 skew = 30 237 self.r = List(self.screen, top=10+skew, left=10 + self.rwidth, width=self.rwidth, 238 height=self.height - 20 - skew, itemlist=self.menu.rmenu(), selectednr=0, fg=self.fg, bg=self.bg) 239 # self.r.clear() 240 241 self.active = self.l 242 243 pygame.display.flip() 244 245 # --- Mainloop --- 246 self.wait_answer() 247 248 def splash(self): 249 if self.splashimage: 250 scaled_image = self.splashimage 251 else: 252 image = pygame.image.load("superfrog.png") 253 scaled_image = pygame.transform.scale(image, (self.width, self.height)) 254 return self.screen.blit(scaled_image, (0, 0)) 255 256 def on_select(self, text): 257 if self.active == self.l: 258 self.active.clear() 259 self.menu.lselect(text) 260 261 self.r.set(self.menu.rmenu(), 0) 262 #self.r = List(self.screen, top=10, left=10 + self.rwidth, width=self.rwidth, height=self.height - 20, itemlist=self.menu.rmenu(), selectednr=0, fg=self.fg, bg=self.bg) 263 264 self.active = self.r 265 else: 266 self.menu.execute(text) 267 268 def on_back(self): 269 if self.active == self.r: 270 self.splash() 271 self.menu.back() 272 self.active = self.l 273 self.active.draw() 274 # else: 275 # print "can't back from left" 276 277 def on_move(self): 278 if self.active == self.r: 279 # print "display content about", self.active.selected() 280 pass 281 282 def wait_answer(self): 283 LETTERS = list(map(ord, "abcdefghijklmnopqrstuvwxyz" + chr(230) + chr(248) + chr(229))) 284 285 keep_running = True 286 287 # --- Mainloop --- 288 while keep_running: 289 290 for event in pygame.event.get(): 291 if event.type == QUIT: 292 keep_running = False 293 elif event.type == KEYDOWN: 294 if event.key in [K_ESCAPE, K_LEFT]: 295 if self.active == self.l: 296 keep_running = False 297 else: 298 self.on_back() 299 elif event.key == K_DOWN: 300 next(self.active) 301 self.on_move() 302 self.active.draw() 303 elif event.key == K_UP: 304 self.active.prev() 305 self.on_move() 306 self.active.draw() 307 elif event.key in [K_RETURN, K_RIGHT]: 308 self.on_select(self.active.selected()) 309 self.on_move() 310 self.active.draw() 311 elif event.key == K_PAGEUP: 312 self.active.pageup() 313 self.on_move() 314 self.active.draw() 315 elif event.key in [K_PAGEDOWN, K_SPACE]: 316 self.active.pagedown() 317 self.on_move() 318 self.active.draw() 319 elif event.key == K_HOME: 320 self.active.home() 321 self.on_move() 322 self.active.draw() 323 elif event.key == K_END: 324 self.active.end() 325 self.on_move() 326 self.active.draw() 327 elif event.key: 328 if event.key in LETTERS: 329 if self.active.jumptoletter(chr(event.key)): 330 self.on_move() 331 self.active.draw() 332 # else: 333 # print "bah", event.key 334 else: 335 pass 336 337 pygame.display.flip() 338 339 # self.clock.tick(60) 340 341 342def popen3(cmd, mode='t', bufsize=-1): 343 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True) 344 return p.stdin, p.stdout, p.stderr 345 346 347class Menu: 348 349 def __init__(self, lselected=0, rselected=0): 350 self.lactive = True 351 self.ractive = False 352 self.lselected = lselected 353 self.rselected = rselected 354 self.menudata = open("menu.txt").read().replace(" ", "\t") 355 356 self.commands = {} 357 358 current_category = "" 359 current_name = "" 360 current_command = "" 361 362 self.loptions = {0: []} 363 self.roptions = {} 364 365 for line in self.menudata.strip().split("\n"): 366 if not line: 367 continue 368 if (not line.startswith(" ")) and (not line.startswith("\t")): 369 current_category = line.strip() 370 self.loptions[0].append(current_category) 371 elif line.strip().startswith("*"): 372 current_name = line.strip()[1:].strip() 373 if self.loptions[0].index(current_category) not in self.roptions: 374 self.roptions[self.loptions[0].index(current_category)] = [current_name] 375 else: 376 self.roptions[self.loptions[0].index(current_category)].append(current_name) 377 elif line.strip().startswith("!"): 378 current_command = line.strip()[1:].strip() 379 self.roptions[self.loptions[0].index(current_category)] = popen3( 380 current_command)[1].read().split("\n")[:-1] 381 else: 382 current_command = line.strip() 383 self.commands[current_name] = current_command 384 385 def execute(self, what): 386 if what in self.commands: 387 os.system(self.commands[what]) 388 else: 389 print("No command for:", what) 390 print(self.commands) 391 392 def lselect(self, text): 393 if self.lactive: 394 self.lselected = self.loptions[0].index(text) 395 self.lactive = False 396 self.ractive = True 397 398 def back(self): 399 self.lactive = True 400 self.ractive = False 401 402 def lmenu(self): 403 return self.loptions[0] 404 405 def rmenu(self): 406 return self.roptions[self.lselected] 407 408 409def main(): 410 411 W = 1024 412 H = 768 413 BG = (255, 255, 220) 414 FG = (5, 5, 10) 415 LAYOUT = [0.2, 0.2, 0.6] 416 SPLASH = 0.5 417 FULL = False 418 419 mp = MenuProgram(W, H, BG, FG, LAYOUT, SPLASH, FULL) 420 421 pygame.display.quit() 422 print("Bye!") 423 sys.exit(0) 424 425 426if __name__ == "__main__": 427 main() 428