Back to index

unity  6.0.0
HudView.cpp
Go to the documentation of this file.
00001 /*
00002  * Copyright (C) 2010 Canonical Ltd
00003  *
00004  * This program is free software: you can redistribute it and/or modify
00005  * it under the terms of the GNU General Public License version 3 as
00006  * published by the Free Software Foundation.
00007  *
00008  * This program is distributed in the hope that it will be useful,
00009  * but WITHOUT ANY WARRANTY; without even the implied warranty of
00010  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00011  * GNU General Public License for more details.
00012  *
00013  * You should have received a copy of the GNU General Public License
00014  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
00015  *
00016  * Authored by: Gord Allott <gord.allott@canonical.com>
00017  */
00018 
00019 #include "HudView.h"
00020 
00021 #include <math.h>
00022 
00023 #include <glib/gi18n-lib.h>
00024 #include <NuxCore/Logger.h>
00025 #include <UnityCore/GLibWrapper.h>
00026 #include <UnityCore/Variant.h>
00027 #include <Nux/HLayout.h>
00028 #include <Nux/VLayout.h>
00029 
00030 #include "unity-shared/Introspectable.h"
00031 
00032 #include "unity-shared/UBusMessages.h"
00033 #include "unity-shared/DashStyle.h"
00034 
00035 namespace unity
00036 {
00037 namespace hud
00038 {
00039 namespace
00040 {
00041 
00042 nux::logging::Logger logger("unity.hud.view");
00043 
00044 const int grow_anim_length = 90 * 1000;
00045 const int pause_before_grow_length = 32 * 1000;
00046 
00047 const int default_width = 960;
00048 const int default_height = 276;
00049 const int content_width = 939;
00050 
00051 const int top_padding = 11;
00052 const int bottom_padding = 10;
00053 const int left_padding = 11;
00054 const int right_padding = 0;
00055 
00056 }
00057 
00058 NUX_IMPLEMENT_OBJECT_TYPE(View);
00059 
00060 View::View()
00061   : AbstractView()
00062   , button_views_(nullptr)
00063   , start_time_(0)
00064   , last_known_height_(0)
00065   , current_height_(0)
00066   , timeline_need_more_draw_(false)
00067   , selected_button_(0)
00068   , show_embedded_icon_(true)
00069 {
00070   renderer_.SetOwner(this);
00071   renderer_.need_redraw.connect([this] () {
00072     QueueDraw();
00073   });
00074 
00075   SetupViews();
00076   search_bar_->key_down.connect (sigc::mem_fun (this, &View::OnKeyDown));
00077 
00078   search_bar_->activated.connect (sigc::mem_fun (this, &View::OnSearchbarActivated));
00079 
00080   search_bar_->text_entry()->SetLoseKeyFocusOnKeyNavDirectionUp(false);
00081   search_bar_->text_entry()->SetLoseKeyFocusOnKeyNavDirectionDown(false);
00082 
00083   search_bar_->text_entry()->key_nav_focus_change.connect([&](nux::Area *area, bool receiving, nux::KeyNavDirection direction)
00084   {
00085     // We get here when the Hud closes.
00086     // The TextEntry should always have the keyboard focus as long as the hud is open.
00087 
00088     if (buttons_.empty())
00089       return;// early return on empty button list
00090 
00091     if (receiving)
00092     {
00093       if (!buttons_.empty())
00094       {
00095         // If the search_bar gets focus, fake focus the first button if it exists
00096         buttons_.back()->fake_focused = true;
00097       }
00098     }
00099     else
00100     {
00101       // The hud is closing and there are HudButtons visible. Remove the fake_focus.
00102       // There should be only one HudButton with the fake_focus set to true.
00103       std::list<HudButton::Ptr>::iterator it;
00104       for(it = buttons_.begin(); it != buttons_.end(); ++it)
00105       {
00106         if ((*it)->fake_focused)
00107         {
00108           (*it)->fake_focused = false;
00109         }
00110       }
00111     }
00112   });
00113 
00114   mouse_down.connect(sigc::mem_fun(this, &View::OnMouseButtonDown));
00115 
00116   Relayout();
00117 }
00118 
00119 View::~View()
00120 {
00121 }
00122 
00123 void View::ProcessGrowShrink()
00124 {
00125   float diff = g_get_monotonic_time() - start_time_;
00126   int target_height = content_layout_->GetGeometry().height;
00127   // only animate if we are after our defined pause time
00128   if (diff > pause_before_grow_length)
00129   {
00130    float progress = (diff - pause_before_grow_length) / grow_anim_length;
00131    int last_height = last_known_height_;
00132    int new_height = 0;
00133 
00134    if (last_height < target_height)
00135    {
00136      // grow
00137      new_height = last_height + ((target_height - last_height) * progress);
00138    }
00139    else
00140    {
00141      //shrink
00142      new_height = last_height - ((last_height - target_height) * progress);
00143    }
00144 
00145    LOG_DEBUG(logger) << "resizing to " << target_height << " (" << new_height << ")"
00146                      << "View height: " << GetGeometry().height;
00147    current_height_ = new_height;
00148   }
00149 
00150   for (auto button : buttons_)
00151   {
00152     button->SetSkipDraw((button->GetAbsoluteY() + button->GetBaseHeight()) > (GetAbsoluteY() + current_height_));
00153   }
00154 
00155   QueueDraw();
00156 
00157   if (diff > grow_anim_length + pause_before_grow_length)
00158   {
00159     // ensure we are at our final location and update last known height
00160     current_height_ = target_height;
00161     last_known_height_ = target_height;
00162     timeline_need_more_draw_ = false;
00163   }
00164 }
00165 
00166 
00167 void View::ResetToDefault()
00168 {
00169   SetQueries(Hud::Queries());
00170   current_height_ = content_layout_->GetBaseHeight();;
00171 
00172   search_bar_->search_string = "";
00173   search_bar_->search_hint = _("Type your command");
00174 }
00175 
00176 void View::Relayout()
00177 {
00178   nux::Geometry const& geo = GetGeometry();
00179   content_geo_ = GetBestFitGeometry(geo);
00180   LOG_DEBUG(logger) << "content_geo: " << content_geo_.width << "x" << content_geo_.height;
00181 
00182   layout_->SetMinimumWidth(content_geo_.width);
00183   layout_->SetMaximumSize(content_geo_.width, content_geo_.height);
00184 
00185   QueueDraw();
00186 }
00187 
00188 long View::PostLayoutManagement(long LayoutResult)
00189 {
00190   Relayout();
00191   if (GetGeometry().height != last_known_height_)
00192   {
00193     // Start the timeline of drawing the dash resize
00194     if (timeline_need_more_draw_)
00195     {
00196       // already started, just reset the last known height
00197       last_known_height_ = current_height_;
00198     }
00199 
00200     timeline_need_more_draw_ = true;
00201     start_time_ = g_get_monotonic_time();
00202     QueueDraw();
00203   }
00204 
00205   return LayoutResult;
00206 }
00207 
00208 
00209 nux::View* View::default_focus() const
00210 {
00211   return search_bar_->text_entry();
00212 }
00213 
00214 std::list<HudButton::Ptr> const& View::buttons() const
00215 {
00216   return buttons_;
00217 }
00218 
00219 void View::SetQueries(Hud::Queries queries)
00220 {
00221   // early exit, if the user is key navigating on the hud, we don't want to set new
00222   // queries under them, that is just rude
00223   if (!buttons_.empty() && buttons_.back()->fake_focused == false)
00224     return;
00225 
00226   // remove the previous children
00227   for (auto button : buttons_)
00228   {
00229     RemoveChild(button.GetPointer());
00230   }
00231 
00232   selected_button_ = 0;
00233   queries_ = queries_;
00234   buttons_.clear();
00235   button_views_->Clear();
00236   int found_items = 0;
00237   for (auto query : queries)
00238   {
00239     if (found_items >= 5)
00240       break;
00241 
00242     HudButton::Ptr button(new HudButton());
00243     buttons_.push_front(button);
00244     button->SetMinimumWidth(content_width);
00245     button->SetMaximumWidth(content_width);
00246     button->SetQuery(query);
00247 
00248     button_views_->AddView(button.GetPointer(), 0, nux::MINOR_POSITION_LEFT);
00249 
00250     button->click.connect([&](nux::View* view) {
00251       query_activated.emit(dynamic_cast<HudButton*>(view)->GetQuery());
00252     });
00253 
00254     button->key_nav_focus_activate.connect([&](nux::Area* area) {
00255       query_activated.emit(dynamic_cast<HudButton*>(area)->GetQuery());
00256     });
00257 
00258     button->key_nav_focus_change.connect([&](nux::Area* area, bool recieving, nux::KeyNavDirection direction){
00259       if (recieving)
00260         query_selected.emit(dynamic_cast<HudButton*>(area)->GetQuery());
00261     });
00262 
00263     ++found_items;
00264   }
00265 
00266   if (found_items)
00267   {
00268     buttons_.front()->is_rounded = true;
00269     buttons_.back()->fake_focused = true;
00270     selected_button_ = 1;
00271   }
00272 
00273   QueueRelayout();
00274   QueueDraw();
00275 }
00276 
00277 void View::SetIcon(std::string const& icon_name, unsigned int tile_size, unsigned int size, unsigned int padding)
00278 {
00279   if (!icon_)
00280     return;
00281 
00282   LOG_DEBUG(logger) << "Setting icon to " << icon_name;
00283 
00284   icon_->SetIcon(icon_name, size, tile_size);
00285   icon_->SetMinimumWidth(tile_size + padding);
00286 
00287   /* We need to compute this value manually, since the _content_layout height changes */
00288   int content_height = search_bar_->GetBaseHeight() + top_padding + bottom_padding;
00289   icon_->SetMinimumHeight(std::max(icon_->GetMinimumHeight(), content_height));
00290 
00291   QueueDraw();
00292 }
00293 
00294 void View::ShowEmbeddedIcon(bool show)
00295 {
00296   LOG_DEBUG(logger) << "Hide icon called";
00297   if (show == show_embedded_icon_)
00298     return;
00299 
00300   show_embedded_icon_ = show;
00301 
00302   if (show_embedded_icon_)
00303   {
00304     layout_->AddView(icon_.GetPointer(), 0, nux::MINOR_POSITION_LEFT,
00305                      nux::MINOR_SIZE_FULL, 100.0f, nux::LayoutPosition::NUX_LAYOUT_BEGIN);
00306     AddChild(icon_.GetPointer());
00307   }
00308   else
00309   {
00310     layout_->RemoveChildObject(icon_.GetPointer());
00311     RemoveChild(icon_.GetPointer());
00312   }
00313 
00314   Relayout();
00315 }
00316 
00317 // Gives us the width and height of the contents that will give us the best "fit",
00318 // which means that the icons/views will not have uneccessary padding, everything will
00319 // look tight
00320 nux::Geometry View::GetBestFitGeometry(nux::Geometry const& for_geo)
00321 {
00322   //FIXME - remove magic values, replace with scalable text depending on DPI
00323   // requires smarter font settings really...
00324   int width = default_width;
00325   int height = default_height;
00326 
00327   if (show_embedded_icon_)
00328     width += icon_->GetGeometry().width;
00329 
00330   LOG_DEBUG (logger) << "best fit is, " << width << ", " << height;
00331 
00332   return nux::Geometry(0, 0, width, height);
00333 }
00334 
00335 void View::AboutToShow()
00336 {
00337   renderer_.AboutToShow();
00338 }
00339 
00340 void View::AboutToHide()
00341 {
00342   renderer_.AboutToHide();
00343 }
00344 
00345 void View::SetWindowGeometry(nux::Geometry const& absolute_geo, nux::Geometry const& geo)
00346 {
00347   window_geometry_ = geo;
00348   window_geometry_.x = 0;
00349   window_geometry_.y = 0;
00350   absolute_window_geometry_ = absolute_geo;
00351 }
00352 
00353 void View::SetupViews()
00354 {
00355   dash::Style& style = dash::Style::Instance();
00356 
00357   nux::VLayout* super_layout = new nux::VLayout();
00358   layout_ = new nux::HLayout();
00359   {
00360     // fill layout with icon
00361     icon_ = new Icon();
00362     {
00363       AddChild(icon_.GetPointer());
00364       layout_->AddView(icon_.GetPointer(), 0, nux::MINOR_POSITION_LEFT, nux::MINOR_SIZE_FULL);
00365     }
00366 
00367     // fill the content layout
00368     content_layout_ = new nux::VLayout();
00369     {
00370       // Set the layout paddings
00371       content_layout_->SetLeftAndRightPadding(left_padding, right_padding);
00372       content_layout_->SetTopAndBottomPadding(top_padding, bottom_padding);
00373 
00374       // add the search bar to the composite
00375       search_bar_ = new unity::SearchBar(true);
00376       search_bar_->SetMinimumHeight(style.GetSearchBarHeight());
00377       search_bar_->SetMaximumHeight(style.GetSearchBarHeight());
00378       search_bar_->search_hint = _("Type your command");
00379       search_bar_->live_search_reached.connect(sigc::mem_fun(this, &View::OnSearchChanged));
00380       AddChild(search_bar_.GetPointer());
00381       content_layout_->AddView(search_bar_.GetPointer(), 0, nux::MINOR_POSITION_LEFT);
00382 
00383       button_views_ = new nux::VLayout();
00384       button_views_->SetMaximumWidth(content_width);
00385 
00386       content_layout_->AddLayout(button_views_.GetPointer(), 1, nux::MINOR_POSITION_LEFT);
00387     }
00388 
00389     layout_->AddLayout(content_layout_.GetPointer(), 1, nux::MINOR_POSITION_TOP);
00390   }
00391 
00392   super_layout->AddLayout(layout_.GetPointer(), 0);
00393   SetLayout(super_layout);
00394 }
00395 
00396 void View::OnSearchChanged(std::string const& search_string)
00397 {
00398   LOG_DEBUG(logger) << "got search change";
00399   search_changed.emit(search_string);
00400 
00401   for(auto button : buttons_)
00402   {
00403     button->fake_focused = false;
00404   }
00405 
00406   if (!buttons_.empty())
00407     buttons_.back()->fake_focused = true;
00408 }
00409 
00410 
00411 void View::OnKeyDown (unsigned long event_type, unsigned long keysym,
00412                       unsigned long event_state, const TCHAR* character,
00413                       unsigned short key_repeat_count)
00414 {
00415   if (keysym == NUX_VK_ESCAPE)
00416   {
00417     LOG_DEBUG(logger) << "got escape key";
00418     ubus.SendMessage(UBUS_HUD_CLOSE_REQUEST);
00419   }
00420 }
00421 
00422 void View::OnMouseButtonDown(int x, int y, unsigned long button, unsigned long key)
00423 {
00424   nux::Geometry current_geo(content_geo_);
00425   current_geo.height = current_height_;
00426   if (!current_geo.IsPointInside(x, y))
00427   {
00428     ubus.SendMessage(UBUS_HUD_CLOSE_REQUEST);
00429   }
00430 }
00431 
00432 void View::Draw(nux::GraphicsEngine& gfx_context, bool force_draw)
00433 {
00434   if (timeline_need_more_draw_)
00435   {
00436     ProcessGrowShrink();
00437   }
00438 
00439   nux::Geometry draw_content_geo(layout_->GetGeometry());
00440   draw_content_geo.height = current_height_;
00441   renderer_.DrawFull(gfx_context, draw_content_geo, absolute_window_geometry_, window_geometry_, true);
00442 }
00443 
00444 void View::DrawContent(nux::GraphicsEngine& gfx_context, bool force_draw)
00445 {
00446   nux::Geometry draw_content_geo(layout_->GetGeometry());
00447   draw_content_geo.height = current_height_;
00448 
00449   renderer_.DrawInner(gfx_context, draw_content_geo, absolute_window_geometry_, window_geometry_);
00450 
00451   gfx_context.PushClippingRectangle(draw_content_geo);
00452 
00453   if (IsFullRedraw())
00454   {
00455     nux::GetPainter().PushBackgroundStack();
00456 
00457     if (!buttons_.empty()) // See bug #1008603.
00458     {
00459       int height = 3;
00460       int x = search_bar_->GetBaseX() + 1;
00461       int y = search_bar_->GetBaseY() + search_bar_->GetBaseHeight() - height;
00462       nux::GetPainter().Draw2DLine(gfx_context, x, y, x, y + height, nux::color::White * 0.13);
00463       x += content_width - 1;
00464       nux::GetPainter().Draw2DLine(gfx_context, x, y, x, y + height, nux::color::White * 0.13);
00465     }
00466 
00467     GetLayout()->ProcessDraw(gfx_context, force_draw);
00468     nux::GetPainter().PopBackgroundStack();
00469   }
00470   else
00471   {
00472     GetLayout()->ProcessDraw(gfx_context, force_draw);
00473   }
00474   gfx_context.PopClippingRectangle();
00475 
00476   renderer_.DrawInnerCleanup(gfx_context, draw_content_geo, absolute_window_geometry_, window_geometry_);
00477 
00478   if (timeline_need_more_draw_ && !timeline_idle_)
00479   {
00480     timeline_idle_.reset(new glib::Idle([&] () {
00481       QueueDraw();
00482       timeline_idle_.reset();
00483       return false;
00484     }));
00485   }
00486 }
00487 
00488 // Keyboard navigation
00489 bool View::AcceptKeyNavFocus()
00490 {
00491   return false;
00492 }
00493 
00494 // Introspectable
00495 std::string View::GetName() const
00496 {
00497   return "HudView";
00498 }
00499 
00500 void View::AddProperties(GVariantBuilder* builder)
00501 {
00502   unsigned num_buttons = buttons_.size();
00503   variant::BuilderWrapper(builder)
00504     .add(GetGeometry())
00505     .add("selected_button", selected_button_)
00506     .add("num_buttons", num_buttons);
00507 }
00508 
00509 debug::Introspectable::IntrospectableList View::GetIntrospectableChildren()
00510 {
00511     introspectable_children_.clear();
00512     introspectable_children_.merge(debug::Introspectable::GetIntrospectableChildren());
00513     for (auto button: buttons_)
00514     {
00515       introspectable_children_.push_front(button.GetPointer());
00516     }
00517 
00518     return introspectable_children_;
00519 }
00520 
00521 bool View::InspectKeyEvent(unsigned int eventType,
00522                            unsigned int key_sym,
00523                            const char* character)
00524 {
00525   if ((eventType == nux::NUX_KEYDOWN) && (key_sym == NUX_VK_ESCAPE))
00526   {
00527     if (search_bar_->search_string == "")
00528     {
00529       ubus.SendMessage(UBUS_HUD_CLOSE_REQUEST);
00530     }
00531     else
00532     {
00533       search_bar_->search_string = "";
00534     }
00535     return true;
00536   }
00537   return false;
00538 }
00539 
00540 void View::SearchFinished()
00541 {
00542   search_bar_->SearchFinished();
00543 }
00544 
00545 void View::OnSearchbarActivated()
00546 {
00547   // The "Enter" key has been received and the text entry has the key focus.
00548   // If one of the button has the fake_focus, we get it to emit the query_activated signal.
00549   if (!buttons_.empty())
00550   {
00551     std::list<HudButton::Ptr>::iterator it;
00552     for(it = buttons_.begin(); it != buttons_.end(); ++it)
00553     {
00554       if ((*it)->fake_focused)
00555       {
00556         query_activated.emit((*it)->GetQuery());
00557         return;
00558       }
00559     }
00560   }
00561   search_activated.emit(search_bar_->search_string);
00562 }
00563 
00564 nux::Area* View::FindKeyFocusArea(unsigned int event_type,
00565       unsigned long x11_key_code,
00566       unsigned long special_keys_state)
00567 {
00568   nux::KeyNavDirection direction = nux::KEY_NAV_NONE;
00569   switch (x11_key_code)
00570   {
00571   case NUX_VK_UP:
00572     direction = nux::KEY_NAV_UP;
00573     break;
00574   case NUX_VK_DOWN:
00575     direction = nux::KEY_NAV_DOWN;
00576     break;
00577   case NUX_VK_LEFT:
00578     direction = nux::KEY_NAV_LEFT;
00579     break;
00580   case NUX_VK_RIGHT:
00581     direction = nux::KEY_NAV_RIGHT;
00582     break;
00583   case NUX_VK_LEFT_TAB:
00584     direction = nux::KEY_NAV_TAB_PREVIOUS;
00585     break;
00586   case NUX_VK_TAB:
00587     direction = nux::KEY_NAV_TAB_NEXT;
00588     break;
00589   case NUX_VK_ENTER:
00590   case NUX_KP_ENTER:
00591     // Not sure if Enter should be a navigation key
00592     direction = nux::KEY_NAV_ENTER;
00593     break;
00594   case NUX_VK_F4:
00595     if (special_keys_state == nux::NUX_STATE_ALT)
00596     {
00597       ubus.SendMessage(UBUS_HUD_CLOSE_REQUEST);
00598     }
00599     break;
00600   default:
00601     direction = nux::KEY_NAV_NONE;
00602     break;
00603   }
00604 
00605 
00606   if ((event_type == nux::NUX_KEYDOWN) && (x11_key_code == NUX_VK_ESCAPE))
00607   {
00608     // Escape key! This is how it works:
00609     //    -If there is text, clear it and give the focus to the text entry view.
00610     //    -If there is no text text, then close the hud.
00611 
00612     if (search_bar_->search_string == "")
00613     {
00614       ubus.SendMessage(UBUS_HUD_CLOSE_REQUEST);
00615     }
00616     else
00617     {
00618       search_bar_->search_string = "";
00619       return search_bar_->text_entry();
00620     }
00621     return NULL;
00622   }
00623 
00624   if (search_bar_->text_entry()->HasKeyFocus() && !search_bar_->im_preedit)
00625   {
00626     if (direction == nux::KEY_NAV_NONE ||
00627         direction == nux::KEY_NAV_UP ||
00628         direction == nux::KEY_NAV_DOWN ||
00629         direction == nux::KEY_NAV_LEFT ||
00630         direction == nux::KEY_NAV_RIGHT)
00631     {
00632       // We have received a key character or a keyboard arrow Up or Down (navigation keys).
00633       // Because we have called "SetLoseKeyFocusOnKeyNavDirectionUp(false);" and "SetLoseKeyFocusOnKeyNavDirectionDown(false);"
00634       // on the text entry, the text entry will not loose the keyboard focus.
00635       // All that we need to do here is set the fake_focused value on the HudButton.
00636 
00637       if (!buttons_.empty())
00638       {
00639         if (event_type == nux::NUX_KEYDOWN && direction == nux::KEY_NAV_UP)
00640         {
00641           std::list<HudButton::Ptr>::iterator it;
00642           for(it = buttons_.begin(); it != buttons_.end(); ++it)
00643           {
00644             if ((*it)->fake_focused)
00645             {
00646               std::list<HudButton::Ptr>::iterator next = it;
00647               ++next;
00648               if (next != buttons_.end())
00649               {
00650                 // The button with the current fake_focus looses it.
00651                 (*it)->fake_focused = false;
00652                 // The next button gets the fake_focus
00653                 (*next)->fake_focused = true;
00654                 query_selected.emit((*next)->GetQuery());
00655                 --selected_button_;
00656               }
00657               break;
00658             }
00659           }
00660         }
00661 
00662         if (event_type == nux::NUX_KEYDOWN && direction == nux::KEY_NAV_DOWN)
00663         {
00664           std::list<HudButton::Ptr>::reverse_iterator rit;
00665           for(rit = buttons_.rbegin(); rit != buttons_.rend(); ++rit)
00666           {
00667             if ((*rit)->fake_focused)
00668             {
00669               std::list<HudButton::Ptr>::reverse_iterator next = rit;
00670               ++next;
00671               if(next != buttons_.rend())
00672               {
00673                 // The button with the current fake_focus looses it.
00674                 (*rit)->fake_focused = false;
00675                 // The next button bellow gets the fake_focus.
00676                 (*next)->fake_focused = true;
00677                 query_selected.emit((*next)->GetQuery());
00678                 ++selected_button_;
00679               }
00680               break;
00681             }
00682           }
00683         }
00684       }
00685       return search_bar_->text_entry();
00686     }
00687 
00688     if (event_type == nux::NUX_KEYDOWN && direction == nux::KEY_NAV_ENTER)
00689     {
00690       // We still choose the text_entry as the receiver of the key focus.
00691       return search_bar_->text_entry();
00692     }
00693   }
00694   else if (direction == nux::KEY_NAV_NONE || search_bar_->im_preedit)
00695   {
00696     return search_bar_->text_entry();
00697   }
00698   else if (next_object_to_key_focus_area_)
00699   {
00700     return next_object_to_key_focus_area_->FindKeyFocusArea(event_type, x11_key_code, special_keys_state);
00701   }
00702   return search_bar_->text_entry();
00703 }
00704 
00705 }
00706 }
00707