class PackageManagerWindow(yutani.Window): base_width = 400 base_height = 300 def __init__(self, decorator): super(PackageManagerWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title="Package Manager", icon="package", 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 Package Manager", "/usr/share/icons/48/package.png", _description, "package") def help_browser(action): subprocess.Popen(["help-browser.py", "packages.trt"]) menus = [ ("File", [ MenuEntryAction("Exit", "exit", exit_app, None), ]), ("Help", [ MenuEntryAction("Contents", "help", help_browser, None), MenuEntryDivider(), MenuEntryAction("About Package Manager", "star", about_window, None), ]), ] self.menubar = MenuBarWidget(self, menus) self.menus = {} self.hovered_menu = None self.scroll_y = 0 self.hilighted = None self.buf = None self.hilighted = None def load_packages(self): self.packages = sorted([ Package(name) for name in toaru_package.manifest['packages'].keys() ], key=lambda x: x.name) def redraw_buf(self, clips=None): if self.buf: self.buf.destroy() w = self.width - self.decorator.width() self.buf = yutani.GraphicsBuffer(w, len(self.packages) * 24) surface = self.buf.get_cairo_surface() ctx = cairo.Context(surface) if clips: for clip in clips: ctx.rectangle(clip.x, clip.y, w, 24) ctx.clip() ctx.rectangle(0, 0, surface.get_width(), surface.get_height()) ctx.set_source_rgb(1, 1, 1) ctx.fill() offset_y = 0 for f in self.packages: f.y = offset_y if not clips or f in clips: tr = text_region.TextRegion(4, offset_y + 4, w - 4, 20) if f.hilight: gradient = cairo.LinearGradient(0, 0, 0, 18) 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 + 4, w, 1) ctx.set_source_rgb(*hilight_border_top) ctx.fill() ctx.rectangle(0, offset_y + 4 + 20 - 1, w, 1) ctx.set_source_rgb(*hilight_border_bottom) ctx.fill() ctx.save() ctx.translate(0, offset_y + 4 + 1) ctx.rectangle(0, 0, w, 20 - 2) ctx.set_source(gradient) ctx.fill() ctx.restore() tr.font.font_color = 0xFFFFFFFF else: ctx.rectangle(0, offset_y + 4, w, 20) ctx.set_source_rgb(1, 1, 1) ctx.fill() tr.font.font_color = 0xFF000000 tr.set_richtext(f.text) tr.set_one_line() tr.set_ellipsis() tr.draw(self.buf) offset_y += 24 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() rows = 1000 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 or x >= w or y < 0 or y >= h: 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 if x >= 0 and x < w and y >= self.menubar.height and y < h: if msg.buttons & yutani.MouseButton.BUTTON_RIGHT: if not self.menus: pass # no context menu at the moment #menu_entries = [ # MenuEntryAction("Up",None,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_y = self.scroll_y + self.menubar.height redraw = [] hit = False for f in self.packages: if offset_y > h: break if y >= offset_y and y < offset_y + 24: 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 += 24 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(): redraw = [] self.redraw_buf() self.draw() 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)
class PaintingWindow(yutani.Window): base_width = 600 base_height = 600 def __init__(self, decorator, path): super(PaintingWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=app_name, icon="applications-painting", doublebuffer=True) self.move(100, 100) self.x = 100 self.y = 100 self.decorator = decorator self.picker = None self.last_color = (0, 0, 0) self.modifiers = None self.checkpattern = self.checkerboard(24) def about_window(action): AboutAppletWindow(self.decorator, f"About {app_name}", "/usr/share/icons/48/applications-painting.png", _description, "applications-painting") def help_browser(action): subprocess.Popen(["help-browser.py", "painting.trt"]) def close_picker(): self.last_color = self.picker.color self.picker = None def open_file(action): OpenFileDialog(self.decorator, "Open...", glob="*.png", callback=self.load_buffer, window=self) def new_surface(action): # TODO: prompt for size if self.buf: self.buf.destroy() self.new_buffer(*action) self.draw() def new_prompt(action): def input_callback(input_window): width = int(input_window.tr.text) input_window.close() def second_callback(input_window): height = int(input_window.tr.text) input_window.close() new_surface((width, height)) TextInputWindow(self.decorator, "Height?", "new", text="500", callback=second_callback, window=self) TextInputWindow(self.decorator, "Width?", "new", text="500", callback=input_callback, window=self) def save_file(action): self.modified = False path = '/tmp/painting.png' self.set_title(f'{os.path.basename(path)} - {app_name}', self.icon) self.surface.write_to_png(path) def select_color(action): if self.picker: return else: self.picker = ColorPickerWindow(self.decorator, close_picker) self.picker.draw() def clear_everything(action): self.draw_ctx.save() self.draw_ctx.set_operator(cairo.OPERATOR_SOURCE) self.draw_ctx.rectangle(0, 0, self.surface.get_width(), self.surface.get_height()) self.draw_ctx.set_source_rgba(0, 0, 0, 0) self.draw_ctx.fill() self.draw_ctx.restore() menus = [ ("File", [ MenuEntrySubmenu("New...", [ MenuEntryAction("500×500", "new", new_surface, (500, 500)), MenuEntryAction("800×600", "new", new_surface, (800, 600)), MenuEntryAction("Custom...", "new", new_prompt, None), ], icon="new"), MenuEntryAction("Open", "open", open_file, None), MenuEntryAction("Save", "save", save_file, None), MenuEntryDivider(), MenuEntryAction("Exit", "exit", self.exit_app, None), ]), ("Tools", [ MenuEntryAction("Color", None, select_color, None), MenuEntryAction("Clear Everything", None, clear_everything, None), ]), ("Help", [ MenuEntryAction("Contents", "help", help_browser, None), MenuEntryDivider(), MenuEntryAction(f"About {app_name}", "star", about_window, None), ]), ] self.menubar = MenuBarWidget(self, menus) self.menus = {} self.hovered_menu = None if not path: self.new_buffer(500, 500) else: self.load_buffer(path) self.hilighted = None self.was_drawing = False self.line_width = 2.0 self.curs_x = None self.curs_y = None self.moving = False self.scale = 1.0 self.modified = False def actually_exit(self): menus = [x for x in self.menus.values()] for x in menus: x.definitely_close() self.close() sys.exit(0) def exit_app(self, action=None): if self.modified: DialogWindow( self.decorator, app_name, "You have unsaved changes.\nAre you sure you want to quit?", callback=self.actually_exit, window=self) else: self.actually_exit() def load_buffer(self, path): self.set_title(f'{os.path.basename(path)} - {app_name}', self.icon) s = cairo.ImageSurface.create_from_png(path) self.init_buffer(s.get_width(), s.get_height()) self.draw_ctx.save() self.draw_ctx.set_operator(cairo.OPERATOR_SOURCE) self.draw_ctx.rectangle(0, 0, self.surface.get_width(), self.surface.get_height()) self.draw_ctx.set_source_rgba(0, 0, 0, 0) self.draw_ctx.fill() self.draw_ctx.restore() self.draw_ctx.set_source_surface(s, 0, 0) self.draw_ctx.paint() def init_buffer(self, w, h): self.modified = False self.buf = yutani.GraphicsBuffer(w, h) self.surface = self.buf.get_cairo_surface() self.draw_ctx = cairo.Context(self.surface) self.offset_x = int( (self.width - self.decorator.width() - self.buf.width) / 2) self.offset_y = int((self.height - self.decorator.height() - self.buf.height - self.menubar.height) / 2) def new_buffer(self, w, h): self.init_buffer(w, h) self.set_title(f'Untitled - {app_name}', self.icon) self.draw_ctx.rectangle(0, 0, self.surface.get_width(), self.surface.get_height()) self.draw_ctx.set_source_rgb(1, 1, 1) self.draw_ctx.fill() def color(self): if self.picker: return self.picker.color else: return self.last_color def checkerboard(self, size): s = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size) c = cairo.Context(s) c.set_source_rgb(128 / 255, 128 / 255, 128 / 255) c.paint() c.set_source_rgb(200 / 255, 200 / 255, 200 / 255) c.rectangle(size / 2, 0, size / 2, size / 2) c.rectangle(0, size / 2, size / 2, size / 2) c.fill() return s def draw(self, decor=True, menu=True, clips=None): 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()) if clips: for clip in clips: x, y, a, b = clip width = a - x height = b - y ctx.rectangle(x, y, width, height) ctx.clip() ctx.save() ctx.set_source_surface(self.checkpattern, 0, 0) ctx.get_source().set_filter(cairo.FILTER_NEAREST) ctx.get_source().set_extend(cairo.EXTEND_REPEAT) ctx.rectangle(0, self.menubar.height, WIDTH, HEIGHT - self.menubar.height) ctx.fill() ctx.restore() ctx.save() ctx.translate(0, self.menubar.height) ctx.rectangle(0, 0, WIDTH, HEIGHT - self.menubar.height) ctx.clip() ctx.save() ctx.scale(self.scale, self.scale) ctx.set_source_surface(self.surface, self.offset_x / self.scale, self.offset_y / self.scale) ctx.get_source().set_filter(cairo.FILTER_FAST) ctx.paint() if not self.curs_x is None: if self.scale < 1.0: ctx.set_line_width(0.5 / self.scale) else: ctx.set_line_width(0.5) ctx.arc(self.curs_x / self.scale, self.curs_y / self.scale, self.line_width / 2, 0, 2 * math.pi) ctx.set_source_rgba(0, 0, 0, 0.7) ctx.stroke() ctx.arc(self.curs_x / self.scale, self.curs_y / self.scale, self.line_width / 2 - 0.5, 0, 2 * math.pi) ctx.set_source_rgba(1, 1, 1, 0.7) ctx.stroke() ctx.restore() ctx.restore() # For debugging clip regions. #if clips: # for clip in clips: # x,y,a,b = clip # width = a - x # height = b - y # ctx.rectangle(x,y,width,height) # ctx.set_source_rgba(1.0,0,0,0.4) # ctx.paint() if menu: self.menubar.draw(ctx, 0, 0, WIDTH) if decor: 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.offset_x = int( (self.width - self.decorator.width() - self.buf.width) / 2) self.offset_y = int((self.height - self.decorator.height() - self.buf.height - self.menubar.height) / 2) self.draw() self.resize_done() self.flip() def get_color(self, x, y): c = self.buf.get_value(x, y) a = (c >> 24) & 0xFF r = (c >> 16) & 0xFF g = (c >> 8) & 0xFF b = (c) & 0xFF return (r, g, b) def mouse_event(self, msg): if d.handle_event(msg) == yutani.Decor.EVENT_CLOSE: self.exit_app() 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 not self.was_drawing: 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 or x >= w or y < 0 or y >= h: return if x >= 0 and x < w and y >= self.menubar.height and y < h: if msg.buttons & yutani.MouseButton.BUTTON_RIGHT: if self.picker: _x, _y = x - self.offset_x, y - self.menubar.height - self.offset_y if _x >= 0 and _x < self.surface.get_width( ) and _y >= 0 and _y < self.surface.get_height(): self.picker.set_color(*self.get_color(_x, _y)) if not self.menus: pass # No context menu at the moment. #menu_entries = [ # MenuEntryAction("Up",None,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 decor = False regions = [] if not self.modifiers: if msg.buttons & yutani.MouseButton.SCROLL_UP: self.line_width *= 1.2 elif msg.buttons & yutani.MouseButton.SCROLL_DOWN: self.line_width /= 1.2 elif self.modifiers & yutani.Modifier.MOD_LEFT_CTRL: if msg.buttons & yutani.MouseButton.SCROLL_UP: self.scale += 0.1 regions.append((0, 0, w, h)) elif msg.buttons & yutani.MouseButton.SCROLL_DOWN: self.scale -= 0.1 if self.scale < 0.1: self.scale = 0.1 regions.append((0, 0, w, h)) if not (msg.buttons & yutani.MouseButton.BUTTON_LEFT): self.was_drawing = False if (msg.buttons & yutani.MouseButton.BUTTON_MIDDLE): if not self.moving: self.initial = msg.new_x, msg.new_y self.initial_off = self.offset_x, self.offset_y self.moving = True regions.append((self.offset_x, self.offset_y + self.menubar.height, self.offset_x + int(self.buf.width * self.scale), self.offset_y + self.menubar.height + int(self.buf.height * self.scale))) self.offset_x = self.initial_off[0] + msg.new_x - self.initial[0] self.offset_y = self.initial_off[1] + msg.new_y - self.initial[1] regions.append((self.offset_x, self.offset_y + self.menubar.height, self.offset_x + int(self.buf.width * self.scale), self.offset_y + self.menubar.height + int(self.buf.height * self.scale))) else: self.moving = False cur_x_new = msg.new_x - self.decorator.left_width() cur_y_new = msg.new_y - self.decorator.top_height() cur_x_old = msg.old_x - self.decorator.left_width() cur_y_old = msg.old_y - self.decorator.top_height() if (msg.command == yutani.MouseEvent.DRAG or msg.command == yutani.MouseEvent.DOWN ) and msg.buttons & yutani.MouseButton.BUTTON_LEFT: self.was_drawing = True self.draw_ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.draw_ctx.set_line_join(cairo.LINE_JOIN_ROUND) self.draw_ctx.set_source_rgb(*self.color()) self.draw_ctx.set_line_width(self.line_width) x_1 = 0.5 + (cur_x_new - self.offset_x) / self.scale y_1 = 0.5 + (cur_y_new - self.offset_y - self.menubar.height) / self.scale if msg.command == yutani.MouseEvent.DOWN: x_0 = x_1 y_0 = y_1 regions.append(( int(cur_x_new - self.line_width * self.scale), int(cur_y_new - self.line_width * self.scale), int(cur_x_new + self.line_width * self.scale), int(cur_y_new + self.line_width * self.scale), )) else: x_0 = 0.5 + (cur_x_old - self.offset_x) / self.scale y_0 = 0.5 + (cur_y_old - self.offset_y - self.menubar.height) / self.scale regions.append(( int( min(cur_x_new, cur_x_old) - self.line_width * self.scale), int( min(cur_y_new, cur_y_old) - self.line_width * self.scale), int( max(cur_x_new, cur_x_old) + self.line_width * self.scale), int( max(cur_y_new, cur_y_old) + self.line_width * self.scale), )) self.draw_ctx.move_to(x_0, y_0) self.draw_ctx.line_to(x_1, y_1) self.draw_ctx.stroke() if not self.modified: self.modified = True self.set_title("*" + self.title, self.icon) decor = True else: regions.append(( int(cur_x_new - self.line_width * self.scale), int(cur_y_new - self.line_width * self.scale), int(cur_x_new + self.line_width * self.scale), int(cur_y_new + self.line_width * self.scale), )) if self.curs_x: regions.append(( int(self.curs_x - self.line_width * self.scale - 1.0), int(self.curs_y - self.line_width * self.scale + self.menubar.height - 1.0), int(self.curs_x + self.line_width * self.scale + 1.0), int(self.curs_y + self.line_width * self.scale + self.menubar.height + 1.0), )) self.curs_x = 0.5 + msg.new_x - self.decorator.left_width() self.curs_y = 0.5 + msg.new_y - self.decorator.top_height( ) - self.menubar.height self.draw(menu=False, decor=decor, clips=regions) def keyboard_event(self, msg): self.modifiers = msg.event.modifiers if msg.event.action != yutani.KeyAction.ACTION_DOWN: return # Ignore anything that isn't a key down. if msg.event.key == b"q": self.exit_app()
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)
class HelpBrowserWindow(yutani.Window): base_width = 800 base_height = 600 def __init__(self, decorator): super(HelpBrowserWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=app_name, icon="help", doublebuffer=True) self.move(100, 100) self.decorator = decorator self.current_topic = "0_index.trt" self.text_buffer = None self.text_offset = 0 self.scroll_offset = 0 self.tr = None self.size_changed = False self.special = {} self.special['contents'] = self.special_contents self.special['demo'] = self.special_demo self.down_text = None self.cache = {} self.history = [] self.history_index = 0 def herp(action): print(action) self.history_menu = MenuEntrySubmenu('History...', [MenuEntryDivider()]) 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/help.png", _description, "help") menus = [ ( "File", [ #MenuEntryAction("Open...",None,print_derp,None), #MenuEntryDivider(), MenuEntryAction("Exit", "exit", exit_app, None), ]), ("Go", [ MenuEntryAction("Home", "home", self.go_page, "0_index.trt"), MenuEntryAction("Topics", "bookmark", self.go_page, "special:contents"), MenuEntryDivider(), self.history_menu, MenuEntryAction("Back", "back", self.go_back, None), MenuEntryAction("Forward", "forward", self.go_forward, None), ]), ("Help", [ MenuEntryAction("Contents", "help", self.go_page, "help_browser.trt"), MenuEntryDivider(), MenuEntryAction(f"About {app_name}", "star", about_window, None), ]), ] self.menubar = MenuBarWidget(self, menus) self.menus = {} self.hovered_menu = None self.update_text_buffer() self.navigate("0_index.trt") def get_title(self, document): if document.startswith("special:"): if document[8:] in self.special: return self.special[document[8:]].__doc__ return "???" elif document.startswith("http:"): if document in self.cache: lines = self.cache[document].split('\n') for x in lines: x = x.strip() if x.startswith('<h1>') and x.endswith('</h1>'): return x[4:-5] return document.split('/')[-1].replace('.trt', '').title() else: return document elif document.startswith("file:"): path = document.replace("file:", "") else: path = f'/usr/share/help/{document}' if not os.path.exists(path): return "(file not found)" with open(path, 'r') as f: lines = f.readlines() for x in lines: x = x.strip() if x.startswith('<h1>') and x.endswith('</h1>'): return x[4:-5] return document.replace('.trt', '').title() def special_contents(self): """Table of Contents""" # List all things. output = "\n<h1>Table of Contents</h1>\n\nThis table of contents is automatically generated.\n\n" output += "<h2>Special Pages</h2>\n\n" for k in self.special: output += f"➤ <link target=\"special:{k}\">{self.special[k].__doc__}</link>\n" output += "\n<h2>Documentation</h2>\n\n" for k in sorted(os.listdir('/usr/share/help')): if k.endswith('.trt'): output += f"➤ <link target=\"{k}\">{self.get_title(k)}</link>\n" for directory, _, files in os.walk('/usr/share/help'): if directory == '/usr/share/help': continue files = sorted([x for x in files if not x.startswith('.')]) if files: d = directory.replace('/usr/share/help/', '') output += "\n<h3>" + d.title() + "</h3>\n\n" for k in files: if k.endswith('.trt'): k = d + '/' + k output += f"➤ <link target=\"{k}\">{self.get_title(k)}</link>\n" return output def special_demo(self): """Formatting demo""" return f""" <h1>This is a big header</h1> This is text below that. <h2>This is a medium header</h2> <h3>This is a small header</h3> This is normal text. <b>This is bold text.</b> <i>This is italic text.</i> <b><i>This is both.</i></b> <link target=\"0_index.trt\">go home</link>""" def get_cache(self, url): if url in self.cache: return self.cache[url] else: try: text = subprocess.check_output(['fetch', url]).decode('utf-8') except: text = '\n<h1>Error</h1>\n\nThere was an error obtaining this file.' self.cache[url] = text return text def get_document_text(self): if self.current_topic.startswith("special:"): if self.current_topic[8:] in self.special: return self.special[self.current_topic[8:]]() elif self.current_topic.startswith("http:"): # Good luck return self.get_cache(self.current_topic) elif self.current_topic.startswith("file:"): path = self.current_topic.replace("file:", "") else: path = f'/usr/share/help/{self.current_topic}' if os.path.exists(path): with open(path, 'r') as f: return f.read() return f""" <h1>Document Not Found</h1> Uh oh, looks like the help document you tried to open ({self.current_topic}) wasn't available. Do you want to <link target=\"0_index.trt\">return to the index</link>? You can also <link target=\"special:contents\">check the Table of Contents</link>. """ def update_history(self): def go_history(action): self.navigate(self.history[action], touch_history=False) self.history_index = action self.update_history() entries = [] for x in range(len(self.history)): t = self.get_title(self.history[x]) e = MenuEntryAction(t, None, go_history, x) if x == self.history_index: e.title = f'<b>{t}</b>' e.rich = True e.update_text() entries.append(e) entries.reverse() self.history_menu.entries = entries def navigate(self, target, touch_history=True): if touch_history: del self.history[self.history_index + 1:] self.history.append(target) self.history_index = len(self.history) - 1 self.update_history() self.current_topic = target self.text_offset = 0 self.scroll_offset = 0 self.tr.set_richtext(self.get_document_text()) self.update_text_buffer() self.set_title(f"{self.get_title(self.current_topic)} - {app_name}", "help") def update_text_buffer(self): if self.size_changed or not self.text_buffer: if self.text_buffer: self.text_buffer.destroy() self.text_buffer = yutani.GraphicsBuffer( self.width - self.decorator.width(), self.height - self.decorator.height() + 80 - self.menubar.height) surface = self.text_buffer.get_cairo_surface() ctx = cairo.Context(surface) ctx.rectangle(0, 0, surface.get_width(), surface.get_height()) ctx.set_source_rgb(1, 1, 1) ctx.fill() pad = 10 if not self.tr: self.tr = text_region.TextRegion(pad, 0, surface.get_width() - pad * 2, surface.get_height()) self.tr.set_line_height(18) self.tr.base_dir = '/usr/share/help/' self.tr.set_richtext(self.get_document_text()) elif self.size_changed: self.size_changed = False self.tr.resize(surface.get_width() - pad * 2, surface.get_height() - pad * 2) self.tr.scroll = self.scroll_offset self.tr.draw(self.text_buffer) 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.save() ctx.translate(0, self.menubar.height) text = self.text_buffer.get_cairo_surface() ctx.set_source_surface(text, 0, -self.text_offset) 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 < 100 or msg.height < 100: self.resize_offer(max(msg.width, 100), max(msg.height, 100)) return self.resize_accept(msg.width, msg.height) self.reinit() self.size_changed = True self.update_text_buffer() self.draw() self.resize_done() self.flip() def scroll(self, amount): self.text_offset += amount while self.text_offset < 0: if self.scroll_offset == 0: self.text_offset = 0 else: self.scroll_offset -= 1 self.text_offset += self.tr.line_height while self.text_offset >= self.tr.line_height: self.scroll_offset += 1 self.text_offset -= self.tr.line_height n = (len(self.tr.lines) - self.tr.visible_lines()) + 5 n = n if n >= 0 else 0 if self.scroll_offset >= n: self.scroll_offset = n self.text_offset = 0 self.update_text_buffer() def text_under_cursor(self, msg): """Get the text unit under the cursor.""" x = msg.new_x - self.decorator.left_width() y = msg.new_y - self.decorator.top_height( ) + self.text_offset - self.menubar.height return self.tr.click(x, y) def go_page(self, action): """Navigate to a page.""" self.navigate(action) self.draw() def go_back(self, action): """Go back.""" if self.history and self.history_index > 0: self.history_index -= 1 self.navigate(self.history[self.history_index], touch_history=False) self.update_history() self.draw() def go_forward(self, action): """Go forward.""" if self.history and self.history_index < len(self.history) - 1: self.history_index += 1 self.navigate(self.history[self.history_index], touch_history=False) self.update_history() self.draw() def mouse_event(self, msg): if self.mouse_check(msg): self.draw() def mouse_check(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) if x >= 0 and x < w and y >= self.menubar.height and y < h: if msg.buttons & yutani.MouseButton.BUTTON_RIGHT: if not self.menus: menu_entries = [ MenuEntryAction("Back", "back", self.go_back, None), MenuEntryAction("Forward", "forward", self.go_forward, None), ] menu = MenuWindow(menu_entries, (self.x + msg.new_x, self.y + msg.new_y), root=self) if msg.command == yutani.MouseEvent.DOWN: e = self.text_under_cursor(msg) r = False if self.down_text and e != self.down_text: for u in self.down_text.tag_group: if u.unit_type == 4: u.set_extra('hilight', False) else: u.set_font(self.down_font[u]) del self.down_font self.down_text = None self.update_text_buffer() r = True if e and 'link' in e.extra and e.tag_group: self.down_font = {} for u in e.tag_group: if u.unit_type == 4: u.set_extra('hilight', True) else: new_font = toaru_fonts.Font(u.font.font_number, u.font.font_size, 0xFFFF0000) self.down_font[u] = u.font u.set_font(new_font) self.update_text_buffer() r = True self.down_text = e else: self.down_text = None return r if msg.command == yutani.MouseEvent.CLICK or msg.command == yutani.MouseEvent.RAISE: e = self.text_under_cursor(msg) if self.down_text and e == self.down_text: self.navigate(e.extra['link']) return True elif self.down_text: for u in self.down_text.tag_group: if u.unit_type == 4: u.set_extra('hilight', False) else: u.set_font(self.down_font[u]) del self.down_font self.down_text = None self.update_text_buffer() return True if msg.buttons & yutani.MouseButton.SCROLL_UP: self.scroll(-30) return True elif msg.buttons & yutani.MouseButton.SCROLL_DOWN: self.scroll(30) return True return False def keyboard_event(self, msg): if self.keyboard_check(msg): self.draw() def keyboard_check(self, msg): if msg.event.action != 0x01: return False # Ignore anything that isn't a key down. if msg.event.keycode == yutani.Keycode.HOME: self.text_offset = 0 self.scroll_offset = 0 self.update_text_buffer() return True elif msg.event.keycode == yutani.Keycode.END: n = (len(self.tr.lines) - self.tr.visible_lines()) + 5 self.scroll_offset = n if n >= 0 else 0 self.text_offset = 0 self.update_text_buffer() return True elif msg.event.keycode == yutani.Keycode.PAGE_UP: self.scroll(int(-self.height / 2)) return True elif msg.event.keycode == yutani.Keycode.PAGE_DOWN: self.scroll(int(self.height / 2)) return True elif msg.event.key == b"q": self.close() sys.exit(0)
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)
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)
class PackageManagerWindow(yutani.Window): base_width = 640 base_height = 480 def __init__(self, decorator): super(PackageManagerWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=app_name, icon="package", 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/package.png",_description,"package") def help_browser(action): subprocess.Popen(["help-browser.py","packages.trt"]) def refresh_index(action): global _manifest msk.fetch_manifest() _manifest = msk.get_manifest() self.redraw_buf() self.draw() menus = [ ("File", [ MenuEntryAction("Exit","exit",exit_app,None), ]), ("Index", [ MenuEntryAction("Refresh","refresh",refresh_index,None), ]), ("Help", [ MenuEntryAction("Contents","help",help_browser,None), MenuEntryDivider(), MenuEntryAction(f"About {app_name}","star",about_window,None), ]), ] self.menubar = MenuBarWidget(self,menus) self.menus = {} self.hovered_menu = None self.scroll_y = 0 self.hilighted = None self.buf = None self.hilighted = None def load_packages(self): self.packages = sorted([Package(name) for name in _manifest.keys()],key=lambda x: x.name) def redraw_buf(self,clips=None): if self.buf: self.buf.destroy() w = self.width - self.decorator.width() self.buf = yutani.GraphicsBuffer(w,len(self.packages)*package_height) surface = self.buf.get_cairo_surface() ctx = cairo.Context(surface) if clips: for clip in clips: ctx.rectangle(clip.x,clip.y,w,package_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 button = Button('Install', None) for f in self.packages: f.y = offset_y if not clips or f in clips: tr = text_region.TextRegion(64,offset_y+4,w-64 - 120,package_height-4) tr.line_height = 19 if False and f.hilight: gradient = cairo.LinearGradient(0,0,0,18) 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+4,w,1) ctx.set_source_rgb(*hilight_border_top) ctx.fill() ctx.rectangle(0,offset_y+package_height-1,w,1) ctx.set_source_rgb(*hilight_border_bottom) ctx.fill() ctx.save() ctx.translate(0,offset_y+4+1) ctx.rectangle(0,0,w,package_height-6) ctx.set_source(gradient) ctx.fill() ctx.restore() tr.font.font_color = 0xFFFFFFFF else: ctx.rectangle(0,offset_y+4,w,package_height-4) ctx.set_source_rgb(1,1,1) ctx.fill() tr.font.font_color = 0xFF000000 if f.installed: package_icon = get_icon(f.icon if f.icon else 'package',48,'package') else: if f.hilight: button.hilight = f.hilight else: button.hilight = 0 button.draw(self.buf,ctx,w-110,offset_y+11,100,32) package_icon = get_icon('package-uninstalled',48) ctx.set_source_surface(package_icon,8,11+offset_y) ctx.paint() tr.set_richtext(f.text) tr.set_ellipsis() tr.set_max_lines(3) tr.draw(self.buf) offset_y += package_height 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() - self.menubar.height self.scroll_y += amount if self.scroll_y > 0: self.scroll_y = 0 max_scroll = self.buf.height - h if h < self.buf.height else 0 if self.scroll_y < -max_scroll: self.scroll_y = -max_scroll 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 or x >= w or y < 0 or y >= h: 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: pass # no context menu at the moment #menu_entries = [ # MenuEntryAction("Up",None,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_y = self.scroll_y + self.menubar.height redraw = [] hit = False for f in self.packages: if offset_y > h: break if y >= offset_y + 11 and y < offset_y + 11 + 32 and x >= w - 110 and x <= w - 10: # button 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 += package_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: self.hilighted.hilight = 2 redraw.append(self.hilighted) if msg.command == yutani.MouseEvent.RAISE or msg.command == yutani.MouseEvent.CLICK: if self.hilighted.do_action(): redraw = [] self.redraw_buf() self.draw() 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) elif msg.event.keycode == yutani.Keycode.PAGE_UP: h = self.height - self.decorator.height() - self.menubar.height self.scroll(int(h/2)) self.draw() elif msg.event.keycode == yutani.Keycode.PAGE_DOWN: h = self.height - self.decorator.height() - self.menubar.height self.scroll(-int(h/2)) self.draw() elif msg.event.keycode == yutani.Keycode.HOME: self.scroll_y = 0 self.draw() elif msg.event.keycode == yutani.Keycode.END: h = self.height - self.decorator.height() - self.menubar.height self.scroll_y = -(self.buf.height - h if h < self.buf.height else 0) self.draw()
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)
class PaintingWindow(yutani.Window): base_width = 600 base_height = 600 def __init__(self, decorator, path): super(PaintingWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=app_name, icon="applications-painting", doublebuffer=True) self.move(100,100) self.x = 100 self.y = 100 self.decorator = decorator self.picker = None self.last_color = (0,0,0) self.modifiers = None self.checkpattern = self.checkerboard(24) def about_window(action): AboutAppletWindow(self.decorator,f"About {app_name}","/usr/share/icons/48/applications-painting.png",_description,"applications-painting") def help_browser(action): subprocess.Popen(["help-browser.py","painting.trt"]) def close_picker(): self.last_color = self.picker.color self.picker = None def open_file(action): OpenFileDialog(self.decorator,"Open...",glob="*.png",callback=self.load_buffer,window=self) def new_surface(action): # TODO: prompt for size if self.buf: self.buf.destroy() self.new_buffer(*action) self.draw() def new_prompt(action): def input_callback(input_window): width = int(input_window.tr.text) input_window.close() def second_callback(input_window): height = int(input_window.tr.text) input_window.close() new_surface((width,height)) TextInputWindow(self.decorator,"Height?","new",text="500",callback=second_callback,window=self) TextInputWindow(self.decorator,"Width?","new",text="500",callback=input_callback,window=self) def save_file(action): self.modified = False path = '/tmp/painting.png' self.set_title(f'{os.path.basename(path)} - {app_name}',self.icon) self.surface.write_to_png(path) def select_color(action): if self.picker: return else: self.picker = ColorPickerWindow(self.decorator, close_picker) self.picker.draw() def clear_everything(action): self.draw_ctx.save() self.draw_ctx.set_operator(cairo.OPERATOR_SOURCE) self.draw_ctx.rectangle(0,0,self.surface.get_width(),self.surface.get_height()) self.draw_ctx.set_source_rgba(0,0,0,0) self.draw_ctx.fill() self.draw_ctx.restore() menus = [ ("File", [ MenuEntrySubmenu("New...",[ MenuEntryAction("500×500","new",new_surface,(500,500)), MenuEntryAction("800×600","new",new_surface,(800,600)), MenuEntryAction("Custom...","new",new_prompt,None), ],icon="new"), MenuEntryAction("Open","open",open_file,None), MenuEntryAction("Save","save",save_file,None), MenuEntryDivider(), MenuEntryAction("Exit","exit",self.exit_app,None), ]), ("Tools", [ MenuEntryAction("Color",None,select_color,None), MenuEntryAction("Clear Everything",None,clear_everything,None), ]), ("Help", [ MenuEntryAction("Contents","help",help_browser,None), MenuEntryDivider(), MenuEntryAction(f"About {app_name}","star",about_window,None), ]), ] self.menubar = MenuBarWidget(self,menus) self.menus = {} self.hovered_menu = None if not path: self.new_buffer(500,500) else: self.load_buffer(path) self.hilighted = None self.was_drawing = False self.line_width = 2.0 self.curs_x = None self.curs_y = None self.moving = False self.scale = 1.0 self.modified = False def actually_exit(self): menus = [x for x in self.menus.values()] for x in menus: x.definitely_close() self.close() sys.exit(0) def exit_app(self, action=None): if self.modified: DialogWindow(self.decorator,app_name,"You have unsaved changes.\nAre you sure you want to quit?",callback=self.actually_exit,window=self) else: self.actually_exit() def load_buffer(self,path): self.set_title(f'{os.path.basename(path)} - {app_name}',self.icon) s = cairo.ImageSurface.create_from_png(path) self.init_buffer(s.get_width(),s.get_height()) self.draw_ctx.save() self.draw_ctx.set_operator(cairo.OPERATOR_SOURCE) self.draw_ctx.rectangle(0,0,self.surface.get_width(),self.surface.get_height()) self.draw_ctx.set_source_rgba(0,0,0,0) self.draw_ctx.fill() self.draw_ctx.restore() self.draw_ctx.set_source_surface(s,0,0) self.draw_ctx.paint() def init_buffer(self,w,h): self.modified = False self.buf = yutani.GraphicsBuffer(w,h) self.surface = self.buf.get_cairo_surface() self.draw_ctx = cairo.Context(self.surface) self.offset_x = int((self.width-self.decorator.width()-self.buf.width)/2) self.offset_y = int((self.height-self.decorator.height()-self.buf.height-self.menubar.height)/2) def new_buffer(self,w,h): self.init_buffer(w,h) self.set_title(f'Untitled - {app_name}', self.icon) self.draw_ctx.rectangle(0,0,self.surface.get_width(),self.surface.get_height()) self.draw_ctx.set_source_rgb(1,1,1) self.draw_ctx.fill() def color(self): if self.picker: return self.picker.color else: return self.last_color def checkerboard(self,size): s = cairo.ImageSurface(cairo.FORMAT_ARGB32,size,size) c = cairo.Context(s) c.set_source_rgb(128/255,128/255,128/255) c.paint() c.set_source_rgb(200/255,200/255,200/255) c.rectangle(size/2,0,size/2,size/2) c.rectangle(0,size/2,size/2,size/2) c.fill() return s def draw(self,decor=True,menu=True,clips=None): 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()) if clips: for clip in clips: x,y,a,b = clip width = a - x height = b - y ctx.rectangle(x,y,width,height) ctx.clip() ctx.save() ctx.set_source_surface(self.checkpattern,0,0) ctx.get_source().set_filter(cairo.FILTER_NEAREST) ctx.get_source().set_extend(cairo.EXTEND_REPEAT) ctx.rectangle(0,self.menubar.height,WIDTH,HEIGHT-self.menubar.height) ctx.fill() ctx.restore() ctx.save() ctx.translate(0,self.menubar.height) ctx.rectangle(0,0,WIDTH,HEIGHT-self.menubar.height) ctx.clip() ctx.save() ctx.scale(self.scale,self.scale) ctx.set_source_surface(self.surface,self.offset_x/self.scale,self.offset_y/self.scale) ctx.get_source().set_filter(cairo.FILTER_FAST) ctx.paint() if not self.curs_x is None: if self.scale < 1.0: ctx.set_line_width(0.5/self.scale) else: ctx.set_line_width(0.5) ctx.arc(self.curs_x/self.scale,self.curs_y/self.scale,self.line_width/2,0,2*math.pi) ctx.set_source_rgba(0,0,0,0.7) ctx.stroke() ctx.arc(self.curs_x/self.scale,self.curs_y/self.scale,self.line_width/2-0.5,0,2*math.pi) ctx.set_source_rgba(1,1,1,0.7) ctx.stroke() ctx.restore() ctx.restore() # For debugging clip regions. #if clips: # for clip in clips: # x,y,a,b = clip # width = a - x # height = b - y # ctx.rectangle(x,y,width,height) # ctx.set_source_rgba(1.0,0,0,0.4) # ctx.paint() if menu: self.menubar.draw(ctx,0,0,WIDTH) if decor: 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.offset_x = int((self.width-self.decorator.width()-self.buf.width)/2) self.offset_y = int((self.height-self.decorator.height()-self.buf.height-self.menubar.height)/2) self.draw() self.resize_done() self.flip() def get_color(self,x,y): c = self.buf.get_value(x,y) a = (c >> 24) & 0xFF r = (c >> 16) & 0xFF g = (c >> 8) & 0xFF b = (c) & 0xFF return (r,g,b) def mouse_event(self, msg): if d.handle_event(msg) == yutani.Decor.EVENT_CLOSE: self.exit_app() 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 not self.was_drawing: 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 or x >= w or y < 0 or y >= h: return if x >= 0 and x < w and y >= self.menubar.height and y < h: if msg.buttons & yutani.MouseButton.BUTTON_RIGHT: if self.picker: _x,_y = x-self.offset_x,y-self.menubar.height-self.offset_y if _x >= 0 and _x < self.surface.get_width() and _y >= 0 and _y < self.surface.get_height(): self.picker.set_color(*self.get_color(_x,_y)) if not self.menus: pass # No context menu at the moment. #menu_entries = [ # MenuEntryAction("Up",None,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 decor = False regions = [] if not self.modifiers: if msg.buttons & yutani.MouseButton.SCROLL_UP: self.line_width *= 1.2 elif msg.buttons & yutani.MouseButton.SCROLL_DOWN: self.line_width /= 1.2 elif self.modifiers & yutani.Modifier.MOD_LEFT_CTRL: if msg.buttons & yutani.MouseButton.SCROLL_UP: self.scale += 0.1 regions.append((0,0,w,h)) elif msg.buttons & yutani.MouseButton.SCROLL_DOWN: self.scale -= 0.1 if self.scale < 0.1: self.scale = 0.1 regions.append((0,0,w,h)) if not (msg.buttons & yutani.MouseButton.BUTTON_LEFT): self.was_drawing = False if (msg.buttons & yutani.MouseButton.BUTTON_MIDDLE): if not self.moving: self.initial = msg.new_x, msg.new_y self.initial_off = self.offset_x, self.offset_y self.moving = True regions.append( ( self.offset_x, self.offset_y+self.menubar.height, self.offset_x+int(self.buf.width*self.scale), self.offset_y+self.menubar.height+int(self.buf.height*self.scale) ) ) self.offset_x = self.initial_off[0] + msg.new_x - self.initial[0] self.offset_y = self.initial_off[1] + msg.new_y - self.initial[1] regions.append( ( self.offset_x, self.offset_y+self.menubar.height, self.offset_x+int(self.buf.width*self.scale), self.offset_y+self.menubar.height+int(self.buf.height*self.scale) ) ) else: self.moving = False cur_x_new = msg.new_x - self.decorator.left_width() cur_y_new = msg.new_y - self.decorator.top_height() cur_x_old = msg.old_x - self.decorator.left_width() cur_y_old = msg.old_y - self.decorator.top_height() if (msg.command == yutani.MouseEvent.DRAG or msg.command == yutani.MouseEvent.DOWN) and msg.buttons & yutani.MouseButton.BUTTON_LEFT: self.was_drawing = True self.draw_ctx.set_line_cap(cairo.LINE_CAP_ROUND) self.draw_ctx.set_line_join(cairo.LINE_JOIN_ROUND) self.draw_ctx.set_source_rgb(*self.color()) self.draw_ctx.set_line_width(self.line_width) x_1 = 0.5 + (cur_x_new - self.offset_x) / self.scale y_1 = 0.5 + (cur_y_new - self.offset_y - self.menubar.height) / self.scale if msg.command == yutani.MouseEvent.DOWN: x_0 = x_1 y_0 = y_1 regions.append( ( int(cur_x_new - self.line_width * self.scale), int(cur_y_new - self.line_width * self.scale), int(cur_x_new + self.line_width * self.scale), int(cur_y_new + self.line_width * self.scale), ) ) else: x_0 = 0.5 + (cur_x_old - self.offset_x) / self.scale y_0 = 0.5 + (cur_y_old - self.offset_y - self.menubar.height) / self.scale regions.append( ( int(min(cur_x_new,cur_x_old) - self.line_width * self.scale), int(min(cur_y_new,cur_y_old) - self.line_width * self.scale), int(max(cur_x_new,cur_x_old) + self.line_width * self.scale), int(max(cur_y_new,cur_y_old) + self.line_width * self.scale), ) ) self.draw_ctx.move_to(x_0,y_0) self.draw_ctx.line_to(x_1,y_1) self.draw_ctx.stroke() if not self.modified: self.modified = True self.set_title("*" + self.title, self.icon) decor = True else: regions.append( ( int(cur_x_new - self.line_width * self.scale), int(cur_y_new - self.line_width * self.scale), int(cur_x_new + self.line_width * self.scale), int(cur_y_new + self.line_width * self.scale), ) ) if self.curs_x: regions.append( ( int(self.curs_x - self.line_width * self.scale - 1.0), int(self.curs_y - self.line_width * self.scale + self.menubar.height - 1.0), int(self.curs_x + self.line_width * self.scale + 1.0), int(self.curs_y + self.line_width * self.scale + self.menubar.height + 1.0), ) ) self.curs_x = 0.5+msg.new_x - self.decorator.left_width() self.curs_y = 0.5+msg.new_y - self.decorator.top_height() - self.menubar.height self.draw(menu=False,decor=decor,clips=regions) def keyboard_event(self, msg): self.modifiers = msg.event.modifiers if msg.event.action != yutani.KeyAction.ACTION_DOWN: return # Ignore anything that isn't a key down. if msg.event.key == b"q": self.exit_app()
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 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 x >= 0 and x < w and y >= self.menubar.height and y < h: 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)
class HelpBrowserWindow(yutani.Window): base_width = 800 base_height = 600 def __init__(self, decorator): super(HelpBrowserWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=app_name, icon="help", doublebuffer=True) self.move(100,100) self.x = 100 self.y = 100 self.decorator = decorator self.current_topic = "0_index.trt" self.text_buffer = None self.text_offset = 0 self.tr = None self.size_changed = True self.text_scroller = ScrollableText() self.special = {} self.special['contents'] = self.special_contents self.special['demo'] = self.special_demo self.down_text = None self.cache = {} self.history = [] self.history_index = 0 self.title_cache = {} def herp(action): print(action) self.history_menu = MenuEntrySubmenu('History...',[MenuEntryDivider()]) 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/help.png",_description,"help") menus = [ ("File", [ #MenuEntryAction("Open...",None,print_derp,None), #MenuEntryDivider(), MenuEntryAction("Exit","exit",exit_app,None), ]), ("Go", [ MenuEntryAction("Home","home",self.go_page,"0_index.trt"), MenuEntryAction("Topics","bookmark",self.go_page,"special:contents"), MenuEntryDivider(), self.history_menu, MenuEntryAction("Back","back",self.go_back,None), MenuEntryAction("Forward","forward",self.go_forward,None), ]), ("Help", [ MenuEntryAction("Contents","help",self.go_page,"help_browser.trt"), MenuEntryDivider(), MenuEntryAction(f"About {app_name}","star",about_window,None), ]), ] self.menubar = MenuBarWidget(self,menus) self.menus = {} self.hovered_menu = None self.update_text_buffer() self.navigate("0_index.trt") def get_title(self, document): if document.startswith("special:"): if document[8:] in self.special: return self.special[document[8:]].__doc__ return "???" elif document.startswith("http:") or document.startswith('https:'): if document in self.title_cache: return self.title_cache[document] if document in self.cache: lines = self.cache[document].split('\n') for x in lines: x = x.strip() if x.startswith('<h1>') and x.endswith('</h1>'): return x[4:-5] return document.split('/')[-1].replace('.trt','').title() else: return document elif document.startswith("file:"): path = document.replace("file:","") else: path = f'/usr/share/help/{document}' if not os.path.exists(path): return "(file not found)" with open(path,'r') as f: lines = f.readlines() for x in lines: x = x.strip() if x.startswith('<h1>') and x.endswith('</h1>'): return x[4:-5] return document.replace('.trt','').title() def special_contents(self): """Table of Contents""" # List all things. output = "\n<h1>Table of Contents</h1>\n\nThis table of contents is automatically generated.\n\n" output += "<h2>Special Pages</h2>\n\n" for k in self.special: output += f"➤ <link target=\"special:{k}\">{self.special[k].__doc__}</link>\n" output += "\n<h2>Documentation</h2>\n\n" for k in sorted(os.listdir('/usr/share/help')): if k.endswith('.trt'): output += f"➤ <link target=\"{k}\">{self.get_title(k)}</link>\n" for directory,_,files in os.walk('/usr/share/help'): if directory == '/usr/share/help': continue files = sorted([x for x in files if not x.startswith('.')]) if files: d = directory.replace('/usr/share/help/','') output += "\n<h3>" + d.title() + "</h3>\n\n" for k in files: if k.endswith('.trt'): k = d + '/' + k output += f"➤ <link target=\"{k}\">{self.get_title(k)}</link>\n" return output def special_demo(self): """Formatting demo""" return f""" <h1>This is a big header</h1> This is text below that. <h2>This is a medium header</h2> <h3>This is a small header</h3> This is normal text. <b>This is bold text.</b> <i>This is italic text.</i> <b><i>This is both.</i></b> <link target=\"0_index.trt\">go home</link>""" def get_cache(self, url): if url in self.cache: return self.cache[url] else: try: text = subprocess.check_output(['fetch',url]) if text.startswith(b'\x89PNG'): text = f"<html><body><img src=\"{url}\"></body></html>" else: text = text.decode('utf-8') except: text = '\n<h1>Error</h1>\n\nThere was an error obtaining this file.' self.cache[url] = text return text def get_document_text(self): if self.current_topic.startswith("special:"): if self.current_topic[8:] in self.special: return self.special[self.current_topic[8:]]() elif self.current_topic.startswith("http:") or self.current_topic.startswith('https:'): # Good luck return self.get_cache(self.current_topic) elif self.current_topic.startswith("file:"): path = self.current_topic.replace("file:","") else: path = f'/usr/share/help/{self.current_topic}' if os.path.exists(path): with open(path,'r') as f: return f.read() return f""" <h1>Document Not Found</h1> Uh oh, looks like the help document you tried to open ({self.current_topic}) wasn't available. Do you want to <link target=\"0_index.trt\">return to the index</link>? You can also <link target=\"special:contents\">check the Table of Contents</link>. """ def is_html(self): if self.current_topic.endswith('.html') or self.current_topic.endswith('.htm'): return True if self.current_topic.startswith('http') and not self.current_topic.endswith('.trt'): return True if '<html' in self.get_document_text(): return True return False def update_history(self): def go_history(action): self.navigate(self.history[action],touch_history=False) self.history_index = action self.update_history() entries = [] for x in range(len(self.history)): t = self.get_title(self.history[x]) e = MenuEntryAction(t,None,go_history,x) if x == self.history_index: e.title = f'<b>{t}</b>' e.rich = True e.update_text() entries.append(e) entries.reverse() self.history_menu.entries = entries def navigate(self, target, touch_history=True): #if target.startswith('https:'): # DialogWindow(self.decorator,app_name,f"<mono>https</mono> is not supported. Could not load the URL <mono>{target}</mono>",callback=lambda: None,window=self,cancel_label=False) # return if touch_history: del self.history[self.history_index+1:] self.history.append(target) self.history_index = len(self.history)-1 self.current_topic = target self.text_offset = 0 if self.is_html(): self.tr.base_dir = os.path.dirname(target) + '/' else: self.tr.base_dir = '/usr/share/help/' self.tr.set_richtext(self.get_document_text(),html=self.is_html()) self.update_text_buffer() if self.tr.title: self.set_title(f"{self.tr.title} - {app_name}","help") self.title_cache[target] = self.tr.title else: self.set_title(f"{self.get_title(self.current_topic)} - {app_name}","help") self.update_history() def update_text_buffer(self): if not self.tr: self.tr = text_region.TextRegion(0,0,100,100) self.tr.set_line_height(18) self.tr.base_dir = '/usr/share/help/' self.tr.set_richtext(self.get_document_text(),html=self.is_html()) self.text_scroller.tr = self.tr if self.size_changed: self.text_scroller.update(self.width - self.decorator.width()) #self.tr.scroll = self.scroll_offset #self.tr.draw(self.text_buffer) 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.set_source_rgb(1,1,1) ctx.fill() ctx.save() ctx.translate(0,self.menubar.height) """ text = self.text_buffer.get_cairo_surface() ctx.set_source_surface(text,0,-self.text_offset) ctx.paint() """ self.text_scroller.draw(ctx,0,0,HEIGHT-self.menubar.height,self.text_offset) 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 < 100 or msg.height < 100: self.resize_offer(max(msg.width,100),max(msg.height,100)) return self.resize_accept(msg.width, msg.height) self.reinit() self.size_changed = True self.update_text_buffer() self.draw() self.resize_done() self.flip() def scroll(self, amount): self.text_offset += amount if self.text_offset < 0: self.text_offset = 0 if self.text_offset > self.text_scroller.scroll_max(): self.text_offset = self.text_scroller.scroll_max() def text_under_cursor(self, msg): """Get the text unit under the cursor.""" x = msg.new_x - self.decorator.left_width() y = msg.new_y - self.decorator.top_height() + self.text_offset - self.menubar.height return self.tr.click(x,y) def go_page(self, action): """Navigate to a page.""" self.navigate(action) self.draw() def go_back(self,action): """Go back.""" if self.history and self.history_index > 0: self.history_index -= 1 self.navigate(self.history[self.history_index], touch_history=False) self.update_history() self.draw() def go_forward(self,action): """Go forward.""" if self.history and self.history_index < len(self.history)-1: self.history_index += 1 self.navigate(self.history[self.history_index], touch_history=False) self.update_history() self.draw() def mouse_event(self, msg): if self.mouse_check(msg): self.draw() def mouse_check(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) if x >= 0 and x < w and y >= self.menubar.height and y < h: if msg.buttons & yutani.MouseButton.BUTTON_RIGHT: if not self.menus: menu_entries = [ MenuEntryAction("Back","back",self.go_back,None), MenuEntryAction("Forward","forward",self.go_forward,None), ] menu = MenuWindow(menu_entries,(self.x+msg.new_x,self.y+msg.new_y),root=self) if msg.buttons & yutani.MouseButton.SCROLL_UP: self.scroll(-30) return True elif msg.buttons & yutani.MouseButton.SCROLL_DOWN: self.scroll(30) return True if msg.command == yutani.MouseEvent.DOWN: e = self.text_under_cursor(msg) r = False if self.down_text and e != self.down_text: for u in self.down_text.tag_group: if u.unit_type == 4: u.set_extra('hilight',False) else: u.set_font(self.down_font[u]) del self.down_font self.down_text = None self.update_text_buffer() r = True if e and 'link' in e.extra and e.tag_group: self.down_font = {} for u in e.tag_group: if u.unit_type == 4: u.set_extra('hilight',True) else: new_font = toaru_fonts.Font(u.font.font_number,u.font.font_size,0xFFFF0000) self.down_font[u] = u.font u.set_font(new_font) self.update_text_buffer() r = True self.down_text = e else: self.down_text = None return r if msg.command == yutani.MouseEvent.CLICK or msg.command == yutani.MouseEvent.RAISE: e = self.text_under_cursor(msg) if self.down_text and e == self.down_text: self.navigate(e.extra['link']) return True elif self.down_text: for u in self.down_text.tag_group: if u.unit_type == 4: u.set_extra('hilight',False) else: u.set_font(self.down_font[u]) del self.down_font self.down_text = None self.update_text_buffer() return True return False def keyboard_event(self, msg): if self.keyboard_check(msg): self.draw() def keyboard_check(self,msg): if msg.event.action != 0x01: return False # Ignore anything that isn't a key down. if msg.event.keycode == yutani.Keycode.HOME: self.text_offset = 0 return True elif msg.event.keycode == yutani.Keycode.END: n = (len(self.tr.lines)-self.tr.visible_lines())+5 self.text_offset = self.text_scroller.scroll_max() return True elif msg.event.keycode == yutani.Keycode.PAGE_UP: self.scroll(int(-self.height/2)) return True elif msg.event.keycode == yutani.Keycode.PAGE_DOWN: self.scroll(int(self.height/2)) return True elif msg.event.key == b"q": self.close() sys.exit(0)
class PDFViewerWindow(yutani.Window): base_width = 640 base_height = 480 def __init__(self, decorator, path): super(PDFViewerWindow, self).__init__(self.base_width + decorator.width(), self.base_height + decorator.height(), title=app_name, icon="pdfviewer", doublebuffer=True) self.move(100,100) self.x = 100 self.y = 100 self.decorator = decorator self.fitz_lib = ctypes.CDLL('libtoaru-fitz.so') self.fitz_lib.init_fitz.restype = ctypes.c_void_p self.fitz_lib.load_document.argtypes = [ctypes.c_void_p, ctypes.c_char_p] self.fitz_lib.load_document.restype = ctypes.c_void_p self.fitz_lib.set_fit.argtypes = [ctypes.c_int] self.fitz_lib.draw_page.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_int, ctypes.c_void_p] self.fitz_lib.page_count.argtypes = [ctypes.c_void_p] self.fitz_lib.page_count.restype = ctypes.c_int def open_file(action): OpenFileDialog(self.decorator,"Open PDF...",glob="*.pdf",callback=self.load_file,window=self) 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/external/pdfviewer.png",_description,"pdfviewer") def help_browser(action): subprocess.Popen(["help-browser.py","downloaded/mupdf.trt"]) menus = [ ("File", [ MenuEntryAction("Open","open",open_file,None), 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.hover_widget = None self.down_button = None self.menus = {} self.hovered_menu = None self.buf = None self.load_file(path) self.hilighted = None def load_file(self, path): if not path or not os.path.exists(path): self.document = None self.redraw_buf(True) self.page = 1 self.page_count = 1 else: self.fitz_ctx = self.fitz_lib.init_fitz() self.path = path self.document = self.fitz_lib.load_document(self.fitz_ctx, path.encode('utf-8')) self.page = 1 self.page_count = self.fitz_lib.page_count(self.document) self.redraw_buf(True) def go_up(self, action): pass def redraw_buf(self,size_changed=False): if size_changed: if self.buf: self.buf.destroy() w = self.width - self.decorator.width() h = self.height - self.decorator.height() self.buf = yutani.GraphicsBuffer(w,h) yutani.Window.fill(self.buf,0xFF777777) if self.document: self.fitz_lib.draw_page(self.fitz_ctx, self.document, self.page, self.buf._gfx) self.set_title(f"{self.path} - {self.page}/{self.page_count} - {app_name}","pdfviewer") else: self.set_title(app_name,"pdfviewer") 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,0) 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(True) self.draw() self.resize_done() self.flip() def scroll(self, amount): self.page += amount if self.page < 1: self.page = 1 if self.page > self.page_count: self.page = self.page_count self.redraw_buf(False) 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 msg.buttons & yutani.MouseButton.SCROLL_UP: self.scroll(-1) self.draw() return elif msg.buttons & yutani.MouseButton.SCROLL_DOWN: self.scroll(1) self.draw() return 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)