From be922940a2e72a71964814b1fbf71f942524f3cb Mon Sep 17 00:00:00 2001 From: twanvl Date: Wed, 4 Aug 2010 21:52:26 +0000 Subject: [PATCH] Added a "quick search" box for filtering the card list git-svn-id: svn://svn.code.sf.net/p/magicseteditor/code/trunk@1483 0fc631ac-6414-0410-93d0-97cfa31319b6 --- src/data/card.cpp | 7 + src/data/card.hpp | 2 + src/gui/control/filtered_card_list.cpp | 11 +- src/gui/control/filtered_card_list.hpp | 11 +- src/gui/control/image_card_list.cpp | 25 +++ src/gui/control/image_card_list.hpp | 19 +++ src/gui/set/cards_panel.cpp | 160 +++++++++++++++++- src/gui/set/cards_panel.hpp | 7 +- src/gui/set/window.cpp | 2 + src/gui/set/window.hpp | 1 + src/resource/common/btn_clear_filter_down.png | Bin 0 -> 773 bytes .../common/btn_clear_filter_focus.png | Bin 0 -> 732 bytes .../common/btn_clear_filter_hover.png | Bin 0 -> 732 bytes .../common/btn_clear_filter_normal.png | Bin 0 -> 742 bytes src/resource/common/expected_locale_keys | 4 +- src/resource/msw/mse.rc | 4 + src/util/window_id.hpp | 1 + 17 files changed, 239 insertions(+), 15 deletions(-) create mode 100644 src/resource/common/btn_clear_filter_down.png create mode 100644 src/resource/common/btn_clear_filter_focus.png create mode 100644 src/resource/common/btn_clear_filter_hover.png create mode 100644 src/resource/common/btn_clear_filter_normal.png diff --git a/src/data/card.cpp b/src/data/card.cpp index 149534ef..be2ab4ce 100644 --- a/src/data/card.cpp +++ b/src/data/card.cpp @@ -55,6 +55,13 @@ String Card::identification() const { } } +bool Card::contains(String const& query) const { + FOR_EACH_CONST(v, data) { + if (v->toString().find(query) != String::npos) return true; + } + return false; +} + IndexMap& Card::extraDataFor(const StyleSheet& stylesheet) { return extra_data.get(stylesheet.name(), stylesheet.extra_card_fields); } diff --git a/src/data/card.hpp b/src/data/card.hpp index 3fd0c9a2..644857d9 100644 --- a/src/data/card.hpp +++ b/src/data/card.hpp @@ -61,6 +61,8 @@ class Card : public IntrusivePtrVirtualBase { /// Get the identification of this card, an identification is something like a name, title, etc. /** May return "" */ String identification() const; + /// Does any field contains the given query string? + bool contains(String const& query) const; /// Find a value in the data by name and type template T& value(const String& name) { diff --git a/src/gui/control/filtered_card_list.cpp b/src/gui/control/filtered_card_list.cpp index 4f53d042..493931fd 100644 --- a/src/gui/control/filtered_card_list.cpp +++ b/src/gui/control/filtered_card_list.cpp @@ -13,8 +13,8 @@ DECLARE_TYPEOF_COLLECTION(CardP); // ----------------------------------------------------------------------------- : FilteredCardList -FilteredCardList::FilteredCardList(Window* parent, int id, long style) - : CardListBase(parent, id, style) +FilteredCardList::FilteredCardList(Window* parent, int id, long additional_style) + : CardListBase(parent, id, additional_style) {} void FilteredCardList::setFilter(const CardListFilterP& filter) { @@ -34,6 +34,8 @@ void FilteredCardList::getItems(vector& out) const { } } +// ----------------------------------------------------------------------------- : CardListFilter + void CardListFilter::getItems(const vector& cards, vector& out) const { FOR_EACH_CONST(c, cards) { if (keep(c)) { @@ -41,3 +43,8 @@ void CardListFilter::getItems(const vector& cards, vector& out) co } } } + +bool QueryCardListFilter::keep(const CardP& card) const { + return card->contains(query); +} + diff --git a/src/gui/control/filtered_card_list.hpp b/src/gui/control/filtered_card_list.hpp index 3e2032eb..2a4a9472 100644 --- a/src/gui/control/filtered_card_list.hpp +++ b/src/gui/control/filtered_card_list.hpp @@ -26,12 +26,21 @@ class CardListFilter : public IntrusivePtrVirtualBase { virtual void getItems(const vector& cards, vector& out) const; }; +/// A filter function that searches for cards containing a string +class QueryCardListFilter : public CardListFilter { + public: + QueryCardListFilter(String const& query) : query(query) {} + virtual bool keep(const CardP& card) const; + private: + String query; +}; + // ----------------------------------------------------------------------------- : FilteredCardList /// A card list that lists a subset of the cards in the set class FilteredCardList : public CardListBase { public: - FilteredCardList(Window* parent, int id, long style = 0); + FilteredCardList(Window* parent, int id, long additional_style = 0); /// Change the filter to use void setFilter(const CardListFilterP& filter); diff --git a/src/gui/control/image_card_list.cpp b/src/gui/control/image_card_list.cpp index 4449e3dd..899fa5f4 100644 --- a/src/gui/control/image_card_list.cpp +++ b/src/gui/control/image_card_list.cpp @@ -114,3 +114,28 @@ void ImageCardList::onIdle(wxIdleEvent&) { BEGIN_EVENT_TABLE(ImageCardList, CardListBase) EVT_IDLE (ImageCardList::onIdle) END_EVENT_TABLE () + +// ----------------------------------------------------------------------------- : FilteredImageCardList + +FilteredImageCardList::FilteredImageCardList(Window* parent, int id, long additional_style) + : ImageCardList(parent, id, additional_style) +{} + +void FilteredImageCardList::setFilter(const CardListFilterP& filter) { + this->filter = filter; + rebuild(); +} + +void FilteredImageCardList::onChangeSet() { + // clear filter before changing set, the filter might not make sense for a different set + filter = CardListFilterP(); + CardListBase::onChangeSet(); +} + +void FilteredImageCardList::getItems(vector& out) const { + if (filter) { + filter->getItems(set->cards,out); + } else { + ImageCardList::getItems(out); + } +} diff --git a/src/gui/control/image_card_list.hpp b/src/gui/control/image_card_list.hpp index 7f93b381..eb78b148 100644 --- a/src/gui/control/image_card_list.hpp +++ b/src/gui/control/image_card_list.hpp @@ -11,6 +11,7 @@ #include #include +#include DECLARE_POINTER_TYPE(ImageField); @@ -39,5 +40,23 @@ class ImageCardList : public CardListBase { friend class CardThumbnailRequest; }; +// ----------------------------------------------------------------------------- : FilteredImageCardList + +class FilteredImageCardList : public ImageCardList { + public: + FilteredImageCardList(Window* parent, int id, long additional_style = 0); + + /// Change the filter to use, if null then don't use a filter + void setFilter(const CardListFilterP& filter); + + protected: + /// Get only the subset of the cards + virtual void getItems(vector& out) const; + virtual void onChangeSet(); + + private: + CardListFilterP filter; ///< Filter with which this.cards is made +}; + // ----------------------------------------------------------------------------- : EOF #endif diff --git a/src/gui/set/cards_panel.cpp b/src/gui/set/cards_panel.cpp index 6088a3ab..0460c9ec 100644 --- a/src/gui/set/cards_panel.cpp +++ b/src/gui/set/cards_panel.cpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include // for HoverButton #include #include #include @@ -34,6 +34,142 @@ DECLARE_TYPEOF_COLLECTION(AddCardsScriptP); #define HAVE_TOOLBAR_DROPDOWN_MENU 1 #endif +// ----------------------------------------------------------------------------- : FilterControl + +/// Text control that forwards focus events to the parent +class TextCtrlWithFocus : public wxTextCtrl { + public: + DECLARE_EVENT_TABLE(); + void forwardEvent(wxFocusEvent&); +}; + +/// A search/filter textbox +class FilterCtrl : public wxControl { + public: + FilterCtrl(wxWindow* parent, int id); + /// Set the filter text + void setFilter(const String& filter, bool event = false); + void clearFilter() { setFilter(String()); } + bool hasFilter() const { return !value.empty(); } + String const& getFilter() const { return value; } + + //bool AcceptsFocus() const { return false; } + private: + DECLARE_EVENT_TABLE(); + bool changing; + wxString value; + TextCtrlWithFocus* filter_ctrl; + HoverButton* clear_button; + + void update(); + bool hasFocus(); + void onChange(); + void onChange(wxCommandEvent&); + void onClear(wxCommandEvent&); + void onSize(wxSizeEvent&); + void onSize(); + public: + void onSetFocus(wxFocusEvent&); + void onKillFocus(wxFocusEvent&); +}; + +FilterCtrl::FilterCtrl(wxWindow* parent, int id) + : wxControl(parent, id, wxDefaultPosition, wxSize(160,41), wxSTATIC_BORDER) + , changing(false) +{ + wxColour bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + SetBackgroundColour(bg); + SetCursor(wxCURSOR_IBEAM); + filter_ctrl = new TextCtrlWithFocus(); + filter_ctrl->Create(this, wxID_ANY, _(""), wxDefaultPosition, wxSize(130,-1), wxNO_BORDER); + clear_button = new HoverButton(this, wxID_ANY, _("btn_clear_filter"), bg, false); + clear_button->SetCursor(*wxSTANDARD_CURSOR); + onSize(); + update(); +} + +void FilterCtrl::setFilter(const String& new_value, bool event) { + if (this->value == new_value) return; + // update ui + this->value = new_value; + update(); + // send event + if (event) { + wxCommandEvent ev(wxEVT_COMMAND_TEXT_UPDATED, GetId()); + GetParent()->ProcessEvent(ev); + } +} + +void FilterCtrl::update() { + changing = true; + if (!value.empty() || hasFocus()) { + filter_ctrl->SetValue(value); + wxColour fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + filter_ctrl->SetDefaultStyle(wxTextAttr(fg)); + filter_ctrl->SetForegroundColour(fg); + } else { + filter_ctrl->SetValue(_LABEL_("search cards")); + wxColour fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + wxColour bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + filter_ctrl->SetDefaultStyle(wxTextAttr(lerp(fg,bg,0.5))); + filter_ctrl->SetForegroundColour(lerp(fg,bg,0.5)); + } + clear_button->Show(!value.empty()); + changing = false; +} + +void FilterCtrl::onChange(wxCommandEvent&) { + if (!changing) { + setFilter(filter_ctrl->GetValue(),true); + } +} + +void FilterCtrl::onClear(wxCommandEvent&) { + setFilter(String(),true); +} + +void FilterCtrl::onSize(wxSizeEvent&) { + onSize(); +} +void FilterCtrl::onSize() { + wxSize s = GetClientSize(); + wxSize fs = filter_ctrl->GetBestSize(); + wxSize cs = clear_button->GetBestSize(); + int margin = 2; + filter_ctrl ->SetSize(margin, max(margin,(s.y-fs.y)/2), s.x - cs.x - 3*margin, fs.y); + clear_button->SetSize(s.x - cs.x - margin, (s.y-cs.y)/2, cs.x, cs.y); +} + +void FilterCtrl::onSetFocus(wxFocusEvent&) { + filter_ctrl->SetFocus(); + update(); +} +void FilterCtrl::onKillFocus(wxFocusEvent&) { + update(); +} + +bool FilterCtrl::hasFocus() { + wxWindow* focus = wxWindow::FindFocus(); + return focus == this || focus == filter_ctrl || focus == clear_button; +} + +BEGIN_EVENT_TABLE(FilterCtrl, wxControl) + EVT_BUTTON (wxID_ANY, FilterCtrl::onClear) + EVT_TEXT (wxID_ANY, FilterCtrl::onChange) + EVT_SIZE (FilterCtrl::onSize) + EVT_SET_FOCUS (FilterCtrl::onSetFocus) + EVT_KILL_FOCUS(FilterCtrl::onKillFocus) +END_EVENT_TABLE() + +void TextCtrlWithFocus::forwardEvent(wxFocusEvent& ev) { + GetParent()->ProcessEvent(ev); +} + +BEGIN_EVENT_TABLE(TextCtrlWithFocus, wxTextCtrl) + EVT_SET_FOCUS (TextCtrlWithFocus::forwardEvent) + EVT_KILL_FOCUS(TextCtrlWithFocus::forwardEvent) +END_EVENT_TABLE() + // ----------------------------------------------------------------------------- : CardsPanel CardsPanel::CardsPanel(Window* parent, int id) @@ -42,7 +178,7 @@ CardsPanel::CardsPanel(Window* parent, int id) // init controls editor = new CardEditor(this, ID_EDITOR); splitter = new wxSplitterWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); - card_list = new ImageCardList(splitter, ID_CARD_LIST); + card_list = new FilteredImageCardList(splitter, ID_CARD_LIST); nodes_panel = new Panel(splitter, wxID_ANY); notes = new TextCtrl(nodes_panel, ID_NOTES, true); collapse_notes = new HoverButton(nodes_panel, ID_COLLAPSE_NOTES, _("btn_collapse"), wxNullColour, false); @@ -213,9 +349,10 @@ void CardsPanel::initUI(wxToolBar* tb, wxMenuBar* mb) { #else tb->AddTool(ID_CARD_ROTATE, _(""), load_resource_tool_image(_("card_rotate")), wxNullBitmap,wxITEM_NORMAL, _TOOLTIP_("rotate card"), _HELP_("rotate card")); #endif -//% tb->AddSeparator(); -//% if (!filter) filter = new wxTextCtrl(tb, wxID_ANY, _(""), wxDefaultPosition, wxDefaultSize, wxSTATIC_BORDER); -//% tb->AddControl(filter); + // Filter/search textbox + tb->AddSeparator(); + if (!filter) filter = new FilterCtrl(tb, ID_CARD_FILTER); + tb->AddControl(filter); tb->Realize(); // Menus mb->Insert(2, menuCard, _MENU_("cards")); @@ -231,11 +368,11 @@ void CardsPanel::destroyUI(wxToolBar* tb, wxMenuBar* mb) { tb->DeleteTool(ID_CARD_ADD); tb->DeleteTool(ID_CARD_REMOVE); tb->DeleteTool(ID_CARD_ROTATE); -//% tb->DeleteTool(filter->GetId()); filter = nullptr; + tb->DeleteTool(filter->GetId()); filter = nullptr; // HACK: hardcoded size of rest of toolbar tb->DeleteToolByPos(12); // delete separator tb->DeleteToolByPos(12); // delete separator -//% tb->DeleteToolByPos(12); // delete separator + tb->DeleteToolByPos(12); // delete separator // Menus mb->Remove(3); mb->Remove(2); @@ -343,6 +480,15 @@ void CardsPanel::onCommand(int id) { } break; } + case ID_CARD_FILTER: { + // card filter has changed, update the card list + if (filter->hasFilter()) { + card_list->setFilter(intrusive(new QueryCardListFilter(filter->getFilter()))); + } else { + card_list->setFilter(CardListFilterP()); + } + break; + } default: { if (id >= ID_INSERT_SYMBOL_MENU_MIN && id <= ID_INSERT_SYMBOL_MENU_MAX) { // pass on to editor diff --git a/src/gui/set/cards_panel.hpp b/src/gui/set/cards_panel.hpp index 59eb366d..48446a3a 100644 --- a/src/gui/set/cards_panel.hpp +++ b/src/gui/set/cards_panel.hpp @@ -13,12 +13,13 @@ #include class wxSplitterWindow; -class ImageCardList; +class FilteredImageCardList; class DataEditor; class TextCtrl; class IconMenu; class HoverButton; class FindInfo; +class FilterCtrl; // ----------------------------------------------------------------------------- : CardsPanel @@ -75,11 +76,11 @@ class CardsPanel : public SetWindowPanel { wxSizer* s_left; wxSplitterWindow* splitter; DataEditor* editor; - ImageCardList* card_list; + FilteredImageCardList* card_list; Panel* nodes_panel; TextCtrl* notes; HoverButton* collapse_notes; - wxTextCtrl* filter; + FilterCtrl* filter; bool notes_below_editor; /// Move the notes panel below the editor or below the card list diff --git a/src/gui/set/window.cpp b/src/gui/set/window.cpp index e5bd3cac..f17a5007 100644 --- a/src/gui/set/window.cpp +++ b/src/gui/set/window.cpp @@ -769,6 +769,7 @@ BEGIN_EVENT_TABLE(SetWindow, wxFrame) EVT_COMMAND_RANGE (ID_CHILD_MIN, ID_CHILD_MAX, wxEVT_COMMAND_BUTTON_CLICKED, SetWindow::onChildMenu) EVT_COMMAND_RANGE (ID_CHILD_MIN, ID_CHILD_MAX, wxEVT_COMMAND_SPINCTRL_UPDATED, SetWindow::onChildMenu) EVT_COMMAND_RANGE (ID_CHILD_MIN, ID_CHILD_MAX, wxEVT_COMMAND_RADIOBUTTON_SELECTED, SetWindow::onChildMenu) + EVT_COMMAND_RANGE (ID_CHILD_MIN, ID_CHILD_MAX, wxEVT_COMMAND_TEXT_UPDATED, SetWindow::onChildMenu) EVT_GALLERY_SELECT (ID_FIELD_LIST, SetWindow::onChildMenu) // for StatsPanel, because it is not a EVT_TOOL EVT_UPDATE_UI (wxID_ANY, SetWindow::onUpdateUI) @@ -781,4 +782,5 @@ BEGIN_EVENT_TABLE(SetWindow, wxFrame) EVT_CARD_SELECT (wxID_ANY, SetWindow::onCardSelect) EVT_CARD_ACTIVATE (wxID_ANY, SetWindow::onCardActivate) EVT_SIZE_CHANGE (wxID_ANY, SetWindow::onSizeChange) + EVT_ERASE_BACKGROUND( SetWindow::onEraseBackground) END_EVENT_TABLE () diff --git a/src/gui/set/window.hpp b/src/gui/set/window.hpp index 4b2f2789..e9664df2 100644 --- a/src/gui/set/window.hpp +++ b/src/gui/set/window.hpp @@ -165,6 +165,7 @@ class SetWindow : public wxFrame, public SetView { void onIdle (wxIdleEvent&); void onSizeChange (wxCommandEvent&); + void onEraseBackground (wxEraseEvent&) {} // reduce flicker }; // ----------------------------------------------------------------------------- : EOF diff --git a/src/resource/common/btn_clear_filter_down.png b/src/resource/common/btn_clear_filter_down.png new file mode 100644 index 0000000000000000000000000000000000000000..b94ec936e65d0f900afe89c775e6eea45eca6d70 GIT binary patch literal 773 zcmV+g1N!`lP)pF33NqRbVF}#ZDnqB004<9jRpV!0-{MoK~#90m6F>t)L|IM->c3? zC0miOVw%D_WwC9GREu0x%1k0TZPsC{#jc`O8D^SX8EYDi5?WE&9Z@lvVr54nWtz}T zGfjVk&BeEU-fcIXF1q;se(${VeV=*0pXXuo^H#PpWP=Hl zF2Ogs63j)MhwREGY?c+lWB++D-=uPct9qPb8zQo>T$GO0VP~Me-e$XqQa~tI<97ED zJmb}1-U;PseQZH+YPG$n>S_mfC zSBNb!#n_pwg?VBYEhaO%`d`B*K?7l;*51~xw0hj@pF;P`4-n_xgk|~*LNfLIo?UJ* zLRLG2xVo=@Xm@%en6l~?mIR!{vY=B~5uD-d$33P1M2e%LkOXMGI2V-DRgw=kX#sX5 zmfBeT==!c{7}T&(UV`GI`5G(_(`_s^kXO=zPwM2J0SM%rOxank|uW zk2?qEFc4UeI2BrZMlk$l25q*#|6vXMk~Fwu9OjGA`D_gS$>s1mR0?l-8D9uSRcKiq z;I|(vLBNqp82T(W=5sc*b9gW?j?mPrPWFq%sEXgw8u%Y0)gn0U2HKyDWAxo;JQ|!r z=fEUC-|wG5Sb81zIK&IkCn%@IsfuPZOe1rUsydu^k|F~h4$b0P>nKEvyu3hAiVl&Q z7l^K~{FmsB=(rAS-ujL2JQeyq-@ioOD|#cW3k&Q!Sr<6==xo#U00000NkvXXu0mjf DZ&YFm literal 0 HcmV?d00001 diff --git a/src/resource/common/btn_clear_filter_focus.png b/src/resource/common/btn_clear_filter_focus.png new file mode 100644 index 0000000000000000000000000000000000000000..b944224faffe903f8c0ea6521525aff10f90740f GIT binary patch literal 732 zcmV<20wev2P)pF33NqRbVF}#ZDnqB004<9jRpV!0(nV9K~#90l~essQehOne^Ik7 zw`#V^MXlD>oNH_454M(MI*S!(q?v?RG16A8(gvC|KS)yp6^lM*HkV%w=0Xi2AVdPM z0`j5Z(|zANuDI-n?)Kc>d+vFkbI$Xe%OsNP%#O2-pf_^S2O7yb-vR;}8L+P4((^eK z)Q|Fk{2mBkc3;pUeNNT-3CxY&0IIv)a0TKxR@DzCyQB?PM*#aJ?L2Av>=|PQ$Nk~o zP``_#&K!ZlxP;1Aew255P^z=@RZFzrP}(#_JGFaQ%==iOFiqH3Gl6MO7_MLf$JJJv zC{j()I8)2DCTHVNrtlX;^=3XHO_zy1#X1nB7`0^;_dc%S&f8Te2bNLY_Y3kaAEfO+ zu=k=4%+1$9T<>1MPj(Fwy#vx0bLboMBVYBNZ?ddq7DZ33aQY*dUkK9%b1%1UPLU(& z!cK83a?0BHG2KC=t)K~Hlw9Q`{dhuS!}rBFW_?jQB27q3TVTW%n+Rxvs9|VZ4diSL zb@~xrhdjj)9uCLQ^koB&M>p_bB!vdfAI*m(_2~ASC{*S!ZuPUMF@~s5qKaOgfRwpw z;n;>Es2yB^!n6dXDTu0`MJi8)!3$Z38|57?DicwKcq?wSZF)%pF33NqRbVF}#ZDnqB004<9jRpV!0(nV9K~#90l~essQehOne^Ik7 zw`#V^MXlD>oNH_454M(MI*S!(q?v?RG16A8(gvC|KS)yp6^lM*HkV%w=0Xi2AVdPM z0`j5Z(|zANuDI-n?)Kc>d+vFkbI$Xe%OsNP%#O2-pf_^S2O7yb-vR;}8L+P4((^eK z)Q|Fk{2mBkc3;pUeNNT-3CxY&0IIv)a0TKxR@DzCyQB?PM*#aJ?L2Av>=|PQ$Nk~o zP``_#&K!ZlxP;1Aew255P^z=@RZFzrP}(#_JGFaQ%==iOFiqH3Gl6MO7_MLf$JJJv zC{j()I8)2DCTHVNrtlX;^=3XHO_zy1#X1nB7`0^;_dc%S&f8Te2bNLY_Y3kaAEfO+ zu=k=4%+1$9T<>1MPj(Fwy#vx0bLboMBVYBNZ?ddq7DZ33aQY*dUkK9%b1%1UPLU(& z!cK83a?0BHG2KC=t)K~Hlw9Q`{dhuS!}rBFW_?jQB27q3TVTW%n+Rxvs9|VZ4diSL zb@~xrhdjj)9uCLQ^koB&M>p_bB!vdfAI*m(_2~ASC{*S!ZuPUMF@~s5qKaOgfRwpw z;n;>Es2yB^!n6dXDTu0`MJi8)!3$Z38|57?DicwKcq?wSZF)%pF33NqRbVF}#ZDnqB004<9jRpV!0)t6JK~#90l~e6Y(@_-u{WE4C ztQ1L55;go3l>})8mZhuAv`FXDY&vJYPv+F&HcRs*DqUt_xH zX-PjHh`BKs0Cym@9R9j;l@6Tv!AFR@rm^h#f#h5bxQNtB%*;CwlU~D<`fC%4UIaOs zMm=a4bfEg_63QQ&al2;@qV_4|G`;3e&2D^!?1o{wsd@PetafkUU`mKMtHhku4V!BN z@j3037$eY7oRhTbkW{;fWclh}*q5Q^6Z~|ch)k1$DaF<6KSMt3LY-~}ceD#Ad-w^( zogW}PM0D4gapxNs%X<>TP4K^EP=HWovEVD#Jmnz>|bGC@luPF;FmVBXk>>?43x0{ zyn0s+l4=$pRq1&hj^y7bKQxB#XdPci%S$g5BW`M6_S}xzXUnMSx1yqNp1fi{C0tfQ zs@C&XFs*6^Y^Z#)2x+ec6}@J#xwzFegPR@G5Z*OHbZ?Sm%55>=ieelB*=W#9##qoc ziNdz`aIS4q!w&Bzt~HP0V%-p|K9AY5IoA-MB}c?bDZ*34L4veWEshGia5zVW*b8zb z2;?x^-0&|pJ+UF$hk^+9rq%{2Qg;9<{Tys= z-+L(324YG1eVBJ_pmWrOsE}MDU{e=YY`~#X<9~tG&@i%*WmYnx-Fp1jsIw=$wY|;# Y0o070EY0{`eE