/* * Copyright (C) 2008 Emweb bvba, Kessel-Lo, Belgium. * * See the LICENSE file for terms of use. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "CsvUtil.h" #include "FolderView.h" using namespace Wt; /** * \defgroup treeviewdragdrop Drag and drop in WTreeView example */ /*@{*/ /*! \class FileModel * \brief A specialized standard item model which report a specific * drag and drop mime type. * * A specific drag and drop mime type instead of the generic abstract * item model is returned by the model. */ class FileModel : public WStandardItemModel { public: /*! \brief Constructor. */ FileModel(WObject *parent) : WStandardItemModel(parent) { } /*! \brief Return the mime type. */ virtual std::string mimeType() const { return FolderView::FileSelectionMimeType; } /// Date display format. static WString dateDisplayFormat; /// Date edit format. static WString dateEditFormat; }; WString FileModel::dateDisplayFormat(WString::fromUTF8("MMM dd, yyyy")); WString FileModel::dateEditFormat(WString::fromUTF8("dd-MM-yyyy")); /*! \class FileEditDialog * \brief A dialog for editing a 'file'. */ class FileEditDialog : public WDialog { public: FileEditDialog(WAbstractItemModel *model, const WModelIndex& item) : WDialog("Edit..."), model_(model), item_(item) { int modelRow = item_.row(); resize(300, WLength::Auto); /* * Create the form widgets, and load them with data from the model. */ // name nameEdit_ = new WLineEdit(asString(model_->data(modelRow, 1))); // type typeEdit_ = new WComboBox(); typeEdit_->addItem("Document"); typeEdit_->addItem("Spreadsheet"); typeEdit_->addItem("Presentation"); typeEdit_->setCurrentIndex (typeEdit_->findText(asString(model_->data(modelRow, 2)))); // size sizeEdit_ = new WLineEdit(asString(model_->data(modelRow, 3))); sizeEdit_->setValidator (new WIntValidator(0, std::numeric_limits::max(), this)); // created createdPicker_ = new WDatePicker(); createdPicker_->lineEdit()->validator()->setMandatory(true); createdPicker_->setFormat(FileModel::dateEditFormat); createdPicker_->setDate(boost::any_cast(model_->data(modelRow, 4))); // modified modifiedPicker_ = new WDatePicker(); modifiedPicker_->lineEdit()->validator()->setMandatory(true); modifiedPicker_->setFormat(FileModel::dateEditFormat); modifiedPicker_->setDate(boost::any_cast(model_->data(modelRow, 5))); /* * Use a grid layout for the labels and fields */ WGridLayout *layout = new WGridLayout(); WLabel *l; int row = 0; layout->addWidget(l = new WLabel("Name:"), row, 0); layout->addWidget(nameEdit_, row, 1); l->setBuddy(nameEdit_); ++row; layout->addWidget(l = new WLabel("Type:"), row, 0); layout->addWidget(typeEdit_, row, 1); l->setBuddy(typeEdit_); ++row; layout->addWidget(l = new WLabel("Size:"), row, 0); layout->addWidget(sizeEdit_, row, 1); l->setBuddy(sizeEdit_); ++row; layout->addWidget(l = new WLabel("Created:"), row, 0); layout->addWidget(createdPicker_->lineEdit(), row, 1); layout->addWidget(createdPicker_, row, 2); l->setBuddy(createdPicker_->lineEdit()); ++row; layout->addWidget(l = new WLabel("Modified:"), row, 0); layout->addWidget(modifiedPicker_->lineEdit(), row, 1); layout->addWidget(modifiedPicker_, row, 2); l->setBuddy(modifiedPicker_->lineEdit()); ++row; WPushButton *b; WContainerWidget *buttons = new WContainerWidget(); buttons->addWidget(b = new WPushButton("Save")); b->clicked().connect(this, &WDialog::accept); contents()->enterPressed().connect(this, &WDialog::accept); buttons->addWidget(b = new WPushButton("Cancel")); b->clicked().connect(this, &WDialog::reject); /* * Focus the form widget that corresonds to the selected item. */ switch (item.column()) { case 2: typeEdit_->setFocus(); break; case 3: sizeEdit_->setFocus(); break; case 4: createdPicker_->lineEdit()->setFocus(); break; case 5: modifiedPicker_->lineEdit()->setFocus(); break; default: nameEdit_->setFocus(); break; } layout->addWidget(buttons, row, 0, 0, 3, AlignCenter); layout->setColumnStretch(1, 1); contents()->setLayout(layout); finished().connect(this, &FileEditDialog::handleFinish); show(); } private: WAbstractItemModel *model_; WModelIndex item_; WLineEdit *nameEdit_, *sizeEdit_; WComboBox *typeEdit_; WDatePicker *createdPicker_, *modifiedPicker_; void handleFinish(DialogCode result) { if (result == WDialog::Accepted) { /* * Update the model with data from the edit widgets. * * You will want to do some validation here... * * Note that we directly update the source model to avoid * problems caused by the dynamic sorting of the proxy model, * which reorders row numbers, and would cause us to switch to editing * the wrong data. */ WAbstractItemModel *m = model_; int modelRow = item_.row(); WAbstractProxyModel *proxyModel = dynamic_cast(m); if (proxyModel) { m = proxyModel->sourceModel(); modelRow = proxyModel->mapToSource(item_).row(); } m->setData(modelRow, 1, boost::any(nameEdit_->text())); m->setData(modelRow, 2, boost::any(typeEdit_->currentText())); m->setData(modelRow, 3, boost::any(boost::lexical_cast (sizeEdit_->text().toUTF8()))); m->setData(modelRow, 4, boost::any(createdPicker_->date())); m->setData(modelRow, 5, boost::any(modifiedPicker_->date())); } delete this; } }; /*! \class TreeViewDragDrop * \brief Main application class. */ class TreeViewDragDrop : public WApplication { public: /*! \brief Constructor. */ TreeViewDragDrop(const WEnvironment &env) : WApplication(env), popup_(0), popupActionBox_(0) { setCssTheme("polished"); /* * Create the data models. */ folderModel_ = new WStandardItemModel(0, 1, this); populateFolders(); fileModel_ = new FileModel(this); populateFiles(); /* The header items are also endered using an ItemDelegate, and thus support other data, e.g.: fileModel_->setHeaderFlags(0, Horizontal, HeaderIsUserCheckable); fileModel_->setHeaderData(0, Horizontal, std::string("icons/file.gif"), Wt::DecorationRole); */ fileFilterModel_ = new WSortFilterProxyModel(this); fileFilterModel_->setSourceModel(fileModel_); fileFilterModel_->setDynamicSortFilter(true); fileFilterModel_->setFilterKeyColumn(0); fileFilterModel_->setFilterRole(UserRole); /* * Setup the user interface. */ createUI(); if (!popup_) { popup_ = new WPopupMenu(); popup_->addItem("icons/folder_new.gif", "Create a New Folder"); popup_->addItem("Rename this Folder")->setCheckable(true); popup_->addItem("Delete this Folder"); popup_->addSeparator(); popup_->addItem("Folder Details"); popup_->addSeparator(); popup_->addItem("Application Inventory"); popup_->addItem("Hardware Inventory"); popup_->addSeparator(); WPopupMenu *subMenu = new WPopupMenu(); subMenu->addItem("Sub Item 1"); subMenu->addItem("Sub Item 2"); popup_->addMenu("File Deployments", subMenu); /* * This is one method of executing a popup, which does not block a * thread for a reentrant event loop, and thus scales. * * Alternatively you could call WPopupMenu::exec(), which returns * the result, but while waiting for it, blocks the thread. */ popup_->aboutToHide().connect(this, &TreeViewDragDrop::popupAction); } } virtual ~TreeViewDragDrop() { delete popup_; delete popupActionBox_; } private: /// The folder model (used by folderView_) WStandardItemModel *folderModel_; /// The file model (used by fileView_) WStandardItemModel *fileModel_; /// The sort filter proxy model that adapts fileModel_ WSortFilterProxyModel *fileFilterModel_; /// Maps folder id's to folder descriptions. std::map folderNameMap_; /// The folder view. WTreeView *folderView_; /// The file view. WTableView *fileView_; /// Popup menu on the folder view WPopupMenu *popup_; /// Message box to confirm the poup menu action WMessageBox *popupActionBox_; /*! \brief Setup the user interface. */ void createUI() { WContainerWidget *w = root(); w->setStyleClass("maindiv"); /* * The main layout is a 3x2 grid layout. */ WGridLayout *layout = new WGridLayout(); layout->addWidget(createTitle("Folders"), 0, 0); layout->addWidget(createTitle("Files"), 0, 1); layout->addWidget(folderView(), 1, 0); layout->setColumnResizable(0); // select the first folder folderView_->select(folderModel_->index(0, 0, folderModel_->index(0, 0))); WVBoxLayout *vbox = new WVBoxLayout(); vbox->addWidget(fileView(), 1); vbox->addWidget(pieChart(), 1); vbox->setResizable(0); layout->addLayout(vbox, 1, 1); layout->addWidget(aboutDisplay(), 2, 0, 1, 2); /* * Let row 1 and column 1 take the excess space. */ layout->setRowStretch(1, 1); layout->setColumnStretch(1, 1); w->setLayout(layout); } /*! \brief Creates a title widget. */ WText *createTitle(const WString& title) { WText *result = new WText(title); result->setInline(false); result->setStyleClass("title"); return result; } /*! \brief Creates the folder WTreeView */ WTreeView *folderView() { WTreeView *treeView = new FolderView(); /* * To support right-click, we need to disable the built-in browser * context menu. * * Note that disabling the context menu and catching the * right-click does not work reliably on all browsers. */ treeView->setAttributeValue ("oncontextmenu", "event.cancelBubble = true; event.returnValue = false; return false;"); treeView->setModel(folderModel_); treeView->resize(200, WLength::Auto); treeView->setSelectionMode(SingleSelection); treeView->expandToDepth(1); treeView->selectionChanged() .connect(this, &TreeViewDragDrop::folderChanged); treeView->mouseWentUp().connect(this, &TreeViewDragDrop::showPopup); folderView_ = treeView; return treeView; } /*! \brief Creates the file table view (a WTableView) */ WTableView *fileView() { WTableView *tableView = new WTableView(); tableView->setAlternatingRowColors(true); tableView->setModel(fileFilterModel_); tableView->setSelectionMode(ExtendedSelection); tableView->setDragEnabled(true); tableView->setColumnWidth(0, 100); tableView->setColumnWidth(1, 150); tableView->setColumnWidth(2, 100); tableView->setColumnWidth(3, 60); tableView->setColumnWidth(4, 100); tableView->setColumnWidth(5, 100); WItemDelegate *delegate = new WItemDelegate(this); delegate->setTextFormat(FileModel::dateDisplayFormat); tableView->setItemDelegateForColumn(4, delegate); tableView->setItemDelegateForColumn(5, delegate); tableView->setColumnAlignment(3, AlignRight); tableView->setColumnAlignment(4, AlignRight); tableView->setColumnAlignment(5, AlignRight); tableView->sortByColumn(1, AscendingOrder); tableView->doubleClicked().connect(this, &TreeViewDragDrop::editFile); fileView_ = tableView; return tableView; } /*! \brief Edit a particular row. */ void editFile(const WModelIndex& item) { new FileEditDialog(fileView_->model(), item); } /*! \brief Creates the chart. */ WWidget *pieChart() { using namespace Chart; WPieChart *chart = new WPieChart(); // chart->setPreferredMethod(WPaintedWidget::PngImage); chart->setModel(fileFilterModel_); chart->setTitle("File sizes"); chart->setLabelsColumn(1); // Name chart->setDataColumn(3); // Size chart->setPerspectiveEnabled(true, 0.2); chart->setDisplayLabels(Outside | TextLabel); if (!WApplication::instance()->environment().ajax()) { chart->resize(500, 200); chart->setMargin(WLength::Auto, Left | Right); WContainerWidget *w = new WContainerWidget(); w->addWidget(chart); w->setStyleClass("about"); return w; } else { chart->setStyleClass("about"); return chart; } } /*! \brief Creates the hints text. */ WWidget *aboutDisplay() { WText *result = new WText(WString::tr("about-text")); result->setStyleClass("about"); return result; } /*! \brief Change the filter on the file view when the selected folder * changes. */ void folderChanged() { if (folderView_->selectedIndexes().empty()) return; WModelIndex selected = *folderView_->selectedIndexes().begin(); boost::any d = selected.data(UserRole); if (!d.empty()) { std::string folder = boost::any_cast(d); // For simplicity, we assume here that the folder-id does not // contain special regexp characters, otherwise these need to be // escaped -- or use the \Q \E qutoing escape regular expression // syntax (and escape \E) fileFilterModel_->setFilterRegExp(folder); } } /*! \brief Show a popup for a folder item. */ void showPopup(const WModelIndex& item, const WMouseEvent& event) { if (event.button() == WMouseEvent::RightButton) { // Select the item, it was not yet selected. if (!folderView_->isSelected(item)) folderView_->select(item); if (popup_->isHidden()) popup_->popup(event); else popup_->hide(); } } /** \brief Process the result of the popup menu */ void popupAction() { if (popup_->result()) { /* * You could also bind extra data to an item using setData() and * check here for the action asked. For now, we just use the text. */ WString text = popup_->result()->text(); popup_->hide(); popupActionBox_ = new WMessageBox("Sorry.","Action '" + text + "' is not implemented.", NoIcon, Ok); popupActionBox_->buttonClicked() .connect(this, &TreeViewDragDrop::dialogDone); popupActionBox_->show(); } else { popup_->hide(); } } /** \brief Process the result of the message box. */ void dialogDone() { delete popupActionBox_; popupActionBox_ = 0; } /*! \brief Populate the files model. * * Data (and headers) is read from the CSV file data/files.csv. We * add icons to the first column, resolve the folder id to the * actual folder name, and configure item flags, and parse date * values. */ void populateFiles() { fileModel_->invisibleRootItem()->setRowCount(0); std::ifstream f((appRoot() + "data/files.csv").c_str()); if (!f) throw std::runtime_error("Could not read: data/files.csv"); readFromCsv(f, fileModel_); for (int i = 0; i < fileModel_->rowCount(); ++i) { WStandardItem *item = fileModel_->item(i, 0); item->setFlags(item->flags() | ItemIsDragEnabled); item->setIcon("icons/file.gif"); std::string folderId = item->text().toUTF8(); item->setData(boost::any(folderId), UserRole); item->setText(folderNameMap_[folderId]); convertToDate(fileModel_->item(i, 4)); convertToDate(fileModel_->item(i, 5)); } } /*! \brief Convert a string to a date. */ void convertToDate(WStandardItem *item) { WDate d = WDate::fromString(item->text(), FileModel::dateEditFormat); item->setData(boost::any(d), DisplayRole); } /*! \brief Populate the folders model. */ void populateFolders() { WStandardItem *level1, *level2; folderModel_->appendRow(level1 = createFolderItem("San Fransisco")); level1->appendRow(level2 = createFolderItem("Investors", "sf-investors")); level1->appendRow(level2 = createFolderItem("Fellows", "sf-fellows")); folderModel_->appendRow(level1 = createFolderItem("Sophia Antipolis")); level1->appendRow(level2 = createFolderItem("R&D", "sa-r_d")); level1->appendRow(level2 = createFolderItem("Services", "sa-services")); level1->appendRow(level2 = createFolderItem("Support", "sa-support")); level1->appendRow(level2 = createFolderItem("Billing", "sa-billing")); folderModel_->appendRow(level1 = createFolderItem("New York")); level1->appendRow(level2 = createFolderItem("Marketing", "ny-marketing")); level1->appendRow(level2 = createFolderItem("Sales", "ny-sales")); level1->appendRow(level2 = createFolderItem("Advisors", "ny-advisors")); folderModel_->appendRow(level1 = createFolderItem (WString::fromUTF8("Frankfürt"))); level1->appendRow(level2 = createFolderItem("Sales", "frank-sales")); folderModel_->setHeaderData(0, Horizontal, boost::any(std::string("SandBox"))); } /*! \brief Create a folder item. * * Configures flags for drag and drop support. */ WStandardItem *createFolderItem(const WString& location, const std::string& folderId = std::string()) { WStandardItem *result = new WStandardItem(location); if (!folderId.empty()) { result->setData(boost::any(folderId)); result->setFlags(result->flags() | ItemIsDropEnabled); folderNameMap_[folderId] = location; } else result->setFlags(result->flags().clear(ItemIsSelectable)); result->setIcon("icons/folder.gif"); return result; } }; WApplication *createApplication(const WEnvironment& env) { WApplication *app = new TreeViewDragDrop(env); app->setTwoPhaseRenderingThreshold(0); app->setTitle("WTreeView Drag & Drop"); app->useStyleSheet("styles.css"); app->messageResourceBundle().use(WApplication::appRoot() + "about"); app->refresh(); return app; } int main(int argc, char **argv) { return WRun(argc, argv, &createApplication); } /*@}*/