/* * This file is part of modulo. * * Copyright (C) 2020-2021 Federico Terzi * * modulo is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * modulo is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with modulo. If not, see . */ // Mouse dragging mechanism greatly inspired by: https://developpaper.com/wxwidgets-implementing-the-drag-effect-of-titleless-bar-window/ #define _UNICODE #include "../common/common.h" #include "../interop/interop.h" #include "wx/htmllbox.h" #include #include #include // Platform-specific styles #ifdef __WXMSW__ const int SEARCH_BAR_FONT_SIZE = 16; const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW; #endif #ifdef __WXOSX__ const int SEARCH_BAR_FONT_SIZE = 20; const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW | wxRESIZE_BORDER; #endif #ifdef __LINUX__ const int SEARCH_BAR_FONT_SIZE = 20; const long DEFAULT_STYLE = wxSTAY_ON_TOP; #endif const int HELP_TEXT_FONT_SIZE = 10; const wxColour SELECTION_LIGHT_BG = wxColour(164, 210, 253); const wxColour SELECTION_DARK_BG = wxColour(49, 88, 126); // https://docs.wxwidgets.org/stable/classwx_frame.html const int MIN_WIDTH = 500; const int MIN_HEIGHT = 80; typedef void (*QueryCallback)(const char *query, void *app, void *data); typedef void (*ResultCallback)(const char *id, void *data); SearchMetadata *searchMetadata = nullptr; QueryCallback queryCallback = nullptr; ResultCallback resultCallback = nullptr; void *data = nullptr; void *resultData = nullptr; wxArrayString wxItems; wxArrayString wxTriggers; wxArrayString wxIds; // App Code class SearchApp : public wxApp { public: virtual bool OnInit(); }; class ResultListBox : public wxHtmlListBox { public: ResultListBox() {} ResultListBox(wxWindow *parent, bool isDark, const wxWindowID id, const wxPoint &pos, const wxSize &size); protected: // override this method to return data to be shown in the listbox (this is // mandatory) virtual wxString OnGetItem(size_t n) const; // change the appearance by overriding these functions (this is optional) virtual void OnDrawBackground(wxDC &dc, const wxRect &rect, size_t n) const; bool isDark; public: wxDECLARE_NO_COPY_CLASS(ResultListBox); wxDECLARE_DYNAMIC_CLASS(ResultListBox); }; wxIMPLEMENT_DYNAMIC_CLASS(ResultListBox, wxHtmlListBox); ResultListBox::ResultListBox(wxWindow *parent, bool isDark, const wxWindowID id, const wxPoint &pos, const wxSize &size) : wxHtmlListBox(parent, id, pos, size, 0) { this->isDark = isDark; SetMargins(5, 5); Refresh(); } void ResultListBox::OnDrawBackground(wxDC &dc, const wxRect &rect, size_t n) const { if (IsSelected(n)) { if (isDark) { dc.SetBrush(wxBrush(SELECTION_DARK_BG)); } else { dc.SetBrush(wxBrush(SELECTION_LIGHT_BG)); } } else { dc.SetBrush(*wxTRANSPARENT_BRUSH); } dc.SetPen(*wxTRANSPARENT_PEN); dc.DrawRectangle(0, 0, rect.GetRight(), rect.GetBottom()); } wxString ResultListBox::OnGetItem(size_t n) const { wxString textColor = isDark ? "white" : ""; wxString shortcut = (n < 8) ? wxString::Format(wxT("Alt+%i"), (int)n + 1) : " "; return wxString::Format(wxT("
%s%s %s
"), textColor, wxItems[n], wxTriggers[n], shortcut); } class SearchFrame : public wxFrame { public: SearchFrame(const wxString &title, const wxPoint &pos, const wxSize &size); wxPanel *panel; wxTextCtrl *searchBar; wxStaticBitmap *iconPanel; wxStaticText *helpText; ResultListBox *resultBox; void SetItems(SearchItem *items, int itemSize); private: void OnCharEvent(wxKeyEvent &event); void OnQueryChange(wxCommandEvent &event); void OnItemClickEvent(wxCommandEvent &event); void OnActivate(wxActivateEvent &event); // Mouse events void OnMouseCaptureLost(wxMouseCaptureLostEvent &event); void OnMouseLeave(wxMouseEvent &event); void OnMouseMove(wxMouseEvent &event); void OnMouseLUp(wxMouseEvent &event); void OnMouseLDown(wxMouseEvent &event); wxPoint mLastPt; // Selection void SelectNext(); void SelectPrevious(); void Submit(); }; bool SearchApp::OnInit() { SearchFrame *frame = new SearchFrame(wxString::FromUTF8(searchMetadata->windowTitle), wxPoint(50, 50), wxSize(450, 340)); frame->Show(true); SetupWindowStyle(frame); Activate(frame); return true; } SearchFrame::SearchFrame(const wxString &title, const wxPoint &pos, const wxSize &size) : wxFrame(NULL, wxID_ANY, title, pos, size, DEFAULT_STYLE) { wxInitAllImageHandlers(); #if wxCHECK_VERSION(3, 1, 3) bool isDark = wxSystemSettings::GetAppearance().IsDark(); #else // Workaround needed for previous versions of wxWidgets const wxColour bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); const wxColour fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); unsigned int bgSum = (bg.Red() + bg.Blue() + bg.Green()); unsigned int fgSum = (fg.Red() + fg.Blue() + fg.Green()); bool isDark = fgSum > bgSum; #endif panel = new wxPanel(this, wxID_ANY); wxBoxSizer *vbox = new wxBoxSizer(wxVERTICAL); panel->SetSizer(vbox); wxBoxSizer *topBox = new wxBoxSizer(wxHORIZONTAL); int iconId = NewControlId(); iconPanel = nullptr; if (searchMetadata->iconPath) { wxString iconPath = wxString::FromUTF8(searchMetadata->iconPath); if (wxFileExists(iconPath)) { wxBitmap bitmap = wxBitmap(iconPath, wxBITMAP_TYPE_PNG); if (bitmap.IsOk()) { wxImage image = bitmap.ConvertToImage(); image.Rescale(32, 32, wxIMAGE_QUALITY_HIGH); wxBitmap resizedBitmap = wxBitmap(image, -1); iconPanel = new wxStaticBitmap(panel, iconId, resizedBitmap, wxDefaultPosition, wxSize(32, 32)); topBox->Add(iconPanel, 0, wxEXPAND | wxLEFT | wxUP | wxDOWN, 10); } } } int textId = NewControlId(); searchBar = new wxTextCtrl(panel, textId, "", wxDefaultPosition, wxDefaultSize); wxFont font = searchBar->GetFont(); font.SetPointSize(SEARCH_BAR_FONT_SIZE); searchBar->SetFont(font); topBox->Add(searchBar, 1, wxEXPAND | wxALL, 10); vbox->Add(topBox, 1, wxEXPAND); if (searchMetadata->hintText) { helpText = new wxStaticText(panel, wxID_ANY, wxString::FromUTF8(searchMetadata->hintText)); vbox->Add(helpText, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, 10); wxFont helpFont = helpText->GetFont(); helpFont.SetPointSize(HELP_TEXT_FONT_SIZE); helpText->SetFont(helpFont); } wxArrayString choices; int resultId = NewControlId(); resultBox = new ResultListBox(panel, isDark, resultId, wxDefaultPosition, wxSize(MIN_WIDTH, MIN_HEIGHT)); vbox->Add(resultBox, 5, wxEXPAND | wxALL, 0); Bind(wxEVT_CHAR_HOOK, &SearchFrame::OnCharEvent, this, wxID_ANY); searchBar->Bind(wxEVT_CHAR, &SearchFrame::OnCharEvent, this, wxID_ANY); Bind(wxEVT_TEXT, &SearchFrame::OnQueryChange, this, textId); Bind(wxEVT_LISTBOX_DCLICK, &SearchFrame::OnItemClickEvent, this, resultId); Bind(wxEVT_ACTIVATE, &SearchFrame::OnActivate, this, wxID_ANY); // Events to handle the mouse drag if (iconPanel) { iconPanel->Bind(wxEVT_LEFT_UP, &SearchFrame::OnMouseLUp, this); iconPanel->Bind(wxEVT_LEFT_DOWN, &SearchFrame::OnMouseLDown, this); Bind(wxEVT_MOTION, &SearchFrame::OnMouseMove, this); Bind(wxEVT_LEFT_UP, &SearchFrame::OnMouseLUp, this); Bind(wxEVT_LEFT_DOWN, &SearchFrame::OnMouseLDown, this); Bind(wxEVT_MOUSE_CAPTURE_LOST, &SearchFrame::OnMouseCaptureLost, this); Bind(wxEVT_LEAVE_WINDOW, &SearchFrame::OnMouseLeave, this); } this->SetClientSize(panel->GetBestSize()); this->CentreOnScreen(); // Trigger the first data update queryCallback("", (void *)this, data); } void SearchFrame::OnCharEvent(wxKeyEvent &event) { if (event.GetKeyCode() == WXK_ESCAPE) { Close(true); } else if (event.GetKeyCode() == WXK_TAB) { if (wxGetKeyState(WXK_SHIFT)) { SelectPrevious(); } else { SelectNext(); } } else if (event.GetUnicodeKey() >= '1' && event.GetUnicodeKey() <= '9') { // Alt + num shortcut int index = event.GetUnicodeKey() - '1'; if (wxGetKeyState(WXK_ALT)) { if (resultBox->GetItemCount() > index) { resultBox->SetSelection(index); Submit(); } } else { event.Skip(); } } else if (event.GetKeyCode() == WXK_DOWN) { SelectNext(); } else if (event.GetKeyCode() == WXK_UP) { SelectPrevious(); } else if (event.GetKeyCode() == WXK_RETURN) { Submit(); } else { event.Skip(); } } void SearchFrame::OnQueryChange(wxCommandEvent &event) { if (helpText != nullptr) { helpText->Destroy(); panel->Layout(); helpText = nullptr; } wxString queryString = searchBar->GetValue(); const char *query = queryString.ToUTF8(); queryCallback(query, (void *)this, data); } void SearchFrame::OnItemClickEvent(wxCommandEvent &event) { resultBox->SetSelection(event.GetInt()); Submit(); } void SearchFrame::OnActivate(wxActivateEvent &event) { if (!event.GetActive()) { Close(true); } event.Skip(); } void SearchFrame::OnMouseMove(wxMouseEvent &event) { if (event.LeftIsDown() && event.Dragging()) { wxPoint pt = event.GetPosition(); wxPoint wndLeftTopPt = GetPosition(); int distanceX = pt.x - mLastPt.x; int distanceY = pt.y - mLastPt.y; wxPoint desPt; desPt.x = distanceX + wndLeftTopPt.x - 24; desPt.y = distanceY + wndLeftTopPt.y - 24; this->Move(desPt); } if (event.LeftDown()) { this->CaptureMouse(); mLastPt = event.GetPosition(); } } void SearchFrame::OnMouseLeave(wxMouseEvent &event) { if (event.LeftIsDown() && event.Dragging()) { wxPoint pt = event.GetPosition(); wxPoint wndLeftTopPt = GetPosition(); int distanceX = pt.x - mLastPt.x; int distanceY = pt.y - mLastPt.y; wxPoint desPt; desPt.x = distanceX + wndLeftTopPt.x - 24; desPt.y = distanceY + wndLeftTopPt.y - 24; this->Move(desPt); } } void SearchFrame::OnMouseLDown(wxMouseEvent &event) { if (!HasCapture()) this->CaptureMouse(); } void SearchFrame::OnMouseLUp(wxMouseEvent &event) { if (HasCapture()) ReleaseMouse(); } void SearchFrame::OnMouseCaptureLost(wxMouseCaptureLostEvent &event) { if (HasCapture()) ReleaseMouse(); } void SearchFrame::SetItems(SearchItem *items, int itemSize) { wxItems.Clear(); wxIds.Clear(); wxTriggers.Clear(); for (int i = 0; i < itemSize; i++) { wxString item = wxString::FromUTF8(items[i].label); wxItems.Add(item); wxString id = wxString::FromUTF8(items[i].id); wxIds.Add(id); wxString trigger = wxString::FromUTF8(items[i].trigger); wxTriggers.Add(trigger); } resultBox->SetItemCount(itemSize); if (itemSize > 0) { resultBox->SetSelection(0); } resultBox->RefreshAll(); resultBox->Refresh(); } void SearchFrame::SelectNext() { if (resultBox->GetItemCount() > 0 && resultBox->GetSelection() != wxNOT_FOUND) { int newSelected = 0; if (resultBox->GetSelection() < (resultBox->GetItemCount() - 1)) { newSelected = resultBox->GetSelection() + 1; } resultBox->SetSelection(newSelected); } } void SearchFrame::SelectPrevious() { if (resultBox->GetItemCount() > 0 && resultBox->GetSelection() != wxNOT_FOUND) { int newSelected = resultBox->GetItemCount() - 1; if (resultBox->GetSelection() > 0) { newSelected = resultBox->GetSelection() - 1; } resultBox->SetSelection(newSelected); } } void SearchFrame::Submit() { if (resultBox->GetItemCount() > 0 && resultBox->GetSelection() != wxNOT_FOUND) { long index = resultBox->GetSelection(); wxString id = wxIds[index]; if (resultCallback) { resultCallback(id.ToUTF8(), resultData); } Close(true); } } extern "C" void interop_show_search(SearchMetadata *_metadata, QueryCallback _queryCallback, void *_data, ResultCallback _resultCallback, void *_resultData) { // Setup high DPI support on Windows #ifdef __WXMSW__ SetProcessDPIAware(); #endif searchMetadata = _metadata; queryCallback = _queryCallback; resultCallback = _resultCallback; data = _data; resultData = _resultData; wxApp::SetInstance(new SearchApp()); int argc = 0; wxEntry(argc, (char **)nullptr); } extern "C" void update_items(void *app, SearchItem *items, int itemSize) { SearchFrame *frame = (SearchFrame *)app; frame->SetItems(items, itemSize); }