Add more Python apps
This commit is contained in:
parent
95b1f33e7f
commit
3a303fa843
@ -24,6 +24,8 @@ class AboutAppletWindow(yutani.Window):
|
||||
super(AboutAppletWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=title, icon=icon, doublebuffer=True)
|
||||
self.move(int((yutani.yutani_ctx._ptr.contents.display_width-self.width)/2),int((yutani.yutani_ctx._ptr.contents.display_height-self.height)/2))
|
||||
self.decorator = decorator
|
||||
if logo.endswith('.png'):
|
||||
logo = logo.replace('.png','.bmp') # Hope that works
|
||||
self.logo = yutani.Sprite.from_file(logo).get_cairo_surface()
|
||||
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, 13, 0xFF000000)
|
||||
self.tr = text_region.TextRegion(0,0,self.base_width-30,self.base_height-self.text_offset,font=self.font)
|
||||
|
101
base/home/local/python-demos/button.py
Normal file
101
base/home/local/python-demos/button.py
Normal file
@ -0,0 +1,101 @@
|
||||
import math
|
||||
|
||||
import cairo
|
||||
|
||||
import yutani
|
||||
import text_region
|
||||
import toaru_fonts
|
||||
|
||||
def rounded_rectangle(ctx,x,y,w,h,r):
|
||||
degrees = math.pi / 180
|
||||
ctx.new_sub_path()
|
||||
|
||||
ctx.arc(x + w - r, y + r, r, -90 * degrees, 0 * degrees)
|
||||
ctx.arc(x + w - r, y + h - r, r, 0 * degrees, 90 * degrees)
|
||||
ctx.arc(x + r, y + h - r, r, 90 * degrees, 180 * degrees)
|
||||
ctx.arc(x + r, y + r, r, 180 * degrees, 270 * degrees)
|
||||
ctx.close_path()
|
||||
|
||||
def draw_button(ctx,x,y,w,h,hilight):
|
||||
"""Theme definition for drawing a button."""
|
||||
ctx.save()
|
||||
|
||||
ctx.set_line_cap(cairo.LINE_CAP_ROUND)
|
||||
ctx.set_line_join(cairo.LINE_JOIN_ROUND)
|
||||
|
||||
if hilight == 2:
|
||||
rounded_rectangle(ctx, 2 + x, 2 + y, w - 4, h - 4, 2.0)
|
||||
ctx.set_source_rgba(134/255,173/255,201/255,1.0)
|
||||
ctx.set_line_width(2)
|
||||
ctx.stroke()
|
||||
|
||||
rounded_rectangle(ctx, 2 + x, 2 + y, w - 4, h - 4, 2.0)
|
||||
ctx.set_source_rgba(202/255,211/255,232/255,1.0)
|
||||
ctx.fill()
|
||||
else:
|
||||
rounded_rectangle(ctx, 2 + x, 2 + y, w - 4, h - 4, 2.0)
|
||||
ctx.set_source_rgba(44/255,71/255,91/255,29/255)
|
||||
ctx.set_line_width(4)
|
||||
ctx.stroke()
|
||||
|
||||
rounded_rectangle(ctx, 2 + x, 2 + y, w - 4, h - 4, 2.0)
|
||||
ctx.set_source_rgba(158/255,169/255,177/255,1.0)
|
||||
ctx.set_line_width(2)
|
||||
ctx.stroke()
|
||||
|
||||
if hilight == 1:
|
||||
pat = cairo.LinearGradient(2+x,2+y,2+x,2+y+h-4)
|
||||
pat.add_color_stop_rgba(0,1,1,1,1)
|
||||
pat.add_color_stop_rgba(1,229/255,229/255,246/255,1)
|
||||
rounded_rectangle(ctx,2+x,2+y,w-4,h-4,2.0)
|
||||
ctx.set_source(pat)
|
||||
ctx.fill()
|
||||
|
||||
pat = cairo.LinearGradient(3+x,3+y,3+x,3+y+h-4)
|
||||
pat.add_color_stop_rgba(0,252/255,252/255,254/255,1)
|
||||
pat.add_color_stop_rgba(1,212/255,223/255,251/255,1)
|
||||
rounded_rectangle(ctx,3+x,3+y,w-5,h-5,2.0)
|
||||
ctx.set_source(pat)
|
||||
ctx.fill()
|
||||
|
||||
else:
|
||||
pat = cairo.LinearGradient(2+x,2+y,2+x,2+y+h-4)
|
||||
pat.add_color_stop_rgba(0,1,1,1,1)
|
||||
pat.add_color_stop_rgba(1,241/255,241/255,244/255,1)
|
||||
rounded_rectangle(ctx,2+x,2+y,w-4,h-4,2.0)
|
||||
ctx.set_source(pat)
|
||||
ctx.fill()
|
||||
|
||||
pat = cairo.LinearGradient(3+x,3+y,3+x,3+y+h-4)
|
||||
pat.add_color_stop_rgba(0,252/255,252/255,254/255,1)
|
||||
pat.add_color_stop_rgba(1,223/255,225/255,230/255,1)
|
||||
rounded_rectangle(ctx,3+x,3+y,w-5,h-5,2.0)
|
||||
ctx.set_source(pat)
|
||||
ctx.fill()
|
||||
|
||||
ctx.restore()
|
||||
|
||||
class Button(object):
|
||||
|
||||
def __init__(self, text, callback):
|
||||
self.text = text
|
||||
self.callback = callback
|
||||
self.hilight = 0
|
||||
self.x, self.y, self.width, self.height = 0,0,0,0
|
||||
|
||||
def draw(self, window, ctx, x, y, w, h):
|
||||
self.x, self.y, self.width, self.height = x, y, w, h
|
||||
draw_button(ctx,x,y,w,h,self.hilight)
|
||||
|
||||
x_, y_ = ctx.user_to_device(x,y)
|
||||
tr = text_region.TextRegion(int(x_),int(y_),w,h)
|
||||
tr.set_alignment(2)
|
||||
tr.set_valignment(2)
|
||||
tr.set_text(self.text)
|
||||
tr.draw(window)
|
||||
|
||||
def focus_enter(self):
|
||||
self.hilight = 1
|
||||
|
||||
def focus_leave(self):
|
||||
self.hilight = 0
|
280
base/home/local/python-demos/calculator.py
Executable file
280
base/home/local/python-demos/calculator.py
Executable file
@ -0,0 +1,280 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
Calculator for ToaruOS
|
||||
"""
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import cairo
|
||||
|
||||
import yutani
|
||||
import text_region
|
||||
import toaru_fonts
|
||||
|
||||
from button import Button
|
||||
from menu_bar import MenuBarWidget, MenuEntryAction, MenuEntrySubmenu, MenuEntryDivider, MenuWindow
|
||||
from about_applet import AboutAppletWindow
|
||||
|
||||
import yutani_mainloop
|
||||
|
||||
import ast
|
||||
import operator as op
|
||||
|
||||
operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
|
||||
ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor,
|
||||
ast.USub: op.neg}
|
||||
|
||||
app_name = "Calculator"
|
||||
version = "1.0.0"
|
||||
_description = f"<b>{app_name} {version}</b>\n© 2017-2018 K. Lange\n\nSimple four-function calculator using Python.\n\n<color 0x0000FF>http://github.com/klange/toaruos</color>"
|
||||
|
||||
def eval_expr(expr):
|
||||
"""
|
||||
>>> eval_expr('2^6')
|
||||
4
|
||||
>>> eval_expr('2**6')
|
||||
64
|
||||
>>> eval_expr('1 + 2*3**(4^5) / (6 + -7)')
|
||||
-5.0
|
||||
"""
|
||||
return eval_(ast.parse(expr, mode='eval').body)
|
||||
|
||||
def eval_(node):
|
||||
if isinstance(node, ast.Num): # <number>
|
||||
return node.n
|
||||
elif isinstance(node, ast.BinOp): # <left> <operator> <right>
|
||||
return operators[type(node.op)](eval_(node.left), eval_(node.right))
|
||||
elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
|
||||
return operators[type(node.op)](eval_(node.operand))
|
||||
else:
|
||||
raise TypeError("invalid operation")
|
||||
|
||||
|
||||
class CalculatorWindow(yutani.Window):
|
||||
|
||||
base_width = 200
|
||||
base_height = 240
|
||||
|
||||
def __init__(self, decorator):
|
||||
super(CalculatorWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=app_name, icon="calculator", doublebuffer=True)
|
||||
self.move(100,100)
|
||||
self.decorator = decorator
|
||||
|
||||
def add_string(button):
|
||||
self.add_string(button.text)
|
||||
|
||||
def clear(button):
|
||||
self.clear_text()
|
||||
|
||||
def calculate(button):
|
||||
self.calculate()
|
||||
|
||||
self.buttons = [
|
||||
[Button("C",clear), None, Button("(",add_string), Button(")",add_string)],
|
||||
[Button("7",add_string), Button("8",add_string), Button("9",add_string), Button("/",add_string)],
|
||||
[Button("4",add_string), Button("5",add_string), Button("6",add_string), Button("*",add_string)],
|
||||
[Button("1",add_string), Button("2",add_string), Button("3",add_string), Button("-",add_string)],
|
||||
[Button("0",add_string), Button(".",add_string), Button("=",calculate), Button("+",add_string)],
|
||||
]
|
||||
|
||||
def exit_app(action):
|
||||
menus = [x for x in self.menus.values()]
|
||||
for x in menus:
|
||||
x.definitely_close()
|
||||
self.close()
|
||||
sys.exit(0)
|
||||
def about_window(action):
|
||||
AboutAppletWindow(self.decorator,f"About {app_name}","/usr/share/icons/48/calculator.png",_description,"calculator")
|
||||
def help_browser(action):
|
||||
subprocess.Popen(["help-browser.py","calculator.trt"])
|
||||
menus = [
|
||||
("File", [
|
||||
MenuEntryAction("Exit","exit",exit_app,None),
|
||||
]),
|
||||
("Help", [
|
||||
MenuEntryAction("Contents","help",help_browser,None),
|
||||
MenuEntryDivider(),
|
||||
MenuEntryAction(f"About {app_name}","star",about_window,None),
|
||||
]),
|
||||
]
|
||||
|
||||
self.menubar = MenuBarWidget(self,menus)
|
||||
|
||||
self.tr = text_region.TextRegion(self.decorator.left_width()+5,self.decorator.top_height()+self.menubar.height,self.base_width-10,40)
|
||||
self.tr.set_font(toaru_fonts.Font(toaru_fonts.FONT_MONOSPACE,18))
|
||||
self.tr.set_text("")
|
||||
self.tr.set_alignment(1)
|
||||
self.tr.set_valignment(2)
|
||||
self.tr.set_one_line()
|
||||
self.tr.set_ellipsis()
|
||||
|
||||
self.error = False
|
||||
|
||||
self.hover_widget = None
|
||||
self.down_button = None
|
||||
|
||||
self.menus = {}
|
||||
self.hovered_menu = None
|
||||
|
||||
|
||||
def calculate(self):
|
||||
if self.error or len(self.tr.text) == 0:
|
||||
self.tr.set_text("0")
|
||||
self.error = False
|
||||
try:
|
||||
self.tr.set_text(str(eval_expr(self.tr.text)))
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
if "(" in error:
|
||||
error = error[:error.find("(")-1]
|
||||
self.tr.set_richtext(f"<i><color 0xFF0000>{e.__class__.__name__}</color>: {error}</i>")
|
||||
self.error = True
|
||||
self.draw()
|
||||
self.flip()
|
||||
|
||||
def add_string(self, text):
|
||||
if self.error:
|
||||
self.tr.text = ""
|
||||
self.error = False
|
||||
self.tr.set_text(self.tr.text + text)
|
||||
self.draw()
|
||||
self.flip()
|
||||
|
||||
def clear_text(self):
|
||||
self.error = False
|
||||
self.tr.set_text("")
|
||||
self.draw()
|
||||
self.flip()
|
||||
|
||||
def clear_last(self):
|
||||
if self.error:
|
||||
self.error = False
|
||||
self.tr.set_text("")
|
||||
if len(self.tr.text):
|
||||
self.tr.set_text(self.tr.text[:-1])
|
||||
self.draw()
|
||||
self.flip()
|
||||
|
||||
def draw(self):
|
||||
surface = self.get_cairo_surface()
|
||||
|
||||
WIDTH, HEIGHT = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
ctx = cairo.Context(surface)
|
||||
ctx.translate(self.decorator.left_width(), self.decorator.top_height())
|
||||
ctx.rectangle(0,0,WIDTH,HEIGHT)
|
||||
ctx.set_source_rgb(204/255,204/255,204/255)
|
||||
ctx.fill()
|
||||
|
||||
ctx.rectangle(0,5+self.menubar.height,WIDTH,self.tr.height-10)
|
||||
ctx.set_source_rgb(1,1,1)
|
||||
ctx.fill()
|
||||
self.tr.resize(WIDTH-10, self.tr.height)
|
||||
self.tr.draw(self)
|
||||
|
||||
offset_x = 0
|
||||
offset_y = self.tr.height + self.menubar.height
|
||||
button_height = int((HEIGHT - self.tr.height - self.menubar.height) / len(self.buttons))
|
||||
for row in self.buttons:
|
||||
button_width = int(WIDTH / len(row))
|
||||
for button in row:
|
||||
if button:
|
||||
button.draw(self,ctx,offset_x,offset_y,button_width,button_height)
|
||||
offset_x += button_width
|
||||
offset_x = 0
|
||||
offset_y += button_height
|
||||
|
||||
self.menubar.draw(ctx,0,0,WIDTH)
|
||||
self.decorator.render(self)
|
||||
self.flip()
|
||||
|
||||
def finish_resize(self, msg):
|
||||
"""Accept a resize."""
|
||||
if msg.width < 200 or msg.height < 200:
|
||||
self.resize_offer(max(msg.width,200),max(msg.height,200))
|
||||
return
|
||||
self.resize_accept(msg.width, msg.height)
|
||||
self.reinit()
|
||||
self.draw()
|
||||
self.resize_done()
|
||||
self.flip()
|
||||
|
||||
def mouse_event(self, msg):
|
||||
if d.handle_event(msg) == yutani.Decor.EVENT_CLOSE:
|
||||
window.close()
|
||||
sys.exit(0)
|
||||
x,y = msg.new_x - self.decorator.left_width(), msg.new_y - self.decorator.top_height()
|
||||
w,h = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
if x >= 0 and x < w and y >= 0 and y < self.menubar.height:
|
||||
self.menubar.mouse_event(msg, x, y)
|
||||
return
|
||||
|
||||
redraw = False
|
||||
if self.down_button:
|
||||
if msg.command == yutani.MouseEvent.RAISE or msg.command == yutani.MouseEvent.CLICK:
|
||||
if not (msg.buttons & yutani.MouseButton.BUTTON_LEFT):
|
||||
if x >= self.down_button.x and \
|
||||
x < self.down_button.x + self.down_button.width and \
|
||||
y >= self.down_button.y and \
|
||||
y < self.down_button.y + self.down_button.height:
|
||||
self.down_button.focus_enter()
|
||||
self.down_button.callback(self.down_button)
|
||||
self.down_button = None
|
||||
redraw = True
|
||||
else:
|
||||
self.down_button.focus_leave()
|
||||
self.down_button = None
|
||||
redraw = True
|
||||
|
||||
else:
|
||||
if y > self.tr.height + self.menubar.height and y < h and x >= 0 and x < w:
|
||||
row = int((y - self.tr.height - self.menubar.height) / (self.height - self.decorator.height() - self.tr.height - self.menubar.height) * len(self.buttons))
|
||||
col = int(x / (self.width - self.decorator.width()) * len(self.buttons[row]))
|
||||
button = self.buttons[row][col]
|
||||
if button != self.hover_widget:
|
||||
if button:
|
||||
button.focus_enter()
|
||||
redraw = True
|
||||
if self.hover_widget:
|
||||
self.hover_widget.focus_leave()
|
||||
redraw = True
|
||||
self.hover_widget = button
|
||||
|
||||
if msg.command == yutani.MouseEvent.DOWN:
|
||||
if button:
|
||||
button.hilight = 2
|
||||
self.down_button = button
|
||||
redraw = True
|
||||
else:
|
||||
if self.hover_widget:
|
||||
self.hover_widget.focus_leave()
|
||||
redraw = True
|
||||
self.hover_widget = None
|
||||
|
||||
if redraw:
|
||||
self.draw()
|
||||
|
||||
def keyboard_event(self, msg):
|
||||
if msg.event.action != 0x01:
|
||||
return # Ignore anything that isn't a key down.
|
||||
if msg.event.key in b"0123456789.+-/*()":
|
||||
self.add_string(msg.event.key.decode('utf-8'))
|
||||
if msg.event.key == b"\n":
|
||||
self.calculate()
|
||||
if msg.event.key == b"c":
|
||||
self.clear_text()
|
||||
if msg.event.keycode == 8:
|
||||
self.clear_last()
|
||||
if msg.event.key == b"q":
|
||||
self.close()
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
yutani.Yutani()
|
||||
d = yutani.Decor()
|
||||
|
||||
window = CalculatorWindow(d)
|
||||
window.draw()
|
||||
|
||||
yutani_mainloop.mainloop()
|
453
base/home/local/python-demos/dialog.py
Normal file
453
base/home/local/python-demos/dialog.py
Normal file
@ -0,0 +1,453 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
Simple okay/cancel dialog.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
import time
|
||||
import fnmatch
|
||||
|
||||
import cairo
|
||||
|
||||
import yutani
|
||||
import text_region
|
||||
import toaru_fonts
|
||||
|
||||
from button import Button
|
||||
from icon_cache import get_icon
|
||||
|
||||
import yutani_mainloop
|
||||
|
||||
_default_text = "This is a dialog. <b>Formatting is available.</b>"
|
||||
|
||||
hilight_border_top = (54/255,128/255,205/255)
|
||||
hilight_gradient_top = (93/255,163/255,236/255)
|
||||
hilight_gradient_bottom = (56/255,137/255,220/55)
|
||||
hilight_border_bottom = (47/255,106/255,167/255)
|
||||
|
||||
class DialogButtons(object):
|
||||
|
||||
OKAY_CANCEL = 1
|
||||
YES_NO_CANCEL = 2
|
||||
|
||||
class DialogWindow(yutani.Window):
|
||||
|
||||
base_width = 500
|
||||
base_height = 150
|
||||
|
||||
text_offset = 40
|
||||
|
||||
okay_label = "Okay"
|
||||
cancel_label = "Cancel"
|
||||
|
||||
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):
|
||||
super(DialogWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=title, icon=icon, doublebuffer=True)
|
||||
|
||||
if window:
|
||||
# Center window
|
||||
self.move(window.x+int((window.width-self.width)/2),window.y+int((window.height-self.height)/2))
|
||||
else:
|
||||
# Center screen
|
||||
self.move(int((yutani.yutani_ctx._ptr.contents.display_width-self.width)/2),int((yutani.yutani_ctx._ptr.contents.display_height-self.height)/2))
|
||||
self.decorator = decorator
|
||||
self.logo = get_icon(icon,48,'help')
|
||||
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, 13, 0xFF000000)
|
||||
self.tr = text_region.TextRegion(0,0,self.base_width-60,self.base_height-self.text_offset,font=self.font)
|
||||
self.tr.set_richtext(text)
|
||||
|
||||
if cancel_label is not True:
|
||||
self.cancel_label = cancel_label
|
||||
|
||||
self.button_ok = Button(self.okay_label,self.ok_click)
|
||||
self.button_cancel = Button(self.cancel_label,self.cancel_click)
|
||||
self.buttons = [self.button_ok]
|
||||
if self.cancel_label:
|
||||
self.buttons.append(self.button_cancel)
|
||||
|
||||
self.close_is_cancel = close_is_cancel
|
||||
|
||||
self.hover_widget = None
|
||||
self.down_button = None
|
||||
|
||||
self.callback = callback
|
||||
self.cancel_callback = cancel_callback
|
||||
self.draw()
|
||||
|
||||
def ok_click(self, button):
|
||||
self.close()
|
||||
if self.callback:
|
||||
self.callback()
|
||||
return False
|
||||
|
||||
def cancel_click(self, button):
|
||||
self.close()
|
||||
if self.cancel_callback:
|
||||
self.cancel_callback()
|
||||
return False
|
||||
|
||||
def draw(self):
|
||||
surface = self.get_cairo_surface()
|
||||
|
||||
WIDTH, HEIGHT = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
ctx = cairo.Context(surface)
|
||||
ctx.translate(self.decorator.left_width(), self.decorator.top_height())
|
||||
ctx.rectangle(0,0,WIDTH,HEIGHT)
|
||||
ctx.set_source_rgb(204/255,204/255,204/255)
|
||||
ctx.fill()
|
||||
|
||||
ctx.set_source_surface(self.logo,30,30)
|
||||
ctx.paint()
|
||||
|
||||
self.tr.resize(WIDTH-90,HEIGHT-self.text_offset)
|
||||
self.tr.move(self.decorator.left_width() + 90,self.decorator.top_height()+self.text_offset)
|
||||
self.tr.draw(self)
|
||||
|
||||
self.button_ok.draw(self,ctx,WIDTH-130,HEIGHT-60,100,30)
|
||||
if self.cancel_label:
|
||||
self.button_cancel.draw(self,ctx,WIDTH-240,HEIGHT-60,100,30)
|
||||
|
||||
self.decorator.render(self)
|
||||
self.flip()
|
||||
|
||||
def finish_resize(self, msg):
|
||||
"""Accept a resize."""
|
||||
self.resize_accept(msg.width, msg.height)
|
||||
self.reinit()
|
||||
self.draw()
|
||||
self.resize_done()
|
||||
self.flip()
|
||||
|
||||
def mouse_event(self, msg):
|
||||
if self.decorator.handle_event(msg) == yutani.Decor.EVENT_CLOSE:
|
||||
if self.close_is_cancel:
|
||||
self.cancel_click(None)
|
||||
else:
|
||||
self.ok_click(None)
|
||||
return
|
||||
x,y = msg.new_x - self.decorator.left_width(), msg.new_y - self.decorator.top_height()
|
||||
w,h = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
redraw = False
|
||||
if self.down_button:
|
||||
if msg.command == yutani.MouseEvent.RAISE or msg.command == yutani.MouseEvent.CLICK:
|
||||
if not (msg.buttons & yutani.MouseButton.BUTTON_LEFT):
|
||||
if x >= self.down_button.x and \
|
||||
x < self.down_button.x + self.down_button.width and \
|
||||
y >= self.down_button.y and \
|
||||
y < self.down_button.y + self.down_button.height:
|
||||
self.down_button.focus_enter()
|
||||
if self.down_button.callback(self.down_button):
|
||||
redraw = True
|
||||
self.down_button = None
|
||||
else:
|
||||
self.down_button.focus_leave()
|
||||
self.down_button = None
|
||||
redraw = True
|
||||
|
||||
else:
|
||||
button = None
|
||||
for b in self.buttons:
|
||||
if x >= b.x and x < b.x + b.width and y >= b.y and y < b.y + b.height:
|
||||
button = b
|
||||
break
|
||||
if button != self.hover_widget:
|
||||
if button:
|
||||
button.focus_enter()
|
||||
redraw = True
|
||||
if self.hover_widget:
|
||||
self.hover_widget.focus_leave()
|
||||
redraw = True
|
||||
self.hover_widget = button
|
||||
|
||||
if msg.command == yutani.MouseEvent.DOWN:
|
||||
if button:
|
||||
button.hilight = 2
|
||||
self.down_button = button
|
||||
redraw = True
|
||||
if not button:
|
||||
if self.hover_widget:
|
||||
self.hover_widget.focus_leave()
|
||||
redraw = True
|
||||
self.hover_widget = None
|
||||
|
||||
if redraw:
|
||||
self.draw()
|
||||
|
||||
def keyboard_event(self, msg):
|
||||
if msg.event.action == yutani.KeyAction.ACTION_DOWN:
|
||||
if msg.event.key == b'\n':
|
||||
self.ok_click(None)
|
||||
|
||||
|
||||
class File(object):
|
||||
|
||||
def __init__(self, path,name=None):
|
||||
if not name:
|
||||
self.name = os.path.basename(path)
|
||||
else:
|
||||
self.name = name
|
||||
self.path = os.path.normpath(path)
|
||||
self.stat = os.stat(path)
|
||||
self.y = 0
|
||||
self.x = 0
|
||||
self.hilight = False
|
||||
self.tr = text_region.TextRegion(0,0,400,20)
|
||||
self.tr.set_one_line()
|
||||
self.tr.set_ellipsis()
|
||||
self.tr.set_text(self.name)
|
||||
|
||||
def do_action(self, dialog):
|
||||
if self.is_directory:
|
||||
dialog.load_directory(self.path)
|
||||
dialog.redraw_buf()
|
||||
return True
|
||||
else:
|
||||
dialog.path = self.path
|
||||
dialog.ok_click(None)
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_directory(self):
|
||||
return stat.S_ISDIR(self.stat.st_mode)
|
||||
|
||||
@property
|
||||
def is_executable(self):
|
||||
return stat.S_IXUSR & self.stat.st_mode and not self.is_directory
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
if self.is_directory: return get_icon('folder',16)
|
||||
if self.is_executable: return get_icon('applications-generic',16)
|
||||
return get_icon('file',16) # Need file icon
|
||||
|
||||
@property
|
||||
def sortkey(self):
|
||||
if self.is_directory: return "___" + self.name
|
||||
else: return "zzz" + self.name
|
||||
|
||||
|
||||
class OpenFileDialog(DialogWindow):
|
||||
|
||||
base_width = 500
|
||||
base_height = 450
|
||||
okay_label = "Open"
|
||||
|
||||
buf = None
|
||||
path = None
|
||||
icon_width = 16
|
||||
unit_height = 20
|
||||
|
||||
def __init__(self, decorator, title, glob=None, callback=None, cancel_callback=None,window=None):
|
||||
self.buf = None
|
||||
self.path = None
|
||||
if glob:
|
||||
self.matcher = re.compile(fnmatch.translate(glob))
|
||||
else:
|
||||
self.matcher = None
|
||||
self.tr = None
|
||||
self.load_directory(os.getcwd())
|
||||
self.redraw_buf()
|
||||
self.hilighted = None
|
||||
super(OpenFileDialog, self).__init__(decorator,title,"Open...",icon="open",callback=callback,cancel_callback=cancel_callback,window=window)
|
||||
self.tr.set_text(self.directory)
|
||||
|
||||
def ok_click(self, button):
|
||||
self.close()
|
||||
if self.callback:
|
||||
self.callback(self.path)
|
||||
return False
|
||||
|
||||
def load_directory(self, directory):
|
||||
self.directory = os.path.normpath(directory)
|
||||
if self.tr:
|
||||
self.tr.set_text(self.directory)
|
||||
self.files = sorted([File(os.path.join(self.directory,f)) for f in os.listdir(self.directory)],key=lambda x: x.sortkey)
|
||||
if self.matcher:
|
||||
self.files = [x for x in self.files if x.is_directory or self.matcher.match(x.name)]
|
||||
if directory != '/':
|
||||
self.files.insert(0,File(os.path.join(self.directory,'..'),'(Go up)'))
|
||||
self.scroll_y = 0
|
||||
|
||||
def redraw_buf(self,clips=None):
|
||||
if self.buf:
|
||||
self.buf.destroy()
|
||||
w = 450
|
||||
height = self.unit_height
|
||||
self.buf = yutani.GraphicsBuffer(w,len(self.files)*height)
|
||||
|
||||
surface = self.buf.get_cairo_surface()
|
||||
ctx = cairo.Context(surface)
|
||||
|
||||
if clips:
|
||||
for clip in clips:
|
||||
ctx.rectangle(clip.x,clip.y,w,height)
|
||||
ctx.clip()
|
||||
|
||||
ctx.rectangle(0,0,surface.get_width(),surface.get_height())
|
||||
ctx.set_source_rgb(1,1,1)
|
||||
ctx.fill()
|
||||
|
||||
offset_y = 0
|
||||
|
||||
i = 0
|
||||
for f in self.files:
|
||||
f.y = offset_y
|
||||
if not clips or f in clips:
|
||||
tr = f.tr
|
||||
tr.move(26,offset_y+2)
|
||||
if f.hilight:
|
||||
gradient = cairo.LinearGradient(0,0,0,height-2)
|
||||
gradient.add_color_stop_rgba(0.0,*hilight_gradient_top,1.0)
|
||||
gradient.add_color_stop_rgba(1.0,*hilight_gradient_bottom,1.0)
|
||||
ctx.rectangle(0,offset_y,w,1)
|
||||
ctx.set_source_rgb(*hilight_border_top)
|
||||
ctx.fill()
|
||||
ctx.rectangle(0,offset_y+height-1,w,1)
|
||||
ctx.set_source_rgb(*hilight_border_bottom)
|
||||
ctx.fill()
|
||||
ctx.save()
|
||||
ctx.translate(0,offset_y+1)
|
||||
ctx.rectangle(0,0,w,height-2)
|
||||
ctx.set_source(gradient)
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
tr.font.font_color = 0xFFFFFFFF
|
||||
else:
|
||||
ctx.rectangle(0,offset_y,w,height)
|
||||
if i % 2:
|
||||
ctx.set_source_rgb(0.9,0.9,0.9)
|
||||
else:
|
||||
ctx.set_source_rgb(1,1,1)
|
||||
ctx.fill()
|
||||
tr.font.font_color = 0xFF000000
|
||||
ctx.set_source_surface(f.icon,4,offset_y+2)
|
||||
ctx.paint()
|
||||
tr.draw(self.buf)
|
||||
offset_y += height
|
||||
i += 1
|
||||
|
||||
def draw(self):
|
||||
surface = self.get_cairo_surface()
|
||||
|
||||
WIDTH, HEIGHT = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
ctx = cairo.Context(surface)
|
||||
ctx.translate(self.decorator.left_width(), self.decorator.top_height())
|
||||
ctx.rectangle(0,0,WIDTH,HEIGHT)
|
||||
ctx.set_source_rgb(204/255,204/255,204/255)
|
||||
ctx.fill()
|
||||
|
||||
#ctx.set_source_surface(self.logo,30,30)
|
||||
#ctx.paint()
|
||||
|
||||
self.tr.resize(WIDTH,30)
|
||||
self.tr.move(self.decorator.left_width(),self.decorator.top_height()+10)
|
||||
self.tr.set_alignment(2)
|
||||
self.tr.draw(self)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(20, 40)
|
||||
ctx.rectangle(0,0,450,HEIGHT-130)
|
||||
ctx.set_line_width(2)
|
||||
ctx.set_source_rgb(0.7,0.7,0.7)
|
||||
ctx.stroke_preserve()
|
||||
ctx.set_source_rgb(1,1,1)
|
||||
ctx.fill()
|
||||
ctx.rectangle(0,0,450,HEIGHT-130)
|
||||
ctx.clip()
|
||||
text = self.buf.get_cairo_surface()
|
||||
ctx.set_source_surface(text,0,self.scroll_y)
|
||||
ctx.paint()
|
||||
ctx.restore()
|
||||
|
||||
self.button_cancel.draw(self,ctx,WIDTH-130,HEIGHT-60,100,30)
|
||||
self.button_ok.draw(self,ctx,WIDTH-240,HEIGHT-60,100,30)
|
||||
|
||||
self.decorator.render(self)
|
||||
self.flip()
|
||||
|
||||
def scroll(self, amount):
|
||||
w,h = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
self.scroll_y += amount
|
||||
if self.scroll_y > 0:
|
||||
self.scroll_y = 0
|
||||
top = min(-(self.buf.height - (h-130)),0)
|
||||
if self.scroll_y < top:
|
||||
self.scroll_y = top
|
||||
|
||||
|
||||
def mouse_event(self, msg):
|
||||
super(OpenFileDialog,self).mouse_event(msg)
|
||||
|
||||
x,y = msg.new_x - self.decorator.left_width(), msg.new_y - self.decorator.top_height()
|
||||
w,h = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
if y < 0: return
|
||||
if x < 0 or x >= w: return
|
||||
|
||||
if msg.buttons & yutani.MouseButton.SCROLL_UP:
|
||||
self.scroll(30)
|
||||
self.draw()
|
||||
return
|
||||
elif msg.buttons & yutani.MouseButton.SCROLL_DOWN:
|
||||
self.scroll(-30)
|
||||
self.draw()
|
||||
return
|
||||
|
||||
offset_y = self.scroll_y + 40
|
||||
|
||||
redraw = []
|
||||
hit = False
|
||||
|
||||
if x >= 20 and x < 450+20:
|
||||
for f in self.files:
|
||||
if offset_y > h: break
|
||||
if y >= offset_y and y < offset_y + self.unit_height:
|
||||
if not f.hilight:
|
||||
redraw.append(f)
|
||||
if self.hilighted:
|
||||
redraw.append(self.hilighted)
|
||||
self.hilighted.hilight = False
|
||||
f.hilight = True
|
||||
self.hilighted = f
|
||||
hit = True
|
||||
break
|
||||
offset_y += self.unit_height
|
||||
|
||||
if not hit:
|
||||
if self.hilighted:
|
||||
redraw.append(self.hilighted)
|
||||
self.hilighted.hilight = False
|
||||
self.hilighted = None
|
||||
|
||||
if self.hilighted:
|
||||
if msg.command == yutani.MouseEvent.DOWN:
|
||||
if self.hilighted.do_action(self):
|
||||
redraw = []
|
||||
self.redraw_buf()
|
||||
self.draw()
|
||||
|
||||
if redraw:
|
||||
self.redraw_buf(redraw)
|
||||
self.draw()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
yutani.Yutani()
|
||||
d = yutani.Decor()
|
||||
|
||||
def okay(path):
|
||||
print("You hit Okay!",path)
|
||||
sys.exit(0)
|
||||
|
||||
def cancel():
|
||||
print("You hit Cancel!")
|
||||
sys.exit(0)
|
||||
|
||||
#window = DialogWindow(d,"Okay/Cancel Dialog","A thing happend!",cancel_callback=cancel,callback=okay)
|
||||
window = OpenFileDialog(d,"Open...",glob="*.png",cancel_callback=cancel,callback=okay)
|
||||
|
||||
yutani_mainloop.mainloop()
|
||||
|
331
base/home/local/python-demos/file_browser.py
Executable file
331
base/home/local/python-demos/file_browser.py
Executable file
@ -0,0 +1,331 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
File Browser
|
||||
"""
|
||||
import os
|
||||
import math
|
||||
import stat
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
import cairo
|
||||
|
||||
import yutani
|
||||
import text_region
|
||||
import toaru_fonts
|
||||
|
||||
from menu_bar import MenuBarWidget, MenuEntryAction, MenuEntrySubmenu, MenuEntryDivider, MenuWindow
|
||||
from icon_cache import get_icon
|
||||
from about_applet import AboutAppletWindow
|
||||
from input_box import TextInputWindow
|
||||
from dialog import DialogWindow
|
||||
|
||||
import yutani_mainloop
|
||||
|
||||
app_name = "File Browser"
|
||||
version = "1.0.0"
|
||||
_description = f"<b>{app_name} {version}</b>\n© 2017-2018 K. Lange\n\nFile system navigator.\n\n<color 0x0000FF>http://github.com/klange/toaruos</color>"
|
||||
|
||||
class File(object):
|
||||
|
||||
def __init__(self, path, window):
|
||||
self.path = path
|
||||
self.name = os.path.basename(path)
|
||||
self.stat = os.stat(path)
|
||||
self.hilight = False
|
||||
self.window = window
|
||||
self.tr = text_region.TextRegion(0,0,100,20)
|
||||
self.tr.set_alignment(2)
|
||||
self.tr.set_ellipsis()
|
||||
self.tr.set_text(self.name)
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
|
||||
@property
|
||||
def is_directory(self):
|
||||
return stat.S_ISDIR(self.stat.st_mode)
|
||||
|
||||
@property
|
||||
def is_executable(self):
|
||||
return stat.S_IXUSR & self.stat.st_mode and not self.is_directory
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
if self.is_directory: return get_icon('folder',48)
|
||||
if self.is_executable: return get_icon(self.name,48)
|
||||
return get_icon('file',48) # Need file icon
|
||||
|
||||
def do_action(self):
|
||||
if self.is_directory:
|
||||
self.window.load_directory(self.path)
|
||||
self.window.draw()
|
||||
elif self.is_executable:
|
||||
subprocess.Popen([self.path])
|
||||
elif self.name.endswith('.png'):
|
||||
subprocess.Popen(['painting.py',self.path])
|
||||
elif self.name.endswith('.pdf') and os.path.exists('/usr/bin/pdfviewer.py'):
|
||||
subprocess.Popen(['pdfviewer.py',self.path])
|
||||
elif self.name.endswith('.pdf') and os.path.exists('/usr/bin/pdfviewer'):
|
||||
subprocess.Popen(['pdfviewer',self.path])
|
||||
# Nothing to do.
|
||||
|
||||
@property
|
||||
def sortkey(self):
|
||||
if self.is_directory: return "___" + self.name
|
||||
else: return "zzz" + self.name
|
||||
|
||||
|
||||
class FileBrowserWindow(yutani.Window):
|
||||
|
||||
base_width = 400
|
||||
base_height = 300
|
||||
|
||||
def __init__(self, decorator, path):
|
||||
super(FileBrowserWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=app_name, icon="folder", doublebuffer=True)
|
||||
self.move(100,100)
|
||||
self.x = 100
|
||||
self.y = 100
|
||||
self.decorator = decorator
|
||||
|
||||
def exit_app(action):
|
||||
menus = [x for x in self.menus.values()]
|
||||
for x in menus:
|
||||
x.definitely_close()
|
||||
self.close()
|
||||
sys.exit(0)
|
||||
def about_window(action):
|
||||
AboutAppletWindow(self.decorator,f"About {app_name}","/usr/share/icons/48/folder.png",_description,"folder")
|
||||
def help_browser(action):
|
||||
subprocess.Popen(["help-browser.py","file_browser.trt"])
|
||||
|
||||
def input_path(action):
|
||||
def input_callback(input_window):
|
||||
text = input_window.tr.text
|
||||
input_window.close()
|
||||
self.load_directory(text)
|
||||
TextInputWindow(self.decorator,"Open directory...","open",text=self.path,callback=input_callback,window=self)
|
||||
|
||||
menus = [
|
||||
("File", [
|
||||
MenuEntryAction("Exit","exit",exit_app,None),
|
||||
]),
|
||||
("Go", [
|
||||
MenuEntryAction("Path...","open",input_path,None),
|
||||
MenuEntryDivider(),
|
||||
MenuEntryAction("Home","home",self.load_directory,os.environ.get("HOME")),
|
||||
MenuEntryAction("File System",None,self.load_directory,"/"),
|
||||
MenuEntryAction("Up","up",self.go_up,None),
|
||||
]),
|
||||
("Help", [
|
||||
MenuEntryAction("Contents","help",help_browser,None),
|
||||
MenuEntryDivider(),
|
||||
MenuEntryAction(f"About {app_name}","star",about_window,None),
|
||||
]),
|
||||
]
|
||||
|
||||
self.menubar = MenuBarWidget(self,menus)
|
||||
|
||||
self.hover_widget = None
|
||||
self.down_button = None
|
||||
|
||||
self.menus = {}
|
||||
self.hovered_menu = None
|
||||
|
||||
self.buf = None
|
||||
self.load_directory(path)
|
||||
self.hilighted = None
|
||||
|
||||
def go_up(self, action):
|
||||
self.load_directory(os.path.abspath(os.path.join(self.path,'..')))
|
||||
self.draw()
|
||||
|
||||
def load_directory(self, path):
|
||||
if not os.path.exists(path):
|
||||
DialogWindow(self.decorator,app_name,f"The path <mono>{path}</mono> could not be opened. (Not found)",window=self,icon='folder')
|
||||
return
|
||||
if not os.path.isdir(path):
|
||||
DialogWindow(self.decorator,app_name,f"The path <mono>{path}</mono> could not be opened. (Not a directory)",window=self,icon='folder')
|
||||
return
|
||||
path = os.path.normpath(path)
|
||||
self.path = path
|
||||
title = "/" if path == "/" else os.path.basename(path)
|
||||
self.set_title(f"{title} - {app_name}",'folder')
|
||||
|
||||
self.files = sorted([File(os.path.join(path,f), self) for f in os.listdir(path)], key=lambda x: x.sortkey)
|
||||
self.scroll_y = 0
|
||||
self.hilighted = None
|
||||
self.redraw_buf()
|
||||
|
||||
def redraw_buf(self,icons=None):
|
||||
if self.buf:
|
||||
self.buf.destroy()
|
||||
w = self.width - self.decorator.width()
|
||||
files_per_row = int(w / 100)
|
||||
self.buf = yutani.GraphicsBuffer(w,math.ceil(len(self.files)/files_per_row)*100)
|
||||
|
||||
surface = self.buf.get_cairo_surface()
|
||||
ctx = cairo.Context(surface)
|
||||
|
||||
if icons:
|
||||
for icon in icons:
|
||||
ctx.rectangle(icon.x,icon.y,100,100)
|
||||
ctx.clip()
|
||||
|
||||
|
||||
ctx.rectangle(0,0,surface.get_width(),surface.get_height())
|
||||
ctx.set_source_rgb(1,1,1)
|
||||
ctx.fill()
|
||||
|
||||
offset_x = 0
|
||||
offset_y = 0
|
||||
|
||||
for f in self.files:
|
||||
if not icons or f in icons:
|
||||
x_, y_ = ctx.user_to_device(0,0)
|
||||
f.tr.move(offset_x,offset_y+60)
|
||||
f.tr.draw(self.buf)
|
||||
ctx.set_source_surface(f.icon,offset_x + 26,offset_y+10)
|
||||
ctx.paint_with_alpha(1.0 if not f.hilight else 0.7)
|
||||
f.x = offset_x
|
||||
f.y = offset_y
|
||||
offset_x += 100
|
||||
if offset_x + 100 > surface.get_width():
|
||||
offset_x = 0
|
||||
offset_y += 100
|
||||
|
||||
def draw(self):
|
||||
surface = self.get_cairo_surface()
|
||||
|
||||
WIDTH, HEIGHT = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
ctx = cairo.Context(surface)
|
||||
ctx.translate(self.decorator.left_width(), self.decorator.top_height())
|
||||
ctx.rectangle(0,0,WIDTH,HEIGHT)
|
||||
ctx.set_source_rgb(1,1,1)
|
||||
ctx.fill()
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(0,self.menubar.height)
|
||||
text = self.buf.get_cairo_surface()
|
||||
ctx.set_source_surface(text,0,self.scroll_y)
|
||||
ctx.paint()
|
||||
ctx.restore()
|
||||
|
||||
self.menubar.draw(ctx,0,0,WIDTH)
|
||||
|
||||
self.decorator.render(self)
|
||||
self.flip()
|
||||
|
||||
def finish_resize(self, msg):
|
||||
"""Accept a resize."""
|
||||
if msg.width < 120 or msg.height < 120:
|
||||
self.resize_offer(max(msg.width,120),max(msg.height,120))
|
||||
return
|
||||
self.resize_accept(msg.width, msg.height)
|
||||
self.reinit()
|
||||
self.redraw_buf()
|
||||
self.draw()
|
||||
self.resize_done()
|
||||
self.flip()
|
||||
|
||||
def scroll(self, amount):
|
||||
w,h = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
files_per_row = int(w / 100)
|
||||
rows_total = math.ceil(len(self.files) / files_per_row)
|
||||
rows_visible = int((h - 24) / 100)
|
||||
rows = rows_total - rows_visible
|
||||
if rows < 0: rows = 0
|
||||
self.scroll_y += amount
|
||||
if self.scroll_y > 0:
|
||||
self.scroll_y = 0
|
||||
if self.scroll_y < -100 * rows:
|
||||
self.scroll_y = -100 * rows
|
||||
|
||||
def mouse_event(self, msg):
|
||||
if d.handle_event(msg) == yutani.Decor.EVENT_CLOSE:
|
||||
window.close()
|
||||
sys.exit(0)
|
||||
x,y = msg.new_x - self.decorator.left_width(), msg.new_y - self.decorator.top_height()
|
||||
w,h = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
if x >= 0 and x < w and y >= 0 and y < self.menubar.height:
|
||||
self.menubar.mouse_event(msg, x, y)
|
||||
return
|
||||
|
||||
if x >= 0 and x < w and y >= self.menubar.height and y < h:
|
||||
if msg.buttons & yutani.MouseButton.SCROLL_UP:
|
||||
self.scroll(30)
|
||||
self.draw()
|
||||
return
|
||||
elif msg.buttons & yutani.MouseButton.SCROLL_DOWN:
|
||||
self.scroll(-30)
|
||||
self.draw()
|
||||
return
|
||||
|
||||
if msg.buttons & yutani.MouseButton.BUTTON_RIGHT:
|
||||
if not self.menus:
|
||||
menu_entries = [
|
||||
MenuEntryAction("Up","up",self.go_up,None),
|
||||
]
|
||||
menu = MenuWindow(menu_entries,(self.x+msg.new_x,self.y+msg.new_y),root=self)
|
||||
return
|
||||
|
||||
if y < 0: return
|
||||
|
||||
offset_x = 0
|
||||
offset_y = self.scroll_y + self.menubar.height
|
||||
|
||||
redraw = []
|
||||
|
||||
files_per_row = int(w / 100)
|
||||
rows_total = math.ceil(len(self.files) / files_per_row)
|
||||
skip_files = files_per_row * (int(-offset_y / 100))
|
||||
offset_y += int(-offset_y/100) * 100
|
||||
|
||||
hit = False
|
||||
for f in self.files[skip_files:]:
|
||||
if offset_y > h: break
|
||||
if offset_y > -100:
|
||||
if x >= offset_x and x < offset_x + 100 and y >= offset_y and y < offset_y + 100:
|
||||
if not f.hilight:
|
||||
redraw.append(f)
|
||||
if self.hilighted:
|
||||
redraw.append(self.hilighted)
|
||||
self.hilighted.hilight = False
|
||||
f.hilight = True
|
||||
self.hilighted = f
|
||||
hit = True
|
||||
break
|
||||
offset_x += 100
|
||||
if offset_x + 100 > w:
|
||||
offset_x = 0
|
||||
offset_y += 100
|
||||
if not hit:
|
||||
if self.hilighted:
|
||||
redraw.append(self.hilighted)
|
||||
self.hilighted.hilight = False
|
||||
self.hilighted = None
|
||||
|
||||
if self.hilighted:
|
||||
if msg.command == yutani.MouseEvent.DOWN:
|
||||
self.hilighted.do_action()
|
||||
|
||||
if redraw:
|
||||
self.redraw_buf(redraw)
|
||||
self.draw()
|
||||
|
||||
def keyboard_event(self, msg):
|
||||
if msg.event.action != yutani.KeyAction.ACTION_DOWN:
|
||||
return # Ignore anything that isn't a key down.
|
||||
if msg.event.key == b"q":
|
||||
self.close()
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
yutani.Yutani()
|
||||
d = yutani.Decor()
|
||||
|
||||
window = FileBrowserWindow(d,os.environ.get('HOME','/') if len(sys.argv) < 2 else sys.argv[1])
|
||||
window.draw()
|
||||
|
||||
yutani_mainloop.mainloop()
|
439
base/home/local/python-demos/input_box.py
Executable file
439
base/home/local/python-demos/input_box.py
Executable file
@ -0,0 +1,439 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
Shows an input box.
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import cairo
|
||||
|
||||
import yutani
|
||||
import text_region
|
||||
import toaru_fonts
|
||||
import fswait
|
||||
|
||||
from button import Button
|
||||
|
||||
import yutani_mainloop
|
||||
|
||||
def rounded_rectangle(ctx,x,y,w,h,r):
|
||||
degrees = math.pi / 180
|
||||
ctx.new_sub_path()
|
||||
|
||||
ctx.arc(x + w - r, y + r, r, -90 * degrees, 0 * degrees)
|
||||
ctx.arc(x + w - r, y + h - r, r, 0 * degrees, 90 * degrees)
|
||||
ctx.arc(x + r, y + h - r, r, 90 * degrees, 180 * degrees)
|
||||
ctx.arc(x + r, y + r, r, 180 * degrees, 270 * degrees)
|
||||
ctx.close_path()
|
||||
|
||||
def draw_input_box(ctx,x,y,w,h,focused):
|
||||
# Outer
|
||||
rounded_rectangle(ctx,x,y,w,h,4)
|
||||
if focused:
|
||||
ctx.set_source_rgb(0x8E/0xFF,0xD8/0xFF,1)
|
||||
else:
|
||||
ctx.set_source_rgb(192/255,192/255,192/255)
|
||||
ctx.fill()
|
||||
|
||||
# Inner
|
||||
rounded_rectangle(ctx,x+1,y+1,w-2,h-2,4)
|
||||
if focused:
|
||||
ctx.set_source_rgb(246/255,246/255,246/255)
|
||||
else:
|
||||
ctx.set_source_rgb(234/255,234/255,234/255)
|
||||
ctx.fill()
|
||||
|
||||
class InputBox(object):
|
||||
|
||||
padding = 5
|
||||
cursor_height = 15
|
||||
text_offset = 2
|
||||
color = 0xFF000000
|
||||
ph_color = 0x88000000
|
||||
ctrl_chars = [' ','/']
|
||||
|
||||
def __init__(self,text="",password=False,placeholder="",width=200,height=20):
|
||||
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, 13, self.color)
|
||||
self.tr = text_region.TextRegion(0,0,200,20,font=self.font)
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
self.placeholder = placeholder
|
||||
self.ph_font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, 13, self.ph_color)
|
||||
self.ph_tr = text_region.TextRegion(0,0,200,20,font=self.ph_font)
|
||||
self.ph_tr.set_text(placeholder)
|
||||
|
||||
self.is_password = password
|
||||
self.tr.set_one_line()
|
||||
self.tr.break_all = True
|
||||
self.update_text(text)
|
||||
self.is_focused = False
|
||||
self.cursor_index = len(self.text)
|
||||
self.cursor_x = self.tr.get_offset_at_index(self.cursor_index)[1][1]
|
||||
|
||||
if password:
|
||||
self.ctrl_chars = []
|
||||
|
||||
self.x = 0
|
||||
self.y = 0
|
||||
|
||||
self.text_changed = None
|
||||
|
||||
self.submit = None
|
||||
self.tab_handler = None
|
||||
|
||||
def draw(self,window,ctx):
|
||||
self.parent = window
|
||||
x_, y_ = ctx.user_to_device(0,0)
|
||||
|
||||
self.x = int(x_)
|
||||
self.y = int(y_)
|
||||
|
||||
draw_input_box(ctx,0,0,self.width,self.height,self.is_focused)
|
||||
|
||||
self.tr.resize(self.width - self.padding * 2, self.height)
|
||||
self.tr.move(self.x + self.padding,self.y + self.text_offset)
|
||||
|
||||
if not self.is_focused and not self.text and self.placeholder:
|
||||
self.ph_tr.resize(self.width - self.padding * 2, self.height)
|
||||
self.ph_tr.move(self.x + self.padding,self.y + self.text_offset)
|
||||
self.ph_tr.draw(window)
|
||||
else:
|
||||
self.tr.draw(window)
|
||||
|
||||
if self.cursor_x is not None and self.is_focused:
|
||||
ctx.rectangle(self.cursor_x + self.padding, self.text_offset, 1, self.cursor_height)
|
||||
ctx.set_source_rgb(0,0,0)
|
||||
ctx.fill()
|
||||
|
||||
def mouse_event(self, msg):
|
||||
if msg.command == yutani.MouseEvent.DOWN:
|
||||
if msg.new_x >= self.x and msg.new_x < self.x + self.width and \
|
||||
msg.new_y >= self.y and msg.new_y < self.y + self.height:
|
||||
self.focus_enter()
|
||||
changed = False
|
||||
u,l = self.tr.pick(msg.new_x,msg.new_y)
|
||||
if u:
|
||||
changed = True
|
||||
self.cursor_x = l[1]
|
||||
self.cursor_index = l[3]
|
||||
if (l[2]-l[1]) > u.width / 2:
|
||||
self.cursor_x += u.width
|
||||
self.cursor_index += 1
|
||||
elif l:
|
||||
changed = True
|
||||
self.cursor_x = l[1]
|
||||
self.cursor_index = l[3]
|
||||
if changed:
|
||||
self.update()
|
||||
return True
|
||||
return False
|
||||
|
||||
def update(self):
|
||||
self.parent.draw()
|
||||
|
||||
def update_text(self, new_text):
|
||||
self.text = new_text
|
||||
if self.is_password:
|
||||
self.tr.set_text("●" * len(new_text))
|
||||
else:
|
||||
self.tr.set_text(new_text)
|
||||
|
||||
def reset_cursor(self):
|
||||
self.cursor_index = 0
|
||||
self.cursor_x = self.tr.get_offset_at_index(self.cursor_index)[1][1]
|
||||
|
||||
def focus_enter(self):
|
||||
if not self.is_focused:
|
||||
self.is_focused = True
|
||||
self.update()
|
||||
|
||||
def focus_leave(self):
|
||||
if self.is_focused:
|
||||
self.is_focused = False
|
||||
self.update()
|
||||
|
||||
def keyboard_event(self, msg):
|
||||
if msg.event.action == yutani.KeyAction.ACTION_DOWN:
|
||||
if self.cursor_x is not None:
|
||||
if msg.event.key == b'\x08':
|
||||
if msg.event.modifiers & yutani.Modifier.MOD_LEFT_CTRL:
|
||||
new_c = self.cursor_index
|
||||
while new_c > 0 and self.text[new_c-1] in self.ctrl_chars:
|
||||
new_c -= 1
|
||||
while new_c > 0 and self.text[new_c-1] not in self.ctrl_chars:
|
||||
new_c -= 1
|
||||
text = self.text
|
||||
before = text[:new_c]
|
||||
after = text[self.cursor_index:]
|
||||
self.update_text(before + after)
|
||||
self.cursor_index = new_c
|
||||
self.cursor_x = self.tr.get_offset_at_index(self.cursor_index)[1][1]
|
||||
if self.text_changed:
|
||||
self.text_changed(self)
|
||||
self.update()
|
||||
else:
|
||||
if self.cursor_index > 0:
|
||||
text = self.text
|
||||
before = text[:self.cursor_index-1]
|
||||
after = text[self.cursor_index:]
|
||||
self.update_text(before + after)
|
||||
self.cursor_index -= 1
|
||||
self.cursor_x = self.tr.get_offset_at_index(self.cursor_index)[1][1]
|
||||
if self.text_changed:
|
||||
self.text_changed(self)
|
||||
self.update()
|
||||
elif msg.event.key == b'\t':
|
||||
if self.tab_handler:
|
||||
self.tab_handler()
|
||||
elif msg.event.keycode == yutani.Keycode.ARROW_LEFT:
|
||||
if msg.event.modifiers & yutani.Modifier.MOD_LEFT_CTRL:
|
||||
while self.cursor_index > 0 and self.text[self.cursor_index-1] in self.ctrl_chars:
|
||||
self.cursor_index -= 1
|
||||
while self.cursor_index > 0 and self.text[self.cursor_index-1] not in self.ctrl_chars:
|
||||
self.cursor_index -= 1
|
||||
self.cursor_x = self.tr.get_offset_at_index(self.cursor_index)[1][1]
|
||||
self.update()
|
||||
else:
|
||||
if self.cursor_index > 0:
|
||||
self.cursor_index -= 1
|
||||
self.cursor_x = self.tr.get_offset_at_index(self.cursor_index)[1][1]
|
||||
self.update()
|
||||
elif msg.event.keycode == yutani.Keycode.ARROW_RIGHT:
|
||||
if msg.event.modifiers & yutani.Modifier.MOD_LEFT_CTRL:
|
||||
while self.cursor_index < len(self.text) and self.text[self.cursor_index] in self.ctrl_chars:
|
||||
self.cursor_index += 1
|
||||
while self.cursor_index < len(self.text) and self.text[self.cursor_index] not in self.ctrl_chars:
|
||||
self.cursor_index += 1
|
||||
self.cursor_x = self.tr.get_offset_at_index(self.cursor_index)[1][1]
|
||||
self.update()
|
||||
else:
|
||||
if self.cursor_index < len(self.text):
|
||||
self.cursor_index += 1
|
||||
self.cursor_x = self.tr.get_offset_at_index(self.cursor_index)[1][1]
|
||||
self.update()
|
||||
elif msg.event.keycode == yutani.Keycode.END:
|
||||
self.cursor_index = len(self.text)
|
||||
self.cursor_x = self.tr.get_offset_at_index(self.cursor_index)[1][1]
|
||||
self.update()
|
||||
elif msg.event.keycode == yutani.Keycode.HOME:
|
||||
self.cursor_index = 0
|
||||
self.cursor_x = self.tr.get_offset_at_index(self.cursor_index)[1][1]
|
||||
self.update()
|
||||
elif msg.event.keycode == yutani.Keycode.DEL:
|
||||
if msg.event.modifiers & yutani.Modifier.MOD_LEFT_CTRL:
|
||||
new_c = self.cursor_index
|
||||
while new_c < len(self.text) and self.text[new_c] in self.ctrl_chars:
|
||||
new_c += 1
|
||||
while new_c < len(self.text) and self.text[new_c] not in self.ctrl_chars:
|
||||
new_c += 1
|
||||
text = self.text
|
||||
before = text[:self.cursor_index]
|
||||
after = text[new_c+1:]
|
||||
self.update_text(before + after)
|
||||
if self.text_changed:
|
||||
self.text_changed(self)
|
||||
self.update()
|
||||
else:
|
||||
if self.cursor_index < len(self.text):
|
||||
text = self.text
|
||||
before = text[:self.cursor_index]
|
||||
after = text[self.cursor_index+1:]
|
||||
self.update_text(before + after)
|
||||
if self.text_changed:
|
||||
self.text_changed(self)
|
||||
self.update()
|
||||
elif msg.event.key == b'\n':
|
||||
if self.submit:
|
||||
self.submit()
|
||||
elif msg.event.key != b'\x00':
|
||||
text = self.text
|
||||
before = text[:self.cursor_index]
|
||||
after = text[self.cursor_index:]
|
||||
self.update_text(before + msg.event.key.decode('utf-8') + after)
|
||||
self.cursor_index += 1
|
||||
self.cursor_x = self.tr.get_offset_at_index(self.cursor_index)[1][1]
|
||||
if self.text_changed:
|
||||
self.text_changed(self)
|
||||
self.update()
|
||||
|
||||
|
||||
class TextInputWindow(yutani.Window):
|
||||
|
||||
base_width = 500
|
||||
base_height = 120
|
||||
|
||||
text_offset = 22
|
||||
|
||||
okay_label = "Okay"
|
||||
cancel_label = "Cancel"
|
||||
|
||||
def __init__(self, decorator, title, icon, text="", text_changed=None, callback=None,window=None,cancel_callback=None):
|
||||
super(TextInputWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=title, icon=icon, doublebuffer=True)
|
||||
if window:
|
||||
# Center window
|
||||
self.move(window.x+int((window.width-self.width)/2),window.y+int((window.height-self.height)/2))
|
||||
else:
|
||||
# Center screen
|
||||
self.move(int((yutani.yutani_ctx._ptr.contents.display_width-self.width)/2),int((yutani.yutani_ctx._ptr.contents.display_height-self.height)/2))
|
||||
self.decorator = decorator
|
||||
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, 13, 0xFF000000)
|
||||
|
||||
# Initialize here
|
||||
self.input = InputBox(text=text)
|
||||
self.input.text_changed = text_changed
|
||||
self.input.submit = self.ok_click
|
||||
self.input.is_focused = True
|
||||
|
||||
self.text_changed = text_changed
|
||||
self.callback = callback
|
||||
self.cancel_callback = cancel_callback
|
||||
|
||||
self.button_ok = Button(self.okay_label,self.ok_click)
|
||||
self.button_cancel = Button(self.cancel_label,self.cancel_click)
|
||||
self.buttons = [self.button_ok, self.button_cancel]
|
||||
|
||||
self.hover_widget = None
|
||||
self.down_button = None
|
||||
|
||||
# For backwards compatibility with old API
|
||||
self.tr = lambda: None
|
||||
|
||||
def text(self):
|
||||
return self.input.text
|
||||
|
||||
|
||||
def cancel_click(self, button):
|
||||
self.close()
|
||||
if __name__ == '__main__':
|
||||
sys.exit(0)
|
||||
if self.cancel_callback:
|
||||
self.cancel_callback()
|
||||
|
||||
def ok_click(self, button=None):
|
||||
self.tr.text = self.text()
|
||||
if self.callback:
|
||||
self.callback(self)
|
||||
|
||||
def draw(self):
|
||||
if self.closed: return
|
||||
|
||||
surface = self.get_cairo_surface()
|
||||
|
||||
WIDTH, HEIGHT = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
ctx = cairo.Context(surface)
|
||||
ctx.translate(self.decorator.left_width(), self.decorator.top_height())
|
||||
ctx.rectangle(0,0,WIDTH,HEIGHT)
|
||||
ctx.set_source_rgb(204/255,204/255,204/255)
|
||||
ctx.fill()
|
||||
|
||||
# XXX draw here
|
||||
ctx.save()
|
||||
ctx.translate(15,self.text_offset)
|
||||
self.input.width = WIDTH-30
|
||||
self.input.draw(self,ctx)
|
||||
ctx.restore()
|
||||
|
||||
self.button_ok.draw(self,ctx,WIDTH-130,HEIGHT-60,100,30)
|
||||
self.button_cancel.draw(self,ctx,WIDTH-240,HEIGHT-60,100,30)
|
||||
|
||||
self.decorator.render(self)
|
||||
self.flip()
|
||||
|
||||
def finish_resize(self, msg):
|
||||
"""Accept a resize."""
|
||||
self.resize_accept(msg.width, msg.height)
|
||||
self.reinit()
|
||||
self.int_width = msg.width - self.decorator.width()
|
||||
self.int_height = msg.height - self.decorator.height()
|
||||
self.draw()
|
||||
self.resize_done()
|
||||
self.flip()
|
||||
|
||||
def mouse_event(self, msg):
|
||||
if self.closed: return
|
||||
if self.decorator.handle_event(msg) == yutani.Decor.EVENT_CLOSE:
|
||||
self.cancel_click(None)
|
||||
x,y = msg.new_x - self.decorator.left_width(), msg.new_y - self.decorator.top_height()
|
||||
w,h = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
redraw = False
|
||||
if self.down_button:
|
||||
if msg.command == yutani.MouseEvent.RAISE or msg.command == yutani.MouseEvent.CLICK:
|
||||
if not (msg.buttons & yutani.MouseButton.BUTTON_LEFT):
|
||||
if x >= self.down_button.x and \
|
||||
x < self.down_button.x + self.down_button.width and \
|
||||
y >= self.down_button.y and \
|
||||
y < self.down_button.y + self.down_button.height:
|
||||
self.down_button.focus_enter()
|
||||
if self.down_button.callback(self.down_button):
|
||||
redraw = True
|
||||
self.down_button = None
|
||||
else:
|
||||
self.down_button.focus_leave()
|
||||
self.down_button = None
|
||||
redraw = True
|
||||
|
||||
else:
|
||||
button = None
|
||||
for b in self.buttons:
|
||||
if x >= b.x and x < b.x + b.width and y >= b.y and y < b.y + b.height:
|
||||
button = b
|
||||
break
|
||||
if button != self.hover_widget:
|
||||
if button:
|
||||
button.focus_enter()
|
||||
redraw = True
|
||||
if self.hover_widget:
|
||||
self.hover_widget.focus_leave()
|
||||
redraw = True
|
||||
self.hover_widget = button
|
||||
|
||||
if msg.command == yutani.MouseEvent.DOWN:
|
||||
if button:
|
||||
button.hilight = 2
|
||||
self.down_button = button
|
||||
redraw = True
|
||||
if not button:
|
||||
if self.hover_widget:
|
||||
self.hover_widget.focus_leave()
|
||||
redraw = True
|
||||
self.hover_widget = None
|
||||
|
||||
if not self.closed:
|
||||
if self.input.mouse_event(msg):
|
||||
changed = True
|
||||
elif msg.command == yutani.MouseEvent.DOWN:
|
||||
changed = self.input.focus_leave()
|
||||
# Do unfocus stuff here
|
||||
else:
|
||||
changed = False
|
||||
|
||||
if changed or redraw:
|
||||
self.draw()
|
||||
|
||||
def keyboard_event(self, msg):
|
||||
if msg.event.action == yutani.KeyAction.ACTION_DOWN:
|
||||
if not self.input.is_focused:
|
||||
self.input.focus_enter()
|
||||
self.input.keyboard_event(msg)
|
||||
# Do keyboard stuff here
|
||||
|
||||
if __name__ == '__main__':
|
||||
yutani.Yutani()
|
||||
d = yutani.Decor()
|
||||
|
||||
title = "Input Box" if len(sys.argv) < 2 else sys.argv[1]
|
||||
icon = "default" if len(sys.argv) < 3 else sys.argv[2]
|
||||
|
||||
def print_text(text_box):
|
||||
print(text_box.text())
|
||||
sys.exit(0)
|
||||
|
||||
window = TextInputWindow(d,title,icon,callback=print_text)
|
||||
window.draw()
|
||||
|
||||
yutani_mainloop.mainloop()
|
||||
|
378
base/home/local/python-demos/mines.py
Executable file
378
base/home/local/python-demos/mines.py
Executable file
@ -0,0 +1,378 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
Minesweeper clone
|
||||
"""
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import cairo
|
||||
|
||||
import yutani
|
||||
import text_region
|
||||
import toaru_fonts
|
||||
|
||||
from button import Button, rounded_rectangle
|
||||
from menu_bar import MenuBarWidget, MenuEntryAction, MenuEntrySubmenu, MenuEntryDivider, MenuWindow
|
||||
from about_applet import AboutAppletWindow
|
||||
from input_box import TextInputWindow
|
||||
from dialog import DialogWindow
|
||||
|
||||
import yutani_mainloop
|
||||
|
||||
version = "1.0.0"
|
||||
app_name = "Mines"
|
||||
_description = f"<b>{app_name} {version}</b>\n© 2017-2018 K. Lange\n\nClassic logic game.\n\n<color 0x0000FF>http://github.com/klange/toaruos</color>"
|
||||
|
||||
class MineButton(Button):
|
||||
|
||||
def __init__(self,action,r,c,is_mine,neighbor_mines):
|
||||
super(MineButton,self).__init__("",action)
|
||||
self.row = r
|
||||
self.col = c
|
||||
self.tr = text_region.TextRegion(0,0,10,10)
|
||||
self.is_mine = is_mine
|
||||
self.tr.set_text("")
|
||||
self.tr.set_alignment(2)
|
||||
self.tr.set_valignment(2)
|
||||
self.width = None
|
||||
self.revealed = False
|
||||
self.mines = neighbor_mines
|
||||
self.flagged = False
|
||||
|
||||
def reveal(self):
|
||||
if self.revealed: return
|
||||
self.revealed = True
|
||||
if self.is_mine:
|
||||
self.tr.set_text("X")
|
||||
elif self.mines == 0:
|
||||
self.tr.set_text("")
|
||||
else:
|
||||
self.tr.set_text(str(self.mines))
|
||||
|
||||
def set_flagged(self):
|
||||
self.flagged = not self.flagged
|
||||
|
||||
def draw(self, window, ctx, x, y, w, h):
|
||||
if self.width != w:
|
||||
self.x, self.y, self.width, self.height = x, y, w, h
|
||||
x_, y_ = ctx.user_to_device(x,y)
|
||||
self.tr.move(int(x_)+2,int(y_)+2)
|
||||
self.tr.resize(w-4,h-4)
|
||||
rounded_rectangle(ctx,x+2,y+2,w-4,h-4,3)
|
||||
if self.revealed:
|
||||
ctx.set_source_rgb(0.6,0.6,0.6)
|
||||
elif self.flagged:
|
||||
ctx.set_source_rgb(0.6,0.1,0.1)
|
||||
elif self.hilight == 1:
|
||||
ctx.set_source_rgb(0.7,0.7,0.7)
|
||||
elif self.hilight == 2:
|
||||
ctx.set_source_rgb(0.3,0.3,0.3)
|
||||
else:
|
||||
ctx.set_source_rgb(1,1,1)
|
||||
ctx.fill()
|
||||
|
||||
if self.tr.text:
|
||||
self.tr.draw(window)
|
||||
|
||||
class MinesWindow(yutani.Window):
|
||||
|
||||
base_width = 400
|
||||
base_height = 440
|
||||
|
||||
def __init__(self, decorator):
|
||||
super(MinesWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=app_name, icon="mines", doublebuffer=True)
|
||||
self.move(100,100)
|
||||
self.decorator = decorator
|
||||
self.button_width = {}
|
||||
self.button_height = 0
|
||||
|
||||
def exit_app(action):
|
||||
menus = [x for x in self.menus.values()]
|
||||
for x in menus:
|
||||
x.definitely_close()
|
||||
self.close()
|
||||
sys.exit(0)
|
||||
def about_window(action):
|
||||
AboutAppletWindow(self.decorator,f"About {app_name}","/usr/share/icons/48/mines.png",_description,"mines")
|
||||
def help_browser(action):
|
||||
subprocess.Popen(["help-browser.py","mines.trt"])
|
||||
def custom_game(action):
|
||||
def input_callback(input_window):
|
||||
size = int(input_window.tr.text)
|
||||
input_window.close()
|
||||
def second_callback(input_window):
|
||||
mines = int(input_window.tr.text)
|
||||
input_window.close()
|
||||
self.new_game((size,mines))
|
||||
|
||||
TextInputWindow(self.decorator,"How many mines?","mines",text="90",callback=second_callback,window=self)
|
||||
TextInputWindow(self.decorator,"How wide/tall?","mines",text="20",callback=input_callback,window=self)
|
||||
|
||||
menus = [
|
||||
("File", [
|
||||
MenuEntrySubmenu("New Game...",[
|
||||
MenuEntryAction("9×9, 10 mines",None,self.new_game,(9,10)),
|
||||
MenuEntryAction("16×16, 40 mines",None,self.new_game,(16,40)),
|
||||
MenuEntryAction("20×20, 90 mines",None,self.new_game,(20,90)),
|
||||
MenuEntryAction("Custom...",None,custom_game,None),
|
||||
],icon="new"),
|
||||
MenuEntryDivider(),
|
||||
MenuEntryAction("Exit","exit",exit_app,None),
|
||||
]),
|
||||
("Help", [
|
||||
MenuEntryAction("Contents","help",help_browser,None),
|
||||
MenuEntryDivider(),
|
||||
MenuEntryAction(f"About {app_name}","star",about_window,None),
|
||||
]),
|
||||
]
|
||||
|
||||
self.menubar = MenuBarWidget(self,menus)
|
||||
|
||||
self.tr = text_region.TextRegion(self.decorator.left_width()+5,self.decorator.top_height()+self.menubar.height,self.base_width-10,40)
|
||||
self.tr.set_font(toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF,18))
|
||||
self.tr.set_alignment(2)
|
||||
self.tr.set_valignment(2)
|
||||
self.tr.set_one_line()
|
||||
self.tr.set_ellipsis()
|
||||
|
||||
|
||||
self.error = False
|
||||
|
||||
self.hover_widget = None
|
||||
self.down_button = None
|
||||
|
||||
self.menus = {}
|
||||
self.hovered_menu = None
|
||||
self.modifiers = 0
|
||||
|
||||
self.new_game((9,10))
|
||||
|
||||
def basic_game(self):
|
||||
self.new_game((9,10))
|
||||
|
||||
def new_game(self,action):
|
||||
def mine_func(b):
|
||||
button = b
|
||||
if self.first_click:
|
||||
i = 0
|
||||
while button.is_mine or button.mines:
|
||||
if i > 30:
|
||||
DialogWindow(self.decorator,app_name,"Failed to generate a board.",callback=self.basic_game,window=self,icon='mines')
|
||||
return
|
||||
self.new_game(action)
|
||||
button = self.buttons[button.row][button.col]
|
||||
i += 1
|
||||
self.first_click = False
|
||||
if button.flagged:
|
||||
return
|
||||
if button.is_mine and not button.revealed:
|
||||
self.tr.set_text("You lose.")
|
||||
for row in self.buttons:
|
||||
for b in row:
|
||||
b.reveal()
|
||||
self.draw()
|
||||
def new():
|
||||
self.new_game(action)
|
||||
DialogWindow(self.decorator,app_name,"Oops, you clicked a mine! Play another game?",callback=new,window=self,icon='mines')
|
||||
return
|
||||
else:
|
||||
if not button.revealed:
|
||||
button.reveal()
|
||||
if button.mines == 0:
|
||||
n = [x for x in check_neighbor_buttons(button.row,button.col) if not x.revealed]
|
||||
while n:
|
||||
b = n.pop()
|
||||
b.reveal()
|
||||
if b.mines == 0:
|
||||
n.extend([x for x in check_neighbor_buttons(b.row,b.col) if not x.revealed and not x in n])
|
||||
self.check_win()
|
||||
|
||||
self.field_size, self.mine_count = action
|
||||
self.first_click = True
|
||||
self.tr.set_text(f"There are {self.mine_count} mines.")
|
||||
|
||||
self.mines = []
|
||||
i = 0
|
||||
while len(self.mines) < self.mine_count:
|
||||
x,y = random.randrange(self.field_size),random.randrange(self.field_size)
|
||||
i += 1
|
||||
if not (x,y) in self.mines:
|
||||
i = 0
|
||||
self.mines.append((x,y))
|
||||
if i > 50:
|
||||
DialogWindow(self.decorator,app_name,"Failed to generate a board.",callback=self.basic_game,window=self,icon='mines')
|
||||
return
|
||||
|
||||
def check_neighbors(r,c):
|
||||
n = []
|
||||
if r > 0:
|
||||
if c > 0: n.append((r-1,c-1))
|
||||
n.append((r-1,c))
|
||||
if c < self.field_size-1: n.append((r-1,c+1))
|
||||
if r < self.field_size-1:
|
||||
if c > 0: n.append((r+1,c-1))
|
||||
n.append((r+1,c))
|
||||
if c < self.field_size-1: n.append((r+1,c+1))
|
||||
if c > 0: n.append((r,c-1))
|
||||
if c < self.field_size-1: n.append((r,c+1))
|
||||
return n
|
||||
|
||||
def check_neighbor_buttons(r,c):
|
||||
return [self.buttons[x][y] for x,y in check_neighbors(r,c)]
|
||||
|
||||
self.buttons = []
|
||||
for row in range(self.field_size):
|
||||
r = []
|
||||
for col in range(self.field_size):
|
||||
is_mine = (row,col) in self.mines
|
||||
neighbor_mines = len([x for x in check_neighbors(row,col) if x in self.mines])
|
||||
r.append(MineButton(mine_func,row,col,is_mine,neighbor_mines))
|
||||
self.buttons.append(r)
|
||||
|
||||
|
||||
def check_win(self):
|
||||
buttons = []
|
||||
for row in self.buttons:
|
||||
buttons.extend(row)
|
||||
n_flagged = len([x for x in buttons if x.flagged and not x.revealed])
|
||||
n_revealed = len([x for x in buttons if x.revealed])
|
||||
if n_flagged == self.mine_count and n_revealed + n_flagged == self.field_size ** 2:
|
||||
self.tr.set_text("You win!")
|
||||
for b in buttons:
|
||||
b.reveal()
|
||||
def new():
|
||||
self.new_game((self.field_size,self.mine_count))
|
||||
DialogWindow(self.decorator,app_name,"You won! Play another game?",callback=new,window=self,icon='mines')
|
||||
|
||||
def flag(self,button):
|
||||
button.set_flagged()
|
||||
self.check_win()
|
||||
self.draw()
|
||||
|
||||
def draw(self):
|
||||
surface = self.get_cairo_surface()
|
||||
|
||||
WIDTH, HEIGHT = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
ctx = cairo.Context(surface)
|
||||
ctx.translate(self.decorator.left_width(), self.decorator.top_height())
|
||||
ctx.rectangle(0,0,WIDTH,HEIGHT)
|
||||
ctx.set_source_rgb(204/255,204/255,204/255)
|
||||
ctx.fill()
|
||||
|
||||
self.tr.resize(WIDTH-10, self.tr.height)
|
||||
self.tr.draw(self)
|
||||
|
||||
offset_x = 0
|
||||
offset_y = self.tr.height + self.menubar.height
|
||||
self.button_height = int((HEIGHT - self.tr.height - self.menubar.height) / len(self.buttons))
|
||||
i = 0
|
||||
for row in self.buttons:
|
||||
self.button_width[i] = int(WIDTH / len(row))
|
||||
for button in row:
|
||||
if button:
|
||||
button.draw(self,ctx,offset_x,offset_y,self.button_width[i],self.button_height)
|
||||
offset_x += self.button_width[i]
|
||||
offset_x = 0
|
||||
offset_y += self.button_height
|
||||
i += 1
|
||||
|
||||
self.menubar.draw(ctx,0,0,WIDTH)
|
||||
self.decorator.render(self)
|
||||
self.flip()
|
||||
|
||||
def finish_resize(self, msg):
|
||||
"""Accept a resize."""
|
||||
if msg.width < 400 or msg.height < 400:
|
||||
self.resize_offer(max(msg.width,400),max(msg.height,400))
|
||||
return
|
||||
self.resize_accept(msg.width, msg.height)
|
||||
self.reinit()
|
||||
self.draw()
|
||||
self.resize_done()
|
||||
self.flip()
|
||||
|
||||
def mouse_event(self, msg):
|
||||
if d.handle_event(msg) == yutani.Decor.EVENT_CLOSE:
|
||||
window.close()
|
||||
sys.exit(0)
|
||||
x,y = msg.new_x - self.decorator.left_width(), msg.new_y - self.decorator.top_height()
|
||||
w,h = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
if x >= 0 and x < w and y >= 0 and y < self.menubar.height:
|
||||
self.menubar.mouse_event(msg, x, y)
|
||||
return
|
||||
|
||||
redraw = False
|
||||
if self.down_button:
|
||||
if msg.command == yutani.MouseEvent.RAISE or msg.command == yutani.MouseEvent.CLICK:
|
||||
if not (msg.buttons & yutani.MouseButton.BUTTON_LEFT):
|
||||
if x >= self.down_button.x and \
|
||||
x < self.down_button.x + self.down_button.width and \
|
||||
y >= self.down_button.y and \
|
||||
y < self.down_button.y + self.down_button.height:
|
||||
self.down_button.focus_enter()
|
||||
if self.modifiers & yutani.Modifier.MOD_LEFT_CTRL:
|
||||
self.flag(self.down_button)
|
||||
else:
|
||||
self.down_button.callback(self.down_button)
|
||||
self.down_button = None
|
||||
redraw = True
|
||||
else:
|
||||
self.down_button.focus_leave()
|
||||
self.down_button = None
|
||||
redraw = True
|
||||
|
||||
else:
|
||||
if y > self.tr.height + self.menubar.height and y < h and x >= 0 and x < w:
|
||||
xh = self.button_height * len(self.buttons)
|
||||
row = int((y - self.tr.height - self.menubar.height) / (xh) * len(self.buttons))
|
||||
if row < len(self.buttons):
|
||||
xw = self.button_width[row] * len(self.buttons[row])
|
||||
col = int(x / (xw) * len(self.buttons[row]))
|
||||
if col < len(self.buttons[row]):
|
||||
button = self.buttons[row][col]
|
||||
else:
|
||||
button = None
|
||||
else:
|
||||
button = None
|
||||
if button != self.hover_widget:
|
||||
if button:
|
||||
button.focus_enter()
|
||||
redraw = True
|
||||
if self.hover_widget:
|
||||
self.hover_widget.focus_leave()
|
||||
redraw = True
|
||||
self.hover_widget = button
|
||||
|
||||
if msg.command == yutani.MouseEvent.DOWN:
|
||||
if button:
|
||||
button.hilight = 2
|
||||
self.down_button = button
|
||||
redraw = True
|
||||
else:
|
||||
if self.hover_widget:
|
||||
self.hover_widget.focus_leave()
|
||||
redraw = True
|
||||
self.hover_widget = None
|
||||
|
||||
if redraw:
|
||||
self.draw()
|
||||
|
||||
def keyboard_event(self, msg):
|
||||
self.modifiers = msg.event.modifiers
|
||||
if msg.event.action != 0x01:
|
||||
return # Ignore anything that isn't a key down.
|
||||
if msg.event.key == b"q":
|
||||
self.close()
|
||||
sys.exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
yutani.Yutani()
|
||||
d = yutani.Decor()
|
||||
|
||||
window = MinesWindow(d)
|
||||
window.draw()
|
||||
|
||||
yutani_mainloop.mainloop()
|
128
base/home/local/python-demos/progress-bar.py
Executable file
128
base/home/local/python-demos/progress-bar.py
Executable file
@ -0,0 +1,128 @@
|
||||
#!/usr/bin/python3
|
||||
"""
|
||||
Shows a progress bar.
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
import cairo
|
||||
|
||||
import yutani
|
||||
import text_region
|
||||
import toaru_fonts
|
||||
import fswait
|
||||
|
||||
import yutani_mainloop
|
||||
|
||||
def rounded_rectangle(ctx,x,y,w,h,r):
|
||||
degrees = math.pi / 180
|
||||
ctx.new_sub_path()
|
||||
|
||||
ctx.arc(x + w - r, y + r, r, -90 * degrees, 0 * degrees)
|
||||
ctx.arc(x + w - r, y + h - r, r, 0 * degrees, 90 * degrees)
|
||||
ctx.arc(x + r, y + h - r, r, 90 * degrees, 180 * degrees)
|
||||
ctx.arc(x + r, y + r, r, 180 * degrees, 270 * degrees)
|
||||
ctx.close_path()
|
||||
|
||||
def draw_progress_bar(ctx,x,y,w,h,percent):
|
||||
# Outer
|
||||
rounded_rectangle(ctx,x,y,w,h,4)
|
||||
ctx.set_source_rgb(192/255,192/255,192/255)
|
||||
ctx.fill()
|
||||
|
||||
# Inner
|
||||
rounded_rectangle(ctx,x+1,y+1,w-2,h-2,4)
|
||||
ctx.set_source_rgb(217/255,217/255,217/255)
|
||||
ctx.fill()
|
||||
|
||||
rounded_rectangle(ctx,x+2,y+2,(w-4) * percent,h-4,4)
|
||||
ctx.set_source_rgb(0,92/255,229/255)
|
||||
ctx.fill()
|
||||
|
||||
class ProgressBarWindow(yutani.Window):
|
||||
|
||||
base_width = 350
|
||||
base_height = 80
|
||||
text_offset = 50
|
||||
|
||||
def __init__(self, decorator, title, icon):
|
||||
super(ProgressBarWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=title, icon=icon, doublebuffer=True)
|
||||
self.move(int((yutani.yutani_ctx._ptr.contents.display_width-self.width)/2),int((yutani.yutani_ctx._ptr.contents.display_height-self.height)/2))
|
||||
self.decorator = decorator
|
||||
self.font = toaru_fonts.Font(toaru_fonts.FONT_SANS_SERIF, 13, 0xFF000000)
|
||||
self.tr = text_region.TextRegion(0,0,self.base_width-30,self.base_height-self.text_offset,font=self.font)
|
||||
self.tr.set_alignment(2)
|
||||
self.progress = 0
|
||||
self.total = 1
|
||||
|
||||
def draw(self):
|
||||
surface = self.get_cairo_surface()
|
||||
|
||||
WIDTH, HEIGHT = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
ctx = cairo.Context(surface)
|
||||
ctx.translate(self.decorator.left_width(), self.decorator.top_height())
|
||||
ctx.rectangle(0,0,WIDTH,HEIGHT)
|
||||
ctx.set_source_rgb(204/255,204/255,204/255)
|
||||
ctx.fill()
|
||||
|
||||
draw_progress_bar(ctx,10,20,WIDTH-20,20,self.progress / self.total)
|
||||
|
||||
percent = int(100 * self.progress / self.total)
|
||||
self.tr.set_text(f"{percent}%")
|
||||
self.tr.resize(WIDTH-30,HEIGHT-self.text_offset)
|
||||
self.tr.move(self.decorator.left_width() + 15,self.decorator.top_height()+self.text_offset)
|
||||
self.tr.draw(self)
|
||||
|
||||
self.decorator.render(self)
|
||||
self.flip()
|
||||
|
||||
def finish_resize(self, msg):
|
||||
"""Accept a resize."""
|
||||
self.resize_accept(msg.width, msg.height)
|
||||
self.reinit()
|
||||
self.int_width = msg.width - self.decorator.width()
|
||||
self.int_height = msg.height - self.decorator.height()
|
||||
self.draw()
|
||||
self.resize_done()
|
||||
self.flip()
|
||||
|
||||
def mouse_event(self, msg):
|
||||
if d.handle_event(msg) == yutani.Decor.EVENT_CLOSE:
|
||||
window.close()
|
||||
sys.exit(0)
|
||||
x,y = msg.new_x - self.decorator.left_width(), msg.new_y - self.decorator.top_height()
|
||||
w,h = self.width - self.decorator.width(), self.height - self.decorator.height()
|
||||
|
||||
def keyboard_event(self, msg):
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
yutani.Yutani()
|
||||
d = yutani.Decor()
|
||||
|
||||
title = "Progress" if len(sys.argv) < 2 else sys.argv[1]
|
||||
icon = "default" if len(sys.argv) < 3 else sys.argv[2]
|
||||
|
||||
window = ProgressBarWindow(d,title,icon)
|
||||
window.draw()
|
||||
|
||||
fds = [yutani.yutani_ctx,sys.stdin]
|
||||
while 1:
|
||||
# Poll for events.
|
||||
fd = fswait.fswait(fds)
|
||||
|
||||
if fd < 0:
|
||||
print("? fswait error.")
|
||||
elif fd == 0:
|
||||
msg = yutani.yutani_ctx.poll()
|
||||
yutani_mainloop.handle_event(msg)
|
||||
elif fd == 1:
|
||||
status = sys.stdin.readline().strip()
|
||||
if status == "done":
|
||||
window.close()
|
||||
break
|
||||
window.progress, window.total = map(int,status.split(" "))
|
||||
window.draw()
|
||||
|
Loading…
Reference in New Issue
Block a user