diff --git a/mfp/gui/app_window_select.py b/mfp/gui/app_window_select.py index e1f858a8..2853dbd6 100644 --- a/mfp/gui/app_window_select.py +++ b/mfp/gui/app_window_select.py @@ -91,7 +91,10 @@ async def select(self, obj): if obj not in self.selected: self.selected = [obj] + self.selected + obj.layer.patch.selected_layer = obj.layer + obj.select() + await self.signal_emit("select", obj) return True @@ -221,7 +224,11 @@ async def delete_selected(self): @extends(AppWindow) def reset_zoom(self): di = self.selected_patch.display_info - di.view_zoom = 1.0 + if self.backend_name == "imgui": + di.view_zoom = self.imgui_global_scale + else: + di.view_zoom = 1.0 + di.view_x = 0 di.view_y = 0 self.viewport_pos_set = True diff --git a/mfp/gui/base_element.py b/mfp/gui/base_element.py index 1d1e739d..d13bfa4e 100644 --- a/mfp/gui/base_element.py +++ b/mfp/gui/base_element.py @@ -101,14 +101,14 @@ def move_to_top(self): # their own editors. It's awkward to have type-specific info here # and in the Processor definition but I can't see a way around it PROPERTY_ATTRS = { + 'lv2_description': ParamInfo( + label="Description", param_type=str, show=True + ), 'lv2_type': ParamInfo( label="(lv2) Port type", choices=lambda o: [('MIDI', 'midi'), ('Control', 'control')], param_type=str, show=True ), - 'lv2_description': ParamInfo( - label="(lv2) Description", param_type=str, show=True - ), 'lv2_default_val': ParamInfo( label="(lv2) Default value [control ports]", param_type=float, show=True ), diff --git a/mfp/gui/imgui/app_window/app_window.py b/mfp/gui/imgui/app_window/app_window.py index c0655095..ccf67733 100644 --- a/mfp/gui/imgui/app_window/app_window.py +++ b/mfp/gui/imgui/app_window/app_window.py @@ -24,7 +24,17 @@ MAX_RENDER_US = 200000 PEAK_FPS = 60 - +BASE_MARKDOWN_FONT_SCALES = [ + 1.42, 1.33, 1.24, 1.15, 1.1, 1.05 +] + +def monospace_font(): + atlas = imgui.get_io().fonts + fonts = atlas.fonts + for f in fonts: + family = f.get_debug_name().split(' ')[0] + if family == 'Inconsolata-Medium': + return f class ImguiAppWindowImpl(AppWindow, AppWindowImpl): backend_name = "imgui" @@ -45,6 +55,7 @@ def __init__(self, *args, **kwargs): self.imgui_prevent_idle = 0 self.imgui_tile_selected = False self.imgui_popup_open = None + self.imgui_global_scale = 1.0 self.nedit_config = None @@ -94,7 +105,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.signal_listen("motion-event", self.handle_motion) + self.signal_listen("motion-event", self.handle_motion, prepend=True) self.signal_listen("toggle-console", self.handle_toggle_console) self.signal_listen("toggle-info-panel", self.handle_toggle_info_panel) @@ -129,6 +140,15 @@ async def handle_toggle_console(self, *rest): async def handle_toggle_info_panel(self, *rest): self.info_panel_visible = not self.info_panel_visible + def scaled(self, *args): + if len(args) == 1: + return args[0] * self.imgui_global_scale + + return tuple( + v * self.imgui_global_scale + for v in args + ) + async def _render_task(self): from mfp.gui.imgui.text_widget import ImguiTextWidgetImpl @@ -150,8 +170,8 @@ async def _render_task(self): md_options.callbacks.on_image = ImguiTextWidgetImpl.image_callback md_options.callbacks.on_open_link = ImguiTextWidgetImpl.url_callback md_options.font_options.regular_size = 16 - md_options.font_options.size_diff_between_levels = 4 - md_options.font_options.max_header_level = 5 + # md_options.font_options.size_diff_between_levels = 4 + # md_options.font_options.max_header_level = 5 markdown.initialize_markdown(md_options) font_loader = markdown.get_font_loader_function() font_loader() @@ -169,6 +189,8 @@ async def _render_task(self): gl.glClearColor(1.0, 1.0, 1.0, 1) + default_font = None + sync_time = None while ( keep_going @@ -204,9 +226,18 @@ async def _render_task(self): # start processing for this frame imgui.new_frame() + if not default_font: + default_font = monospace_font() + + if default_font: + imgui.push_font(default_font, 16) + # hard work keep_going = self.render() + if default_font: + imgui.pop_font() + ###################### # bottom of loop stuff - hand over the frame to imgui @@ -257,6 +288,7 @@ def shutdown(self): ##################### # renderer def render(self): + self.imgui_prevent_idle = max(0, self.imgui_prevent_idle - 1) keep_going = True @@ -267,6 +299,10 @@ def render(self): nedit.push_style_color(nedit.StyleColor.flow_marker, (1, 1, 1, 0.2)) nedit.push_style_color(nedit.StyleColor.flow, (1, 1, 1, 0.5)) + vp = imgui.get_main_viewport() + vp.framebuffer_scale = (2*self.imgui_global_scale, 2*self.imgui_global_scale) + imgui.get_style().font_scale_main = self.imgui_global_scale + ######################################## # menu bar self.imgui_popup_open = False @@ -295,9 +331,9 @@ def render(self): ) if imgui.begin_popup("About MFP##popup"): from mfp.mfp_main import mfp_banner, mfp_footer, version - imgui.push_style_var(imgui.StyleVar_.item_spacing, (0, 8)) - imgui.dummy([1, 4]) - imgui.dummy([8, 1]) + imgui.push_style_var(imgui.StyleVar_.item_spacing, self.scaled(0, 8)) + imgui.dummy(self.scaled(1, 4)) + imgui.dummy(self.scaled(8, 1)) imgui.same_line() imgui.begin_group() imgui.text(mfp_banner % version()) @@ -308,8 +344,8 @@ def render(self): imgui.text(mfp_footer) imgui.end_group() imgui.same_line() - imgui.dummy([8, 1]) - imgui.dummy([1, 4]) + imgui.dummy(self.scaled(8, 1)) + imgui.dummy(self.scaled(1, 4)) imgui.pop_style_var() imgui.end_popup() popup_open = True @@ -431,7 +467,6 @@ def render(self): # bottom panel ######################################## - imgui.pop_style_var() # padding imgui.pop_style_var() # border imgui.end() @@ -531,6 +566,19 @@ def rezoom(self, **kwargs): def get_size(self): return (self.window_width, self.window_height) + def set_app_scale(self, new_scale): + scale_ratio = new_scale / self.imgui_global_scale + self.imgui_global_scale = new_scale + self.canvas_tile_manager.default_zoom = new_scale + + for p in self.patches: + if p.display_info: + p.display_info.view_zoom *= scale_ratio + + self.viewport_zoom_set = True + self.viewport_pos_set = True + return True + ##################### # element operations def register(self, element): diff --git a/mfp/gui/imgui/app_window/canvas_panel.py b/mfp/gui/imgui/app_window/canvas_panel.py index 72c7bd4a..3906c182 100644 --- a/mfp/gui/imgui/app_window/canvas_panel.py +++ b/mfp/gui/imgui/app_window/canvas_panel.py @@ -76,6 +76,7 @@ def render_tile(app_window, patch): """ render a patch tile """ + # for some reason this still leaves a gap under the menu bar canvas_pane_origin = (1, app_window.menu_height + 1) @@ -191,8 +192,8 @@ def render_tile(app_window, patch): app_window.get_color('grid-color:operate').to_rgbaf() ) + imgui.get_style().font_scale_main = 1.0 nedit.begin("canvas_editor", (0.0, 0.0)) - conf = nedit.get_config() # disable NodeEditor dragging and selecting unless we are hovering on @@ -417,6 +418,7 @@ def render_tile(app_window, patch): nedit.end() # node_editor nedit.pop_style_color(5) + imgui.get_style().font_scale_main = app_window.imgui_global_scale imgui.end() diff --git a/mfp/gui/imgui/app_window/console_panel.py b/mfp/gui/imgui/app_window/console_panel.py index 0abf781d..08a0772f 100644 --- a/mfp/gui/imgui/app_window/console_panel.py +++ b/mfp/gui/imgui/app_window/console_panel.py @@ -43,9 +43,9 @@ def render(app_window): imgui.push_style_var(imgui.StyleVar_.item_spacing, [2, 2]) - imgui.dummy((1, 1)) + imgui.dummy(app_window.scaled(1, 1)) imgui.begin_group() - imgui.dummy((1, 1)) + imgui.dummy(app_window.scaled(1, 1)) imgui.text(" Filter regex:") imgui.end_group() imgui.same_line() @@ -56,7 +56,7 @@ def render(app_window): imgui.same_line() cur = imgui.get_cursor_pos() imgui.set_cursor_pos(( - app_window.window_width - 110, + app_window.window_width - 120*app_window.imgui_global_scale, cur[1] )) _, app_window.log_scroll_follow = imgui.checkbox( diff --git a/mfp/gui/imgui/app_window/info_panel.py b/mfp/gui/imgui/app_window/info_panel.py index 2295d4b7..edc7a5f9 100644 --- a/mfp/gui/imgui/app_window/info_panel.py +++ b/mfp/gui/imgui/app_window/info_panel.py @@ -195,7 +195,7 @@ def render_param( if item_selected and choice_label != current_choice[0]: changed = True newval = choice_value - imgui.dummy([1, 4]) + imgui.dummy(app_window.scaled(1, 4)) imgui.end_popup() imgui.pop_style_var(2) @@ -590,8 +590,8 @@ def render_patch_tab(app_window): ###################### # a little padding - imgui.dummy([1, TAB_PADDING_Y]) - imgui.dummy([TAB_PADDING_X, 1]) + imgui.dummy(app_window.scaled(1, TAB_PADDING_Y)) + imgui.dummy(app_window.scaled(TAB_PADDING_X, 1)) imgui.same_line() imgui.begin_group() @@ -678,8 +678,8 @@ def render_patch_tab(app_window): def render_object_tab(app_window): ###################### # a little padding - imgui.dummy([1, TAB_PADDING_Y]) - imgui.dummy([TAB_PADDING_X, 1]) + imgui.dummy(app_window.scaled(1, TAB_PADDING_Y)) + imgui.dummy(app_window.scaled(TAB_PADDING_X, 1)) imgui.same_line() imgui.begin_group() @@ -721,8 +721,8 @@ def render_params_tab(app_window, param_list): ###################### # a little padding - imgui.dummy([1, TAB_PADDING_Y]) - imgui.dummy([TAB_PADDING_X, 1]) + imgui.dummy(app_window.scaled(1, TAB_PADDING_Y)) + imgui.dummy(app_window.scaled(TAB_PADDING_X, 1)) imgui.same_line() imgui.begin_group() @@ -755,8 +755,8 @@ def render_style_tab(app_window): # the Element tab is the only on where params can be edited if imgui.begin_tab_item("Element")[0]: - imgui.dummy([1, TAB_PADDING_Y]) - imgui.dummy([TAB_PADDING_X, 1]) + imgui.dummy(app_window.scaled(1, TAB_PADDING_Y)) + imgui.dummy(app_window.scaled(TAB_PADDING_X, 1)) imgui.same_line() imgui.begin_group() @@ -771,8 +771,8 @@ def render_style_tab(app_window): # defaults for this type of element if imgui.begin_tab_item("Type")[0]: - imgui.dummy([1, TAB_PADDING_Y]) - imgui.dummy([TAB_PADDING_X, 1]) + imgui.dummy(app_window.scaled(1, TAB_PADDING_Y)) + imgui.dummy(app_window.scaled(TAB_PADDING_X, 1)) imgui.same_line() imgui.begin_group() @@ -794,8 +794,8 @@ def render_style_tab(app_window): # style shared by all element types if imgui.begin_tab_item("Base")[0]: - imgui.dummy([1, TAB_PADDING_Y]) - imgui.dummy([TAB_PADDING_X, 1]) + imgui.dummy(app_window.scaled(1, TAB_PADDING_Y)) + imgui.dummy(app_window.scaled(TAB_PADDING_X, 1)) imgui.same_line() imgui.begin_group() @@ -817,8 +817,8 @@ def render_style_tab(app_window): imgui.end_tab_item() if imgui.begin_tab_item("Global")[0]: - imgui.dummy([1, TAB_PADDING_Y]) - imgui.dummy([TAB_PADDING_X, 1]) + imgui.dummy(app_window.scaled(1, TAB_PADDING_Y)) + imgui.dummy(app_window.scaled(TAB_PADDING_X, 1)) imgui.same_line() imgui.begin_group() @@ -840,8 +840,8 @@ def render_style_tab(app_window): if len(app_window.selected) == 1: if imgui.begin_tab_item("Computed")[0]: - imgui.dummy([1, TAB_PADDING_Y]) - imgui.dummy([TAB_PADDING_X, 1]) + imgui.dummy(app_window.scaled(1, TAB_PADDING_Y)) + imgui.dummy(app_window.scaled(TAB_PADDING_X, 1)) imgui.same_line() imgui.begin_group() @@ -866,12 +866,12 @@ def render_bindings_tab(app_window): ###################### # a little padding - imgui.dummy([1, TAB_PADDING_Y]) - imgui.dummy([TAB_PADDING_X, 1]) + imgui.dummy(app_window.scaled(1, TAB_PADDING_Y)) + imgui.dummy(app_window.scaled(TAB_PADDING_X, 1)) imgui.same_line() imgui.begin_group() - imgui.push_style_var(imgui.StyleVar_.item_spacing, (4.0, 8.0)) + imgui.push_style_var(imgui.StyleVar_.item_spacing, app_window.scaled(4.0, 8.0)) imgui.text("OSC bindings") if imgui.begin_table( @@ -938,8 +938,8 @@ def render_activity_tab(app_window): ###################### # a little padding - imgui.dummy([1, TAB_PADDING_Y]) - imgui.dummy([TAB_PADDING_X, 1]) + imgui.dummy(app_window.scaled(1, TAB_PADDING_Y)) + imgui.dummy(app_window.scaled(TAB_PADDING_X, 1)) imgui.same_line() imgui.begin_group() diff --git a/mfp/gui/imgui/app_window/menu_bar.py b/mfp/gui/imgui/app_window/menu_bar.py index 30c07a7a..5a57ba6a 100644 --- a/mfp/gui/imgui/app_window/menu_bar.py +++ b/mfp/gui/imgui/app_window/menu_bar.py @@ -30,6 +30,8 @@ def add_menu_items(app_window, itemdict): if itemname.startswith("|"): continue if isinstance(value, dict): + imgui.dummy(app_window.scaled(1, 1)) + imgui.same_line() if imgui.begin_menu(itemname): add_menu_items(app_window, value) imgui.end_menu() @@ -47,11 +49,21 @@ def add_menu_items(app_window, itemdict): itemname = itemname[3:] else: itemname = itemname[2:] - toggle_state = toggle_items_state.setdefault(menu_path, default_toggle) + + if value.selected and callable(value.selected): + toggle_state = value.selected() + elif value.selected is not None: + toggle_state = value.selected + else: + toggle_state = toggle_items_state.setdefault(menu_path, default_toggle) # make the actual menu item + imgui.dummy(app_window.scaled(1, 1)) + imgui.same_line() item_selected, item_toggled = imgui.menu_item( - itemname, keysym, toggle_state, value.enabled + itemname, + '' if keysym.startswith('__') else keysym, + toggle_state, value.enabled ) # send synthesized keypress(es) if selected @@ -69,9 +81,9 @@ def add_menu_items(app_window, itemdict): sep_items = itemdict.get(separators * '|') if not sep_items: continue - imgui.dummy([1, 2]) + imgui.dummy(app_window.scaled(1, 2)) imgui.separator() - imgui.dummy([1, 2]) + imgui.dummy(app_window.scaled(1, 2)) add_menu_items(app_window, sep_items) def prune_paths(pathdict): @@ -185,11 +197,13 @@ def render(app_window): menu_open = True add_menu_items(app_window, by_menu.get("Layer", {})) if app_window.selected_patch and len(app_window.selected_patch.layers) > 0: - imgui.dummy([1, 2]) + imgui.dummy(app_window.scaled(1, 2)) imgui.separator() - imgui.dummy([1, 2]) + imgui.dummy(app_window.scaled(1, 2)) for layer_num, layer in enumerate(app_window.selected_patch.layers): imgui.push_id(layer_num) + imgui.dummy(app_window.scaled(1, 1)) + imgui.same_line() layer_selected, _ = imgui.menu_item( layer.name, '', @@ -205,13 +219,15 @@ def render(app_window): add_menu_items(app_window, by_menu.get("Window", {})) if len(app_window.patches) > 0: - imgui.dummy([1, 2]) + imgui.dummy(app_window.scaled(1, 2)) imgui.separator() - imgui.dummy([1, 2]) + imgui.dummy(app_window.scaled(1, 2)) for patch in app_window.patches: if not patch.display_info: continue imgui.push_id(str(id(patch))) + imgui.dummy(app_window.scaled(1, 1)) + imgui.same_line() patch_selected, _ = imgui.menu_item( patch.obj_name, '', diff --git a/mfp/gui/imgui/app_window/status_line.py b/mfp/gui/imgui/app_window/status_line.py index 8c67cd52..9650c9c9 100644 --- a/mfp/gui/imgui/app_window/status_line.py +++ b/mfp/gui/imgui/app_window/status_line.py @@ -42,7 +42,7 @@ def render(app_window): if app_window.cmd_input_filename: imgui.same_line() cursor_x, cursor_y = imgui.get_cursor_pos() - imgui.set_cursor_pos([window_w - 100, cursor_y]) + imgui.set_cursor_pos([window_w - app_window.scaled(100), cursor_y]) imgui.push_id("file_select") if imgui.button("Select..."): if app_window.cmd_input_filename == "open": @@ -105,7 +105,7 @@ def render(app_window): ]) cur = imgui.get_cursor_pos() imgui.set_cursor_pos(( - app_window.window_width - len(right_corner_text) * CHAR_PIXELS - 24, + app_window.window_width - app_window.scaled(len(right_corner_text) * CHAR_PIXELS + 32), cur[1] )) imgui.text(right_corner_text) diff --git a/mfp/gui/imgui/base_element.py b/mfp/gui/imgui/base_element.py index 1111d1c8..2533d567 100644 --- a/mfp/gui/imgui/base_element.py +++ b/mfp/gui/imgui/base_element.py @@ -237,6 +237,8 @@ def render_badge(self): 24 ) draw_list.add_text( + imgui.get_font(), + 14, (xpos - halfbadge/2.0 + self.position_x + 0.5, ypos - halfbadge + self.position_y + 0.5), imgui.IM_COL32(255, 255, 255, 255), btext diff --git a/mfp/gui/imgui/text_element.py b/mfp/gui/imgui/text_element.py index 5eea688b..328d8231 100644 --- a/mfp/gui/imgui/text_element.py +++ b/mfp/gui/imgui/text_element.py @@ -117,7 +117,7 @@ def render(self): if content_h < self.min_height: imgui.dummy([1, self.min_height - content_h]) - eff_width = min(max(content_w, self.min_width), self.max_width) + eff_width = min(max(content_w + 8, self.min_width), self.max_width) imgui.end_group() self.width = eff_width diff --git a/mfp/gui/imgui/text_widget.py b/mfp/gui/imgui/text_widget.py index ce2ef4e3..d31a0084 100644 --- a/mfp/gui/imgui/text_widget.py +++ b/mfp/gui/imgui/text_widget.py @@ -3,7 +3,7 @@ """ import re -from imgui_bundle import imgui, ImVec4 +from imgui_bundle import imgui from imgui_bundle import imgui_node_editor as nedit from imgui_bundle import imgui_md as markdown @@ -12,42 +12,11 @@ from mfp.gui.colordb import ColorDB from ..text_widget import TextWidget, TextWidgetImpl - -# markdown images are ![caption](image spec) -# image spec is not well defined. We are extending it a little -# to enable passing size params. -# ![caption](filename\ spaces\ escaped alt_text\ spaces\ escaped width=123 height=123) -# everything is optional but spaces must always be escaped in the filename - -# FIXME looks like we don't get the image arg if there are any spaces -# in it :/ -def _parse_md_image_arg(arg): - if not arg: - return "", None, None - - parts = re.split( - r'(? ||||Magnification (DPI) > []100%", + keysym=cls.NO_KEY, + selected=lambda: MFPGUI().appwin.imgui_global_scale == 1.0 + ) + cls.bind( + "app-scale-125", lambda mode: mode.set_app_scale(1.25), + keysym=cls.NO_KEY, + menupath="Window > ||||Magnification (DPI) > []125%", + selected=lambda: MFPGUI().appwin.imgui_global_scale == 1.25 + ) + cls.bind( + "app-scale-150", lambda mode: mode.set_app_scale(1.50), + keysym=cls.NO_KEY, + menupath="Window > ||||Magnification (DPI) > []150%", + selected=lambda: MFPGUI().appwin.imgui_global_scale == 1.50 + ) + cls.bind( + "app-scale-200", lambda mode: mode.set_app_scale(2.0), + keysym=cls.NO_KEY, + menupath="Window > ||||Magnification (DPI) > []200%", + selected=lambda: MFPGUI().appwin.imgui_global_scale == 2.0 + ) + cls.bind( + "app-scale-250", lambda mode: mode.set_app_scale(2.5), + keysym=cls.NO_KEY, + menupath="Window > ||||Magnification (DPI) > []250%", + selected=lambda: MFPGUI().appwin.imgui_global_scale == 2.5 + ) + + + async def set_app_scale(self, new_scale): + return self.window.set_app_scale(new_scale) async def toggle_panel_mode(self): patch = self.window.selected_patch @@ -786,6 +821,12 @@ async def toggle_snoop(self): self.window.hud_write("Snooping disabled") async def search_interactive(self): + def match_sort_key(obj): + return ( + obj.layer != self.window.selected_layer, + - obj.obj_id, + ) + async def search_changed(newval, incremental=True): matches = [] for element in self.window.objects: @@ -796,12 +837,14 @@ async def search_changed(newval, incremental=True): newval in (element.obj_type or '') or newval in (element.obj_name or '') or newval in (element.obj_args or '') + or (hasattr(element, 'value') and newval in str(element.value)) ): matches.append(element) element.highlight_text = newval else: element.highlight_text = None + matches.sort(key=match_sort_key) if incremental: if matches != self.search_interactive_matches: self.search_interactive_position = 0 diff --git a/mfp/gui/modes/label_edit.py b/mfp/gui/modes/label_edit.py index 8884c2d6..840fafd0 100644 --- a/mfp/gui/modes/label_edit.py +++ b/mfp/gui/modes/label_edit.py @@ -224,13 +224,13 @@ def insert_text(self, keysym): return True def delete_left(self): - if self.editpos == 0: - return True - if self.selection_start != self.selection_end: self.delete_selection() return True + if self.editpos == 0: + return True + newtext = self.text[:self.editpos-1] + self.text[self.editpos:] self.text = newtext self.editpos -= 1 @@ -239,13 +239,13 @@ def delete_left(self): return True def delete_right(self): - if self.editpos == len(self.text): - return True - if self.selection_start != self.selection_end: self.delete_selection() return True + if self.editpos == len(self.text): + return True + newtext = self.text[:self.editpos] + self.text[self.editpos+1:] self.text = newtext self.update_label(raw=True) @@ -411,12 +411,16 @@ def _one_line_up(self, pos): line_pos = len(lines_above[-1]) if len(lines_above) > 2: - return ( + return max( + 0, sum(len(ll) + 1 for ll in lines_above[:-2]) + min(len(lines_above[-2]), line_pos) ) if len(lines_above) > 1: - return min(len(lines_above[0]), line_pos) + return max( + 0, + min(len(lines_above[0]), line_pos) + ) return 0 def move_up(self): diff --git a/mfp/gui/text_element.py b/mfp/gui/text_element.py index e74def99..226b0e34 100644 --- a/mfp/gui/text_element.py +++ b/mfp/gui/text_element.py @@ -61,6 +61,7 @@ def __init__(self, window, x, y): 'fill-color': ColorDB().find('transparent'), 'fill-color:selected': ColorDB().find('transparent'), }) + self._all_styles = self.combine_styles() self.label = TextWidget.build(self) self.label.set_color(self.get_color('text-color')) diff --git a/mfp/gui/tile_manager.py b/mfp/gui/tile_manager.py index 166d195f..14bce343 100644 --- a/mfp/gui/tile_manager.py +++ b/mfp/gui/tile_manager.py @@ -36,6 +36,7 @@ def __init__(self, total_width, total_height): self.tiles = [] self.next_page_id = 0 self.next_tile_id = 0 + self.default_zoom = 1.0 def find_tile(self, **kwargs): if kwargs.get("new_page") or not self.tiles: @@ -48,7 +49,8 @@ def find_tile(self, **kwargs): tile_id=tile_id, page_id=page_id, width=self.total_width, - height=self.total_height + height=self.total_height, + view_zoom=self.default_zoom, ) self.add_tile(tile) return tile @@ -92,7 +94,7 @@ def init_tile(self, **kwargs): frame_offset_y=0, view_x=0, view_y=0, - view_zoom=1.0, + view_zoom=self.default_zoom, width=self.total_width, height=self.total_height, page_id=self.next_page_id, @@ -284,7 +286,7 @@ def split_tile(self, tile, direction): origin_y=tile.origin_y + dh, view_x=0, view_y=0, - view_zoom=1.0, + view_zoom=self.default_zoom, frame_offset_x=tile.frame_offset_x, frame_offset_y=tile.frame_offset_y, width=tile.width - dw, diff --git a/mfp/gui_main.py b/mfp/gui_main.py index 406292af..19a2ca12 100644 --- a/mfp/gui_main.py +++ b/mfp/gui_main.py @@ -272,6 +272,7 @@ async def main(cmdline): debug = cmdline.get('debug') backend = cmdline.get('backend') searchpath = cmdline.get('searchpath') + init_mag = cmdline.get("magnification", 1.0) log.log_module = "gui" log.log_func = log.rpclog @@ -326,6 +327,9 @@ async def main(cmdline): gui.appwin = AppWindow.build() + if backend == "imgui": + gui.appwin.set_app_scale(init_mag) + if debug: import yappi yappi.start() @@ -375,6 +379,8 @@ def main_sync_wrapper(): help="UI framework to use") parser.add_argument("-p", "--searchpath", default="", help="Paths to search for assets (colon-separated string)") + parser.add_argument("-m", "--magnification", default=1.0, type=float, + help="Initial magnification of UI (HiDPI compensation) (default: 1.0)") cmdline = vars(parser.parse_args()) backend_name = cmdline.get("backend") diff --git a/mfp/mfp_app.py b/mfp/mfp_app.py index b8ee24a4..a4b33efc 100644 --- a/mfp/mfp_app.py +++ b/mfp/mfp_app.py @@ -61,6 +61,7 @@ def __init__(self): self.batch_args = None self.batch_eval = False self.batch_input_file = None + self.gui_init_magnification = 1.0 # RPC host self.rpc_listener = None @@ -139,6 +140,7 @@ def _exception(exc, tbinfo, traceback=""): "mfpgui", "-s", self.socket_path, "-l", logstart, + "-m", self.gui_init_magnification, '--backend', self.gui_backend, "--searchpath", self.searchpath ] diff --git a/mfp/mfp_main.py b/mfp/mfp_main.py index 52436e4e..ae40c501 100644 --- a/mfp/mfp_main.py +++ b/mfp/mfp_main.py @@ -160,6 +160,8 @@ async def main(): help="Number of MIDI output ports") parser.add_argument("-u", "--osc-udp-port", default=5555, type=int, help="UDP port to listen for OSC (default: 5555)") + parser.add_argument("-m", "--magnification", default=1.0, type=float, + help="Initial magnification of UI (HiDPI compensation) (default: 1.0)") parser.add_argument("-v", "--verbose", action="store_true", help="Log all messages to console") parser.add_argument("--verbose-remote", action="store_true", @@ -241,6 +243,9 @@ async def main(): app.searchpath += ':' + ':'.join(patch_dirs) + if args.get("magnification"): + app.gui_init_magnification = args.get("magnification") + if args.get('batch'): app.batch_mode = True app.batch_args = args.get("args") diff --git a/mfp/patch.py b/mfp/patch.py index 6d24ad85..80069fdc 100644 --- a/mfp/patch.py +++ b/mfp/patch.py @@ -47,7 +47,8 @@ def __init__(self, init_type, init_args, patch, scope, name, context=None): self.scopes = {'__patch__': LexicalScope('__patch__')} self.default_scope = self.scopes['__patch__'] self.evaluator = Evaluator() - + self.doc_tooltip_inlet = [] + self.doc_tooltip_outlet = [] self.inlet_objects = [] self.outlet_objects = [] self.dispatch_objects = [] @@ -60,11 +61,28 @@ def __init__(self, init_type, init_args, patch, scope, name, context=None): self.gui_params['layers'] = [] self.gui_params['dsp_context'] = self.context.context_name if self.context else "" + self.properties = { + "lv2_description": f"User patch: {init_type}", + } if patch is None: self.gui_params['top_level'] = True else: self.gui_params['top_level'] = False + async def setup(self, **kwargs): + for inlet in self.inlet_objects: + num = inlet.inletnum + if num >= len(self.doc_tooltip_inlet): + self.doc_tooltip_inlet.extend([''] * (num - len(self.doc_tooltip_inlet) + 1)) + self.doc_tooltip_inlet[num] = inlet.properties.get('lv2_description', f'Inlet {num}') + + for outlet in self.outlet_objects: + num = outlet.outletnum + if num >= len(self.doc_tooltip_outlet): + self.doc_tooltip_outlet.extend([''] * (num - len(self.doc_tooltip_outlet) + 1)) + self.doc_tooltip_outlet[num] = outlet.properties.get('lv2_description', f'Outlet {num}') + self.doc_tooltip_obj = self.properties.get("lv2_description", "No documentation") + ############################# # API methods used in patches by @methodname ############################# @@ -295,6 +313,7 @@ def add(self, obj): num = obj.inletnum if num >= len(self.inlet_objects): self.inlet_objects.extend([None] * (num - len(self.inlet_objects) + 1)) + self.inlet_objects[num] = obj self.resize(len(self.inlet_objects), len(self.outlet_objects)) @@ -309,7 +328,10 @@ def add(self, obj): num = obj.outletnum if num >= len(self.outlet_objects): self.outlet_objects.extend([None] * (num - len(self.outlet_objects) + 1)) + if num >= len(self.doc_tooltip_outlet): + self.doc_tooltip_outlet.extend([''] * (num - len(self.doc_tooltip_outlet) + 1)) self.outlet_objects[num] = obj + self.doc_tooltip_outlet[num] = obj.properties.get('lv2_description', f'Outlet {num}') self.resize(len(self.inlet_objects), len(self.outlet_objects)) if obj.init_type == 'outlet~': diff --git a/mfp/patch_json.py b/mfp/patch_json.py index c2b21740..aa9642eb 100644 --- a/mfp/patch_json.py +++ b/mfp/patch_json.py @@ -141,7 +141,9 @@ async def json_deserialize(self, json_data): self.objects = {} self.scopes = {} self.inlet_objects = [] + self.doc_tooltip_inlet = [] self.outlet_objects = [] + self.doc_tooltip_outlet = [] self.dispatch_objects = [] self.presets = f.get("presets", {}) @@ -264,6 +266,7 @@ async def json_unpack_objects(self, data, scope): @extends(Patch) async def json_serialize(self): + from .processor import Processor from .mfp_app import MFPApp f = {} f['type'] = self.init_type @@ -281,7 +284,13 @@ async def json_serialize(self): keys.sort() for oid in keys: o = self.objects.get(oid) - if o and (isinstance(o, MFPApp) or not o.save_to_patch): + if not o: + continue + if ( + isinstance(o, MFPApp) + or not o.save_to_patch + or not o.status == Processor.READY + ): continue oinfo = o.save() allobj[oid] = oinfo diff --git a/mfp/utils.py b/mfp/utils.py index 5da72683..042e1299 100644 --- a/mfp/utils.py +++ b/mfp/utils.py @@ -251,11 +251,14 @@ def __init__(self, *args, **kwargs): self.handlers_by_id = {} self.last_handler_id = 0 - def signal_listen(self, signal, handler, *args): + def signal_listen(self, signal, handler, *args, **kwargs): new_id = self.last_handler_id + 1 self.last_handler_id = new_id old_handlers = self.signal_handlers[signal] - old_handlers.append((new_id, handler, args)) + if kwargs.get("prepend"): + old_handlers[:0] = [(new_id, handler, args)] + else: + old_handlers.append((new_id, handler, args)) self.handlers_by_id[new_id] = signal return new_id diff --git a/requirements.txt b/requirements.txt index 69010816..9c4172cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ cffi==1.17.1 Cython==3.0.12 flopsy==0.0.7 glfw==2.8.0 -imgui-bundle==1.6.3 +imgui-bundle==1.92.3 munch==4.0.0 numpy==2.2.4 pillow==11.1.0