from textual.app import App, ComposeResult from textual.widgets import Footer, Tab, Tabs, Header from textual import on, events from textual_fspicker import FileOpen, FileSave, Filters from ui.widgets.sidebar import Sidebar from ui.widgets.timeline import Timeline, TimelineRow from ui.widgets.project_settings import ProjectSettings from ui.widgets.channel import Channel from ui.widgets.context_menu import ContextMenu, NoSelectStatic from ui.widgets.chunk_types.audio import AudioChunk, Chunk from ui.screens.settings import SettingsScreen from ui.screens.license_screen import LicenseScreen from project import ProjectChannel, Project, ChunkType from settings_store import ConfigHandler import webbrowser class AppUI(App): CSS_PATH = "../assets/style.tcss" def __init__(self, project): super().__init__() self.zoom_level = 0.05 self.last_zoom_level = self.zoom_level self.project = project self.config_handler = ConfigHandler(self) self.open_project_path = None self.first_tab_click = True # stupid events firing when the app is first composed :/ @on(events.Key) async def key_pressed(self, event: events.Key): if event.key == "space": timeline = self.query_one(Timeline) if not timeline.song_player.paused: timeline.song_player.pause() else: timeline.song_player.play_project(self.app.project) def create_channel(self, name: str): new_project_channel = ProjectChannel( name ) self.query_one("#channels").mount(Channel( len(self.project.channels), new_project_channel, name, ), before=-1) self.query_one("#rows").mount(TimelineRow(new_project_channel)) self.project.channels.append(new_project_channel) def handle_menu_click(self, choice: str): # user clicked off the menu if choice == None: return match choice: # File menu case "Save": if not self.open_project_path: self.handle_menu_click("Save as") # just move it to save as return self.project.write_to_file(self.open_project_path) self.notify("Saved project.") case "Save as": def callback(path: str): if path == None: return self.open_project_path = path self.project.write_to_file(path) self.notify(f"Saved as \"{path}\"", title="Saved") self.push_screen(FileSave( filters=Filters( ("TerminalDAW Project File", lambda p: p.suffix.lower() == ".tdp"), ("Any", lambda _: True) ) ), callback=callback) case "Open": def callback(path: str): if path == None: return try: self.project = Project.from_file(path) except Exception as e: self.notify(str(e), title="Failed to open project", severity="error") return self.open_project_path = path ### load all the ui elements # sidebar channels sidebar_channels = self.query_one("#channels") # we cant use sidebar_channels.remove_children() because that would # also remove the "Add Channel" button for child in sidebar_channels.children: if child.id != "add-channel": child.remove() for i, channel in enumerate(self.app.project.channels): sidebar_channels.mount(Channel( i, channel, channel.name, channel.mute, channel.solo, channel.pan, channel.volume ), before=-1) # timeline tracks timeline = self.query_one(Timeline) rows = timeline.query_one("#rows") for channel in self.project.channels: row = TimelineRow(channel) row.styles.width = timeline.bar_offset * self.project.song_length rows.mount(row) for chunk in channel.chunks: if chunk.chunk_type == ChunkType.CHUNK: row.mount(Chunk(chunk_name=chunk.name, bar_pos=chunk.position)) elif chunk.chunk_type == ChunkType.AUDIO: row.mount(AudioChunk(chunk.audio_data, chunk.sample_rate, chunk.name, chunk.position)) self.notify(f"Loaded \"{path}\"") self.push_screen(FileOpen( filters=Filters( ("TerminalDAW Project File", lambda p: p.suffix.lower() == ".tdp"), ("Any", lambda _: True) ) ), callback=callback) case "Settings": self.push_screen(SettingsScreen()) # About menu case "Chookspace repo": webbrowser.open("https://chookspace.com/SpookyDervish/Terminal-DAW") self.notify("Openned the repo in your default browser.") case "License": self.push_screen(LicenseScreen()) case _: self.notify("Sorry, that isn't implemented yet... ;-;", severity="warning") @on(Tabs.TabActivated) async def top_menu_tab_clicked(self, event: Tabs.TabActivated): if self.first_tab_click: event.tabs.active = None self.first_tab_click = False return options = { "File": [ "Save", "Save as", "Open", "Render", "Settings" ], "Edit": [ "Copy", "Paste", "Cut" ], "About": [ "Chookspace repo", "License" ] } self.app.push_screen(ContextMenu(options[event.tab.label], self.app.mouse_position + (0, 1)), callback=self.handle_menu_click) def compose(self) -> ComposeResult: yield Tabs( Tab("File"), Tab("Edit"), Tab("About"), id="top-menu") yield Sidebar() yield Timeline() yield ProjectSettings() yield Footer() def on_mount(self): # load config into the UI self.config_handler.apply_settings()