Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use forked pyimgui for imgui.input_text_multiline() widget which supports paragraph indentation #1

Merged
merged 11 commits into from
Sep 15, 2020

Conversation

NaleRaphael
Copy link
Owner

Currently, paragraph indentation is not supported in the original version of pyimgui (swistakm/pyimgui). In order to implement this facility, we have to fork that repository and extend it by ourselves.

The ideal approach to extend pyimgui is writing a ctypes wrapper for ImGuiInputTextCallbackData, so that users can access those data in it or manipulate them with the help of those member functions (e.g. DeleteChars, InsertChars).

However, we had tried this approach for days and with no luck. Because those data are actually new Python objects created by the ctypes wrapper instead of the real data used by that widget (literally, their memory addresses are different), and it seems there is no chance to access the memory address of real data from these Python objects either.

Though we had also tried to manipulate those data by helper functions, application always crashed with segmentation fault (access violation) when we were trying to invoke them. With the help of gdb, we've known that it was resulted by invoking a function of which the address is actually pointing to an unknown location (part of message generated by backtrace is shown as below).

#0  0x00007ffff69ea920 in ?? ()        # <- not a valid address for the function we want to invoke
#1  0x00007ffff6419ec0 in ffi_call_unix64 ()
   from /home/nale/anaconda3/envs/py37/lib/python3.7/lib-dynload/../../libffi.so.6
#2  0x00007ffff641987d in ffi_call ()
   from /home/nale/anaconda3/envs/py37/lib/python3.7/lib-dynload/../../libffi.so.6
#3  0x00007ffff662eede in _call_function_pointer (argcount=3, resmem=0x7fffffffaa60, 
    restype=<optimized out>, atypes=0x7fffffffaa00, avalues=0x7fffffffaa30, 
    pProc=0x7ffff69ea920, flags=4353)
    at /usr/local/src/conda/python-3.7.4/Modules/_ctypes/callproc.c:827
#4  _ctypes_callproc () at /usr/local/src/conda/python-3.7.4/Modules/_ctypes/callproc.c:1184
#5  0x00007ffff662f914 in PyCFuncPtr_call ()
    at /usr/local/src/conda/python-3.7.4/Modules/_ctypes/_ctypes.c:3971
#6  0x00005555556c08fb in _PyObject_FastCallKeywords ()
    at /tmp/build/80754af9/python_1565725737370/work/Objects/call.c:199
#7  0x00005555557246e8 in call_function (kwnames=0x0, oparg=<optimized out>, 
    pp_stack=<synthetic pointer>)
    at /tmp/build/80754af9/python_1565725737370/work/Python/ceval.c:4619
#8  _PyEval_EvalFrameDefault ()
    at /tmp/build/80754af9/python_1565725737370/work/Python/ceval.c:3093
#9  0x0000555555668539 in _PyEval_EvalCodeWithName ()
    at /tmp/build/80754af9/python_1565725737370/work/Python/ceval.c:3930
#10 0x0000555555669424 in PyEval_EvalCodeEx ()
    at /tmp/build/80754af9/python_1565725737370/work/Python/ceval.c:3959
#11 0x00007ffff685bf18 in __Pyx_PyFunction_FastCallDict (func=func@entry=0x7fffe2d243b0, 
    args=args@entry=0x7fffffffaf70, nargs=nargs@entry=1, kwargs=0x0) at imgui/core.cpp:87347
#12 0x00007ffff6885880 in __Pyx_PyObject_CallOneArg (arg=<optimized out>, func=0x7fffe2d243b0)
    at imgui/core.cpp:87893
#13 __pyx_pf_5imgui_4core_24InputTextCallbackWrapper_2inner_func (__pyx_v_self=<optimized out>, 
    __pyx_v_user_data=0x7fffe2d24290) at imgui/core.cpp:40362
#14 __pyx_pw_5imgui_4core_24InputTextCallbackWrapper_3inner_func (__pyx_v_self=<optimized out>, 
    __pyx_v_user_data=0x7fffe2d24290) at imgui/core.cpp:40325
