Skip to content

Commit

Permalink
Recommend directories
Browse files Browse the repository at this point in the history
Fix state on new project
  • Loading branch information
JohnDTill committed Sep 19, 2023
1 parent 8c680b7 commit a46b4c5
Show file tree
Hide file tree
Showing 12 changed files with 149 additions and 46 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ src/generated/*
*.dll
**/config/lambda.ico
**/com.automath.forscape/meta/LICENSE
**/in/hello_world_import_abs_path.π

#Qt
*.pro.user*
Expand Down
12 changes: 8 additions & 4 deletions app/mainwindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -457,12 +457,15 @@ void MainWindow::onKeyboardNew() {
dialog.setWindowTitle("Creation Prompt");
dialog.setLabelText(tr("Create new..."));
dialog.setComboBoxItems({
"Project",
"File",
"Project (" + ui->actionNew_Project->shortcut().toString() + ")",
"File (" + ui->actionNew->shortcut().toString() + ")",
});

const QComboBox* const combo_box = dialog.findChild<QComboBox*>();
assert( combo_box != nullptr );

if(dialog.exec() != QDialog::Accepted) return;
switch( dialog.findChild<QComboBox*>()->currentIndex() ){
switch( combo_box->currentIndex() ){
case 0: on_actionNew_Project_triggered(); break;
case 1: on_actionNew_triggered(); break;
default: assert(false);
Expand All @@ -481,6 +484,7 @@ void MainWindow::on_actionNew_Project_triggered() {

ui->actionSave->setEnabled(false);
ui->actionSave_All->setEnabled(false);
ui->actionReload->setEnabled(false);

console->clearModel();

Expand Down Expand Up @@ -512,7 +516,7 @@ void MainWindow::on_actionNew_triggered(){
Program::instance()->program_entry_point = model;
setWindowTitle(NEW_SCRIPT_TITLE WINDOW_TITLE_SUFFIX);
active_file_path.clear();
project_browser->addFile(model);
project_browser->addUnsavedFile(model);

viewModel(model, 0);
}
Expand Down
109 changes: 84 additions & 25 deletions app/projectbrowser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,28 @@
#include <qt_compatability.h>

#include <QAction>
#include <QClipboard>
#include <QDir>
#include <QFileIconProvider>
#include <QGuiApplication>
#include <QInputDialog>
#include <QMenu>
#include <QMessageBox>
#include <QProcess>

//PROJECT BROWSER UPDATE: the project view is unintuitive. Allow multiple open projects?
//PROJECT BROWSER UPDATE: switch to QTreeView/QFileSystemModel, or continue using QTreeWidget w/ std::filesystem?

//PROJECT BROWSER UPDATE: a program should not be singleton
//PROJECT BROWSER UPDATE: need assurance that the viewing history remains valid as files are deleted, renamed, w/e
//PROJECT BROWSER UPDATE: the project browser should probably have an undo stack
//PROJECT BROWSER UPDATE: add copy path option
//PROJECT BROWSER UPDATE: import from absolute path is broken (should probably warn)
//PROJECT BROWSER UPDATE: import from folder up is broken
//PROJECT BROWSER UPDATE: finish implementing interaction actions
//PROJECT BROWSER UPDATE: show/hide unused files which are in a directory, but not imported anywhere
//PROJECT BROWSER UPDATE: fix save-as to rename files, projects
//PROJECT BROWSER UPDATE: allow multiple open projects, with one active project, and ability to close
//PROJECT BROWSER UPDATE: Parse file-level descriptions for use in tooltips, with full paths in tooltips
//PROJECT BROWSER UPDATE: scan for external changes
//PROJECT BROWSER UPDATE: limit external dependence on the project browser
//PROJECT BROWSER UPDATE: should support drag and drop

Q_DECLARE_METATYPE(Forscape::Typeset::Model*); //EVENTUALLY: this is only for compability with old versions

Expand Down Expand Up @@ -53,6 +62,7 @@ void ProjectBrowser::FileEntry::setModel(Forscape::Typeset::Model& m) noexcept {
model = &m;
path = m.path;
setText(0, isSavedToDisk() ? toQString(path.filename()) : "untitled");
if(isSavedToDisk()) setToolTip(0, toQString(path));
}

bool ProjectBrowser::FileEntry::isSavedToDisk() const noexcept {
Expand Down Expand Up @@ -91,6 +101,7 @@ class ProjectBrowser::DirectoryEntry : public QTreeWidgetItem {
void setPath(const std::filesystem::path& p) {
path = p;
setText(0, toQString(p.filename()));
setToolTip(0, toQString(path));
}

const std::filesystem::path& getPath() const noexcept {
Expand All @@ -103,7 +114,6 @@ ProjectBrowser::ProjectBrowser(QWidget* parent, MainWindow* main_window)
setHeaderHidden(true);
setIndentation(10);
setMinimumWidth(120);
//PROJECT BROWSER UPDATE: should only set this if there are no folders in the project
setRootIsDecorated(false); //Hide a universal root entry, so the user has the illusion of multiple "top-level" entries
setContextMenuPolicy(Qt::CustomContextMenu); //Needed to enable context menu

Expand All @@ -122,9 +132,10 @@ void ProjectBrowser::setProject(Forscape::Typeset::Model* model) {
FileEntry* main_file = new FileEntry(root);
main_file->setIcon(0, main_icon);
main_file->setModel(*model);
entries[std_path] = main_file;
files_in_filesystem[std_path] = main_file;
files_in_memory[model] = main_file;
const std::filesystem::path parent_path = std_path.parent_path();
entries[parent_path] = root;
files_in_filesystem[parent_path] = root;
root->setData(0, Qt::UserRole, toQString(parent_path));
currently_viewed_file = main_file;
QFont normal_font = font();
Expand All @@ -133,8 +144,9 @@ void ProjectBrowser::setProject(Forscape::Typeset::Model* model) {
currently_viewed_file->setFont(0, bold_font);
}

void ProjectBrowser::addFile(Forscape::Typeset::Model* model) {
void ProjectBrowser::addUnsavedFile(Forscape::Typeset::Model* model) {
FileEntry* item = new FileEntry(invisibleRootItem());
files_in_memory[model] = item;
item->setText(0, "untitled");
item->setIcon(0, file_icon);
item->model = model;
Expand All @@ -149,15 +161,15 @@ void ProjectBrowser::saveModel(Forscape::Typeset::Model* saved_model, const std:
if(rename_file){
//EVENTUALLY: this is a hacky solution to keep the old file, which is likely referenced in code
Forscape::Program::instance()->source_files.erase(old_path);
entries.erase(old_path);
files_in_filesystem.erase(old_path);
Forscape::Program::instance()->openFromAbsolutePath(old_path);
}

if(create_new_file || rename_file){
saved_model->path = std_path;
FileEntry* item = debug_cast<FileEntry*>(entries[std_path]);
FileEntry* item = debug_cast<FileEntry*>(files_in_filesystem[std_path]);
item->setModel(*saved_model);
auto result = entries.insert({std_path, item});
auto result = files_in_filesystem.insert({std_path, item});
if(!result.second){
//Saving over existing project file
QTreeWidgetItem* overwritten = result.first->second;
Expand Down Expand Up @@ -192,11 +204,12 @@ void ProjectBrowser::updateProjectBrowser() {

void ProjectBrowser::addProjectEntry(Forscape::Typeset::Model* model) {
std::filesystem::path path = model->path;
assert(entries.find(path) == entries.end());
assert(files_in_filesystem.find(path) == files_in_filesystem.end());

FileEntry* tree_item = new FileEntry();
tree_item->setModel(*model);
entries[path] = tree_item;
files_in_filesystem[path] = tree_item;
files_in_memory[model] = tree_item;
model->write_time = std::filesystem::file_time_type::clock::now();

linkFileToAncestor(tree_item, path);
Expand Down Expand Up @@ -225,16 +238,19 @@ void ProjectBrowser::setCurrentlyViewed(Forscape::Typeset::Model* model) {
if(currently_viewed_file) currently_viewed_file->setFont(0, QFont());

//Highlight the new item
const auto lookup = entries.find(model->path);
if(lookup == entries.end()) return;
const auto lookup = files_in_memory.find(model);
assert(lookup != files_in_memory.end());
currently_viewed_file = debug_cast<FileEntry*>(lookup->second);
currently_viewed_file->setFont(0, bold_font);
setCurrentItem(currently_viewed_file);
}

void ProjectBrowser::clear() noexcept {
QTreeWidget::clear();
entries.clear();
files_in_filesystem.clear();
files_in_memory.clear();

setRootIsDecorated(false); //Hide a universal root entry, so the user has the illusion of multiple "top-level" entries
}

void ProjectBrowser::onFileClicked(QTreeWidgetItem* item, int column) {
Expand Down Expand Up @@ -266,7 +282,8 @@ void ProjectBrowser::onShowInExplorer() {
void ProjectBrowser::onDeleteFile() {
FileEntry& item = getSelectedFileEntry();
assert(item.model != Forscape::Program::instance()->program_entry_point);
entries.erase(item.path);
files_in_filesystem.erase(item.path);
files_in_memory.erase(item.model);
main_window->removeFile(item.model);
item.deleteFile();

Expand Down Expand Up @@ -311,6 +328,17 @@ void ProjectBrowser::onRightClick(const QPoint& pos) {
QAction* rename_file = menu.addAction(tr("Rename"));
rename_file->setToolTip(tr("Change the filename"));
connect(rename_file, SIGNAL(triggered(bool)), this, SLOT(onFileRenamed()));

menu.addSeparator();
QAction* copy_name = menu.addAction(tr("Copy file name"));
copy_name->setToolTip(tr("Copy the file name to the clipboard"));
connect(copy_name, SIGNAL(triggered(bool)), this, SLOT(onCopyFileName()));

if( file_item->isSavedToDisk() ){
QAction* copy_full_path = menu.addAction(tr("Copy full path"));
copy_full_path->setToolTip(tr("Copy the full path to the clipboard"));
connect(copy_full_path, SIGNAL(triggered(bool)), this, SLOT(onCopyFilePath()));
}
}else{
QAction* show_in_explorer = menu.addAction(tr("Show in Explorer"));
show_in_explorer->setToolTip(tr("Show in the OS file browser"));
Expand All @@ -322,6 +350,15 @@ void ProjectBrowser::onRightClick(const QPoint& pos) {

QAction* expand = menu.addAction(item->isExpanded() ? tr("Collapse") : tr("Expand"));
connect(expand, SIGNAL(triggered(bool)), this, SLOT(expandDirectory()));

menu.addSeparator();
QAction* copy_name = menu.addAction(tr("Copy directory name"));
copy_name->setToolTip(tr("Copy the directory name to the clipboard"));
connect(copy_name, SIGNAL(triggered(bool)), this, SLOT(onCopyDirName()));

QAction* copy_full_path = menu.addAction(tr("Copy full path"));
copy_full_path->setToolTip(tr("Copy the full path to the clipboard"));
connect(copy_full_path, SIGNAL(triggered(bool)), this, SLOT(onCopyDirPath()));
}

menu.exec(mapToGlobal(pos));
Expand Down Expand Up @@ -392,6 +429,27 @@ void ProjectBrowser::expandDirectory() {
currentItem()->setExpanded(!currentItem()->isExpanded());
}

void ProjectBrowser::onCopyFilePath() {
const FileEntry& entry = getSelectedFileEntry();
if(!entry.isSavedToDisk()) return;
QGuiApplication::clipboard()->setText(toQString(entry.getPath()));
}

void ProjectBrowser::onCopyFileName() {
const FileEntry& entry = getSelectedFileEntry();
QGuiApplication::clipboard()->setText(entry.text(0));
}

void ProjectBrowser::onCopyDirPath() {
const DirectoryEntry& entry = getSelectedDirectory();
QGuiApplication::clipboard()->setText(toQString(entry.getPath()));
}

void ProjectBrowser::onCopyDirName() {
const DirectoryEntry& entry = getSelectedDirectory();
QGuiApplication::clipboard()->setText(entry.text(0));
}

ProjectBrowser::FileEntry& ProjectBrowser::getSelectedFileEntry() const noexcept {
return *debug_cast<ProjectBrowser::FileEntry*>(currentItem());
}
Expand All @@ -409,8 +467,8 @@ void ProjectBrowser::linkFileToAncestor(FileEntry* file_item, const std::filesys
std::filesystem::path folder_path = file_path.parent_path();

//Link the file to it's folder
auto result = entries.find(folder_path);
if(result != entries.end()){
auto result = files_in_filesystem.find(folder_path);
if(result != files_in_filesystem.end()){
//Folder already exists
QTreeWidgetItem* preexisting_folder_entry = result->second;
preexisting_folder_entry->addChild(file_item);
Expand All @@ -424,7 +482,7 @@ void ProjectBrowser::linkFileToAncestor(FileEntry* file_item, const std::filesys
//Browser already showing different drives, do nothing
}else if(stale_root_path.root_name() != file_path.root_name()){
//These files come from different drives; change root to nothing since no common ancestor
entries.erase(stale_root_path);
files_in_filesystem.erase(stale_root_path);
root->setData(0, Qt::UserRole, QString());
taken_children = root->takeChildren();
}else{
Expand All @@ -449,8 +507,8 @@ void ProjectBrowser::linkFileToAncestor(FileEntry* file_item, const std::filesys
}

taken_children = root->takeChildren();
entries.erase(stale_root_path);
entries[new_root_path] = root;
files_in_filesystem.erase(stale_root_path);
files_in_filesystem[new_root_path] = root;
root->setData(0, Qt::UserRole, toQString(new_root_path));
}
}
Expand All @@ -460,17 +518,18 @@ void ProjectBrowser::linkFileToAncestor(FileEntry* file_item, const std::filesys
if(!taken_children.empty()){
linkItemToExistingAncestor(taken_children.back(), stale_root_path / "fake");
taken_children.pop_back();
QTreeWidgetItem* entry_for_stale_root = entries[stale_root_path];
QTreeWidgetItem* entry_for_stale_root = files_in_filesystem[stale_root_path];
entry_for_stale_root->addChildren(taken_children);
setCurrentItem(currently_viewed_file);
}
}

void ProjectBrowser::linkItemToExistingAncestor(QTreeWidgetItem* item, std::filesystem::path path) {
path = path.parent_path();
auto parent_result = entries.insert({path, nullptr});
auto parent_result = files_in_filesystem.insert({path, nullptr});
while(parent_result.second){
DirectoryEntry* new_item = new DirectoryEntry;
setRootIsDecorated(true); //Effectively adding padding for expand/collapse arrows
new_item->addChild(item);
item = new_item;
new_item->setPath(path);
Expand All @@ -479,7 +538,7 @@ void ProjectBrowser::linkItemToExistingAncestor(QTreeWidgetItem* item, std::file
auto parent_path = path.parent_path();
if(parent_path != path){ //has_parent_path() lies, causing an infinite loop
path = parent_path;
parent_result = entries.insert({path, nullptr});
parent_result = files_in_filesystem.insert({path, nullptr});
}else{
invisibleRootItem()->addChild(item);
item->setText(0, toQString(*path.begin()));
Expand Down
10 changes: 7 additions & 3 deletions app/projectbrowser.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ class ProjectBrowser : public QTreeWidget {
ProjectBrowser(QWidget* parent, MainWindow* main_window);
void setProject(Forscape::Typeset::Model* model);
void addProject(const std::filesystem::path& entry_point_path);
void addFile(const std::filesystem::path& file_path);
void addFile(Forscape::Typeset::Model* model);
void addUnsavedFile(Forscape::Typeset::Model* model);
void saveModel(Forscape::Typeset::Model* saved_model, const std::filesystem::path& std_path);
void updateProjectBrowser();
void addProjectEntry(Forscape::Typeset::Model* model);
Expand All @@ -34,6 +33,10 @@ private slots:
void onDirectoryRenamed();
void onFileRenamed();
void expandDirectory();
void onCopyFilePath();
void onCopyFileName();
void onCopyDirPath();
void onCopyDirName();

private:
class FileEntry;
Expand All @@ -43,7 +46,8 @@ private slots:
void linkFileToAncestor(FileEntry* file_item, const std::filesystem::path file_path);
void linkItemToExistingAncestor(QTreeWidgetItem* item, std::filesystem::path path);

FORSCAPE_UNORDERED_MAP<std::filesystem::path, QTreeWidgetItem*> entries;
FORSCAPE_UNORDERED_MAP<std::filesystem::path, QTreeWidgetItem*> files_in_filesystem;
FORSCAPE_UNORDERED_MAP<Typeset::Model*, QTreeWidgetItem*> files_in_memory;
FileEntry* currently_viewed_file = nullptr; //Used to give affordance
MainWindow* main_window;
};
Expand Down
18 changes: 12 additions & 6 deletions src/forscape_program.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,9 @@ void Program::getFileSuggestions(std::vector<std::string>& suggestions, Typeset:
if(std::filesystem::exists(path_entry)){
for(auto const& dir_entry : std::filesystem::directory_iterator{path_entry}){
const std::string candidate = dir_entry.path().filename().u8string();
if(notPiFile(candidate) && candidate != model_filename) continue;
suggestions.push_back(candidate);
const bool is_candidate_dir = dir_entry.is_directory();
if((notPiFile(candidate) && !is_candidate_dir) || (candidate == model_filename)) continue;
suggestions.push_back(candidate + (is_candidate_dir ? "/" : ""));
}
}
}
Expand All @@ -171,13 +172,18 @@ void Program::getFileSuggestions(std::vector<std::string>& suggestions, std::str
for(const auto& dir_entry : std::filesystem::directory_iterator{dir_of_input}){
const std::filesystem::path candidate_path = dir_entry.path();
const std::string candidate_filename = candidate_path.filename().u8string();
const bool is_candidate_dir = dir_entry.is_directory(); //EVENTUALLY: doesn't work with symbolic links to directories

if(candidate_filename.size() < input_filename.size() ||
notPiFile(candidate_filename) ||
std::string_view(candidate_filename.data(), input_filename.size()) != input_filename) continue;
if(candidate_filename.size() < input_filename.size() || //Abort early; search term is larger than candidate
(notPiFile(candidate_filename) && !is_candidate_dir) || //Not the right file type
std::string_view(candidate_filename.data(), input_filename.size()) != input_filename //Doesn't match search term
) continue;

std::filesystem::path rel_path = std::filesystem::relative(candidate_path, path_entry);
if(rel_path != model_rel_path) suggestions.push_back(rel_path.u8string());
if( rel_path == "." || //Do not suggest same directory, e.g. "../dir_we_came_from/" resolves to "./"
rel_path == model_rel_path //Do not suggest self import
) continue;
suggestions.push_back(rel_path.u8string() + (is_candidate_dir ? "/" : ""));
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/typeset_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Model {
std::string error_warning_buffer;
std::filesystem::path path;
bool notOnDisk() const noexcept { return path.empty(); }
bool isSavedToDisk() const noexcept { return !notOnDisk(); }
#ifdef QT_VERSION
std::filesystem::file_time_type write_time;
#endif
Expand Down

0 comments on commit a46b4c5

Please sign in to comment.