From dba0f1dcd7e9118e4b14be9d13ad96d2fef2a674 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Fri, 11 Jul 2025 16:20:21 -0400 Subject: [PATCH 01/22] Initial work on imgui 1.92 and scalable UI --- mfp/gui/imgui/app_window/app_window.py | 34 +++++++++++---- mfp/gui/imgui/app_window/canvas_panel.py | 3 +- mfp/gui/imgui/app_window/info_panel.py | 44 ++++++++++---------- mfp/gui/imgui/app_window/menu_bar.py | 24 +++++++---- mfp/gui/imgui/text_widget.py | 53 +++++++++++++----------- mfp/gui/input_mode.py | 10 ++++- mfp/gui/modes/global_mode.py | 36 ++++++++++++++++ requirements.txt | 2 +- 8 files changed, 138 insertions(+), 68 deletions(-) diff --git a/mfp/gui/imgui/app_window/app_window.py b/mfp/gui/imgui/app_window/app_window.py index c0655095..5a85582c 100644 --- a/mfp/gui/imgui/app_window/app_window.py +++ b/mfp/gui/imgui/app_window/app_window.py @@ -24,7 +24,9 @@ MAX_RENDER_US = 200000 PEAK_FPS = 60 - +BASE_MARKDOWN_FONT_SCALES = [ + 1.42, 1.33, 1.24, 1.15, 1.1, 1.05 +] class ImguiAppWindowImpl(AppWindow, AppWindowImpl): backend_name = "imgui" @@ -45,6 +47,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 @@ -129,6 +132,12 @@ 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): + return tuple( + v * self.imgui_global_scale if v != 1 else 1 + for v in args + ) + async def _render_task(self): from mfp.gui.imgui.text_widget import ImguiTextWidgetImpl @@ -150,8 +159,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() @@ -204,6 +213,9 @@ async def _render_task(self): # start processing for this frame imgui.new_frame() + # tweak font scalings if magnification has changed + md_options.font_options.regular_size = 16 / self.imgui_global_scale + # hard work keep_going = self.render() @@ -257,6 +269,7 @@ def shutdown(self): ##################### # renderer def render(self): + self.imgui_prevent_idle = max(0, self.imgui_prevent_idle - 1) keep_going = True @@ -267,6 +280,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 = (self.imgui_global_scale, self.imgui_global_scale) + imgui.get_style().font_scale_main = self.imgui_global_scale + ######################################## # menu bar self.imgui_popup_open = False @@ -295,9 +312,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 +325,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 +448,6 @@ def render(self): # bottom panel ######################################## - imgui.pop_style_var() # padding imgui.pop_style_var() # border imgui.end() diff --git a/mfp/gui/imgui/app_window/canvas_panel.py b/mfp/gui/imgui/app_window/canvas_panel.py index f2c1b65c..efbac4bf 100644 --- a/mfp/gui/imgui/app_window/canvas_panel.py +++ b/mfp/gui/imgui/app_window/canvas_panel.py @@ -191,8 +191,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 @@ -410,6 +410,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/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..54c8adf4 100644 --- a/mfp/gui/imgui/app_window/menu_bar.py +++ b/mfp/gui/imgui/app_window/menu_bar.py @@ -47,11 +47,19 @@ 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 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 +77,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,9 +193,9 @@ 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) layer_selected, _ = imgui.menu_item( @@ -205,9 +213,9 @@ 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 diff --git a/mfp/gui/imgui/text_widget.py b/mfp/gui/imgui/text_widget.py index ce2ef4e3..e83e5fdb 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,6 +12,8 @@ from mfp.gui.colordb import ColorDB from ..text_widget import TextWidget, TextWidgetImpl +default_tt_font_name = "ProggyClean.ttf" +default_tt_font_size = 13 # markdown images are ![caption](image spec) # image spec is not well defined. We are extending it a little @@ -80,7 +82,7 @@ def _font_key(f): ) family, shape, _ = _fontinfo(f.get_debug_name()) shape_name = font_shapes.get(shape, 'regular') - fkey = f"{family or 'unnamed'}__{shape_name}__{f.font_size}" + fkey = f"{family or 'unnamed'}__{shape_name}" return fkey @@ -159,19 +161,22 @@ def image_callback(cls, filepath): def markdown_div_callback(cls, div_class, is_opening_div): classes = div_class.split(' ') sizes = { - "size-x-small": "8.0", - "size-small": "12.0", - "size-normal": "16.0", - "size-large": "20.0", - "size-x-large": "24.0", - "size-xx-large": "28.0", + "size-x-small": 8.0, + "size-small": 12.0, + "size-normal": 16.0, + "size-large": 20.0, + "size-x-large": 24.0, + "size-xx-large": 28.0, } font_key_stack = cls.imgui_currently_rendering.font_key_stack if not font_key_stack: - font_key_stack.append(_font_key(imgui.get_current_context().font)) - font_key = list(f for f in font_key_stack if f)[-1] - font_name, font_weight, font_size = font_key.split('__') + font_key_stack.append(( + _font_key(imgui.get_current_context().font), + imgui.get_current_context().font_size + )) + font_key, font_size = list(f for f in font_key_stack if f)[-1] + font_name, font_weight = font_key.split('__') font_changed = False color = None @@ -195,9 +200,8 @@ def markdown_div_callback(cls, div_class, is_opening_div): font_changed = True if c == "tt": - font_name = "ProggyClean.ttf," + font_name = default_tt_font_name font_weight = "regular" - font_size = "13.0" font_changed = True if c.startswith('color'): @@ -217,15 +221,15 @@ def markdown_div_callback(cls, div_class, is_opening_div): if is_opening_div: if font_changed: - new_font_key = f"{font_name}__{font_weight}__{font_size}" + new_font_key = f"{font_name}__{font_weight}" new_font = cls.imgui_font_atlas.get( new_font_key, None ) font_key_stack.append( - new_font_key if new_font else None + (new_font_key if new_font else None, font_size) ) if new_font: - imgui.push_font(new_font) + imgui.push_font(new_font, font_size) if color: imgui.push_style_color(imgui.Col_.text, color) else: @@ -505,7 +509,6 @@ def render(self, wrap_width=None, highlight=None): # sometimes imgui_md doesn't call the div-callback # so we need to be defensive about the font stack context = imgui.get_current_context() - font_depth_before = len(context.font_stack) text_changed = False # transform converts to better-renderable form @@ -526,20 +529,12 @@ def render(self, wrap_width=None, highlight=None): md_text = '\n'.join(transformed_blocks) self.transform_out = md_text - imgui.dummy((1, 3)) if self.markdown_text or self.text: markdown.render(md_text) # check for stray fonts on the stack and get rid of them - context = imgui.get_current_context() draw_list.pop_clip_rect() - font_depth_after = len(context.font_stack) - - if font_depth_before != font_depth_after: - log.debug("[text] Warning: font stack is messed up, doing my best") - for _ in range(font_depth_before, font_depth_after): - imgui.pop_font() else: if self.multiline and wrap_width: label_text = self.simple_wrap(self.text, int(wrap_width / self.font_width)) @@ -548,7 +543,15 @@ def render(self, wrap_width=None, highlight=None): self.wrapped_text = self.text if self.text: + fkey = f"{default_tt_font_name}__regular" + new_font = self.imgui_font_atlas.get(fkey) + new_size = default_tt_font_size + + if new_font: + imgui.push_font(new_font, new_size) imgui.text(label_text + extra_bit) + if new_font: + imgui.pop_font() # text bounding box does not account for descenders imgui.dummy([1, 3]) diff --git a/mfp/gui/input_mode.py b/mfp/gui/input_mode.py index e86a3bac..834f2406 100644 --- a/mfp/gui/input_mode.py +++ b/mfp/gui/input_mode.py @@ -18,6 +18,7 @@ class Binding: keysym: str menupath: str = '' mode: any = None + selected: any = None enabled: bool = False def copy(self, **kwargs): @@ -28,6 +29,8 @@ def copy(self, **kwargs): class InputMode: + NO_KEY = False + _registry = {} # global for all modes @@ -81,7 +84,7 @@ def extend_mode(cls, mode_type): cls._extensions.append(mode_type) @classmethod - def bind(cls, label, action, helptext=None, keysym=None, menupath=None): + def bind(cls, label, action, helptext=None, keysym=None, menupath=None, selected=None): """ binding at the class level lets us know what the mode's bindings are before we create an instance of it @@ -91,8 +94,11 @@ def bind(cls, label, action, helptext=None, keysym=None, menupath=None): "default", action, helptext, InputMode._num_bindings, None, None, cls ) else: + if keysym == cls.NO_KEY: + keysym = f"__{len(cls._bindings)}" + binding = Binding( - label, action, helptext, InputMode._num_bindings, keysym, menupath, cls + label, action, helptext, InputMode._num_bindings, keysym, menupath, cls, selected ) cls._bindings[keysym] = binding InputMode._bindings_by_label[label] = binding diff --git a/mfp/gui/modes/global_mode.py b/mfp/gui/modes/global_mode.py index f39aaa73..cbf3e5c5 100644 --- a/mfp/gui/modes/global_mode.py +++ b/mfp/gui/modes/global_mode.py @@ -296,6 +296,42 @@ def init_bindings(cls): "tile-control", cls.tile_manager_prefix, keysym="C-a" ) + cls.bind( + "app-scale-100", + lambda mode: mode.set_app_scale(1.0), + menupath="Window > DPI Scale > []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 > DPI Scale > []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 > DPI Scale > []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 > DPI Scale > []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 > DPI Scale > []250%", + selected=lambda: MFPGUI().appwin.imgui_global_scale == 2.5 + ) + + + def set_app_scale(self, scale_factor): + self.window.imgui_global_scale = scale_factor + return True async def toggle_panel_mode(self): patch = self.window.selected_patch diff --git a/requirements.txt b/requirements.txt index 69010816..4d860743 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.0 munch==4.0.0 numpy==2.2.4 pillow==11.1.0 From 2ee2c65331a96c33025f51698c01b252ec61aaf3 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Fri, 11 Jul 2025 17:32:48 -0400 Subject: [PATCH 02/22] More work on dummy and spacing elements --- mfp/gui/imgui/app_window/app_window.py | 3 +++ mfp/gui/imgui/app_window/console_panel.py | 6 +++--- mfp/gui/imgui/app_window/status_line.py | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/mfp/gui/imgui/app_window/app_window.py b/mfp/gui/imgui/app_window/app_window.py index 5a85582c..b403b26a 100644 --- a/mfp/gui/imgui/app_window/app_window.py +++ b/mfp/gui/imgui/app_window/app_window.py @@ -133,6 +133,9 @@ 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 if v != 1 else 1 for v in args diff --git a/mfp/gui/imgui/app_window/console_panel.py b/mfp/gui/imgui/app_window/console_panel.py index 0abf781d..cad9d1c7 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 - 110*app_window.imgui_global_scale, cur[1] )) _, app_window.log_scroll_follow = imgui.checkbox( diff --git a/mfp/gui/imgui/app_window/status_line.py b/mfp/gui/imgui/app_window/status_line.py index 8c67cd52..e9039481 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 + 24), cur[1] )) imgui.text(right_corner_text) From 2a6f2caa956b9d1e958f8a49e728853a03ee7176 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Sat, 12 Jul 2025 15:16:40 -0400 Subject: [PATCH 03/22] Make reset-zoom zoom to the current DPI scaling --- mfp/gui/app_window_select.py | 6 +++++- mfp/gui/modes/global_mode.py | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/mfp/gui/app_window_select.py b/mfp/gui/app_window_select.py index e1f858a8..71af50ad 100644 --- a/mfp/gui/app_window_select.py +++ b/mfp/gui/app_window_select.py @@ -221,7 +221,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/modes/global_mode.py b/mfp/gui/modes/global_mode.py index cbf3e5c5..1b26413a 100644 --- a/mfp/gui/modes/global_mode.py +++ b/mfp/gui/modes/global_mode.py @@ -331,6 +331,7 @@ def init_bindings(cls): def set_app_scale(self, scale_factor): self.window.imgui_global_scale = scale_factor + self.window.reset_zoom() return True async def toggle_panel_mode(self): From b706fd24d30a225cfd9796c81d6e6a3368094e29 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Tue, 15 Jul 2025 16:37:30 -0400 Subject: [PATCH 04/22] Use Inconsolata as app font; tweak formatting --- mfp/gui/imgui/app_window/app_window.py | 23 ++++++++++-- mfp/gui/imgui/app_window/canvas_panel.py | 1 + mfp/gui/imgui/app_window/menu_bar.py | 8 +++++ mfp/gui/imgui/base_element.py | 2 ++ mfp/gui/imgui/text_widget.py | 45 ++++-------------------- mfp/gui/modes/global_mode.py | 29 ++++++++++----- 6 files changed, 60 insertions(+), 48 deletions(-) diff --git a/mfp/gui/imgui/app_window/app_window.py b/mfp/gui/imgui/app_window/app_window.py index b403b26a..f0802955 100644 --- a/mfp/gui/imgui/app_window/app_window.py +++ b/mfp/gui/imgui/app_window/app_window.py @@ -28,6 +28,14 @@ 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" motion_overrides = ["scroll-zoom", "canvas-pos"] @@ -137,7 +145,7 @@ def scaled(self, *args): return args[0] * self.imgui_global_scale return tuple( - v * self.imgui_global_scale if v != 1 else 1 + v * self.imgui_global_scale for v in args ) @@ -181,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 @@ -219,9 +229,18 @@ async def _render_task(self): # tweak font scalings if magnification has changed md_options.font_options.regular_size = 16 / self.imgui_global_scale + 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 @@ -284,7 +303,7 @@ def render(self): nedit.push_style_color(nedit.StyleColor.flow, (1, 1, 1, 0.5)) vp = imgui.get_main_viewport() - vp.framebuffer_scale = (self.imgui_global_scale, self.imgui_global_scale) + vp.framebuffer_scale = (2*self.imgui_global_scale, 2*self.imgui_global_scale) imgui.get_style().font_scale_main = self.imgui_global_scale ######################################## diff --git a/mfp/gui/imgui/app_window/canvas_panel.py b/mfp/gui/imgui/app_window/canvas_panel.py index efbac4bf..98c3ab5a 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) diff --git a/mfp/gui/imgui/app_window/menu_bar.py b/mfp/gui/imgui/app_window/menu_bar.py index 54c8adf4..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() @@ -56,6 +58,8 @@ def add_menu_items(app_window, itemdict): 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, '' if keysym.startswith('__') else keysym, @@ -198,6 +202,8 @@ def render(app_window): 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, '', @@ -220,6 +226,8 @@ def render(app_window): 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/base_element.py b/mfp/gui/imgui/base_element.py index 11c5f949..032726c6 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_widget.py b/mfp/gui/imgui/text_widget.py index e83e5fdb..811db1a1 100644 --- a/mfp/gui/imgui/text_widget.py +++ b/mfp/gui/imgui/text_widget.py @@ -12,44 +12,11 @@ from mfp.gui.colordb import ColorDB from ..text_widget import TextWidget, TextWidgetImpl -default_tt_font_name = "ProggyClean.ttf" -default_tt_font_size = 13 - -# 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'(? DPI Scale > []100%", + menupath="Window > ||||UI magnify > []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 > DPI Scale > []125%", + menupath="Window > ||||UI magnify > []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 > DPI Scale > []150%", + menupath="Window > ||||UI magnify > []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 > DPI Scale > []200%", + menupath="Window > ||||UI magnify > []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 > DPI Scale > []250%", + menupath="Window > ||||UI magnify > []250%", selected=lambda: MFPGUI().appwin.imgui_global_scale == 2.5 ) - def set_app_scale(self, scale_factor): - self.window.imgui_global_scale = scale_factor - self.window.reset_zoom() + async def set_app_scale(self, new_scale): + scale_ratio = new_scale / self.window.imgui_global_scale + self.window.imgui_global_scale = new_scale + for p in self.window.patches: + if p.display_info: + p.display_info.view_zoom *= scale_ratio + self.window.viewport_zoom_set = True + self.window.viewport_pos_set = True return True async def toggle_panel_mode(self): @@ -823,6 +828,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: @@ -833,12 +844,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 From 3667c2162f6d7e87bc49da785d73450c3b30db5f Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Wed, 16 Jul 2025 09:26:04 -0400 Subject: [PATCH 05/22] Show layer for selected object on select --- mfp/gui/app_window_select.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mfp/gui/app_window_select.py b/mfp/gui/app_window_select.py index 71af50ad..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 From 44cfc49c8e1fd101b659ae8245611416f4dc38a8 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Wed, 16 Jul 2025 14:56:33 -0400 Subject: [PATCH 06/22] Change magnification menu item name --- mfp/gui/modes/global_mode.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mfp/gui/modes/global_mode.py b/mfp/gui/modes/global_mode.py index 31cb4cd4..ed6129cd 100644 --- a/mfp/gui/modes/global_mode.py +++ b/mfp/gui/modes/global_mode.py @@ -299,32 +299,32 @@ def init_bindings(cls): cls.bind( "app-scale-100", lambda mode: mode.set_app_scale(1.0), - menupath="Window > ||||UI magnify > []100%", + menupath="Window > ||||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 > ||||UI magnify > []125%", + 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 > ||||UI magnify > []150%", + 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 > ||||UI magnify > []200%", + 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 > ||||UI magnify > []250%", + menupath="Window > ||||Magnification (DPI) > []250%", selected=lambda: MFPGUI().appwin.imgui_global_scale == 2.5 ) From cd58cb5e5fc1a24201272980ac7e45c088c41e61 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Wed, 16 Jul 2025 14:57:06 -0400 Subject: [PATCH 07/22] Always process motion events to change the selected app window region --- mfp/gui/imgui/app_window/app_window.py | 2 +- mfp/utils.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mfp/gui/imgui/app_window/app_window.py b/mfp/gui/imgui/app_window/app_window.py index f0802955..4c3d7891 100644 --- a/mfp/gui/imgui/app_window/app_window.py +++ b/mfp/gui/imgui/app_window/app_window.py @@ -105,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) 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 From 0c689f4aa80fa65e0fb24db0b209d77953d233d1 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Thu, 17 Jul 2025 08:14:08 -0400 Subject: [PATCH 08/22] Add placeholder for height if there's no text --- mfp/gui/imgui/text_widget.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/mfp/gui/imgui/text_widget.py b/mfp/gui/imgui/text_widget.py index 811db1a1..d15a94d7 100644 --- a/mfp/gui/imgui/text_widget.py +++ b/mfp/gui/imgui/text_widget.py @@ -509,19 +509,22 @@ def render(self, wrap_width=None, highlight=None): else: self.wrapped_text = self.text - if self.text: - fkey = f"{default_tt_font_name}__regular" - new_font = self.imgui_font_atlas.get(fkey) - new_size = default_tt_font_size + fkey = f"{default_tt_font_name}__regular" + new_font = self.imgui_font_atlas.get(fkey) + new_size = default_tt_font_size - if new_font: - imgui.push_font(new_font, new_size) - else: - log.debug(f"[font] can't find {fkey} in {list(self.imgui_font_atlas.keys())}") - self.font_width, self.font_height = imgui.calc_text_size("M") - imgui.text(label_text + extra_bit) - if new_font: - imgui.pop_font() + if new_font: + imgui.push_font(new_font, new_size) + else: + log.debug(f"[font] can't find {fkey} in {list(self.imgui_font_atlas.keys())}") + self.font_width, self.font_height = imgui.calc_text_size("M") + if self.wrapped_text: + imgui.text(self.wrapped_text + extra_bit) + else: + imgui.dummy((1, self.font_height)) + + if new_font: + imgui.pop_font() # text bounding box does not account for descenders imgui.dummy([1, 3]) From 90c8e3f1608d41c85f042c92943d24ad0c8cc095 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Thu, 17 Jul 2025 08:18:35 -0400 Subject: [PATCH 09/22] Include padding in text element size computation --- mfp/gui/imgui/text_element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 8c35440617cd952c7934900d59f4f0a673399195 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Thu, 17 Jul 2025 08:39:18 -0400 Subject: [PATCH 10/22] Increase widget height for newline --- mfp/gui/imgui/text_widget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mfp/gui/imgui/text_widget.py b/mfp/gui/imgui/text_widget.py index d15a94d7..d31a0084 100644 --- a/mfp/gui/imgui/text_widget.py +++ b/mfp/gui/imgui/text_widget.py @@ -437,7 +437,7 @@ def proctable(match): def render(self, wrap_width=None, highlight=None): extra_bit = '' - if self.multiline and self.text[:-1] == '\n': + if self.multiline and self.text and self.text[-1] == '\n': extra_bit = ' ' if type(self).imgui_font_atlas == {}: @@ -520,7 +520,7 @@ def render(self, wrap_width=None, highlight=None): self.font_width, self.font_height = imgui.calc_text_size("M") if self.wrapped_text: imgui.text(self.wrapped_text + extra_bit) - else: + else: imgui.dummy((1, self.font_height)) if new_font: From 74626257ae0854886b327babd20f3eea1b530f64 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Thu, 17 Jul 2025 08:55:38 -0400 Subject: [PATCH 11/22] Initialize TextElement style defaults correctly --- mfp/gui/imgui/app_window/app_window.py | 3 --- mfp/gui/text_element.py | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/mfp/gui/imgui/app_window/app_window.py b/mfp/gui/imgui/app_window/app_window.py index 4c3d7891..822da934 100644 --- a/mfp/gui/imgui/app_window/app_window.py +++ b/mfp/gui/imgui/app_window/app_window.py @@ -226,9 +226,6 @@ async def _render_task(self): # start processing for this frame imgui.new_frame() - # tweak font scalings if magnification has changed - md_options.font_options.regular_size = 16 / self.imgui_global_scale - if not default_font: default_font = monospace_font() 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')) From 652e6f00c41c305ae9aa89192e5fdb49ae3a4745 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Thu, 17 Jul 2025 09:06:11 -0400 Subject: [PATCH 12/22] Delete selection before checking editpos --- mfp/gui/modes/label_edit.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) 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): From 487b4f3c50d4cb1ed68495e3bc647b40fa2e9bd6 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Thu, 17 Jul 2025 15:36:58 -0400 Subject: [PATCH 13/22] Add inlet/outlet description params to tooltip --- mfp/gui/base_element.py | 6 +++--- mfp/patch.py | 24 +++++++++++++++++++++++- mfp/patch_json.py | 2 ++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/mfp/gui/base_element.py b/mfp/gui/base_element.py index 052af5d7..f43fd788 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/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 efc413a3..ce218e40 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", {}) From eebb50aafb7f17ed115a1071cd55359b87fd9a21 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Tue, 22 Jul 2025 10:03:22 -0400 Subject: [PATCH 14/22] Initial implementation of [messagerec] --- mfp/builtins/__init__.py | 3 +- mfp/builtins/messagerec.py | 94 ++++++++++++++++++++++++++++++++++++++ mfp/processor.py | 6 ++- 3 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 mfp/builtins/messagerec.py diff --git a/mfp/builtins/__init__.py b/mfp/builtins/__init__.py index 7c735e68..7d012940 100644 --- a/mfp/builtins/__init__.py +++ b/mfp/builtins/__init__.py @@ -88,6 +88,7 @@ def register(): replay.register() from . import breakpoint breakpoint.register() - from . import faust faust.register() + from . import messagerec + messagerec.register() diff --git a/mfp/builtins/messagerec.py b/mfp/builtins/messagerec.py new file mode 100644 index 00000000..cfac6e0b --- /dev/null +++ b/mfp/builtins/messagerec.py @@ -0,0 +1,94 @@ +#! /usr/bin/env python +''' +builtins/messagerec.py: Record/playback for message sequences + +Copyright (c) Bill Grbble +''' + +from datetime import datetime +from mfp import log +from mfp.bang import Uninit +from mfp.processor import Processor, MultiOutput +from ..mfp_app import MFPApp + + +class MessageRec(Processor): + doc_tooltip_obj = "Message recorder" + doc_tooltip_inlet = ["Values/control", "Clock"] + doc_tooltip_outlet = ["Value output"] + + def __init__(self, init_type, init_args, patch, scope, name, defs=None): + Processor.__init__(self, 2, 1, init_type, init_args, patch, scope, name, defs) + extra = defs or {} + _, _ = self.parse_args(init_args, **extra) + + self.messages = [] + self.clock_tempo_ts_timestamp = None + self.clock_tempo_ts_beat = None + self.clock_tempo_ms = None + self.clock_beat = 0 + self.rec_state = False + self.play_state = True + self.play_state = False + self.hot_inlets = (0, 1) + + # methods callable by messages on inlet 0 + def record(self, new_state=True): + self.rec_state = new_state + + def clear(self): + self.messages = [] + + def play(self): + self.play_state = True + + def stop(self): + self.play_state = False + + async def trigger(self): + rightnow = datetime.now() + beatnow = self.clock_beat or 0 + + if self.inlets[1] is not Uninit: + if isinstance(self.inlets[1], (float, int)): + self.clock_beat = self.inlets[1] + else: + self.clock_beat += 1 + + if self.clock_tempo_ts_timestamp is None: + self.clock_tempo_ts_timestamp = rightnow + self.clock_tempo_ts_beat = self.clock_beat + else: + delta_beats = self.clock_beat - self.clock_tempo_ts_beat + if delta_beats > 0: + self.clock_tempo_ms = ( + 1000 * (rightnow - self.clock_tempo_ts_timestamp).total_seconds() / delta_beats + ) + self.clock_tempo_ts_timestamp = rightnow + self.clock_tempo_ts_beat = self.clock_beat + self.inlets[1] = Uninit + + if self.inlets[0] is not Uninit: + self.messages.append((self.clock_beat, rightnow, self.inlets[0])) + self.messages.sort() + self.inlets[0] = Uninit + + if self.clock_beat < beatnow: + beatnow = -1 + + pending = [ + m[2] for m in self.messages + if m[0] > beatnow and m[0] <= self.clock_beat + ] + + if not pending: + return + + if len(pending) == 1: + self.outlets[0] = pending[0] + else: + self.outlets[0] = MultiOutput(pending) + + +def register(): + MFPApp().register("messagerec", MessageRec) diff --git a/mfp/processor.py b/mfp/processor.py index 78fd67da..d72bad4a 100644 --- a/mfp/processor.py +++ b/mfp/processor.py @@ -24,8 +24,10 @@ def __init__(self, value, outlet): class MultiOutput: - def __init__(self): - self.values = [] + def __init__(self, values=None): + if values is None: + values = [] + self.values = values @classmethod def all_values(cls, val): From 90724b76d5e456591ef9468a9fc212cf0870a2ee Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Thu, 24 Jul 2025 12:10:56 -0400 Subject: [PATCH 15/22] Work on [messagerec] --- mfp/builtins/messagerec.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/mfp/builtins/messagerec.py b/mfp/builtins/messagerec.py index cfac6e0b..0e876cef 100644 --- a/mfp/builtins/messagerec.py +++ b/mfp/builtins/messagerec.py @@ -26,6 +26,7 @@ def __init__(self, init_type, init_args, patch, scope, name, defs=None): self.clock_tempo_ts_timestamp = None self.clock_tempo_ts_beat = None self.clock_tempo_ms = None + self.clock_tick_ms = None self.clock_beat = 0 self.rec_state = False self.play_state = True @@ -49,6 +50,7 @@ async def trigger(self): rightnow = datetime.now() beatnow = self.clock_beat or 0 + logm = False if self.inlets[1] is not Uninit: if isinstance(self.inlets[1], (float, int)): self.clock_beat = self.inlets[1] @@ -56,9 +58,11 @@ async def trigger(self): self.clock_beat += 1 if self.clock_tempo_ts_timestamp is None: + self.clock_tick_ms = 0 self.clock_tempo_ts_timestamp = rightnow self.clock_tempo_ts_beat = self.clock_beat else: + self.clock_tick_ms = (rightnow - self.clock_tempo_ts_timestamp).total_seconds() * 1000 delta_beats = self.clock_beat - self.clock_tempo_ts_beat if delta_beats > 0: self.clock_tempo_ms = ( @@ -68,26 +72,46 @@ async def trigger(self): self.clock_tempo_ts_beat = self.clock_beat self.inlets[1] = Uninit + newevent = None if self.inlets[0] is not Uninit: - self.messages.append((self.clock_beat, rightnow, self.inlets[0])) + beat_fraction = 0 + if self.clock_tempo_ms: + ms_since_clock = 1000 * (rightnow - self.clock_tempo_ts_timestamp).total_seconds() + if self.clock_tempo_ts_beat != self.clock_beat: + ms_since_clock = ( + ms_since_clock + + self.clock_tempo_ms * (self.clock_tempo_ts_beat - self.clock_beat) + ) + beat_fraction = ms_since_clock / self.clock_tempo_ms + newevent = (self.clock_beat + beat_fraction, rightnow, self.inlets[0]) + self.messages.append(newevent) self.messages.sort() self.inlets[0] = Uninit + logm = True if self.clock_beat < beatnow: beatnow = -1 + fudge = 0 + if self.clock_tick_ms and self.clock_tempo_ms: + fudge = 0.05 * self.clock_tick_ms / self.clock_tempo_ms + pending = [ - m[2] for m in self.messages - if m[0] > beatnow and m[0] <= self.clock_beat + m for m in self.messages + if (m[0] > beatnow or (m == newevent and m[0] >= beatnow)) and m[0] <= (self.clock_beat + fudge) ] if not pending: return + # if we grabbed any late events, make sure we don't play them again + beat_max = max(m[0] for m in pending) + self.clock_beat = max(self.clock_beat, beat_max) + if len(pending) == 1: - self.outlets[0] = pending[0] + self.outlets[0] = pending[0][2] else: - self.outlets[0] = MultiOutput(pending) + self.outlets[0] = MultiOutput([p[2] for p in pending]) def register(): From b06fa6e0fc5207d0c3f35dbaf1e5aa7543e94abf Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Fri, 1 Aug 2025 11:51:52 -0400 Subject: [PATCH 16/22] Work on [messagerec] --- mfp/builtins/messagerec.py | 34 +++++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/mfp/builtins/messagerec.py b/mfp/builtins/messagerec.py index 0e876cef..64c6cd19 100644 --- a/mfp/builtins/messagerec.py +++ b/mfp/builtins/messagerec.py @@ -48,9 +48,8 @@ def stop(self): async def trigger(self): rightnow = datetime.now() - beatnow = self.clock_beat or 0 + clock_starting = self.clock_beat or 0 - logm = False if self.inlets[1] is not Uninit: if isinstance(self.inlets[1], (float, int)): self.clock_beat = self.inlets[1] @@ -87,18 +86,34 @@ async def trigger(self): self.messages.append(newevent) self.messages.sort() self.inlets[0] = Uninit - logm = True - - if self.clock_beat < beatnow: - beatnow = -1 + # we want to emit any events that are "due" when quantized to + # the current clock. To make this sound natural, we want to + # look ahead in time to find events that aren't due yet but will + # be due well before the next clock (ones that were likely "late" when + # recorded) fudge = 0 if self.clock_tick_ms and self.clock_tempo_ms: - fudge = 0.05 * self.clock_tick_ms / self.clock_tempo_ms + # fudge factor is .1 of a clock tick. This is quite conservative, + # could probably be as much as .5 + fudge = 0.1 * self.clock_tick_ms / self.clock_tempo_ms + + def test_beat(beat): + if self.clock_beat > clock_starting: + # normal case: clock is moving forward + return ( + clock_starting < beat <= (self.clock_beat + fudge) + ) + else: + # edge case: clock wrapped around + return ( + (beat > clock_starting and beat <= clock_starting + 1) + or beat <= (self.clock_beat + fudge) + ) pending = [ m for m in self.messages - if (m[0] > beatnow or (m == newevent and m[0] >= beatnow)) and m[0] <= (self.clock_beat + fudge) + if test_beat(m[0]) or (m == newevent and m[0] == clock_starting) ] if not pending: @@ -106,7 +121,8 @@ async def trigger(self): # if we grabbed any late events, make sure we don't play them again beat_max = max(m[0] for m in pending) - self.clock_beat = max(self.clock_beat, beat_max) + if (self.clock_beat > clock_starting) or (beat_max <= clock_starting): + self.clock_beat = max(self.clock_beat, beat_max) if len(pending) == 1: self.outlets[0] = pending[0][2] From b6b8ddcdfad715b41ba68c1a0b9ffbde6a3d2b59 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Sat, 2 Aug 2025 12:23:47 -0400 Subject: [PATCH 17/22] Work on quantization behavior and beat wraparound --- mfp/builtins/messagerec.py | 72 +++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/mfp/builtins/messagerec.py b/mfp/builtins/messagerec.py index 64c6cd19..fd18505b 100644 --- a/mfp/builtins/messagerec.py +++ b/mfp/builtins/messagerec.py @@ -10,6 +10,14 @@ from mfp.bang import Uninit from mfp.processor import Processor, MultiOutput from ..mfp_app import MFPApp +from dataclasses import dataclass + +class RecEvent: + def __init__(self, beat, timestamp, event, first_play=True): + self.beat = beat + self.timestamp = timestamp + self.event = event + self.first_play = first_play class MessageRec(Processor): @@ -49,8 +57,12 @@ def stop(self): async def trigger(self): rightnow = datetime.now() clock_starting = self.clock_beat or 0 + clock_arrived = False + # inlet 1 is the clock, should be integers representing the + # beat number if self.inlets[1] is not Uninit: + clock_arrived = True if isinstance(self.inlets[1], (float, int)): self.clock_beat = self.inlets[1] else: @@ -71,22 +83,33 @@ async def trigger(self): self.clock_tempo_ts_beat = self.clock_beat self.inlets[1] = Uninit + # inlet 0 is the event input newevent = None if self.inlets[0] is not Uninit: - beat_fraction = 0 - if self.clock_tempo_ms: - ms_since_clock = 1000 * (rightnow - self.clock_tempo_ts_timestamp).total_seconds() - if self.clock_tempo_ts_beat != self.clock_beat: - ms_since_clock = ( - ms_since_clock - + self.clock_tempo_ms * (self.clock_tempo_ts_beat - self.clock_beat) - ) - beat_fraction = ms_since_clock / self.clock_tempo_ms - newevent = (self.clock_beat + beat_fraction, rightnow, self.inlets[0]) - self.messages.append(newevent) - self.messages.sort() + msg = self.inlets[0] self.inlets[0] = Uninit + if isinstance(msg, (list, tuple)): + for e in msg: + self.messages.append( + RecEvent(e[0], rightnow, e[1], False) + ) + else: + beat_fraction = 0 + if self.clock_tempo_ms: + ms_since_clock = 1000 * (rightnow - self.clock_tempo_ts_timestamp).total_seconds() + if self.clock_tempo_ts_beat != self.clock_beat: + ms_since_clock = ( + ms_since_clock + + self.clock_tempo_ms * (self.clock_tempo_ts_beat - self.clock_beat) + ) + beat_fraction = ms_since_clock / self.clock_tempo_ms + newevent = RecEvent(self.clock_beat + beat_fraction, rightnow, msg, True) + self.messages.append(newevent) + self.messages.sort( + key=lambda ev: (ev.beat, ev.timestamp, ev.event) + ) + # we want to emit any events that are "due" when quantized to # the current clock. To make this sound natural, we want to # look ahead in time to find events that aren't due yet but will @@ -96,7 +119,7 @@ async def trigger(self): if self.clock_tick_ms and self.clock_tempo_ms: # fudge factor is .1 of a clock tick. This is quite conservative, # could probably be as much as .5 - fudge = 0.1 * self.clock_tick_ms / self.clock_tempo_ms + fudge = 0.25 * self.clock_tick_ms / self.clock_tempo_ms def test_beat(beat): if self.clock_beat > clock_starting: @@ -111,23 +134,32 @@ def test_beat(beat): or beat <= (self.clock_beat + fudge) ) - pending = [ - m for m in self.messages - if test_beat(m[0]) or (m == newevent and m[0] == clock_starting) - ] + pending = [] + if clock_arrived: + pending = [ + m for m in self.messages if not m.first_play and test_beat(m.beat) + ] + already_played = [ + m for m in self.messages if m.first_play and test_beat(m.beat) + ] + pending.sort(key=lambda e: (0 if e.beat >= clock_starting else 1, e.beat)) + for p in already_played: + p.first_play = False + elif newevent: + pending = [newevent] if not pending: return # if we grabbed any late events, make sure we don't play them again - beat_max = max(m[0] for m in pending) + beat_max = pending[-1].beat if (self.clock_beat > clock_starting) or (beat_max <= clock_starting): self.clock_beat = max(self.clock_beat, beat_max) if len(pending) == 1: - self.outlets[0] = pending[0][2] + self.outlets[0] = pending[0].event else: - self.outlets[0] = MultiOutput([p[2] for p in pending]) + self.outlets[0] = MultiOutput([p.event for p in pending]) def register(): From 4949c909c061f4b118e181d2c780e13b53884473 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Sat, 2 Aug 2025 16:16:28 -0400 Subject: [PATCH 18/22] Update comment --- mfp/builtins/messagerec.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mfp/builtins/messagerec.py b/mfp/builtins/messagerec.py index fd18505b..3f99784f 100644 --- a/mfp/builtins/messagerec.py +++ b/mfp/builtins/messagerec.py @@ -59,8 +59,8 @@ async def trigger(self): clock_starting = self.clock_beat or 0 clock_arrived = False - # inlet 1 is the clock, should be integers representing the - # beat number + # inlet 1 is the clock, should be numbers representing the + # beat number (incl fractions) if self.inlets[1] is not Uninit: clock_arrived = True if isinstance(self.inlets[1], (float, int)): From 5ffe72ec10807bb18c112808fe88c7c72ff9e023 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Fri, 8 Aug 2025 17:50:06 -0400 Subject: [PATCH 19/22] Tweak spacing for scalable font --- mfp/gui/imgui/app_window/console_panel.py | 2 +- mfp/gui/imgui/app_window/status_line.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mfp/gui/imgui/app_window/console_panel.py b/mfp/gui/imgui/app_window/console_panel.py index cad9d1c7..08a0772f 100644 --- a/mfp/gui/imgui/app_window/console_panel.py +++ b/mfp/gui/imgui/app_window/console_panel.py @@ -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.imgui_global_scale, + 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/status_line.py b/mfp/gui/imgui/app_window/status_line.py index e9039481..9650c9c9 100644 --- a/mfp/gui/imgui/app_window/status_line.py +++ b/mfp/gui/imgui/app_window/status_line.py @@ -105,7 +105,7 @@ def render(app_window): ]) cur = imgui.get_cursor_pos() imgui.set_cursor_pos(( - app_window.window_width - app_window.scaled(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) From 3df9fdf973c5d6a532b7092718f07ba605cbe601 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Sat, 16 Aug 2025 13:44:22 -0400 Subject: [PATCH 20/22] Don't save deleted objects --- mfp/patch_json.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/mfp/patch_json.py b/mfp/patch_json.py index ce218e40..46ba22de 100644 --- a/mfp/patch_json.py +++ b/mfp/patch_json.py @@ -266,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 @@ -283,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 @@ -297,7 +304,11 @@ async def json_serialize(self): if not obj: log.warning("json_serialize: name", objname, "has no bound object") continue - if obj and (isinstance(obj, MFPApp) or not obj.save_to_patch): + if ( + isinstance(obj, MFPApp) + or not obj.save_to_patch + or not o.status == Processor.READY + ): continue bindings[objname] = obj.obj_id From bd0eef1140f52277da669c06e8220c9018c8dc70 Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Mon, 8 Sep 2025 14:09:30 -0400 Subject: [PATCH 21/22] Add -m/--magnification option for initial HiDPI comp --- mfp/gui/imgui/app_window/app_window.py | 13 +++++++++++++ mfp/gui/modes/global_mode.py | 9 +-------- mfp/gui/tile_manager.py | 8 +++++--- mfp/gui_main.py | 6 ++++++ mfp/mfp_app.py | 2 ++ mfp/mfp_main.py | 5 +++++ 6 files changed, 32 insertions(+), 11 deletions(-) diff --git a/mfp/gui/imgui/app_window/app_window.py b/mfp/gui/imgui/app_window/app_window.py index 822da934..ccf67733 100644 --- a/mfp/gui/imgui/app_window/app_window.py +++ b/mfp/gui/imgui/app_window/app_window.py @@ -566,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/modes/global_mode.py b/mfp/gui/modes/global_mode.py index ed6129cd..92c44286 100644 --- a/mfp/gui/modes/global_mode.py +++ b/mfp/gui/modes/global_mode.py @@ -330,14 +330,7 @@ def init_bindings(cls): async def set_app_scale(self, new_scale): - scale_ratio = new_scale / self.window.imgui_global_scale - self.window.imgui_global_scale = new_scale - for p in self.window.patches: - if p.display_info: - p.display_info.view_zoom *= scale_ratio - self.window.viewport_zoom_set = True - self.window.viewport_pos_set = True - return True + return self.window.set_app_scale(new_scale) async def toggle_panel_mode(self): patch = self.window.selected_patch 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") From ace424aa3da2a5d668ea0e1454357392f96e309c Mon Sep 17 00:00:00 2001 From: Bill Gribble Date: Tue, 23 Sep 2025 16:18:57 -0400 Subject: [PATCH 22/22] Update imgui-bundle to 1.92.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4d860743..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.92.0 +imgui-bundle==1.92.3 munch==4.0.0 numpy==2.2.4 pillow==11.1.0