diff --git a/src/86box.c b/src/86box.c index f54a69b79..c17e697e1 100644 --- a/src/86box.c +++ b/src/86box.c @@ -251,6 +251,8 @@ struct accelKey def_acc_keys[NUM_ACCELS] = { .seq="Ctrl+Alt+M" } }; +char vmm_path[1024] = { '\0'}; /* TEMPORARY - VM manager path to scan for VMs */ +int vmm_enabled = 0; /* Statistics. */ extern int mmuflush; @@ -598,8 +600,8 @@ pc_show_usage(char *s) #ifdef _WIN32 "-D or --debug\t\t\t- force debug output logging\n" #endif -#if 0 - "-E or --nographic\t\t- forces the old behavior\n" +#if 1 + "-E or --vmmpath\t\t- vm manager path\n" #endif "-F or --fullscreen\t\t- start in fullscreen mode\n" "-G or --lang langid\t\t- start with specified language\n" @@ -732,13 +734,14 @@ usage: } else if (!strcasecmp(argv[c], "--debug") || !strcasecmp(argv[c], "-D")) { force_debug = 1; #endif -#ifdef ENABLE_NG - } else if (!strcasecmp(argv[c], "--nographic") || !strcasecmp(argv[c], "-E")) { - /* Currently does nothing, but if/when we implement a built-in manager, - it's going to force the manager not to run, allowing the old usage - without parameter. */ - ng = 1; -#endif +//#ifdef ENABLE_NG + } else if (!strcasecmp(argv[c], "--vmmpath") || + !strcasecmp(argv[c], "-E")) { + /* Using this variable for vm manager path + Temporary solution!*/ + if ((c+1) == argc) goto usage; + strcpy(vmm_path, argv[++c]); + //#endif } else if (!strcasecmp(argv[c], "--fullscreen") || !strcasecmp(argv[c], "-F")) { start_in_fullscreen = 1; } else if (!strcasecmp(argv[c], "--logfile") || !strcasecmp(argv[c], "-L")) { @@ -1025,6 +1028,10 @@ usage: } pclog("# Configuration file: %s\n#\n\n", cfg_path); + if (strlen(vmm_path) != 0) { + vmm_enabled = 1; + pclog("# VM Manager enabled. Path: %s\n", vmm_path); + } /* * We are about to read the configuration file, which MAY * put data into global variables (the hard- and floppy diff --git a/src/include/86box/86box.h b/src/include/86box/86box.h index 76f311d17..488585ce2 100644 --- a/src/include/86box/86box.h +++ b/src/include/86box/86box.h @@ -176,6 +176,8 @@ extern char usr_path[1024]; /* path (dir) of user data */ extern char cfg_path[1024]; /* full path of config file */ extern int open_dir_usr_path; /* default file open dialog directory of usr_path */ extern char uuid[MAX_UUID_LEN]; /* UUID or machine identifier */ +extern char vmm_path[1024]; /* VM Manager path to scan (temporary) */ +extern int vmm_enabled; #ifndef USE_NEW_DYNAREC extern FILE *stdlog; /* file to log output to */ #endif diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index df13d42e2..dc430d68c 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -189,6 +189,44 @@ add_library(ui STATIC qt_mediahistorymanager.cpp qt_mediahistorymanager.hpp + qt_updatecheck.cpp + qt_updatecheck.hpp + qt_updatecheckdialog.cpp + qt_updatecheckdialog.hpp + qt_updatedetails.cpp + qt_updatedetails.hpp + qt_downloader.cpp + qt_downloader.hpp + + qt_vmmanager_clientsocket.cpp + qt_vmmanager_clientsocket.hpp + qt_vmmanager_serversocket.cpp + qt_vmmanager_serversocket.hpp + qt_vmmanager_protocol.cpp + qt_vmmanager_protocol.hpp + qt_vmmanager_details.hpp + qt_vmmanager_details.cpp + qt_vmmanager_details.ui + qt_vmmanager_addmachine.cpp + qt_vmmanager_addmachine.hpp + qt_vmmanager_detailsection.cpp + qt_vmmanager_detailsection.hpp + qt_vmmanager_listviewdelegate.hpp + qt_vmmanager_listviewdelegate.cpp + qt_vmmanager_preferences.cpp + qt_vmmanager_preferences.hpp + qt_vmmanager_main.hpp + qt_vmmanager_main.cpp + qt_vmmanager_main.ui + qt_vmmanager_model.cpp + qt_vmmanager_model.hpp + qt_vmmanager_system.cpp + qt_vmmanager_system.hpp + qt_vmmanager_config.cpp + qt_vmmanager_config.hpp + qt_vmmanager_mainwindow.cpp + qt_vmmanager_mainwindow.hpp + ../qt_resources.qrc ./qdarkstyle/dark/darkstyle.qrc diff --git a/src/qt/assets/86box-wizard.png b/src/qt/assets/86box-wizard.png new file mode 100644 index 000000000..19ecda8c7 Binary files /dev/null and b/src/qt/assets/86box-wizard.png differ diff --git a/src/qt/assets/systemicons/cpq_deskpro.png b/src/qt/assets/systemicons/cpq_deskpro.png new file mode 100644 index 000000000..1bbca68a3 Binary files /dev/null and b/src/qt/assets/systemicons/cpq_deskpro.png differ diff --git a/src/qt/assets/systemicons/cpq_port_386.png b/src/qt/assets/systemicons/cpq_port_386.png new file mode 100644 index 000000000..4c55b3559 Binary files /dev/null and b/src/qt/assets/systemicons/cpq_port_386.png differ diff --git a/src/qt/assets/systemicons/cpq_port_II.png b/src/qt/assets/systemicons/cpq_port_II.png new file mode 100644 index 000000000..3254d9b5e Binary files /dev/null and b/src/qt/assets/systemicons/cpq_port_II.png differ diff --git a/src/qt/assets/systemicons/cpq_port_III.png b/src/qt/assets/systemicons/cpq_port_III.png new file mode 100644 index 000000000..65d2f714e Binary files /dev/null and b/src/qt/assets/systemicons/cpq_port_III.png differ diff --git a/src/qt/assets/systemicons/cpq_portable.png b/src/qt/assets/systemicons/cpq_portable.png new file mode 100644 index 000000000..d3dc381e4 Binary files /dev/null and b/src/qt/assets/systemicons/cpq_portable.png differ diff --git a/src/qt/assets/systemicons/cpq_pres_2240.png b/src/qt/assets/systemicons/cpq_pres_2240.png new file mode 100644 index 000000000..c50043cdd Binary files /dev/null and b/src/qt/assets/systemicons/cpq_pres_2240.png differ diff --git a/src/qt/assets/systemicons/cpq_pres_4500.png b/src/qt/assets/systemicons/cpq_pres_4500.png new file mode 100644 index 000000000..edd955b5a Binary files /dev/null and b/src/qt/assets/systemicons/cpq_pres_4500.png differ diff --git a/src/qt/assets/systemicons/ibm330.png b/src/qt/assets/systemicons/ibm330.png new file mode 100644 index 000000000..90637dc4c Binary files /dev/null and b/src/qt/assets/systemicons/ibm330.png differ diff --git a/src/qt/assets/systemicons/ibm_at.png b/src/qt/assets/systemicons/ibm_at.png new file mode 100644 index 000000000..e6710180e Binary files /dev/null and b/src/qt/assets/systemicons/ibm_at.png differ diff --git a/src/qt/assets/systemicons/ibm_pc_81.png b/src/qt/assets/systemicons/ibm_pc_81.png new file mode 100644 index 000000000..4f398d468 Binary files /dev/null and b/src/qt/assets/systemicons/ibm_pc_81.png differ diff --git a/src/qt/assets/systemicons/ibm_pc_82.png b/src/qt/assets/systemicons/ibm_pc_82.png new file mode 100644 index 000000000..4f398d468 Binary files /dev/null and b/src/qt/assets/systemicons/ibm_pc_82.png differ diff --git a/src/qt/assets/systemicons/ibm_pcjr.png b/src/qt/assets/systemicons/ibm_pcjr.png new file mode 100644 index 000000000..1f3014bb4 Binary files /dev/null and b/src/qt/assets/systemicons/ibm_pcjr.png differ diff --git a/src/qt/assets/systemicons/ibm_ps2_m70.png b/src/qt/assets/systemicons/ibm_ps2_m70.png new file mode 100644 index 000000000..af433ebb3 Binary files /dev/null and b/src/qt/assets/systemicons/ibm_ps2_m70.png differ diff --git a/src/qt/assets/systemicons/ibm_ps2_m80.png b/src/qt/assets/systemicons/ibm_ps2_m80.png new file mode 100644 index 000000000..9df02adff Binary files /dev/null and b/src/qt/assets/systemicons/ibm_ps2_m80.png differ diff --git a/src/qt/assets/systemicons/ibm_psvp_486.png b/src/qt/assets/systemicons/ibm_psvp_486.png new file mode 100644 index 000000000..af5a95675 Binary files /dev/null and b/src/qt/assets/systemicons/ibm_psvp_486.png differ diff --git a/src/qt/assets/systemicons/ibm_psvp_p60.png b/src/qt/assets/systemicons/ibm_psvp_p60.png new file mode 100644 index 000000000..af5a95675 Binary files /dev/null and b/src/qt/assets/systemicons/ibm_psvp_p60.png differ diff --git a/src/qt/assets/systemicons/ibm_xt_82.png b/src/qt/assets/systemicons/ibm_xt_82.png new file mode 100644 index 000000000..4f398d468 Binary files /dev/null and b/src/qt/assets/systemicons/ibm_xt_82.png differ diff --git a/src/qt/assets/systemicons/ibm_xt_86.png b/src/qt/assets/systemicons/ibm_xt_86.png new file mode 100644 index 000000000..4f398d468 Binary files /dev/null and b/src/qt/assets/systemicons/ibm_xt_86.png differ diff --git a/src/qt/assets/systemicons/olivetti_m19.png b/src/qt/assets/systemicons/olivetti_m19.png new file mode 100644 index 000000000..766e335f7 Binary files /dev/null and b/src/qt/assets/systemicons/olivetti_m19.png differ diff --git a/src/qt/assets/systemicons/olivetti_m21.png b/src/qt/assets/systemicons/olivetti_m21.png new file mode 100644 index 000000000..5e0ee4b52 Binary files /dev/null and b/src/qt/assets/systemicons/olivetti_m21.png differ diff --git a/src/qt/assets/systemicons/olivetti_m24.png b/src/qt/assets/systemicons/olivetti_m24.png new file mode 100644 index 000000000..e48356c32 Binary files /dev/null and b/src/qt/assets/systemicons/olivetti_m24.png differ diff --git a/src/qt/assets/systemicons/olivetti_m24sp.png b/src/qt/assets/systemicons/olivetti_m24sp.png new file mode 100644 index 000000000..e48356c32 Binary files /dev/null and b/src/qt/assets/systemicons/olivetti_m24sp.png differ diff --git a/src/qt/assets/systemicons/os_archlinux_x2.png b/src/qt/assets/systemicons/os_archlinux_x2.png new file mode 100644 index 000000000..1b218a76f Binary files /dev/null and b/src/qt/assets/systemicons/os_archlinux_x2.png differ diff --git a/src/qt/assets/systemicons/os_cloud_x2.png b/src/qt/assets/systemicons/os_cloud_x2.png new file mode 100644 index 000000000..9bd42f9ac Binary files /dev/null and b/src/qt/assets/systemicons/os_cloud_x2.png differ diff --git a/src/qt/assets/systemicons/os_debian_x2.png b/src/qt/assets/systemicons/os_debian_x2.png new file mode 100644 index 000000000..a6644c33d Binary files /dev/null and b/src/qt/assets/systemicons/os_debian_x2.png differ diff --git a/src/qt/assets/systemicons/os_dos_x2.png b/src/qt/assets/systemicons/os_dos_x2.png new file mode 100644 index 000000000..b6df56ef5 Binary files /dev/null and b/src/qt/assets/systemicons/os_dos_x2.png differ diff --git a/src/qt/assets/systemicons/os_fedora_x2.png b/src/qt/assets/systemicons/os_fedora_x2.png new file mode 100644 index 000000000..120d247a8 Binary files /dev/null and b/src/qt/assets/systemicons/os_fedora_x2.png differ diff --git a/src/qt/assets/systemicons/os_freebsd_x2.png b/src/qt/assets/systemicons/os_freebsd_x2.png new file mode 100644 index 000000000..dd6f88d41 Binary files /dev/null and b/src/qt/assets/systemicons/os_freebsd_x2.png differ diff --git a/src/qt/assets/systemicons/os_gentoo_x2.png b/src/qt/assets/systemicons/os_gentoo_x2.png new file mode 100644 index 000000000..f27afa5fc Binary files /dev/null and b/src/qt/assets/systemicons/os_gentoo_x2.png differ diff --git a/src/qt/assets/systemicons/os_jrockitve_x2.png b/src/qt/assets/systemicons/os_jrockitve_x2.png new file mode 100644 index 000000000..4283cad5e Binary files /dev/null and b/src/qt/assets/systemicons/os_jrockitve_x2.png differ diff --git a/src/qt/assets/systemicons/os_l4_x2.png b/src/qt/assets/systemicons/os_l4_x2.png new file mode 100644 index 000000000..9fbbaa183 Binary files /dev/null and b/src/qt/assets/systemicons/os_l4_x2.png differ diff --git a/src/qt/assets/systemicons/os_linux22_x2.png b/src/qt/assets/systemicons/os_linux22_x2.png new file mode 100644 index 000000000..0de56653a Binary files /dev/null and b/src/qt/assets/systemicons/os_linux22_x2.png differ diff --git a/src/qt/assets/systemicons/os_linux24_x2.png b/src/qt/assets/systemicons/os_linux24_x2.png new file mode 100644 index 000000000..b3df83b7f Binary files /dev/null and b/src/qt/assets/systemicons/os_linux24_x2.png differ diff --git a/src/qt/assets/systemicons/os_linux26_x2.png b/src/qt/assets/systemicons/os_linux26_x2.png new file mode 100644 index 000000000..8fdf52df7 Binary files /dev/null and b/src/qt/assets/systemicons/os_linux26_x2.png differ diff --git a/src/qt/assets/systemicons/os_linux_x2.png b/src/qt/assets/systemicons/os_linux_x2.png new file mode 100644 index 000000000..d4c2eeebe Binary files /dev/null and b/src/qt/assets/systemicons/os_linux_x2.png differ diff --git a/src/qt/assets/systemicons/os_macosx_x2.png b/src/qt/assets/systemicons/os_macosx_x2.png new file mode 100644 index 000000000..38eb2e5a7 Binary files /dev/null and b/src/qt/assets/systemicons/os_macosx_x2.png differ diff --git a/src/qt/assets/systemicons/os_mandriva_x2.png b/src/qt/assets/systemicons/os_mandriva_x2.png new file mode 100644 index 000000000..a09e9c170 Binary files /dev/null and b/src/qt/assets/systemicons/os_mandriva_x2.png differ diff --git a/src/qt/assets/systemicons/os_netbsd_x2.png b/src/qt/assets/systemicons/os_netbsd_x2.png new file mode 100644 index 000000000..9fa38a7a6 Binary files /dev/null and b/src/qt/assets/systemicons/os_netbsd_x2.png differ diff --git a/src/qt/assets/systemicons/os_netware_x2.png b/src/qt/assets/systemicons/os_netware_x2.png new file mode 100644 index 000000000..4bcc96521 Binary files /dev/null and b/src/qt/assets/systemicons/os_netware_x2.png differ diff --git a/src/qt/assets/systemicons/os_openbsd_x2.png b/src/qt/assets/systemicons/os_openbsd_x2.png new file mode 100644 index 000000000..13ae3a0db Binary files /dev/null and b/src/qt/assets/systemicons/os_openbsd_x2.png differ diff --git a/src/qt/assets/systemicons/os_opensuse_x2.png b/src/qt/assets/systemicons/os_opensuse_x2.png new file mode 100644 index 000000000..4d1696d9b Binary files /dev/null and b/src/qt/assets/systemicons/os_opensuse_x2.png differ diff --git a/src/qt/assets/systemicons/os_oracle_x2.png b/src/qt/assets/systemicons/os_oracle_x2.png new file mode 100644 index 000000000..545e885cb Binary files /dev/null and b/src/qt/assets/systemicons/os_oracle_x2.png differ diff --git a/src/qt/assets/systemicons/os_oraclesolaris_x2.png b/src/qt/assets/systemicons/os_oraclesolaris_x2.png new file mode 100644 index 000000000..8db66abe3 Binary files /dev/null and b/src/qt/assets/systemicons/os_oraclesolaris_x2.png differ diff --git a/src/qt/assets/systemicons/os_os2_other_x2.png b/src/qt/assets/systemicons/os_os2_other_x2.png new file mode 100644 index 000000000..2d37846a0 Binary files /dev/null and b/src/qt/assets/systemicons/os_os2_other_x2.png differ diff --git a/src/qt/assets/systemicons/os_os2ecs_x2.png b/src/qt/assets/systemicons/os_os2ecs_x2.png new file mode 100644 index 000000000..261f9f56c Binary files /dev/null and b/src/qt/assets/systemicons/os_os2ecs_x2.png differ diff --git a/src/qt/assets/systemicons/os_os2warp3_x2.png b/src/qt/assets/systemicons/os_os2warp3_x2.png new file mode 100644 index 000000000..1be5a696c Binary files /dev/null and b/src/qt/assets/systemicons/os_os2warp3_x2.png differ diff --git a/src/qt/assets/systemicons/os_os2warp45_x2.png b/src/qt/assets/systemicons/os_os2warp45_x2.png new file mode 100644 index 000000000..d1a4df0c2 Binary files /dev/null and b/src/qt/assets/systemicons/os_os2warp45_x2.png differ diff --git a/src/qt/assets/systemicons/os_os2warp4_x2.png b/src/qt/assets/systemicons/os_os2warp4_x2.png new file mode 100644 index 000000000..0a81c8759 Binary files /dev/null and b/src/qt/assets/systemicons/os_os2warp4_x2.png differ diff --git a/src/qt/assets/systemicons/os_other_x2.png b/src/qt/assets/systemicons/os_other_x2.png new file mode 100644 index 000000000..9602353c0 Binary files /dev/null and b/src/qt/assets/systemicons/os_other_x2.png differ diff --git a/src/qt/assets/systemicons/os_qnx_x2.png b/src/qt/assets/systemicons/os_qnx_x2.png new file mode 100644 index 000000000..b124cfa6e Binary files /dev/null and b/src/qt/assets/systemicons/os_qnx_x2.png differ diff --git a/src/qt/assets/systemicons/os_redhat_x2.png b/src/qt/assets/systemicons/os_redhat_x2.png new file mode 100644 index 000000000..a756e8aac Binary files /dev/null and b/src/qt/assets/systemicons/os_redhat_x2.png differ diff --git a/src/qt/assets/systemicons/os_solaris_x2.png b/src/qt/assets/systemicons/os_solaris_x2.png new file mode 100644 index 000000000..2dfd2ac74 Binary files /dev/null and b/src/qt/assets/systemicons/os_solaris_x2.png differ diff --git a/src/qt/assets/systemicons/os_turbolinux_x2.png b/src/qt/assets/systemicons/os_turbolinux_x2.png new file mode 100644 index 000000000..4d5ee4ab1 Binary files /dev/null and b/src/qt/assets/systemicons/os_turbolinux_x2.png differ diff --git a/src/qt/assets/systemicons/os_ubuntu_x2.png b/src/qt/assets/systemicons/os_ubuntu_x2.png new file mode 100644 index 000000000..9b6302b37 Binary files /dev/null and b/src/qt/assets/systemicons/os_ubuntu_x2.png differ diff --git a/src/qt/assets/systemicons/os_win10_x2.png b/src/qt/assets/systemicons/os_win10_x2.png new file mode 100644 index 000000000..1d6e81392 Binary files /dev/null and b/src/qt/assets/systemicons/os_win10_x2.png differ diff --git a/src/qt/assets/systemicons/os_win2k3_x2.png b/src/qt/assets/systemicons/os_win2k3_x2.png new file mode 100644 index 000000000..45bec005d Binary files /dev/null and b/src/qt/assets/systemicons/os_win2k3_x2.png differ diff --git a/src/qt/assets/systemicons/os_win2k8_x2.png b/src/qt/assets/systemicons/os_win2k8_x2.png new file mode 100644 index 000000000..d97a2789d Binary files /dev/null and b/src/qt/assets/systemicons/os_win2k8_x2.png differ diff --git a/src/qt/assets/systemicons/os_win2k_x2.png b/src/qt/assets/systemicons/os_win2k_x2.png new file mode 100644 index 000000000..a773288ed Binary files /dev/null and b/src/qt/assets/systemicons/os_win2k_x2.png differ diff --git a/src/qt/assets/systemicons/os_win31_x2.png b/src/qt/assets/systemicons/os_win31_x2.png new file mode 100644 index 000000000..5ca2005d6 Binary files /dev/null and b/src/qt/assets/systemicons/os_win31_x2.png differ diff --git a/src/qt/assets/systemicons/os_win7_x2.png b/src/qt/assets/systemicons/os_win7_x2.png new file mode 100644 index 000000000..c8ce57a2b Binary files /dev/null and b/src/qt/assets/systemicons/os_win7_x2.png differ diff --git a/src/qt/assets/systemicons/os_win81_x2.png b/src/qt/assets/systemicons/os_win81_x2.png new file mode 100644 index 000000000..07131ed6c Binary files /dev/null and b/src/qt/assets/systemicons/os_win81_x2.png differ diff --git a/src/qt/assets/systemicons/os_win8_x2.png b/src/qt/assets/systemicons/os_win8_x2.png new file mode 100644 index 000000000..ff94c2dd1 Binary files /dev/null and b/src/qt/assets/systemicons/os_win8_x2.png differ diff --git a/src/qt/assets/systemicons/os_win95_x2.png b/src/qt/assets/systemicons/os_win95_x2.png new file mode 100644 index 000000000..efd62799e Binary files /dev/null and b/src/qt/assets/systemicons/os_win95_x2.png differ diff --git a/src/qt/assets/systemicons/os_win98_x2.png b/src/qt/assets/systemicons/os_win98_x2.png new file mode 100644 index 000000000..c8fa4e7bb Binary files /dev/null and b/src/qt/assets/systemicons/os_win98_x2.png differ diff --git a/src/qt/assets/systemicons/os_win_other_x2.png b/src/qt/assets/systemicons/os_win_other_x2.png new file mode 100644 index 000000000..fac95889f Binary files /dev/null and b/src/qt/assets/systemicons/os_win_other_x2.png differ diff --git a/src/qt/assets/systemicons/os_winme_x2.png b/src/qt/assets/systemicons/os_winme_x2.png new file mode 100644 index 000000000..1c268c84b Binary files /dev/null and b/src/qt/assets/systemicons/os_winme_x2.png differ diff --git a/src/qt/assets/systemicons/os_winnt4_x2.png b/src/qt/assets/systemicons/os_winnt4_x2.png new file mode 100644 index 000000000..c3352abcd Binary files /dev/null and b/src/qt/assets/systemicons/os_winnt4_x2.png differ diff --git a/src/qt/assets/systemicons/os_winvista_x2.png b/src/qt/assets/systemicons/os_winvista_x2.png new file mode 100644 index 000000000..7b633b79f Binary files /dev/null and b/src/qt/assets/systemicons/os_winvista_x2.png differ diff --git a/src/qt/assets/systemicons/os_winxp_x2.png b/src/qt/assets/systemicons/os_winxp_x2.png new file mode 100644 index 000000000..b97e6b51d Binary files /dev/null and b/src/qt/assets/systemicons/os_winxp_x2.png differ diff --git a/src/qt/assets/systemicons/os_xandros_x2.png b/src/qt/assets/systemicons/os_xandros_x2.png new file mode 100644 index 000000000..23de5ca91 Binary files /dev/null and b/src/qt/assets/systemicons/os_xandros_x2.png differ diff --git a/src/qt/assets/systemicons/pb_bora_pro.png b/src/qt/assets/systemicons/pb_bora_pro.png new file mode 100644 index 000000000..6aaaf9d26 Binary files /dev/null and b/src/qt/assets/systemicons/pb_bora_pro.png differ diff --git a/src/qt/assets/systemicons/pb_pb410.png b/src/qt/assets/systemicons/pb_pb410.png new file mode 100644 index 000000000..c78c0496b Binary files /dev/null and b/src/qt/assets/systemicons/pb_pb410.png differ diff --git a/src/qt/assets/systemicons/pb_pb640.png b/src/qt/assets/systemicons/pb_pb640.png new file mode 100644 index 000000000..d0be550f1 Binary files /dev/null and b/src/qt/assets/systemicons/pb_pb640.png differ diff --git a/src/qt/assets/systemicons/pb_pb680.png b/src/qt/assets/systemicons/pb_pb680.png new file mode 100644 index 000000000..a697d5dd0 Binary files /dev/null and b/src/qt/assets/systemicons/pb_pb680.png differ diff --git a/src/qt/assets/systemicons/tandy_1000.png b/src/qt/assets/systemicons/tandy_1000.png new file mode 100644 index 000000000..b50f2c1e6 Binary files /dev/null and b/src/qt/assets/systemicons/tandy_1000.png differ diff --git a/src/qt/assets/systemicons/tandy_1000_hx.png b/src/qt/assets/systemicons/tandy_1000_hx.png new file mode 100644 index 000000000..b770a6b7c Binary files /dev/null and b/src/qt/assets/systemicons/tandy_1000_hx.png differ diff --git a/src/qt/assets/systemicons/tandy_1000_sl2.png b/src/qt/assets/systemicons/tandy_1000_sl2.png new file mode 100644 index 000000000..b7feae92a Binary files /dev/null and b/src/qt/assets/systemicons/tandy_1000_sl2.png differ diff --git a/src/qt/assets/systemicons/toshiba_t1000.png b/src/qt/assets/systemicons/toshiba_t1000.png new file mode 100644 index 000000000..a2dbfc382 Binary files /dev/null and b/src/qt/assets/systemicons/toshiba_t1000.png differ diff --git a/src/qt/assets/systemicons/toshiba_t1200.png b/src/qt/assets/systemicons/toshiba_t1200.png new file mode 100644 index 000000000..8c3cf0c37 Binary files /dev/null and b/src/qt/assets/systemicons/toshiba_t1200.png differ diff --git a/src/qt/assets/systemicons/toshiba_t1200_hdd.png b/src/qt/assets/systemicons/toshiba_t1200_hdd.png new file mode 100644 index 000000000..8c3cf0c37 Binary files /dev/null and b/src/qt/assets/systemicons/toshiba_t1200_hdd.png differ diff --git a/src/qt/icons/green-square-16.png b/src/qt/icons/green-square-16.png new file mode 100644 index 000000000..cb42c38eb Binary files /dev/null and b/src/qt/icons/green-square-16.png differ diff --git a/src/qt/icons/pause-16.png b/src/qt/icons/pause-16.png new file mode 100644 index 000000000..375780232 Binary files /dev/null and b/src/qt/icons/pause-16.png differ diff --git a/src/qt/icons/play-16.png b/src/qt/icons/play-16.png new file mode 100644 index 000000000..bde40f503 Binary files /dev/null and b/src/qt/icons/play-16.png differ diff --git a/src/qt/icons/red-power-16.png b/src/qt/icons/red-power-16.png new file mode 100644 index 000000000..c90ef491f Binary files /dev/null and b/src/qt/icons/red-power-16.png differ diff --git a/src/qt/icons/red-square-16.png b/src/qt/icons/red-square-16.png new file mode 100644 index 000000000..32faa7cdc Binary files /dev/null and b/src/qt/icons/red-square-16.png differ diff --git a/src/qt/icons/stop-16.png b/src/qt/icons/stop-16.png new file mode 100644 index 000000000..d73cad140 Binary files /dev/null and b/src/qt/icons/stop-16.png differ diff --git a/src/qt/icons/yellow-square-16.png b/src/qt/icons/yellow-square-16.png new file mode 100644 index 000000000..197cf394e Binary files /dev/null and b/src/qt/icons/yellow-square-16.png differ diff --git a/src/qt/qt_downloader.cpp b/src/qt/qt_downloader.cpp new file mode 100644 index 000000000..329aeeafb --- /dev/null +++ b/src/qt/qt_downloader.cpp @@ -0,0 +1,95 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Downloader module + * + * + * + * Authors: cold-brewed + * + * Copyright 2024 cold-brewed + */ + +#include +#include +#include +#include + +#include "qt_downloader.hpp" + +extern "C" { +#include <86box/plat.h> +} + +Downloader:: +Downloader(const DownloadLocation downloadLocation, QObject *parent) + : QObject(parent) + , file(nullptr) + , reply(nullptr) + , variantData(QVariant::Invalid) +{ + char PATHBUF[256]; + switch (downloadLocation) { + case DownloadLocation::Data: + plat_get_global_data_dir(PATHBUF, 255); + break; + case DownloadLocation::Config: + plat_get_global_config_dir(PATHBUF, 255); + break; + case DownloadLocation::Temp: + plat_get_temp_dir(PATHBUF, 255); + break; + } + downloadDirectory = QDir(PATHBUF); +} + +Downloader::~Downloader() { delete file; } + +void Downloader::download(const QUrl &url, const QString &filepath, const QVariant &varData) { + + variantData = varData; + // temporary until I get the plat stuff fixed + // const auto global_dir = temporaryGetGlobalDataDir(); + // qDebug() << "I was passed filepath " << filepath; + // Join with filename to create final file + // const auto final_path = QDir(global_dir).filePath(filepath); + const auto final_path = downloadDirectory.filePath(filepath); + + file = new QFile(final_path); + if(!file->open(QIODevice::WriteOnly)) { + qWarning() << "Unable to open file " << final_path; + return; + } + + const auto nam = new QNetworkAccessManager(this); + // Create the network request and execute + const auto request = QNetworkRequest(url); + reply = nam->get(request); + // Connect to the finished signal + connect(reply, &QNetworkReply::finished, this, &Downloader::onResult); +} + +void +Downloader::onResult() +{ + if (reply->error()) { + qWarning() << "Error returned from QNetworkRequest: " << reply->errorString(); + emit errorOccurred(reply->errorString()); + reply->deleteLater(); + return; + } + + file->write(reply->readAll()); + file->flush(); + file->close(); + + reply->deleteLater(); + qDebug() << Q_FUNC_INFO << "Downloaded complete: file written to " << file->fileName(); + emit downloadCompleted(file->fileName(), variantData); +} + diff --git a/src/qt/qt_downloader.hpp b/src/qt/qt_downloader.hpp new file mode 100644 index 000000000..0bd3f1fee --- /dev/null +++ b/src/qt/qt_downloader.hpp @@ -0,0 +1,57 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Header for the downloader module + * + * + * + * Authors: cold-brewed + * + * Copyright 2024 cold-brewed + */ + +#ifndef QT_DOWNLOADER_HPP +#define QT_DOWNLOADER_HPP + +#include +#include +#include +#include + + +class Downloader : public QObject { + Q_OBJECT +public: + enum class DownloadLocation { + Data, // AppDataLocation via plat_get_global_data_dir() + Config, // AppConfigLocation via plat_get_global_config_dir() + Temp // TempLocation via plat_get_temp_dir() + }; + explicit Downloader(DownloadLocation downloadLocation = DownloadLocation::Data, QObject *parent = nullptr); + ~Downloader() final; + + void download(const QUrl &url, const QString &filepath, const QVariant &varData = QVariant::Invalid); + +signals: + // Signal emitted when the download is successful + void downloadCompleted(QString filename, QVariant varData); + // Signal emitted when an error occurs + void errorOccurred(const QString&); + +private slots: + void onResult(); + +private: + QFile *file; + QNetworkAccessManager nam; + QNetworkReply *reply; + QVariant variantData; + QDir downloadDirectory; +}; + +#endif diff --git a/src/qt/qt_main.cpp b/src/qt/qt_main.cpp index 3f8476374..4d8a69cc1 100644 --- a/src/qt/qt_main.cpp +++ b/src/qt/qt_main.cpp @@ -77,6 +77,8 @@ extern "C" { #include "qt_styleoverride.hpp" #include "qt_unixmanagerfilter.hpp" #include "qt_util.hpp" +#include "qt_vmmanager_clientsocket.hpp" +#include "qt_vmmanager_mainwindow.hpp" // Void Cast #define VC(x) const_cast(x) @@ -662,6 +664,19 @@ main(int argc, char *argv[]) return 0; } + if (vmm_enabled) { + // VMManagerMain vmm; + // // Hackish until there is a proper solution + // QApplication::setApplicationName("86Box VM Manager"); + // QApplication::setApplicationDisplayName("86Box VM Manager"); + // vmm.show(); + // vmm.exec(); + const auto vmm_main_window = new VMManagerMainWindow(); + vmm_main_window->show(); + QApplication::exec(); + return 0; + } + #ifdef DISCORD discord_load(); #endif @@ -758,6 +773,36 @@ main(int argc, char *argv[]) socket.connectToServer(qgetenv("86BOX_MANAGER_SOCKET")); } + VMManagerClientSocket manager_socket; + if (qgetenv("VMM_86BOX_SOCKET").size()) { + manager_socket.IPCConnect(qgetenv("VMM_86BOX_SOCKET")); + QObject::connect(&manager_socket, &VMManagerClientSocket::pause, main_window, &MainWindow::togglePause); + QObject::connect(&manager_socket, &VMManagerClientSocket::resetVM, main_window, &MainWindow::hardReset); + QObject::connect(&manager_socket, &VMManagerClientSocket::showsettings, main_window, &MainWindow::showSettings); + QObject::connect(&manager_socket, &VMManagerClientSocket::ctrlaltdel, []() { pc_send_cad(); }); + QObject::connect(&manager_socket, &VMManagerClientSocket::request_shutdown, main_window, &MainWindow::close); + QObject::connect(&manager_socket, &VMManagerClientSocket::force_shutdown, []() { + do_stop(); + emit main_window->close(); + }); + QObject::connect(main_window, &MainWindow::vmmRunningStateChanged, &manager_socket, &VMManagerClientSocket::clientRunningStateChanged); + main_window->installEventFilter(&manager_socket); + } + + /* Warn the user about unsupported configs */ + if (cpu_override) { + QMessageBox warningbox(QMessageBox::Icon::Warning, QObject::tr("You are loading an unsupported configuration"), + QObject::tr("CPU type filtering based on selected machine is disabled for this emulated machine.\n\nThis makes it possible to choose a CPU that is otherwise incompatible with the selected machine. However, you may run into incompatibilities with the machine BIOS or other software.\n\nEnabling this setting is not officially supported and any bug reports filed may be closed as invalid."), + QMessageBox::NoButton, main_window); + warningbox.addButton(QObject::tr("Continue"), QMessageBox::AcceptRole); + warningbox.addButton(QObject::tr("Exit"), QMessageBox::RejectRole); + warningbox.exec(); + if (warningbox.result() == QDialog::Accepted) { + confirm_exit_cmdl = 0; /* skip the confirmation prompt without touching the config */ + emit main_window->close(); + } + } + // pc_reset_hard_init(); QTimer onesec; diff --git a/src/qt/qt_mainwindow.cpp b/src/qt/qt_mainwindow.cpp index 4594ee5be..5c9e10a85 100644 --- a/src/qt/qt_mainwindow.cpp +++ b/src/qt/qt_mainwindow.cpp @@ -2131,6 +2131,7 @@ MainWindow::updateUiPauseState() QString(tr("Pause execution")); ui->actionPause->setIcon(pause_icon); ui->actionPause->setToolTip(tooltip_text); + emit vmmRunningStateChanged(static_cast(dopause)); } void diff --git a/src/qt/qt_mainwindow.hpp b/src/qt/qt_mainwindow.hpp index 917fd43f5..54a04a975 100644 --- a/src/qt/qt_mainwindow.hpp +++ b/src/qt/qt_mainwindow.hpp @@ -13,6 +13,8 @@ #include #include +#include "qt_vmmanager_protocol.hpp" + class MediaMenu; class RendererStack; @@ -63,6 +65,8 @@ signals: void showMessageForNonQtThread(int flags, const QString &header, const QString &message, bool richText, std::atomic_bool* done); void getTitleForNonQtThread(wchar_t *title); + + void vmmRunningStateChanged(VMManagerProtocol::RunningState state); public slots: void showSettings(); void hardReset(); diff --git a/src/qt/qt_updatecheck.cpp b/src/qt/qt_updatecheck.cpp new file mode 100644 index 000000000..83e2b34d9 --- /dev/null +++ b/src/qt/qt_updatecheck.cpp @@ -0,0 +1,360 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Update check module + * + * + * + * Authors: cold-brewed + * + * Copyright 2024 cold-brewed + */ + +#include +#include +#include +#include +#include + +#include "qt_updatecheck.hpp" +#include "qt_downloader.hpp" +#include "qt_updatedetails.hpp" + +extern "C" { +#include <86box/version.h> +} + +UpdateCheck:: +UpdateCheck(const UpdateChannel channel, QObject *parent) : QObject(parent) +{ + updateChannel = channel; + currentVersion = getCurrentVersion(channel); +} + +UpdateCheck::~ +UpdateCheck() + = default; + +void +UpdateCheck::checkForUpdates() +{ + if (updateChannel == UpdateChannel::Stable) { + const auto githubDownloader = new Downloader(Downloader::DownloadLocation::Temp); + connect(githubDownloader, &Downloader::downloadCompleted, this, &UpdateCheck::githubDownloadComplete); + connect(githubDownloader, &Downloader::errorOccurred, this, &UpdateCheck::generalDownloadError); + githubDownloader->download(QUrl(githubReleaseApi), "github_releases.json"); + } else { + const auto jenkinsDownloader = new Downloader(Downloader::DownloadLocation::Temp); + connect(jenkinsDownloader, &Downloader::downloadCompleted, this, &UpdateCheck::jenkinsDownloadComplete); + connect(jenkinsDownloader, &Downloader::errorOccurred, this, &UpdateCheck::generalDownloadError); + jenkinsDownloader->download(jenkinsLatestNReleasesUrl(10), "jenkins_list.json"); + } +} + +void +UpdateCheck::jenkinsDownloadComplete(const QString &filename, const QVariant &varData) +{ + auto generalError = tr("Unable to determine release information"); + auto jenkinsReleaseListResult = parseJenkinsJson(filename); + auto latestVersion = 0; // NOLINT (Default value as a fallback) + + if(!jenkinsReleaseListResult.has_value() || jenkinsReleaseListResult.value().isEmpty()) { + generalDownloadError(generalError); + return; + } + const auto jenkinsReleaseList = jenkinsReleaseListResult.value(); + latestVersion = jenkinsReleaseListResult->first().buildNumber; + + // If we can't determine the local build (blank current version), always show an update as available. + // Callers can adjust accordingly. + // Otherwise, do a comparison with EMU_BUILD_NUM + bool updateAvailable = false; + bool upToDate = true; + if(currentVersion.isEmpty() || EMU_BUILD_NUM < latestVersion) { + updateAvailable = true; + upToDate = false; + } + + const auto updateResult = UpdateResult { + .channel = updateChannel, + .updateAvailable = updateAvailable, + .upToDate = upToDate, + .currentVersion = currentVersion, + .latestVersion = QString::number(latestVersion), + .githubInfo = {}, + .jenkinsInfo = jenkinsReleaseList, + }; + + emit updateCheckComplete(updateResult); +} + +void +UpdateCheck::generalDownloadError(const QString &error) +{ + emit updateCheckError(error); +} + +void +UpdateCheck::githubDownloadComplete(const QString &filename, const QVariant &varData) +{ + const auto generalError = tr("Unable to determine release information"); + const auto githubReleaseListResult = parseGithubJson(filename); + QString latestVersion = "0.0"; + if(!githubReleaseListResult.has_value() || githubReleaseListResult.value().isEmpty()) { + generalDownloadError(generalError); + } + auto githubReleaseList = githubReleaseListResult.value(); + // Warning: this check (using the tag name) relies on a consistent naming scheme: "v" + // where is the release number. For example, 4.2 from v4.2 as the tag name. + // Another option would be parsing the name field which is generally "86Box " but + // either option requires a consistent naming scheme. + latestVersion = githubReleaseList.first().tag_name.replace("v", ""); + for (const auto &release: githubReleaseList) { + qDebug().noquote().nospace() << release.name << ": " << release.html_url << " (" << release.created_at << ")"; + } + + // const auto updateDetails = new UpdateDetails(githubReleaseList, currentVersion); + bool updateAvailable = false; + bool upToDate = true; + if(currentVersion.isEmpty() || (versionCompare(currentVersion, latestVersion) < 0)) { + updateAvailable = true; + upToDate = false; + } + + const auto updateResult = UpdateResult { + .channel = updateChannel, + .updateAvailable = updateAvailable, + .upToDate = upToDate, + .currentVersion = currentVersion, + .latestVersion = latestVersion, + .githubInfo = githubReleaseList, + .jenkinsInfo = {}, + }; + + emit updateCheckComplete(updateResult); + +} + +QUrl +UpdateCheck::jenkinsLatestNReleasesUrl(const int &count) +{ + const auto urlPath = QString("https://ci.86box.net/job/86box/api/json?tree=builds[number,result,timestamp,changeSets[items[commitId,affectedPaths,author[fullName],msg,id]]]{0,%1}").arg(count); + return { urlPath }; +} + +QString +UpdateCheck::getCurrentVersion(const UpdateChannel &updateChannel) +{ + if (updateChannel == UpdateChannel::Stable) { + return {EMU_VERSION}; + } + // If EMU_BUILD_NUM is anything other than the default of zero it was set by the build process + if constexpr (EMU_BUILD_NUM != 0) { + return QString::number(EMU_BUILD_NUM); // NOLINT because EMU_BUILD_NUM is defined as 0 by default and is set at build time + } + // EMU_BUILD_NUM is not set, most likely a local build + return {}; // NOLINT (Having EMU_BUILD_NUM assigned to a default number throws off the linter) +} + +std::optional> +UpdateCheck::parseJenkinsJson(const QString &filename) +{ + QList releaseInfoList; + QFile json_file(filename); + if (!json_file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning() << "Couldn't open the json file: error" << json_file.error(); + return std::nullopt; + } + + const QString read_file = json_file.readAll(); + json_file.close(); + + const auto json_doc = QJsonDocument::fromJson(read_file.toUtf8()); + + if (json_doc.isNull()) { + qWarning("Failed to create QJsonDocument, possibly invalid JSON"); + return std::nullopt; + } + + if (!json_doc.isObject()) { + qWarning("JSON does not have the expected format (object in root), cannot continue"); + return std::nullopt; + } + + auto json_object = json_doc.object(); + + // The json contains multiple release + if(json_object.contains("builds") && json_object["builds"].isArray()) { + + QJsonArray builds = json_object["builds"].toArray(); + for (const auto &each_build: builds) { + if (auto build = parseJenkinsRelease(each_build.toObject()); build.has_value() && build.value().result == "SUCCESS") { + releaseInfoList.append(build.value()); + } + } + } else if(json_object.contains("changeSets") && json_object["changeSets"].isArray()) { + // The json contains only one release, as obtained by the lastSuccessfulBuild api + if (const auto build = parseJenkinsRelease(json_object); build.has_value()) { + releaseInfoList.append(build.value()); + } + } else { + qWarning("JSON is missing data or has invalid data, cannot continue"); + qDebug() << json_object; + return std::nullopt; + } + + return releaseInfoList; +} + +std::optional +UpdateCheck::parseJenkinsRelease(const QJsonObject &json) +{ + // The root should contain number, result, and timestamp. + if (!json.contains("number") || !json.contains("result") || !json.contains("timestamp")) { + return std::nullopt; + } + + auto releaseInfo = JenkinsReleaseInfo { + .buildNumber = json["number"].toInt(), + .result = json["result"].toString(), + .timestamp = static_cast(json["timestamp"].toDouble()) + }; + + // Overview + // Each build should contain a changeSets object with an array. Only the first element is needed. + // The first element should be an object containing an items object with an array. + // Each array element in the items object has information releated to the build. More or less: commit data + // In jq parlance it would be similar to `builds[].changeSets[0].items[]` + + // To break down the somewhat complicated if-init statement below: + // * Get the object for `changeSets` + // * Convert the value to array + // * Grab the first element in the array + // Proceed if + // * the element (first in changeSets) is an object that contains the key `items` + if (const auto changeSet = json["changeSets"].toArray().first(); changeSet.isObject() && changeSet.toObject().contains("items")) { + // Then proceed to process each `items` array element + for (const auto &item : changeSet.toObject()["items"].toArray()) { + auto itemObject = item.toObject(); + // Basic validation + if (!itemObject.contains("commitId") || !itemObject.contains("msg") || !itemObject.contains("affectedPaths")) { + return std::nullopt; + } + // Convert the paths for each commit to a string list + QStringList paths; + for (const auto &each_path : itemObject["affectedPaths"].toArray().toVariantList()) { + if (each_path.type() == QVariant::String) { + paths.append(each_path.toString()); + } + } + // Build the structure + const auto releaseItem = JenkinsChangeSetItem { + .buildId = itemObject["commitId"].toString(), + .author = itemObject["author"].toObject()["fullName"].toString(), + .message = itemObject["msg"].toString(), + .affectedPaths = paths, + }; + releaseInfo.changeSetItems.append(releaseItem); + } + } else { + qWarning("Could not parse release information, possibly invalid JSON"); + } + return releaseInfo; +} + +std::optional> +UpdateCheck::parseGithubJson(const QString &filename) +{ + QList releaseInfoList; + QFile json_file(filename); + if (!json_file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning("Couldn't open the json file: error %d", json_file.error()); + return std::nullopt; + } + + const QString read_file = json_file.readAll(); + json_file.close(); + + const auto json_doc = QJsonDocument::fromJson(read_file.toUtf8()); + + if (json_doc.isNull()) { + qWarning("Failed to create QJsonDocument, possibly invalid JSON"); + return std::nullopt; + } + + if (!json_doc.isArray()) { + qWarning("JSON does not have the expected format (array in root), cannot continue"); + return std::nullopt; + } + + auto release_array = json_doc.array(); + + for (const auto &each_release: release_array) { + if (auto release = parseGithubRelease(each_release.toObject()); release.has_value()) { + releaseInfoList.append(release.value()); + } + } + return releaseInfoList; +} +std::optional +UpdateCheck::parseGithubRelease(const QJsonObject &json) +{ + // Perform some basic validation + if (!json.contains("name") || !json.contains("tag_name") || !json.contains("html_url")) { + return std::nullopt; + } + + auto githubRelease = GithubReleaseInfo { + .name = json["name"].toString(), + .tag_name = json["tag_name"].toString(), + .html_url = json["html_url"].toString(), + .target_commitish = json["target_commitish"].toString(), + .created_at = json["created_at"].toString(), + .published_at = json["published_at"].toString(), + .body = json["body"].toString(), + }; + + return githubRelease; +} + +// A simple method to compare version numbers +// Should work for comparing x.y.z and x.y. Missing +// values (parts) will be treated as zeroes +int +UpdateCheck::versionCompare(const QString &version1, const QString &version2) +{ + // Split both + QStringList v1List = version1.split('.'); + QStringList v2List = version2.split('.'); + + // Out of the two versions get the maximum amount of "parts" + const int maxParts = std::max(v1List.size(), v2List.size()); + + // Initialize both with zeros + QVector v1Parts(maxParts, 0); + QVector v2Parts(maxParts, 0); + + for (int i = 0; i < v1List.size(); ++i) { + v1Parts[i] = v1List[i].toInt(); + } + + for (int i = 0; i < v2List.size(); ++i) { + v2Parts[i] = v2List[i].toInt(); + } + + for (int i = 0; i < maxParts; ++i) { + // First version is greater + if (v1Parts[i] > v2Parts[i]) + return 1; + // First version is less + if (v1Parts[i] < v2Parts[i]) + return -1; + } + // They are equal + return 0; +} diff --git a/src/qt/qt_updatecheck.hpp b/src/qt/qt_updatecheck.hpp new file mode 100644 index 000000000..2c04993fc --- /dev/null +++ b/src/qt/qt_updatecheck.hpp @@ -0,0 +1,104 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Header for the update check module + * + * + * + * Authors: cold-brewed + * + * Copyright 2024 cold-brewed + */ + +#ifndef QT_UPDATECHECK_HPP +#define QT_UPDATECHECK_HPP + +#include +#include +#include +#include + +#include + +class UpdateCheck final : public QObject { + Q_OBJECT +public: + enum class UpdateChannel { + Stable, + CI, + }; + + struct JenkinsChangeSetItem { + QString buildId; // sha hash + QString author; // github username + QString message; // commit message + QStringList affectedPaths; // list of files in the change + }; + + struct JenkinsReleaseInfo { + int buildNumber = 0; + QString result; + qint64 timestamp = 0; + QList changeSetItems; + }; + + struct GithubReleaseInfo { + QString name; + QString tag_name; + QString html_url; + QString target_commitish; + QString created_at; + QString published_at; + QString body; + }; + + struct UpdateResult { + UpdateChannel channel; + bool updateAvailable = false; + bool upToDate = false; + QString currentVersion; + QString latestVersion; + QList githubInfo; + QList jenkinsInfo; + }; + + explicit UpdateCheck(UpdateChannel channel, QObject *parent = nullptr); + ~UpdateCheck() override; + void checkForUpdates(); + static int versionCompare(const QString &version1, const QString &version2); + [[nodiscard]] static QString getCurrentVersion(const UpdateChannel &updateChannel = UpdateChannel::Stable); + +signals: + // void updateCheckComplete(const UpdateCheck::UpdateChannel &channel, const QVariant &updateData); + void updateCheckComplete(const UpdateCheck::UpdateResult &result); + void updateCheckError(const QString &errorMsg); + +private: + UpdateChannel updateChannel = UpdateChannel::Stable; + + const QUrl githubReleaseApi = QUrl("https://api.github.com/repos/86box/86Box/releases"); + const QUrl jenkinsLatestApi = QUrl("https://ci.86box.net/job/86box/lastSuccessfulBuild/api/json"); + QString jenkinsLatestVersion; + QString currentVersion; + + static QUrl jenkinsLatestNReleasesUrl(const int &count); + + static std::optional> parseJenkinsJson(const QString &filename); + static std::optional parseJenkinsRelease(const QJsonObject &json); + + static std::optional> parseGithubJson(const QString &filename); + static std::optional parseGithubRelease(const QJsonObject &json); + + +private slots: + void jenkinsDownloadComplete(const QString &filename, const QVariant& varData); + void githubDownloadComplete(const QString &filename, const QVariant& varData); + void generalDownloadError(const QString &error); +}; + +#endif // QT_UPDATECHECK_HPP diff --git a/src/qt/qt_updatecheckdialog.cpp b/src/qt/qt_updatecheckdialog.cpp new file mode 100644 index 000000000..d0cb6941d --- /dev/null +++ b/src/qt/qt_updatecheckdialog.cpp @@ -0,0 +1,90 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Update check dialog module + * + * + * + * Authors: cold-brewed + * + * Copyright 2024 cold-brewed + */ + +#include +#include + +#include "qt_updatecheckdialog.hpp" +#include "ui_qt_updatecheckdialog.h" +#include "qt_updatedetails.hpp" + +extern "C" { +#include <86box/version.h> +} + +UpdateCheckDialog:: +UpdateCheckDialog(const UpdateCheck::UpdateChannel channel, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateCheckDialog), updateCheck(new UpdateCheck(channel)) +{ + ui->setupUi(this); + setWindowTitle(tr("Update check")); + ui->statusLabel->setHidden(true); + updateChannel = channel; + currentVersion = UpdateCheck::getCurrentVersion(updateChannel); + connect(updateCheck, &UpdateCheck::updateCheckError, [=](const QString &errorMsg) { + generalDownloadError(errorMsg); + }); + connect(updateCheck, &UpdateCheck::updateCheckComplete, this, &UpdateCheckDialog::downloadComplete); + + QTimer::singleShot(0, [this] { + updateCheck->checkForUpdates(); + }); +} + +UpdateCheckDialog::~ +UpdateCheckDialog() + = default; + +void +UpdateCheckDialog::generalDownloadError(const QString &error) const +{ + ui->progressBar->setMaximum(100); + ui->progressBar->setValue(100); + ui->statusLabel->setVisible(true); + const auto statusText = tr("There was an error checking for updates:\n\n%1\n\nPlease try again later.").arg(error); + ui->statusLabel->setText(statusText); + ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok); +} + +void +UpdateCheckDialog::downloadComplete(const UpdateCheck::UpdateResult &result) +{ + if (result.upToDate) { + upToDate(); + return; + } + + const auto updateDetails = new UpdateDetails(result); + connect(updateDetails, &QDialog::accepted, [this] { + accept(); + }); + connect(updateDetails, &QDialog::rejected, [this] { + reject(); + }); + updateDetails->exec(); +} + +void +UpdateCheckDialog::upToDate() +{ + ui->titleLabel->setText(tr("Update check complete")); + ui->progressBar->setMaximum(100); + ui->progressBar->setValue(100); + ui->statusLabel->setVisible(true); + const auto statusText = tr("You are running the latest %1 version of 86Box: %2").arg(updateChannel == UpdateCheck::UpdateChannel::Stable ? "stable" : "beta", currentVersion); + ui->statusLabel->setText(statusText); + ui->buttonBox->setStandardButtons(QDialogButtonBox::Ok); +} diff --git a/src/qt/qt_updatecheckdialog.hpp b/src/qt/qt_updatecheckdialog.hpp new file mode 100644 index 000000000..0f1c0dfa8 --- /dev/null +++ b/src/qt/qt_updatecheckdialog.hpp @@ -0,0 +1,47 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Header for the update check dialog module + * + * + * + * Authors: cold-brewed + * + * Copyright 2024 cold-brewed + */ + +#ifndef QT_UPDATECHECKDIALOG_HPP +#define QT_UPDATECHECKDIALOG_HPP + +#include + +#include + +namespace Ui { +class UpdateCheckDialog; +} + +class UpdateCheckDialog final : public QDialog { + Q_OBJECT +public: + explicit UpdateCheckDialog(UpdateCheck::UpdateChannel channel, QWidget *parent = nullptr); + ~UpdateCheckDialog() override; + +private: + Ui::UpdateCheckDialog *ui; + UpdateCheck::UpdateChannel updateChannel = UpdateCheck::UpdateChannel::Stable; + UpdateCheck *updateCheck; + QString currentVersion; + void upToDate(); + +private slots: + void downloadComplete(const UpdateCheck::UpdateResult &result); + void generalDownloadError(const QString &error) const; +}; + +#endif // QT_UPDATECHECKDIALOG_HPP diff --git a/src/qt/qt_updatecheckdialog.ui b/src/qt/qt_updatecheckdialog.ui new file mode 100644 index 000000000..e50a541bb --- /dev/null +++ b/src/qt/qt_updatecheckdialog.ui @@ -0,0 +1,106 @@ + + + UpdateCheckDialog + + + + 0 + 0 + 350 + 134 + + + + + 0 + 0 + + + + Dialog + + + + + + + 0 + 0 + + + + Checking for updates.. + + + Qt::AlignCenter + + + + + + + 0 + + + -1 + + + + + + + Status text here + + + + + + + true + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + + + + + + buttonBox + accepted() + UpdateCheckDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + UpdateCheckDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/qt/qt_updatedetails.cpp b/src/qt/qt_updatedetails.cpp new file mode 100644 index 000000000..1328da732 --- /dev/null +++ b/src/qt/qt_updatedetails.cpp @@ -0,0 +1,111 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Update details module + * + * + * + * Authors: cold-brewed + * + * Copyright 2024 cold-brewed + */ + +#include "qt_updatedetails.hpp" +#include "ui_qt_updatedetails.h" + +#include +#include + + +UpdateDetails:: +UpdateDetails(const UpdateCheck::UpdateResult &updateResult, QWidget *parent) : ui(new Ui::UpdateDetails) +{ + ui->setupUi(this); + setWindowTitle("86Box Update"); + ui->updateTitle->setText("An update to 86Box is available!"); + QString currentVersionText; + QString releaseType = updateResult.channel == UpdateCheck::UpdateChannel::Stable ? tr("version") : tr("build"); + if(!updateResult.currentVersion.isEmpty()) { + currentVersionText = tr("You are currently running %1 %2. ").arg(releaseType, updateResult.currentVersion); + } + + const auto updateDetailsText = tr("%1 %2 is now available. %3Would you like to visit the download page?").arg(releaseType[0].toUpper() + releaseType.mid(1), updateResult.latestVersion, currentVersionText); + ui->updateDetails->setText(updateDetailsText); + + if(updateResult.channel == UpdateCheck::UpdateChannel::Stable) { + ui->updateText->setMarkdown(githubUpdateToMarkdown(updateResult.githubInfo)); + } else { + ui->updateText->setMarkdown(jenkinsUpdateToMarkdown(updateResult.jenkinsInfo)); + } + + const auto downloadButton = new QPushButton(tr("Visit download page")); + ui->buttonBox->addButton(downloadButton, QDialogButtonBox::AcceptRole); + // Override accepted to mean "I want to visit the download page" + connect(ui->buttonBox, &QDialogButtonBox::accepted, [this, updateResult] { + visitDownloadPage(updateResult.channel); + }); + const auto logo = QPixmap(":/assets/86box.png").scaled(QSize(64, 64), Qt::KeepAspectRatio, Qt::SmoothTransformation); + + ui->icon->setPixmap(logo); +} + +UpdateDetails::~ +UpdateDetails() + = default; + +QString +UpdateDetails::jenkinsUpdateToMarkdown(const QList &releaseInfoList) +{ + QStringList fullText; + for (const auto &update : releaseInfoList) { + fullText.append(QString("### Build %1").arg(update.buildNumber)); + fullText.append("Changes:"); + for (const auto &item : update.changeSetItems) { + fullText.append(QString("* %1").arg(item.message)); + } + fullText.append("\n\n\n---\n\n\n"); + } + // pop off the last hr + fullText.removeLast(); + // return fullText.join("\n\n---\n\n"); + return fullText.join("\n"); +} + +QString +UpdateDetails::githubUpdateToMarkdown(const QList &releaseInfoList) +{ + // The github release info can be rather large so we'll only + // display the most recent one + QList singleRelease; + if (!releaseInfoList.isEmpty()) { + singleRelease.append(releaseInfoList.first()); + } + QStringList fullText; + for (const auto &release : singleRelease) { + fullText.append(QString("#### %1").arg(release.name)); + // Github body text should already be in markdown and can just + // be placed here as-is + fullText.append(release.body); + fullText.append("\n\n\n---\n\n\n"); + } + // pop off the last hr + fullText.removeLast(); + return fullText.join("\n"); +} +void +UpdateDetails::visitDownloadPage(const UpdateCheck::UpdateChannel &channel) +{ + switch (channel) { + case UpdateCheck::UpdateChannel::Stable: + QDesktopServices::openUrl(QUrl("https://ci.86box.net/job/86Box/lastSuccessfulBuild/artifact/")); + break; + case UpdateCheck::UpdateChannel::CI: + QDesktopServices::openUrl(QUrl("https://github.com/86Box/86Box/releases/latest")); + break; + } +} diff --git a/src/qt/qt_updatedetails.hpp b/src/qt/qt_updatedetails.hpp new file mode 100644 index 000000000..8c3528e11 --- /dev/null +++ b/src/qt/qt_updatedetails.hpp @@ -0,0 +1,43 @@ +/* + * 86Box A hypervisor and IBM PC system emulator that specializes in + * running old operating systems and software designed for IBM + * PC systems and compatibles from 1981 through fairly recent + * system designs based on the PCI bus. + * + * This file is part of the 86Box distribution. + * + * Header for the update details module + * + * + * + * Authors: cold-brewed + * + * Copyright 2024 cold-brewed + */ + +#ifndef QT_UPDATEDETAILS_HPP +#define QT_UPDATEDETAILS_HPP + +#include +#include +#include "qt_updatecheck.hpp" + +namespace Ui { +class UpdateDetails; +} + +class UpdateDetails final : public QDialog { + Q_OBJECT +public: + explicit UpdateDetails(const UpdateCheck::UpdateResult &updateResult, QWidget *parent = nullptr); + ~UpdateDetails() override; +private: + Ui::UpdateDetails *ui; + static QString jenkinsUpdateToMarkdown(const QList &releaseInfoList); + static QString githubUpdateToMarkdown(const QList &releaseInfoList); +private slots: + static void visitDownloadPage(const UpdateCheck::UpdateChannel &channel); +}; + + +#endif // QT_UPDATEDETAILS_HPP diff --git a/src/qt/qt_updatedetails.ui b/src/qt/qt_updatedetails.ui new file mode 100644 index 000000000..7b9c0aa2a --- /dev/null +++ b/src/qt/qt_updatedetails.ui @@ -0,0 +1,192 @@ + + + UpdateDetails + + + + 0 + 0 + 700 + 500 + + + + + 600 + 400 + + + + Dialog + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 12 + + + 12 + + + 12 + + + 0 + + + + + + 0 + 0 + + + + + 32 + 0 + + + + + 64 + 16777215 + + + + + + + false + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Title + + + + + + + Details + + + true + + + + + + + Release notes: + + + + + + + true + + + true + + + false + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + + + + + + buttonBox + accepted() + UpdateDetails + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + UpdateDetails + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/qt/qt_vmmanager_addmachine.cpp b/src/qt/qt_vmmanager_addmachine.cpp new file mode 100644 index 000000000..db40972a1 --- /dev/null +++ b/src/qt/qt_vmmanager_addmachine.cpp @@ -0,0 +1,361 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager add machine wizard +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include "qt_vmmanager_addmachine.hpp" + +extern "C" { +#include <86box/86box.h> +} + +// Implementation note: There are several classes in this file: +// One for the main Wizard class and one for each page of the wizard + +VMManagerAddMachine:: +VMManagerAddMachine(QWidget *parent) : QWizard(parent) +{ + setPage(Page_Intro, new IntroPage); + setPage(Page_WithExistingConfig, new WithExistingConfigPage); + setPage(Page_NameAndLocation, new NameAndLocationPage); + setPage(Page_Conclusion, new ConclusionPage); + + // Need to create a better image + // QPixmap originalPixmap(":/assets/86box.png"); + // QPixmap scaledPixmap = originalPixmap.scaled(150, 150, Qt::KeepAspectRatio); + QPixmap wizardPixmap(":/assets/86box-wizard.png"); + +#ifndef Q_OS_MACOS + setWizardStyle(ModernStyle); + // setPixmap(LogoPixmap, scaledPixmap); + // setPixmap(LogoPixmap, wizardPixmap); + // setPixmap(WatermarkPixmap, scaledPixmap); + setPixmap(WatermarkPixmap, wizardPixmap); +#else + // macos + // setPixmap(BackgroundPixmap, scaledPixmap); + setPixmap(BackgroundPixmap, wizardPixmap); +#endif + + // Wizard wants to resize based on image. This keeps the size + setMinimumSize(size()); + setOption(HaveHelpButton, true); + // setPixmap(LogoPixmap, QPixmap(":/settings/qt/icons/86Box-gray.ico")); + + connect(this, &QWizard::helpRequested, this, &VMManagerAddMachine::showHelp); + + setWindowTitle(tr("Add new system wizard")); +} + +void +VMManagerAddMachine::showHelp() +{ + // TBD + static QString lastHelpMessage; + + QString message; + + // Help will depend on the current page + switch (currentId()) { + case Page_Intro: + message = tr("This is the into page."); + break; + default: + message = tr("No help has been added yet, you're on your own."); + break; + } + + if (lastHelpMessage == message) { + message = tr("Did you click help twice?"); + } + + QMessageBox::information(this, tr("Add new system wizard help"), message); + lastHelpMessage = message; +} + +IntroPage:: +IntroPage(QWidget *parent) +{ + setTitle(tr("Introduction")); + + setPixmap(QWizard::WatermarkPixmap, QPixmap(":/assets/qt/assets/86box.png")); + + topLabel = new QLabel(tr("This will help you add a new system to 86Box.")); + // topLabel = new QLabel(tr("This will help you add a new system to 86Box.\n\n Choose \"New configuration\" if you'd like to create a new machine.\n\nChoose \"Use existing configuration\" if you'd like to paste in an existing configuration from elsewhere.")); + topLabel->setWordWrap(true); + + newConfigRadioButton = new QRadioButton(tr("New configuration")); + // auto newDescription = new QLabel(tr("Choose this option to start with a fresh configuration.")); + existingConfigRadioButton = new QRadioButton(tr("Use existing configuraion")); + // auto existingDescription = new QLabel(tr("Use this option if you'd like to paste in the configuration file from an existing system.")); + newConfigRadioButton->setChecked(true); + + const auto layout = new QVBoxLayout(); + layout->addWidget(topLabel); + layout->addWidget(newConfigRadioButton); + // layout->addWidget(newDescription); + layout->addWidget(existingConfigRadioButton); + // layout->addWidget(existingDescription); + + setLayout(layout); +} + +int +IntroPage::nextId() const +{ + if (newConfigRadioButton->isChecked()) { + return VMManagerAddMachine::Page_NameAndLocation; + } else { + return VMManagerAddMachine::Page_WithExistingConfig; + } +} + +WithExistingConfigPage:: +WithExistingConfigPage(QWidget *parent) +{ + setTitle(tr("Use existing configuration")); + + const auto topLabel = new QLabel(tr("Paste the contents of the existing configuration file into the box below.")); + topLabel->setWordWrap(true); + + existingConfiguration = new QPlainTextEdit(); + connect(existingConfiguration, &QPlainTextEdit::textChanged, this, &WithExistingConfigPage::completeChanged); + registerField("existingConfiguration*", this, "configuration"); + + const auto layout = new QVBoxLayout(); + layout->addWidget(topLabel); + layout->addWidget(existingConfiguration); + const auto loadFileButton = new QPushButton(); + const auto loadFileLabel = new QLabel(tr("Load configuration from file")); + const auto hLayout = new QHBoxLayout(); + loadFileButton->setIcon(QApplication::style()->standardIcon(QStyle::SP_FileIcon)); + loadFileButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + connect(loadFileButton, &QPushButton::clicked, this, &WithExistingConfigPage::chooseExistingConfigFile); + hLayout->addWidget(loadFileButton); + hLayout->addWidget(loadFileLabel); + layout->addLayout(hLayout); + setLayout(layout); +} + +void +WithExistingConfigPage::chooseExistingConfigFile() +{ + // TODO: FIXME: This is using the CLI arg and needs to instead use a proper variable + const auto startDirectory = QString(vmm_path); + const auto selectedConfigFile = QFileDialog::getOpenFileName(this, tr("Choose configuration file"), + startDirectory, + tr("86Box configuration files (86box.cfg)")); + // Empty value means the dialog was canceled + if (!selectedConfigFile.isEmpty()) { + QFile configFile(selectedConfigFile); + if (!configFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + QMessageBox::critical(this, tr("Configuration read failed"), tr("Unable to open the selected configuration file for reading: %1").arg(configFile.errorString())); + return; + } + const QString configFileContents = configFile.readAll(); + existingConfiguration->setPlainText(configFileContents); + configFile.close(); + emit completeChanged(); + } +} + +QString +WithExistingConfigPage::configuration() const +{ + return existingConfiguration->toPlainText(); +} +void +WithExistingConfigPage::setConfiguration(const QString &configuration) +{ + if (configuration != existingConfiguration->toPlainText()) { + existingConfiguration->setPlainText(configuration); + emit configurationChanged(configuration); + } +} + +int +WithExistingConfigPage::nextId() const +{ + return VMManagerAddMachine::Page_NameAndLocation; +} + +bool +WithExistingConfigPage::isComplete() const +{ + return !existingConfiguration->toPlainText().isEmpty(); +} + +NameAndLocationPage:: +NameAndLocationPage(QWidget *parent) +{ + setTitle(tr("System name and location")); + + dirValidate = QRegularExpression(R"(^[^\\/:*?"<>|\s]+$)"); + + const auto topLabel = new QLabel(tr("Enter the name of the system and choose the location")); + topLabel->setWordWrap(true); + + const auto chooseDirectoryButton = new QPushButton(); + chooseDirectoryButton->setIcon(QApplication::style()->standardIcon(QStyle::SP_DirIcon)); + + const auto systemNameLabel = new QLabel(tr("System Name")); + systemName = new QLineEdit(); + // Special event filter to override enter key + systemName->installEventFilter(this); + registerField("systemName*", systemName); + systemNameValidation = new QLabel(); + + const auto systemLocationLabel = new QLabel(tr("System Location")); + systemLocation = new QLineEdit(); + // TODO: FIXME: This is using the CLI arg and needs to instead use a proper variable + systemLocation->setText(QDir::toNativeSeparators(vmm_path)); + registerField("systemLocation*", systemLocation); + systemLocationValidation = new QLabel(); + systemLocationValidation->setWordWrap(true); + + const auto layout = new QGridLayout(); + layout->addWidget(topLabel, 0, 0, 1, -1); + // Spacer row + layout->setRowMinimumHeight(1, 20); + layout->addWidget(systemNameLabel, 2, 0); + layout->addWidget(systemName, 2, 1); + // Validation text, appears only as necessary + layout->addWidget(systemNameValidation, 3, 0, 1, -1); + // Set height on validation because it may not always be present + layout->setRowMinimumHeight(3, 20); + + // Another spacer + layout->setRowMinimumHeight(4, 20); + layout->addWidget(systemLocationLabel, 5, 0); + layout->addWidget(systemLocation, 5, 1); + layout->addWidget(chooseDirectoryButton, 5, 2); + // Validation text + layout->addWidget(systemLocationValidation, 6, 0, 1, -1); + layout->setRowMinimumHeight(6, 20); + + setLayout(layout); + + + connect(chooseDirectoryButton, &QPushButton::clicked, this, &NameAndLocationPage::chooseDirectoryLocation); +} + +int +NameAndLocationPage::nextId() const +{ + return VMManagerAddMachine::Page_Conclusion; +} + +void +NameAndLocationPage::chooseDirectoryLocation() +{ + // TODO: FIXME: This is pulling in the CLI directory! Needs to be set properly elsewhere + const auto directory = QFileDialog::getExistingDirectory(this, "Choose directory", QDir(vmm_path).path()); + systemLocation->setText(QDir::toNativeSeparators(directory)); + emit completeChanged(); +} +bool +NameAndLocationPage::isComplete() const +{ + bool nameValid = false; + bool locationValid = false; + // return true if complete + if (systemName->text().isEmpty()) { + systemNameValidation->setText(tr("Please enter a system name")); + } else if (!systemName->text().contains(dirValidate)) { + systemNameValidation->setText(tr("System name cannot contain a space or certain characters")); + } else if (const QDir newDir = QDir::cleanPath(systemLocation->text() + "/" + systemName->text()); newDir.exists()) { + systemNameValidation->setText(tr("System name already exists")); + } else { + systemNameValidation->clear(); + nameValid = true; + } + + if (systemLocation->text().isEmpty()) { + systemLocationValidation->setText(tr("Please enter a directory for the system")); + } else if (const auto dir = QDir(systemLocation->text()); !dir.exists()) { + systemLocationValidation->setText(tr("Directory does not exist")); + } else { + systemLocationValidation->setText("A new directory for the system will be created in the selected directory above"); + locationValid = true; + } + + return nameValid && locationValid; +} +bool +NameAndLocationPage::eventFilter(QObject *watched, QEvent *event) +{ + // Override the enter key to hit the next wizard button + // if the validator (isComplete) is satisfied + if (event->type() == QEvent::KeyPress) { + const auto keyEvent = dynamic_cast(event); + if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) { + // Only advance if the validator is satisfied (isComplete) + if(const auto wizard = qobject_cast(this->wizard())) { + if (wizard->currentPage()->isComplete()) { + wizard->next(); + } + } + // Discard the key event + return true; + } + } + return QWizardPage::eventFilter(watched, event); +} + +ConclusionPage:: +ConclusionPage(QWidget *parent) +{ + setTitle(tr("Complete")); + + topLabel = new QLabel(tr("The wizard will now launch the configuration for the new system.")); + topLabel->setWordWrap(true); + + const auto systemNameLabel = new QLabel(tr("System name:")); + systemNameLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + systemName = new QLabel(); + const auto systemLocationLabel = new QLabel(tr("System location:")); + systemLocationLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + systemLocation = new QLabel(); + + const auto layout = new QGridLayout(); + layout->addWidget(topLabel, 0, 0, 1, -1); + layout->setRowMinimumHeight(1, 20); + layout->addWidget(systemNameLabel, 2, 0); + layout->addWidget(systemName, 2, 1); + layout->addWidget(systemLocationLabel, 3, 0); + layout->addWidget(systemLocation, 3, 1); + + setLayout(layout); +} + +// initializePage() runs after the page has been created with the constructor +void +ConclusionPage::initializePage() +{ + const auto finalPath = QDir::cleanPath(field("systemLocation").toString() + "/" + field("systemName").toString()); + const auto nativePath = QDir::toNativeSeparators(finalPath); + const auto systemNameDisplay = field("systemName").toString(); + + systemName->setText(systemNameDisplay); + systemLocation->setText(nativePath); +} diff --git a/src/qt/qt_vmmanager_addmachine.hpp b/src/qt/qt_vmmanager_addmachine.hpp new file mode 100644 index 000000000..c1355b471 --- /dev/null +++ b/src/qt/qt_vmmanager_addmachine.hpp @@ -0,0 +1,115 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for 86Box VM manager add machine wizard +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#ifndef QT_VMMANAGER_ADDMACHINE_H +#define QT_VMMANAGER_ADDMACHINE_H + +#include +#include +#include +#include +#include +#include + +// Implementation note: There are several classes in this header: +// One for the main Wizard class and one for each page of the wizard + +class VMManagerAddMachine final : public QWizard { + Q_OBJECT + +public: + enum { + Page_Intro, + Page_Fresh, + Page_WithExistingConfig, + Page_NameAndLocation, + Page_Conclusion + }; + + explicit VMManagerAddMachine(QWidget *parent = nullptr); + +private slots: + void showHelp(); +}; + +class IntroPage : public QWizardPage { + Q_OBJECT + +public: + explicit IntroPage(QWidget *parent = nullptr); + [[nodiscard]] int nextId() const override; + +private: + QLabel *topLabel; + QRadioButton *newConfigRadioButton; + QRadioButton *existingConfigRadioButton; +}; + +class WithExistingConfigPage final : public QWizardPage { + Q_OBJECT + Q_PROPERTY(QString configuration READ configuration WRITE setConfiguration NOTIFY configurationChanged) + +public: + explicit WithExistingConfigPage(QWidget *parent = nullptr); + // These extra functions are required to register QPlainTextEdit fields + [[nodiscard]] QString configuration() const; + void setConfiguration(const QString &configuration); +signals: + void configurationChanged(const QString &configuration); +private: + QPlainTextEdit *existingConfiguration; +private slots: + void chooseExistingConfigFile(); +protected: + [[nodiscard]] int nextId() const override; + [[nodiscard]] bool isComplete() const override; + +}; + +class NameAndLocationPage final : public QWizardPage { + Q_OBJECT + +public: + explicit NameAndLocationPage(QWidget *parent = nullptr); + [[nodiscard]] int nextId() const override; +private: + QLineEdit *systemName; + QLineEdit *systemLocation; + QLabel *systemNameValidation; + QLabel *systemLocationValidation; + QRegularExpression dirValidate; +private slots: + void chooseDirectoryLocation(); +protected: + [[nodiscard]] bool isComplete() const override; + bool eventFilter(QObject *watched, QEvent *event) override; + +}; + +class ConclusionPage final : public QWizardPage { + Q_OBJECT +public: + explicit ConclusionPage(QWidget *parent = nullptr); +private: + QLabel *topLabel; + QLabel *systemName; + QLabel *systemLocation; +protected: + void initializePage() override; +}; + +#endif // QT_VMMANAGER_ADDMACHINE_H \ No newline at end of file diff --git a/src/qt/qt_vmmanager_clientsocket.cpp b/src/qt/qt_vmmanager_clientsocket.cpp new file mode 100644 index 000000000..1035a846c --- /dev/null +++ b/src/qt/qt_vmmanager_clientsocket.cpp @@ -0,0 +1,236 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager client socket module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#include "qt_vmmanager_clientsocket.hpp" +#include "qt_vmmanager_protocol.hpp" +#include +#include +#include + +extern "C" { +#include "86box/plat.h" +} + +VMManagerClientSocket::VMManagerClientSocket(QObject* obj) : server_connected(false) +{ + socket = new QLocalSocket; + +} + +void +VMManagerClientSocket::dataReady() +{ + // emit signal? + QDataStream stream(socket); + stream.setVersion(QDataStream::Qt_5_7); + QByteArray jsonData; + for (;;) { + // start a transaction + stream.startTransaction(); + // try to read the data + stream >> jsonData; + if (stream.commitTransaction()) { + // first try to successfully read some data + // need to also make sure it's valid json + QJsonParseError parse_error{}; + // try to create a document with the data received + const QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parse_error); + if (parse_error.error == QJsonParseError::NoError) { + // the data received was valid json + if (jsonDoc.isObject()) { + // and is a valid json object + // parse the json + jsonReceived(jsonDoc.object()); + } + } + // If the received data isn't valid json, + // loop and try to read more if available + } else { + // read failed, socket is reverted to its previous state (before the transaction) + // Exit the loop and wait for more data to become available + break; + } + } + +} + +bool +VMManagerClientSocket::IPCConnect(const QString &server) +{ + server_name = server; + connect(socket, &QLocalSocket::connected, this, &VMManagerClientSocket::connected); + connect(socket, &QLocalSocket::disconnected, this, &VMManagerClientSocket::disconnected); + connect(socket, &QLocalSocket::errorOccurred, this, &VMManagerClientSocket::connectionError); + connect(socket, &QLocalSocket::readyRead, this, &VMManagerClientSocket::dataReady); + + socket->connectToServer(server_name); + + if(!socket->isValid()) { + qInfo("Could not connect to server: %s", qPrintable(socket->errorString())); + return false; + } + + qInfo("Connection Successful"); + return true; +} + +void +VMManagerClientSocket::connected() const +{ + // TODO: signal + qDebug("Connected to %s", qPrintable(server_name)); +} + +void +VMManagerClientSocket::disconnected() const +{ + // TODO: signal + qDebug("Disconnected from %s", qPrintable(server_name)); +} + +void +VMManagerClientSocket::sendMessage(const VMManagerProtocol::ClientMessage protocol_message) const +{ + sendMessageFull(protocol_message, QStringList(), QJsonObject()); +} + +void +VMManagerClientSocket::sendMessageWithList(const VMManagerProtocol::ClientMessage protocol_message, const QStringList &list) const +{ + sendMessageFull(protocol_message, list, QJsonObject()); +} + +void +VMManagerClientSocket::sendMessageWithObject(const VMManagerProtocol::ClientMessage protocol_message, const QJsonObject &json) const +{ + sendMessageFull(protocol_message, QStringList(), json); +} + +void +VMManagerClientSocket::sendMessageFull(const VMManagerProtocol::ClientMessage protocol_message, const QStringList &list, const QJsonObject &json) const +{ + QDataStream clientStream(socket); + clientStream.setVersion(QDataStream::Qt_5_7); + auto packet = new VMManagerProtocol(VMManagerProtocol::Sender::Client); + auto jsonMessage = packet->protocolClientMessage(protocol_message); + if (!list.isEmpty()) { + jsonMessage["list"] = QJsonArray::fromStringList(list); + } + // TODO: Add the logic for including objects + if(!json.isEmpty()) { + jsonMessage["params"] = json; + } + clientStream << QJsonDocument(jsonMessage).toJson(QJsonDocument::Compact); +} + +void +VMManagerClientSocket::jsonReceived(const QJsonObject &json) +{ + // The serialization portion has already validated the message as json. + // Ensure it has the required fields + if (!VMManagerProtocol::hasRequiredFields(json)) { + // TODO: Error handling of some sort, emit signals + qDebug("Invalid message received from client: required fields missing. Object:"); + qDebug() << json; + return; + } + // qDebug() << Q_FUNC_INFO << json; + + // Parsing happens here. When adding new types, make sure to first add them + // to VMManagerProtocol::ManagerMessage and then add it to the list here. + // If a signal needs to be emitted, add that as well and connect to slots + // as appropriate. + + switch (VMManagerProtocol::getManagerMessageType(json)) { + case VMManagerProtocol::ManagerMessage::Pause: + qDebug("Pause command received from manager"); + emit pause(); + break; + case VMManagerProtocol::ManagerMessage::ResetVM: + qDebug("Reset VM command received from manager"); + emit resetVM(); + break; + case VMManagerProtocol::ManagerMessage::ShowSettings: + qDebug("Show settings command received from manager"); + emit showsettings(); + break; + case VMManagerProtocol::ManagerMessage::CtrlAltDel: + qDebug("CtrlAltDel command received from manager"); + emit ctrlaltdel(); + break; + case VMManagerProtocol::ManagerMessage::RequestShutdown: + qDebug("RequestShutdown command received from manager"); + emit request_shutdown(); + break; + case VMManagerProtocol::ManagerMessage::ForceShutdown: + qDebug("ForceShutdown command received from manager"); + emit force_shutdown(); + break; + case VMManagerProtocol::ManagerMessage::RequestStatus: + qDebug("Status request command received from manager"); + break; + default: + qDebug("Unknown client message type received:"); + qDebug() << json; + break; + } +} + +void +VMManagerClientSocket::connectionError(const QLocalSocket::LocalSocketError socketError) +{ + qInfo("A connection error has occurred: "); + switch (socketError) { + case QLocalSocket::ServerNotFoundError: + qInfo("Server not found"); + break; + case QLocalSocket::ConnectionRefusedError: + qInfo("Connection refused"); + break; + case QLocalSocket::PeerClosedError: + qInfo("Peer closed"); + break; + default: + qInfo() << "QLocalSocket::LocalSocketError " << socketError; + break; + } +} + +bool +VMManagerClientSocket::eventFilter(QObject *obj, QEvent *event) +{ + if (socket->state() == QLocalSocket::ConnectedState) { + VMManagerProtocol::RunningState running_state; + if (event->type() == QEvent::WindowBlocked) { + running_state = dopause ? VMManagerProtocol::RunningState::PausedWaiting : VMManagerProtocol::RunningState::RunningWaiting; + clientRunningStateChanged(running_state); + } else if (event->type() == QEvent::WindowUnblocked) { + running_state = dopause ? VMManagerProtocol::RunningState::Paused : VMManagerProtocol::RunningState::Running; + clientRunningStateChanged(running_state); + } + } + return QObject::eventFilter(obj, event); +} + +void +VMManagerClientSocket::clientRunningStateChanged(VMManagerProtocol::RunningState state) const +{ + QJsonObject extra_object; + extra_object["status"] = static_cast(state); + sendMessageWithObject(VMManagerProtocol::ClientMessage::RunningStateChanged, extra_object); + +} diff --git a/src/qt/qt_vmmanager_clientsocket.hpp b/src/qt/qt_vmmanager_clientsocket.hpp new file mode 100644 index 000000000..10a053347 --- /dev/null +++ b/src/qt/qt_vmmanager_clientsocket.hpp @@ -0,0 +1,71 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header file for 86Box VM manager client socket module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#ifndef QT_VMMANAGER_CLIENTSOCKET_HPP +#define QT_VMMANAGER_CLIENTSOCKET_HPP + +#include "qt_vmmanager_protocol.hpp" +#include +#include +#include +#include + +class VMManagerClientSocket final : public QObject { + Q_OBJECT + +public: + explicit VMManagerClientSocket(QObject* object = nullptr); + bool IPCConnect(const QString &server); + +signals: + void pause(); + void ctrlaltdel(); + void showsettings(); + void resetVM(); + void request_shutdown(); + void force_shutdown(); + void dialogstatus(bool open); + +public slots: + void clientRunningStateChanged(VMManagerProtocol::RunningState state) const; + +private: + QString server_name; + QLocalSocket *socket; + bool server_connected; + void connected() const; + void disconnected() const; + static void connectionError(QLocalSocket::LocalSocketError socketError); + + // Main convenience send function + void sendMessage(VMManagerProtocol::ClientMessage protocol_message) const; + // Send message with optional params array convenience function + void sendMessageWithList(VMManagerProtocol::ClientMessage protocol_message, const QStringList &list) const; + // Send message with optional json object convenience function + void sendMessageWithObject(VMManagerProtocol::ClientMessage protocol_message, const QJsonObject &json) const; + // Full send message function called by all convenience functions + void sendMessageFull(VMManagerProtocol::ClientMessage protocol_message, const QStringList &list, const QJsonObject &json) const; + void jsonReceived(const QJsonObject &json); + + void dataReady(); + +protected: + bool eventFilter(QObject *obj, QEvent *event) override; + +}; + +#endif // QT_VMMANAGER_CLIENTSOCKET_HPP diff --git a/src/qt/qt_vmmanager_config.cpp b/src/qt/qt_vmmanager_config.cpp new file mode 100644 index 000000000..08bf4e7c4 --- /dev/null +++ b/src/qt/qt_vmmanager_config.cpp @@ -0,0 +1,76 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager configuration module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#include +#include +#include "qt_vmmanager_config.hpp" + +extern "C" { +#include <86box/plat.h> +} + +VMManagerConfig::VMManagerConfig(const ConfigType type, const QString& section) +{ + char BUF[256]; + plat_get_global_config_dir(BUF, 255); + const auto configDir = QString(BUF); + const auto configFile = QDir::cleanPath(configDir + "/" + "vmm.ini"); + + config_type = type; + + settings = new QSettings(configFile, QSettings::IniFormat, this); + settings->setFallbacksEnabled(false); + if(type == ConfigType::System && !section.isEmpty()) { + settings->beginGroup(section); + } +} + +VMManagerConfig::~VMManagerConfig() { + settings->endGroup(); +} + +QString +VMManagerConfig::getStringValue(const QString& key) const +{ + const auto value = settings->value(key); + // An invalid QVariant with toString will give a default QString value which is blank. + // Therefore any variables that do not exist will return blank strings + return value.toString(); +} + +void +VMManagerConfig::setStringValue(const QString &key, const QString &value) const +{ + if (value.isEmpty()) { + remove(key); + return; + } + settings->setValue(key, value); +} + +void +VMManagerConfig::remove(const QString &key) const +{ + settings->remove(key); +} + +void +VMManagerConfig::sync() const +{ + settings->sync(); +} + diff --git a/src/qt/qt_vmmanager_config.hpp b/src/qt/qt_vmmanager_config.hpp new file mode 100644 index 000000000..bc63aaa03 --- /dev/null +++ b/src/qt/qt_vmmanager_config.hpp @@ -0,0 +1,46 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for the 86Box VM manager configuration module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#ifndef QT_VMMANAGER_CONFIG_H +#define QT_VMMANAGER_CONFIG_H + +#include + +class VMManagerConfig : QObject { + Q_OBJECT + +public: + enum class ConfigType { + General, + System, + }; + Q_ENUM(ConfigType); + + explicit VMManagerConfig(ConfigType type, const QString& section = {}); + ~VMManagerConfig() override; + [[nodiscard]] QString getStringValue(const QString& key) const; + void setStringValue(const QString& key, const QString& value) const; + void remove(const QString &key) const; + + void sync() const; + + QSettings *settings; + ConfigType config_type; + QString system_name; +}; + +#endif // QT_VMMANAGER_CONFIG_H \ No newline at end of file diff --git a/src/qt/qt_vmmanager_details.cpp b/src/qt/qt_vmmanager_details.cpp new file mode 100644 index 000000000..f72102c25 --- /dev/null +++ b/src/qt/qt_vmmanager_details.cpp @@ -0,0 +1,345 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager system details module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#include +#include +#include + +#include "qt_vmmanager_details.hpp" +#include "ui_qt_vmmanager_details.h" + +VMManagerDetails::VMManagerDetails(QWidget *parent) : + QWidget(parent), ui(new Ui::VMManagerDetails) { + ui->setupUi(this); + + const auto leftColumnLayout = qobject_cast(ui->leftColumn->layout()); + + // Each section here gets its own VMManagerDetailSection, named in the constructor. + // When a system is selected in the list view it is updated through this object + // See updateData() for the implementation + + systemSection = new VMManagerDetailSection(tr("System", "Header for System section in VM Manager Details")); + ui->leftColumn->layout()->addWidget(systemSection); + // These horizontal lines are used for the alternate layout which may possibly + // be a preference one day. + // ui->leftColumn->layout()->addWidget(createHorizontalLine()); + + videoSection = new VMManagerDetailSection(tr("Display", "Header for Display section in VM Manager Details")); + ui->leftColumn->layout()->addWidget(videoSection); + // ui->leftColumn->layout()->addWidget(createHorizontalLine()); + + storageSection = new VMManagerDetailSection(tr("Storage", "Header for Storage section in VM Manager Details")); + ui->leftColumn->layout()->addWidget(storageSection); + // ui->leftColumn->layout()->addWidget(createHorizontalLine()); + + audioSection = new VMManagerDetailSection(tr("Audio", "Header for Audio section in VM Manager Details")); + ui->leftColumn->layout()->addWidget(audioSection); + // ui->leftColumn->layout()->addWidget(createHorizontalLine()); + + networkSection = new VMManagerDetailSection(tr("Network", "Header for Network section in VM Manager Details")); + ui->leftColumn->layout()->addWidget(networkSection); + // ui->leftColumn->layout()->addWidget(createHorizontalLine()); + + inputSection = new VMManagerDetailSection(tr("Input Devices", "Header for Input section in VM Manager Details")); + ui->leftColumn->layout()->addWidget(inputSection); + // ui->leftColumn->layout()->addWidget(createHorizontalLine()); + + portsSection = new VMManagerDetailSection(tr("Ports", "Header for Input section in VM Manager Details")); + ui->leftColumn->layout()->addWidget(portsSection); + + // This is like adding a spacer + leftColumnLayout->addStretch(); + + // Event filter for the notes to save when it loses focus + ui->notesTextEdit->installEventFilter(this); + + // Default screenshot label and thumbnail (image inside the label) sizes + screenshotThumbnailSize = QSize(240, 160); + + // Set the icons for the screenshot navigation buttons + ui->screenshotNext->setIcon(QApplication::style()->standardIcon(QStyle::SP_MediaSeekForward)); + ui->screenshotPrevious->setIcon(QApplication::style()->standardIcon(QStyle::SP_MediaSeekBackward)); + ui->screenshotNextTB->setIcon(QApplication::style()->standardIcon(QStyle::SP_MediaSeekForward)); + ui->screenshotPreviousTB->setIcon(QApplication::style()->standardIcon(QStyle::SP_MediaSeekBackward)); + // Disabled by default + ui->screenshotNext->setEnabled(false); + ui->screenshotPrevious->setEnabled(false); + ui->screenshotNextTB->setEnabled(false); + ui->screenshotPreviousTB->setEnabled(false); + // Connect their signals + connect(ui->screenshotNext, &QPushButton::clicked, this, &VMManagerDetails::nextScreenshot); + connect(ui->screenshotNextTB, &QToolButton::clicked, this, &VMManagerDetails::nextScreenshot); + connect(ui->screenshotPreviousTB, &QToolButton::clicked, this, &VMManagerDetails::previousScreenshot); + connect(ui->screenshotPrevious, &QPushButton::clicked, this, &VMManagerDetails::previousScreenshot); + // These push buttons can be taken out if the tool buttons stay + ui->screenshotNext->setVisible(false); + ui->screenshotPrevious->setVisible(false); + QString toolButtonStyleSheet; + // Simple method to try and determine if light mode is enabled + const bool lightMode = QApplication::palette().window().color().value() > QApplication::palette().windowText().color().value(); + if (lightMode) { + toolButtonStyleSheet = "QToolButton {background: transparent; border: none; padding: 5px} QToolButton:hover {background: palette(midlight)} QToolButton:pressed {background: palette(mid)}"; + } else { + toolButtonStyleSheet = "QToolButton {background: transparent; border: none; padding: 5px} QToolButton:hover {background: palette(dark)} QToolButton:pressed {background: palette(mid)}"; + } + ui->ssNavTBHolder->setStyleSheet(toolButtonStyleSheet); + + // Experimenting + startPauseButton = new QToolButton(); + startPauseButton->setIcon(QIcon(":/menuicons/qt/icons/run.ico")); + startPauseButton->setAutoRaise(true); + ui->toolButtonHolder->setStyleSheet(toolButtonStyleSheet); + resetButton = new QToolButton(); + resetButton->setIcon(QIcon(":/menuicons/qt/icons/hard_reset.ico")); + stopButton = new QToolButton(); + stopButton->setIcon(QIcon(":/menuicons/qt/icons/acpi_shutdown.ico")); + configureButton = new QToolButton(); + configureButton->setIcon(QIcon(":/menuicons/qt/icons/settings.ico")); + + ui->toolButtonHolder->layout()->addWidget(configureButton); + ui->toolButtonHolder->layout()->addWidget(resetButton); + ui->toolButtonHolder->layout()->addWidget(stopButton); + ui->toolButtonHolder->layout()->addWidget(startPauseButton); + + sysconfig = new VMManagerSystem(); +} + +VMManagerDetails::~VMManagerDetails() { + delete ui; +} + +void +VMManagerDetails::updateData(VMManagerSystem *passed_sysconfig) { + + // Set the scrollarea background but also set the scroll bar to none. Otherwise it will also + // set the scrollbar background to the same. + ui->scrollArea->setStyleSheet("QWidget {background-color: palette(light)} QScrollBar{ background-color: none }"); + ui->systemLabel->setStyleSheet("background-color: palette(midlight);"); + // Margins are a little different on macos +#ifdef Q_OS_MACOS + ui->systemLabel->setMargin(15); +#else + ui->systemLabel->setMargin(10); +#endif + + // disconnect old signals before assigning the passed systemconfig object + disconnect(startPauseButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::startButtonPressed); + disconnect(startPauseButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::pauseButtonPressed); + disconnect(resetButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::restartButtonPressed); + disconnect(stopButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::shutdownForceButtonPressed); + disconnect(configureButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::launchSettings); + + sysconfig = passed_sysconfig; + connect(resetButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::restartButtonPressed); + connect(stopButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::shutdownForceButtonPressed); + connect(configureButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::launchSettings); + + bool running = sysconfig->getProcessStatus() == VMManagerSystem::ProcessStatus::Running || + sysconfig->getProcessStatus() == VMManagerSystem::ProcessStatus::RunningWaiting; + if(running) { + startPauseButton->setIcon(QIcon(":/menuicons/qt/icons/pause.ico")); + connect(startPauseButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::pauseButtonPressed); + } else { + startPauseButton->setIcon(QIcon(":/menuicons/qt/icons/run.ico")); + connect(startPauseButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::startButtonPressed); + } + + // Each detail section here has its own VMManagerDetailSection. + // When a system is selected in the list view it is updated here, through this object: + // * First you clear it with VMManagerDetailSection::clear() + // * Then you add each line with VMManagerDetailSection::addSection() + + // System + systemSection->clear(); + systemSection->addSection("Machine", passed_sysconfig->getDisplayValue(Display::Name::Machine)); + systemSection->addSection("CPU", passed_sysconfig->getDisplayValue(Display::Name::CPU)); + systemSection->addSection("Memory", passed_sysconfig->getDisplayValue(Display::Name::Memory)); + + // Video + videoSection->clear(); + videoSection->addSection("Video", passed_sysconfig->getDisplayValue(Display::Name::Video)); + if(!passed_sysconfig->getDisplayValue(Display::Name::Voodoo).isEmpty()) { + videoSection->addSection("Voodoo", passed_sysconfig->getDisplayValue(Display::Name::Voodoo)); + } + + // Disks + storageSection->clear(); + storageSection->addSection("Disks", passed_sysconfig->getDisplayValue(Display::Name::Disks)); + storageSection->addSection("Floppy", passed_sysconfig->getDisplayValue(Display::Name::Floppy)); + storageSection->addSection("CD-ROM", passed_sysconfig->getDisplayValue(Display::Name::CD)); + storageSection->addSection("SCSI", passed_sysconfig->getDisplayValue(Display::Name::SCSIController)); + + // Audio + audioSection->clear(); + audioSection->addSection("Audio", passed_sysconfig->getDisplayValue(Display::Name::Audio)); + audioSection->addSection("MIDI Out", passed_sysconfig->getDisplayValue(Display::Name::MidiOut)); + + // Network + networkSection->clear(); + networkSection->addSection("NIC", passed_sysconfig->getDisplayValue(Display::Name::NIC)); + + // Input + inputSection->clear(); + inputSection->addSection(tr("Mouse"), passed_sysconfig->getDisplayValue(Display::Name::Mouse)); + inputSection->addSection(tr("Joystick"), passed_sysconfig->getDisplayValue(Display::Name::Joystick)); + + // Ports + portsSection->clear(); + portsSection->addSection(tr("Serial Ports"), passed_sysconfig->getDisplayValue(Display::Name::Serial)); + portsSection->addSection(tr("Parallel Ports"), passed_sysconfig->getDisplayValue(Display::Name::Parallel)); + + // Disable screenshot navigation buttons by default + ui->screenshotNext->setEnabled(false); + ui->screenshotPrevious->setEnabled(false); + ui->screenshotNextTB->setEnabled(false); + ui->screenshotPreviousTB->setEnabled(false); + + // Different actions are taken depending on the existence and number of screenshots + screenshots = passed_sysconfig->getScreenshots(); + if (!screenshots.empty()) { + ui->screenshot->setFrameStyle(QFrame::NoFrame); + ui->screenshot->setEnabled(true); + if(screenshots.size() > 1) { + ui->screenshotNext->setEnabled(true); + ui->screenshotPrevious->setEnabled(true); + ui->screenshotNextTB->setEnabled(true); + ui->screenshotPreviousTB->setEnabled(true); + } + if(QFileInfo::exists(screenshots.last().filePath())) { + screenshotIndex = screenshots.size() - 1; + const QPixmap pic(screenshots.at(screenshotIndex).filePath()); + ui->screenshot->setPixmap(pic.scaled(240, 160, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } + } else { + ui->screenshotNext->setEnabled(false); + ui->screenshotPrevious->setEnabled(false); + ui->screenshotNextTB->setEnabled(false); + ui->screenshotPreviousTB->setEnabled(false); + ui->screenshot->setPixmap(QString()); + ui->screenshot->setFixedSize(240, 160); + ui->screenshot->setFrameStyle(QFrame::Box | QFrame::Sunken); + ui->screenshot->setText(tr("No screenshot")); + ui->screenshot->setEnabled(false); + ui->screenshot->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter); + } + + ui->systemLabel->setText(passed_sysconfig->displayName); + ui->statusLabel->setText(sysconfig->process->processId() == 0 ? + tr("Not running") : + QString("%1: PID %2").arg(tr("Running"), QString::number(sysconfig->process->processId()))); + ui->notesTextEdit->setPlainText(passed_sysconfig->notes); + + disconnect(sysconfig->process, &QProcess::stateChanged, this, &VMManagerDetails::updateProcessStatus); + connect(sysconfig->process, &QProcess::stateChanged, this, &VMManagerDetails::updateProcessStatus); + + disconnect(sysconfig, &VMManagerSystem::windowStatusChanged, this, &VMManagerDetails::updateWindowStatus); + connect(sysconfig, &VMManagerSystem::windowStatusChanged, this, &VMManagerDetails::updateWindowStatus); + + disconnect(sysconfig, &VMManagerSystem::clientProcessStatusChanged, this, &VMManagerDetails::updateProcessStatus); + connect(sysconfig, &VMManagerSystem::clientProcessStatusChanged, this, &VMManagerDetails::updateProcessStatus); + + updateProcessStatus(); +} + +void +VMManagerDetails::updateProcessStatus() { + const bool running = sysconfig->process->state() == QProcess::ProcessState::Running; + QString status_text = running ? + QString("%1: PID %2").arg(tr("Running"), QString::number(sysconfig->process->processId())) : + tr("Not running"); + status_text.append(sysconfig->window_obscured ? QString(" (%1)").arg(tr("waiting")) : ""); + ui->statusLabel->setText(status_text); + resetButton->setEnabled(running); + stopButton->setEnabled(running); + if(running) { + if(sysconfig->getProcessStatus() == VMManagerSystem::ProcessStatus::Running) { + startPauseButton->setIcon(QIcon(":/menuicons/qt/icons/pause.ico")); + } else { + startPauseButton->setIcon(QIcon(":/menuicons/qt/icons/run.ico")); + } + + disconnect(startPauseButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::pauseButtonPressed); + disconnect(startPauseButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::startButtonPressed); + connect(startPauseButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::pauseButtonPressed); + } else { + startPauseButton->setIcon(QIcon(":/menuicons/qt/icons/run.ico")); + disconnect(startPauseButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::pauseButtonPressed); + disconnect(startPauseButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::startButtonPressed); + connect(startPauseButton, &QToolButton::clicked, sysconfig, &VMManagerSystem::startButtonPressed); + } +} + +void +VMManagerDetails::updateWindowStatus() +{ + qInfo("Window status changed: %i", sysconfig->window_obscured); + updateProcessStatus(); +} + +QWidget * +VMManagerDetails::createHorizontalLine(const int leftSpacing, const int rightSpacing) +{ + const auto container = new QWidget; + const auto hLayout = new QHBoxLayout(container); + + hLayout->addSpacing(leftSpacing); + + const auto line = new QFrame(); + line->setFrameShape(QFrame::HLine); + line->setFrameShadow(QFrame::Sunken); + + hLayout->addWidget(line); + hLayout->addSpacing(rightSpacing); + hLayout->setContentsMargins(0, 5, 0, 5); + + return container; +} + +void +VMManagerDetails::saveNotes() const +{ + sysconfig->setNotes(ui->notesTextEdit->toPlainText()); +} + +void +VMManagerDetails::nextScreenshot() +{ + screenshotIndex = (screenshotIndex + 1) % screenshots.size(); + const QPixmap pic(screenshots.at(screenshotIndex).filePath()); + ui->screenshot->setPixmap(pic.scaled(240, 160, Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} + +void +VMManagerDetails::previousScreenshot() +{ + screenshotIndex = screenshotIndex == 0 ? screenshots.size() - 1 : screenshotIndex - 1; + const QPixmap pic(screenshots.at(screenshotIndex).filePath()); + ui->screenshot->setPixmap(pic.scaled(240, 160, Qt::KeepAspectRatio, Qt::SmoothTransformation)); +} + +bool +VMManagerDetails::eventFilter(QObject *watched, QEvent *event) +{ + if (watched->isWidgetType() && event->type() == QEvent::FocusOut) { + // Make sure it's the textedit + if (const auto *textEdit = qobject_cast(watched); textEdit) { + saveNotes(); + } + } + return QWidget::eventFilter(watched, event); +} + diff --git a/src/qt/qt_vmmanager_details.hpp b/src/qt/qt_vmmanager_details.hpp new file mode 100644 index 000000000..c6db78127 --- /dev/null +++ b/src/qt/qt_vmmanager_details.hpp @@ -0,0 +1,89 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for 86Box VM manager system details module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#ifndef QT_VMMANAGER_DETAILS_H +#define QT_VMMANAGER_DETAILS_H + +#include +#include "qt_vmmanager_system.hpp" +// #include "qt_vmmanager_details_section.hpp" +#include "qt_vmmanager_detailsection.hpp" + + +QT_BEGIN_NAMESPACE +//namespace Ui { class VMManagerDetails; class CollapseButton;} +namespace Ui { class VMManagerDetails;} +QT_END_NAMESPACE + +class VMManagerDetails : public QWidget { + Q_OBJECT + +public: + explicit VMManagerDetails(QWidget *parent = nullptr); + + ~VMManagerDetails() override; + + void updateData(VMManagerSystem *passed_sysconfig); + + void updateProcessStatus(); + + void updateWindowStatus(); +// CollapseButton *systemCollapseButton; + +private: + Ui::VMManagerDetails *ui; + VMManagerSystem *sysconfig; + + VMManagerDetailSection *systemSection; + VMManagerDetailSection *videoSection; + VMManagerDetailSection *storageSection; + VMManagerDetailSection *audioSection; + VMManagerDetailSection *networkSection; + VMManagerDetailSection *inputSection; + VMManagerDetailSection *portsSection; + + QFileInfoList screenshots; + int screenshotIndex = 0; + QSize screenshotLabelSize; + QSize screenshotThumbnailSize; + + QToolButton *startPauseButton; + QToolButton *resetButton; + QToolButton *stopButton; + QToolButton *configureButton; + + static QWidget* createHorizontalLine(int leftSpacing = 25, int rightSpacing = 25); + // QVBoxLayout *detailsLayout; +private slots: + void saveNotes() const; + void nextScreenshot(); + void previousScreenshot(); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + +// CollapseButton *systemCollapseButton; +// QFrame *systemFrame; +// CollapseButton *displayCollapseButton; +// QFrame *displayFrame; +// CollapseButton *storageCollapseButton; +// QFrame *storageFrame; +}; + + + +#endif //QT_VMMANAGER_DETAILS_H diff --git a/src/qt/qt_vmmanager_details.ui b/src/qt/qt_vmmanager_details.ui new file mode 100644 index 000000000..9b4fbbe8b --- /dev/null +++ b/src/qt/qt_vmmanager_details.ui @@ -0,0 +1,293 @@ + + + VMManagerDetails + + + + 0 + 0 + 497 + 444 + + + + VMManagerDetails + + + + 0 + + + 0 + + + + + + 18 + + + + No Machines Found! + + + Qt::AlignCenter + + + + + + + + 0 + + + 0 + + + + + QFrame::StyledPanel + + + true + + + + + 0 + 0 + 139 + 388 + + + + + 0 + + + 12 + + + 12 + + + 12 + + + 12 + + + + + + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 240 + 160 + + + + + 240 + 160 + + + + + + + Qt::AlignCenter + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + + + false + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + ... + + + + + + + ... + + + true + + + + + + + + + + + 0 + 0 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Type some notes here + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + Qt::AlignCenter + + + + + + + + + + + + + + diff --git a/src/qt/qt_vmmanager_detailsection.cpp b/src/qt/qt_vmmanager_detailsection.cpp new file mode 100644 index 000000000..95a600d8f --- /dev/null +++ b/src/qt/qt_vmmanager_detailsection.cpp @@ -0,0 +1,294 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager system details section module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#include "qt_vmmanager_detailsection.hpp" +#include "ui_qt_vmmanager_detailsection.h" + +#include + +const QString VMManagerDetailSection::sectionSeparator = ";"; + +VMManagerDetailSection:: +VMManagerDetailSection(const QString §ionName) + : mainLayout(new QVBoxLayout()) + , buttonLayout(new QHBoxLayout()) + , frame(new QFrame()) + , ui(new Ui::DetailSection) +{ + ui->setupUi(this); + + frameGridLayout = new QGridLayout(); + // Create the collapse button, set the name and add it to the layout + collapseButton = new CollapseButton(); + setSectionName(sectionName); + ui->collapseButtonHolder->setContentsMargins(getMargins(MarginSection::ToolButton)); + + // Simple method to try and determine if light mode is enabled on the host + const bool lightMode = QApplication::palette().window().color().value() > QApplication::palette().windowText().color().value(); + // Alternate layout + if ( lightMode) { + ui->collapseButtonHolder->setStyleSheet("background-color: palette(midlight);"); + } else { + ui->collapseButtonHolder->setStyleSheet("background-color: palette(mid);"); + } + const auto sectionLabel = new QLabel(sectionName); + sectionLabel->setStyleSheet(sectionLabel->styleSheet().append("font-weight: bold;")); + ui->collapseButtonHolder->setContentsMargins(QMargins(3, 2, 0, 2)); + ui->collapseButtonHolder->layout()->addWidget(sectionLabel); + + // ui->collapseButtonHolder->layout()->addWidget(collapseButton); + collapseButton->setContent(ui->detailFrame); + // Horizontal line added after the section name / button + // const auto hLine = new QFrame(); + // hLine->setFrameShape(QFrame::HLine); + // hLine->setFrameShadow(QFrame::Sunken); + // hLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); + // ui->collapseButtonHolder->layout()->addWidget(hLine); + const auto hSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); + ui->collapseButtonHolder->layout()->addItem(hSpacer); + // collapseButton->setContent(frame); + // ui->sectionName->setVisible(false); +} + +VMManagerDetailSection:: +VMManagerDetailSection(const QVariant &varSectionName) : ui(new Ui::DetailSection) +{ + const auto sectionName = varSectionName.toString(); + + // Initialize even though they will get wiped out + // (to keep clang-tidy happy) + frameGridLayout = new QGridLayout(); + const auto outerFrame = new QFrame(); + // for the CSS + outerFrame->setObjectName("outer_frame"); + outerFrame->setContentsMargins(QMargins(0, 0, 0, 0)); + const auto innerFrameLayout = new QVBoxLayout(); + + outerFrame->setLayout(innerFrameLayout); + auto *outerFrameLayout = new QVBoxLayout(); + outerFrameLayout->addWidget(outerFrame); + outerFrameLayout->setContentsMargins(QMargins(0, 0, 0, 0)); + + const auto buttonWidget = new QWidget(this); + + mainLayout = new QVBoxLayout(); + buttonLayout = new QHBoxLayout(); + buttonWidget->setLayout(buttonLayout); + + collapseButton = new CollapseButton(); + setSectionName(sectionName); + buttonLayout->setContentsMargins(getMargins(MarginSection::ToolButton)); + buttonLayout->addWidget(collapseButton); + + // buttonLayout->addStretch(); + auto *hLine = new QFrame(); + hLine->setFrameShape(QFrame::HLine); + hLine->setFrameShadow(QFrame::Sunken); + hLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); + buttonLayout->addWidget(hLine); + + mainLayout->addLayout(buttonLayout); + + frame = new QFrame(); + frame->setFrameShape(QFrame::Box); + frame->setFrameStyle(QFrame::NoFrame); + collapseButton->setContent(frame); + + mainLayout->addWidget(frame); + innerFrameLayout->addWidget(buttonWidget); + innerFrameLayout->addWidget(frame); + setLayout(outerFrameLayout); +} + +VMManagerDetailSection::~VMManagerDetailSection() += default; + +void +VMManagerDetailSection::setSectionName(const QString &name) +{ + sectionName = name; + collapseButton->setButtonText(" " + sectionName); + // Bold the section headers + collapseButton->setStyleSheet(collapseButton->styleSheet().append("font-weight: bold;")); +} + +void +VMManagerDetailSection::addSection(const QString &name, const QString &value, Display::Name displayField) +{ + const auto new_section = DetailSection { name, value}; + sections.push_back(new_section); + setSections(); +} + +void +VMManagerDetailSection::setupMainLayout() +{ + // clang-tidy says I don't need to check before deleting + delete mainLayout; + mainLayout = new QVBoxLayout; +} +void +VMManagerDetailSection::clearContentsSetupGrid() +{ + // Clear everything out + if(frameGridLayout) { + while(frameGridLayout->count()) { + QLayoutItem * cur_item = frameGridLayout->takeAt(0); + if(cur_item->widget()) + delete cur_item->widget(); + delete cur_item; + } + } + + delete frameGridLayout; + frameGridLayout = new QGridLayout(); + qint32 *left = nullptr, *top = nullptr, *right = nullptr, *bottom = nullptr; + frameGridLayout->getContentsMargins(left, top, right, bottom); + frameGridLayout->setContentsMargins(getMargins(MarginSection::DisplayGrid)); + ui->detailFrame->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); + ui->detailFrame->setLayout(frameGridLayout); +} +void +VMManagerDetailSection::setSections() +{ + clearContentsSetupGrid(); + int row = 0; + + + for ( const auto& section : sections) { + // if the string contains the separator (defined elsewhere) then split and + // add each entry on a new line. Otherwise, just add the one. + QStringList sectionsToAdd; + if(section.value.contains(sectionSeparator)) { + sectionsToAdd = section.value.split(sectionSeparator); + } else { + sectionsToAdd.push_back(section.value); + } + bool keyAdded = false; + for(const auto&line : sectionsToAdd) { + if(line.isEmpty()) { + // Don't bother adding entries if the values are blank + continue; + } + const auto labelKey = new QLabel(); + labelKey->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + const auto labelValue = new QLabel(); + labelKey->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); + labelValue->setTextInteractionFlags(labelValue->textInteractionFlags() | Qt::TextSelectableByMouse); + labelKey->setTextInteractionFlags(labelValue->textInteractionFlags() | Qt::TextSelectableByMouse); + + // Reduce the text size for the label + // First, get the existing font + auto smaller_font = labelValue->font(); + // Get a smaller size + // Not sure if I like the smaller size, back to regular for now + // auto smaller_size = 0.85 * smaller_font.pointSize(); + const auto smaller_size = 1 * smaller_font.pointSize(); + // Set the font to the smaller size + smaller_font.setPointSizeF(smaller_size); + // Assign that new, smaller font to the label + labelKey->setFont(smaller_font); + labelValue->setFont(smaller_font); + + labelKey->setText(section.name + ":"); + labelValue->setText(line); + if(!keyAdded) { + frameGridLayout->addWidget(labelKey, row, 0, Qt::AlignLeft); + keyAdded = true; + } + frameGridLayout->addWidget(labelValue, row, 1, Qt::AlignLeft); + const auto hSpacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); + frameGridLayout->addItem(hSpacer, row, 2); + row++; + } + } + collapseButton->setContent(ui->detailFrame); +} +void +VMManagerDetailSection::clear() +{ + sections.clear(); +} + +// QT for Linux and Windows doesn't have the same default margins as QT on MacOS. +// For consistency in appearance we'll have to return the margins on a per-OS basis +QMargins +VMManagerDetailSection::getMargins(const MarginSection section) +{ + switch (section) { + case MarginSection::ToolButton: +#if defined(Q_OS_WINDOWS) or defined(Q_OS_LINUX) + return {10, 0, 5, 0}; +#else + return {0, 0, 5, 0}; +#endif + case MarginSection::DisplayGrid: +#if defined(Q_OS_WINDOWS) or defined(Q_OS_LINUX) + return {10, 0, 0, 10}; +#else + return {0, 0, 0, 10}; +#endif + default: + return {}; + } +} + +// CollapseButton Class + +CollapseButton::CollapseButton(QWidget *parent) : QToolButton(parent), content_(nullptr) { + setCheckable(true); + setStyleSheet("background:none; border:none;"); + setIconSize(QSize(8, 8)); + setFont(QApplication::font()); + setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + connect(this, &QToolButton::toggled, [=](const bool checked) { + setArrowType(checked ? Qt::ArrowType::DownArrow : Qt::ArrowType::RightArrow); + content_ != nullptr && checked ? showContent() : hideContent(); + }); + setChecked(true); +} + +void CollapseButton::setButtonText(const QString &text) { + setText(" " + text); +} + +void CollapseButton::setContent(QWidget *content) { + assert(content != nullptr); + content_ = content; + const auto animation_ = new QPropertyAnimation(content_, "maximumHeight"); // QObject with auto delete + animation_->setStartValue(0); + animation_->setEasingCurve(QEasingCurve::InOutQuad); + animation_->setDuration(300); + animation_->setEndValue(content->geometry().height() + 50); + // qDebug() << "section" << text() << "has a height of" << content->geometry().height(); + animator_.clear(); + animator_.addAnimation(animation_); + if (!isChecked()) { + content->setMaximumHeight(0); + } +} + +void CollapseButton::hideContent() { + animator_.setDirection(QAbstractAnimation::Backward); + animator_.start(); +} + +void CollapseButton::showContent() { + animator_.setDirection(QAbstractAnimation::Forward); + animator_.start(); +} + diff --git a/src/qt/qt_vmmanager_detailsection.hpp b/src/qt/qt_vmmanager_detailsection.hpp new file mode 100644 index 000000000..2eb63685a --- /dev/null +++ b/src/qt/qt_vmmanager_detailsection.hpp @@ -0,0 +1,101 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager system details section module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed + */ + +#ifndef QT_VMMANAGER_DETAILSECTION_H +#define QT_VMMANAGER_DETAILSECTION_H + +#include +#include +#include +#include +#include +#include +#include "qt_vmmanager_system.hpp" + +QT_BEGIN_NAMESPACE +namespace Ui { class DetailSection; } +QT_END_NAMESPACE + +class CollapseButton final : public QToolButton { + Q_OBJECT + +public: + explicit CollapseButton(QWidget *parent = nullptr); + + void setButtonText(const QString &text); + + void setContent(QWidget *content); + + void hideContent(); + + void showContent(); + +private: + QWidget *content_; + QString text_; + QParallelAnimationGroup animator_; +}; + +class VMManagerDetailSection final : public QWidget { + Q_OBJECT + +public: + explicit VMManagerDetailSection(const QString §ionName); + explicit VMManagerDetailSection(const QVariant &varSectionName); + // explicit VMManagerDetailSection(); + + ~VMManagerDetailSection() override; + + void addSection(const QString &name, const QString &value, Display::Name displayField = Display::Name::Unknown); + void clear(); + + CollapseButton *collapseButton; +// QGridLayout *buttonGridLayout; + QGridLayout *frameGridLayout; + QVBoxLayout *mainLayout; + QHBoxLayout *buttonLayout; + QFrame *frame; + + static const QString sectionSeparator; + + +private: + enum class MarginSection { + ToolButton, + DisplayGrid, + }; + + void setSectionName(const QString &name); + void setupMainLayout(); + void clearContentsSetupGrid(); + void setSections(); + + static QMargins getMargins(MarginSection section); + + QString sectionName; + + struct DetailSection { + QString name; + QString value; + }; + + QVector sections; + Ui::DetailSection *ui; + +}; + +#endif // QT_VMMANAGER_DETAILSECTION_H diff --git a/src/qt/qt_vmmanager_detailsection.ui b/src/qt/qt_vmmanager_detailsection.ui new file mode 100644 index 000000000..18e89ee65 --- /dev/null +++ b/src/qt/qt_vmmanager_detailsection.ui @@ -0,0 +1,91 @@ + + + DetailSection + + + + 0 + 0 + 348 + 151 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + + + + + + + + diff --git a/src/qt/qt_vmmanager_listviewdelegate.cpp b/src/qt/qt_vmmanager_listviewdelegate.cpp new file mode 100644 index 000000000..167c805ee --- /dev/null +++ b/src/qt/qt_vmmanager_listviewdelegate.cpp @@ -0,0 +1,241 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager list view delegate module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + + +#include + +#include "qt_vmmanager_listviewdelegate.hpp" +#include "qt_vmmanager_model.hpp" + + +// Thanks to scopchanov https://github.com/scopchanov/SO-MessageLog +// from https://stackoverflow.com/questions/53105343/is-it-possible-to-add-a-custom-widget-into-a-qlistview + + +VMManagerListViewDelegate::VMManagerListViewDelegate(QObject *parent) + : QStyledItemDelegate(parent), + m_ptr(new VMManagerListViewDelegateStyle) +{ +} + +VMManagerListViewDelegate::~VMManagerListViewDelegate() += default; + +void VMManagerListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const { + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + const QPalette &palette(opt.palette); + // opt.rect = opt.rect.adjusted(0, 0, 0, 20); + const QRect &rect(opt.rect); + const QRect &contentRect(rect.adjusted(m_ptr->margins.left(), + m_ptr->margins.top(), + -m_ptr->margins.right(), + -m_ptr->margins.bottom())); + + // The status icon represents the current state of the vm. Initially set to a default state. + QIcon status_icon = QApplication::style()->standardIcon(QStyle::SP_MediaStop); + auto process_variant = index.data(VMManagerModel::Roles::ProcessStatus); + auto process_status = process_variant.value(); + // The main icon, configurable. Falls back to default if it cannot be loaded. + auto customIcom = index.data(VMManagerModel::Roles::Icon).toString(); + opt.icon = QIcon(":/settings/qt/icons/86Box-gray.ico"); + if(!customIcom.isEmpty()) { + if (const auto customPixmap = QPixmap(customIcom); !customPixmap.isNull()) { + opt.icon = customPixmap; + } + } + // opt.icon = QIcon(":/settings/qt/icons/86Box-gray.ico"); + + // Set the status icon based on the process status + switch(process_status) { + case VMManagerSystem::ProcessStatus::Running: + status_icon = QIcon(":/menuicons/qt/icons/run.ico"); + break; + case VMManagerSystem::ProcessStatus::Stopped: + status_icon = QIcon(":/menuicons/qt/icons/acpi_shutdown.ico"); + break; + case VMManagerSystem::ProcessStatus::PausedWaiting: + case VMManagerSystem::ProcessStatus::RunningWaiting: + case VMManagerSystem::ProcessStatus::Paused: + status_icon = QIcon(":/menuicons/qt/icons/pause.ico"); + break; + default: + status_icon = QApplication::style()->standardIcon(QStyle::SP_MessageBoxQuestion); + } + + + // Used to determine if the horizontal separator should be drawn + const bool lastIndex = (index.model()->rowCount() - 1) == index.row(); + const bool hasIcon = !opt.icon.isNull(); + const int bottomEdge = rect.bottom(); + QFont f(opt.font); + + f.setPointSizeF(m_ptr->statusFontPointSize(opt.font)); + + painter->save(); + painter->setClipping(true); + painter->setClipRect(rect); + painter->setFont(opt.font); + + // Draw the background + if (opt.state & QStyle::State_Selected) { + // When selected, only draw the highlighted part until the horizontal separator + int offset = 2; + auto highlightRect = rect.adjusted(0, 0, 0, -offset); + painter->fillRect(highlightRect, palette.highlight().color()); + // Then fill the remainder with the normal color + auto regularRect = rect.adjusted(0, rect.height()-offset, 0, 0); + painter->fillRect(regularRect, palette.light().color()); + } else { + // Otherwise just draw the background color as usual + painter->fillRect(rect, palette.light().color()); + } + + // Draw bottom line. Last line gets a different color + painter->setPen(lastIndex ? palette.dark().color() + : palette.mid().color()); + painter->drawLine(lastIndex ? rect.left() : m_ptr->margins.left(), + bottomEdge, rect.right(), bottomEdge); + + // Draw system icon + if (hasIcon) { + painter->drawPixmap(contentRect.left(), contentRect.top(), + opt.icon.pixmap(m_ptr->iconSize)); + } + + // System name + QRect systemNameRect(m_ptr->systemNameBox(opt, index)); + + systemNameRect.moveTo(m_ptr->margins.left() + m_ptr->iconSize.width() + + m_ptr->spacingHorizontal, contentRect.top()); + // If desired, font can be changed here +// painter->setFont(f); + painter->setFont(opt.font); + painter->setPen(palette.text().color()); + painter->drawText(systemNameRect, Qt::TextSingleLine, opt.text); + + // Draw status icon + painter->drawPixmap(systemNameRect.left(), systemNameRect.bottom() + + m_ptr->spacingVertical, + status_icon.pixmap(m_ptr->smallIconSize)); + + // This rectangle is around the status icon + // auto point = QPoint(systemNameRect.left(), systemNameRect.bottom() + // + m_ptr->spacingVertical); + // auto point2 = QPoint(point.x() + m_ptr->smallIconSize.width(), point.y() + m_ptr->smallIconSize.height()); + // auto arect = QRect(point, point2); + // painter->drawRect(arect); + + // Draw status text + QRect statusRect(m_ptr->statusBox(opt, index)); + int extraaa = 2; + statusRect.moveTo(systemNameRect.left() + m_ptr->margins.left() + m_ptr->smallIconSize.width(), + systemNameRect.bottom() + m_ptr->spacingVertical + extraaa + (m_ptr->smallIconSize.height() - systemNameRect.height() )); + +// painter->setFont(opt.font); + painter->setFont(f); + painter->setPen(palette.windowText().color()); + painter->drawText(statusRect, Qt::TextSingleLine, + index.data(VMManagerModel::Roles::ProcessStatusString).toString()); + + painter->restore(); + +} + +QMargins VMManagerListViewDelegate::contentsMargins() const +{ + return m_ptr->margins; +} + +void VMManagerListViewDelegate::setContentsMargins(const int left, const int top, const int right, const int bottom) const +{ + m_ptr->margins = QMargins(left, top, right, bottom); +} + +int VMManagerListViewDelegate::horizontalSpacing() const +{ + return m_ptr->spacingHorizontal; +} + +void VMManagerListViewDelegate::setHorizontalSpacing(const int spacing) const +{ + m_ptr->spacingHorizontal = spacing; +} + +int VMManagerListViewDelegate::verticalSpacing() const +{ + return m_ptr->spacingVertical; +} + +void VMManagerListViewDelegate::setVerticalSpacing(const int spacing) const +{ + m_ptr->spacingVertical = spacing; +} + + +QSize VMManagerListViewDelegate::sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + const int textHeight = m_ptr->systemNameBox(opt, index).height() + + m_ptr->spacingVertical + m_ptr->statusBox(opt, index).height(); + const int iconHeight = m_ptr->iconSize.height(); + const int h = textHeight > iconHeight ? textHeight : iconHeight; + + // return the same width + // for height, add margins on top and bottom *plus* either the text or icon height, whichever is greater + // Note: text height is the combined value of the system name and the status just below the name + return {opt.rect.width(), m_ptr->margins.top() + h + + m_ptr->margins.bottom()}; +} + +VMManagerListViewDelegateStyle::VMManagerListViewDelegateStyle() : + iconSize(32, 32), + smallIconSize(16, 16), + // bottom gets a little more than the top because of the custom separator + margins(4, 10, 8, 12), + // Spacing between icon and text + spacingHorizontal(8), + spacingVertical(4) +{ + +} + +QRect VMManagerListViewDelegateStyle::statusBox(const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QFont f(option.font); + + f.setPointSizeF(statusFontPointSize(option.font)); + + return QFontMetrics(f).boundingRect(index.data(VMManagerModel::Roles::ProcessStatusString).toString()) + .adjusted(0, 0, 1, 1); +} + +qreal VMManagerListViewDelegateStyle::statusFontPointSize(const QFont &f) const +{ + return 0.75*f.pointSize(); +// return 1*f.pointSize(); +} + +QRect VMManagerListViewDelegateStyle::systemNameBox(const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return option.fontMetrics.boundingRect(option.text).adjusted(0, 0, 1, 1); +} \ No newline at end of file diff --git a/src/qt/qt_vmmanager_listviewdelegate.hpp b/src/qt/qt_vmmanager_listviewdelegate.hpp new file mode 100644 index 000000000..84325086d --- /dev/null +++ b/src/qt/qt_vmmanager_listviewdelegate.hpp @@ -0,0 +1,67 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for 86Box VM manager list view delegate module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#ifndef QT_VMMANAGER_LISTVIEWDELEGATE_H +#define QT_VMMANAGER_LISTVIEWDELEGATE_H + +#include +#include +#include "qt_vmmanager_system.hpp" + +class VMManagerListViewDelegateStyle +{ + VMManagerListViewDelegateStyle(); + + [[nodiscard]] inline QRect systemNameBox(const QStyleOptionViewItem &option, + const QModelIndex &index) const; + [[nodiscard]] inline qreal statusFontPointSize(const QFont &f) const; + [[nodiscard]] inline QRect statusBox(const QStyleOptionViewItem &option, const QModelIndex &index) const; + + QSize iconSize; + QSize smallIconSize; + QMargins margins; + int spacingHorizontal; + int spacingVertical; + + friend class VMManagerListViewDelegate; +}; + +class VMManagerListViewDelegate final : public QStyledItemDelegate { + Q_OBJECT + +public: + explicit VMManagerListViewDelegate(QObject *parent = nullptr); + ~VMManagerListViewDelegate() override; + using QStyledItemDelegate::QStyledItemDelegate; + + [[nodiscard]] QMargins contentsMargins() const; + void setContentsMargins(int left, int top, int right, int bottom) const; + + [[nodiscard]] int horizontalSpacing() const; + void setHorizontalSpacing(int spacing) const; + + [[nodiscard]] int verticalSpacing() const; + void setVerticalSpacing(int spacing) const; + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + [[nodiscard]] QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +private: + VMManagerListViewDelegateStyle *m_ptr; +}; +#endif // QT_VMMANAGER_LISTVIEWDELEGATE_H \ No newline at end of file diff --git a/src/qt/qt_vmmanager_main.cpp b/src/qt/qt_vmmanager_main.cpp new file mode 100644 index 000000000..a8480eee7 --- /dev/null +++ b/src/qt/qt_vmmanager_main.cpp @@ -0,0 +1,518 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager main module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "qt_vmmanager_main.hpp" +#include "ui_qt_vmmanager_main.h" +#include "qt_vmmanager_model.hpp" +#include "qt_vmmanager_addmachine.hpp" + +VMManagerMain::VMManagerMain(QWidget *parent) : + QWidget(parent), ui(new Ui::VMManagerMain), selected_sysconfig(new VMManagerSystem) { + ui->setupUi(this); + this->setWindowTitle("86Box VM Manager"); + + // Set up the main listView + ui->listView->setItemDelegate(new VMManagerListViewDelegate); + vm_model = new VMManagerModel; + proxy_model = new StringListProxyModel(this); + proxy_model->setSourceModel(vm_model); + ui->listView->setModel(proxy_model); + proxy_model->setSortCaseSensitivity(Qt::CaseInsensitive); + ui->listView->model()->sort(0, Qt::AscendingOrder); + + // Connect the model signal + connect(vm_model, &VMManagerModel::systemDataChanged, this, &VMManagerMain::modelDataChange); + + // Set up the context menu for the list view + ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->listView, &QListView::customContextMenuRequested, [this](const QPoint &pos) { + const auto indexAt = ui->listView->indexAt(pos); + if (indexAt.isValid()) { + QMenu contextMenu(tr("Context Menu"), ui->listView); + + QAction nameChangeAction(tr("Change display name")); + contextMenu.addAction(&nameChangeAction); + // Use a lambda to call a function so indexAt can be passed + connect(&nameChangeAction, &QAction::triggered, ui->listView, [this, indexAt] { + updateDisplayName(indexAt); + }); + + QAction openSystemFolderAction(tr("Open folder")); + contextMenu.addAction(&openSystemFolderAction); + connect(&openSystemFolderAction, &QAction::triggered, [this, indexAt] { + if (const auto configDir = indexAt.data(VMManagerModel::Roles::ConfigDir).toString(); !configDir.isEmpty()) { + QDesktopServices::openUrl(QUrl(QString("file:///") + configDir)); + } + }); + + QAction convertToP3(tr("Convert system to PIII")); + contextMenu.addAction(&convertToP3); + convertToP3.setEnabled(false); + + QAction setSystemIcon(tr("Set icon")); + contextMenu.addAction(&setSystemIcon); + connect(&setSystemIcon, &QAction::triggered, [this, indexAt] { + IconSelectionDialog dialog(":/systemicons/"); + if(dialog.exec() == QDialog::Accepted) { + const QString iconName = dialog.getSelectedIconName(); + // A Blank iconName will cause setIcon to reset to the default + selected_sysconfig->setIcon(iconName); + } + }); + + contextMenu.addSeparator(); + + QAction showRawConfigFile(tr("Show config file")); + contextMenu.addAction(&showRawConfigFile); + connect(&showRawConfigFile, &QAction::triggered, [this, indexAt] { + if (const auto configFile = indexAt.data(VMManagerModel::Roles::ConfigFile).toString(); !configFile.isEmpty()) { + showTextFileContents(indexAt.data(Qt::DisplayRole).toString(), configFile); + } + }); + + contextMenu.exec(ui->listView->viewport()->mapToGlobal(pos)); + } + }); + + // Initial default details view + vm_details = new VMManagerDetails(); + ui->detailsArea->layout()->addWidget(vm_details); + const QItemSelectionModel *selection_model = ui->listView->selectionModel(); + + connect(selection_model, &QItemSelectionModel::currentChanged, this, &VMManagerMain::currentSelectionChanged); + // If there are items in the model, make sure to select the first item by default. + // When settings are loaded, the last selected item will be selected (if available) + if (proxy_model->rowCount(QModelIndex()) > 0) { + const QModelIndex first_index = proxy_model->index(0, 0); + ui->listView->setCurrentIndex(first_index); + } + + // Load and apply settings + loadSettings(); + + // Set up search bar + connect(ui->searchBar, &QLineEdit::textChanged, this, &VMManagerMain::searchSystems); + // Create the completer + auto *completer = new QCompleter(this); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + // Get the completer list + const auto allStrings = getSearchCompletionList(); + // Set up the completer + auto *completerModel = new QStringListModel(allStrings, completer); + completer->setModel(completerModel); + ui->searchBar->setCompleter(completer); + + // Set initial status bar after the event loop starts + QTimer::singleShot(0, this, [this] { + emit updateStatusRight(totalCountString()); + }); + + // Start update check after a slight delay + QTimer::singleShot(1000, this, [this] { + if(updateCheck) { + backgroundUpdateCheckStart(); + } + }); +} + +VMManagerMain::~VMManagerMain() { + delete ui; + delete vm_model; +} + +void +VMManagerMain::currentSelectionChanged(const QModelIndex ¤t, + const QModelIndex &previous) +{ + if(!current.isValid()) { + return; + } + + const auto mapped_index = proxy_model->mapToSource(current); + selected_sysconfig = vm_model->getConfigObjectForIndex(mapped_index); + vm_details->updateData(selected_sysconfig); + + // Emit that the selection changed, include with the process state + emit selectionChanged(current, selected_sysconfig->process->state()); + +} + +void +VMManagerMain::settingsButtonPressed() { + if(!currentSelectionIsValid()) { + return; + } + selected_sysconfig->launchSettings(); + // If the process is already running, the system will be instructed to open its settings window. + // Otherwise the process will be launched and will need to be tracked here. + if (!selected_sysconfig->isProcessRunning()) { + connect(selected_sysconfig->process, QOverload::of(&QProcess::finished), + [=](const int exitCode, const QProcess::ExitStatus exitStatus){ + if (exitCode != 0 || exitStatus != QProcess::NormalExit) { + qInfo().nospace().noquote() << "Abnormal program termination while launching settings: exit code " << exitCode << ", exit status " << exitStatus; + return; + } + selected_sysconfig->reloadConfig(); + vm_details->updateData(selected_sysconfig); + }); + } +} + +void +VMManagerMain::startButtonPressed() const +{ + if(!currentSelectionIsValid()) { + return; + } + selected_sysconfig->startButtonPressed(); +} + +void +VMManagerMain::restartButtonPressed() const +{ + if(!currentSelectionIsValid()) { + return; + } + selected_sysconfig->restartButtonPressed(); + +} + +void +VMManagerMain::pauseButtonPressed() const +{ + if(!currentSelectionIsValid()) { + return; + } + selected_sysconfig->pauseButtonPressed(); +} + +void +VMManagerMain::shutdownRequestButtonPressed() const +{ + if (!currentSelectionIsValid()) { + return; + } + selected_sysconfig->shutdownRequestButtonPressed(); +} + +void +VMManagerMain::shutdownForceButtonPressed() const +{ + if (!currentSelectionIsValid()) { + return; + } + selected_sysconfig->shutdownForceButtonPressed(); +} + +// This function doesn't appear to be needed any longer +void +VMManagerMain::refresh() +{ + bool running = selected_sysconfig->process->state() == QProcess::ProcessState::Running; + const auto current_index = ui->listView->currentIndex(); + emit selectionChanged(current_index, selected_sysconfig->process->state()); + + // if(!selected_sysconfig->config_file.path().isEmpty()) { + if(!selected_sysconfig->isValid()) { + // what was happening here? + } +} + +void +VMManagerMain::updateDisplayName(const QModelIndex &index) +{ + QDialog dialog; + dialog.setMinimumWidth(400); + dialog.setWindowTitle(tr("Set display name")); + const auto layout = new QVBoxLayout(&dialog); + const auto label = new QLabel(tr("Enter the new display name (blank to reset)")); + label->setAlignment(Qt::AlignHCenter); + label->setContentsMargins(QMargins(0, 0, 0, 5)); + layout->addWidget(label); + const auto lineEdit = new QLineEdit(index.data().toString(), &dialog); + layout->addWidget(lineEdit); + lineEdit->selectAll(); + + const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + layout->addWidget(buttonBox); + + connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + if (const bool accepted = dialog.exec() == QDialog::Accepted; accepted) { + const auto mapped_index = proxy_model->mapToSource(index); + vm_model->updateDisplayName(mapped_index, lineEdit->text()); + selected_sysconfig = vm_model->getConfigObjectForIndex(mapped_index); + vm_details->updateData(selected_sysconfig); + ui->listView->scrollTo(ui->listView->currentIndex(), QAbstractItemView::PositionAtCenter); + } +} + +void +VMManagerMain::loadSettings() +{ + const auto config = new VMManagerConfig(VMManagerConfig::ConfigType::General); + const auto lastSelection = config->getStringValue("last_selection"); + updateCheck = config->getStringValue("update_check").toInt(); + regexSearch = config->getStringValue("regex_search").toInt(); + + const auto matches = ui->listView->model()->match(vm_model->index(0, 0), VMManagerModel::Roles::ConfigName, QVariant::fromValue(lastSelection)); + if (!matches.empty()) { + ui->listView->setCurrentIndex(matches.first()); + ui->listView->scrollTo(ui->listView->currentIndex(), QAbstractItemView::PositionAtCenter); + } +} +bool +VMManagerMain::currentSelectionIsValid() const +{ + return ui->listView->currentIndex().isValid() && selected_sysconfig->isValid(); +} + +// Used from MainWindow during app exit to obtain and persist the current selection +QString +VMManagerMain::getCurrentSelection() const +{ + return ui->listView->currentIndex().data(VMManagerModel::Roles::ConfigName).toString(); +} + +void +VMManagerMain::searchSystems(const QString &text) const +{ + // Escape the search text string unless regular expression searching is enabled. + // When escaped, the search string functions as a plain text match. + const auto searchText = regexSearch ? text : QRegularExpression::escape(text); + const QRegularExpression regex(searchText, QRegularExpression::CaseInsensitiveOption); + if (!regex.isValid()) { + qDebug() << "Skipping, invalid regex"; + return; + } + proxy_model->setFilterRegularExpression(regex); + // Searching (filtering) can cause the list view to change. If there is still a valid selection, + // make sure to scroll to it + if (ui->listView->currentIndex().isValid()) { + ui->listView->scrollTo(ui->listView->currentIndex(), QAbstractItemView::PositionAtCenter); + } +} + +void +VMManagerMain::newMachineWizard() +{ + const auto wizard = new VMManagerAddMachine(this); + if (wizard->exec() == QDialog::Accepted) { + const auto newName = wizard->field("systemName").toString(); + const auto systemDir = wizard->field("systemLocation").toString(); + const auto existingConfiguration = wizard->field("existingConfiguration").toString(); + addNewSystem(newName, systemDir, existingConfiguration); + } +} + +void +VMManagerMain::addNewSystem(const QString &name, const QString &dir, const QString &configFile) +{ + const auto newSytemDirectory = QDir(QDir::cleanPath(dir + "/" + name)); + + // qt replaces `/` with native separators + const auto newSystemConfigFile = QFileInfo(newSytemDirectory.path() + "/" + "86box.cfg"); + if (newSystemConfigFile.exists() || newSytemDirectory.exists()) { + QMessageBox::critical(this, tr("Directory in use"), tr("The selected directory is already in use. Please select a different directory.")); + return; + } + // Create the directory + const QDir qmkdir; + if (const bool mkdirResult = qmkdir.mkdir(newSytemDirectory.path()); !mkdirResult) { + QMessageBox::critical(this, tr("Create directory failed"), tr("Unable to create the directory for the new system")); + return; + } + // If specified, write the contents of the configuration file before starting + if (!configFile.isEmpty()) { + const auto configPath = newSystemConfigFile.absoluteFilePath(); + const auto file = new QFile(configPath); + if (!file->open(QIODevice::WriteOnly)) { + qWarning() << "Unable to open file " << configPath; + QMessageBox::critical(this, tr("Configuration write failed"), tr("Unable to open the configuration file at %1 for writing").arg(configPath)); + return; + } + file->write(configFile.toUtf8()); + file->flush(); + file->close(); + } + + const auto new_system = new VMManagerSystem(newSystemConfigFile.absoluteFilePath()); + new_system->launchSettings(); + // Handle this in a closure so we can capture the temporary new_system object + connect(new_system->process, QOverload::of(&QProcess::finished), + [=](const int exitCode, const QProcess::ExitStatus exitStatus) { + if (exitCode != 0 || exitStatus != QProcess::NormalExit) { + qInfo().nospace().noquote() << "Abnormal program termination while creating new system: exit code " << exitCode << ", exit status " << exitStatus; + qInfo() << "Not adding system due to errors"; + QMessageBox::critical(this, tr("Error adding system"), + tr("Abnormal program termination while creating new system: exit code %1, exit status %2.\n\nThe system will not be added.").arg(QString::number(exitCode), exitStatus)); + delete new_system; + return; + } + // Create a new QFileInfo because the info from the old one may be cached + if (const auto fi = QFileInfo(new_system->config_file.absoluteFilePath()); !fi.exists()) { + // No config file which means the cancel button was pressed in the settings dialog + // Attempt to clean up the directory that was created + const QDir qrmdir; + if (const bool result = qrmdir.rmdir(newSytemDirectory.path()); !result) { + qWarning() << "Error cleaning up the old directory for canceled operation. Continuing anyway."; + } + delete new_system; + return; + } + const auto current_index = ui->listView->currentIndex(); + vm_model->reload(); + const auto created_object = vm_model->getIndexForConfigFile(new_system->config_file); + if (created_object.row() < 0) { + // For some reason the index of the new object couldn't be determined. Fall back to the old index. + ui->listView->setCurrentIndex(current_index); + delete new_system; + return; + } + // Get the index of the newly-created system and select it + const QModelIndex mapped_index = proxy_model->mapFromSource(created_object); + ui->listView->setCurrentIndex(mapped_index); + delete new_system; + }); +} + +QStringList +VMManagerMain::getSearchCompletionList() const +{ + QSet uniqueStrings; + for (int row = 0; row < vm_model->rowCount(QModelIndex()); ++row) { + QModelIndex index = vm_model->index(row, 0); + auto fullList = vm_model->data(index, VMManagerModel::Roles::SearchList).toStringList(); + QSet uniqueSet(fullList.begin(), fullList.end()); + uniqueStrings.unite(uniqueSet); + } + // Convert the set back to a QStringList + QStringList allStrings = uniqueStrings.values(); + return allStrings; +} + +QString +VMManagerMain::totalCountString() const +{ + const auto count = vm_model->rowCount(QModelIndex()); + return QString("%1 %2").arg(QString::number(count), tr("total")); +} + +void +VMManagerMain::modelDataChange() +{ + // Model data has changed. This includes process status. + // Update the counts / totals accordingly + auto modelStats = vm_model->getProcessStats(); + QStringList stats; + for (auto it = modelStats.constBegin(); it != modelStats.constEnd(); ++it) { + const auto &key = it.key(); + stats.append(QString("%1 %2").arg(QString::number(modelStats[key]), key)); + } + auto states = stats.join(", "); + if (!modelStats.isEmpty()) { + states.append(", "); + } + + emit updateStatusRight(states + totalCountString()); +} + +void +VMManagerMain::onPreferencesUpdated() +{ + // Only reload values that we care about + const auto config = new VMManagerConfig(VMManagerConfig::ConfigType::General); + const auto oldRegexSearch = regexSearch; + regexSearch = config->getStringValue("regex_search").toInt(); + if (oldRegexSearch != regexSearch) { + ui->searchBar->clear(); + } +} + +void +VMManagerMain::backgroundUpdateCheckStart() const +{ + auto updateChannel = UpdateCheck::UpdateChannel::CI; +#ifdef RELEASE_BUILD + updateChannel = UpdateCheck::UpdateChannel::Stable; +#endif + const auto updateCheck = new UpdateCheck(updateChannel); + connect(updateCheck, &UpdateCheck::updateCheckComplete, this, &VMManagerMain::backgroundUpdateCheckComplete); + connect(updateCheck, &UpdateCheck::updateCheckError, this, &VMManagerMain::backgroundUpdateCheckError); + updateCheck->checkForUpdates(); +} + +void +VMManagerMain::backgroundUpdateCheckComplete(const UpdateCheck::UpdateResult &result) +{ + qDebug() << "Check complete: update available?" << result.updateAvailable; + auto type = result.channel == UpdateCheck::UpdateChannel::CI ? tr("Build") : tr("Version"); + const auto updateMessage = QString("%1: %2 %3").arg( tr("An update to 86Box is available"), type, result.latestVersion); + emit updateStatusLeft(updateMessage); +} + +void +VMManagerMain::backgroundUpdateCheckError(const QString &errorMsg) +{ + qDebug() << "Update check failed with the following error:" << errorMsg; + // TODO: Update the status bar +} + +void +VMManagerMain::showTextFileContents(const QString &title, const QString &path) +{ + // Make sure we can open the file + const auto fi = QFileInfo(path); + if(!fi.exists()) { + qWarning("Requested file does not exist: %s", path.toUtf8().constData()); + return; + } + // Read the file + QFile displayFile(path); + if (!displayFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning("Couldn't open the file: error %d", displayFile.error()); + return; + } + const QString configFileContents = displayFile.readAll(); + displayFile.close(); + + const auto textDisplayDialog = new QDialog(this); + textDisplayDialog->setFixedSize(QSize(540, 360)); + textDisplayDialog->setWindowTitle(QString("%1 - %2").arg(title, fi.fileName())); + + const auto textEdit = new QPlainTextEdit(); + textEdit->setReadOnly(true); + textEdit->setPlainText(configFileContents); + const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok); + connect(buttonBox, &QDialogButtonBox::accepted, textDisplayDialog, &QDialog::accept); + const auto layout = new QVBoxLayout(); + textDisplayDialog->setLayout(layout); + textDisplayDialog->layout()->addWidget(textEdit); + textDisplayDialog->layout()->addWidget(buttonBox); + textDisplayDialog->exec(); +} diff --git a/src/qt/qt_vmmanager_main.hpp b/src/qt/qt_vmmanager_main.hpp new file mode 100644 index 000000000..c3c541b4f --- /dev/null +++ b/src/qt/qt_vmmanager_main.hpp @@ -0,0 +1,167 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for 86Box VM manager main module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#ifndef QT_VMMANAGER_MAIN_H +#define QT_VMMANAGER_MAIN_H + +#include "qt_updatecheck.hpp" + +#include +#include "qt_vmmanager_model.hpp" +#include "qt_vmmanager_details.hpp" +#include "qt_vmmanager_listviewdelegate.hpp" + +#include + +extern "C" { +#include <86box/86box.h> // for vmm_path +} + + +QT_BEGIN_NAMESPACE +namespace Ui { class VMManagerMain; } +QT_END_NAMESPACE + +class VMManagerMain final : public QWidget { + Q_OBJECT + +public: + explicit VMManagerMain(QWidget *parent = nullptr); + ~VMManagerMain() override; + // Used to save the current selection + [[nodiscard]] QString getCurrentSelection() const; + + enum class ToolbarButton { + Start, + Pause, + StartPause, + Shutdown, + Reset, + CtrlAltDel, + Settings, + }; +signals: + void selectionChanged(const QModelIndex ¤tSelection, QProcess::ProcessState processState); + void updateStatusLeft(const QString &text); + void updateStatusRight(const QString &text); + +public slots: + void startButtonPressed() const; + void settingsButtonPressed(); + void restartButtonPressed() const; + void pauseButtonPressed() const; + void shutdownRequestButtonPressed() const; + void shutdownForceButtonPressed() const; + void searchSystems(const QString &text) const; + void newMachineWizard(); + void addNewSystem(const QString &name, const QString &dir, const QString &configFile = {}); + [[nodiscard]] QStringList getSearchCompletionList() const; + void modelDataChange(); + void onPreferencesUpdated(); + +private: + Ui::VMManagerMain *ui; + + VMManagerModel *vm_model; + VMManagerDetails *vm_details; + VMManagerSystem *selected_sysconfig; + // VMManagerConfig *config; + QSortFilterProxyModel *proxy_model; + bool updateCheck = false; + bool regexSearch = false; + + // void updateSelection(const QItemSelection &selected, + // const QItemSelection &deselected); + void currentSelectionChanged(const QModelIndex ¤t, + const QModelIndex &previous); + void refresh(); + void updateDisplayName(const QModelIndex &index); + void loadSettings(); + [[nodiscard]] bool currentSelectionIsValid() const; + [[nodiscard]] QString totalCountString() const; + void backgroundUpdateCheckStart() const; + void showTextFileContents(const QString &title, const QString &path); +private slots: + void backgroundUpdateCheckComplete(const UpdateCheck::UpdateResult &result); + void backgroundUpdateCheckError(const QString &errorMsg); +}; + +#include +#include +#include +#include + +class IconSelectionDialog final : public QDialog { + Q_OBJECT + +public: + explicit IconSelectionDialog(QString assetPath, QWidget *parent = nullptr) : QDialog(parent), listWidget(new QListWidget) { + // Set the list widget to icon mode + listWidget->setViewMode(QListWidget::IconMode); + setFixedSize(QSize(540, 360)); + listWidget->setGridSize(QSize(96, 96)); + listWidget->setIconSize(QSize(64, 64)); + // Read in all the assets from the given path + const QDir iconsDir(assetPath); + if (!assetPath.endsWith("/")) { + assetPath.append("/"); + } + setWindowTitle(tr("Select an icon")); + + // Loop on all files and add them as items (icons) in QListWidget + for(const QString& iconName : iconsDir.entryList()) { + const auto item = new QListWidgetItem(QIcon(assetPath + iconName), iconName); + // Set the UserRole to the resource bundle path + item->setData(Qt::UserRole, assetPath + iconName); + listWidget->addItem(item); + } + + // Dialog buttons + const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Reset); + // Use the reset button for resetting the icon to the default + const QPushButton* resetButton = buttonBox->button(QDialogButtonBox::Reset); + + // Connect the buttons signals + connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(listWidget, &QListWidget::doubleClicked, this, &QDialog::accept); + connect(resetButton, &QPushButton::clicked, [this] { + // For reset, set the index to invalid so the caller will receive a blank string + listWidget->setCurrentIndex(QModelIndex()); + // Then accept + QDialog::accept(); + }); + + const auto layout = new QVBoxLayout(this); + layout->addWidget(listWidget); + layout->addWidget(buttonBox); + } + + public slots: + [[nodiscard]] QString getSelectedIconName() const { + if (listWidget->currentIndex().isValid()) { + return listWidget->currentItem()->data(Qt::UserRole).toString(); + } + // Index is invalid because the reset button was pressed + return {}; + } + +private: + QListWidget* listWidget; +}; + +#endif //QT_VMMANAGER_MAIN_H diff --git a/src/qt/qt_vmmanager_main.ui b/src/qt/qt_vmmanager_main.ui new file mode 100644 index 000000000..c19094345 --- /dev/null +++ b/src/qt/qt_vmmanager_main.ui @@ -0,0 +1,119 @@ + + + VMManagerMain + + + + 0 + 0 + 815 + 472 + + + + + 0 + 0 + + + + VMManagerMain + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 200 + 0 + + + + + 200 + 16777215 + + + + + + + + Qt::ClickFocus + + + Search + + + true + + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + diff --git a/src/qt/qt_vmmanager_mainwindow.cpp b/src/qt/qt_vmmanager_mainwindow.cpp new file mode 100644 index 000000000..767bbcef2 --- /dev/null +++ b/src/qt/qt_vmmanager_mainwindow.cpp @@ -0,0 +1,178 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager main window +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#include "qt_vmmanager_mainwindow.hpp" +#include "qt_vmmanager_main.hpp" +#include "qt_vmmanager_preferences.hpp" +#include "ui_qt_vmmanager_mainwindow.h" +#include "qt_updatecheckdialog.hpp" + +#include +#include +#include + +VMManagerMainWindow:: +VMManagerMainWindow(QWidget *parent) + : ui(new Ui::VMManagerMainWindow) + , vmm(new VMManagerMain(this)) + , statusLeft(new QLabel) + , statusRight(new QLabel) +{ + ui->setupUi(this); + + // Connect signals from the VMManagerMain widget + connect(vmm, &VMManagerMain::selectionChanged, this, &VMManagerMainWindow::vmmSelectionChanged); + + setWindowTitle(tr("86Box VM Manager")); + setCentralWidget(vmm); + + // Set up the buttons + connect(ui->actionStartPause, &QAction::triggered, vmm, &VMManagerMain::startButtonPressed); + connect(ui->actionSettings, &QAction::triggered, vmm, &VMManagerMain::settingsButtonPressed); + connect(ui->actionHard_Reset, &QAction::triggered, vmm, &VMManagerMain::restartButtonPressed); + connect(ui->actionForce_Shutdown, &QAction::triggered, vmm, &VMManagerMain::shutdownForceButtonPressed); + connect(ui->actionNew_Machine, &QAction::triggered, vmm, &VMManagerMain::newMachineWizard); + + // Set up menu actions + connect(ui->actionCheck_for_updates, &QAction::triggered, this, &VMManagerMainWindow::checkForUpdatesTriggered); + + // TODO: Remove all of this (all the way to END REMOVE) once certain the search will no longer be in the toolbar. + // BEGIN REMOVE + // Everything is still setup here for it but it is all hidden. None of it will be + // needed if the search stays in VMManagerMain + ui->actionStartPause->setEnabled(true); + ui->actionStartPause->setIcon(QIcon(":/menuicons/qt/icons/run.ico")); + ui->actionStartPause->setText(tr("Start")); + ui->actionStartPause->setToolTip(tr("Start")); + ui->actionHard_Reset->setEnabled(false); + ui->actionForce_Shutdown->setEnabled(false); + ui->actionCtrl_Alt_Del->setEnabled(false); + + const auto searchBar = new QLineEdit(); + searchBar->setMinimumWidth(150); + searchBar->setPlaceholderText(" " + tr("Search")); + searchBar->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); + searchBar->setClearButtonEnabled(true); + // Spacer to make the search go all the way to the right + const auto spacer = new QWidget(); + spacer->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Preferred); + ui->toolBar->addWidget(spacer); + ui->toolBar->addWidget(searchBar); + // Connect signal for search + connect(searchBar, &QLineEdit::textChanged, vmm, &VMManagerMain::searchSystems); + // Preferences + connect(ui->actionPreferences, &QAction::triggered, this, &VMManagerMainWindow::preferencesTriggered); + + // Create a completer for the search bar + auto *completer = new QCompleter(this); + completer->setCaseSensitivity(Qt::CaseInsensitive); + completer->setFilterMode(Qt::MatchContains); + // Get the completer list + const auto allStrings = vmm->getSearchCompletionList(); + // Set up the completer + auto *completerModel = new QStringListModel(allStrings, completer); + completer->setModel(completerModel); + searchBar->setCompleter(completer); + ui->toolBar->setVisible(false); + // END REMOVE + + // Status bar widgets + statusLeft->setAlignment(Qt::AlignLeft); + statusRight->setAlignment(Qt::AlignRight); + ui->statusbar->addPermanentWidget(statusLeft, 1); + ui->statusbar->addPermanentWidget(statusRight, 1); + connect(vmm, &VMManagerMain::updateStatusLeft, this, &VMManagerMainWindow::setStatusLeft); + connect(vmm, &VMManagerMain::updateStatusRight, this, &VMManagerMainWindow::setStatusRight); + + // Inform the main view when preferences are updated + connect(this, &VMManagerMainWindow::preferencesUpdated, vmm, &VMManagerMain::onPreferencesUpdated); + +} + +VMManagerMainWindow::~ +VMManagerMainWindow() + = default; + +void +VMManagerMainWindow::vmmSelectionChanged(const QModelIndex ¤tSelection, const QProcess::ProcessState processState) const +{ + if (processState == QProcess::Running) { + ui->actionStartPause->setEnabled(true); + ui->actionStartPause->setIcon(QIcon(":/menuicons/qt/icons/pause.ico")); + ui->actionStartPause->setText(tr("Pause")); + ui->actionStartPause->setToolTip(tr("Pause")); + ui->actionHard_Reset->setEnabled(true); + ui->actionForce_Shutdown->setEnabled(true); + ui->actionCtrl_Alt_Del->setEnabled(true); + } else { + ui->actionStartPause->setEnabled(true); + ui->actionStartPause->setIcon(QIcon(":/menuicons/qt/icons/run.ico")); + ui->actionStartPause->setText(tr("Start")); + ui->actionStartPause->setToolTip(tr("Start")); + ui->actionHard_Reset->setEnabled(false); + ui->actionForce_Shutdown->setEnabled(false); + ui->actionCtrl_Alt_Del->setEnabled(false); + } +} +void +VMManagerMainWindow::preferencesTriggered() +{ + const auto prefs = new VMManagerPreferences(); + if (prefs->exec() == QDialog::Accepted) { + emit preferencesUpdated(); + } +} + +void +VMManagerMainWindow::saveSettings() const +{ + const auto currentSelection = vmm->getCurrentSelection(); + const auto config = new VMManagerConfig(VMManagerConfig::ConfigType::General); + config->setStringValue("last_selection", currentSelection); + // Sometimes required to ensure the settings save before the app exits + config->sync(); +} + +void +VMManagerMainWindow::closeEvent(QCloseEvent *event) +{ + saveSettings(); + QMainWindow::closeEvent(event); +} + +void +VMManagerMainWindow::setStatusLeft(const QString &text) const +{ + statusLeft->setText(text); +} + +void +VMManagerMainWindow::setStatusRight(const QString &text) const +{ + statusRight->setText(text); +} + +void +VMManagerMainWindow::checkForUpdatesTriggered() +{ + auto updateChannel = UpdateCheck::UpdateChannel::CI; +#ifdef RELEASE_BUILD + updateChannel = UpdateCheck::UpdateChannel::Stable; +#endif + const auto updateCheck = new UpdateCheckDialog(updateChannel); + updateCheck->exec(); +} diff --git a/src/qt/qt_vmmanager_mainwindow.hpp b/src/qt/qt_vmmanager_mainwindow.hpp new file mode 100644 index 000000000..764ed5ef8 --- /dev/null +++ b/src/qt/qt_vmmanager_mainwindow.hpp @@ -0,0 +1,59 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for 86Box VM manager main window +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#ifndef VMM_MAINWINDOW_H +#define VMM_MAINWINDOW_H + +#include "qt_vmmanager_main.hpp" + +#include +#include +#include + +namespace Ui { +class VMManagerMainWindow; +} + +class VMManagerMainWindow final : public QMainWindow +{ + Q_OBJECT +public: + explicit VMManagerMainWindow(QWidget *parent = nullptr); + ~VMManagerMainWindow() override; +signals: + void preferencesUpdated(); + +private: + Ui::VMManagerMainWindow *ui; + VMManagerMain *vmm; + void saveSettings() const; + QLabel *statusLeft; + QLabel *statusRight; +public slots: + void setStatusLeft(const QString &text) const; + void setStatusRight(const QString &text) const; + +private slots: + void vmmSelectionChanged(const QModelIndex ¤tSelection, QProcess::ProcessState processState) const; + void preferencesTriggered(); + static void checkForUpdatesTriggered(); + +protected: + void closeEvent(QCloseEvent *event) override; +}; + +#endif // VMM_MAINWINDOW_H diff --git a/src/qt/qt_vmmanager_mainwindow.ui b/src/qt/qt_vmmanager_mainwindow.ui new file mode 100644 index 000000000..b19610593 --- /dev/null +++ b/src/qt/qt_vmmanager_mainwindow.ui @@ -0,0 +1,224 @@ + + + VMManagerMainWindow + + + + 0 + 0 + 900 + 600 + + + + MainWindow + + + + + + 0 + 0 + 900 + 24 + + + + + Tools + + + + + + + Machine + + + + + + + + + + + File + + + + + + + + + + + toolBar + + + false + + + + 16 + 16 + + + + Qt::ToolButtonIconOnly + + + TopToolBarArea + + + false + + + + + + + + + + + Do something + + + + + true + + + + :/menuicons/qt/icons/run.ico:/menuicons/qt/icons/run.ico + + + Start + + + false + + + + + + :/menuicons/qt/icons/hard_reset.ico:/menuicons/qt/icons/hard_reset.ico + + + &Hard Reset... + + + false + + + + + true + + + + :/menuicons/qt/icons/acpi_shutdown.ico:/menuicons/qt/icons/acpi_shutdown.ico + + + Force shutdown + + + Force shutdown + + + true + + + false + + + + + false + + + + :/menuicons/qt/icons/send_cad.ico:/menuicons/qt/icons/send_cad.ico + + + &Ctrl+Alt+Del + + + Ctrl+Alt+Del + + + false + + + false + + + false + + + + + + :/menuicons/qt/icons/settings.ico:/menuicons/qt/icons/settings.ico + + + &Settings... + + + QAction::NoRole + + + false + + + + + + :/settings/qt/icons/86Box-yellow.ico:/settings/qt/icons/86Box-yellow.ico + + + New Machine + + + New Machine + + + + + Preferences + + + Preferences + + + QAction::PreferencesRole + + + + + true + + + + :/menuicons/qt/icons/run.ico:/menuicons/qt/icons/run.ico + + + Start + + + false + + + + + Check for updates + + + + + + + + diff --git a/src/qt/qt_vmmanager_model.cpp b/src/qt/qt_vmmanager_model.cpp new file mode 100644 index 000000000..7dc079df0 --- /dev/null +++ b/src/qt/qt_vmmanager_model.cpp @@ -0,0 +1,163 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager model module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed + */ + +#include +#include "qt_vmmanager_model.hpp" + +VMManagerModel::VMManagerModel() { + auto machines_vec = VMManagerSystem::scanForConfigs(); + for ( const auto& each_config : machines_vec) { + machines.append(each_config); + connect(each_config, &VMManagerSystem::itemDataChanged, this, &VMManagerModel::modelDataChanged); + } +} + +VMManagerModel::~VMManagerModel() { + for ( auto machine : machines) { + delete machine; + } +} + +int +VMManagerModel::rowCount(const QModelIndex &parent) const { + return machines.size(); +} + +QVariant +VMManagerModel::data(const QModelIndex &index, int role) const { + if (!index.isValid()) + return {}; + + if (index.row() >= machines.size()) + return {}; + + switch (role) { + case Qt::DisplayRole: + return machines.at(index.row())->displayName; + case ConfigName: + return machines.at(index.row())->config_name; + case ConfigDir: + return machines.at(index.row())->config_dir; + case ConfigFile: + return machines.at(index.row())->config_file.canonicalFilePath(); + case UUID: + return machines.at(index.row())->uuid; + case Notes: + return machines.at(index.row())->notes; + case SearchList: + return machines.at(index.row())->searchTerms; + case LastUsed: + return machines.at(index.row())->timestamp(); + case Icon: + return machines.at(index.row())->icon; + case Qt::ToolTipRole: + return machines.at(index.row())->shortened_dir; + case Qt::UserRole: + return machines.at(index.row())->getAll("General"); + case ProcessStatusString: + return machines.at(index.row())->getProcessStatusString(); + case ProcessStatus: + return QVariant::fromValue(machines.at(index.row())->getProcessStatus()); + default: + return {}; + } +} + +QVariant +VMManagerModel::headerData(int section, Qt::Orientation orientation, int role) const { + + if (role != Qt::DisplayRole) + return {}; + + if (orientation == Qt::Horizontal) + return QStringLiteral("Column %1").arg(section); + else + return QStringLiteral("Row %1").arg(section); +} + +VMManagerSystem * +VMManagerModel::getConfigObjectForIndex(const QModelIndex &index) const +{ + return machines.at(index.row()); +} +void +VMManagerModel::reload() +{ + // Scan for configs + auto machines_vec = VMManagerSystem::scanForConfigs(); + for (const auto &scanned_config : machines_vec) { + int found = 0; + for (const auto &existing_config : machines) { + if (*scanned_config == *existing_config) { + found = 1; + } + } + if (!found) { + addConfigToModel(scanned_config); + } + } + // TODO: Remove missing configs +} + +QModelIndex +VMManagerModel::getIndexForConfigFile(const QFileInfo& config_file) +{ + int object_index = 0; + for (const auto& config_object: machines) { + if (config_object->config_file == config_file) { + return this->index(object_index); + } + object_index++; + } + return {}; +} + +void +VMManagerModel::addConfigToModel(VMManagerSystem *system_config) +{ + beginInsertRows(QModelIndex(), this->rowCount(QModelIndex()), this->rowCount(QModelIndex())); + machines.append(system_config); + connect(system_config, &VMManagerSystem::itemDataChanged, this, &VMManagerModel::modelDataChanged); + endInsertRows(); +} +void +VMManagerModel::modelDataChanged() +{ + // Inform the model + emit dataChanged(this->index(0), this->index(machines.size()-1)); + // Inform any interested observers + emit systemDataChanged(); +} + +void +VMManagerModel::updateDisplayName(const QModelIndex &index, const QString &newDisplayName) +{ + machines.at(index.row())->setDisplayName(newDisplayName); + modelDataChanged(); +} +QHash +VMManagerModel::getProcessStats() +{ + QHash stats; + for (const auto& system: machines) { + if (system->getProcessStatus() != VMManagerSystem::ProcessStatus::Stopped) { + auto statusString = system->getProcessStatusString(); + stats[statusString] += 1; + } + } + return stats; +} \ No newline at end of file diff --git a/src/qt/qt_vmmanager_model.hpp b/src/qt/qt_vmmanager_model.hpp new file mode 100644 index 000000000..858b55d14 --- /dev/null +++ b/src/qt/qt_vmmanager_model.hpp @@ -0,0 +1,91 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for 86Box VM manager model module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed + */ + +#ifndef QT_VMMANAGER_MODEL_H +#define QT_VMMANAGER_MODEL_H + + +#include "qt_vmmanager_system.hpp" + +#include + +class VMManagerModel final : public QAbstractListModel { + + Q_OBJECT + +public: + // VMManagerModel(const QStringList &strings, QObject *parent = nullptr) + // : QAbstractListModel(parent), machines(strings) {} + VMManagerModel(); + ~VMManagerModel() override; + enum Roles { + ProcessStatusString = Qt::UserRole + 1, + ProcessStatus, + DisplayName, + ConfigName, + ConfigDir, + ConfigFile, + LastUsed, + UUID, + Notes, + SearchList, + Icon + }; + + [[nodiscard]] int rowCount(const QModelIndex &parent) const override; + [[nodiscard]] QVariant data(const QModelIndex &index, int role) const override; + [[nodiscard]] QVariant headerData(int section, Qt::Orientation orientation, + int role) const override; + void addConfigToModel(VMManagerSystem *system_config); + + [[nodiscard]] VMManagerSystem * getConfigObjectForIndex(const QModelIndex &index) const; + QModelIndex getIndexForConfigFile(const QFileInfo& config_file); + void reload(); + void updateDisplayName(const QModelIndex &index, const QString &newDisplayName); + QHash getProcessStats(); +signals: + void systemDataChanged(); + +private: + QVector machines; + void modelDataChanged(); + +}; + +// Note: Custom QSortFilterProxyModel is included here instead of its own file as +// its only use is in this model + +class StringListProxyModel final : public QSortFilterProxyModel { +public: + explicit StringListProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {} + +protected: + [[nodiscard]] bool filterAcceptsRow(const int sourceRow, const QModelIndex &sourceParent) const override { + const QModelIndex index = sourceModel()->index(sourceRow, filterKeyColumn(), sourceParent); + + QStringList stringList = sourceModel()->data(index, VMManagerModel::Roles::SearchList).toStringList(); + + const QRegularExpression regex = filterRegularExpression(); + + const auto result = std::any_of(stringList.begin(), stringList.end(), [®ex](const QString &string) { + return regex.match(string).hasMatch(); + }); + return result; + } +}; + +#endif //QT_VMMANAGER_MODEL_H diff --git a/src/qt/qt_vmmanager_preferences.cpp b/src/qt/qt_vmmanager_preferences.cpp new file mode 100644 index 000000000..0730f875b --- /dev/null +++ b/src/qt/qt_vmmanager_preferences.cpp @@ -0,0 +1,82 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager preferences module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#include +#include + +#include "qt_vmmanager_preferences.hpp" +#include "qt_vmmanager_config.hpp" +#include "ui_qt_vmmanager_preferences.h" + +extern "C" { +#include <86box/86box.h> +} + +VMManagerPreferences:: +VMManagerPreferences(QWidget *parent) : ui(new Ui::VMManagerPreferences) +{ + ui->setupUi(this); + ui->dirSelectButton->setIcon(QApplication::style()->standardIcon(QStyle::SP_DirIcon)); + connect(ui->dirSelectButton, &QPushButton::clicked, this, &VMManagerPreferences::chooseDirectoryLocation); + + const auto config = new VMManagerConfig(VMManagerConfig::ConfigType::General); + const auto configSystemDir = config->getStringValue("system_directory"); + if(!configSystemDir.isEmpty()) { + // Prefer this one + ui->systemDirectory->setText(configSystemDir); + } else if(!QString(vmm_path).isEmpty()) { + // If specified on command line + ui->systemDirectory->setText(QDir(vmm_path).path()); + } + + // TODO: Defaults + const auto configUpdateCheck = config->getStringValue("update_check").toInt(); + ui->updateCheckBox->setChecked(configUpdateCheck); + const auto useRegexSearch = config->getStringValue("regex_search").toInt(); + ui->regexSearchCheckBox->setChecked(useRegexSearch); + + +} + +VMManagerPreferences::~ +VMManagerPreferences() + = default; + +// Bad copy pasta from machine add +void +VMManagerPreferences::chooseDirectoryLocation() +{ + // TODO: FIXME: This is pulling in the CLI directory! Needs to be set properly elsewhere + const auto directory = QFileDialog::getExistingDirectory(this, "Choose directory", QDir(vmm_path).path()); + ui->systemDirectory->setText(QDir::toNativeSeparators(directory)); +} + +void +VMManagerPreferences::accept() +{ + const auto config = new VMManagerConfig(VMManagerConfig::ConfigType::General); + config->setStringValue("system_directory", ui->systemDirectory->text()); + config->setStringValue("update_check", ui->updateCheckBox->isChecked() ? "1" : "0"); + config->setStringValue("regex_search", ui->regexSearchCheckBox->isChecked() ? "1" : "0"); + QDialog::accept(); +} + +void +VMManagerPreferences::reject() +{ + QDialog::reject(); +} \ No newline at end of file diff --git a/src/qt/qt_vmmanager_preferences.hpp b/src/qt/qt_vmmanager_preferences.hpp new file mode 100644 index 000000000..aedba862a --- /dev/null +++ b/src/qt/qt_vmmanager_preferences.hpp @@ -0,0 +1,46 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for 86Box VM manager preferences module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#ifndef VMMANAGER_PREFERENCES_H +#define VMMANAGER_PREFERENCES_H + +#include + +QT_BEGIN_NAMESPACE +namespace Ui { class VMManagerPreferences; } +QT_END_NAMESPACE + + +class VMManagerPreferences final : public QDialog +{ + Q_OBJECT +public: + explicit VMManagerPreferences(QWidget *parent = nullptr); + ~VMManagerPreferences() override; + +private: + Ui::VMManagerPreferences *ui; + QString settingsFile; +private slots: + void chooseDirectoryLocation(); +protected: + void accept() override; + void reject() override; + +}; + +#endif // VMMANAGER_PREFERENCES_H diff --git a/src/qt/qt_vmmanager_preferences.ui b/src/qt/qt_vmmanager_preferences.ui new file mode 100644 index 000000000..1743a0bfb --- /dev/null +++ b/src/qt/qt_vmmanager_preferences.ui @@ -0,0 +1,130 @@ + + + VMManagerPreferences + + + + 0 + 0 + 400 + 300 + + + + Preferences + + + + + + System Directory: + + + + + + + + 0 + + + 0 + + + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + Check for updates on startup + + + + + + + Use regular expressions in search box + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + VMManagerPreferences + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + VMManagerPreferences + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/qt/qt_vmmanager_protocol.cpp b/src/qt/qt_vmmanager_protocol.cpp new file mode 100644 index 000000000..26f8a1f93 --- /dev/null +++ b/src/qt/qt_vmmanager_protocol.cpp @@ -0,0 +1,131 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager protocol module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed + */ + +#include "qt_vmmanager_protocol.hpp" +#include +#include +VMManagerProtocol::VMManagerProtocol(VMManagerProtocol::Sender sender) +{ + message_class = sender; +} + +VMManagerProtocol::~VMManagerProtocol() += default; + +QJsonObject +VMManagerProtocol::protocolManagerMessage(VMManagerProtocol::ManagerMessage message_type) +{ + auto json_message = constructDefaultObject(VMManagerProtocol::Sender::Manager); + json_message["message"] = managerMessageTypeToString(message_type); + return json_message; +} + +QJsonObject +VMManagerProtocol::protocolClientMessage(VMManagerProtocol::ClientMessage message_type) +{ + auto json_message = constructDefaultObject(VMManagerProtocol::Sender::Client); + json_message["message"] = clientMessageTypeToString(message_type); + return json_message; +} + +QString +VMManagerProtocol::managerMessageTypeToString(VMManagerProtocol::ManagerMessage message) +{ + QMetaEnum qme = QMetaEnum::fromType(); + return qme.valueToKey(static_cast(message)); +} + +QString +VMManagerProtocol::clientMessageTypeToString(VMManagerProtocol::ClientMessage message) +{ + QMetaEnum qme = QMetaEnum::fromType(); + return qme.valueToKey(static_cast(message)); +} + +QJsonObject +VMManagerProtocol::constructDefaultObject(VMManagerProtocol::Sender type) +{ + QJsonObject json_message; + QString sender_type = ( type == VMManagerProtocol::Sender::Client ) ? "Client" : "VMManager"; + json_message["type"] = QString(sender_type); + json_message["version"] = QStringLiteral(EMU_VERSION); + return json_message; +} +bool +VMManagerProtocol::hasRequiredFields(const QJsonObject& json_document) +{ + for (const auto& field : ProtocolRequiredFields) { + if (!json_document.contains(field)) { + qDebug("Received json missing field \"%s\"", qPrintable(field)); + return false; + } + } + return true; +} +VMManagerProtocol::ClientMessage +VMManagerProtocol::getClientMessageType(const QJsonObject &json_document) +{ + // FIXME: This key ("message") is hardcoded here. Make a hash which maps these + // required values. + QString message_type = json_document.value("message").toString(); + // Can't use switch with strings, manual compare + if (message_type == "Status") { + return VMManagerProtocol::ClientMessage::Status; + } else if (message_type == "WindowBlocked") { + return VMManagerProtocol::ClientMessage::WindowBlocked; + } else if (message_type == "WindowUnblocked") { + return VMManagerProtocol::ClientMessage::WindowUnblocked; + } else if (message_type == "RunningStateChanged") { + return VMManagerProtocol::ClientMessage::RunningStateChanged; + } + return VMManagerProtocol::ClientMessage::UnknownMessage; +} +VMManagerProtocol::ManagerMessage +VMManagerProtocol::getManagerMessageType(const QJsonObject &json_document) +{ + // FIXME: This key ("message") is hardcoded here. Make a hash which maps these + // required values. + QString message_type = json_document.value("message").toString(); + // Can't use switch with strings, manual compare + if (message_type == "RequestStatus") { + return VMManagerProtocol::ManagerMessage::RequestStatus; + } else if (message_type == "Pause") { + return VMManagerProtocol::ManagerMessage::Pause; + } if (message_type == "CtrlAltDel") { + return VMManagerProtocol::ManagerMessage::CtrlAltDel; + } if (message_type == "ShowSettings") { + return VMManagerProtocol::ManagerMessage::ShowSettings; + } if (message_type == "ResetVM") { + return VMManagerProtocol::ManagerMessage::ResetVM; + } if (message_type == "RequestShutdown") { + return VMManagerProtocol::ManagerMessage::RequestShutdown; + } if (message_type == "ForceShutdown") { + return VMManagerProtocol::ManagerMessage::ForceShutdown; + } + return VMManagerProtocol::ManagerMessage::UnknownMessage; +} +QJsonObject +VMManagerProtocol::getParams(const QJsonObject &json_document) +{ + // FIXME: This key ("params") is hardcoded here. Make a hash which maps these + // required values. + auto params_object = json_document.value("params"); + if (params_object.type() != QJsonValue::Object) { + return {}; + } + return params_object.toObject(); +} diff --git a/src/qt/qt_vmmanager_protocol.hpp b/src/qt/qt_vmmanager_protocol.hpp new file mode 100644 index 000000000..48f0a2d8f --- /dev/null +++ b/src/qt/qt_vmmanager_protocol.hpp @@ -0,0 +1,93 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for 86Box VM manager protocol module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed + */ + +#ifndef QT_VMMANAGER_PROTOCOL_H +#define QT_VMMANAGER_PROTOCOL_H + +#include +extern "C" { +#include <86box/version.h> +} + +static QVector ProtocolRequiredFields = { "type", "message" }; + +class VMManagerProtocol : QObject { + Q_OBJECT + +public: + enum class Sender { + Manager, + Client, + }; + Q_ENUM(Sender); + + enum class ManagerMessage { + RequestStatus, + Pause, + CtrlAltDel, + ShowSettings, + ResetVM, + RequestShutdown, + ForceShutdown, + UnknownMessage, + }; + + // This macro allows us to do a reverse lookup of the enum with `QMetaEnum` + Q_ENUM(ManagerMessage); + + enum class ClientMessage { + Status, + WindowBlocked, + WindowUnblocked, + RunningStateChanged, + UnknownMessage, + }; + Q_ENUM(ClientMessage); + + enum class WindowStatus { + WindowUnblocked = 0, + WindowBlocked, + }; + + enum class RunningState { + Running = 0, + Paused, + RunningWaiting, + PausedWaiting, + Unknown, + }; + Q_ENUM(RunningState); + + explicit VMManagerProtocol(Sender sender); + ~VMManagerProtocol(); + + QJsonObject protocolManagerMessage(ManagerMessage message_type); + QJsonObject protocolClientMessage(ClientMessage message_type); + static QString managerMessageTypeToString(ManagerMessage message); + static QString clientMessageTypeToString(ClientMessage message); + + static bool hasRequiredFields(const QJsonObject &json_document); + static QJsonObject getParams(const QJsonObject &json_document); + static ClientMessage getClientMessageType(const QJsonObject &json_document); + static ManagerMessage getManagerMessageType(const QJsonObject &json_document); + +private: + Sender message_class; + static QJsonObject constructDefaultObject(VMManagerProtocol::Sender type); +}; + +#endif // QT_VMMANAGER_PROTOCOL_H diff --git a/src/qt/qt_vmmanager_serversocket.cpp b/src/qt/qt_vmmanager_serversocket.cpp new file mode 100644 index 000000000..50ff352d6 --- /dev/null +++ b/src/qt/qt_vmmanager_serversocket.cpp @@ -0,0 +1,207 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager server socket module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed + */ + +#include "qt_vmmanager_serversocket.hpp" +#include +#include +#include +#include +#include +#include + +VMManagerServerSocket::VMManagerServerSocket(const QFileInfo &config_path, const ServerType type) +{ + server_type = type; + config_file = config_path; + serverIsRunning = false; + socket = nullptr; + server = new QLocalServer; + setupVars(); +} + +VMManagerServerSocket::~VMManagerServerSocket() +{ + delete server; +} + +bool +VMManagerServerSocket::startServer() { + + // Remove socket file (if it exists) in order to start a new one + qInfo("Socket path is %s", qPrintable(socket_path.filePath())); + if (socket_path.exists() and !socket_path.isDir()) { + auto socket_file = new QFile(socket_path.filePath()); + if (!socket_file->remove()) { + qInfo("Failed to remove the old socket file (Error %i): %s", socket_file->error(), qPrintable(socket_file->errorString())); + return false; + } + } + + if (server->listen(socket_path.fileName())) { + serverIsRunning = true; + connect(server, &QLocalServer::newConnection, this, &VMManagerServerSocket::serverConnectionReceived); + return true; + } else { + qInfo("Failed to start server: %s", qPrintable(server->errorString())); + serverIsRunning = false; + return false; + } +} + +void +VMManagerServerSocket::serverConnectionReceived() { + qDebug("Connection received on %s", qPrintable(socket_path.fileName())); + socket = server->nextPendingConnection(); + if(!socket) { + qInfo("Invalid socket when trying to receive the connection"); + return; + } + connect(socket, &QLocalSocket::readyRead, this, &VMManagerServerSocket::serverReceivedMessage); + connect(socket, &QLocalSocket::disconnected, this, &VMManagerServerSocket::serverDisconnected); +} + +void +VMManagerServerSocket::serverReceivedMessage() { + + // Handle legacy socket connections first. These connections only receive + // information on window status + if(server_type == VMManagerServerSocket::ServerType::Legacy) { + QByteArray tempString = socket->read(1); + int window_obscured = tempString.toInt(); + emit windowStatusChanged(window_obscured); + return; + } + + // Normal connections here + QDataStream stream(socket); + stream.setVersion(QDataStream::Qt_5_7); + QByteArray jsonData; + for (;;) { + // Start a transaction + stream.startTransaction(); + // Try to read the data + stream >> jsonData; + if (stream.commitTransaction()) { + QJsonParseError parse_error{}; + // Validate the received data to make sure it's valid json + const QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parse_error); + if (parse_error.error == QJsonParseError::NoError) { + // The data received was valid json + if (jsonDoc.isObject()) { + // The data is a valid json object + emit dataReceived(); + jsonReceived(jsonDoc.object()); + } + } + // The data was not valid json. + // Loop and try to read more data + } else { + // read failed, socket is reverted to its previous state (before the transaction) + // exit the loop and wait for more data to become available + break; + } + } +} + +void +VMManagerServerSocket::serverSendMessage(VMManagerProtocol::ManagerMessage protocol_message, const QStringList& arguments) const { + if(!socket) { + qInfo("Cannot send message: Invalid socket"); + return; + } + + // Regular connection + QDataStream stream(socket); + stream.setVersion(QDataStream::Qt_5_7); + auto packet = new VMManagerProtocol(VMManagerProtocol::Sender::Manager); + auto jsonMessage = packet->protocolManagerMessage(protocol_message); + stream << QJsonDocument(jsonMessage).toJson(QJsonDocument::Compact); +} + +void +VMManagerServerSocket::serverDisconnected() +{ + qInfo("Connection disconnected"); +} +void +VMManagerServerSocket::jsonReceived(const QJsonObject &json) +{ + // The serialization portion has already validated the message as json. + // Now ensure it has the required fields. + if (!VMManagerProtocol::hasRequiredFields(json)) { + // TODO: Error handling of some sort, emit signals + qDebug("Invalid message received from client: required fields missing. Object:"); + qDebug() << json; + return; + } +// qDebug().noquote() << Q_FUNC_INFO << json; + QJsonObject params_object; + + auto message_type = VMManagerProtocol::getClientMessageType(json); + switch (message_type) { + case VMManagerProtocol::ClientMessage::Status: + qDebug("Status message received from client"); + break; + case VMManagerProtocol::ClientMessage::WindowBlocked: + qDebug("Window blocked message received from client"); + emit windowStatusChanged(static_cast(VMManagerProtocol::WindowStatus::WindowBlocked)); + break; + case VMManagerProtocol::ClientMessage::WindowUnblocked: + qDebug("Window unblocked received from client"); + emit windowStatusChanged(static_cast(VMManagerProtocol::WindowStatus::WindowUnblocked)); + break; + case VMManagerProtocol::ClientMessage::RunningStateChanged: + qDebug("Running state change received from client"); + params_object = VMManagerProtocol::getParams(json); + if (!params_object.isEmpty()) { + // valid object + if(params_object.value("status").type() == QJsonValue::Double) { + // has status key, value is an int (qt assigns it as Double) + emit runningStatusChanged(static_cast(params_object.value("status").toInt())); + } + } + break; + default: + qDebug("Unknown client message type received:"); + qDebug() << json; + return; + } +} + +void +VMManagerServerSocket::setupVars() +{ + QString unique_name = QCryptographicHash::hash(config_file.path().toUtf8().constData(), QCryptographicHash::Algorithm::Sha256).toHex().right(6); + socket_path.setFile(QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/" + QApplication::applicationName() + ".socket." + unique_name); +} + +QString +VMManagerServerSocket::getSocketPath() const +{ + if (server) { + return server->fullServerName(); + } + return {}; +} + +QString +VMManagerServerSocket::serverTypeToString(VMManagerServerSocket::ServerType server_type_lookup) +{ + QMetaEnum qme = QMetaEnum::fromType(); + return qme.valueToKey(static_cast(server_type_lookup)); + +} diff --git a/src/qt/qt_vmmanager_serversocket.hpp b/src/qt/qt_vmmanager_serversocket.hpp new file mode 100644 index 000000000..3e6e43a80 --- /dev/null +++ b/src/qt/qt_vmmanager_serversocket.hpp @@ -0,0 +1,82 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for 86Box VM manager server socket module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed + */ + +#ifndef QT_VMMANAGER_SERVERSOCKET_H +#define QT_VMMANAGER_SERVERSOCKET_H + +#include +#include +#include +#include +#include +#include "qt_vmmanager_protocol.hpp" + +// This macro helps give us the required `qHash()` function in order to use the +// enum as a hash key +#define QHASH_FOR_CLASS_ENUM(T) \ +inline uint qHash(const T &t, uint seed) { \ + return ::qHash(static_cast::type>(t), seed); \ +} + +class VMManagerServerSocket : public QWidget { + + Q_OBJECT + +public: + + enum class ServerType { + Standard, + Legacy, + }; + // This macro allows us to do a reverse lookup of the enum with `QMetaEnum` + Q_ENUM(ServerType) + + QHASH_FOR_CLASS_ENUM(ServerType) + + explicit VMManagerServerSocket(const QFileInfo &config_path, ServerType type = ServerType::Standard); + ~VMManagerServerSocket() override; + + QFileInfo socket_path; + QFileInfo config_file; + + QLocalServer *server; + QLocalSocket *socket; + ServerType server_type; + bool serverIsRunning; + + // Server functions + bool startServer(); + void serverConnectionReceived(); + void serverReceivedMessage(); + void serverSendMessage(VMManagerProtocol::ManagerMessage protocol_message, const QStringList& arguments = QStringList()) const; + static void serverDisconnected(); + void jsonReceived(const QJsonObject &json); + QString getSocketPath() const; + + static QString serverTypeToString(ServerType server_type_lookup); + + void setupVars(); + +signals: + void dataReceived(); + void windowStatusChanged(int status); + void runningStatusChanged(VMManagerProtocol::RunningState state); + + +}; + +#endif // QT_VMMANAGER_SERVERSOCKET_H diff --git a/src/qt/qt_vmmanager_system.cpp b/src/qt/qt_vmmanager_system.cpp new file mode 100644 index 000000000..a28e27892 --- /dev/null +++ b/src/qt/qt_vmmanager_system.cpp @@ -0,0 +1,888 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* 86Box VM manager system module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "qt_vmmanager_system.hpp" +// #include "qt_vmmanager_details_section.hpp" +#include "qt_vmmanager_detailsection.hpp" + + +extern "C" { +#include <86box/86box.h> +#include <86box/device.h> +#include <86box/video.h> +// #include <86box/vid_xga_device.h> +#include <86box/machine.h> +#include <86box/plat.h> +#include <86box/sound.h> +#include +#include <86box/thread.h> // required for network.h +#include <86box/timer.h> // required for network.h and fdd.h +#include <86box/cdrom.h> +#include <86box/scsi.h> +#include <86box/fdd.h> +#include <86box/gameport.h> +#include <86box/midi.h> +#include <86box/network.h> +#include <86box/mouse.h> +} + +VMManagerSystem::VMManagerSystem(const QString &sysconfig_file) { + + // The 86Box configuration file + config_file = QFileInfo(sysconfig_file); + // The default name of the system. This is the name of the directory + // that contains the 86box configuration file + config_name = config_file.dir().dirName(); + // The full path of the directory that contains the 86box configuration file + config_dir = shortened_dir = config_file.dir().path(); + process_status = ProcessStatus::Stopped; + // Main 86Box uses usr_path for UUID which includes the trailing slash. + // Make sure to append the slash here so the UUIDs will match + auto uuid_path = config_dir; + if (!uuid_path.endsWith("/")) { + uuid_path.append("/"); + } + // In the configuration file the UUID is used as a unique value + uuid = QUuid::createUuidV5(QUuid{}, uuid_path).toString(QUuid::WithoutBraces); + // That unique value is used to map the information to each individual system. + config_settings = new VMManagerConfig(VMManagerConfig::ConfigType::System, uuid); + + // On non-windows platforms, shortened_dir will replace the home directory path with ~ + // and be used as the tool tip in the list view +#if not defined(Q_OS_WINDOWS) + if (config_dir.startsWith(QDir::homePath())) { + shortened_dir.replace(QDir::homePath(), "~"); + } +#endif + loadSettings(); + setupPaths(); + // Paths must be setup before vars! + setupVars(); + + serverIsRunning = false; + window_obscured = false; + + find86BoxBinary(); + platform = QApplication::platformName(); + process = new QProcess(); + connect(process, &QProcess::stateChanged, this, &VMManagerSystem::processStatusChanged); + + // Server type for this instance (Standard should always be used instead of Legacy) + socket_server_type = VMManagerServerSocket::ServerType::Standard; + socket_server = new VMManagerServerSocket(config_file, socket_server_type); + + // NOTE: When unique names or UUIDs are written to the individual VM config file, use that + // here instead of the auto-generated unique_name + // Save settings once everything is initialized + saveSettings(); +} + +VMManagerSystem::~VMManagerSystem() { + delete socket_server; +} + +QVector +VMManagerSystem::scanForConfigs(const QString &searchPath) +{ + QElapsedTimer scanTimer; + scanTimer.start(); + QVector system_configs; + + const auto config = new VMManagerConfig(VMManagerConfig::ConfigType::General); + auto systemDirConfig = config->getStringValue("system_directory"); + + const auto config_file_name = QString("86box.cfg"); + const QStringList filters = {config_file_name}; + QStringList matches; + // TODO: Preferences. Once I get the CLI args worked out. + // For now it just takes vmm_path from the CLI + QString search_directory; + // if(searchPath.isEmpty()) { + // // If the location isn't specified in function call, use the one loaded + // // from the config file + // search_directory = systemDirConfig; + // } else { + // search_directory = searchPath; + // } + + search_directory = searchPath.isEmpty()? vmm_path : searchPath; + + if(!QDir(search_directory).exists()) { + qWarning() << "Path" << search_directory << "does not exist. Cannot continue"; + return {}; + } + + QDirIterator dir_iterator(search_directory, filters, QDir::Files, QDirIterator::Subdirectories); + + qInfo("Searching %s for %s", qPrintable(search_directory), qPrintable(config_file_name)); + + QElapsedTimer timer; + timer.start(); + while (dir_iterator.hasNext()) { + QString filename = dir_iterator.next(); + matches.append(filename); + } + + const auto scanElapsed = timer.elapsed(); + qDebug().noquote().nospace() << "Found " << matches.size() << " configs in " << search_directory <<". Scan took " << scanElapsed << " ms"; + + timer.restart(); + // foreach (QFileInfo hit, matches) { + // system_configs.append(new VMManagerSystem(hit)); + // } + for (const auto &filename : matches) { + system_configs.append(new VMManagerSystem(filename)); + } + auto elapsed = timer.elapsed(); + qDebug() << "Load loop took" << elapsed << "ms for" << matches.size() << "loads"; + qDebug() << "Overall scan time was" << scanTimer.elapsed() << "ms, average" << elapsed / matches.size() << "ms / load"; + return system_configs; +} + +QString +VMManagerSystem::generateTemporaryFilename() +{ + QTemporaryFile tempFile; + // File will be closed once the QTemporaryFile object goes out of scope + tempFile.setAutoRemove(true); + tempFile.open(); + return tempFile.fileName(); +} + +QFileInfoList +VMManagerSystem::getScreenshots() { + + // Don't bother unless the directory exists + if(!screenshot_directory.exists()) { + return {}; + } + + auto screen_scan_dir = QDir(screenshot_directory.path(), "Monitor_1*", QDir::SortFlag::LocaleAware | QDir::SortFlag::IgnoreCase, QDir::Files); + auto screenshot_files = screen_scan_dir.entryInfoList(); + return screenshot_files; +} + +void +VMManagerSystem::loadSettings() +{ + // First, load the information from the 86box.cfg + QSettings settings(config_file.filePath(), QSettings::IniFormat); + if (settings.status() != QSettings::NoError) { + qWarning() << "Error loading" << config_file.path() << " status:" << settings.status(); + } + // qInfo() << "Loaded "<< config_file.filePath() << "status:" << settings.status(); + + // Clear out the config hash in case the config is reloaded + for (const auto &outer_key : config_hash.keys()) { + config_hash[outer_key].clear(); + } + + // General + for (const auto &key_name : settings.childKeys()) { + config_hash["General"][key_name] = settings.value(key_name).toString(); + } + + for (auto &group_name : settings.childGroups()) { + settings.beginGroup(group_name); + for (const auto &key_name : settings.allKeys()) { + QString setting_value; + // QSettings will interpret lines with commas as QStringList. + // Check for it and join them back to a string. + if (settings.value(key_name).type() == QVariant::StringList) { + setting_value = settings.value(key_name).toStringList().join(", "); + } else { + setting_value = settings.value(key_name).toString(); + } + config_hash[group_name][key_name] = setting_value; + } + settings.endGroup(); + } + + // Next, load the information from the vmm config for this system + // Display name + auto loadedDisplayName = config_settings->getStringValue("display_name"); + if (!loadedDisplayName.isEmpty()) { + displayName = loadedDisplayName; + } else { + displayName = config_name; + } + // Notes + auto loadedNotes = config_settings->getStringValue("notes"); + if (!loadedNotes.isEmpty()) { + notes = loadedNotes; + } + // Timestamp + auto loadedTimestamp = config_settings->getStringValue("timestamp"); + if (!loadedTimestamp.isEmpty()) { + // Make sure it is valid + if (auto newTimestamp = QDateTime::fromString(loadedTimestamp, Qt::ISODate); newTimestamp.isValid()) { + lastUsedTimestamp = newTimestamp; + } + } + // Icon + auto loadedIcon = config_settings->getStringValue("icon"); + if (!loadedIcon.isEmpty()) { + icon = loadedIcon; + } +} +void +VMManagerSystem::saveSettings() +{ + if(!isValid()) { + return; + } + config_settings->setStringValue("system_name", config_name); + config_settings->setStringValue("config_file", config_file.canonicalFilePath()); + config_settings->setStringValue("config_dir", config_file.canonicalPath()); + if (displayName != config_name) { + config_settings->setStringValue("display_name", displayName); + } else { + config_settings->remove("display_name"); + } + + config_settings->setStringValue("notes", notes); + if(lastUsedTimestamp.isValid()) { + config_settings->setStringValue("timestamp", lastUsedTimestamp.toString(Qt::ISODate)); + } + config_settings->setStringValue("icon", icon); + generateSearchTerms(); +} +void +VMManagerSystem::generateSearchTerms() +{ + searchTerms.clear(); + for (const auto &config_key : config_hash.keys()) { + // searchTerms.append(config_hash[config_key].values()); + // brute force temporarily don't add paths + for(const auto &value: config_hash[config_key].values()) { + if(!value.startsWith("/")) { + searchTerms.append(value); + } + } + } + searchTerms.append(display_table.values()); + searchTerms.append(displayName); + searchTerms.append(config_name); + QRegularExpression whitespaceRegex("\\s+"); + searchTerms.append(notes.split(whitespaceRegex)); +} +void +VMManagerSystem::updateTimestamp() +{ + lastUsedTimestamp = QDateTime::currentDateTimeUtc(); + saveSettings(); +} + +QString +VMManagerSystem::getAll(const QString& category) const { + auto value = config_hash[category].keys().join(", "); + return value; +} + +QHash> +VMManagerSystem::getConfigHash() const +{ + return config_hash; +} + +void +VMManagerSystem::setDisplayName(const QString &newDisplayName) +{ + // If blank, reset to the default + if (newDisplayName.isEmpty()) { + displayName = config_name; + } else { + displayName = newDisplayName; + } + saveSettings(); +} +void +VMManagerSystem::setNotes(const QString &newNotes) +{ + notes = newNotes; + saveSettings(); +} +bool +VMManagerSystem::isValid() const +{ + return config_file.exists() && config_file.isFile() && config_file.size() != 0; +} + +bool +VMManagerSystem::isProcessRunning() const +{ + return process->processId() != 0; +} + +qint64 +VMManagerSystem::processId() const +{ + return process->processId(); +} + +QHash +VMManagerSystem::getCategory(const QString &category) const { + return config_hash[category]; +} + +void +VMManagerSystem::find86BoxBinary() { + // We'll use our own self to launch the VMs + main_binary = QFileInfo(QCoreApplication::applicationFilePath()); +} + +bool +VMManagerSystem::has86BoxBinary() { + return main_binary.exists(); +} + +void +VMManagerSystem::launchMainProcess() { + + if(!has86BoxBinary()) { + qWarning("No binary found! returning"); + return; + } + + // start the server first to get the socket name + if (!serverIsRunning) { + if(!startServer()) { + // FIXME: Better error handling + qInfo("Failed to start VM Manager server"); + return; + } + } + setProcessEnvVars(); + QString program = main_binary.filePath(); + QStringList args; + args << "-P" << config_dir; + args << "--vmname" << displayName; + process->setProgram(program); + process->setArguments(args); + qDebug() << Q_FUNC_INFO << " Full Command:" << process->program() << " " << process->arguments(); + process->start(); + updateTimestamp(); +} + +void +VMManagerSystem::startButtonPressed() { + launchMainProcess(); +} + +void +VMManagerSystem::launchSettings() { + if(!has86BoxBinary()) { + qWarning("No binary found! returning"); + return; + } + + // If the system is already running, instruct it to show settings + if (process->processId() != 0) { + socket_server->serverSendMessage(VMManagerProtocol::ManagerMessage::ShowSettings); + return; + } + + // Otherwise, launch the system with the settings parameter + setProcessEnvVars(); + QString program = main_binary.filePath(); + QStringList open_command_args; + QStringList args; + args << "-P" << config_dir << "-S"; + process->setProgram(program); + process->setArguments(args); + qDebug() << Q_FUNC_INFO << " Full Command:" << process->program() << " " << process->arguments(); + process->start(); +} + +void +VMManagerSystem::setupPaths() { + // application_temp_directory.setPath(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); + // standard_temp_directory.setPath(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); + // QString temp_subdir = QApplication::applicationName(); + // if (!application_temp_directory.exists(temp_subdir)) { + // // FIXME: error checking + // application_temp_directory.mkdir(temp_subdir); + // } + // // QT always replaces `/` with native separators, so it is safe to use here for all platforms + // application_temp_directory.setPath(application_temp_directory.path() + "/" + temp_subdir); + // app_data_directory.setPath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); + // // TODO: come back here and update with the new plat_get_global_* + // if (!app_data_directory.exists()) { + // // FIXME: Error checking + // app_data_directory.mkpath(app_data_directory.path()); + // } + screenshot_directory.setPath(config_dir + "/" + "screenshots"); +} + +void +VMManagerSystem::setupVars() { + unique_name = QCryptographicHash::hash(config_file.path().toUtf8().constData(), QCryptographicHash::Algorithm::Sha256).toHex().right(9); + // unique_name = "aaaaaa"; + // Set up the display vars + // This will likely get moved out to its own class + // This will likely get moved out to its own class + auto machine_config = getCategory("Machine"); + auto video_config = getCategory("Video"); + auto disk_config = getCategory("Hard disks"); + auto audio_config = getCategory("Sound"); + auto network_config = getCategory("Network"); + auto input_config = getCategory("Input devices"); + auto floppy_cdrom_config = getCategory("Floppy and CD-ROM drives"); + auto scsi_config = getCategory("Storage controllers"); + auto ports_config = getCategory("Ports (COM & LPT)"); + // auto general_config = getCategory("General"); + // auto config_uuid = QString("Not set"); + // if(!general_config["uuid"].isEmpty()) { + // config_uuid = general_config["uuid"]; + // qDebug() << "btw config dir:" << config_dir; + // } + // qDebug() << "Generated UUID:" << uuid; + // qDebug() << "Config file UUID:" << config_uuid; + auto machine_name = QString(); + int i = 0; + int ram_granularity = 0; + // Machine + for (int ci = 0; ci < machine_count(); ++ci) { + if (machine_available(ci)) { + if (machines[ci].internal_name == machine_config["machine"]) { + machine_name = machines[ci].name; + ram_granularity = machines[ci].ram.step; + } + } + } + display_table[Display::Name::Machine] = machine_name; + + // CPU: Combine name with speed + auto cpu_name = QString(); + while (cpu_families[i].package != 0) { + if (cpu_families[i].internal_name == machine_config["cpu_family"]) { + cpu_name = QString("%1 %2").arg(cpu_families[i].manufacturer, cpu_families[i].name); + } + i++; + } + int speed_display = machine_config["cpu_speed"].toInt() / 1000000; + cpu_name.append(QString::number(speed_display).prepend(" / ")); + cpu_name.append(QCoreApplication::translate("", "MHz").prepend(' ')); + display_table[Display::Name::CPU] = cpu_name; + + // Memory + int divisor = (ram_granularity < 1024) ? 1 : 1024; + QString display_unit = (divisor == 1) ? "KB" : "MB"; + auto mem_display = QString::number(machine_config["mem_size"].toInt() / divisor); + mem_display.append(QCoreApplication::translate("", display_unit.toUtf8().constData()).prepend(' ')); + display_table[Display::Name::Memory] = mem_display; + + // Video card + int video_int = video_get_video_from_internal_name(video_config["gfxcard"].toUtf8().data()); + const device_t* video_dev = video_card_getdevice(video_int); + display_table[Display::Name::Video] = DeviceConfig::DeviceName(video_dev, video_get_internal_name(video_int), 1); + if (!video_config["voodoo"].isEmpty()) { + // FIXME: Come back to this later to add more for secondary video +// display_table[Display::Name::Video].append(" (with voodoo)"); + display_table[Display::Name::Voodoo] = "Voodoo enabled"; + } + + // Drives + // First the number of disks + QMap disks; + for(const auto& key: disk_config.keys()) { + // Assuming the format hdd_NN_* + QStringList pieces = key.split('_'); + QString disk = QString("%1_%2").arg(pieces.at(0), pieces.at(1)); + if(!disk.isEmpty()) { + disks[disk] = 1; + } + } + // Next, the types + QHash bus_types; + for (const auto& key: disks.keys()) { + auto disk_parameter_key = QString("%1_parameters").arg(key); + QStringList pieces = disk_config[disk_parameter_key].split(","); + QString bus_type = pieces.value(pieces.length() - 1).trimmed(); + bus_types[bus_type] = 1; + } + QString disks_display = tr("%n disk(s)", "", disks.count()); + if (disks.count()) { + disks_display.append(" / ").append(bus_types.keys().join(", ").toUpper()); + } +// display_table[Display::Name::Disks] = disks_display; + + // Drives + QString new_disk_display; + for (const auto& key: disks.keys()) { + auto disk_parameter_key = QString("%1_parameters").arg(key); + // Converting a string to an int back to a string to remove the zero (e.g. 01 to 1) + auto disk_number = QString::number(key.split("_").last().toInt()); + QStringList pieces = disk_config[disk_parameter_key].split(","); + QString sectors = pieces.value(0).trimmed(); + QString heads = pieces.value(1).trimmed(); + QString cylinders = pieces.value(2).trimmed(); + QString bus_type = pieces.value(pieces.length() - 1).trimmed(); + // Add separator for each subsequent value, skipping the first + if(!new_disk_display.isEmpty()) { + new_disk_display.append(QString("%1").arg(VMManagerDetailSection::sectionSeparator)); + } + int diskSizeRaw = (cylinders.toInt() * heads.toInt() * sectors.toInt()) >> 11; + QString diskSizeFinal; + QString unit = "MiB"; + if(diskSizeRaw > 1000) { + unit = "GiB"; + diskSizeFinal = QString::number(diskSizeRaw * 1.0 / 1000, 'f', 1); + } else { + diskSizeFinal = QString::number(diskSizeRaw); + } + // Only prefix each disk when there are multiple disks + QString diskNumberDisplay = disks.count() > 1 ? QString("Disk %1: ").arg(disk_number) : ""; + new_disk_display.append(QString("%1%2 %3 (%4)").arg(diskNumberDisplay, diskSizeFinal, unit, bus_type.toUpper())); + } + if(new_disk_display.isEmpty()) { + new_disk_display = "No disks"; + } + display_table[Display::Name::Disks] = new_disk_display; + + // Floppy & CD-ROM + QStringList floppyDevices; + QStringList cdromDevices; + static auto floppy_match = QRegularExpression("fdd_\\d\\d_type", QRegularExpression::CaseInsensitiveOption); + static auto cdrom_match = QRegularExpression("cdrom_\\d\\d_type", QRegularExpression::CaseInsensitiveOption); + for(const auto& key: floppy_cdrom_config.keys()) { + if(key.contains(floppy_match)) { + // auto device_number = key.split("_").at(1); + auto floppy_internal_name = QString(floppy_cdrom_config[key]); + // Not interested in the nones + if(floppy_internal_name == "none") { + continue; + } + auto floppy_type = fdd_get_from_internal_name(floppy_internal_name.toUtf8().data()); + if(auto fddName = QString(fdd_getname(floppy_type)); !fddName.isEmpty()) { + floppyDevices.append(fddName); + } + } + if(key.contains(cdrom_match)) { + auto device_number = key.split("_").at(1); + auto cdrom_internal_name = QString(floppy_cdrom_config[key]); + auto cdrom_type = cdrom_get_from_internal_name(cdrom_internal_name.toUtf8().data()); + + auto cdrom_speed_key = QString("cdrom_%1_speed").arg(device_number); + auto cdrom_parameters_key = QString("cdrom_%1_parameters").arg(device_number); + auto cdrom_speed = QString(floppy_cdrom_config[cdrom_speed_key]); + auto cdrom_parameters = QString(floppy_cdrom_config[cdrom_parameters_key]); + auto cdrom_bus = cdrom_parameters.split(",").at(1).trimmed().toUpper(); + + if(auto cdromName = QString(cdrom_getname(cdrom_type)); !cdromName.isEmpty()) { + if(!cdrom_speed.isEmpty()) { + cdrom_speed = QString("%1x ").arg(cdrom_speed); + } + if(!cdrom_bus.isEmpty()) { + cdrom_bus = QString(" (%1)").arg(cdrom_bus); + } + cdromDevices.append(QString("%1%2%3").arg(cdrom_speed, cdromName, cdrom_bus)); + } + } + } + + display_table[Display::Name::Floppy] = floppyDevices.join(VMManagerDetailSection::sectionSeparator); + display_table[Display::Name::CD] = cdromDevices.join(VMManagerDetailSection::sectionSeparator); + + // SCSI controllers + QStringList scsiControllers; + static auto scsi_match = QRegularExpression("scsicard_\\d", QRegularExpression::CaseInsensitiveOption); + for(const auto& key: scsi_config.keys()) { + if(key.contains(scsi_match)) { + auto device_number = key.split("_").at(1); + auto scsi_internal_name = QString(scsi_config[key]); + auto scsi_id = scsi_card_get_from_internal_name(scsi_internal_name.toUtf8().data()); + auto scsi_device = scsi_card_getdevice(scsi_id); + auto scsi_name = QString(scsi_device->name); + if(!scsi_name.isEmpty()) { + scsiControllers.append(scsi_name); + } + } + } + display_table[Display::Name::SCSIController] = scsiControllers.join(VMManagerDetailSection::sectionSeparator); + + // Audio + int sound_int = sound_card_get_from_internal_name(audio_config["sndcard"].toUtf8().data()); + const device_t* audio_dev = sound_card_getdevice(sound_int); + display_table[Display::Name::Audio] = DeviceConfig::DeviceName(audio_dev, sound_card_get_internal_name(sound_int), 1); + + // MIDI + QString midiOutDev; + if(auto midi_out_device = QString(audio_config["midi_device"]); !midi_out_device.isEmpty()) { + auto midi_device_int = midi_out_device_get_from_internal_name(midi_out_device.toUtf8().data()); + auto midi_out = midi_out_device_getdevice(midi_device_int); + if(auto midiDevName = QString(midi_out->name); !midiDevName.isEmpty()) { + midiOutDev = midiDevName; + } + } + display_table[Display::Name::MidiOut] = midiOutDev; + + // midi_device = mt32 (output) + // mpu401_standalone = 1 + // midi_in_device (input) + + // Network + QString nicList; + static auto nic_match = QRegularExpression("net_\\d\\d_card", QRegularExpression::CaseInsensitiveOption); + for(const auto& key: network_config.keys()) { + if(key.contains(nic_match)) { + auto device_number = key.split("_").at(1); + auto nic_internal_name = QString(network_config[key]); + auto nic_id = network_card_get_from_internal_name(nic_internal_name.toUtf8().data()); + auto nic = network_card_getdevice(nic_id); + auto nic_name = QString(nic->name); + // Add separator for each subsequent value, skipping the first + if(!nicList.isEmpty()) { + nicList.append(QString("%1").arg(VMManagerDetailSection::sectionSeparator)); + } + auto net_type_key = QString("net_%1_net_type").arg(device_number); + auto net_type = network_config[net_type_key]; + if (!net_type.isEmpty()) { + nicList.append(nic_name + " (" + net_type + ")"); + } else { + nicList.append(nic_name); + } + + } + } + if(nicList.isEmpty()) { + nicList = "None"; + } + display_table[Display::Name::NIC] = nicList; + + // Input (Mouse) + auto mouse_internal_name = input_config["mouse_type"]; + auto mouse_dev = mouse_get_from_internal_name(mouse_internal_name.toUtf8().data()); + auto mouse_dev_name = mouse_get_name(mouse_dev); + display_table[Display::Name::Mouse] = mouse_dev_name; + + // Input (joystick) + QString joystickDevice; + if(auto joystick_internal = QString(input_config["joystick_type"]); !joystick_internal.isEmpty()) { + auto joystick_dev = joystick_get_from_internal_name(joystick_internal.toUtf8().data()); + if (auto joystickName = QString(joystick_get_name(joystick_dev)); !joystickName.isEmpty()) { + joystickDevice = joystickName; + } + } + display_table[Display::Name::Joystick] = joystickDevice; + + // # Ports + // Serial + // By default serial 1 and 2 are enabled unless otherwise specified + static auto serial_match = QRegularExpression("serial\\d_enabled", QRegularExpression::CaseInsensitiveOption); + QList serial_enabled = {true, true, false, false}; + // Parallel + // By default lpt 1 is enabled unless otherwise specified + static auto lpt_match = QRegularExpression("lpt\\d_enabled", QRegularExpression::CaseInsensitiveOption); + QList lpt_enabled = {true, false, false, false}; + for (const auto &key: ports_config.keys()) { + if (key.contains(serial_match)) { + if (auto serial_dev = key.split("_").at(0); !serial_dev.isEmpty()) { + auto serial_num = serial_dev.at(serial_dev.size() - 1); + // qDebug() << "serial is set" << key << ":" << ports_config[key]; + if(serial_num.isDigit() && serial_num.digitValue() >= 1 && serial_num.digitValue() <= 4) { + // Already verified that it is a digit with isDigit() + serial_enabled[serial_num.digitValue() - 1] = ports_config[key].toInt() == 1; + } + } + } + if (key.contains(lpt_match)) { + if (auto lpt_dev = key.split("_").at(0); !lpt_dev.isEmpty()) { + auto lpt_num = lpt_dev.at(lpt_dev.size() - 1); + // qDebug() << "lpt is set" << key << ":" << ports_config[key]; + if (lpt_num.isDigit() && lpt_num.digitValue() >= 1 && lpt_num.digitValue() <= 4) { + lpt_enabled[lpt_num.digitValue() - 1] = ports_config[key].toInt() == 1; + } + } + } + } + // qDebug() << "ports final" << serial_enabled << lpt_enabled; + QStringList serialFinal; + QStringList lptFinal; + int portIndex = 0; + for(const auto &serialNum: serial_enabled) { + if (serial_enabled[portIndex]) { + serialFinal.append(QString("COM%1").arg(portIndex + 1)); + } + ++portIndex; + } + portIndex = 0; + for (const auto &lptNum: lpt_enabled) { + if (lpt_enabled[portIndex]) { + lptFinal.append(QString("LPT%1").arg(portIndex + 1)); + } + ++portIndex; + } + display_table[Display::Name::Serial] = serialFinal.empty() ? tr("None") : serialFinal.join(", "); + display_table[Display::Name::Parallel] = lptFinal.empty() ? tr("None") : lptFinal.join(", "); + +} + +bool +VMManagerSystem::startServer() { + if (socket_server->startServer()) { + serverIsRunning = true; + connect(socket_server, &VMManagerServerSocket::dataReceived, this, &VMManagerSystem::dataReceived); + connect(socket_server, &VMManagerServerSocket::windowStatusChanged, this, &VMManagerSystem::windowStatusChangeReceived); + connect(socket_server, &VMManagerServerSocket::runningStatusChanged, this, &VMManagerSystem::runningStatusChangeReceived); + return true; + } else { + return false; + } +} + +void +VMManagerSystem::setProcessEnvVars() { + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + QString env_var_name = (socket_server_type == VMManagerServerSocket::ServerType::Standard) ? "VMM_86BOX_SOCKET" : "86BOX_MANAGER_SOCKET"; + env.insert(env_var_name, socket_server->getSocketPath()); + process->setProcessEnvironment(env); +} + +void +VMManagerSystem::restartButtonPressed() { + socket_server->serverSendMessage(VMManagerProtocol::ManagerMessage::ResetVM); + +} + +void +VMManagerSystem::pauseButtonPressed() { + socket_server->serverSendMessage(VMManagerProtocol::ManagerMessage::Pause); +} +void +VMManagerSystem::dataReceived() +{ + qInfo() << Q_FUNC_INFO << "Note: Respond to data received events here."; +} +void +VMManagerSystem::windowStatusChangeReceived(int status) +{ + window_obscured = status; + emit windowStatusChanged(); + processStatusChanged(); +} +QString +VMManagerSystem::getDisplayValue(Display::Name key) +{ + return (display_table.contains(key)) ? display_table[key] : ""; +} +void +VMManagerSystem::shutdownRequestButtonPressed() +{ + socket_server->serverSendMessage(VMManagerProtocol::ManagerMessage::RequestShutdown); +} +void +VMManagerSystem::shutdownForceButtonPressed() +{ + socket_server->serverSendMessage(VMManagerProtocol::ManagerMessage::ForceShutdown); +} +void +VMManagerSystem::processStatusChanged() +{ + // set to running if the process is running and the state is stopped + if (process->state() == QProcess::ProcessState::Running) { + if (process_status == VMManagerSystem::ProcessStatus::Stopped) { + process_status = VMManagerSystem::ProcessStatus::Running; + } + } else if (process->state() == QProcess::ProcessState::NotRunning) { + process_status = VMManagerSystem::ProcessStatus::Stopped; + } + emit itemDataChanged(); + emit clientProcessStatusChanged(); +} +void +VMManagerSystem::statusRefresh() +{ + processStatusChanged(); +} +QString +VMManagerSystem::processStatusToString(VMManagerSystem::ProcessStatus status) +{ +// QMetaEnum qme = QMetaEnum::fromType(); +// return qme.valueToKey(static_cast(status)); + switch (status) { + case VMManagerSystem::ProcessStatus::Stopped: + return tr("Powered Off"); + case VMManagerSystem::ProcessStatus::Running: + return tr("Running"); + case VMManagerSystem::ProcessStatus::Paused: + return tr("Paused"); + case VMManagerSystem::ProcessStatus::PausedWaiting: + case VMManagerSystem::ProcessStatus::RunningWaiting: + return tr("Paused (Waiting)"); + default: + return tr("Unknown Status"); + } +} + +QString +VMManagerSystem::getProcessStatusString() const +{ + return processStatusToString(process_status); +} + +VMManagerSystem::ProcessStatus +VMManagerSystem::getProcessStatus() const +{ + return process_status; +} +// Maps VMManagerProtocol::RunningState to VMManagerSystem::ProcessStatus +void +VMManagerSystem::runningStatusChangeReceived(VMManagerProtocol::RunningState state) +{ + if(state == VMManagerProtocol::RunningState::Running) { + process_status = VMManagerSystem::ProcessStatus::Running; + } else if(state == VMManagerProtocol::RunningState::Paused) { + process_status = VMManagerSystem::ProcessStatus::Paused; + } else if(state == VMManagerProtocol::RunningState::RunningWaiting) { + process_status = VMManagerSystem::ProcessStatus::RunningWaiting; + } else if(state == VMManagerProtocol::RunningState::PausedWaiting) { + process_status = VMManagerSystem::ProcessStatus::PausedWaiting; + } else { + process_status = VMManagerSystem::ProcessStatus::Unknown; + } + processStatusChanged(); +} +void +VMManagerSystem::reloadConfig() +{ + loadSettings(); + setupVars(); +} + +QDateTime +VMManagerSystem::timestamp() +{ + return lastUsedTimestamp; +} +void +VMManagerSystem::setIcon(const QString &newIcon) +{ + icon = newIcon; + saveSettings(); + emit itemDataChanged(); +} diff --git a/src/qt/qt_vmmanager_system.hpp b/src/qt/qt_vmmanager_system.hpp new file mode 100644 index 000000000..46fb2a0d9 --- /dev/null +++ b/src/qt/qt_vmmanager_system.hpp @@ -0,0 +1,188 @@ +/* +* 86Box A hypervisor and IBM PC system emulator that specializes in +* running old operating systems and software designed for IBM +* PC systems and compatibles from 1981 through fairly recent +* system designs based on the PCI bus. +* +* This file is part of the 86Box distribution. +* +* Header for 86Box VM manager system module +* +* +* +* Authors: cold-brewed +* +* Copyright 2024 cold-brewed +*/ + +#ifndef QT_VMMANAGER_SYSTEM_H +#define QT_VMMANAGER_SYSTEM_H + +#include +#include +#include +#include +#include +#include "qt_vmmanager_serversocket.hpp" +#include "qt_vmmanager_config.hpp" +#include "qt_deviceconfig.hpp" + +// This macro helps give us the required `qHash()` function in order to use the +// enum as a hash key +#define QHASH_FOR_CLASS_ENUM(T) \ +inline uint qHash(const T &t, uint seed) { \ + return ::qHash(static_cast::type>(t), seed); \ +} + +namespace Display { +Q_NAMESPACE +enum class Name { + Machine, + CPU, + Memory, + Video, + Disks, + Floppy, + CD, + SCSIController, + MidiOut, + Joystick, + Serial, + Parallel, + Audio, + Voodoo, + NIC, + Mouse, + Unknown +}; +Q_ENUM_NS(Name) +QHASH_FOR_CLASS_ENUM(Name) +} + +class VMManagerSystem : public QWidget { + Q_OBJECT + + typedef QHash display_table_t; + typedef QHash > config_hash_t; + +public: + + enum class ProcessStatus { + Stopped, + Running, + Paused, + PausedWaiting, + RunningWaiting, + Unknown, + }; + Q_ENUM(ProcessStatus); + + explicit VMManagerSystem(const QString &sysconfig_file); + // Default constructor will generate a temporary filename as the config file + // but it will not be valid (isValid() will return false) + VMManagerSystem() : VMManagerSystem(generateTemporaryFilename()) {} + + ~VMManagerSystem() override; + + static QVector scanForConfigs(const QString &searchPath = {}); + static QString generateTemporaryFilename(); + + QFileInfo config_file; + QString config_name; + QString config_dir; + QString shortened_dir; + QString uuid; + QString displayName; + QString notes; + QString icon; + QStringList searchTerms; + + config_hash_t config_hash; + + [[nodiscard]] QString getAll(const QString& category) const; + [[nodiscard]] QHash getCategory(const QString& category) const; + [[nodiscard]] QHash > getConfigHash() const; + + void setDisplayName(const QString& newDisplayName); + void setNotes(const QString& newNotes); + + [[nodiscard]] bool isValid() const; + [[nodiscard]] bool isProcessRunning() const; + [[nodiscard]] qint64 processId() const; + void launchMainProcess(); + void launchSettings(); + void startButtonPressed(); + void restartButtonPressed(); + void pauseButtonPressed(); + void shutdownRequestButtonPressed(); + void shutdownForceButtonPressed(); + void reloadConfig(); + QDateTime timestamp(); + void setIcon(const QString &newIcon); + + QProcess *process = new QProcess(); + + bool window_obscured; + + QString getDisplayValue(Display::Name key); + QFileInfoList getScreenshots(); + + inline bool operator==(const VMManagerSystem &rhs) const + { + return config_file.filePath() == rhs.config_file.filePath(); + } + + static QString + processStatusToString(VMManagerSystem::ProcessStatus status) ; + ProcessStatus process_status; + [[nodiscard]] QString getProcessStatusString() const; + [[nodiscard]] ProcessStatus getProcessStatus() const; + +signals: + void windowStatusChanged(); + void itemDataChanged(); + void clientProcessStatusChanged(); + +private: + void loadSettings(); + void saveSettings(); + void generateSearchTerms(); + void updateTimestamp(); + + display_table_t display_table; + + QFileInfo main_binary; + QString platform; + + // QDir application_temp_directory; + // QDir standard_temp_directory; + // QDir app_data_directory; + QDir screenshot_directory; + + QString unique_name; + QDateTime lastUsedTimestamp; + + VMManagerServerSocket *socket_server; + VMManagerServerSocket::ServerType socket_server_type; + + // Configuration file settings + VMManagerConfig *config_settings; + + bool serverIsRunning; + bool startServer(); + + bool has86BoxBinary(); + void find86BoxBinary(); + void setupPaths(); + void setupVars(); + void setProcessEnvVars(); + + void dataReceived(); + void windowStatusChangeReceived(int status); + void runningStatusChangeReceived(VMManagerProtocol::RunningState state); + void processStatusChanged(); + void statusRefresh(); +}; + + +#endif //QT_VMMANAGER_SYSTEM_H diff --git a/src/qt_resources.qrc b/src/qt_resources.qrc index a5963c152..51518b8f6 100644 --- a/src/qt_resources.qrc +++ b/src/qt_resources.qrc @@ -59,4 +59,95 @@ qt/texture_vert.spv qt/texture_frag.spv + + qt/assets/86Box-green.png + qt/assets/86box-rb.png + qt/assets/86box-red.png + qt/assets/86box-yellow.png + qt/assets/86box.png + qt/assets/86box-wizard.png + + + qt/assets/systemicons/cpq_deskpro.png + qt/assets/systemicons/cpq_port_386.png + qt/assets/systemicons/cpq_port_II.png + qt/assets/systemicons/cpq_port_III.png + qt/assets/systemicons/cpq_portable.png + qt/assets/systemicons/cpq_pres_2240.png + qt/assets/systemicons/cpq_pres_4500.png + qt/assets/systemicons/ibm330.png + qt/assets/systemicons/ibm_at.png + qt/assets/systemicons/ibm_pc_81.png + qt/assets/systemicons/ibm_pc_82.png + qt/assets/systemicons/ibm_pcjr.png + qt/assets/systemicons/ibm_ps2_m70.png + qt/assets/systemicons/ibm_ps2_m80.png + qt/assets/systemicons/ibm_psvp_486.png + qt/assets/systemicons/ibm_psvp_p60.png + qt/assets/systemicons/ibm_xt_82.png + qt/assets/systemicons/ibm_xt_86.png + qt/assets/systemicons/olivetti_m19.png + qt/assets/systemicons/olivetti_m21.png + qt/assets/systemicons/olivetti_m24.png + qt/assets/systemicons/olivetti_m24sp.png + qt/assets/systemicons/os_archlinux_x2.png + qt/assets/systemicons/os_cloud_x2.png + qt/assets/systemicons/os_debian_x2.png + qt/assets/systemicons/os_dos_x2.png + qt/assets/systemicons/os_fedora_x2.png + qt/assets/systemicons/os_freebsd_x2.png + qt/assets/systemicons/os_gentoo_x2.png + qt/assets/systemicons/os_jrockitve_x2.png + qt/assets/systemicons/os_l4_x2.png + qt/assets/systemicons/os_linux22_x2.png + qt/assets/systemicons/os_linux24_x2.png + qt/assets/systemicons/os_linux26_x2.png + qt/assets/systemicons/os_linux_x2.png + qt/assets/systemicons/os_macosx_x2.png + qt/assets/systemicons/os_mandriva_x2.png + qt/assets/systemicons/os_netbsd_x2.png + qt/assets/systemicons/os_netware_x2.png + qt/assets/systemicons/os_openbsd_x2.png + qt/assets/systemicons/os_opensuse_x2.png + qt/assets/systemicons/os_oracle_x2.png + qt/assets/systemicons/os_oraclesolaris_x2.png + qt/assets/systemicons/os_os2_other_x2.png + qt/assets/systemicons/os_os2ecs_x2.png + qt/assets/systemicons/os_os2warp3_x2.png + qt/assets/systemicons/os_os2warp45_x2.png + qt/assets/systemicons/os_os2warp4_x2.png + qt/assets/systemicons/os_other_x2.png + qt/assets/systemicons/os_qnx_x2.png + qt/assets/systemicons/os_redhat_x2.png + qt/assets/systemicons/os_solaris_x2.png + qt/assets/systemicons/os_turbolinux_x2.png + qt/assets/systemicons/os_ubuntu_x2.png + qt/assets/systemicons/os_win10_x2.png + qt/assets/systemicons/os_win2k3_x2.png + qt/assets/systemicons/os_win2k8_x2.png + qt/assets/systemicons/os_win2k_x2.png + qt/assets/systemicons/os_win31_x2.png + qt/assets/systemicons/os_win7_x2.png + qt/assets/systemicons/os_win81_x2.png + qt/assets/systemicons/os_win8_x2.png + qt/assets/systemicons/os_win95_x2.png + qt/assets/systemicons/os_win98_x2.png + qt/assets/systemicons/os_win_other_x2.png + qt/assets/systemicons/os_winme_x2.png + qt/assets/systemicons/os_winnt4_x2.png + qt/assets/systemicons/os_winvista_x2.png + qt/assets/systemicons/os_winxp_x2.png + qt/assets/systemicons/os_xandros_x2.png + qt/assets/systemicons/pb_bora_pro.png + qt/assets/systemicons/pb_pb410.png + qt/assets/systemicons/pb_pb640.png + qt/assets/systemicons/pb_pb680.png + qt/assets/systemicons/tandy_1000.png + qt/assets/systemicons/tandy_1000_hx.png + qt/assets/systemicons/tandy_1000_sl2.png + qt/assets/systemicons/toshiba_t1000.png + qt/assets/systemicons/toshiba_t1200.png + qt/assets/systemicons/toshiba_t1200_hdd.png + +