#15 0x0000555555689491 in _PyMethodDef_RawFastCallDict ()
    at /tmp/build/80754af9/python_1565725737370/work/Objects/call.c:497
#16 0x00005555556897c1 in _PyCFunction_FastCallDict (func=0x7fffe2bffbe0, args=<optimized out>, 
    nargs=<optimized out>, kwargs=<optimized out>)
    at /tmp/build/80754af9/python_1565725737370/work/Objects/call.c:586
#17 0x00007ffff662e961 in _CallPythonObject (pArgs=<optimized out>, flags=<optimized out>, 
    converters=<optimized out>, callable=<optimized out>, setfunc=<optimized out>, 
    restype=<optimized out>, mem=<optimized out>)
    at /usr/local/src/conda/python-3.7.4/Modules/_ctypes/callbacks.c:232
#18 closure_fcn () at /usr/local/src/conda/python-3.7.4/Modules/_ctypes/callbacks.c:292
#19 0x00007ffff6419c60 in ffi_closure_unix64_inner ()
   from /home/nale/anaconda3/envs/py37/lib/python3.7/lib-dynload/../../libffi.so.6
#20 0x00007ffff641a028 in ffi_closure_unix64 ()
   from /home/nale/anaconda3/envs/py37/lib/python3.7/lib-dynload/../../libffi.so.6
#21 0x00007ffff6918b09 in ImGui::InputTextEx (label=0x7fffee5508f0 "##comment", 
    buf=0x555556909f80 "abc", buf_size=4096, size_arg=..., flags=1049856,
    callback=0x7ffff7fe9278, callback_user_data=0x555556909f80)
    at imgui-cpp/imgui_widgets.cpp:3505
#22 0x00007ffff68bcf94 in __pyx_pf_5imgui_4core_220input_text_multiline (
    __pyx_self=<optimized out>, __pyx_v_callback=<optimized out>, __pyx_v_flags=1280, 
    __pyx_v_height=-5, __pyx_v_width=0, __pyx_v_buffer_length=4096, 
    __pyx_v_value=<optimized out>, __pyx_v_label=<optimized out>) at imgui/core.cpp:41162
#23 __pyx_pw_5imgui_4core_221input_text_multiline (__pyx_self=<optimized out>, 
    __pyx_args=<optimized out>, __pyx_kwds=<optimized out>) at imgui/core.cpp:40979

Hence that we tried another approach: writing an Cython extension which includes cimgui.pxd, *.pxd made by swistakm/pyimgui as dependencies. The benefit of this approach is that we don't have to modify code in swistakm/pyimgui directly.
Though we actually had made an extension, application always terminated by the following exception:

imgui.core.ImGuiError: ImGui assertion error (g.CurrentWindowStack.Size == 1) at imgui-cpp/imgui.cpp:3476

After an investigation, we found that it's resulted by the failure of acquiring context (ImGuiContext) while invoking our_extension.input_text_multline(). Since we can make sure that imgui.new_frame() has been called, the best guess is that we have to share the context created by pyimgui to our extension. But currently we haven't figured out how to achieve it.

The last approach, which is also the one we adopted, is extending code in a forked pyimgui repository directly.

With the issue of manipulating data from Python side (metioned in the paragraph of implementing ctypes wrapper above), we have figure out how to provide a cython wrapper which takes a Python callback function and can be used in imgui. Therefore, we provide a configuration class for user to pass desired settings for callback instead. Though this approach is less flexible, it works fine and more efficiently then implementing the same feature in pure Python.

More details are recorded in commits for this PR.

In previous revision, loaded `pyimgui` module is not the one forked
by us, it is the one which is already installed in site-packages
instead. The reason why this happened is that those `PathFinder`s
in `sys.meta_path` search the site-packages first, and it returns
result right after a module spec is found.

This issue can be solved by making a custom `PathFinder`, but things
can be solved in a easier way. We can register the original module
name into `sys.modules` with our desired module spec before calling
`spec.loader.exec_module()`, e.g.

```python
spec = importlib.util.spec_from_file_location('foobar', fn)
mod = importlib.util.module_from_spec(spec)
sys.modules['foobar'] = mod     # <- register name here
spec.loader.exec_module(mod)
```

