Files
oak-editor/docs/zh/modularization-plan/07-oliveui.md
T

9.9 KiB
Raw Blame History

liboliveui.so — UI 层

依赖libolivecore.so, libolivecodec.so, libolivenode.so, liboliverender.so, liboliveaudio.so
外部依赖Qt::Widgets, Qt::OpenGL, Qt::OpenGLWidgets, KDDockWidgets
包含源码app/widget/, app/panel/, app/window/, app/dialog/, app/tool/, app/ui/
当前状态:单体 OBJECT 库的一部分
改造难度(中等,但代码量大)


1. 当前状态分析

UI 层是代码量最大的模块,但耦合方向是单向的UI 依赖下层(node/render/codec),下层不依赖 UI。这使得 UI 层的拆分相对直接。

组件 说明
widget/ 40+ 自定义 Qt Widget(节点视图、时间线、播放控制、颜色轮等)
panel/ 基于 KDDockWidgets 的可停靠面板包装
window/mainwindow/ 主窗口
dialog/ 模态对话框(导出、首选项、项目属性等)
tool/ 工具枚举
ui/ 图标、光标、样式表、翻译资源

关键问题

  • Core 类(core.h/cpp)混合了业务逻辑和 UI 逻辑(StartGUI(), main_window_ 等)。
  • RenderManagerRenderTicketWatcher 使用 Qt 信号通知 UI。
  • 大量 UI 类直接包含 node/render/ 的 C++ 头文件。

策略

  • liboliveui.so 的 C API 不需要非常完善,因为 UI 层大概率仍然与主进程一同编译UI 是主进程的核心)。
  • 但为了保持架构一致性,仍定义 C API 用于:
    1. 第三方脚本/插件通过 C API 操作 UI(未来扩展)。
    2. 单元测试通过 C API 驱动 UI(自动化测试)。
  • 主进程中的 UI 代码可以继续使用 C++ 直接包含下层头文件,不必全部改为 C API 调用。这是因为 UI 层在最顶层,不需要被其他模块依赖。

修正策略liboliveui.so 的拆分重点在于:

  1. 将 UI 代码从单体 OBJECT 库移出,编译为独立的 liboliveui.so
  2. 主进程显式加载 liboliveui.so
  3. UI 层内部继续使用 C++ 直接调用下层(node/render 等),只在跨库边界处遵循 ABI 规则。

2. C API 设计(精简版)

UI 层的 C API 不需要覆盖所有 Widget,只需提供应用级入口和关键面板操作:

2.1 头文件:c_api/include/olive/ui_api.h

#ifndef OLIVE_UI_API_H
#define OLIVE_UI_API_H

#ifdef __cplusplus
extern "C" {
#endif

#include "core_api.h"
#include "node_api.h"

#define OLIVE_UI_API_VERSION 1

#ifdef OLIVE_BUILDING_UI
#  define OLIVE_UI_API __attribute__((visibility("default")))
#else
#  define OLIVE_UI_API
#endif

/* ========== 不透明类型 ========== */
typedef struct OliveApplication    OliveApplication;
typedef struct OliveMainWindow     OliveMainWindow;
typedef struct OliveViewerPanel    OliveViewerPanel;
typedef struct OliveTimelinePanel  OliveTimelinePanel;
typedef struct OliveNodeEditorPanel OliveNodeEditorPanel;

/* ========== API 版本 ========== */
OLIVE_UI_API int olive_ui_api_version(void);

/* ========== 应用生命周期 ========== */
OLIVE_UI_API OliveApplication* olive_ui_app_create(int argc, char** argv);
OLIVE_UI_API int olive_ui_app_exec(OliveApplication* app);
OLIVE_UI_API void olive_ui_app_quit(OliveApplication* app);
OLIVE_UI_API void olive_ui_app_destroy(OliveApplication* app);

/* ========== 主窗口 ========== */
OLIVE_UI_API OliveMainWindow* olive_ui_main_window_create(OliveApplication* app);
OLIVE_UI_API void olive_ui_main_window_destroy(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_main_window_show(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_main_window_set_fullscreen(OliveMainWindow* win, int fullscreen);

/* ========== 项目操作 ========== */
OLIVE_UI_API int olive_ui_open_project(OliveMainWindow* win, const char* filename);
OLIVE_UI_API int olive_ui_save_project(OliveMainWindow* win, const char* filename);
OLIVE_UI_API int olive_ui_import_footage(OliveMainWindow* win, const char** filenames, int count);

/* ========== 查看器(Viewer ========== */
OLIVE_UI_API OliveViewerPanel* olive_ui_get_active_viewer(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_viewer_set_time(OliveViewerPanel* viewer, OliveRational time);
OLIVE_UI_API void olive_ui_viewer_play(OliveViewerPanel* viewer);
OLIVE_UI_API void olive_ui_viewer_pause(OliveViewerPanel* viewer);
OLIVE_UI_API void olive_ui_viewer_stop(OliveViewerPanel* viewer);

/* ========== 时间线 ========== */
OLIVE_UI_API OliveTimelinePanel* olive_ui_get_active_timeline(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_timeline_set_time(OliveTimelinePanel* timeline, OliveRational time);
OLIVE_UI_API void olive_ui_timeline_set_work_area(OliveTimelinePanel* timeline,
                                                   OliveRational in,
                                                   OliveRational out);

/* ========== 节点编辑器 ========== */
OLIVE_UI_API OliveNodeEditorPanel* olive_ui_get_node_editor(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_node_editor_set_graph(OliveNodeEditorPanel* editor,
                                                  OliveNodeGraph* graph);

/* ========== 导出对话框 ========== */
OLIVE_UI_API int olive_ui_show_export_dialog(OliveMainWindow* win,
                                              OliveViewerOutput* viewer_output,
                                              const char* default_filename);

/* ========== 状态栏消息 ========== */
OLIVE_UI_API void olive_ui_show_status_message(OliveMainWindow* win,
                                                const char* message,
                                                int timeout_ms);

#ifdef __cplusplus
}
#endif

#endif  // OLIVE_UI_API_H

3. CMake 改造

# 由于 UI 层代码分散在 widget/, panel/, window/, dialog/, tool/, ui/ 多个目录,
# 需要在 app/CMakeLists.txt 中统一聚合。

set(UI_INTERNAL_SOURCES
  # widget/
  widget/viewer/viewerwidget.cpp widget/viewer/viewerwidget.h
  widget/timelinewidget/timelinewidget.cpp widget/timelinewidget/timelinewidget.h
  widget/nodeview/nodeview.cpp widget/nodeview/nodeview.h
  # ... 所有 widget 源文件

  # panel/
  panel/viewer/viewerpanel.cpp panel/viewer/viewerpanel.h
  panel/timeline/timelinepanel.cpp panel/timeline/timelinepanel.h
  panel/node/nodepanel.cpp panel/node/nodepanel.h
  # ... 所有 panel 源文件

  # window/
  window/mainwindow/mainwindow.cpp window/mainwindow/mainwindow.h

  # dialog/
  dialog/export/exportdialog.cpp dialog/export/exportdialog.h
  dialog/preferences/preferencesdialog.cpp dialog/preferences/preferencesdialog.h
  # ... 所有 dialog 源文件

  # tool/
  tool/tool.cpp tool/tool.h

  # ui/ 资源(.qrc 等)
  # ...
)

set(UI_API_SOURCES
  ${CMAKE_SOURCE_DIR}/c_api/src/ui_api.cpp
)

add_library(oliveui SHARED
  ${UI_INTERNAL_SOURCES}
  ${UI_API_SOURCES}
)

target_compile_definitions(oliveui PRIVATE OLIVE_BUILDING_UI)

target_include_directories(oliveui
  PRIVATE
    ${CMAKE_SOURCE_DIR}/app
    ${CMAKE_SOURCE_DIR}/c_api/include
    ${CMAKE_SOURCE_DIR}/ext/KDDockWidgets/src
  PUBLIC
    $<INSTALL_INTERFACE:include>
)

target_link_libraries(oliveui
  PUBLIC
    olivenode
    oliverender
    olivecodec
    oliveaudio
    olivecore
    oliveplugin
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Gui
    Qt${QT_VERSION_MAJOR}::Widgets
    Qt${QT_VERSION_MAJOR}::OpenGL
    Qt${QT_VERSION_MAJOR}::OpenGLWidgets
    KDAB::kddockwidgets
)

set_target_properties(oliveui PROPERTIES
  CXX_VISIBILITY_PRESET hidden
  VISIBILITY_INLINES_HIDDEN YES
)

install(TARGETS oliveui DESTINATION lib)
install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/ui_api.h DESTINATION include/olive)

4. 小步快跑实施步骤

Step 0: 解耦 Core 类(2 天)

  • 分析 core.h/cpp 中哪些属于 UI 逻辑(StartGUI(), main_window_, ImportFiles() 等),哪些属于业务逻辑。
  • 将 UI 相关逻辑迁移到 liboliveui.so 内部的一个 UiCore 类中。
  • 保留 Core 类中纯业务逻辑(如 FootageFileDialogFilter, CreateNewSequenceForProject)。

验收标准core.h 不再包含 mainwindow.hprojectexplorer.h 等 UI 头文件。

Step 1: 聚合 UI 源码(1 天)

  • app/CMakeLists.txt 中聚合所有 UI 相关源文件(widget/, panel/, window/, dialog/, tool/, ui/)。
  • 确保 liboliveui.so 可以编译。

验收标准liboliveui.so 编译成功。

Step 2: 主进程加载 UI 库(1 天)

  • 修改 main.cpp:先通过 ModuleLoader 加载 liboliveui.so,然后调用 olive_ui_app_createolive_ui_main_window_create
  • 若动态加载失败,回退到静态链接模式。

验收标准:主程序启动时日志显示成功加载 ui 模块,并正常显示主窗口。

Step 3: C API 实现(按需,23 天)

  • 实现 olive_ui_app_create/exec/quit/destroy
  • 实现 olive_ui_main_window_create/show
  • 实现项目操作:open_project, save_project, import_footage
  • 其他 UI C API 根据测试/脚本需求逐步实现。

验收标准:可以通过一个外部测试程序加载 liboliveui.so 并打开主窗口。


5. 风险与回退

风险 对策
UI 代码量巨大,聚合时容易遗漏源文件 编写脚本自动收集 widget/, panel/, window/, dialog/ 下的所有 .cpp/.h 文件,或在 CMake 中保持原有的 add_subdirectory 结构,只是最终输出为 SHARED 而非 OBJECT。
KDDockWidgets 的符号跨动态库 KDDockWidgets 以静态库形式链接进 liboliveui.so,其符号不外泄。确保 liboliveui.soCXX_VISIBILITY_PRESET hidden
Qt 资源文件(.qrc)在动态库中的加载 .qrc 编译进 liboliveui.so,Qt 的资源系统在动态库中工作正常。确保 Q_INIT_RESOURCE() 在库加载时被调用。
Core 类的信号槽跨模块 Core 保留在主进程,UiCoreliboliveui.so 中。两者通过 C API 或 Qt 的跨进程信号(如果未来需要)通信。初期保持简单:主进程直接调用 UI 的 C API。