mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2024-09-21 10:19:03 +03:00
FileManager: Port to GTableModel/GTableView.
Replace the custom DirectoryView widget with a GTableView subclass. This was pleasantly straightforward and it's so cool seeing the huge increase in app quality from GTableView. :^)
This commit is contained in:
parent
b5dcad932e
commit
ac8fb5da4c
Notes:
sideshowbarker
2024-07-19 15:34:37 +09:00
Author: https://github.com/awesomekling Commit: https://github.com/SerenityOS/serenity/commit/ac8fb5da4c2
180
Applications/FileManager/DirectoryTableModel.cpp
Normal file
180
Applications/FileManager/DirectoryTableModel.cpp
Normal file
@ -0,0 +1,180 @@
|
||||
#include "DirectoryTableModel.h"
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <AK/FileSystemPath.h>
|
||||
#include <AK/StringBuilder.h>
|
||||
|
||||
DirectoryTableModel::DirectoryTableModel()
|
||||
{
|
||||
m_directory_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/folder16.rgb", { 16, 16 });
|
||||
m_file_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/file16.rgb", { 16, 16 });
|
||||
m_symlink_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/link16.rgb", { 16, 16 });
|
||||
m_socket_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/socket16.rgb", { 16, 16 });
|
||||
}
|
||||
|
||||
DirectoryTableModel::~DirectoryTableModel()
|
||||
{
|
||||
}
|
||||
|
||||
int DirectoryTableModel::row_count() const
|
||||
{
|
||||
return m_directories.size() + m_files.size();
|
||||
}
|
||||
|
||||
int DirectoryTableModel::column_count() const
|
||||
{
|
||||
return Column::__Count;
|
||||
}
|
||||
|
||||
String DirectoryTableModel::column_name(int column) const
|
||||
{
|
||||
switch (column) {
|
||||
case Column::Icon: return "";
|
||||
case Column::Name: return "Name";
|
||||
case Column::Size: return "Size";
|
||||
case Column::UID: return "UID";
|
||||
case Column::GID: return "GID";
|
||||
case Column::Permissions: return "Mode";
|
||||
case Column::Inode: return "Inode";
|
||||
}
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
GTableModel::ColumnMetadata DirectoryTableModel::column_metadata(int column) const
|
||||
{
|
||||
switch (column) {
|
||||
case Column::Icon: return { 16, TextAlignment::Center };
|
||||
case Column::Name: return { 120, TextAlignment::CenterLeft };
|
||||
case Column::Size: return { 80, TextAlignment::CenterRight };
|
||||
case Column::UID: return { 80, TextAlignment::CenterRight };
|
||||
case Column::GID: return { 80, TextAlignment::CenterRight };
|
||||
case Column::Permissions: return { 100, TextAlignment::CenterLeft };
|
||||
case Column::Inode: return { 80, TextAlignment::CenterRight };
|
||||
}
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
const GraphicsBitmap& DirectoryTableModel::icon_for(const Entry& entry) const
|
||||
{
|
||||
if (S_ISDIR(entry.mode))
|
||||
return *m_directory_icon;
|
||||
if (S_ISLNK(entry.mode))
|
||||
return *m_symlink_icon;
|
||||
if (S_ISSOCK(entry.mode))
|
||||
return *m_socket_icon;
|
||||
return *m_file_icon;
|
||||
}
|
||||
|
||||
|
||||
static String permission_string(mode_t mode)
|
||||
{
|
||||
StringBuilder builder;
|
||||
if (S_ISDIR(mode))
|
||||
builder.append("d");
|
||||
else if (S_ISLNK(mode))
|
||||
builder.append("l");
|
||||
else if (S_ISBLK(mode))
|
||||
builder.append("b");
|
||||
else if (S_ISCHR(mode))
|
||||
builder.append("c");
|
||||
else if (S_ISFIFO(mode))
|
||||
builder.append("f");
|
||||
else if (S_ISSOCK(mode))
|
||||
builder.append("s");
|
||||
else if (S_ISREG(mode))
|
||||
builder.append("-");
|
||||
else
|
||||
builder.append("?");
|
||||
|
||||
builder.appendf("%c%c%c%c%c%c%c%c",
|
||||
mode & S_IRUSR ? 'r' : '-',
|
||||
mode & S_IWUSR ? 'w' : '-',
|
||||
mode & S_ISUID ? 's' : (mode & S_IXUSR ? 'x' : '-'),
|
||||
mode & S_IRGRP ? 'r' : '-',
|
||||
mode & S_IWGRP ? 'w' : '-',
|
||||
mode & S_ISGID ? 's' : (mode & S_IXGRP ? 'x' : '-'),
|
||||
mode & S_IROTH ? 'r' : '-',
|
||||
mode & S_IWOTH ? 'w' : '-'
|
||||
);
|
||||
|
||||
if (mode & S_ISVTX)
|
||||
builder.append("t");
|
||||
else
|
||||
builder.appendf("%c", mode & S_IXOTH ? 'x' : '-');
|
||||
return builder.to_string();
|
||||
}
|
||||
|
||||
GVariant DirectoryTableModel::data(int row, int column) const
|
||||
{
|
||||
auto& entry = this->entry(row);
|
||||
switch (column) {
|
||||
case Column::Icon: return icon_for(entry);
|
||||
case Column::Name: return entry.name;
|
||||
case Column::Size: return (int)entry.size;
|
||||
case Column::UID: return (int)entry.uid;
|
||||
case Column::GID: return (int)entry.gid;
|
||||
case Column::Permissions: return permission_string(entry.mode);
|
||||
case Column::Inode: return (int)entry.inode;
|
||||
}
|
||||
ASSERT_NOT_REACHED();
|
||||
|
||||
}
|
||||
|
||||
void DirectoryTableModel::update()
|
||||
{
|
||||
DIR* dirp = opendir(m_path.characters());
|
||||
if (!dirp) {
|
||||
perror("opendir");
|
||||
exit(1);
|
||||
}
|
||||
m_directories.clear();
|
||||
m_files.clear();
|
||||
|
||||
m_bytes_in_files = 0;
|
||||
while (auto* de = readdir(dirp)) {
|
||||
Entry entry;
|
||||
entry.name = de->d_name;
|
||||
struct stat st;
|
||||
int rc = lstat(String::format("%s/%s", m_path.characters(), de->d_name).characters(), &st);
|
||||
if (rc < 0) {
|
||||
perror("lstat");
|
||||
continue;
|
||||
}
|
||||
entry.size = st.st_size;
|
||||
entry.mode = st.st_mode;
|
||||
entry.uid = st.st_uid;
|
||||
entry.gid = st.st_gid;
|
||||
entry.inode = st.st_ino;
|
||||
auto& entries = S_ISDIR(st.st_mode) ? m_directories : m_files;
|
||||
entries.append(move(entry));
|
||||
|
||||
if (S_ISREG(entry.mode))
|
||||
m_bytes_in_files += st.st_size;
|
||||
}
|
||||
closedir(dirp);
|
||||
|
||||
did_update();
|
||||
}
|
||||
|
||||
void DirectoryTableModel::open(const String& path)
|
||||
{
|
||||
if (m_path == path)
|
||||
return;
|
||||
DIR* dirp = opendir(path.characters());
|
||||
if (!dirp)
|
||||
return;
|
||||
closedir(dirp);
|
||||
m_path = path;
|
||||
update();
|
||||
set_selected_index({ 0, 0 });
|
||||
}
|
||||
|
||||
void DirectoryTableModel::activate(const GModelIndex& index)
|
||||
{
|
||||
auto& entry = this->entry(index.row());
|
||||
if (entry.is_directory()) {
|
||||
FileSystemPath new_path(String::format("%s/%s", m_path.characters(), entry.name.characters()));
|
||||
open(new_path.string());
|
||||
}
|
||||
}
|
62
Applications/FileManager/DirectoryTableModel.h
Normal file
62
Applications/FileManager/DirectoryTableModel.h
Normal file
@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <LibGUI/GTableModel.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
class DirectoryTableModel final : public GTableModel {
|
||||
public:
|
||||
DirectoryTableModel();
|
||||
virtual ~DirectoryTableModel() override;
|
||||
|
||||
enum Column {
|
||||
Icon = 0,
|
||||
Name,
|
||||
Size,
|
||||
UID,
|
||||
GID,
|
||||
Permissions,
|
||||
Inode,
|
||||
__Count,
|
||||
};
|
||||
|
||||
virtual int row_count() const override;
|
||||
virtual int column_count() const override;
|
||||
virtual String column_name(int column) const override;
|
||||
virtual ColumnMetadata column_metadata(int column) const override;
|
||||
virtual GVariant data(int row, int column) const override;
|
||||
virtual void update() override;
|
||||
virtual void activate(const GModelIndex&) override;
|
||||
|
||||
String path() const { return m_path; }
|
||||
void open(const String& path);
|
||||
size_t bytes_in_files() const { return m_bytes_in_files; }
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
String name;
|
||||
size_t size { 0 };
|
||||
mode_t mode { 0 };
|
||||
uid_t uid { 0 };
|
||||
uid_t gid { 0 };
|
||||
ino_t inode { 0 };
|
||||
bool is_directory() const { return S_ISDIR(mode); }
|
||||
};
|
||||
|
||||
const Entry& entry(int index) const
|
||||
{
|
||||
if (index < m_directories.size())
|
||||
return m_directories[index];
|
||||
return m_files[index - m_directories.size()];
|
||||
}
|
||||
const GraphicsBitmap& icon_for(const Entry& entry) const;
|
||||
|
||||
String m_path;
|
||||
Vector<Entry> m_files;
|
||||
Vector<Entry> m_directories;
|
||||
size_t m_bytes_in_files;
|
||||
|
||||
RetainPtr<GraphicsBitmap> m_directory_icon;
|
||||
RetainPtr<GraphicsBitmap> m_file_icon;
|
||||
RetainPtr<GraphicsBitmap> m_symlink_icon;
|
||||
RetainPtr<GraphicsBitmap> m_socket_icon;
|
||||
};
|
33
Applications/FileManager/DirectoryTableView.cpp
Normal file
33
Applications/FileManager/DirectoryTableView.cpp
Normal file
@ -0,0 +1,33 @@
|
||||
#include "DirectoryTableView.h"
|
||||
|
||||
DirectoryTableView::DirectoryTableView(GWidget* parent)
|
||||
: GTableView(parent)
|
||||
{
|
||||
set_model(make<DirectoryTableModel>());
|
||||
}
|
||||
|
||||
DirectoryTableView::~DirectoryTableView()
|
||||
{
|
||||
}
|
||||
|
||||
void DirectoryTableView::open(const String& path)
|
||||
{
|
||||
model().open(path);
|
||||
}
|
||||
|
||||
void DirectoryTableView::model_notification(const GModelNotification& notification)
|
||||
{
|
||||
if (notification.type() == GModelNotification::Type::ModelUpdated) {
|
||||
set_status_message(String::format("%d item%s (%u byte%s)",
|
||||
model().row_count(),
|
||||
model().row_count() != 1 ? "s" : "",
|
||||
model().bytes_in_files(),
|
||||
model().bytes_in_files() != 1 ? "s" : ""));
|
||||
}
|
||||
}
|
||||
|
||||
void DirectoryTableView::set_status_message(const String& message)
|
||||
{
|
||||
if (on_status_message)
|
||||
on_status_message(message);
|
||||
}
|
25
Applications/FileManager/DirectoryTableView.h
Normal file
25
Applications/FileManager/DirectoryTableView.h
Normal file
@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include <LibGUI/GTableView.h>
|
||||
#include <sys/stat.h>
|
||||
#include "DirectoryTableModel.h"
|
||||
|
||||
class DirectoryTableView final : public GTableView {
|
||||
public:
|
||||
explicit DirectoryTableView(GWidget* parent);
|
||||
virtual ~DirectoryTableView() override;
|
||||
|
||||
void open(const String& path);
|
||||
String path() const { return model().path(); }
|
||||
|
||||
Function<void(const String&)> on_path_change;
|
||||
Function<void(String)> on_status_message;
|
||||
|
||||
private:
|
||||
virtual void model_notification(const GModelNotification&) override;
|
||||
|
||||
DirectoryTableModel& model() { return static_cast<DirectoryTableModel&>(*GTableView::model()); }
|
||||
const DirectoryTableModel& model() const { return static_cast<const DirectoryTableModel&>(*GTableView::model()); }
|
||||
|
||||
void set_status_message(const String&);
|
||||
};
|
@ -1,171 +0,0 @@
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <SharedGraphics/GraphicsBitmap.h>
|
||||
#include <SharedGraphics/Painter.h>
|
||||
#include <LibGUI/GScrollBar.h>
|
||||
#include <AK/FileSystemPath.h>
|
||||
#include "DirectoryView.h"
|
||||
|
||||
DirectoryView::DirectoryView(GWidget* parent)
|
||||
: GWidget(parent)
|
||||
{
|
||||
m_directory_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/folder16.rgb", { 16, 16 });
|
||||
m_file_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/file16.rgb", { 16, 16 });
|
||||
m_symlink_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/link16.rgb", { 16, 16 });
|
||||
m_socket_icon = GraphicsBitmap::load_from_file(GraphicsBitmap::Format::RGBA32, "/res/icons/socket16.rgb", { 16, 16 });
|
||||
|
||||
m_scrollbar = new GScrollBar(Orientation::Vertical, this);
|
||||
m_scrollbar->set_step(4);
|
||||
m_scrollbar->set_big_step(30);
|
||||
m_scrollbar->on_change = [this] (int) {
|
||||
update();
|
||||
};
|
||||
}
|
||||
|
||||
DirectoryView::~DirectoryView()
|
||||
{
|
||||
}
|
||||
|
||||
void DirectoryView::resize_event(GResizeEvent& event)
|
||||
{
|
||||
m_scrollbar->set_relative_rect(event.size().width() - m_scrollbar->preferred_size().width(), 0, m_scrollbar->preferred_size().width(), event.size().height());
|
||||
}
|
||||
|
||||
void DirectoryView::open(const String& path)
|
||||
{
|
||||
if (m_path == path)
|
||||
return;
|
||||
DIR* dirp = opendir(path.characters());
|
||||
if (!dirp)
|
||||
return;
|
||||
closedir(dirp);
|
||||
m_path = path;
|
||||
reload();
|
||||
if (on_path_change)
|
||||
on_path_change(m_path);
|
||||
update();
|
||||
}
|
||||
|
||||
void DirectoryView::reload()
|
||||
{
|
||||
DIR* dirp = opendir(m_path.characters());
|
||||
if (!dirp) {
|
||||
perror("opendir");
|
||||
exit(1);
|
||||
}
|
||||
m_directories.clear();
|
||||
m_files.clear();
|
||||
|
||||
size_t bytes_in_files = 0;
|
||||
while (auto* de = readdir(dirp)) {
|
||||
Entry entry;
|
||||
entry.name = de->d_name;
|
||||
struct stat st;
|
||||
int rc = lstat(String::format("%s/%s", m_path.characters(), de->d_name).characters(), &st);
|
||||
if (rc < 0) {
|
||||
perror("lstat");
|
||||
continue;
|
||||
}
|
||||
entry.size = st.st_size;
|
||||
entry.mode = st.st_mode;
|
||||
auto& entries = S_ISDIR(st.st_mode) ? m_directories : m_files;
|
||||
entries.append(move(entry));
|
||||
|
||||
if (S_ISREG(entry.mode))
|
||||
bytes_in_files += st.st_size;
|
||||
}
|
||||
closedir(dirp);
|
||||
int excess_height = max(0, (item_count() * item_height()) - height());
|
||||
m_scrollbar->set_range(0, excess_height);
|
||||
|
||||
|
||||
|
||||
set_status_message(String::format("%d item%s (%u byte%s)",
|
||||
item_count(),
|
||||
item_count() != 1 ? "s" : "",
|
||||
bytes_in_files,
|
||||
bytes_in_files != 1 ? "s" : ""));
|
||||
}
|
||||
|
||||
const GraphicsBitmap& DirectoryView::icon_for(const Entry& entry) const
|
||||
{
|
||||
if (S_ISDIR(entry.mode))
|
||||
return *m_directory_icon;
|
||||
if (S_ISLNK(entry.mode))
|
||||
return *m_symlink_icon;
|
||||
if (S_ISSOCK(entry.mode))
|
||||
return *m_socket_icon;
|
||||
return *m_file_icon;
|
||||
}
|
||||
|
||||
static String pretty_byte_size(size_t size)
|
||||
{
|
||||
return String::format("%u", size);
|
||||
}
|
||||
|
||||
bool DirectoryView::should_show_size_for(const Entry& entry) const
|
||||
{
|
||||
return S_ISREG(entry.mode);
|
||||
}
|
||||
|
||||
Rect DirectoryView::row_rect(int item_index) const
|
||||
{
|
||||
return { 0, item_index * item_height(), width(), item_height() };
|
||||
}
|
||||
|
||||
void DirectoryView::mousedown_event(GMouseEvent& event)
|
||||
{
|
||||
if (event.button() == GMouseButton::Left) {
|
||||
for (int i = 0; i < item_count(); ++i) {
|
||||
if (!row_rect(i).contains(event.position()))
|
||||
continue;
|
||||
auto& entry = this->entry(i);
|
||||
if (entry.is_directory()) {
|
||||
FileSystemPath new_path(String::format("%s/%s", m_path.characters(), entry.name.characters()));
|
||||
open(new_path.string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DirectoryView::paint_event(GPaintEvent&)
|
||||
{
|
||||
Painter painter(*this);
|
||||
|
||||
painter.translate(0, -m_scrollbar->value());
|
||||
|
||||
int horizontal_padding = 5;
|
||||
int icon_size = 16;
|
||||
int painted_item_index = 0;
|
||||
|
||||
auto process_entries = [&] (const Vector<Entry>& entries) {
|
||||
for (int i = 0; i < entries.size(); ++i, ++painted_item_index) {
|
||||
auto& entry = entries[i];
|
||||
int y = painted_item_index * item_height();
|
||||
Rect icon_rect(horizontal_padding, y, icon_size, item_height());
|
||||
Rect name_rect(icon_rect.right() + horizontal_padding, y, 100, item_height());
|
||||
Rect size_rect(name_rect.right() + horizontal_padding, y, 64, item_height());
|
||||
painter.fill_rect(row_rect(painted_item_index), painted_item_index % 2 ? Color(210, 210, 210) : Color::White);
|
||||
painter.blit(icon_rect.location(), icon_for(entry), { 0, 0, icon_size, icon_size });
|
||||
painter.draw_text(name_rect, entry.name, TextAlignment::CenterLeft, Color::Black);
|
||||
if (should_show_size_for(entry))
|
||||
painter.draw_text(size_rect, pretty_byte_size(entry.size), TextAlignment::CenterRight, Color::Black);
|
||||
}
|
||||
};
|
||||
|
||||
process_entries(m_directories);
|
||||
process_entries(m_files);
|
||||
|
||||
Rect unpainted_rect(0, painted_item_index * item_height(), width(), height());
|
||||
unpainted_rect.intersect(rect());
|
||||
painter.fill_rect(unpainted_rect, Color::White);
|
||||
}
|
||||
|
||||
void DirectoryView::set_status_message(String&& message)
|
||||
{
|
||||
if (on_status_message)
|
||||
on_status_message(move(message));
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <LibGUI/GWidget.h>
|
||||
#include <AK/Function.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
class GScrollBar;
|
||||
|
||||
class DirectoryView final : public GWidget {
|
||||
public:
|
||||
DirectoryView(GWidget* parent);
|
||||
virtual ~DirectoryView() override;
|
||||
|
||||
void open(const String& path);
|
||||
void reload();
|
||||
|
||||
Function<void(const String&)> on_path_change;
|
||||
Function<void(String)> on_status_message;
|
||||
|
||||
int item_height() const { return 16; }
|
||||
int item_count() const { return m_directories.size() + m_files.size(); }
|
||||
|
||||
private:
|
||||
virtual void paint_event(GPaintEvent&) override;
|
||||
virtual void resize_event(GResizeEvent&) override;
|
||||
virtual void mousedown_event(GMouseEvent&) override;
|
||||
|
||||
void set_status_message(String&&);
|
||||
|
||||
Rect row_rect(int item_index) const;
|
||||
|
||||
struct Entry {
|
||||
String name;
|
||||
size_t size { 0 };
|
||||
mode_t mode { 0 };
|
||||
|
||||
bool is_directory() const { return S_ISDIR(mode); }
|
||||
};
|
||||
|
||||
const Entry& entry(int index) const
|
||||
{
|
||||
if (index < m_directories.size())
|
||||
return m_directories[index];
|
||||
return m_files[index - m_directories.size()];
|
||||
}
|
||||
const GraphicsBitmap& icon_for(const Entry&) const;
|
||||
bool should_show_size_for(const Entry&) const;
|
||||
|
||||
Vector<Entry> m_files;
|
||||
Vector<Entry> m_directories;
|
||||
|
||||
String m_path;
|
||||
RetainPtr<GraphicsBitmap> m_directory_icon;
|
||||
RetainPtr<GraphicsBitmap> m_file_icon;
|
||||
RetainPtr<GraphicsBitmap> m_symlink_icon;
|
||||
RetainPtr<GraphicsBitmap> m_socket_icon;
|
||||
|
||||
GScrollBar* m_scrollbar { nullptr };
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
OBJS = \
|
||||
DirectoryView.o \
|
||||
DirectoryTableModel.o \
|
||||
DirectoryTableView.o \
|
||||
main.o
|
||||
|
||||
APP = FileManager
|
||||
|
@ -8,7 +8,7 @@
|
||||
#include <LibGUI/GAction.h>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include "DirectoryView.h"
|
||||
#include "DirectoryTableView.h"
|
||||
|
||||
int main(int argc, char** argv)
|
||||
{
|
||||
@ -63,20 +63,21 @@ int main(int argc, char** argv)
|
||||
toolbar->add_action(copy_action.copy_ref());
|
||||
toolbar->add_action(delete_action.copy_ref());
|
||||
|
||||
auto* directory_view = new DirectoryView(widget);
|
||||
auto* directory_table_view = new DirectoryTableView(widget);
|
||||
|
||||
auto* statusbar = new GStatusBar(widget);
|
||||
statusbar->set_text("Welcome!");
|
||||
|
||||
directory_view->on_path_change = [window] (const String& new_path) {
|
||||
directory_table_view->on_path_change = [window] (const String& new_path) {
|
||||
window->set_title(String::format("FileManager: %s", new_path.characters()));
|
||||
};
|
||||
|
||||
directory_view->on_status_message = [statusbar] (String message) {
|
||||
directory_table_view->on_status_message = [statusbar] (String message) {
|
||||
statusbar->set_text(move(message));
|
||||
};
|
||||
|
||||
directory_view->open("/");
|
||||
directory_table_view->open("/");
|
||||
directory_table_view->set_focus(true);
|
||||
|
||||
window->set_should_exit_app_on_close(true);
|
||||
window->show();
|
||||
|
Loading…
Reference in New Issue
Block a user