// ***************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 * // * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved * // ***************************************************************************** #include "command_box.h" #include #include #include using namespace zen; using namespace fff; namespace { inline wxString getSeparationLine() { return std::wstring(50, EM_DASH); } //no space between dashes! } CommandBox::CommandBox(wxWindow* parent, wxWindowID id, const wxString& value, const wxPoint& pos, const wxSize& size, int n, const wxString choices[], long style, const wxValidator& validator, const wxString& name) : wxComboBox(parent, id, value, pos, size, n, choices, style, validator, name) { //#################################### /*#*/ SetMinSize({dipToWxsize(150), -1}); //# workaround yet another wxWidgets bug: default minimum size is much too large for a wxComboBox //#################################### Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& event) { onKeyEvent (event); }); Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& event) { onUpdateList(event); }); Bind(wxEVT_COMMAND_COMBOBOX_SELECTED, [this](wxCommandEvent& event) { onSelection (event); }); Bind(wxEVT_MOUSEWHEEL, [] (wxMouseEvent& event) {}); //swallow! this gives confusing UI feedback anyway } void CommandBox::addItemHistory() { const Zstring newCommand = trimCpy(getValue()); if (newCommand == utfTo(getSeparationLine()) || //do not add sep. line newCommand.empty()) return; //do not add built-in commands to history for (const auto& [description, cmd] : defaultCommands_) if (newCommand == utfTo(description) || equalNoCase(newCommand, cmd)) return; std::erase_if(history_, [&](const Zstring& item) { return equalNoCase(newCommand, item); }); history_.insert(history_.begin(), newCommand); if (history_.size() > historyMax_) history_.resize(historyMax_); } Zstring CommandBox::getValue() const { return utfTo(trimCpy(GetValue())); } void CommandBox::setValue(const Zstring& value) { setValueAndUpdateList(trimCpy(utfTo(value))); } //set value and update list are technically entangled: see potential bug description below void CommandBox::setValueAndUpdateList(const wxString& value) { //it may be a little lame to update the list on each mouse-button click, but it should be working and we dont't have to manipulate wxComboBox internals std::vector items; //1. built in commands for (const auto& [description, cmd] : defaultCommands_) items.push_back(description); //2. history elements auto histSorted = history_; std::sort(histSorted.begin(), histSorted.end(), LessNaturalSort() /*even on Linux*/); if (!items.empty() && !histSorted.empty()) items.push_back(getSeparationLine()); for (const Zstring& hist : histSorted) items.push_back(utfTo(hist)); //attention: if the target value is not part of the dropdown list, SetValue() will look for a string that *starts with* this value: //e.g. if the dropdown list contains "222" SetValue("22") will erroneously set and select "222" instead, while "111" would be set correctly! // -> by design on Windows! if (std::find(items.begin(), items.end(), value) == items.end()) { if (!items.empty() && !value.empty()) items.insert(items.begin(), {value, getSeparationLine()}); else items.insert(items.begin(), {value}); } //this->Clear(); -> NO! emits yet another wxEVT_COMMAND_TEXT_UPDATED!!! wxItemContainer::Clear(); //suffices to clear the selection items only! this->Append(items); //expensive as fuck! => only call when absolutely needed! //this->SetSelection(wxNOT_FOUND); //don't select anything ChangeValue(value); //preserve main text! } void CommandBox::onSelection(wxCommandEvent& event) { //we cannot replace built-in commands at this position in call stack, so defer to a later time! CallAfter([&] { onValidateSelection(); }); event.Skip(); } void CommandBox::onValidateSelection() { const wxString value = GetValue(); if (value == getSeparationLine()) return setValueAndUpdateList(wxString()); for (const auto& [description, cmd] : defaultCommands_) if (description == value) return setValueAndUpdateList(utfTo(cmd)); //replace GUI name by actual command string } void CommandBox::onUpdateList(wxEvent& event) { setValue(getValue()); event.Skip(); } void CommandBox::onKeyEvent(wxKeyEvent& event) { const int keyCode = event.GetKeyCode(); switch (keyCode) { case WXK_DELETE: case WXK_NUMPAD_DELETE: { //try to delete the currently selected config history item int pos = this->GetCurrentSelection(); if (0 <= pos && pos < static_cast(this->GetCount()) && //what a mess...: (GetValue() != GetString(pos) || //avoid problems when a character shall be deleted instead of list item GetValue().empty())) //exception: always allow removing empty entry { const auto selValue = utfTo(GetString(pos)); if (std::find(history_.begin(), history_.end(), selValue) != history_.end()) //only history elements may be deleted { //save old (selected) value: deletion seems to have influence on this const wxString currentVal = this->GetValue(); //this->SetSelection(wxNOT_FOUND); //delete selected row std::erase(history_, selValue); SetString(pos, wxString()); //in contrast to Delete(), this one does not kill the drop-down list and gives a nice visual feedback! //Delete(pos); //(re-)set value SetValue(currentVal); } return; //eat up key event } } break; case WXK_UP: case WXK_NUMPAD_UP: case WXK_DOWN: case WXK_NUMPAD_DOWN: case WXK_PAGEUP: case WXK_NUMPAD_PAGEUP: case WXK_PAGEDOWN: case WXK_NUMPAD_PAGEDOWN: return; //swallow -> using these keys gives a weird effect due to this weird control } event.Skip(); }