And this trick can make the loader get our desired module when it
is executing `_find_and_load_unlocked()`.
Major updates:
- Modify flags and add configs to `imgui.input_text_multiline()`
  to make it able to convert TAB key with spaces. Note that this
  feature requires installing `pyimgui` of the version forked by
  us instead of the original one (`swistakm/pyimgui`) since those
  callback facilities are not supported in it yet.

  However, in our implementation, callback are not implemented by
  user. Those limited callback features can just be enabled by
  passing a `imgui.core.InputTextCallbackConfig` object into the
  widget `imgui.input_text_multiline()`. See also the following
  link for details:

  NaleRaphael/pyimgui@96d65c7

Minor updates/revisions:
- Missing `imgui.pop_item_width()` calls in
  `CodeNodeCreatorWindow.render()` and
  `CodeNodeViewer.finalize_canvas()` are now added.

- Add input textbox for setting `url` in `CodeNodeCreatorWindow`.

- Arguments `path` and `url` are now passed into `Snippet.__init__()`.
In Unix-like system, name of shared objects would be:
`[FILENAME].[PYTHON_VERSION_AND_PLATFORM].so`

So that the proper pattern for searching shared objects in forked
`pyimgui/imgui` repository should be `dir_imgui.rglob('core.*.so')`.

Besides, we should raise `ImportError` with different message to
indicate the actual cause of execution failure.
In order to make this package runable by command `run codememo` while
using `gdb python`, we have to change the import statement from

```python
from .app import Application
```

to:

```python
from codememo.app import Application
```

Otherwise, we will get `ModuleNotFoundError: '__main__' is not a package`.
Facilities for text indentation are implemented in that forked
repository, see also the link attached below.

NaleRaphael/pyimgui@7c6aaff
User now can use the following command to install this package with
forked pyimgui which currently supports callbacks for text indentation
in `imgui.input_text_multiline()` widget.

```bash
$ pip install -v --global-option="--use-forked-pyimgui" ./
```
`setuptools.command.install` one

In order to make those built files of `pyimgui` able to be installed
to the site-package folder, we should use a custom command class
inheriting from `setuptools.command.install` instead.

Besides, we also have to generate a destination-source pairs as
the argument `data_files` for `setup()` command. See also
`prepare_data_files()` for details.

And the reason why `sys.argv` based approach implemented in previous
revision does not work for this case is that operations written in
`if ... in sys.argv: ...` are not actually executed before the
installation of this package itself.
`--install-option` is for `InstallationCommand` instead.
…yimgui`

There is a critical problem in previous implementation: value of
`data_files` are determined before running operations defined in
pre-intallation stage. Hence that `prepare_data_files()` does not
return desired result. (the reason why it worked previously is that
forked `pyimgui` repository was pulled already)

So that we fallback to `sys.argv` based approach. But we execute
those operations for pre-installation right before creating the
dictionary `metadata`.
Update it for solving the issue of indentation. See also the
following link for details:

NaleRaphael/pyimgui@af56524
@NaleRaphael NaleRaphael reopened this Sep 15, 2020
@NaleRaphael NaleRaphael merged commit e0bf01d into master Sep 15, 2020
NaleRaphael added a commit to NaleRaphael/pyimgui-colortextedit that referenced this pull request Oct 3, 2020
Note that we've added an function `get_current_context_ptr()`
to pass pointer of `ImGuiContext` since we cannot access the actual
memory address of it by the value returned by a Cython-wrapped
`get_current_context()` function. (just like the reason mentioned
in [1], a C object will be wrapped by a Python object, so that
we cannot access its real address even if we have defined a `_ptr`
field in the Cython extension class)

Besides, those functions for setting/getting context should also
be implemented in `pyimgui` (i.e. `get_current_context_ptr()` and
`set_current_context_ptr()`, but at least the former one).

And note that this might not be a good solution since we have to
expose the real address of internal object, so that these functions
are classified as workarounds and we should figure out a better
ones in the future.

[1]: NaleRaphael/codememo#1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant