diff --git a/dearpygui/_dearpygui.pyi b/dearpygui/_dearpygui.pyi index c5c1acb16..f5293d4d7 100644 --- a/dearpygui/_dearpygui.pyi +++ b/dearpygui/_dearpygui.pyi @@ -54,6 +54,10 @@ def add_checkbox(*, label: str ='', user_data: Any ='', use_internal_label: bool """Adds a checkbox.""" ... +def add_mixed_state_checkbox(*, label: str ='', user_data: Any ='', use_internal_label: bool ='', tag: Union[int, str] ='', indent: int ='', parent: Union[int, str] ='', before: Union[int, str] ='', source: Union[int, str] ='', payload_type: str ='', callback: Callable ='', drag_callback: Callable ='', drop_callback: Callable ='', show: bool ='', enabled: bool ='', pos: Union[List[int], Tuple[int, ...]] ='', filter_key: str ='', tracked: bool ='', track_offset: float ='', default_value: int ='') -> Union[int, str]: + """Adds a mixed state (tristate) checkbox. Value is -1 (mixed/indeterminate), 0 (unchecked), or 1 (checked).""" + ... + def add_child_window(*, label: str ='', user_data: Any ='', use_internal_label: bool ='', tag: Union[int, str] ='', width: int ='', height: int ='', indent: int ='', parent: Union[int, str] ='', before: Union[int, str] ='', payload_type: str ='', drop_callback: Callable ='', show: bool ='', pos: Union[List[int], Tuple[int, ...]] ='', filter_key: str ='', tracked: bool ='', track_offset: float ='', border: bool ='', autosize_x: bool ='', autosize_y: bool ='', no_scrollbar: bool ='', horizontal_scrollbar: bool ='', menubar: bool ='', no_scroll_with_mouse: bool ='', flattened_navigation: bool ='', always_use_window_padding: bool ='', resizable_x: bool ='', resizable_y: bool ='', always_auto_resize: bool ='', frame_style: bool ='', auto_resize_x: bool ='', auto_resize_y: bool ='') -> Union[int, str]: """Adds an embedded child window. Will show scrollbars when items do not fit. About using auto_resize/resizable flags: size measurement for a given axis is only performed when the child window is within visible boundaries, or is just appearing and it won't update its auto-size while clipped. While not perfect, it is a better default behavior as the always-on performance gain is more valuable than the occasional 'resizing after becoming visible again' glitch. You may also use always_auto_resize to force an update even when child window is not in view. However doing so will degrade performance. Remember that combining both auto_resize_x and auto_resize_y defeats purpose of a scrolling region and is NOT recommended.""" ... diff --git a/dearpygui/dearpygui.py b/dearpygui/dearpygui.py index e68cd6cdd..bd308ecb7 100644 --- a/dearpygui/dearpygui.py +++ b/dearpygui/dearpygui.py @@ -3473,6 +3473,41 @@ def add_checkbox(*, label: str =None, user_data: Any =None, use_internal_label: return internal_dpg.add_checkbox(label=label, user_data=user_data, use_internal_label=use_internal_label, tag=tag, indent=indent, parent=parent, before=before, source=source, payload_type=payload_type, callback=callback, drag_callback=drag_callback, drop_callback=drop_callback, show=show, enabled=enabled, pos=pos, filter_key=filter_key, tracked=tracked, track_offset=track_offset, default_value=default_value, **kwargs) +def add_mixed_state_checkbox(*, label: str =None, user_data: Any =None, use_internal_label: bool =True, tag: Union[int, str] =0, indent: int =-1, parent: Union[int, str] =0, before: Union[int, str] =0, source: Union[int, str] =0, payload_type: str ='$$DPG_PAYLOAD', callback: Callable =None, drag_callback: Callable =None, drop_callback: Callable =None, show: bool =True, enabled: bool =True, pos: Union[List[int], Tuple[int, ...]] =[], filter_key: str ='', tracked: bool =False, track_offset: float =0.5, default_value: int =0, mixed_click_value: int =1, **kwargs) -> Union[int, str]: + """ Adds a mixed state (tristate) checkbox. Value is -1 (mixed/indeterminate), 0 (unchecked), or 1 (checked). + + Args: + label (str, optional): Overrides 'name' as label. + user_data (Any, optional): User data for callbacks + use_internal_label (bool, optional): Use generated internal label instead of user specified (appends ### uuid). + tag (Union[int, str], optional): Unique id used to programmatically refer to the item.If label is unused this will be the label. + indent (int, optional): Offsets the widget to the right the specified number multiplied by the indent style. + parent (Union[int, str], optional): Parent to add this item to. (runtime adding) + before (Union[int, str], optional): This item will be displayed before the specified item in the parent. + source (Union[int, str], optional): Overrides 'id' as value storage key. + payload_type (str, optional): Sender string type must be the same as the target for the target to run the payload_callback. + callback (Callable, optional): Registers a callback. + drag_callback (Callable, optional): Registers a drag callback for drag and drop. + drop_callback (Callable, optional): Registers a drop callback for drag and drop. + show (bool, optional): Attempt to render widget. + enabled (bool, optional): Turns off functionality of widget and applies the disabled theme. + pos (Union[List[int], Tuple[int, ...]], optional): Places the item relative to window coordinates, [0,0] is top left. + filter_key (str, optional): Used by filter widget. + tracked (bool, optional): Scroll tracking + track_offset (float, optional): 0.0f:top, 0.5f:center, 1.0f:bottom + default_value (int, optional): Sets the default value: -1 for mixed/indeterminate, 0 for unchecked, 1 for checked + mixed_click_value (int, optional): Sets the value when clicking the checkbox in mixed state: 0 for unchecked, 1 for checked + id (Union[int, str], optional): (deprecated) + Returns: + Union[int, str] + """ + + if 'id' in kwargs.keys(): + warnings.warn('id keyword renamed to tag', DeprecationWarning, 2) + tag=kwargs['id'] + + return internal_dpg.add_mixed_state_checkbox(label=label, user_data=user_data, use_internal_label=use_internal_label, tag=tag, indent=indent, parent=parent, before=before, source=source, payload_type=payload_type, callback=callback, drag_callback=drag_callback, drop_callback=drop_callback, show=show, enabled=enabled, pos=pos, filter_key=filter_key, tracked=tracked, track_offset=track_offset, default_value=default_value, mixed_click_value=mixed_click_value, **kwargs) + def add_child_window(*, label: str =None, user_data: Any =None, use_internal_label: bool =True, tag: Union[int, str] =0, width: int =0, height: int =0, indent: int =-1, parent: Union[int, str] =0, before: Union[int, str] =0, payload_type: str ='$$DPG_PAYLOAD', drop_callback: Callable =None, show: bool =True, pos: Union[List[int], Tuple[int, ...]] =[], filter_key: str ='', tracked: bool =False, track_offset: float =0.5, border: bool =True, autosize_x: bool =False, autosize_y: bool =False, no_scrollbar: bool =False, horizontal_scrollbar: bool =False, menubar: bool =False, no_scroll_with_mouse: bool =False, flattened_navigation: bool =True, always_use_window_padding: bool =False, resizable_x: bool =False, resizable_y: bool =False, always_auto_resize: bool =False, frame_style: bool =False, auto_resize_x: bool =False, auto_resize_y: bool =False, **kwargs) -> Union[int, str]: """ Adds an embedded child window. Will show scrollbars when items do not fit. About using auto_resize/resizable flags: size measurement for a given axis is only performed when the child window is within visible boundaries, or is just appearing and it won't update its auto-size while clipped. While not perfect, it is a better default behavior as the always-on performance gain is more valuable than the occasional 'resizing after becoming visible again' glitch. You may also use always_auto_resize to force an update even when child window is not in view. However doing so will degrade performance. Remember that combining both auto_resize_x and auto_resize_y defeats purpose of a scrolling region and is NOT recommended. diff --git a/dearpygui/demo.py b/dearpygui/demo.py index 44686bcec..c436b3e36 100644 --- a/dearpygui/demo.py +++ b/dearpygui/demo.py @@ -1,8 +1,9 @@ -import dearpygui.dearpygui as dpg import math -from math import sin, cos, log10 import random import webbrowser +from math import cos, log10, sin + +import dearpygui.dearpygui as dpg count_2d_histogram = 50_000 xybin_2d_histogram = [100, 100] @@ -300,6 +301,7 @@ def _log(sender, app_data, user_data): dpg.add_button(label="Hold me button", callback=_log, repeat=True) dpg.add_checkbox(label="checkbox", callback=_log) + dpg.add_mixed_state_checkbox(label="mixed state checkbox", callback=_log, default_value=-1) dpg.add_radio_button(("radio a", "radio b", "radio c"), callback=_log, horizontal=True) dpg.add_selectable(label="selectable", callback=_log) diff --git a/src/mvAppItem.cpp b/src/mvAppItem.cpp index 1723072b2..c1c8db345 100644 --- a/src/mvAppItem.cpp +++ b/src/mvAppItem.cpp @@ -297,6 +297,7 @@ CanItemTypeBeHovered(mvAppItemType type) switch (type) { case mvAppItemType::mvCheckbox: + case mvAppItemType::mvMixedStateCheckbox: case mvAppItemType::mvCombo: case mvAppItemType::mvDragInt: case mvAppItemType::mvDragIntMulti: @@ -359,6 +360,7 @@ CanItemTypeBeActive(mvAppItemType type) switch (type) { case mvAppItemType::mvCheckbox: + case mvAppItemType::mvMixedStateCheckbox: case mvAppItemType::mvCombo: case mvAppItemType::mvDragInt: case mvAppItemType::mvDragIntMulti: @@ -410,6 +412,7 @@ CanItemTypeBeFocused(mvAppItemType type) switch (type) { case mvAppItemType::mvCheckbox: + case mvAppItemType::mvMixedStateCheckbox: case mvAppItemType::mvCombo: case mvAppItemType::mvDragInt: case mvAppItemType::mvDragIntMulti: @@ -462,6 +465,7 @@ CanItemTypeBeClicked(mvAppItemType type) switch (type) { case mvAppItemType::mvCheckbox: + case mvAppItemType::mvMixedStateCheckbox: case mvAppItemType::mvCombo: case mvAppItemType::mvDragInt: case mvAppItemType::mvDragFloat: @@ -516,6 +520,7 @@ CanItemTypeBeVisible(mvAppItemType type) switch (type) { case mvAppItemType::mvCheckbox: + case mvAppItemType::mvMixedStateCheckbox: case mvAppItemType::mvCombo: case mvAppItemType::mvDragInt: case mvAppItemType::mvDragIntMulti: @@ -619,6 +624,7 @@ CanItemTypeBeActivated(mvAppItemType type) switch (type) { case mvAppItemType::mvCheckbox: + case mvAppItemType::mvMixedStateCheckbox: case mvAppItemType::mvCombo: case mvAppItemType::mvDragInt: case mvAppItemType::mvDragIntMulti: @@ -669,6 +675,7 @@ CanItemTypeBeDeactivated(mvAppItemType type) switch (type) { case mvAppItemType::mvCheckbox: + case mvAppItemType::mvMixedStateCheckbox: case mvAppItemType::mvCombo: case mvAppItemType::mvDragInt: case mvAppItemType::mvDragIntMulti: @@ -746,7 +753,8 @@ CanItemTypeBeDeactivatedAE(mvAppItemType type) case mvAppItemType::mvColorMapSlider: case mvAppItemType::mvColorPicker: case mvAppItemType::mvGroup: - case mvAppItemType::mvCheckbox: return true; + case mvAppItemType::mvCheckbox: + case mvAppItemType::mvMixedStateCheckbox: return true; default: return false; } @@ -772,6 +780,7 @@ CanItemTypeHaveRectMin(mvAppItemType type) switch (type) { case mvAppItemType::mvCheckbox: + case mvAppItemType::mvMixedStateCheckbox: case mvAppItemType::mvCombo: case mvAppItemType::mvDragInt: case mvAppItemType::mvDragIntMulti: @@ -835,6 +844,7 @@ CanItemTypeHaveRectSize(mvAppItemType type) switch (type) { case mvAppItemType::mvCheckbox: + case mvAppItemType::mvMixedStateCheckbox: case mvAppItemType::mvCombo: case mvAppItemType::mvDragInt: case mvAppItemType::mvDragIntMulti: @@ -897,6 +907,7 @@ CanItemTypeHaveContAvail(mvAppItemType type) switch (type) { case mvAppItemType::mvCheckbox: + case mvAppItemType::mvMixedStateCheckbox: case mvAppItemType::mvCombo: case mvAppItemType::mvDragInt: case mvAppItemType::mvDragIntMulti: @@ -1141,7 +1152,8 @@ DearPyGui::GetEntityValueType(mvAppItemType type) case mvAppItemType::mvIntValue: case mvAppItemType::mvSliderInt: case mvAppItemType::mvInputInt: - case mvAppItemType::mvDragInt: return StorageValueTypes::Int; + case mvAppItemType::mvDragInt: + case mvAppItemType::mvMixedStateCheckbox: return StorageValueTypes::Int; case mvAppItemType::mvFloatValue: case mvAppItemType::mvProgressBar: @@ -1510,6 +1522,7 @@ DearPyGui::GetAllowableParents(mvAppItemType type) MV_START_PARENTS MV_ADD_PARENT(mvAppItemType::mvButton), MV_ADD_PARENT(mvAppItemType::mvCheckbox), + MV_ADD_PARENT(mvAppItemType::mvMixedStateCheckbox), MV_ADD_PARENT(mvAppItemType::mvCombo), MV_ADD_PARENT(mvAppItemType::mvDragIntMulti), MV_ADD_PARENT(mvAppItemType::mvDragFloatMulti), @@ -1908,6 +1921,32 @@ DearPyGui::GetEntityParser(mvAppItemType type) break; } + case mvAppItemType::mvMixedStateCheckbox: + { + AddCommonArgs(args, (CommonParserArgs)( + MV_PARSER_ARG_ID | + MV_PARSER_ARG_INDENT | + MV_PARSER_ARG_PARENT | + MV_PARSER_ARG_BEFORE | + MV_PARSER_ARG_SOURCE | + MV_PARSER_ARG_CALLBACK | + MV_PARSER_ARG_SHOW | + MV_PARSER_ARG_ENABLED | + MV_PARSER_ARG_FILTER | + MV_PARSER_ARG_DROP_CALLBACK | + MV_PARSER_ARG_DRAG_CALLBACK | + MV_PARSER_ARG_PAYLOAD_TYPE | + MV_PARSER_ARG_TRACKED | + MV_PARSER_ARG_POS) + ); + + args.push_back({ mvPyDataType::Integer, "default_value", mvArgType::KEYWORD_ARG, "0", "Sets the default value: -1 for mixed/indeterminate, 0 for unchecked, 1 for checked" }); + args.push_back({ mvPyDataType::Integer, "mixed_click_value", mvArgType::KEYWORD_ARG, "1", "Sets the value when clicking the checkbox in mixed state: 0 for unchecked, 1 for checked" }); + + setup.about = "Adds a mixed state (tristate) checkbox. Value is -1 (mixed), 0 (unchecked), or 1 (checked)."; + break; + } + case mvAppItemType::mvCombo: { AddCommonArgs(args, (CommonParserArgs)( diff --git a/src/mvAppItem.h b/src/mvAppItem.h index 69edfe275..839afda0d 100644 --- a/src/mvAppItem.h +++ b/src/mvAppItem.h @@ -286,6 +286,7 @@ GetEntityCommand(mvAppItemType type) case mvAppItemType::mvButton: return "add_button"; case mvAppItemType::mvCheckbox: return "add_checkbox"; + case mvAppItemType::mvMixedStateCheckbox: return "add_mixed_state_checkbox"; case mvAppItemType::mvInputText: return "add_input_text"; case mvAppItemType::mvRadioButton: return "add_radio_button"; case mvAppItemType::mvTabBar: return "add_tab_bar"; diff --git a/src/mvAppItemTypes.inc b/src/mvAppItemTypes.inc index 68a2f73ea..161780ca6 100644 --- a/src/mvAppItemTypes.inc +++ b/src/mvAppItemTypes.inc @@ -27,6 +27,7 @@ X( mvCollapsingHeader ) \ X( mvSeparator ) \ X( mvCheckbox ) \ + X( mvMixedStateCheckbox ) \ X( mvListbox ) \ X( mvText ) \ X( mvCombo ) \ diff --git a/src/mvBasicWidgets.cpp b/src/mvBasicWidgets.cpp index 43a87df0f..abef8d9c3 100644 --- a/src/mvBasicWidgets.cpp +++ b/src/mvBasicWidgets.cpp @@ -702,6 +702,15 @@ DearPyGui::fill_configuration_dict(const mvTooltipConfig& inConfig, PyObject* ou PyDict_SetItemString(outDict, "hide_on_activity", mvPyObject(ToPyBool(inConfig.hide_on_move))); } +void +DearPyGui::fill_configuration_dict(const mvMixedStateCheckboxConfig& inConfig, PyObject* outDict) +{ + if (outDict == nullptr) + return; + + PyDict_SetItemString(outDict, "mixed_click_value", mvPyObject(ToPyInt(inConfig.mixed_click_value))); +} + //----------------------------------------------------------------------------- // [SECTION] configure_item(...) specifics //----------------------------------------------------------------------------- @@ -1539,6 +1548,15 @@ DearPyGui::set_configuration(PyObject* inDict, mvKnobFloatConfig& outConfig) if (PyObject* item = PyDict_GetItemString(inDict, "max_value")) outConfig.maxv = ToFloat(item); } +void +DearPyGui::set_configuration(PyObject* inDict, mvMixedStateCheckboxConfig& outConfig) +{ + if (inDict == nullptr) + return; + + if (PyObject* item = PyDict_GetItemString(inDict, "mixed_click_value")) outConfig.mixed_click_value = ToInt(item); +} + //----------------------------------------------------------------------------- // [SECTION] required args specifics //----------------------------------------------------------------------------- @@ -1739,6 +1757,28 @@ DearPyGui::set_data_source(mvAppItem& item, mvUUID dataSource, mvCheckboxConfig& outConfig.value = *static_cast*>(srcItem->getValue()); } +void +DearPyGui::set_data_source(mvAppItem& item, mvUUID dataSource, mvMixedStateCheckboxConfig& outConfig) +{ + if (dataSource == item.config.source) return; + item.config.source = dataSource; + + mvAppItem* srcItem = GetItem((*GContext->itemRegistry), dataSource); + if (!srcItem) + { + mvThrowPythonError(mvErrorCode::mvSourceNotFound, "set_value", + "Source item not found: " + std::to_string(dataSource), &item); + return; + } + if (DearPyGui::GetEntityValueType(srcItem->type) != DearPyGui::GetEntityValueType(item.type)) + { + mvThrowPythonError(mvErrorCode::mvSourceNotCompatible, "set_value", + "Values types do not match: " + std::to_string(dataSource), &item); + return; + } + outConfig.value = *static_cast*>(srcItem->getValue()); +} + void DearPyGui::set_data_source(mvAppItem& item, mvUUID dataSource, mvDragFloatConfig& outConfig) { @@ -2714,6 +2754,111 @@ DearPyGui::draw_checkbox(ImDrawList* drawlist, mvAppItem& item, mvCheckboxConfig apply_drag_drop(&item); } +void +DearPyGui::draw_mixed_state_checkbox(ImDrawList* drawlist, mvAppItem& item, mvMixedStateCheckboxConfig& config) +{ + //----------------------------------------------------------------------------- + // pre draw + //----------------------------------------------------------------------------- + + // show/hide + if (!item.config.show) + return; + + // focusing + if (item.info.focusNextFrame) + { + ImGui::SetKeyboardFocusHere(); + item.info.focusNextFrame = false; + } + + // cache old cursor position + ImVec2 previousCursorPos = ImGui::GetCursorPos(); + + // set cursor position if user set + if (item.info.dirtyPos) + ImGui::SetCursorPos(item.state.pos); + + // update widget's position state + item.state.pos = { ImGui::GetCursorPosX(), ImGui::GetCursorPosY() }; + + // set item width + if (item.config.width != 0) + ImGui::SetNextItemWidth((float)item.config.width); + + // set indent + if (item.config.indent > 0.0f) + ImGui::Indent(item.config.indent); + + // push font if a font object is attached + if (item.font) + static_cast(item.font.get())->pushFont(); + + apply_local_theming(&item); + + //----------------------------------------------------------------------------- + // draw + //----------------------------------------------------------------------------- + { + // push imgui id to prevent name collisions + ScopedID id(item.uuid); + + int* v = item.config.enabled ? config.value.get() : &config.disabled_value; + if (!item.config.enabled) config.disabled_value = *config.value; + + bool ret = false; + + if (*v == -1) + { + // Mixed state: push the flag, use temp bool + ImGui::PushItemFlag(ImGuiItemFlags_MixedValue, true); + bool temp = false; + ret = ImGui::Checkbox(item.info.internalLabel.c_str(), &temp); + if (ret) + *v = config.mixed_click_value; + ImGui::PopItemFlag(); + } + else + { + bool b = (*v != 0); + ret = ImGui::Checkbox(item.info.internalLabel.c_str(), &b); + if (ret) + *v = (int)b; + } + + if (ret) + item.submitCallback(*config.value); + } + + //----------------------------------------------------------------------------- + // update state + //----------------------------------------------------------------------------- + UpdateAppItemState(item.state); + + //----------------------------------------------------------------------------- + // post draw + //----------------------------------------------------------------------------- + + // set cursor position to cached position + if (item.info.dirtyPos) + DearPyGui::RestoreImGuiCursor(previousCursorPos); + + if (item.config.indent > 0.0f) + ImGui::Unindent(item.config.indent); + + // pop font off stack + if (item.font) + ImGui::PopFont(); + + cleanup_local_theming(&item); + + if (item.handlerRegistry) + item.handlerRegistry->checkEvents(&item.state); + + // handle drag & drop if used + apply_drag_drop(&item); +} + void DearPyGui::draw_drag_float(ImDrawList* drawlist, mvAppItem& item, mvDragFloatConfig& config) { diff --git a/src/mvBasicWidgets.h b/src/mvBasicWidgets.h index 16d909338..f2db56034 100644 --- a/src/mvBasicWidgets.h +++ b/src/mvBasicWidgets.h @@ -6,6 +6,7 @@ struct mvSimplePlotConfig; struct mvButtonConfig; struct mvCheckboxConfig; +struct mvMixedStateCheckboxConfig; struct mvComboConfig; struct mvDragFloatConfig; struct mvDragIntConfig; @@ -75,6 +76,7 @@ namespace DearPyGui void fill_configuration_dict(const mvImageButtonConfig& inConfig, PyObject* outDict); void fill_configuration_dict(const mvKnobFloatConfig& inConfig, PyObject* outDict); void fill_configuration_dict(const mvTooltipConfig& inConfig, PyObject* outDict); + void fill_configuration_dict(const mvMixedStateCheckboxConfig& inConfig, PyObject* outDict); // specific part of `configure_item(...)` void set_configuration(PyObject* inDict, mvSimplePlotConfig& outConfig); @@ -110,6 +112,7 @@ namespace DearPyGui void set_configuration(PyObject* inDict, mvImageButtonConfig& outConfig); void set_configuration(PyObject* inDict, mvTooltipConfig& outConfig); void set_configuration(PyObject* inDict, mvKnobFloatConfig& outConfig); + void set_configuration(PyObject* inDict, mvMixedStateCheckboxConfig& outConfig); // positional args TODO: combine with above void set_required_configuration(PyObject* inDict, mvImageConfig& outConfig); @@ -126,6 +129,7 @@ namespace DearPyGui void set_data_source(mvAppItem& item, mvUUID dataSource, mvSimplePlotConfig& outConfig); void set_data_source(mvAppItem& item, mvUUID dataSource, mvComboConfig& outConfig); void set_data_source(mvAppItem& item, mvUUID dataSource, mvCheckboxConfig& outConfig); + void set_data_source(mvAppItem& item, mvUUID dataSource, mvMixedStateCheckboxConfig& outConfig); void set_data_source(mvAppItem& item, mvUUID dataSource, mvDragFloatConfig& outConfig); void set_data_source(mvAppItem& item, mvUUID dataSource, mvDragFloatMultiConfig& outConfig); void set_data_source(mvAppItem& item, mvUUID dataSource, mvDragIntConfig& outConfig); @@ -158,6 +162,7 @@ namespace DearPyGui void draw_button (ImDrawList* drawlist, mvAppItem& item, const mvButtonConfig& config); void draw_combo (ImDrawList* drawlist, mvAppItem& item, mvComboConfig& config); void draw_checkbox (ImDrawList* drawlist, mvAppItem& item, mvCheckboxConfig& config); + void draw_mixed_state_checkbox(ImDrawList* drawlist, mvAppItem& item, mvMixedStateCheckboxConfig& config); void draw_drag_float (ImDrawList* drawlist, mvAppItem& item, mvDragFloatConfig& config); void draw_drag_floatx (ImDrawList* drawlist, mvAppItem& item, mvDragFloatMultiConfig& config); void draw_drag_double (ImDrawList* drawlist, mvAppItem& item, mvDragDoubleConfig& config); @@ -239,6 +244,13 @@ struct mvCheckboxConfig bool disabled_value = false; }; +struct mvMixedStateCheckboxConfig +{ + std::shared_ptr value = std::make_shared(0); // -1=mixed, 0=false, 1=true + int disabled_value = 0; + int mixed_click_value = 1; // value to transition to when clicked in mixed state (0 or 1) +}; + struct mvDragFloatConfig { float speed = 1.0f; @@ -624,6 +636,20 @@ class mvCheckbox : public mvAppItem PyObject* getPyValue() override{ return ToPyBool(*configData.value); } }; +class mvMixedStateCheckbox : public mvAppItem +{ +public: + mvMixedStateCheckboxConfig configData{}; + explicit mvMixedStateCheckbox(mvUUID uuid) : mvAppItem(uuid) {} + void draw(ImDrawList* drawlist, float x, float y) override { DearPyGui::draw_mixed_state_checkbox(drawlist, *this, configData); } + void handleSpecificKeywordArgs(PyObject* dict) override { DearPyGui::set_configuration(dict, configData); } + void getSpecificConfiguration(PyObject* dict) override { DearPyGui::fill_configuration_dict(configData, dict); } + void setDataSource(mvUUID dataSource) override { DearPyGui::set_data_source(*this, dataSource, configData); } + void setPyValue(PyObject* value) override { *configData.value = ToInt(value); } + void* getValue() override { return &configData.value; } + PyObject* getPyValue() override { return ToPyInt(*configData.value); } +}; + class mvDragFloat : public mvAppItem { public: diff --git a/testing/mixed_state_checkbox.py b/testing/mixed_state_checkbox.py new file mode 100644 index 000000000..9352689f1 --- /dev/null +++ b/testing/mixed_state_checkbox.py @@ -0,0 +1,61 @@ +"""Test script for mixed_state_checkbox widget.""" + +import sys + +sys.path.insert(0, '../output') # Use built output + +import dearpygui.dearpygui as dpg + + +def on_change(sender, app_data, user_data): + states = {-1: "mixed", 0: "unchecked", 1: "checked"} + print(f"Checkbox changed to: {app_data} ({states.get(app_data, 'unknown')})") + +dpg.create_context() + +with dpg.window(label="Mixed State Checkbox Test", width=400, height=300): + dpg.add_text("Mixed state checkbox test:") + dpg.add_text("Values: -1=mixed, 0=unchecked, 1=checked") + dpg.add_separator() + + # Start in mixed state + cb1 = dpg.add_mixed_state_checkbox( + label="Mixed (start at -1)", + default_value=-1, + callback=on_change, + ) + + # Start unchecked + cb2 = dpg.add_mixed_state_checkbox( + label="Unchecked (start at 0)", + default_value=0, + callback=on_change, + mixed_click_value=1 + ) + + # Start checked + cb3 = dpg.add_mixed_state_checkbox( + label="Checked (start at 1)", + default_value=1, + callback=on_change, + mixed_click_value=0 + ) + + dpg.add_separator() + + def set_all_mixed(): + dpg.set_value(cb1, -1) + dpg.set_value(cb2, -1) + dpg.set_value(cb3, -1) + + def print_values(): + print(f"cb1={dpg.get_value(cb1)}, cb2={dpg.get_value(cb2)}, cb3={dpg.get_value(cb3)}") + + dpg.add_button(label="Set all to mixed (-1)", callback=set_all_mixed) + dpg.add_button(label="Print values", callback=print_values) + +dpg.create_viewport(title="Mixed State Checkbox Test", width=450, height=350) +dpg.setup_dearpygui() +dpg.show_viewport() +dpg.start_dearpygui() +dpg.destroy_context() diff --git a/testing/run_demo.py b/testing/run_demo.py new file mode 100644 index 000000000..d31b8bc1b --- /dev/null +++ b/testing/run_demo.py @@ -0,0 +1,18 @@ +"""Runs the demo application using the locally built DearPyGui in the output directory.""" + +import sys + +sys.path.insert(0, '../output') # Use built output + +import dearpygui.dearpygui as dpg +import dearpygui.demo as demo + +dpg.create_context() +dpg.create_viewport(title='Custom Title', width=600, height=600) + +demo.show_demo() + +dpg.setup_dearpygui() +dpg.show_viewport() +dpg.start_dearpygui() +dpg.destroy_context()