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