TreeViewDragDrop.C 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669
  1. /*
  2. * Copyright (C) 2008 Emweb bvba, Kessel-Lo, Belgium.
  3. *
  4. * See the LICENSE file for terms of use.
  5. */
  6. #include <fstream>
  7. #include <Wt/WApplication>
  8. #include <Wt/WComboBox>
  9. #include <Wt/WContainerWidget>
  10. #include <Wt/WDatePicker>
  11. #include <Wt/WDateValidator>
  12. #include <Wt/WDialog>
  13. #include <Wt/WEnvironment>
  14. #include <Wt/WIntValidator>
  15. #include <Wt/WItemDelegate>
  16. #include <Wt/WLabel>
  17. #include <Wt/WLineEdit>
  18. #include <Wt/WMessageBox>
  19. #include <Wt/WPushButton>
  20. #include <Wt/WRegExpValidator>
  21. #include <Wt/WGridLayout>
  22. #include <Wt/WPopupMenu>
  23. #include <Wt/WSortFilterProxyModel>
  24. #include <Wt/WStandardItem>
  25. #include <Wt/WStandardItemModel>
  26. #include <Wt/WTableView>
  27. #include <Wt/WTreeView>
  28. #include <Wt/WText>
  29. #include <Wt/WVBoxLayout>
  30. #include <Wt/Chart/WPieChart>
  31. #include "CsvUtil.h"
  32. #include "FolderView.h"
  33. using namespace Wt;
  34. /**
  35. * \defgroup treeviewdragdrop Drag and drop in WTreeView example
  36. */
  37. /*@{*/
  38. /*! \class FileModel
  39. * \brief A specialized standard item model which report a specific
  40. * drag and drop mime type.
  41. *
  42. * A specific drag and drop mime type instead of the generic abstract
  43. * item model is returned by the model.
  44. */
  45. class FileModel : public WStandardItemModel
  46. {
  47. public:
  48. /*! \brief Constructor.
  49. */
  50. FileModel(WObject *parent)
  51. : WStandardItemModel(parent) { }
  52. /*! \brief Return the mime type.
  53. */
  54. virtual std::string mimeType() const {
  55. return FolderView::FileSelectionMimeType;
  56. }
  57. /// Date display format.
  58. static WString dateDisplayFormat;
  59. /// Date edit format.
  60. static WString dateEditFormat;
  61. };
  62. WString FileModel::dateDisplayFormat(WString::fromUTF8("MMM dd, yyyy"));
  63. WString FileModel::dateEditFormat(WString::fromUTF8("dd-MM-yyyy"));
  64. /*! \class FileEditDialog
  65. * \brief A dialog for editing a 'file'.
  66. */
  67. class FileEditDialog : public WDialog
  68. {
  69. public:
  70. FileEditDialog(WAbstractItemModel *model, const WModelIndex& item)
  71. : WDialog("Edit..."),
  72. model_(model),
  73. item_(item)
  74. {
  75. int modelRow = item_.row();
  76. resize(300, WLength::Auto);
  77. /*
  78. * Create the form widgets, and load them with data from the model.
  79. */
  80. // name
  81. nameEdit_ = new WLineEdit(asString(model_->data(modelRow, 1)));
  82. // type
  83. typeEdit_ = new WComboBox();
  84. typeEdit_->addItem("Document");
  85. typeEdit_->addItem("Spreadsheet");
  86. typeEdit_->addItem("Presentation");
  87. typeEdit_->setCurrentIndex
  88. (typeEdit_->findText(asString(model_->data(modelRow, 2))));
  89. // size
  90. sizeEdit_ = new WLineEdit(asString(model_->data(modelRow, 3)));
  91. sizeEdit_->setValidator
  92. (new WIntValidator(0, std::numeric_limits<int>::max(), this));
  93. // created
  94. createdPicker_ = new WDatePicker();
  95. createdPicker_->lineEdit()->validator()->setMandatory(true);
  96. createdPicker_->setFormat(FileModel::dateEditFormat);
  97. createdPicker_->setDate(boost::any_cast<WDate>(model_->data(modelRow, 4)));
  98. // modified
  99. modifiedPicker_ = new WDatePicker();
  100. modifiedPicker_->lineEdit()->validator()->setMandatory(true);
  101. modifiedPicker_->setFormat(FileModel::dateEditFormat);
  102. modifiedPicker_->setDate(boost::any_cast<WDate>(model_->data(modelRow, 5)));
  103. /*
  104. * Use a grid layout for the labels and fields
  105. */
  106. WGridLayout *layout = new WGridLayout();
  107. WLabel *l;
  108. int row = 0;
  109. layout->addWidget(l = new WLabel("Name:"), row, 0);
  110. layout->addWidget(nameEdit_, row, 1);
  111. l->setBuddy(nameEdit_);
  112. ++row;
  113. layout->addWidget(l = new WLabel("Type:"), row, 0);
  114. layout->addWidget(typeEdit_, row, 1);
  115. l->setBuddy(typeEdit_);
  116. ++row;
  117. layout->addWidget(l = new WLabel("Size:"), row, 0);
  118. layout->addWidget(sizeEdit_, row, 1);
  119. l->setBuddy(sizeEdit_);
  120. ++row;
  121. layout->addWidget(l = new WLabel("Created:"), row, 0);
  122. layout->addWidget(createdPicker_->lineEdit(), row, 1);
  123. layout->addWidget(createdPicker_, row, 2);
  124. l->setBuddy(createdPicker_->lineEdit());
  125. ++row;
  126. layout->addWidget(l = new WLabel("Modified:"), row, 0);
  127. layout->addWidget(modifiedPicker_->lineEdit(), row, 1);
  128. layout->addWidget(modifiedPicker_, row, 2);
  129. l->setBuddy(modifiedPicker_->lineEdit());
  130. ++row;
  131. WPushButton *b;
  132. WContainerWidget *buttons = new WContainerWidget();
  133. buttons->addWidget(b = new WPushButton("Save"));
  134. b->clicked().connect(this, &WDialog::accept);
  135. contents()->enterPressed().connect(this, &WDialog::accept);
  136. buttons->addWidget(b = new WPushButton("Cancel"));
  137. b->clicked().connect(this, &WDialog::reject);
  138. /*
  139. * Focus the form widget that corresonds to the selected item.
  140. */
  141. switch (item.column()) {
  142. case 2:
  143. typeEdit_->setFocus(); break;
  144. case 3:
  145. sizeEdit_->setFocus(); break;
  146. case 4:
  147. createdPicker_->lineEdit()->setFocus(); break;
  148. case 5:
  149. modifiedPicker_->lineEdit()->setFocus(); break;
  150. default:
  151. nameEdit_->setFocus(); break;
  152. }
  153. layout->addWidget(buttons, row, 0, 0, 3, AlignCenter);
  154. layout->setColumnStretch(1, 1);
  155. contents()->setLayout(layout);
  156. finished().connect(this, &FileEditDialog::handleFinish);
  157. show();
  158. }
  159. private:
  160. WAbstractItemModel *model_;
  161. WModelIndex item_;
  162. WLineEdit *nameEdit_, *sizeEdit_;
  163. WComboBox *typeEdit_;
  164. WDatePicker *createdPicker_, *modifiedPicker_;
  165. void handleFinish(DialogCode result)
  166. {
  167. if (result == WDialog::Accepted) {
  168. /*
  169. * Update the model with data from the edit widgets.
  170. *
  171. * You will want to do some validation here...
  172. *
  173. * Note that we directly update the source model to avoid
  174. * problems caused by the dynamic sorting of the proxy model,
  175. * which reorders row numbers, and would cause us to switch to editing
  176. * the wrong data.
  177. */
  178. WAbstractItemModel *m = model_;
  179. int modelRow = item_.row();
  180. WAbstractProxyModel *proxyModel = dynamic_cast<WAbstractProxyModel *>(m);
  181. if (proxyModel) {
  182. m = proxyModel->sourceModel();
  183. modelRow = proxyModel->mapToSource(item_).row();
  184. }
  185. m->setData(modelRow, 1, boost::any(nameEdit_->text()));
  186. m->setData(modelRow, 2, boost::any(typeEdit_->currentText()));
  187. m->setData(modelRow, 3, boost::any(boost::lexical_cast<int>
  188. (sizeEdit_->text().toUTF8())));
  189. m->setData(modelRow, 4, boost::any(createdPicker_->date()));
  190. m->setData(modelRow, 5, boost::any(modifiedPicker_->date()));
  191. }
  192. delete this;
  193. }
  194. };
  195. /*! \class TreeViewDragDrop
  196. * \brief Main application class.
  197. */
  198. class TreeViewDragDrop : public WApplication
  199. {
  200. public:
  201. /*! \brief Constructor.
  202. */
  203. TreeViewDragDrop(const WEnvironment &env)
  204. : WApplication(env),
  205. popup_(0),
  206. popupActionBox_(0)
  207. {
  208. setCssTheme("polished");
  209. /*
  210. * Create the data models.
  211. */
  212. folderModel_ = new WStandardItemModel(0, 1, this);
  213. populateFolders();
  214. fileModel_ = new FileModel(this);
  215. populateFiles();
  216. /*
  217. The header items are also endered using an ItemDelegate, and thus
  218. support other data, e.g.:
  219. fileModel_->setHeaderFlags(0, Horizontal, HeaderIsUserCheckable);
  220. fileModel_->setHeaderData(0, Horizontal,
  221. std::string("icons/file.gif"),
  222. Wt::DecorationRole);
  223. */
  224. fileFilterModel_ = new WSortFilterProxyModel(this);
  225. fileFilterModel_->setSourceModel(fileModel_);
  226. fileFilterModel_->setDynamicSortFilter(true);
  227. fileFilterModel_->setFilterKeyColumn(0);
  228. fileFilterModel_->setFilterRole(UserRole);
  229. /*
  230. * Setup the user interface.
  231. */
  232. createUI();
  233. }
  234. virtual ~TreeViewDragDrop() {
  235. delete popup_;
  236. delete popupActionBox_;
  237. }
  238. private:
  239. /// The folder model (used by folderView_)
  240. WStandardItemModel *folderModel_;
  241. /// The file model (used by fileView_)
  242. WStandardItemModel *fileModel_;
  243. /// The sort filter proxy model that adapts fileModel_
  244. WSortFilterProxyModel *fileFilterModel_;
  245. /// Maps folder id's to folder descriptions.
  246. std::map<std::string, WString> folderNameMap_;
  247. /// The folder view.
  248. WTreeView *folderView_;
  249. /// The file view.
  250. WTableView *fileView_;
  251. /// Popup menu on the folder view
  252. WPopupMenu *popup_;
  253. /// Message box to confirm the poup menu action
  254. WMessageBox *popupActionBox_;
  255. /*! \brief Setup the user interface.
  256. */
  257. void createUI() {
  258. WContainerWidget *w = root();
  259. w->setStyleClass("maindiv");
  260. /*
  261. * The main layout is a 3x2 grid layout.
  262. */
  263. WGridLayout *layout = new WGridLayout();
  264. layout->addWidget(createTitle("Folders"), 0, 0);
  265. layout->addWidget(createTitle("Files"), 0, 1);
  266. layout->addWidget(folderView(), 1, 0);
  267. layout->setColumnResizable(0);
  268. // select the first folder
  269. folderView_->select(folderModel_->index(0, 0, folderModel_->index(0, 0)));
  270. WVBoxLayout *vbox = new WVBoxLayout();
  271. vbox->addWidget(fileView(), 1);
  272. vbox->addWidget(pieChart(), 1);
  273. vbox->setResizable(0);
  274. layout->addLayout(vbox, 1, 1);
  275. layout->addWidget(aboutDisplay(), 2, 0, 1, 2);
  276. /*
  277. * Let row 1 and column 1 take the excess space.
  278. */
  279. layout->setRowStretch(1, 1);
  280. layout->setColumnStretch(1, 1);
  281. w->setLayout(layout);
  282. }
  283. /*! \brief Creates a title widget.
  284. */
  285. WText *createTitle(const WString& title) {
  286. WText *result = new WText(title);
  287. result->setInline(false);
  288. result->setStyleClass("title");
  289. return result;
  290. }
  291. /*! \brief Creates the folder WTreeView
  292. */
  293. WTreeView *folderView() {
  294. WTreeView *treeView = new FolderView();
  295. /*
  296. * To support right-click, we need to disable the built-in browser
  297. * context menu.
  298. *
  299. * Note that disabling the context menu and catching the
  300. * right-click does not work reliably on all browsers.
  301. */
  302. treeView->setAttributeValue
  303. ("oncontextmenu",
  304. "event.cancelBubble = true; event.returnValue = false; return false;");
  305. treeView->setModel(folderModel_);
  306. treeView->resize(200, WLength::Auto);
  307. treeView->setSelectionMode(SingleSelection);
  308. treeView->expandToDepth(1);
  309. treeView->selectionChanged()
  310. .connect(this, &TreeViewDragDrop::folderChanged);
  311. treeView->mouseWentUp().connect(this, &TreeViewDragDrop::showPopup);
  312. folderView_ = treeView;
  313. return treeView;
  314. }
  315. /*! \brief Creates the file table view (a WTableView)
  316. */
  317. WTableView *fileView() {
  318. WTableView *tableView = new WTableView();
  319. tableView->setAlternatingRowColors(true);
  320. tableView->setModel(fileFilterModel_);
  321. tableView->setSelectionMode(ExtendedSelection);
  322. tableView->setDragEnabled(true);
  323. tableView->setColumnWidth(0, 100);
  324. tableView->setColumnWidth(1, 150);
  325. tableView->setColumnWidth(2, 100);
  326. tableView->setColumnWidth(3, 60);
  327. tableView->setColumnWidth(4, 100);
  328. tableView->setColumnWidth(5, 100);
  329. WItemDelegate *delegate = new WItemDelegate(this);
  330. delegate->setTextFormat(FileModel::dateDisplayFormat);
  331. tableView->setItemDelegateForColumn(4, delegate);
  332. tableView->setItemDelegateForColumn(5, delegate);
  333. tableView->setColumnAlignment(3, AlignRight);
  334. tableView->setColumnAlignment(4, AlignRight);
  335. tableView->setColumnAlignment(5, AlignRight);
  336. tableView->sortByColumn(1, AscendingOrder);
  337. tableView->doubleClicked().connect(this, &TreeViewDragDrop::editFile);
  338. fileView_ = tableView;
  339. return tableView;
  340. }
  341. /*! \brief Edit a particular row.
  342. */
  343. void editFile(const WModelIndex& item) {
  344. new FileEditDialog(fileView_->model(), item);
  345. }
  346. /*! \brief Creates the chart.
  347. */
  348. WWidget *pieChart() {
  349. using namespace Chart;
  350. WPieChart *chart = new WPieChart();
  351. // chart->setPreferredMethod(WPaintedWidget::PngImage);
  352. chart->setModel(fileFilterModel_);
  353. chart->setTitle("File sizes");
  354. chart->setLabelsColumn(1); // Name
  355. chart->setDataColumn(3); // Size
  356. chart->setPerspectiveEnabled(true, 0.2);
  357. chart->setDisplayLabels(Outside | TextLabel);
  358. if (!WApplication::instance()->environment().ajax()) {
  359. chart->resize(500, 200);
  360. chart->setMargin(WLength::Auto, Left | Right);
  361. WContainerWidget *w = new WContainerWidget();
  362. w->addWidget(chart);
  363. w->setStyleClass("about");
  364. return w;
  365. } else {
  366. chart->setStyleClass("about");
  367. return chart;
  368. }
  369. }
  370. /*! \brief Creates the hints text.
  371. */
  372. WWidget *aboutDisplay() {
  373. WText *result = new WText(WString::tr("about-text"));
  374. result->setStyleClass("about");
  375. return result;
  376. }
  377. /*! \brief Change the filter on the file view when the selected folder
  378. * changes.
  379. */
  380. void folderChanged() {
  381. if (folderView_->selectedIndexes().empty())
  382. return;
  383. WModelIndex selected = *folderView_->selectedIndexes().begin();
  384. boost::any d = selected.data(UserRole);
  385. if (!d.empty()) {
  386. std::string folder = boost::any_cast<std::string>(d);
  387. // For simplicity, we assume here that the folder-id does not
  388. // contain special regexp characters, otherwise these need to be
  389. // escaped -- or use the \Q \E qutoing escape regular expression
  390. // syntax (and escape \E)
  391. fileFilterModel_->setFilterRegExp(folder);
  392. }
  393. }
  394. /*! \brief Show a popup for a folder item.
  395. */
  396. void showPopup(const WModelIndex& item, const WMouseEvent& event) {
  397. if (event.button() == WMouseEvent::RightButton) {
  398. // Select the item, it was not yet selected.
  399. if (!folderView_->isSelected(item))
  400. folderView_->select(item);
  401. if (!popup_) {
  402. popup_ = new WPopupMenu();
  403. popup_->addItem("icons/folder_new.gif", "Create a New Folder");
  404. popup_->addItem("Rename this Folder")->setCheckable(true);
  405. popup_->addItem("Delete this Folder");
  406. popup_->addSeparator();
  407. popup_->addItem("Folder Details");
  408. popup_->addSeparator();
  409. popup_->addItem("Application Inventory");
  410. popup_->addItem("Hardware Inventory");
  411. popup_->addSeparator();
  412. WPopupMenu *subMenu = new WPopupMenu();
  413. subMenu->addItem("Sub Item 1");
  414. subMenu->addItem("Sub Item 2");
  415. popup_->addMenu("File Deployments", subMenu);
  416. /*
  417. * This is one method of executing a popup, which does not block a
  418. * thread for a reentrant event loop, and thus scales.
  419. *
  420. * Alternatively you could call WPopupMenu::exec(), which returns
  421. * the result, but while waiting for it, blocks the thread.
  422. */
  423. popup_->aboutToHide().connect(this, &TreeViewDragDrop::popupAction);
  424. }
  425. if (popup_->isHidden())
  426. popup_->popup(event);
  427. else
  428. popup_->hide();
  429. }
  430. }
  431. /** \brief Process the result of the popup menu
  432. */
  433. void popupAction() {
  434. if (popup_->result()) {
  435. /*
  436. * You could also bind extra data to an item using setData() and
  437. * check here for the action asked. For now, we just use the text.
  438. */
  439. WString text = popup_->result()->text();
  440. popup_->hide();
  441. popupActionBox_ = new WMessageBox("Sorry.","Action '" + text
  442. + "' is not implemented.", NoIcon, Ok);
  443. popupActionBox_->buttonClicked()
  444. .connect(this, &TreeViewDragDrop::dialogDone);
  445. popupActionBox_->show();
  446. } else {
  447. popup_->hide();
  448. }
  449. }
  450. /** \brief Process the result of the message box.
  451. */
  452. void dialogDone() {
  453. delete popupActionBox_;
  454. popupActionBox_ = 0;
  455. }
  456. /*! \brief Populate the files model.
  457. *
  458. * Data (and headers) is read from the CSV file data/files.csv. We
  459. * add icons to the first column, resolve the folder id to the
  460. * actual folder name, and configure item flags, and parse date
  461. * values.
  462. */
  463. void populateFiles() {
  464. fileModel_->invisibleRootItem()->setRowCount(0);
  465. std::ifstream f((appRoot() + "data/files.csv").c_str());
  466. if (!f)
  467. throw std::runtime_error("Could not read: data/files.csv");
  468. readFromCsv(f, fileModel_);
  469. for (int i = 0; i < fileModel_->rowCount(); ++i) {
  470. WStandardItem *item = fileModel_->item(i, 0);
  471. item->setFlags(item->flags() | ItemIsDragEnabled);
  472. item->setIcon("icons/file.gif");
  473. std::string folderId = item->text().toUTF8();
  474. item->setData(boost::any(folderId), UserRole);
  475. item->setText(folderNameMap_[folderId]);
  476. convertToNumber(fileModel_->item(i, 3));
  477. convertToDate(fileModel_->item(i, 4));
  478. convertToDate(fileModel_->item(i, 5));
  479. }
  480. }
  481. /*! \brief Convert a string to a date.
  482. */
  483. void convertToDate(WStandardItem *item) {
  484. WDate d = WDate::fromString(item->text(), FileModel::dateEditFormat);
  485. item->setData(boost::any(d), DisplayRole);
  486. }
  487. /*! \brief Convert a string to a number.
  488. */
  489. void convertToNumber(WStandardItem *item) {
  490. int i = boost::lexical_cast<int>(item->text());
  491. item->setData(boost::any(i), EditRole);
  492. }
  493. /*! \brief Populate the folders model.
  494. */
  495. void populateFolders() {
  496. WStandardItem *level1, *level2;
  497. folderModel_->appendRow(level1 = createFolderItem("San Fransisco"));
  498. level1->appendRow(level2 = createFolderItem("Investors", "sf-investors"));
  499. level1->appendRow(level2 = createFolderItem("Fellows", "sf-fellows"));
  500. folderModel_->appendRow(level1 = createFolderItem("Sophia Antipolis"));
  501. level1->appendRow(level2 = createFolderItem("R&D", "sa-r_d"));
  502. level1->appendRow(level2 = createFolderItem("Services", "sa-services"));
  503. level1->appendRow(level2 = createFolderItem("Support", "sa-support"));
  504. level1->appendRow(level2 = createFolderItem("Billing", "sa-billing"));
  505. folderModel_->appendRow(level1 = createFolderItem("New York"));
  506. level1->appendRow(level2 = createFolderItem("Marketing", "ny-marketing"));
  507. level1->appendRow(level2 = createFolderItem("Sales", "ny-sales"));
  508. level1->appendRow(level2 = createFolderItem("Advisors", "ny-advisors"));
  509. folderModel_->appendRow(level1 = createFolderItem
  510. (WString::fromUTF8("Frankfürt")));
  511. level1->appendRow(level2 = createFolderItem("Sales", "frank-sales"));
  512. folderModel_->setHeaderData(0, Horizontal,
  513. boost::any(std::string("SandBox")));
  514. }
  515. /*! \brief Create a folder item.
  516. *
  517. * Configures flags for drag and drop support.
  518. */
  519. WStandardItem *createFolderItem(const WString& location,
  520. const std::string& folderId = std::string())
  521. {
  522. WStandardItem *result = new WStandardItem(location);
  523. if (!folderId.empty()) {
  524. result->setData(boost::any(folderId));
  525. result->setFlags(result->flags() | ItemIsDropEnabled);
  526. folderNameMap_[folderId] = location;
  527. } else
  528. result->setFlags(result->flags().clear(ItemIsSelectable));
  529. result->setIcon("icons/folder.gif");
  530. return result;
  531. }
  532. };
  533. WApplication *createApplication(const WEnvironment& env)
  534. {
  535. WApplication *app = new TreeViewDragDrop(env);
  536. app->setTwoPhaseRenderingThreshold(0);
  537. app->setTitle("WTreeView Drag & Drop");
  538. app->useStyleSheet("styles.css");
  539. app->messageResourceBundle().use(WApplication::appRoot() + "about");
  540. app->refresh();
  541. return app;
  542. }
  543. int main(int argc, char **argv)
  544. {
  545. return WRun(argc, argv, &createApplication);
  546. }
  547. /*@}*/